wing-ops/docs/_backup_20260301/MOCK-TO-API-GUIDE.md
htlee 6fbb3fc249 docs: 전체 프로젝트 문서 최신 기준 신규 작성
Phase 6(MapLibre+deck.gl), CSS 리팩토링, RBAC, 10탭 API 전환 등
현재 시스템 상태를 정확히 반영하여 모든 문서를 처음부터 재작성.

- README.md: 기술 스택(MapLibre+deck.gl), 빌드, 구조, 스킬 갱신
- CLAUDE.md: CSS @layer, RBAC, HTTP 정책, 백엔드 모듈 반영
- docs/README.md: 아키텍처 상세 (3-Layer, 인증, 권한, CSS)
- docs/DEVELOPMENT-GUIDE.md: 워크플로우 전체 흐름 + 실전 예시
- docs/INSTALL_GUIDE.md: 온라인/오프라인 설치 매뉴얼
- docs/COMMON-GUIDE.md: 공통 로직 9개 섹션 (인증~CSS)
- docs/MENU-TAB-GUIDE.md: 새 탭 추가 5단계 + 예시
- docs/CRUD-API-GUIDE.md: End-to-End CRUD API 패턴
- docs/MOCK-TO-API-GUIDE.md: Mock→API 전환 10단계 프로세스
- docs/_backup_20260301/: 기존 문서 백업

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:03:08 +09:00

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 설계 규칙) 참조.

  1. 기존 테이블 활용 가능 여부 확인 (예: ACDNT, LAYER 등)
  2. database/migration/NNN_{탭명}.sql 파일 작성 (번호는 기존 파일 다음 순번)
  3. 초기 데이터 INSERT (mock 데이터를 SQL로 변환)
  4. 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 서비스 + 컴포넌트 전환

  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() 사용 금지)
// 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/{탭명}-cruddevelop 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 생성

관련 문서