wing-ops/docs/MOCK-TO-API-GUIDE.md
htlee ffde4d6694 chore: 팀 워크플로우 v1.4.0 동기화 + 문서 갱신
- 에이전트 파일 YAML frontmatter 형식 갱신 (explorer, implementer, reviewer)
- subagent-policy.md 규칙 추가
- commit-msg hook 패턴 간소화
- COMMON-GUIDE.md API 연동 가이드 보강
- MOCK-TO-API-GUIDE.md mock→API 전환 가이드 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:36:35 +09:00

436 lines
14 KiB
Markdown

# Mock → API 전환 개발 지침
이 문서는 각 탭의 mock 데이터를 PostgreSQL DB + REST API 기반으로 전환할 때 따라야 할 표준 프로세스를 정의한다.
CRUD API 작성 규칙은 [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) 참조.
---
## 1. 전환 프로세스 (탭당 반복)
### Step A. 브랜치 생성
`feature/{탭명}-crud` 형식으로 develop에서 분기한다.
```bash
git checkout develop
git pull
git checkout -b feature/{탭명}-crud
```
### Step B. Mock 전수 조사 (필수!)
탭 디렉토리 전체에서 mock/하드코딩 데이터를 빠짐없이 검색한다.
**검색 키워드**: `mock`, `Mock`, `MOCK`, `sample`, `initial`, `hardcod`, `localStorage`, 인라인 배열 상수
```bash
grep -rn "mock\|Mock\|MOCK\|sample\|initial\|hardcod\|localStorage" frontend/src/tabs/{}/
```
**체크리스트 형식**으로 정리한다:
```
□ 파일명:라인 — 변수명 (N건) — 전환방법
□ components/AssetList.tsx:12 — MOCK_ASSETS (30건) — DB 이전
□ services/assetService.ts:5 — INITIAL_FILTER — 프론트 상수 유지
□ hooks/useAsset.ts:88 — localStorage.getItem('draft') — DB 이전
```
board 전환 시 mock 참조 누락으로 런타임 에러가 발생한 경험이 있다. 전수 조사를 건너뛰지 말 것.
### Step C. 프론트 상수 vs DB 데이터 판단
조사한 mock/하드코딩 데이터를 아래 기준으로 분류한다.
| 분류 | 유지/이전 | 예시 |
|------|-----------|------|
| UI 전용 색상 매핑 | 프론트 상수 유지 | 상태별 뱃지 색, 심각도 색상 |
| 레이아웃/뷰 설정 | 프론트 상수 유지 | 기본 페이지 크기, 컬럼 너비 |
| 비즈니스 목록 데이터 | DB 이전 | 자산 목록, 사고 목록, 보고서 |
| 검색/필터 대상 데이터 | DB 이전 | 카테고리, 기관명, 상태값 |
| 유형/카테고리 코드 | DB 이전 또는 CHECK 제약 | 자산유형, 오염물질유형 |
### Step D. DB 스키마 설계 + 마이그레이션
DDL 규칙은 [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) (4. DB 설계 규칙) 참조.
1. 기존 테이블 활용 가능 여부 확인 (예: ACDNT, LAYER 등)
2. `database/migration/NNN_{탭명}.sql` 파일 작성 (번호는 기존 파일 다음 순번)
3. 초기 데이터 INSERT (mock 데이터를 SQL로 변환)
4. psql로 원격 DB에 직접 실행
```bash
# 원격 wing DB에 마이그레이션 실행
PGPASSWORD=Wing2026 psql -h 211.208.115.83 -U wing -d wing \
-f database/migration/NNN_{탭명}.sql
# 실행 결과 검증 (마이그레이션 파일 끝의 SELECT 확인)
```
마이그레이션 파일 규칙:
- 모든 DDL에 `IF NOT EXISTS` / `IF EXISTS` 사용 (재실행 안전)
- 파일 끝에 검증 SELECT 포함
### Step E. 백엔드 Service + Router 구현
Service/Router 패턴은 [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) (5. Service 레이어 패턴, 6. Router 레이어 패턴) 참조.
**HTTP 메소드 규칙** (보안취약점 가이드 준수):
| 메소드 | 용도 |
|--------|------|
| GET | 단순 조회 (민감하지 않은 경우) |
| POST | 생성/수정/삭제 및 복잡한 조회 파라미터 |
PUT, DELETE, PATCH는 사용하지 않는다. 자세한 내용은 [2. HTTP 메소드 정책](#2-http-메소드-정책-필독) 참조.
**URL 패턴**:
| URL | 설명 |
|-----|------|
| `GET /api/{domain}` | 목록 (간단한 파라미터) |
| `GET /api/{domain}/:sn` | 상세 |
| `POST /api/{domain}/list` | 목록 (복잡한 필터 파라미터) |
| `POST /api/{domain}/detail` | 상세 |
| `POST /api/{domain}/create` | 생성 |
| `POST /api/{domain}/update` | 수정 |
| `POST /api/{domain}/delete` | 삭제 |
| `GET /api/{domain}/templates` | 메타데이터/코드 조회 |
**인증 패턴**:
```typescript
// 현재 로그인 사용자 UUID 추출
const userId = req.user!.sub // JWT payload의 사용자 UUID
// ❌ 절대 사용 금지 (reports 전환 시 실제 발생한 버그)
const user = (req as unknown as { user: { id: string } }).user
const userId = user.id // undefined → DB NOT NULL 제약 위반
```
구현 후 `backend/src/server.ts`에 라우터를 등록한다.
### Step F. 프론트엔드 API 서비스 + 컴포넌트 전환
1. `frontend/src/tabs/{탭}/services/{탭}Api.ts` 생성
2. API 응답 타입 (`interface Api{탭명}Item` 등) 정의
3. API ↔ 프론트 모델 변환 함수 작성 (필요 시)
4. 정적 마스터 데이터 캐싱: 모듈 변수 또는 TanStack Query `staleTime: Infinity`
5. 컴포넌트에서 mock import → API 호출로 교체
6. `api.post()` 사용 (`api.put()`, `api.delete()` 사용 금지)
```typescript
// frontend/src/tabs/{탭}/services/{탭}Api.ts
import { api } from '@common/services/api'
export interface Api{탭명}Item {
sn: number
// ...
}
export async function fetch{탭명}List(params: {
search?: string
page?: number
size?: number
}): Promise<{ items: Api{탭명}Item[]; totalCount: number }> {
const response = await api.post('/{ 탭명}/list', params)
return response.data
}
// 수정 — POST /update 사용
export async function update{탭명}(sn: number, data: Update{탭명}Input): Promise<void> {
await api.post('/{탭명}/update', { sn, ...data })
}
// 삭제 — POST /delete 사용
export async function delete{탭명}(sn: number): Promise<void> {
await api.post('/{탭명}/delete', { sn })
}
```
### Step G. 빌드 검증
```bash
# 백엔드 TypeScript 컴파일
cd backend && npm run build
# 프론트엔드 타입 체크 + ESLint
cd frontend && npx tsc --noEmit && npx eslint .
```
빌드/린트 에러가 0건이어야 다음 단계로 진행한다.
### Step H. 로컬 API 동작 테스트
```bash
# 백엔드 개발 서버 시작
cd backend && npm run dev
# 로그인 — 쿠키 파일 획득
curl -s -c /tmp/wing.cookie -X POST http://localhost:3001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"account":"admin","password":"admin1234"}' | jq .
# 목록 조회
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/list \
-H 'Content-Type: application/json' \
-d '{"page":1,"size":10}' | jq .
# 생성
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/create \
-H 'Content-Type: application/json' \
-d '{...}' | jq .
# 수정
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/update \
-H 'Content-Type: application/json' \
-d '{"sn": 1, ...}' | jq .
# 삭제
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/delete \
-H 'Content-Type: application/json' \
-d '{"sn": 1}' | jq .
```
CRUD 전체 흐름(생성 → 조회 → 수정 → 삭제 → 필터)을 확인하고 테스트 데이터를 정리한다.
### Step I. Mock 잔여 확인
```bash
grep -rn "mock\|Mock\|MOCK\|localStorage" frontend/src/tabs/{}/
# → UI 상수(색상, 레이아웃) 외 결과 0건이어야 함
```
잔여가 있으면 Step F로 돌아가 처리한다.
### Step J. 커밋 + 푸시 + MR
```bash
# 커밋 (Conventional Commits 형식, 한국어)
git add -p
git commit -m "feat({탭명}): mock 데이터 DB + REST API 전환"
# 푸시
git push -u origin feature/{탭명}-crud
```
`feature/{탭명}-crud``develop` MR을 Gitea에서 생성한다.
`/push` 또는 `/mr` 스킬 활용 가능.
---
## 2. HTTP 메소드 정책 (필독)
한국 보안취약점 점검 가이드에 따라 GET/POST만 사용한다.
### 허용
| 메소드 | 용도 | 예시 |
|--------|------|------|
| GET | 단순 조회 (파라미터가 단순하고 민감하지 않은 경우) | `GET /api/reports`, `GET /api/reports/:sn` |
| POST | 생성/수정/삭제, 복잡한 필터 파라미터 조회 | `POST /api/reports/create`, `POST /api/reports/list` |
### 금지
| 메소드 | 이유 |
|--------|------|
| PUT | 보안취약점 가이드 위반 |
| DELETE | 보안취약점 가이드 위반 |
| PATCH | 보안취약점 가이드 위반 |
### 기존 API 현황
`boardRouter`, `userRouter`, `roleRouter` 등은 아직 PUT/DELETE를 사용 중이다.
별도 세션에서 POST 패턴으로 마이그레이션 예정.
**신규 탭 전환 시 반드시 POST 패턴을 적용한다.**
---
## 3. 전환 시 주의사항 (실전 교훈)
### 3.1 req.user 접근 패턴
```typescript
// 올바른 패턴
const userId = req.user!.sub // JWT payload의 사용자 UUID
// 잘못된 패턴 (런타임 에러 발생)
const user = (req as unknown as { user: { id: string } }).user
const userId = user.id // undefined → DB NOT NULL 제약 위반
```
Reports 전환 시 실제 발생한 버그. `boardRouter.ts`의 패턴을 확인하고 `req.user!.sub`을 사용한다.
JWT 페이로드 전체 구조:
```typescript
interface JwtPayload {
sub: string // 사용자 UUID (USER_ID)
acnt: string // 계정명 (USER_ACNT)
name: string // 사용자명 (USER_NM)
roles: string[] // 역할 코드 목록
}
// 사용 예시
const userId = req.user!.sub // UUID
const userName = req.user!.name // 이름
```
### 3.2 AUTH_USER 테이블 컬럼명
```sql
-- 올바른 컬럼명
SELECT u.USER_NM as author_name FROM AUTH_USER u
-- 잘못된 컬럼명 (500 에러 발생)
SELECT u.NM as author_name FROM AUTH_USER u
```
Reports 전환 시 실제 발생한 버그. 반드시 `USER_NM`을 사용한다.
`AUTH_USER` 주요 컬럼 참조:
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `USER_ID` | UUID PK | 사용자 UUID (`req.user!.sub`과 일치) |
| `USER_ACNT` | VARCHAR | 계정명 (`req.user!.acnt`와 일치) |
| `USER_NM` | VARCHAR | 사용자명 (`req.user!.name`와 일치) |
| `EMAIL` | VARCHAR | 이메일 |
### 3.3 Mock 전수 조사 누락
Board 전환 시 일부 mock 참조를 놓쳐 런타임 에러가 발생했다.
[Step B](#step-b-mock-전수-조사-필수)의 전수 조사를 건너뛰지 말 것.
특히 다음 위치를 반드시 확인한다:
- 컴포넌트 파일 내 인라인 배열 (`const ITEMS = [{ id: 1, ... }]`)
- 커스텀 훅 초기값 (`useState([{ ... }])`)
- `localStorage.getItem` / `localStorage.setItem` 호출
- 서비스 파일 내 하드코딩 반환값
### 3.4 프론트 api.put() / api.delete() 금지
```typescript
// 올바른 POST 패턴
await api.post(`/reports/update`, { sn, ...input })
await api.post(`/reports/delete`, { sn })
// 금지 — PUT/DELETE 사용 불가
await api.put(`/reports/${sn}`, input)
await api.delete(`/reports/${sn}`)
```
### 3.5 트랜잭션 사용 시점
- 단일 테이블 INSERT/UPDATE: 트랜잭션 불필요
- 다중 테이블 동시 변경 (예: 헤더 + 섹션, 보고서 + 첨부파일): 반드시 트랜잭션 사용
```typescript
const client = await wingPool.connect()
try {
await client.query('BEGIN')
// 헤더 INSERT
const headerResult = await client.query(
'INSERT INTO REPORT_HDR (...) VALUES ($1, $2) RETURNING HDR_SN',
[...]
)
const hdrSn = headerResult.rows[0].hdr_sn
// 섹션 INSERT (헤더 FK 참조)
for (const section of sections) {
await client.query(
'INSERT INTO REPORT_SECT (HDR_SN, ...) VALUES ($1, ...)',
[hdrSn, ...]
)
}
await client.query('COMMIT')
return { hdrSn }
} catch (err) {
await client.query('ROLLBACK')
throw err
} finally {
client.release()
}
```
### 3.6 에러 처리 일관성
모든 라우트 핸들러에서 동일한 에러 처리 구조를 사용한다.
```typescript
try {
// 비즈니스 로직
} catch (err) {
if (err instanceof AuthError) {
res.status(err.status).json({ error: err.message })
return
}
console.error('[{탭명}] 작업 오류:', err)
res.status(500).json({ error: '처리 중 오류가 발생했습니다.' })
}
```
Board의 GET 목록 라우트에서 `AuthError` 분기 누락 이슈가 있었다.
목록 조회처럼 평범해 보이는 라우트도 예외 없이 동일한 구조를 사용한다.
### 3.7 정적 마스터 데이터 캐싱
코드 목록, 기관 목록 등 변경이 드문 마스터 데이터는 매 호출마다 DB 조회하지 않는다.
```typescript
// 방법 1: 모듈 변수 캐싱 (서버 재시작 시까지 유지)
let cachedOrgList: OrgItem[] | null = null
export async function getOrgList(): Promise<OrgItem[]> {
if (cachedOrgList) return cachedOrgList
const result = await wingPool.query('SELECT * FROM ORG WHERE USE_YN = $1', ['Y'])
cachedOrgList = result.rows.map(mapOrg)
return cachedOrgList
}
// 방법 2: TanStack Query staleTime 설정 (프론트엔드)
const { data: orgList } = useQuery({
queryKey: ['orgList'],
queryFn: fetchOrgList,
staleTime: 1000 * 60 * 10, // 10분간 리패치 없음
})
```
---
## 4. 탭별 전환 우선순위
| # | 탭 | 난이도 | 상태 | 비고 |
|---|---|--------|------|------|
| 1 | Reports (보고서) | ★★★ | 완료 | 7개 DB 테이블, 섹션 단위 JSONB |
| 2 | Assets (방제자산) | ★★☆ | 대기 | mock 1파일 집중, ORG 테이블 활용 |
| 3 | Incidents (사고관리) | ★★★ | 대기 | mock 5파일 분산, ACDNT 테이블 존재 |
| 4 | SCAT (해안조사) | ★★★★ | 대기 | 1,084개 세그먼트, 스키마 격차 |
| 5 | Rescue (구조시나리오) | ★★★★ | 대기 | DB 미정의, 시뮬레이션 복잡 |
| 6 | Prediction (확산예측) | ★★★★★ | 대기 | 시뮬레이션 엔진 의존, 부분 API 연동 |
제외: Weather (KHOA API 연동 완료), HNS (API 연동 완료), Board (API 연동 완료), Aerial (스켈레톤 수준)
---
## 5. 완료 검증 체크리스트 (탭당)
- [ ] 백엔드 빌드 통과 (`cd backend && npm run build`)
- [ ] 프론트 타입 체크 통과 (`cd frontend && npx tsc --noEmit`)
- [ ] 프론트 ESLint 통과 (`cd frontend && npx eslint .`)
- [ ] API CRUD 전체 테스트 (curl: 생성, 조회, 수정, 삭제, 필터)
- [ ] Mock/localStorage 잔여 0건 (UI 상수 제외)
- [ ] PUT/DELETE 사용 0건 (프론트/백엔드 모두)
- [ ] 커밋 + 푸시 + MR 생성
---
## 관련 문서
- [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) — CRUD API 표준 (DB 설계, Service/Router 패턴, 권한 모델)
- [COMMON-GUIDE.md](./COMMON-GUIDE.md) — 공통 로직 (인증, 감사 로그, 메뉴, API 통신)
- [MENU-TAB-GUIDE.md](./MENU-TAB-GUIDE.md) — 새 메뉴 탭 추가 절차