- 에이전트 파일 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>
14 KiB
Mock → API 전환 개발 지침
이 문서는 각 탭의 mock 데이터를 PostgreSQL DB + REST API 기반으로 전환할 때 따라야 할 표준 프로세스를 정의한다. CRUD API 작성 규칙은 CRUD-API-GUIDE.md 참조.
1. 전환 프로세스 (탭당 반복)
Step A. 브랜치 생성
feature/{탭명}-crud 형식으로 develop에서 분기한다.
git checkout develop
git pull
git checkout -b feature/{탭명}-crud
Step B. Mock 전수 조사 (필수!)
탭 디렉토리 전체에서 mock/하드코딩 데이터를 빠짐없이 검색한다.
검색 키워드: mock, Mock, MOCK, sample, initial, hardcod, localStorage, 인라인 배열 상수
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 (4. DB 설계 규칙) 참조.
- 기존 테이블 활용 가능 여부 확인 (예: ACDNT, LAYER 등)
database/migration/NNN_{탭명}.sql파일 작성 (번호는 기존 파일 다음 순번)- 초기 데이터 INSERT (mock 데이터를 SQL로 변환)
- psql로 원격 DB에 직접 실행
# 원격 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 (5. Service 레이어 패턴, 6. Router 레이어 패턴) 참조.
HTTP 메소드 규칙 (보안취약점 가이드 준수):
| 메소드 | 용도 |
|---|---|
| GET | 단순 조회 (민감하지 않은 경우) |
| POST | 생성/수정/삭제 및 복잡한 조회 파라미터 |
PUT, DELETE, PATCH는 사용하지 않는다. 자세한 내용은 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 |
메타데이터/코드 조회 |
인증 패턴:
// 현재 로그인 사용자 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 서비스 + 컴포넌트 전환
frontend/src/tabs/{탭}/services/{탭}Api.ts생성- API 응답 타입 (
interface Api{탭명}Item등) 정의 - API ↔ 프론트 모델 변환 함수 작성 (필요 시)
- 정적 마스터 데이터 캐싱: 모듈 변수 또는 TanStack Query
staleTime: Infinity - 컴포넌트에서 mock import → API 호출로 교체
api.post()사용 (api.put(),api.delete()사용 금지)
// 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. 빌드 검증
# 백엔드 TypeScript 컴파일
cd backend && npm run build
# 프론트엔드 타입 체크 + ESLint
cd frontend && npx tsc --noEmit && npx eslint .
빌드/린트 에러가 0건이어야 다음 단계로 진행한다.
Step H. 로컬 API 동작 테스트
# 백엔드 개발 서버 시작
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 잔여 확인
grep -rn "mock\|Mock\|MOCK\|localStorage" frontend/src/tabs/{탭}/
# → UI 상수(색상, 레이아웃) 외 결과 0건이어야 함
잔여가 있으면 Step F로 돌아가 처리한다.
Step J. 커밋 + 푸시 + MR
# 커밋 (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 접근 패턴
// 올바른 패턴
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 페이로드 전체 구조:
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 테이블 컬럼명
-- 올바른 컬럼명
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의 전수 조사를 건너뛰지 말 것.
특히 다음 위치를 반드시 확인한다:
- 컴포넌트 파일 내 인라인 배열 (
const ITEMS = [{ id: 1, ... }]) - 커스텀 훅 초기값 (
useState([{ ... }])) localStorage.getItem/localStorage.setItem호출- 서비스 파일 내 하드코딩 반환값
3.4 프론트 api.put() / api.delete() 금지
// 올바른 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: 트랜잭션 불필요
- 다중 테이블 동시 변경 (예: 헤더 + 섹션, 보고서 + 첨부파일): 반드시 트랜잭션 사용
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 에러 처리 일관성
모든 라우트 핸들러에서 동일한 에러 처리 구조를 사용한다.
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 조회하지 않는다.
// 방법 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 표준 (DB 설계, Service/Router 패턴, 권한 모델)
- COMMON-GUIDE.md — 공통 로직 (인증, 감사 로그, 메뉴, API 통신)
- MENU-TAB-GUIDE.md — 새 메뉴 탭 추가 절차