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>
This commit is contained in:
부모
4fb06ad43e
커밋
ffde4d6694
47
.claude/agents/explorer.md
Normal file
47
.claude/agents/explorer.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: explorer
|
||||||
|
description: 코드베이스 탐색 및 분석 에이전트. 3개 이상의 파일을 탐색하거나 프로젝트 구조를 파악할 때 사용한다.
|
||||||
|
model: sonnet
|
||||||
|
tools: Read, Glob, Grep
|
||||||
|
maxTurns: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
지정된 영역 내에서 코드베이스를 분석하고 구조화된 결과를 반환한다.
|
||||||
|
읽기 전용 — 파일을 수정하지 않는다.
|
||||||
|
|
||||||
|
## 자율 범위
|
||||||
|
|
||||||
|
- 메인 세션이 지정한 **탐색 영역**(디렉토리 또는 파일 목록) 내에서 자유롭게 탐색
|
||||||
|
- 영역 내 파일 간 의존성 추적, 임포트 체인 분석은 자율 수행
|
||||||
|
- 탐색 영역 밖 파일은 임포트/참조 관계 확인 목적으로만 열람 가능
|
||||||
|
|
||||||
|
## 입력 (메인 세션이 제공)
|
||||||
|
|
||||||
|
- **탐색 영역**: 디렉토리 경로 또는 파일 목록
|
||||||
|
- **목적**: 분석 목적이나 답변할 질문 (구체적일수록 좋음)
|
||||||
|
|
||||||
|
## 출력 형식
|
||||||
|
|
||||||
|
```
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 구조
|
||||||
|
- 핵심 파일/디렉토리 구성 (파일:라인 근거)
|
||||||
|
|
||||||
|
### 발견사항
|
||||||
|
- 목적에 대한 답변 (파일:라인 근거 포함)
|
||||||
|
|
||||||
|
### 패턴
|
||||||
|
- 코드 컨벤션, 반복 패턴, 아키텍처 특성
|
||||||
|
|
||||||
|
### 확신도
|
||||||
|
- 각 발견사항별: 확정 / 추정(근거) / 판단불가(필요 정보)
|
||||||
|
|
||||||
|
### 범위 외 참고
|
||||||
|
- 탐색 영역 밖에서 발견된 관련 사항 (해당 시)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 제약
|
||||||
|
|
||||||
|
- 파일 수정/생성 금지
|
||||||
|
- 정보 부족 시 추측하지 않고 "판단불가 — [필요한 정보]"로 표시
|
||||||
55
.claude/agents/implementer.md
Normal file
55
.claude/agents/implementer.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
name: implementer
|
||||||
|
description: 모듈 단위 코드 구현 에이전트. 독립 모듈 구현이나 병렬 작업이 필요할 때 사용한다.
|
||||||
|
model: sonnet
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash
|
||||||
|
maxTurns: 20
|
||||||
|
---
|
||||||
|
|
||||||
|
메인 세션이 정의한 계약(인터페이스, 타입, 제약)에 따라 코드를 구현한다.
|
||||||
|
내부 구현 방식은 자율 판단하되, 계약과 제약을 벗어나지 않는다.
|
||||||
|
|
||||||
|
## 자율 범위
|
||||||
|
|
||||||
|
- 계약(함수 시그니처, API 스펙, 타입)은 메인 세션이 확정 — 변경 불가
|
||||||
|
- 내부 구현 로직, 헬퍼 함수, 에러 처리 방식은 자율 판단
|
||||||
|
- **[참조]** 파일이 제공되면 해당 파일의 코드 패턴(네이밍, 구조, 에러 처리)을 따름
|
||||||
|
|
||||||
|
## 입력 (메인 세션이 제공)
|
||||||
|
|
||||||
|
- **[파일]**: 수정/생성할 파일 경로
|
||||||
|
- **[계약]**: 인터페이스, 타입, 함수 시그니처, API 스펙 등 외부 계약
|
||||||
|
- **[참조]**: 패턴을 따를 기존 파일 (선택, 제공 시 해당 패턴 준수)
|
||||||
|
- **[제약]**: 특별한 요구사항 (선택)
|
||||||
|
|
||||||
|
## 출력 형식
|
||||||
|
|
||||||
|
```
|
||||||
|
## 구현 결과
|
||||||
|
|
||||||
|
### 수정 파일
|
||||||
|
- 파일 경로 목록
|
||||||
|
|
||||||
|
### 파일별 변경
|
||||||
|
- 각 파일에서 추가/수정한 내용 요약
|
||||||
|
|
||||||
|
### 자체 검증
|
||||||
|
- tsc --noEmit: 통과 / 실패(에러 내용)
|
||||||
|
- [추가 검증 항목]: 결과
|
||||||
|
|
||||||
|
### 계약 외 판단
|
||||||
|
- 자율 판단한 구현 결정 사항 (메인 세션 참고용)
|
||||||
|
|
||||||
|
### 보고 사항 (해당 시)
|
||||||
|
- 계약 불충분: 추가 정보가 필요한 항목
|
||||||
|
- 아키텍처 영향: 범위 밖 변경이 필요한 사항
|
||||||
|
```
|
||||||
|
|
||||||
|
## 제약
|
||||||
|
|
||||||
|
- [파일]에 명시되지 않은 파일 수정 금지
|
||||||
|
- [계약]의 시그니처/타입 임의 변경 금지
|
||||||
|
- 아키텍처 변경이 필요하면 구현하지 않고 "보고 사항"에 기록
|
||||||
|
- 커밋/푸시 금지
|
||||||
|
- any 타입 금지, strict 모드 준수
|
||||||
|
- 구현 완료 후 tsc --noEmit 자체 검증 수행
|
||||||
53
.claude/agents/reviewer.md
Normal file
53
.claude/agents/reviewer.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: reviewer
|
||||||
|
description: 코드 리뷰 및 품질 검증 에이전트. 커밋 전 검증이나 MR 리뷰 시 사용한다.
|
||||||
|
model: sonnet
|
||||||
|
tools: Read, Glob, Grep, Bash
|
||||||
|
maxTurns: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
변경된 코드를 체크리스트 기반으로 검증한다.
|
||||||
|
읽기 전용 — 파일을 수정하지 않는다.
|
||||||
|
|
||||||
|
## 자율 범위
|
||||||
|
|
||||||
|
- 지정된 변경 파일을 자유롭게 분석
|
||||||
|
- 관련 파일(임포트 대상, 호출자)도 열람하여 영향 범위 확인 가능
|
||||||
|
- 기본 체크리스트 + 자체 판단으로 추가 이슈 탐지
|
||||||
|
|
||||||
|
## 입력 (메인 세션이 제공)
|
||||||
|
|
||||||
|
- **[대상]**: 리뷰할 파일 경로 목록 또는 git diff 범위
|
||||||
|
- **[체크리스트]**: 검증 항목 (선택, 미제공 시 기본 체크리스트 사용)
|
||||||
|
|
||||||
|
## 기본 체크리스트
|
||||||
|
|
||||||
|
1. 타입 안전성 — any, 타입 단언(as), non-null 단언(!) 사용
|
||||||
|
2. 에러 처리 — try-catch 누락, empty catch, 에러 무시
|
||||||
|
3. 보안 — 하드코딩 인증정보, injection 가능성, XSS
|
||||||
|
4. 미사용 코드 — 미사용 import, 변수, 함수
|
||||||
|
5. 팀 정책 — team-policy.md 위반 사항
|
||||||
|
6. 일관성 — 기존 코드 패턴과의 불일치
|
||||||
|
|
||||||
|
## 출력 형식
|
||||||
|
|
||||||
|
```
|
||||||
|
## 리뷰 결과
|
||||||
|
|
||||||
|
| # | 항목 | 판정 | 근거 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | [항목명] | PASS / FAIL | [파일:라인] 설명 |
|
||||||
|
|
||||||
|
### 추가 발견 (자체 판단)
|
||||||
|
- [파일:라인] 설명 (심각도: Critical / Warning / Info)
|
||||||
|
|
||||||
|
### 요약
|
||||||
|
- 전체: N개 PASS / M개 FAIL
|
||||||
|
- 커밋 가능 여부: 가능 / 차단 권고(사유)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 제약
|
||||||
|
|
||||||
|
- 파일 수정/생성 금지
|
||||||
|
- 각 항목에 반드시 PASS 또는 FAIL 판정 (애매하면 FAIL + 사유)
|
||||||
|
- 스타일 개선 제안은 "추가 발견"에 Info로만 기록
|
||||||
61
.claude/rules/subagent-policy.md
Normal file
61
.claude/rules/subagent-policy.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# 서브에이전트 활용 정책
|
||||||
|
|
||||||
|
커스텀 에이전트(`.claude/agents/`)를 활용하여 컨텍스트를 보호하고 병렬 작업을 수행한다.
|
||||||
|
메인 세션은 리더 역할(설계, 조율, 최종 판단)에 집중하고, 실제 작업은 서브에이전트에 위임한다.
|
||||||
|
|
||||||
|
## 에이전트 구성
|
||||||
|
|
||||||
|
| 에이전트 | 역할 | 자율성 | 모델 |
|
||||||
|
|----------|------|--------|------|
|
||||||
|
| explorer | 코드베이스 탐색/분석 (읽기 전용) | 높음 | sonnet |
|
||||||
|
| implementer | 모듈 단위 코드 구현 | 중간 | sonnet |
|
||||||
|
| reviewer | 코드 리뷰/품질 검증 (읽기 전용) | 높음 | sonnet |
|
||||||
|
|
||||||
|
## 사용 시점
|
||||||
|
|
||||||
|
### explorer
|
||||||
|
- 3개 이상의 파일/디렉토리를 탐색해야 할 때
|
||||||
|
- 프로젝트 구조나 패턴을 파악할 때
|
||||||
|
- 의존성 체인, 임포트 관계를 추적할 때
|
||||||
|
|
||||||
|
### implementer
|
||||||
|
- 독립 모듈/컴포넌트를 구현할 때
|
||||||
|
- 여러 모듈을 병렬로 구현할 때 (각각 별도 implementer)
|
||||||
|
- 반복 패턴을 여러 파일에 적용할 때
|
||||||
|
|
||||||
|
### reviewer
|
||||||
|
- 구현 완료 후 커밋 전 검증
|
||||||
|
- MR 생성 전 자체 리뷰
|
||||||
|
- 변경 범위가 클 때 (5개 이상 파일)
|
||||||
|
|
||||||
|
## 사용하지 않는 경우
|
||||||
|
|
||||||
|
- 단일 파일의 간단한 수정
|
||||||
|
- 위치를 이미 아는 코드 수정
|
||||||
|
- 설정 파일 변경
|
||||||
|
|
||||||
|
## 메인 세션 작업 흐름
|
||||||
|
|
||||||
|
### 단일 모듈
|
||||||
|
1. 메인: 계약(인터페이스, 타입) 설계
|
||||||
|
2. implementer: 계약 기반 구현 + 자체 검증
|
||||||
|
3. reviewer: 변경 파일 리뷰
|
||||||
|
4. 메인: 결과 확인 → 커밋
|
||||||
|
|
||||||
|
### 다중 모듈 (병렬)
|
||||||
|
1. 메인: 모듈 간 공유 인터페이스 확정
|
||||||
|
2. implementer A + B: 각 모듈 동시 구현
|
||||||
|
3. 메인: 통합 확인 (인터페이스 일치)
|
||||||
|
4. reviewer: 전체 변경 리뷰
|
||||||
|
5. 메인: 최종 확인 → 커밋
|
||||||
|
|
||||||
|
### 분석
|
||||||
|
1. explorer: 탐색 영역 + 목적 전달 → 분석 결과 반환
|
||||||
|
2. 메인: "추정" 항목만 직접 확인 → 판단
|
||||||
|
|
||||||
|
## 핵심 원칙
|
||||||
|
|
||||||
|
- **읽기 전용 에이전트(explorer/reviewer)**: 결과가 부정확해도 손해 없음 → 높은 자율성 부여
|
||||||
|
- **쓰기 에이전트(implementer)**: 계약은 고정, 내부 구현은 자율 → 중간 자율성
|
||||||
|
- **같은 파일을 두 에이전트가 동시에 수정하지 않는다**
|
||||||
|
- **커밋/푸시는 반드시 메인 세션에서 수행**
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"applied_global_version": "1.3.0",
|
"applied_global_version": "1.4.0",
|
||||||
"applied_date": "2026-02-27",
|
"applied_date": "2026-02-28",
|
||||||
"project_type": "react-ts",
|
"project_type": "react-ts",
|
||||||
"gitea_url": "https://gitea.gc-si.dev"
|
"gitea_url": "https://gitea.gc-si.dev"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,9 @@ fi
|
|||||||
# Conventional Commits 정규식
|
# Conventional Commits 정규식
|
||||||
# type(scope): subject
|
# type(scope): subject
|
||||||
# - type: feat|fix|docs|style|refactor|test|chore|ci|perf (필수)
|
# - type: feat|fix|docs|style|refactor|test|chore|ci|perf (필수)
|
||||||
# - scope: 괄호 제외 모든 문자 허용 — 한/영/숫자/특수문자 (선택)
|
# - scope: 영문, 숫자, 한글, 점, 밑줄, 하이픈 허용 (선택)
|
||||||
# - subject: 1자 이상 (길이는 바이트 기반 별도 검증)
|
# - subject: 1~72자, 한/영 혼용 허용 (필수)
|
||||||
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([^)]+\))?: .+$'
|
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([a-zA-Z0-9가-힣._-]+\))?: .{1,72}$'
|
||||||
MAX_SUBJECT_BYTES=200 # UTF-8 한글(3byte) 허용: 72문자 ≈ 최대 216byte
|
|
||||||
|
|
||||||
FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE")
|
FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE")
|
||||||
|
|
||||||
@ -59,13 +58,3 @@ if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then
|
|||||||
echo ""
|
echo ""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 길이 검증 (바이트 기반 — UTF-8 한글 허용)
|
|
||||||
MSG_LEN=$(echo -n "$FIRST_LINE" | wc -c | tr -d ' ')
|
|
||||||
if [ "$MSG_LEN" -gt "$MAX_SUBJECT_BYTES" ]; then
|
|
||||||
echo ""
|
|
||||||
echo " ✗ 커밋 메시지가 너무 깁니다 (${MSG_LEN}바이트, 최대 ${MAX_SUBJECT_BYTES})"
|
|
||||||
echo " 현재 메시지: $FIRST_LINE"
|
|
||||||
echo ""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
@ -416,6 +416,55 @@ const result = await authPool.query('SELECT * FROM AUTH_USER WHERE USER_ID = $1'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 8. Mock → API 전환 가이드
|
||||||
|
|
||||||
|
각 탭의 mock 데이터를 DB/API로 전환하는 프로세스는 **[MOCK-TO-API-GUIDE.md](./MOCK-TO-API-GUIDE.md)** 참조.
|
||||||
|
|
||||||
|
### 전환 완료 탭
|
||||||
|
|
||||||
|
| 탭 | MR | API 경로 | 비고 |
|
||||||
|
|----|-----|----------|------|
|
||||||
|
| Board (게시판) | MR#29 | `/api/board` | PUT/DELETE 사용 (레거시, POST 전환 예정) |
|
||||||
|
| Reports (보고서) | MR#31 | `/api/reports` | GET/POST only 적용 |
|
||||||
|
|
||||||
|
### Reports API 엔드포인트
|
||||||
|
|
||||||
|
| Method | Path | 설명 | 권한 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| GET | `/api/reports/templates` | 템플릿 목록 + 섹션 정의 | requireAuth |
|
||||||
|
| GET | `/api/reports/categories` | 분석 카테고리 목록 + 섹션 | requireAuth |
|
||||||
|
| GET | `/api/reports` | 보고서 목록 (필터: jrsdCd, tmplCd, sttsCd, search) | reports READ |
|
||||||
|
| GET | `/api/reports/:sn` | 보고서 상세 (섹션 데이터 포함) | reports READ |
|
||||||
|
| POST | `/api/reports` | 보고서 생성 | reports CREATE |
|
||||||
|
| POST | `/api/reports/:sn/update` | 보고서 수정 | reports UPDATE |
|
||||||
|
| POST | `/api/reports/:sn/delete` | 보고서 삭제 (논리) | reports DELETE |
|
||||||
|
| POST | `/api/reports/:sn/sections/:sectCd` | 개별 섹션 수정 | reports UPDATE |
|
||||||
|
|
||||||
|
### 프론트엔드 API 서비스
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend/src/tabs/reports/services/reportsApi.ts
|
||||||
|
import { api } from '@common/services/api'
|
||||||
|
|
||||||
|
// 조회 (GET)
|
||||||
|
const templates = await fetchTemplates() // GET /reports/templates (캐싱)
|
||||||
|
const categories = await fetchCategories() // GET /reports/categories (캐싱)
|
||||||
|
const list = await fetchReports({ tmplCd, sttsCd }) // GET /reports
|
||||||
|
const detail = await fetchReport(sn) // GET /reports/:sn
|
||||||
|
|
||||||
|
// 생성/수정/삭제 (POST)
|
||||||
|
await createReportApi({ tmplSn, title, sections }) // POST /reports
|
||||||
|
await updateReportApi(sn, { title, sections }) // POST /reports/:sn/update
|
||||||
|
await deleteReportApi(sn) // POST /reports/:sn/delete
|
||||||
|
|
||||||
|
// 고수준 함수 (OilSpillReportData ↔ API 변환 포함)
|
||||||
|
await saveReport(reportData) // create 또는 update 자동 분기
|
||||||
|
const reports = await loadReportsFromApi() // 전체 목록 + 변환
|
||||||
|
const detail = await loadReportDetail(sn) // 상세 + 섹션 복원
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 파일 구조 요약
|
## 파일 구조 요약
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -433,6 +482,8 @@ backend/src/
|
|||||||
├── auth/ 인증 (JWT, OAuth, 미들웨어, requirePermission)
|
├── auth/ 인증 (JWT, OAuth, 미들웨어, requirePermission)
|
||||||
├── users/ 사용자 관리
|
├── users/ 사용자 관리
|
||||||
├── roles/ 역할/권한 관리 (permResolver, roleService)
|
├── roles/ 역할/권한 관리 (permResolver, roleService)
|
||||||
|
├── board/ 게시판 CRUD (boardService, boardRouter)
|
||||||
|
├── reports/ 보고서 CRUD (reportsService, reportsRouter)
|
||||||
├── settings/ 시스템 설정
|
├── settings/ 시스템 설정
|
||||||
├── menus/ 메뉴 설정
|
├── menus/ 메뉴 설정
|
||||||
├── audit/ 감사 로그
|
├── audit/ 감사 로그
|
||||||
@ -445,5 +496,7 @@ database/
|
|||||||
├── init.sql 운영 DB DDL
|
├── init.sql 운영 DB DDL
|
||||||
└── migration/ 마이그레이션 스크립트
|
└── migration/ 마이그레이션 스크립트
|
||||||
├── 003_perm_tree.sql 리소스 트리 (AUTH_PERM_TREE)
|
├── 003_perm_tree.sql 리소스 트리 (AUTH_PERM_TREE)
|
||||||
└── 004_oper_cd.sql 오퍼레이션 코드 (OPER_CD) 추가
|
├── 004_oper_cd.sql 오퍼레이션 코드 (OPER_CD) 추가
|
||||||
|
├── 006_board.sql 게시판 (BOARD_POST)
|
||||||
|
└── 007_reports.sql 보고서 (REPORT_TMPL, REPORT, REPORT_SECT_DATA 등 7개)
|
||||||
```
|
```
|
||||||
|
|||||||
435
docs/MOCK-TO-API-GUIDE.md
Normal file
435
docs/MOCK-TO-API-GUIDE.md
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
# 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) — 새 메뉴 탭 추가 절차
|
||||||
불러오는 중...
Reference in New Issue
Block a user