# 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 { await api.post('/{탭명}/update', { sn, ...data }) } // 삭제 — POST /delete 사용 export async function delete{탭명}(sn: number): Promise { 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 { 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) — 새 메뉴 탭 추가 절차