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>
436 lines
14 KiB
Markdown
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) — 새 메뉴 탭 추가 절차
|