diff --git a/.gitignore b/.gitignore index a3652b2..9feec38 100755 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ backend/data/*.db-wal # Large reference data (keep locally, do not commit) _reference/ +docs/_backup_*/ /scat/ 참고용/ 논문/ diff --git a/docs/_backup_20260301/CHANGELOG.md b/docs/_backup_20260301/CHANGELOG.md deleted file mode 100644 index 0f92471..0000000 --- a/docs/_backup_20260301/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# 변경 이력 - -## [Unreleased] - -### 2026-03-01 - -## [2026-03-01] Phase 4 완료 — 나머지 6개 탭 Mock → API 전환 - -### Added -- SCAT: 구역/구간 조회 3 API + PostGIS (011_scat.sql) -- Board: 매뉴얼 CRUD + 첨부파일 API (012_board_ext.sql) -- HNS: 분석 CRUD 5 API (013_hns_analysis.sql) -- Prediction: 분석/역추적/오일펜스 7 API (014_prediction.sql) -- Aerial: 미디어/CCTV/위성 6 API + PostGIS (015_aerial.sql) -- Rescue: 구난 작전/시나리오 3 API + JSONB (016_rescue.sql) - -### Fixed -- Prediction 분석 상세 500 에러 (ACDNT_WEATHER 컬럼명 불일치) -- 시뮬레이션 API CORS 에러 (localhost 하드코딩 → api 인스턴스) - -### Changed -- 하드코딩 URL 환경변수 전환 (GeoServer, CORS, CSP 등) -- backtrackMockData.ts 삭제 - -### 2026-02-28 -- feat(reports): 보고서 탭 localStorage → DB/API 전환 (MR#31) - - DB 7개 테이블 (REPORT_TMPL, REPORT_TMPL_SECT, REPORT_ANALYSIS_CTGR, REPORT_CTGR_SECT, REPORT, REPORT_SECT_DATA 등) - - 백엔드 CRUD API (GET/POST only 패턴) - - 프론트 4개 컴포넌트 API 연동 (localStorage 제거) -- refactor(backend): SQLite → PostgreSQL 마이그레이션 + wing DB 연결 (MR#22) -- feat: Phase 5 View 분할 + RBAC 2차원 권한 + 게시판 CRUD API 연동 (MR#29) - - 대형 View 서브탭 분할 + FEATURE_ID 체계 도입 - - RBAC 오퍼레이션 기반 2차원 권한 시스템 (permResolver, AUTH_PERM OPER_CD) - - 게시판 CRUD API (boardService/Router) + 프론트 연동 -- refactor(frontend): 공통 모듈 common/ 분리 + 탭 단위 패키지 구조 전환 (MR#21) -- docs: MOCK-TO-API-GUIDE.md 작성 (Mock→API 전환 개발 지침) -- docs: CRUD-API-GUIDE.md 작성 (RBAC 기반 CRUD API 표준 가이드) -- chore: 팀 워크플로우 v1.4.0 동기화 (서브에이전트 3종 + 정책) -- policy: HTTP 메소드 제한 결정 (GET/POST only — 보안취약점 가이드 준수) - -### 2026-02-27 -- chore: 팀 워크플로우 v1.3.0 초기화 diff --git a/docs/_backup_20260301/COMMON-GUIDE.md b/docs/_backup_20260301/COMMON-GUIDE.md deleted file mode 100644 index b43ec70..0000000 --- a/docs/_backup_20260301/COMMON-GUIDE.md +++ /dev/null @@ -1,502 +0,0 @@ -# WING-OPS 공통 로직 개발 가이드 - -개별 탭 개발자가 공통 영역 구현을 참조하여 연동할 수 있도록 정리한 문서입니다. -공통 기능을 추가/변경할 때 반드시 이 문서를 최신화하세요. - ---- - -## 1. 인증/인가 - -### 개요 -JWT 기반 세션 인증. HttpOnly 쿠키(`WING_SESSION`)로 토큰을 관리하며, 프론트엔드에서는 Zustand `authStore`로 상태를 관리합니다. - -### 권한 모델: 리소스 × 오퍼레이션 (RBAC) - -**2차원 권한 모델**: 리소스 트리(상속) × 오퍼레이션(RCUD, 플랫) - -``` -AUTH_PERM 테이블: (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN) - -리소스 트리 (AUTH_PERM_TREE) 오퍼레이션 (플랫) -├── prediction READ = 조회/열람 -│ ├── prediction:analysis CREATE = 생성 -│ ├── prediction:list UPDATE = 수정 -│ └── prediction:theory DELETE = 삭제 -├── board -│ ├── board:notice -│ └── board:data -└── admin - ├── admin:users - └── admin:permissions -``` - -#### 오퍼레이션 코드 - -| OPER_CD | 설명 | 비고 | -|---------|------|------| -| `READ` | 조회/열람 | 목록, 상세 조회 | -| `CREATE` | 생성 | 새 데이터 등록 | -| `UPDATE` | 수정 | 기존 데이터 변경 | -| `DELETE` | 삭제 | 데이터 삭제 | -| `MANAGE` | 관리 | 관리자 설정 (확장용) | -| `EXPORT` | 내보내기 | 다운로드/출력 (확장용) | - -#### 상속 규칙 - -1. 부모 리소스의 **READ**가 N → 자식의 **모든 오퍼레이션** 강제 N (접근 자체 차단) -2. 해당 `(RSRC_CD, OPER_CD)` 명시적 레코드 있으면 → 그 값 사용 -3. 명시적 레코드 없으면 → 부모의 **같은 OPER_CD** 상속 -4. 최상위까지 없으면 → 기본 N (거부) - -``` -예시: board (READ:Y, CREATE:Y, UPDATE:Y, DELETE:N) -└── board:notice - ├── READ: 상속 Y (부모 READ Y) - ├── CREATE: 상속 Y (부모 CREATE Y) - ├── UPDATE: 명시적 N (override 가능) - └── DELETE: 상속 N (부모 DELETE N) -``` - -#### 키 구분자 -- 리소스 내부 경로: `:` (board:notice) -- 리소스-오퍼레이션 결합 (내부용): `::` (board:notice::READ) - -### 백엔드 - -#### 미들웨어 - -```typescript -import { requireAuth, requireRole, requirePermission } from '../auth/authMiddleware.js' - -// 인증만 필요한 라우트 -router.use(requireAuth) - -// 역할 기반 (관리 API용) -router.use(requireRole('ADMIN')) - -// 리소스×오퍼레이션 기반 (일반 비즈니스 API용) -router.post('/notice/list', requirePermission('board:notice', 'READ'), handler) -router.post('/notice/create', requirePermission('board:notice', 'CREATE'), handler) -router.post('/notice/update', requirePermission('board:notice', 'UPDATE'), handler) -router.post('/notice/delete', requirePermission('board:notice', 'DELETE'), handler) -``` - -`requirePermission`은 요청당 1회만 DB 조회하고 `req.resolvedPermissions`에 캐싱합니다. - -#### JWT 페이로드 (req.user) -`requireAuth` 통과 후 `req.user`에 담기는 정보: -```typescript -interface JwtPayload { - sub: string // 사용자 UUID (USER_ID) - acnt: string // 계정명 (USER_ACNT) - name: string // 사용자명 (USER_NM) - roles: string[] // 역할 코드 목록 (ADMIN, MANAGER, USER, VIEWER) -} -``` - -#### 라우터 패턴 (CRUD 구조) -```typescript -// backend/src/[모듈]/[모듈]Router.ts -import { Router } from 'express' -import { requireAuth, requirePermission } from '../auth/authMiddleware.js' - -const router = Router() -router.use(requireAuth) - -// 리소스별 CRUD 엔드포인트 -router.post('/list', requirePermission('module:sub', 'READ'), listHandler) -router.post('/detail', requirePermission('module:sub', 'READ'), detailHandler) -router.post('/create', requirePermission('module:sub', 'CREATE'), createHandler) -router.post('/update', requirePermission('module:sub', 'UPDATE'), updateHandler) -router.post('/delete', requirePermission('module:sub', 'DELETE'), deleteHandler) - -export default router -``` - -### 프론트엔드 - -#### authStore (Zustand) -```typescript -import { useAuthStore } from '@common/store/authStore' - -const { user, isAuthenticated, hasPermission, logout } = useAuthStore() - -// 사용자 정보 -user?.id // UUID -user?.name // 이름 -user?.roles // ['ADMIN', 'USER'] -user?.permissions // { 'prediction': ['READ','CREATE','UPDATE','DELETE'], ... } - -// 권한 확인 (리소스 × 오퍼레이션) -hasPermission('prediction') // READ 확인 (기본값) -hasPermission('prediction', 'READ') // 명시적 READ 확인 -hasPermission('board:notice', 'CREATE') // 공지사항 생성 권한 -hasPermission('board:notice', 'DELETE') // 공지사항 삭제 권한 - -// 하위 호환: operation 생략 시 'READ' 기본값 -hasPermission('admin') // === hasPermission('admin', 'READ') -``` - -#### API 클라이언트 -```typescript -import { api } from '@common/services/api' - -// withCredentials: true 설정으로 JWT 쿠키 자동 포함 -const response = await api.post('/your-endpoint/list', params) -const response = await api.post('/your-endpoint/create', data) - -// 401 응답 시 자동 로그아웃 처리 (인터셉터) -// 403 응답 시 권한 부족 (requirePermission 미들웨어) -``` - ---- - -## 2. 감사 로그 (Audit Log) - -### 개요 -사용자 행동을 추적하는 감사 로그 시스템. 현재 탭 이동 로그를 자동 기록하며, 향후 API 호출 로깅으로 확장 가능합니다. - -### 자동 기록 (탭 이동) -`App.tsx`의 `useEffect`에서 `activeMainTab` 변경을 감지하여 `navigator.sendBeacon`으로 자동 전송합니다. 개별 탭 개발자는 별도 작업이 필요 없습니다. - -```typescript -// frontend/src/App.tsx (자동 적용, 수정 불필요) -import { API_BASE_URL } from '@common/services/api' - -useEffect(() => { - if (!isAuthenticated) return - const blob = new Blob( - [JSON.stringify({ action: 'TAB_VIEW', detail: activeMainTab })], - { type: 'text/plain' } - ) - navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob) -}, [activeMainTab, isAuthenticated]) -``` - -### 수동 기록 (향후 확장) -특정 작업에 대해 명시적으로 감사 로그를 기록하려면: - -```typescript -import { API_BASE_URL } from '@common/services/api' - -const blob = new Blob( - [JSON.stringify({ action: 'ADMIN_ACTION', detail: '사용자 승인' })], - { type: 'text/plain' } -) -navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob) -``` - -### 감사 로그 테이블 구조 (AUTH_AUDIT_LOG) - -| 컬럼 | 타입 | 용도 | 현재 사용 | -|------|------|------|-----------| -| LOG_SN | SERIAL PK | 로그 순번 | O | -| USER_ID | UUID | 사용자 ID | O | -| ACTION_CD | VARCHAR(30) | 액션 코드 | O (TAB_VIEW) | -| ACTION_DTL | VARCHAR(100) | 액션 상세 (탭ID 등) | O | -| HTTP_METHOD | VARCHAR(10) | GET/POST/PUT/DELETE | - (향후) | -| CRUD_TYPE | VARCHAR(10) | SELECT/INSERT/UPDATE/DELETE | - (향후) | -| REQ_URL | VARCHAR(500) | 요청 URL | - (향후) | -| REQ_DTM | TIMESTAMPTZ | 요청 시각 | O | -| RES_DTM | TIMESTAMPTZ | 응답 완료 시각 | - (향후) | -| RES_STATUS | SMALLINT | HTTP 상태 코드 | - (향후) | -| RES_SIZE | INTEGER | 응답 데이터 크기(bytes) | - (향후) | -| IP_ADDR | VARCHAR(45) | 클라이언트 IP | O | -| USER_AGENT | VARCHAR(500) | 브라우저 정보 | O | -| EXTRA | JSONB | 추가 메타데이터 | - (향후) | - -### ACTION_CD 코드 체계 -| 코드 | 설명 | -|------|------| -| TAB_VIEW | 상단 탭 이동 | -| API_CALL | API 호출 (향후) | -| LOGIN | 로그인 (향후) | -| LOGOUT | 로그아웃 (향후) | -| ADMIN_ACTION | 관리자 작업 (향후) | - -### 관리자 조회 API -```typescript -// frontend/src/services/authApi.ts -import { fetchAuditLogs } from '../services/authApi' - -const result = await fetchAuditLogs({ - page: 1, - size: 50, - actionCd: 'TAB_VIEW', - from: '2026-02-28', - to: '2026-02-28', -}) -// result: { items: AuditLogItem[], total: number, page: number, size: number } -``` - ---- - -## 3. 메뉴 시스템 - -### 개요 -DB 기반 동적 메뉴 구성. 관리자가 메뉴 표시 여부/순서를 설정하면 모든 사용자에게 반영됩니다. -새 메뉴 탭 추가 시 `docs/MENU-TAB-GUIDE.md`를 참조하세요. - -### 메뉴 상태 (menuStore) -```typescript -// frontend/src/store/menuStore.ts -import { useMenuStore } from '../store/menuStore' - -const { menus, loadMenuConfig } = useMenuStore() - -// menus: MenuConfigItem[] — 활성화되고 정렬된 메뉴 목록 -// menus[0].id → 'prediction' -// menus[0].label → '유출유 확산예측' -// menus[0].enabled → true -``` - -### 메뉴 설정 저장소 -- DB: `AUTH_SETTING` 테이블의 `menu.config` 키 (JSON 배열) -- 백엔드: `backend/src/settings/settingsService.ts`의 `DEFAULT_MENU_CONFIG` -- API: `GET/PUT /api/menus` - ---- - -## 4. API 통신 패턴 - -### Axios 인스턴스 설정 -```typescript -// frontend/src/services/api.ts -export const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api', - withCredentials: true, // JWT 쿠키 자동 포함 - timeout: 30000, -}) -``` - -### 새 API 서비스 작성 패턴 -```typescript -// frontend/src/services/newService.ts -import { api } from './api' - -export interface MyData { - id: string - name: string -} - -export async function fetchMyData(): Promise { - const response = await api.get('/my-endpoint') - return response.data -} - -export async function createMyData(data: Omit): Promise { - const response = await api.post('/my-endpoint', data) - return response.data -} -``` - -### 에러 처리 -- 401 응답: `api.ts` 인터셉터가 자동으로 로그아웃 처리 -- 비즈니스 에러: `response.data.error` 메시지로 사용자에게 안내 -- 백엔드에서 `AuthError` 사용 시 적절한 HTTP 상태 코드와 메시지 반환 - ---- - -## 5. 상태 관리 - -### Zustand (클라이언트 상태) -```typescript -// frontend/src/store/newStore.ts -import { create } from 'zustand' - -interface MyState { - items: string[] - addItem: (item: string) => void -} - -export const useMyStore = create((set) => ({ - items: [], - addItem: (item) => set((state) => ({ items: [...state.items, item] })), -})) -``` - -### TanStack Query (서버 상태) — 권장 -```typescript -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { fetchMyData, createMyData } from '../services/newService' - -// 조회 -const { data, isLoading } = useQuery({ - queryKey: ['myData'], - queryFn: fetchMyData, -}) - -// 생성/수정 -const queryClient = useQueryClient() -const mutation = useMutation({ - mutationFn: createMyData, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['myData'] }), -}) -``` - ---- - -## 6. 백엔드 API CRUD 규칙 - -> 상세 가이드 + 게시판 실전 튜토리얼: **[CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md)** 참조 - -### HTTP Method 정책 (보안 가이드 준수) -- 보안 취약점 점검 가이드에 따라 **POST 메서드를 기본**으로 사용한다. -- GET은 단순 조회 중 민감하지 않은 경우에만 허용 (필요 시 POST로 전환). -- PUT, DELETE, PATCH 등 기타 메서드는 사용하지 않는다. - -### 오퍼레이션 기반 권한 미들웨어 -OPER_CD는 HTTP Method가 아닌 **비즈니스 의미**로 결정한다. -`requirePermission` 미들웨어에 명시적으로 오퍼레이션을 지정한다. - -| URL 패턴 | OPER_CD | 미들웨어 | -|----------|---------|----------| -| `/resource/list` | READ | `requirePermission(resource, 'READ')` | -| `/resource/detail` | READ | `requirePermission(resource, 'READ')` | -| `/resource/create` | CREATE | `requirePermission(resource, 'CREATE')` | -| `/resource/update` | UPDATE | `requirePermission(resource, 'UPDATE')` | -| `/resource/delete` | DELETE | `requirePermission(resource, 'DELETE')` | - -### 라우터 작성 예시 - -```typescript -// backend/src/board/noticeRouter.ts -import { Router } from 'express' -import { requireAuth, requirePermission } from '../auth/authMiddleware.js' - -const router = Router() -router.use(requireAuth) - -// 조회 -router.post('/list', requirePermission('board:notice', 'READ'), listHandler) -router.post('/detail', requirePermission('board:notice', 'READ'), detailHandler) - -// 생성/수정/삭제 -router.post('/create', requirePermission('board:notice', 'CREATE'), createHandler) -router.post('/update', requirePermission('board:notice', 'UPDATE'), updateHandler) -router.post('/delete', requirePermission('board:notice', 'DELETE'), deleteHandler) - -export default router -``` - -### 관리 API (예외) -사용자/역할/설정 등 관리 API는 `requireRole('ADMIN')` 유지: -```typescript -router.use(requireAuth) -router.use(requireRole('ADMIN')) -``` - ---- - -## 7. 백엔드 모듈 추가 절차 - -새 백엔드 모듈을 추가할 때: - -1. `backend/src/[모듈명]/` 디렉토리 생성 -2. `[모듈명]Service.ts` — 비즈니스 로직 (DB 쿼리) -3. `[모듈명]Router.ts` — Express 라우터 (CRUD 엔드포인트 + requirePermission) -4. `backend/src/server.ts`에 라우터 등록: - ```typescript - import newRouter from './[모듈명]/[모듈명]Router.js' - app.use('/api/[경로]', newRouter) - ``` -5. DB 테이블 필요 시 `database/auth_init.sql`에 DDL 추가 -6. 리소스 코드를 `AUTH_PERM_TREE`에 등록 (마이그레이션 SQL) - -### DB 접근 -```typescript -// PostgreSQL — wing DB (운영 데이터: 레이어, 사고, 예측 등) -import { wingPool } from '../db/wingDb.js' -const result = await wingPool.query('SELECT * FROM LAYER WHERE LAYER_CD = $1', [id]) - -// PostgreSQL — wing_auth DB (인증 데이터: 사용자, 역할, 권한 등) -import { authPool } from '../db/authDb.js' -const result = await authPool.query('SELECT * FROM AUTH_USER WHERE USER_ID = $1', [id]) -``` - ---- - -## 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) // 상세 + 섹션 복원 -``` - ---- - -## 파일 구조 요약 - -``` -frontend/src/ -├── common/ -│ ├── services/api.ts Axios 인스턴스 + API_BASE_URL + 인터셉터 -│ ├── services/authApi.ts 인증/사용자/역할/설정/메뉴/감사로그 API -│ ├── store/authStore.ts 인증 상태 + hasPermission (Zustand) -│ ├── store/menuStore.ts 메뉴 상태 (Zustand) -│ └── hooks/ useSubMenu, useFeatureTracking 등 -├── tabs/ 탭별 패키지 (11개) -└── App.tsx 탭 라우팅 + 감사 로그 자동 기록 - -backend/src/ -├── auth/ 인증 (JWT, OAuth, 미들웨어, requirePermission) -├── users/ 사용자 관리 -├── roles/ 역할/권한 관리 (permResolver, roleService) -├── board/ 게시판 CRUD (boardService, boardRouter) -├── reports/ 보고서 CRUD (reportsService, reportsRouter) -├── settings/ 시스템 설정 -├── menus/ 메뉴 설정 -├── audit/ 감사 로그 -├── db/ DB 연결 (authDb, wingDb) -├── middleware/ 보안 미들웨어 -└── server.ts Express 진입점 + 라우터 등록 - -database/ -├── auth_init.sql 인증 DB DDL + 초기 데이터 -├── init.sql 운영 DB DDL -└── migration/ 마이그레이션 스크립트 - ├── 003_perm_tree.sql 리소스 트리 (AUTH_PERM_TREE) - ├── 004_oper_cd.sql 오퍼레이션 코드 (OPER_CD) 추가 - ├── 006_board.sql 게시판 (BOARD_POST) - └── 007_reports.sql 보고서 (REPORT_TMPL, REPORT, REPORT_SECT_DATA 등 7개) -``` diff --git a/docs/_backup_20260301/CRUD-API-GUIDE.md b/docs/_backup_20260301/CRUD-API-GUIDE.md deleted file mode 100644 index 8d91db0..0000000 --- a/docs/_backup_20260301/CRUD-API-GUIDE.md +++ /dev/null @@ -1,1436 +0,0 @@ -# RBAC 기반 CRUD API 개발 가이드 - -새 CRUD API를 추가할 때 따라야 할 표준 가이드. -Phase 5 RBAC 체계(리소스 x 오퍼레이션 2차원 모델)를 기반으로 한다. - -**DB 구조**: wing DB 단일 DB, 스키마 분리 -- `wing` 스키마: 운영 데이터 (BOARD_POST, LAYER 등) -- `auth` 스키마: 인증/인가 데이터 (AUTH_USER, AUTH_ROLE, AUTH_PERM 등) -- `public` 스키마: PostGIS 시스템 테이블만 유지 (사용 금지) - ---- - -## Part 1: 범용 가이드 - -### 1. 개요 - -이 문서는 WING-OPS의 **모든 탭 개발자**가 새 CRUD API를 만들 때 참조하는 표준이다. - -- 백엔드: Express Router + Service 2-Layer -- 권한: `requirePermission(resource, operation)` 미들웨어 -- DB: PostgreSQL (`wingPool` 단일 Pool, `search_path = wing, auth, public`) -- 프론트: Axios + `hasPermission()` 조건부 렌더링 - -각 섹션에 복사해서 바로 사용할 수 있는 실제 코드 스니펫을 포함한다. - ---- - -### 2. 아키텍처 - -#### 3-Layer 구조 - -``` -클라이언트 (React) - ↓ Axios (withCredentials: true, JWT 쿠키 자동 포함) -Router (Express) ← requireAuth → requirePermission - ↓ -Service ← 비즈니스 로직, DB 쿼리 - ↓ -DB (pg Pool) ← wingPool (search_path = wing, auth) -``` - -#### 디렉토리 구조 - -``` -backend/src/{domain}/ -├── {domain}Router.ts ← Express 라우터 (엔드포인트 + 미들웨어) -└── {domain}Service.ts ← 비즈니스 로직 (쿼리, 인터페이스) -``` - -#### DB Pool - -```typescript -// backend/src/db/wingDb.ts -import { wingPool } from '../db/wingDb.js' - -// wingPool은 연결 시 search_path = wing, auth, public 자동 설정 -// → 스키마 접두사 없이 wing.BOARD_POST, auth.AUTH_USER 모두 접근 가능 -``` - -> **주의**: `authPool`은 하위 호환용 re-export이다. 신규 코드는 반드시 `wingPool`을 직접 import할 것. - -```typescript -// backend/src/db/authDb.ts (하위 호환 — 신규 코드에서 사용 금지) -import { wingPool } from './wingDb.js' -export const authPool = wingPool // 같은 Pool -``` - ---- - -### 3. 권한 모델 빠른 요약 - -#### 2차원 모델: 리소스 트리 x 오퍼레이션 - -``` -AUTH_PERM 테이블: (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN) - -리소스 트리 (AUTH_PERM_TREE) 오퍼레이션 (플랫) -├── board READ = 조회/열람 -│ ├── board:notice CREATE = 생성 -│ ├── board:data UPDATE = 수정 -│ └── board:qna DELETE = 삭제 -├── prediction -│ ├── prediction:analysis -│ └── prediction:list -└── admin - ├── admin:users - └── admin:permissions -``` - -#### 리소스 코드 - -`AUTH_PERM_TREE` 테이블에 등록된 코드. 콜론(`:`)으로 계층 구분. - -| 형식 | 예시 | 설명 | -|------|------|------| -| `{탭}` | `board` | 메인 탭 (level 0) | -| `{탭}:{서브}` | `board:notice` | 서브 리소스 (level 1) | - -#### 오퍼레이션 - -| OPER_CD | 설명 | 용도 | -|---------|------|------| -| `READ` | 조회/열람 | 목록, 상세 조회 | -| `CREATE` | 생성 | 새 데이터 등록 | -| `UPDATE` | 수정 | 기존 데이터 변경 | -| `DELETE` | 삭제 | 데이터 삭제 | - -#### 백엔드: requirePermission - -```typescript -import { requireAuth, requirePermission } from '../auth/authMiddleware.js' - -// requirePermission(리소스코드, 오퍼레이션코드) -// 오퍼레이션 생략 시 기본값 'READ' -router.post('/list', requirePermission('board:notice', 'READ'), handler) -router.post('/create', requirePermission('board:notice', 'CREATE'), handler) -``` - -`requirePermission`은 **요청당 1회**만 DB를 조회하고 `req.resolvedPermissions`에 캐싱한다. 한 요청에서 여러 번 호출해도 성능 문제 없다. - -#### 프론트엔드: hasPermission - -```typescript -import { useAuthStore } from '@common/store/authStore' - -const { hasPermission } = useAuthStore() - -hasPermission('board:notice') // READ 확인 (기본값) -hasPermission('board:notice', 'CREATE') // 생성 권한 확인 -hasPermission('board:notice', 'UPDATE') // 수정 권한 확인 -hasPermission('board:notice', 'DELETE') // 삭제 권한 확인 -``` - -#### 상속 규칙 - -``` -규칙 1: 부모 READ=N → 자식의 모든 오퍼레이션 강제 N -규칙 2: 명시적 레코드 있으면 → 그 값 사용 -규칙 3: 명시적 레코드 없으면 → 부모의 같은 오퍼레이션 상속 -규칙 4: 최상위까지 없으면 → 기본 N (거부) -``` - ---- - -### 4. DB 설계 규칙 - -#### 스키마 선택 - -| 데이터 성격 | 스키마 | 예시 | -|-------------|--------|------| -| 운영 데이터 | `wing` | BOARD_POST, LAYER, HNS_SUBSTANCE | -| 인증/인가 | `auth` | AUTH_USER, AUTH_ROLE, AUTH_PERM | - -> `search_path = wing, auth, public` 설정으로 스키마 접두사 없이 접근 가능. -> 단, 다른 스키마 테이블을 FK로 참조할 때는 `auth.AUTH_USER(USER_ID)` 처럼 명시한다. - -#### 네이밍 규칙 - -| 항목 | 규칙 | 예시 | -|------|------|------| -| 테이블명 | UPPER_SNAKE_CASE | `BOARD_POST`, `HNS_SUBSTANCE` | -| 컬럼명 | UPPER_SNAKE_CASE | `POST_SN`, `CATEGORY_CD`, `REG_DTM` | -| PK | `{접두어}_SN` (SERIAL) 또는 `{접두어}_ID` (UUID) | `POST_SN`, `USER_ID` | -| FK 컬럼 | 참조 테이블의 PK 컬럼명 그대로 사용 | `AUTHOR_ID` (→ AUTH_USER.USER_ID) | -| 코드성 컬럼 | `{의미}_CD` | `CATEGORY_CD`, `OPER_CD` | -| 여부 컬럼 | `{의미}_YN` (CHAR(1), 'Y'/'N') | `USE_YN`, `PINNED_YN` | -| 일시 컬럼 | `{의미}_DTM` (TIMESTAMPTZ) | `REG_DTM`, `MDFCN_DTM` | - -#### 공통 컬럼 패턴 - -모든 운영 테이블에 포함하는 표준 컬럼: - -```sql -USE_YN CHAR(1) NOT NULL DEFAULT 'Y', -- 논리삭제 (Y=활성, N=삭제) -REG_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 등록일시 -MDFCN_DTM TIMESTAMPTZ, -- 수정일시 -``` - -#### DDL 작성 예시 - -```sql --- database/migration/NNN_description.sql - -CREATE TABLE IF NOT EXISTS BOARD_POST ( - POST_SN SERIAL PRIMARY KEY, - CATEGORY_CD VARCHAR(20) NOT NULL, - TITLE VARCHAR(200) NOT NULL, - CONTENT TEXT, - AUTHOR_ID UUID NOT NULL, - VIEW_CNT INTEGER NOT NULL DEFAULT 0, - PINNED_YN CHAR(1) NOT NULL DEFAULT 'N', - USE_YN CHAR(1) NOT NULL DEFAULT 'Y', - REG_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(), - MDFCN_DTM TIMESTAMPTZ, - - -- FK: 다른 스키마 참조 시 스키마 명시 - CONSTRAINT FK_BOARD_AUTHOR FOREIGN KEY (AUTHOR_ID) - REFERENCES auth.AUTH_USER(USER_ID), - - -- CHECK: 코드성 컬럼에 허용값 명시 - CONSTRAINT CK_BOARD_CATEGORY - CHECK (CATEGORY_CD IN ('NOTICE','DATA','QNA','MANUAL')), - CONSTRAINT CK_BOARD_PINNED CHECK (PINNED_YN IN ('Y','N')), - CONSTRAINT CK_BOARD_USE CHECK (USE_YN IN ('Y','N')) -); - --- COMMENT: 테이블/컬럼 설명 -COMMENT ON TABLE BOARD_POST IS '게시판 게시글'; -COMMENT ON COLUMN BOARD_POST.CATEGORY_CD IS '카테고리: NOTICE=공지, DATA=자료실, QNA=Q&A, MANUAL=해경매뉴얼'; - --- INDEX: 검색/필터 대상, FK 컬럼 -CREATE INDEX IF NOT EXISTS IDX_BOARD_CATEGORY ON BOARD_POST(CATEGORY_CD); -CREATE INDEX IF NOT EXISTS IDX_BOARD_AUTHOR ON BOARD_POST(AUTHOR_ID); -CREATE INDEX IF NOT EXISTS IDX_BOARD_REG_DTM ON BOARD_POST(REG_DTM DESC); -``` - -#### 마이그레이션 파일 규칙 - -- 경로: `database/migration/NNN_description.sql` -- 번호: 기존 파일 다음 번호 (001, 003, 004, 005, 006, ...) -- 모든 DDL에 `IF NOT EXISTS` / `IF EXISTS` 사용 (재실행 안전) -- 파일 끝에 검증 SELECT 포함 - ---- - -### 5. Service 레이어 패턴 - -#### 인터페이스 정의 - -Service 파일 상단에 반환 타입과 입력 타입을 정의한다. - -```typescript -// backend/src/{domain}/{domain}Service.ts - -import { wingPool } from '../db/wingDb.js' -import { AuthError } from '../auth/authService.js' - -// 목록/상세 조회 반환 타입 -interface PostItem { - postSn: number - categoryCd: string - title: string - content: string | null - authorId: string - authorName: string - viewCnt: number - pinnedYn: string - useYn: string - regDtm: string - mdfcnDtm: string | null -} - -// 생성 입력 타입 -interface CreatePostInput { - categoryCd: string - title: string - content?: string - authorId: string - pinnedYn?: string -} - -// 수정 입력 타입 (모든 필드 optional — 부분 업데이트) -interface UpdatePostInput { - title?: string - content?: string - categoryCd?: string - pinnedYn?: string -} - -// 페이징 응답 타입 -interface PagedResult { - items: T[] - totalCount: number - page: number - size: number -} -``` - -#### wingPool 사용 - -```typescript -import { wingPool } from '../db/wingDb.js' - -// 단순 조회 -const result = await wingPool.query( - 'SELECT * FROM BOARD_POST WHERE POST_SN = $1 AND USE_YN = $2', - [postSn, 'Y'] -) - -// Parameterized Query — 반드시 $1, $2, ... 사용 (SQL Injection 방지) -// 문자열 결합으로 쿼리를 만들지 않는다 -``` - -#### 동적 WHERE 빌드 패턴 (필터, 검색) - -```typescript -export async function listPosts( - categoryCd?: string, - search?: string, - page: number = 1, - size: number = 20, -): Promise> { - // 동적 WHERE 조건 - const conditions: string[] = ["p.USE_YN = 'Y'"] - const params: (string | number)[] = [] - let paramIdx = 1 - - if (categoryCd) { - conditions.push(`p.CATEGORY_CD = $${paramIdx++}`) - params.push(categoryCd) - } - - if (search) { - conditions.push(`(p.TITLE ILIKE $${paramIdx} OR p.CONTENT ILIKE $${paramIdx})`) - params.push(`%${search}%`) - paramIdx++ - } - - const whereClause = conditions.join(' AND ') - - // totalCount 조회 - const countResult = await wingPool.query( - `SELECT COUNT(*) as cnt FROM BOARD_POST p WHERE ${whereClause}`, - params - ) - const totalCount = parseInt(countResult.rows[0].cnt, 10) - - // 페이징 데이터 조회 - const offset = (page - 1) * size - const dataParams = [...params, size, offset] - - const dataResult = await wingPool.query( - `SELECT p.POST_SN as post_sn, p.CATEGORY_CD as category_cd, - p.TITLE as title, p.CONTENT as content, - p.AUTHOR_ID as author_id, u.USER_NM as author_name, - p.VIEW_CNT as view_cnt, p.PINNED_YN as pinned_yn, - p.USE_YN as use_yn, p.REG_DTM as reg_dtm, p.MDFCN_DTM as mdfcn_dtm - FROM BOARD_POST p - LEFT JOIN AUTH_USER u ON p.AUTHOR_ID = u.USER_ID - WHERE ${whereClause} - ORDER BY p.PINNED_YN DESC, p.REG_DTM DESC - LIMIT $${paramIdx++} OFFSET $${paramIdx++}`, - dataParams - ) - - const items: PostItem[] = dataResult.rows.map((row) => ({ - postSn: row.post_sn, - categoryCd: row.category_cd, - title: row.title, - content: row.content, - authorId: row.author_id, - authorName: row.author_name, - viewCnt: row.view_cnt, - pinnedYn: row.pinned_yn, - useYn: row.use_yn, - regDtm: row.reg_dtm, - mdfcnDtm: row.mdfcn_dtm, - })) - - return { items, totalCount, page, size } -} -``` - -#### 상세 조회 - -```typescript -export async function getPost(postSn: number): Promise { - const result = await wingPool.query( - `SELECT p.POST_SN as post_sn, p.CATEGORY_CD as category_cd, - p.TITLE as title, p.CONTENT as content, - p.AUTHOR_ID as author_id, u.USER_NM as author_name, - p.VIEW_CNT as view_cnt, p.PINNED_YN as pinned_yn, - p.USE_YN as use_yn, p.REG_DTM as reg_dtm, p.MDFCN_DTM as mdfcn_dtm - FROM BOARD_POST p - LEFT JOIN AUTH_USER u ON p.AUTHOR_ID = u.USER_ID - WHERE p.POST_SN = $1 AND p.USE_YN = 'Y'`, - [postSn] - ) - - if (result.rows.length === 0) { - throw new AuthError('게시글을 찾을 수 없습니다.', 404) - } - - const row = result.rows[0] - return { - postSn: row.post_sn, - categoryCd: row.category_cd, - title: row.title, - content: row.content, - authorId: row.author_id, - authorName: row.author_name, - viewCnt: row.view_cnt, - pinnedYn: row.pinned_yn, - useYn: row.use_yn, - regDtm: row.reg_dtm, - mdfcnDtm: row.mdfcn_dtm, - } -} -``` - -#### 생성 - -```typescript -export async function createPost(input: CreatePostInput): Promise<{ postSn: number }> { - const result = await wingPool.query( - `INSERT INTO BOARD_POST (CATEGORY_CD, TITLE, CONTENT, AUTHOR_ID, PINNED_YN) - VALUES ($1, $2, $3, $4, $5) - RETURNING POST_SN as post_sn`, - [input.categoryCd, input.title, input.content || null, input.authorId, input.pinnedYn || 'N'] - ) - - return { postSn: result.rows[0].post_sn } -} -``` - -#### 동적 SET 빌드 패턴 (부분 업데이트) - -```typescript -export async function updatePost( - postSn: number, - input: UpdatePostInput, - requesterId: string, -): Promise { - // 소유자 검증 - const existing = await wingPool.query( - "SELECT AUTHOR_ID FROM BOARD_POST WHERE POST_SN = $1 AND USE_YN = 'Y'", - [postSn] - ) - if (existing.rows.length === 0) { - throw new AuthError('게시글을 찾을 수 없습니다.', 404) - } - if (existing.rows[0].author_id !== requesterId) { - throw new AuthError('본인의 게시글만 수정할 수 있습니다.', 403) - } - - // 동적 SET 빌드 - const sets: string[] = [] - const params: (string | number | null)[] = [] - let idx = 1 - - if (input.title !== undefined) { - sets.push(`TITLE = $${idx++}`) - params.push(input.title) - } - if (input.content !== undefined) { - sets.push(`CONTENT = $${idx++}`) - params.push(input.content) - } - if (input.categoryCd !== undefined) { - sets.push(`CATEGORY_CD = $${idx++}`) - params.push(input.categoryCd) - } - if (input.pinnedYn !== undefined) { - sets.push(`PINNED_YN = $${idx++}`) - params.push(input.pinnedYn) - } - - if (sets.length === 0) { - throw new AuthError('수정할 항목이 없습니다.', 400) - } - - // MDFCN_DTM 자동 갱신 - sets.push('MDFCN_DTM = NOW()') - params.push(postSn) - - await wingPool.query( - `UPDATE BOARD_POST SET ${sets.join(', ')} WHERE POST_SN = $${idx}`, - params - ) -} -``` - -#### 삭제 (논리삭제) - -```typescript -export async function deletePost(postSn: number, requesterId: string): Promise { - // 소유자 검증 - const existing = await wingPool.query( - "SELECT AUTHOR_ID FROM BOARD_POST WHERE POST_SN = $1 AND USE_YN = 'Y'", - [postSn] - ) - if (existing.rows.length === 0) { - throw new AuthError('게시글을 찾을 수 없습니다.', 404) - } - if (existing.rows[0].author_id !== requesterId) { - throw new AuthError('본인의 게시글만 삭제할 수 있습니다.', 403) - } - - // 논리삭제: USE_YN = 'N' - await wingPool.query( - "UPDATE BOARD_POST SET USE_YN = 'N', MDFCN_DTM = NOW() WHERE POST_SN = $1", - [postSn] - ) -} -``` - -#### 트랜잭션 패턴 - -여러 테이블을 동시에 변경해야 할 때: - -```typescript -export async function createPostWithAttachments( - input: CreatePostInput, - attachments: AttachmentInput[], -): Promise<{ postSn: number }> { - const client = await wingPool.connect() - - try { - await client.query('BEGIN') - - // 게시글 생성 - const postResult = await client.query( - `INSERT INTO BOARD_POST (CATEGORY_CD, TITLE, CONTENT, AUTHOR_ID) - VALUES ($1, $2, $3, $4) - RETURNING POST_SN as post_sn`, - [input.categoryCd, input.title, input.content, input.authorId] - ) - const postSn = postResult.rows[0].post_sn - - // 첨부파일 생성 - for (const att of attachments) { - await client.query( - `INSERT INTO BOARD_ATTACH (POST_SN, FILE_NM, FILE_PATH, FILE_SIZE) - VALUES ($1, $2, $3, $4)`, - [postSn, att.fileName, att.filePath, att.fileSize] - ) - } - - await client.query('COMMIT') - return { postSn } - } catch (err) { - await client.query('ROLLBACK') - throw err - } finally { - client.release() - } -} -``` - -#### 에러 처리 - -```typescript -import { AuthError } from '../auth/authService.js' - -// AuthError: status 코드와 메시지를 포함하는 커스텀 에러 -// Router에서 instanceof 체크로 적절한 HTTP 응답을 반환 - -throw new AuthError('게시글을 찾을 수 없습니다.', 404) -throw new AuthError('권한이 없습니다.', 403) -throw new AuthError('필수 항목이 누락되었습니다.', 400) -throw new AuthError('이미 존재하는 데이터입니다.', 409) -``` - -`AuthError` 클래스 정의 (`backend/src/auth/authService.ts`): - -```typescript -export class AuthError extends Error { - status: number - constructor(message: string, status: number) { - super(message) - this.status = status - this.name = 'AuthError' - } -} -``` - ---- - -### 6. Router 레이어 패턴 - -#### 미들웨어 체인 - -``` -requireAuth → requirePermission(resource, operation) → 핸들러 -``` - -- `requireAuth`: JWT 쿠키 검증, `req.user`에 페이로드 세팅 -- `requirePermission`: 리소스 x 오퍼레이션 권한 확인 - -#### CRUD 엔드포인트 표준 - -보안 취약점 점검 가이드에 따라 **POST 메서드를 기본**으로 사용한다. -OPER_CD는 HTTP Method가 아닌 **비즈니스 의미**로 결정한다. - -| URL 패턴 | OPER_CD | 미들웨어 | -|----------|---------|----------| -| `POST /api/{domain}/list` | READ | `requirePermission(resource, 'READ')` | -| `POST /api/{domain}/detail` | READ | `requirePermission(resource, 'READ')` | -| `POST /api/{domain}/create` | CREATE | `requirePermission(resource, 'CREATE')` | -| `POST /api/{domain}/update` | UPDATE | `requirePermission(resource, 'UPDATE')` | -| `POST /api/{domain}/delete` | DELETE | `requirePermission(resource, 'DELETE')` | - -#### 전체 Router 예시 - -```typescript -// backend/src/board/boardRouter.ts - -import { Router } from 'express' -import { requireAuth, requirePermission } from '../auth/authMiddleware.js' -import { AuthError } from '../auth/authService.js' -import { - listPosts, - getPost, - createPost, - updatePost, - deletePost, -} from './boardService.js' - -const router = Router() - -// 모든 엔드포인트에 인증 필수 -router.use(requireAuth) - -// 목록 조회 -router.post('/list', requirePermission('board:notice', 'READ'), async (req, res) => { - try { - const { categoryCd, search, page, size } = req.body - const result = await listPosts(categoryCd, search, page, size) - res.json(result) - } catch (err) { - if (err instanceof AuthError) { - res.status(err.status).json({ error: err.message }) - return - } - console.error('[board] 목록 조회 오류:', err) - res.status(500).json({ error: '게시글 목록 조회 중 오류가 발생했습니다.' }) - } -}) - -// 상세 조회 -router.post('/detail', requirePermission('board:notice', 'READ'), async (req, res) => { - try { - const { postSn } = req.body - if (!postSn) { - res.status(400).json({ error: '게시글 번호는 필수입니다.' }) - return - } - const post = await getPost(postSn) - res.json(post) - } catch (err) { - if (err instanceof AuthError) { - res.status(err.status).json({ error: err.message }) - return - } - console.error('[board] 상세 조회 오류:', err) - res.status(500).json({ error: '게시글 조회 중 오류가 발생했습니다.' }) - } -}) - -// 생성 -router.post('/create', requirePermission('board:notice', 'CREATE'), async (req, res) => { - try { - const { categoryCd, title, content, pinnedYn } = req.body - - // 필수 필드 검증 - if (!categoryCd || !title) { - res.status(400).json({ error: '카테고리와 제목은 필수입니다.' }) - return - } - - // req.user!.sub = 현재 로그인 사용자 UUID - const result = await createPost({ - categoryCd, - title, - content, - authorId: req.user!.sub, - pinnedYn, - }) - res.status(201).json(result) - } catch (err) { - if (err instanceof AuthError) { - res.status(err.status).json({ error: err.message }) - return - } - console.error('[board] 생성 오류:', err) - res.status(500).json({ error: '게시글 생성 중 오류가 발생했습니다.' }) - } -}) - -// 수정 -router.post('/update', requirePermission('board:notice', 'UPDATE'), async (req, res) => { - try { - const { postSn, title, content, categoryCd, pinnedYn } = req.body - - if (!postSn) { - res.status(400).json({ error: '게시글 번호는 필수입니다.' }) - return - } - - await updatePost(postSn, { title, content, categoryCd, pinnedYn }, req.user!.sub) - res.json({ success: true }) - } catch (err) { - if (err instanceof AuthError) { - res.status(err.status).json({ error: err.message }) - return - } - console.error('[board] 수정 오류:', err) - res.status(500).json({ error: '게시글 수정 중 오류가 발생했습니다.' }) - } -}) - -// 삭제 -router.post('/delete', requirePermission('board:notice', 'DELETE'), async (req, res) => { - try { - const { postSn } = req.body - - if (!postSn) { - res.status(400).json({ error: '게시글 번호는 필수입니다.' }) - return - } - - await deletePost(postSn, req.user!.sub) - res.json({ success: true }) - } catch (err) { - if (err instanceof AuthError) { - res.status(err.status).json({ error: err.message }) - return - } - console.error('[board] 삭제 오류:', err) - res.status(500).json({ error: '게시글 삭제 중 오류가 발생했습니다.' }) - } -}) - -export default router -``` - -#### 입력 검증 패턴 - -핸들러 내부에서 필수 필드를 직접 체크한다. - -```typescript -// 필수 필드 검증 -if (!categoryCd || !title) { - res.status(400).json({ error: '카테고리와 제목은 필수입니다.' }) - return -} - -// 배열 타입 검증 -if (!Array.isArray(roleSns)) { - res.status(400).json({ error: '역할 목록이 필요합니다.' }) - return -} - -// 길이 검증 -if (!password || password.length < 4) { - res.status(400).json({ error: '비밀번호는 4자 이상이어야 합니다.' }) - return -} -``` - -#### 에러 응답 패턴 - -모든 핸들러에서 동일한 에러 처리 구조를 사용한다. - -```typescript -try { - // 비즈니스 로직 -} catch (err) { - // 1. AuthError → 해당 status + message - if (err instanceof AuthError) { - res.status(err.status).json({ error: err.message }) - return - } - // 2. 예상치 못한 에러 → 500 + 일반 메시지 (내부 정보 노출 방지) - console.error('[domain] 작업 오류:', err) - res.status(500).json({ error: '처리 중 오류가 발생했습니다.' }) -} -``` - -#### server.ts 등록 - -```typescript -// backend/src/server.ts - -import boardRouter from './board/boardRouter.js' - -// API 라우트 — 업무 -app.use('/api/board', boardRouter) -``` - -#### req.user 구조 (JWT 페이로드) - -`requireAuth` 통과 후 `req.user`에 담기는 정보: - -```typescript -interface JwtPayload { - sub: string // 사용자 UUID (USER_ID) - acnt: string // 계정명 (USER_ACNT) - name: string // 사용자명 (USER_NM) - roles: string[] // 역할 코드 목록 ['ADMIN', 'MANAGER', 'USER', 'VIEWER'] -} - -// 사용 예시 -const userId = req.user!.sub // 현재 사용자 UUID -const userName = req.user!.name // 현재 사용자 이름 -const isAdmin = req.user!.roles.includes('ADMIN') -``` - ---- - -### 7. 프론트엔드 연동 패턴 - -#### API 서비스 파일 - -탭별로 `services/` 디렉토리에 API 함수를 분리한다. - -```typescript -// frontend/src/tabs/board/services/boardApi.ts - -import { api } from '@common/services/api' - -// 타입 정의 -export interface PostItem { - postSn: number - categoryCd: string - title: string - content: string | null - authorId: string - authorName: string - viewCnt: number - pinnedYn: string - useYn: string - regDtm: string - mdfcnDtm: string | null -} - -export interface PostListResult { - items: PostItem[] - totalCount: number - page: number - size: number -} - -// 목록 조회 -export async function fetchPosts(params: { - categoryCd?: string - search?: string - page?: number - size?: number -}): Promise { - const response = await api.post('/board/list', params) - return response.data -} - -// 상세 조회 -export async function fetchPost(postSn: number): Promise { - const response = await api.post('/board/detail', { postSn }) - return response.data -} - -// 생성 -export async function createPostApi(data: { - categoryCd: string - title: string - content?: string - pinnedYn?: string -}): Promise<{ postSn: number }> { - const response = await api.post<{ postSn: number }>('/board/create', data) - return response.data -} - -// 수정 -export async function updatePostApi( - postSn: number, - data: { title?: string; content?: string; categoryCd?: string; pinnedYn?: string }, -): Promise { - await api.post('/board/update', { postSn, ...data }) -} - -// 삭제 -export async function deletePostApi(postSn: number): Promise { - await api.post('/board/delete', { postSn }) -} -``` - -#### Axios 인스턴스 - -```typescript -// frontend/src/common/services/api.ts (이미 설정됨, 수정 불필요) - -import axios from 'axios' - -export const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api', - withCredentials: true, // JWT 쿠키 자동 포함 - timeout: 30000, // 30초 타임아웃 -}) - -// 401 응답 시 자동 로그아웃 (인터셉터) -// 403 응답 시 권한 부족 (requirePermission 미들웨어) -``` - -#### 권한 기반 UI 분기 - -```tsx -// frontend/src/tabs/board/components/PostList.tsx - -import { useAuthStore } from '@common/store/authStore' - -const PostList = () => { - const { hasPermission } = useAuthStore() - - return ( -
-

게시판

- - {/* CREATE 권한이 있을 때만 글쓰기 버튼 표시 */} - {hasPermission('board:notice', 'CREATE') && ( - - )} - - {/* 목록 렌더링 */} - {posts.map((post) => ( -
- {post.title} - - {/* UPDATE 권한 + 본인 글일 때만 수정 버튼 */} - {hasPermission('board:notice', 'UPDATE') && post.authorId === user?.id && ( - - )} - - {/* DELETE 권한 + 본인 글일 때만 삭제 버튼 */} - {hasPermission('board:notice', 'DELETE') && post.authorId === user?.id && ( - - )} -
- ))} - - {/* 페이징 */} - -
- ) -} -``` - -#### TanStack Query 연동 (권장) - -```typescript -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { fetchPosts, createPostApi, deletePostApi } from '../services/boardApi' - -// 목록 조회 -const { data, isLoading } = useQuery({ - queryKey: ['posts', categoryCd, search, page], - queryFn: () => fetchPosts({ categoryCd, search, page, size: 20 }), -}) - -// 생성 -const queryClient = useQueryClient() -const createMutation = useMutation({ - mutationFn: createPostApi, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['posts'] }) - }, -}) - -// 삭제 -const deleteMutation = useMutation({ - mutationFn: deletePostApi, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['posts'] }) - }, -}) -``` - ---- - -### 8. 권한 상속 실전 시나리오 - -`AUTH_PERM_TREE`와 `AUTH_PERM`의 상속 규칙이 실제로 어떻게 동작하는지 4가지 시나리오로 설명한다. - -#### 시나리오 1: 부모 허용 → 자식 상속 - -``` -AUTH_PERM: - ADMIN 역할 — board READ=Y, CREATE=Y, UPDATE=Y, DELETE=Y - -결과: - board:notice READ → 명시적 레코드 없음 → 부모(board) READ=Y 상속 → Y - board:notice CREATE → 명시적 레코드 없음 → 부모(board) CREATE=Y 상속 → Y - board:data READ → 명시적 레코드 없음 → 부모(board) READ=Y 상속 → Y - -→ 부모에게 권한을 주면 모든 자식이 자동으로 같은 권한을 상속한다. -``` - -#### 시나리오 2: 명시적 거부 (Override) - -``` -AUTH_PERM: - MANAGER 역할 — board READ=Y, CREATE=Y - board:notice CREATE=N (명시적) - -결과: - board:notice READ → 부모 상속 Y - board:notice CREATE → 명시적 N → N (공지 작성 불가) - board:data CREATE → 부모 상속 Y (자료실은 작성 가능) - -→ 자식에 명시적 레코드가 있으면 부모 상속보다 우선한다. -``` - -#### 시나리오 3: 부모 접근 차단 → 자식 전체 차단 - -``` -AUTH_PERM: - VIEWER 역할 — board READ=N - -결과: - board:notice READ → 부모 READ=N → 강제 N (규칙 1) - board:notice CREATE → 부모 READ=N → 강제 N (규칙 1) - board:data READ → 부모 READ=N → 강제 N (규칙 1) - -→ 부모의 READ가 N이면 자식의 모든 오퍼레이션이 강제 차단된다. - 자식에 명시적 Y가 있어도 무시된다. -``` - -#### 시나리오 4: 서브리소스 개별 허용 - -``` -AUTH_PERM: - USER 역할 — board READ=Y, CREATE=N - board:qna CREATE=Y (명시적) - -결과: - board:notice CREATE → 부모 상속 N (공지 작성 불가) - board:data CREATE → 부모 상속 N (자료실 작성 불가) - board:qna CREATE → 명시적 Y → Y (Q&A는 작성 가능) - -→ 부모에서 CUD를 기본 차단하고, 특정 서브리소스만 허용하는 패턴. -``` - -#### 내부 키 형식 - -permResolver에서 리소스와 오퍼레이션을 결합할 때 더블콜론(`::`)을 사용한다. - -``` -리소스 내부 경로: board:notice (싱글콜론) -리소스-오퍼레이션 결합: board:notice::READ (더블콜론, 내부 전용) -``` - -```typescript -// backend/src/roles/permResolver.ts -export function makePermKey(rsrcCode: string, operCd: string): string { - return `${rsrcCode}::${operCd}` -} -``` - ---- - -### 9. 새 CRUD API 추가 체크리스트 - -새 도메인의 CRUD API를 추가할 때 아래 순서대로 진행한다. - -#### 백엔드 - -- [ ] `database/migration/NNN_{domain}.sql` 작성 (DDL + 초기 데이터) - - 테이블 생성 (IF NOT EXISTS) - - FK, CHECK 제약, 인덱스 - - COMMENT - - 검증 SELECT -- [ ] DB 마이그레이션 실행 (`psql`로 직접 실행) -- [ ] `backend/src/{domain}/{domain}Service.ts` 작성 - - 인터페이스 정의 (Item, CreateInput, UpdateInput) - - CRUD 함수 (list, get, create, update, delete) - - wingPool import, AuthError import - - 동적 WHERE/SET 빌드, 소유자 검증 -- [ ] `backend/src/{domain}/{domain}Router.ts` 작성 - - requireAuth + requirePermission 미들웨어 - - POST /list, /detail, /create, /update, /delete - - 입력 검증, AuthError 분기, 500 에러 처리 -- [ ] `backend/src/server.ts`에 라우터 등록 - ```typescript - import boardRouter from './board/boardRouter.js' - app.use('/api/board', boardRouter) - ``` -- [ ] 빌드 확인: `cd backend && npm run build` - -#### 권한 등록 (필요 시) - -- [ ] `AUTH_PERM_TREE`에 리소스 등록 (마이그레이션 SQL) - ```sql - INSERT INTO AUTH_PERM_TREE (RSRC_CD, PARENT_CD, RSRC_NM, RSRC_LEVEL, SORT_ORD) - VALUES ('board:notice', 'board', '공지사항', 1, 2) - ON CONFLICT (RSRC_CD) DO NOTHING; - ``` -- [ ] `AUTH_PERM`에 역할별 권한 초기값 추가 (마이그레이션 SQL) - ```sql - -- ADMIN: 모든 오퍼레이션 허용 - INSERT INTO AUTH_PERM (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN) - SELECT r.ROLE_SN, 'board:notice', op.cd, 'Y' - FROM AUTH_ROLE r, (VALUES ('READ'),('CREATE'),('UPDATE'),('DELETE')) AS op(cd) - WHERE r.ROLE_CD = 'ADMIN' - ON CONFLICT DO NOTHING; - - -- VIEWER: READ만 허용 - INSERT INTO AUTH_PERM (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN) - SELECT r.ROLE_SN, 'board:notice', 'READ', 'Y' - FROM AUTH_ROLE r - WHERE r.ROLE_CD = 'VIEWER' - ON CONFLICT DO NOTHING; - ``` - -#### 프론트엔드 - -- [ ] `frontend/src/tabs/{domain}/services/{domain}Api.ts` 작성 - - 타입 정의 (interface) - - CRUD API 함수 (api.post 사용) -- [ ] 프론트 컴포넌트에서 mock 데이터 → API 호출로 전환 -- [ ] `hasPermission()` 조건부 렌더링 적용 - - CREATE 권한 → 글쓰기 버튼 - - UPDATE 권한 → 수정 버튼 - - DELETE 권한 → 삭제 버튼 -- [ ] 빌드 확인: `cd frontend && npx tsc --noEmit` - ---- - -## Part 2: 게시판 실전 튜토리얼 - -게시판(Board) CRUD API를 처음부터 끝까지 구현한 실전 예제. -Part 1의 규칙을 실제로 어떻게 적용하는지 단계별로 설명한다. - ---- - -### Step 1: DB 테이블 설계 - -**파일**: `database/migration/006_board.sql` - -```sql -CREATE TABLE IF NOT EXISTS BOARD_POST ( - POST_SN SERIAL PRIMARY KEY, - CATEGORY_CD VARCHAR(20) NOT NULL, - TITLE VARCHAR(200) NOT NULL, - CONTENT TEXT, - AUTHOR_ID UUID NOT NULL, - VIEW_CNT INTEGER NOT NULL DEFAULT 0, - PINNED_YN CHAR(1) NOT NULL DEFAULT 'N', - USE_YN CHAR(1) NOT NULL DEFAULT 'Y', - REG_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(), - MDFCN_DTM TIMESTAMPTZ, - - CONSTRAINT FK_BOARD_AUTHOR FOREIGN KEY (AUTHOR_ID) - REFERENCES auth.AUTH_USER(USER_ID), - CONSTRAINT CK_BOARD_CATEGORY - CHECK (CATEGORY_CD IN ('NOTICE','DATA','QNA','MANUAL')), - CONSTRAINT CK_BOARD_PINNED CHECK (PINNED_YN IN ('Y','N')), - CONSTRAINT CK_BOARD_USE CHECK (USE_YN IN ('Y','N')) -); - -CREATE INDEX IF NOT EXISTS IDX_BOARD_CATEGORY ON BOARD_POST(CATEGORY_CD); -CREATE INDEX IF NOT EXISTS IDX_BOARD_AUTHOR ON BOARD_POST(AUTHOR_ID); -CREATE INDEX IF NOT EXISTS IDX_BOARD_REG_DTM ON BOARD_POST(REG_DTM DESC); -``` - -**설계 포인트**: -- `wing` 스키마에 생성 (search_path 덕분에 쿼리에서 스키마 접두사 불필요) -- `AUTHOR_ID`는 `auth.AUTH_USER(USER_ID)`를 cross-schema FK 참조 -- `USE_YN`으로 논리 삭제 (물리 삭제 대신 `'N'`으로 변경) -- `CATEGORY_CD` CHECK 제약으로 유효값 강제 - -#### 카테고리 ↔ 리소스 매핑 - -| CATEGORY_CD | AUTH_PERM_TREE 리소스 | 정책 | -|---|---|---| -| `NOTICE` | `board:notice` | ADMIN/MANAGER만 CUD | -| `DATA` | `board:data` | MANAGER 이상 CUD | -| `QNA` | `board:qna` | 인증 사용자 CUD (본인 글만 UD) | -| `MANUAL` | `board:manual` | ADMIN만 CUD | - ---- - -### Step 2: Service 구현 - -**파일**: `backend/src/board/boardService.ts` - -#### 인터페이스 정의 - -```typescript -interface PostListItem { - sn: number - categoryCd: string - title: string - authorId: string - authorName: string - viewCnt: number - pinnedYn: string - regDtm: string -} - -interface ListPostsInput { - categoryCd?: string - search?: string - page?: number - size?: number -} - -interface ListPostsResult { - items: PostListItem[] - totalCount: number - page: number - size: number -} -``` - -#### 목록 조회 (페이징 + 필터 + 검색) - -```typescript -export async function listPosts(input: ListPostsInput): Promise { - const page = input.page && input.page > 0 ? input.page : 1 - const size = input.size && input.size > 0 ? Math.min(input.size, 100) : 20 - const offset = (page - 1) * size - - let whereClause = `WHERE bp.USE_YN = 'Y'` - const params: (string | number)[] = [] - let paramIdx = 1 - - if (input.categoryCd) { - whereClause += ` AND bp.CATEGORY_CD = $${paramIdx++}` - params.push(input.categoryCd) - } - - if (input.search) { - whereClause += ` AND (bp.TITLE ILIKE $${paramIdx} OR u.USER_NM ILIKE $${paramIdx})` - params.push(`%${input.search}%`) - paramIdx++ - } - - // 전체 건수 - const countResult = await wingPool.query( - `SELECT COUNT(*) as cnt FROM BOARD_POST bp - JOIN AUTH_USER u ON bp.AUTHOR_ID = u.USER_ID ${whereClause}`, - params - ) - const totalCount = parseInt(countResult.rows[0].cnt, 10) - - // 목록 (상단고정 우선 → 등록일 내림차순) - const listParams = [...params, size, offset] - const listResult = await wingPool.query( - `SELECT bp.POST_SN as sn, bp.CATEGORY_CD as category_cd, bp.TITLE as title, - bp.AUTHOR_ID as author_id, u.USER_NM as author_name, - bp.VIEW_CNT as view_cnt, bp.PINNED_YN as pinned_yn, bp.REG_DTM as reg_dtm - FROM BOARD_POST bp - JOIN AUTH_USER u ON bp.AUTHOR_ID = u.USER_ID - ${whereClause} - ORDER BY bp.PINNED_YN DESC, bp.REG_DTM DESC - LIMIT $${paramIdx++} OFFSET $${paramIdx}`, - listParams - ) - // ... 결과 매핑 후 return -} -``` - -**핵심**: `JOIN AUTH_USER`로 cross-schema JOIN 수행 (작성자명 표시). 이것이 DB 통합의 핵심 이점. - -#### 소유자 검증 패턴 (수정/삭제) - -```typescript -export async function updatePost( - postSn: number, - input: UpdatePostInput, - requesterId: string // ← req.user.sub (JWT에서 추출) -): Promise { - const existing = await wingPool.query( - `SELECT AUTHOR_ID as author_id FROM BOARD_POST WHERE POST_SN = $1 AND USE_YN = 'Y'`, - [postSn] - ) - - if (existing.rows.length === 0) { - throw new AuthError('게시글을 찾을 수 없습니다.', 404) - } - - // 본인 글만 수정 가능 - if (existing.rows[0].author_id !== requesterId) { - throw new AuthError('본인의 게시글만 수정할 수 있습니다.', 403) - } - - // ... 동적 SET 빌드 + UPDATE -} -``` - -#### 논리 삭제 - -```typescript -export async function deletePost(postSn: number, requesterId: string): Promise { - // 소유자 검증 (위와 동일) - await wingPool.query( - `UPDATE BOARD_POST SET USE_YN = 'N', MDFCN_DTM = NOW() WHERE POST_SN = $1`, - [postSn] - ) -} -``` - ---- - -### Step 3: Router 구현 - -**파일**: `backend/src/board/boardRouter.ts` - -#### 카테고리별 동적 리소스 결정 - -```typescript -const CATEGORY_RESOURCE: Record = { - NOTICE: 'board:notice', - DATA: 'board:data', - QNA: 'board:qna', - MANUAL: 'board:manual', -} -``` - -#### 엔드포인트별 requirePermission 적용 - -```typescript -// 목록/상세: 부모 리소스 'board' READ -router.get('/', requireAuth, requirePermission('board', 'READ'), listHandler) -router.get('/:sn', requireAuth, requirePermission('board', 'READ'), getHandler) - -// 작성: 카테고리별 서브리소스 CREATE (핵심!) -router.post('/', requireAuth, async (req, res, next) => { - const resource = CATEGORY_RESOURCE[req.body.categoryCd] || 'board' - requirePermission(resource, 'CREATE')(req, res, next) -}, createHandler) - -// 수정/삭제: 부모 리소스 권한 + 서비스에서 소유자 검증 -router.put('/:sn', requireAuth, requirePermission('board', 'UPDATE'), updateHandler) -router.delete('/:sn', requireAuth, requirePermission('board', 'DELETE'), deleteHandler) -``` - -**카테고리별 작성 권한의 원리**: -- POST `/api/board` 요청 시 body에 `categoryCd`가 포함 -- 미들웨어에서 `CATEGORY_RESOURCE[categoryCd]`로 서브리소스 결정 -- `board:notice` CREATE 권한이 없는 사용자는 공지 작성 불가 -- `board:qna` CREATE 권한이 있으면 Q&A는 작성 가능 - ---- - -### Step 4: server.ts 등록 - -```typescript -import boardRouter from './board/boardRouter.js' - -// API 라우트 — 업무 -app.use('/api/board', boardRouter) -``` - ---- - -### Step 5: 프론트엔드 연동 - -#### API 서비스 - -**파일**: `frontend/src/tabs/board/services/boardApi.ts` - -```typescript -import { api } from '@common/services/api'; - -export interface BoardPostItem { - sn: number; - categoryCd: string; - title: string; - authorId: string; - authorName: string; - viewCnt: number; - pinnedYn: string; - regDtm: string; -} - -export interface BoardListResponse { - items: BoardPostItem[]; - totalCount: number; - page: number; - size: number; -} - -export async function fetchBoardPosts(params?: BoardListParams): Promise { - const response = await api.get('/board', { params }); - return response.data; -} - -export async function createBoardPost(input: CreateBoardPostInput): Promise<{ sn: number }> { - const response = await api.post<{ sn: number }>('/board', input); - return response.data; -} -``` - -#### 권한 기반 UI 분기 - -**파일**: `frontend/src/tabs/board/components/BoardView.tsx` - -```tsx -import { useAuthStore } from '@common/store/authStore'; - -const hasPermission = useAuthStore((s) => s.hasPermission); - -// 서브탭 기준 글쓰기 권한 리소스 결정 -const getWriteResource = () => { - if (activeSubTab === 'all') return 'board'; - return `board:${activeSubTab}`; -}; - -// 글쓰기 버튼 조건부 렌더링 -{hasPermission(getWriteResource(), 'CREATE') && ( - -)} -``` - ---- - -### Step 6: 권한 시나리오 테스트 - -| 시나리오 | 역할 | 요청 | 결과 | -|---|---|---|---| -| ADMIN이 공지 작성 | ADMIN | POST `/api/board` `{categoryCd:"NOTICE"}` | 201 Created | -| USER가 공지 작성 | USER | POST `/api/board` `{categoryCd:"NOTICE"}` | 403 (board:notice CREATE 없음) | -| USER가 Q&A 작성 | USER | POST `/api/board` `{categoryCd:"QNA"}` | 201 (board:qna CREATE 있음) | -| VIEWER가 Q&A 작성 | VIEWER | POST `/api/board` `{categoryCd:"QNA"}` | 403 (board:qna CREATE 없음) | -| USER가 본인 글 수정 | USER | PUT `/api/board/11` (본인 글) | 200 | -| USER가 타인 글 수정 | USER | PUT `/api/board/1` (타인 글) | 403 (소유자 검증 실패) | -| ADMIN이 목록 조회 | ADMIN | GET `/api/board` | 200 (board READ 있음) | - ---- - -### 관련 파일 전체 목록 - -| 위치 | 파일 | 설명 | -|---|---|---| -| DB | `database/migration/006_board.sql` | DDL + 초기 데이터 | -| 백엔드 | `backend/src/board/boardService.ts` | CRUD 비즈니스 로직 | -| 백엔드 | `backend/src/board/boardRouter.ts` | 라우터 + requirePermission | -| 백엔드 | `backend/src/server.ts` | boardRouter 등록 | -| 프론트 | `frontend/src/tabs/board/services/boardApi.ts` | API 서비스 | -| 프론트 | `frontend/src/tabs/board/components/BoardView.tsx` | 목록/상세/작성 통합 뷰 (API 연동) | -| 프론트 | `frontend/src/tabs/board/components/BoardWriteForm.tsx` | 게시글 작성/수정 폼 (API 호출) | -| 프론트 | `frontend/src/tabs/board/components/BoardDetailView.tsx` | 게시글 상세 보기 (API 호출) | diff --git a/docs/_backup_20260301/DEVELOPMENT-GUIDE.md b/docs/_backup_20260301/DEVELOPMENT-GUIDE.md deleted file mode 100644 index 68cc157..0000000 --- a/docs/_backup_20260301/DEVELOPMENT-GUIDE.md +++ /dev/null @@ -1,433 +0,0 @@ -# WING 개발 워크플로우 가이드 - -## 목차 -1. [전체 흐름 요약](#1-전체-흐름-요약) -2. [계획 수립 (Plan)](#2-계획-수립-plan) -3. [브랜치 생성 및 개발](#3-브랜치-생성-및-개발) -4. [커밋 & 푸시](#4-커밋--푸시) -5. [MR 생성 (feature → develop)](#5-mr-생성-feature--develop) -6. [릴리즈 PR (develop → main)](#6-릴리즈-pr-develop--main) -7. [자동 배포](#7-자동-배포) -8. [프로젝트 문서 최신화](#8-프로젝트-문서-최신화) -9. [실전 예시: 기능 추가 A to Z](#9-실전-예시-기능-추가-a-to-z) - ---- - -## 1. 전체 흐름 요약 - -``` -계획 수립 → 브랜치 생성 → 개발 → 커밋/푸시 → develop MR → main PR → 자동 배포 -``` - -``` -[Plan Mode] Claude가 코드베이스 분석 후 구현 계획 작성 - ↓ -[Branch] feature/기능명 브랜치 생성 (develop 기반) - ↓ -[Develop] 코드 작성 + TypeScript/ESLint 검증 - ↓ -[Commit & Push] Conventional Commits 형식 + pre-commit 자동 검증 - ↓ -[MR → develop] 코드 리뷰 + 머지 - ↓ -[PR → main] 릴리즈 MR + 머지 - ↓ -[Auto Deploy] Gitea Actions → 빌드 → 서버 배포 -``` - ---- - -## 2. 계획 수립 (Plan) - -3개 이상 파일 수정이 예상되거나 아키텍처에 영향을 주는 작업은 **Plan Mode**로 시작합니다. - -### Claude에게 요청하는 방법 - -``` -"사용자 프로필 페이지를 추가해줘" -→ Claude가 자동으로 Plan Mode 진입 → 코드베이스 분석 → 구현 계획 제시 -→ 사용자 승인 후 구현 시작 -``` - -### 계획에 포함되는 내용 -- 수정/생성할 파일 목록 -- 변경 범위 및 영향도 -- 기술적 선택지와 권장안 -- 구현 순서 - -### Plan Mode가 불필요한 경우 -- 단순 버그 수정 (1~2개 파일) -- 텍스트/스타일 수정 -- 설정 변경 - ---- - -## 3. 브랜치 생성 및 개발 - -### 브랜치 네이밍 규칙 - -| 유형 | 형식 | 예시 | -|------|------|------| -| 기능 | `feature/설명` | `feature/user-profile` | -| 이슈 | `feature/ISSUE-번호-설명` | `feature/ISSUE-42-login-fix` | -| 버그 | `bugfix/ISSUE-번호-설명` | `bugfix/ISSUE-15-token-expired` | -| 긴급 | `hotfix/설명` | `hotfix/security-patch` | - -### 브랜치 생성 - -```bash -# develop에서 분기 -git checkout develop -git pull origin develop -git checkout -b feature/user-profile -``` - -### 개발 중 검증 - -로컬에서 타입 체크와 린트를 수시로 확인합니다: - -```bash -# Frontend -cd frontend && npx tsc --noEmit && npx eslint src/ - -# Backend -cd backend && npx tsc --noEmit -``` - ---- - -## 4. 커밋 & 푸시 - -### Conventional Commits 형식 - -``` -type(scope): 한국어 설명 -``` - -| type | 용도 | 예시 | -|------|------|------| -| `feat` | 새 기능 | `feat(auth): Google OAuth 로그인 추가` | -| `fix` | 버그 수정 | `fix(map): 레이어 겹침 오류 수정` | -| `refactor` | 리팩토링 | `refactor(api): 중복 호출 제거` | -| `docs` | 문서 | `docs: API 엔드포인트 문서 추가` | -| `chore` | 설정/빌드 | `chore: 의존성 버전 업데이트` | -| `ci` | CI/CD | `ci: 백엔드 빌드 스텝 추가` | -| `style` | 포맷팅 | `style: ESLint 경고 수정` | - -### pre-commit 자동 검증 - -커밋 시 `.githooks/pre-commit`이 자동 실행됩니다: -1. Frontend TypeScript 타입 체크 -2. Frontend ESLint 검증 -3. Backend TypeScript 타입 체크 - -**하나라도 실패하면 커밋이 차단됩니다.** - -### 푸시 - -```bash -git push origin feature/user-profile -``` - -### Claude 스킬 활용 - -``` -/push # 변경사항 확인 → 커밋 → 푸시 (한 번에) -/mr # 커밋 → 푸시 → MR 생성 (한 번에) -/mr main # 커밋 → 푸시 → main으로 MR 생성 -``` - ---- - -## 5. MR 생성 (feature → develop) - -### Gitea에서 MR 생성 - -1. https://gitea.gc-si.dev/gc/wing-ops/compare/develop...feature/user-profile -2. 제목: Conventional Commits 형식 -3. 본문: 변경 내용 요약 + 테스트 계획 - -### Claude로 MR 생성 - -``` -/create-mr develop # feature → develop MR 자동 생성 -``` - -### MR 본문 템플릿 - -```markdown -## Summary -- 사용자 프로필 페이지 추가 -- 프로필 수정 API 연동 - -## 변경 파일 -- frontend/src/components/views/ProfileView.tsx (신규) -- backend/src/users/userRouter.ts (수정) - -## Test plan -- [ ] 프로필 페이지 접근 확인 -- [ ] 프로필 수정 후 저장 확인 -``` - -### 머지 후 - -```bash -# 로컬 develop 동기화 -git checkout develop -git pull origin develop -``` - ---- - -## 6. 릴리즈 PR (develop → main) - -develop에 기능이 머지된 후, 배포를 위해 main으로 릴리즈 MR을 생성합니다. - -### Claude로 릴리즈 MR 생성 - -``` -/release # develop → main 릴리즈 MR 자동 생성 -``` - -### 릴리즈 MR 체크리스트 - -```markdown -## Release v2.x.x - -### 포함 기능 -1. feat(auth): Google OAuth 로그인 -2. fix(map): 레이어 오류 수정 - -### 배포 전 확인 -- [ ] 로컬 빌드 성공 (frontend + backend) -- [ ] 서버 환경변수 설정 완료 -- [ ] DB 마이그레이션 적용 (필요 시) -``` - -### main 머지 → 자동 배포 트리거 - -main에 머지되면 `.gitea/workflows/deploy.yml`이 자동 실행됩니다. - ---- - -## 7. 자동 배포 - -### CI/CD 파이프라인 (Gitea Actions) - -``` -main 브랜치 push - ↓ -[Frontend] npm ci → vite build → /deploy/wing-demo/ - ↓ -[Backend] npm ci → tsc → /deploy/wing-demo-backend/ - ↓ -[Server] .deploy-trigger 감지 → wing-demo-api 재시작 -``` - -### 배포 환경 - -| 항목 | 값 | -|------|---| -| 프론트엔드 | https://wing-demo.gc-si.dev | -| 백엔드 API | https://wing-demo.gc-si.dev/api/ | -| 서버 | rocky-211 (Rocky Linux 9.6) | -| 프로세스 | systemd `wing-demo-api.service` | - -### 배포 확인 - -```bash -# 프론트엔드 응답 확인 -curl -s -o /dev/null -w '%{http_code}' https://wing-demo.gc-si.dev/ - -# 백엔드 API 확인 -curl -s https://wing-demo.gc-si.dev/api/auth/me -``` - -### 환경변수 관리 - -| 위치 | 용도 | -|------|------| -| systemd 서비스 파일 | 서버 런타임 환경변수 (DB, JWT 등) | -| Gitea Secrets | CI/CD 빌드 시 환경변수 (API 키 등) | - -```bash -# Gitea Secret 등록 (API) -curl -X PUT "https://gitea.gc-si.dev/api/v1/repos/gc/wing-ops/actions/secrets/KEY_NAME" \ - -H "Authorization: token " \ - -H "Content-Type: application/json" \ - -d '{"data":"secret-value"}' - -# Gitea Secret 등록 (Web UI) -# Settings → Actions → Secrets → Add Secret -``` - ---- - -## 8. 프로젝트 문서 최신화 - -### 자동 관리되는 문서 - -Claude 세션 중 커밋/컴팩트 시 hook이 자동으로 갱신을 안내합니다: - -| 문서 | 위치 | 갱신 시점 | -|------|------|----------| -| MEMORY.md | `~/.claude/projects/.../memory/` | 매 세션 | -| project-snapshot.md | 위와 동일 | 구조 변경 시 | -| project-history.md | 위와 동일 | 매 커밋 | -| api-types.md | 위와 동일 | API 변경 시 | -| CHANGELOG.md | `docs/CHANGELOG.md` | 매 커밋 | - -### 수동으로 최신화해야 하는 문서 - -| 문서 | 위치 | 갱신 주기 | -|------|------|----------| -| CLAUDE.md | 프로젝트 루트 | 기술 스택 변경 시 | -| INSTALL_GUIDE.md | `docs/` | 배포 환경 변경 시 | -| auth_init.sql | `database/` | DB 스키마 변경 시 | - -### 주기적 최신화 체크리스트 - -**매 기능 개발 완료 시:** -``` -Claude에게: "memory 파일 최신화해줘" -→ project-snapshot.md 갱신 -→ api-types.md 갱신 (API 변경 시) -→ project-history.md에 변경 이력 추가 -``` - -**매주 또는 스프린트 종료 시:** -``` -Claude에게: "프로젝트 문서 전체 점검해줘" -→ CLAUDE.md 기술 스택 확인 -→ CHANGELOG.md 누락 항목 보충 -→ 의존성 버전 확인 (npm outdated) -``` - -**팀 워크플로우 업데이트 시:** -``` -/sync-team-workflow # Gitea에서 최신 팀 규칙 동기화 -``` - ---- - -## 9. 실전 예시: 기능 추가 A to Z - -### 시나리오: "사고 상세 조회 페이지 추가" - -#### Step 1. 계획 - -``` -사용자: "사고 상세 조회 페이지를 추가해줘. 사고 목록에서 클릭하면 상세 정보를 보여주는 페이지야" - -Claude: (Plan Mode 진입) - → 코드베이스 분석 - → 구현 계획 제시: - 1. backend/src/incidents/incidentService.ts - 상세 조회 함수 - 2. backend/src/incidents/incidentRouter.ts - GET /api/incidents/:id - 3. frontend/src/services/incidentApi.ts - API 함수 - 4. frontend/src/components/views/IncidentDetailView.tsx - 상세 화면 - 5. frontend/src/App.tsx - 라우팅 연결 - -사용자: (계획 승인) -``` - -#### Step 2. 브랜치 생성 - -```bash -git checkout develop -git pull origin develop -git checkout -b feature/incident-detail -``` - -#### Step 3. 개발 - -Claude가 계획에 따라 코드 작성 (5개 파일 수정/생성) - -#### Step 4. 커밋 & 푸시 - -``` -사용자: "커밋하고 푸시해줘" - -Claude: - git add backend/src/incidents/... frontend/src/... - git commit -m "feat(incidents): 사고 상세 조회 페이지 추가" - # pre-commit: TypeScript ✓, ESLint ✓ - git push origin feature/incident-detail -``` - -또는 스킬 사용: - -``` -/push feat(incidents): 사고 상세 조회 페이지 추가 -``` - -#### Step 5. develop MR - -``` -사용자: "develop MR 만들어줘" - -Claude: - → Gitea API로 MR 생성 - → feature/incident-detail → develop - → MR #5: https://gitea.gc-si.dev/gc/wing-ops/pulls/5 -``` - -또는: -``` -/create-mr develop -``` - -#### Step 6. 코드 리뷰 & 머지 - -- Gitea에서 MR 리뷰 -- 승인 후 Squash Merge - -#### Step 7. 릴리즈 PR - -``` -사용자: "main으로 릴리즈 MR 만들어줘" - -Claude: - → develop → main MR 생성 - → MR #6 (release) -``` - -또는: -``` -/release -``` - -#### Step 8. main 머지 → 자동 배포 - -- main에 머지 → Gitea Actions 실행 -- Frontend 빌드 (Vite) → /deploy/wing-demo/ -- Backend 빌드 (tsc) → /deploy/wing-demo-backend/ -- .deploy-trigger → cron이 감지 → wing-demo-api 재시작 -- https://wing-demo.gc-si.dev 에서 확인 - -#### Step 9. 문서 최신화 - -``` -사용자: "memory 파일 최신화해줘" - -Claude: - → project-snapshot.md: incidents 모듈 추가 반영 - → api-types.md: GET /api/incidents/:id 추가 - → project-history.md: "사고 상세 조회 페이지 추가" 기록 -``` - ---- - -## 부록: 자주 쓰는 Claude 명령 - -| 명령 | 설명 | -|------|------| -| `"커밋하고 푸시해줘"` | 변경사항 커밋 + 푸시 | -| `"develop MR 만들어줘"` | feature → develop MR | -| `"memory 최신화해줘"` | 프로젝트 문서 갱신 | -| `/push` | 커밋 + 푸시 (스킬) | -| `/mr` | 커밋 + 푸시 + MR (스킬) | -| `/release` | develop → main 릴리즈 MR (스킬) | -| `/create-mr develop` | MR만 생성 (스킬) | -| `/sync-team-workflow` | 팀 워크플로우 동기화 (스킬) | -| `/changelog` | CHANGELOG.md 갱신 (스킬) | diff --git a/docs/_backup_20260301/INSTALL_GUIDE.md b/docs/_backup_20260301/INSTALL_GUIDE.md deleted file mode 100755 index 227bcd9..0000000 --- a/docs/_backup_20260301/INSTALL_GUIDE.md +++ /dev/null @@ -1,165 +0,0 @@ -# WING 해양환경 위기대응 통합시스템 - 설치 매뉴얼 - -## 1. 필수 소프트웨어 - -| 소프트웨어 | 최소 버전 | 용도 | 다운로드 | -|-----------|----------|------|---------| -| Node.js | v20 이상 (권장 v25) | 프론트엔드/백엔드 실행 | https://nodejs.org | -| npm | v10 이상 | 패키지 관리 | Node.js에 포함 | - -> **오프라인 환경**: 인터넷이 안 되는 망에서는 `node_modules`가 포함된 압축 파일을 사용하세요 (아래 "오프라인 설치" 참고). - ---- - -## 2. 프로젝트 구조 - -``` -wing/ -├── frontend/ # React + Vite 프론트엔드 (포트 5173) -│ ├── src/ -│ │ ├── components/ # UI 컴포넌트 -│ │ ├── data/ # 정적 데이터 -│ │ ├── hooks/ # 커스텀 훅 -│ │ ├── types/ # TypeScript 타입 정의 -│ │ ├── utils/ # 유틸리티 함수 -│ │ └── store/ # 상태관리 -│ └── package.json -├── backend/ # Express 백엔드 API (포트 3001) -│ ├── src/ -│ │ ├── routes/ # API 라우트 -│ │ ├── middleware/ # 미들웨어 (보안 등) -│ │ ├── db/ # DB 연결 -│ │ └── server.ts # 서버 엔트리 -│ └── package.json -└── database/ # DB 초기화 SQL - ├── database_init.sql - └── auth_init.sql -``` - ---- - -## 3. 온라인 설치 (인터넷 가능한 환경) - -### 3-1. 의존성 설치 - -```bash -# 프론트엔드 -cd wing/frontend -npm install - -# 백엔드 -cd ../backend -npm install -``` - -### 3-2. 데이터베이스 설정 - -운영 PostgreSQL에 직접 연결합니다. `backend/.env` 파일에서 DB 연결 정보를 설정하세요. - -```bash -# backend/.env -AUTH_DB_HOST= -AUTH_DB_PORT=5432 -AUTH_DB_NAME=wing_auth -AUTH_DB_USER=wing_auth -AUTH_DB_PASSWORD=<비밀번호> -``` - -> 신규 DB 초기화가 필요한 경우 `database/auth_init.sql`을 실행하세요. - -### 3-3. 백엔드 실행 - -```bash -cd wing/backend -npm run dev -``` - -→ `http://localhost:3001` 에서 API 서버 시작 - -### 3-4. 프론트엔드 실행 - -```bash -cd wing/frontend -npm run dev -``` - -→ `http://localhost:5173` 에서 웹 앱 시작 - ---- - -## 4. 오프라인 설치 (폐쇄망/다른 망) - -인터넷이 안 되는 환경에서는 `npm install`이 불가능합니다. -이 경우 **node_modules 포함 압축 파일**을 사용하세요. - -### 4-1. 압축 해제 - -```bash -# wing_full.tar.gz 파일을 작업 폴더에 복사한 뒤: -tar -xzf wing_full.tar.gz -``` - -### 4-2. Node.js 설치 - -대상 PC에 Node.js가 없으면 오프라인 설치 파일(.msi 또는 .pkg)을 미리 준비하여 설치합니다. - -- Windows: `node-v25.x.x-x64.msi` -- macOS: `node-v25.x.x.pkg` - -### 4-3. DB 연결 설정 - -`backend/.env` 파일에서 연결 가능한 PostgreSQL 정보를 설정합니다. - -### 4-4. 실행 - -node_modules가 이미 포함되어 있으므로 바로 실행 가능합니다. - -```bash -# 터미널 1 - 백엔드 -cd wing/backend -npm run dev - -# 터미널 2 - 프론트엔드 -cd wing/frontend -npm run dev -``` - ---- - -## 5. 접속 정보 요약 - -| 서비스 | URL | 비고 | -|--------|-----|------| -| 프론트엔드 (WING) | http://localhost:5173 | Vite dev server | -| 백엔드 API | http://localhost:3001 | Express | -| PostgreSQL | 운영 DB 직접 연결 | `.env` 설정 참조 | - ---- - -## 6. 주요 명령어 - -```bash -# 프론트엔드 빌드 (배포용) -cd frontend && npm run build # dist/ 폴더에 정적 파일 생성 - -# 백엔드 빌드 -cd backend && npm run build # dist/ 폴더에 JS 생성 - -# DB 시드 데이터 입력 -cd backend && npm run db:seed - -# TypeScript 타입 체크 -cd frontend && npx tsc --noEmit -``` - ---- - -## 7. 트러블슈팅 - -| 증상 | 해결 | -|------|------| -| `npm run dev` 실행 시 포트 충돌 | `lsof -i :5173` 또는 `lsof -i :3001`로 확인 후 프로세스 종료 | -| `EACCES` 권한 오류 | `sudo chown -R $(whoami) wing/` | -| 프론트엔드에서 API 호출 실패 | 백엔드(`localhost:3001`)가 실행 중인지 확인 | -| DB 연결 실패 | `backend/.env`의 DB 연결 정보 확인, PostgreSQL 접근 가능 여부 확인 | -| `MODULE_NOT_FOUND` 오류 | `npm install` 재실행 (온라인) 또는 node_modules 포함 압축본 사용 | diff --git a/docs/_backup_20260301/MENU-TAB-GUIDE.md b/docs/_backup_20260301/MENU-TAB-GUIDE.md deleted file mode 100644 index e9bfb70..0000000 --- a/docs/_backup_20260301/MENU-TAB-GUIDE.md +++ /dev/null @@ -1,194 +0,0 @@ -# WING 메뉴 탭 추가 가이드 - -새로운 메뉴 탭을 추가할 때 필요한 절차를 설명합니다. - -## 메뉴 시스템 구조 - -``` -DB: AUTH_SETTING (menu.config JSON) - ↕ GET/PUT /api/menus -Backend: settingsService.ts (DEFAULT_MENU_CONFIG, VALID_MENU_IDS) - ↕ API -Frontend: menuStore.ts → TopBar.tsx (탭 렌더링) - → App.tsx (renderView 라우팅) -``` - -- **DB**가 메뉴 정의의 단일 소스 (id, label, icon, enabled, order) -- **TopBar**는 `enabled && hasPermission` 조건으로 탭을 필터링하고 `order` 순 정렬 -- **App.tsx**의 `renderView`가 탭 ID에 따라 뷰 컴포넌트를 매핑 -- **admin** 탭은 메뉴 관리 대상에서 제외 (TopBar에서 별도 아이콘 버튼으로 접근) - -## 수정 파일 요약 - -| 순서 | 파일 | 작업 | 필수 | -|------|------|------|------| -| 1 | `frontend/src/tabs/{탭명}/components/XxxView.tsx` | 뷰 컴포넌트 생성 | O | -| 2 | `frontend/src/tabs/{탭명}/index.ts` | re-export 생성 | O | -| 3 | `frontend/src/App.tsx` | MainTab 타입 + import + renderView | O | -| 4 | `backend/src/settings/settingsService.ts` | DEFAULT_MENU_CONFIG에 항목 추가 | O | -| 5 | `database/auth_init.sql` | menu.config 초기 JSON에 추가 | O | -| 6 | 관리자 UI | 메뉴 관리에서 활성화 | O | - -## Step 1: 뷰 컴포넌트 생성 - -`frontend/src/tabs/{탭명}/components/` 에 새 뷰 컴포넌트를 생성합니다. - -```tsx -// frontend/src/tabs/monitoring/components/MonitoringView.tsx - -export function MonitoringView() { - return ( -
-
-

실시간 모니터링

- {/* 뷰 콘텐츠 */} -
-
- ) -} -``` - -`index.ts`에서 re-export합니다: -```tsx -// frontend/src/tabs/monitoring/index.ts -export { MonitoringView } from './components/MonitoringView' -``` - -기존 탭(`@tabs/prediction`, `@tabs/weather` 등)의 레이아웃 패턴을 참고하세요. -공통 모듈은 `@common/` alias로 import합니다. - -## Step 2: App.tsx 탭 등록 - -3가지를 수정합니다. - -### 2-1. MainTab 타입에 ID 추가 - -```tsx -// frontend/src/App.tsx (line 20) - -// Before -export type MainTab = 'prediction' | 'hns' | ... | 'admin' - -// After -export type MainTab = 'prediction' | 'hns' | ... | 'monitoring' | 'admin' -``` - -### 2-2. 뷰 컴포넌트 import - -```tsx -import { MonitoringView } from '@tabs/monitoring' -``` - -### 2-3. renderView switch에 case 추가 - -```tsx -const renderView = () => { - switch (activeMainTab) { - // ... 기존 case들 ... - case 'monitoring': - return - // ... - } -} -``` - -## Step 3: 백엔드 메뉴 설정 등록 - -`backend/src/settings/settingsService.ts`의 `DEFAULT_MENU_CONFIG` 배열에 항목을 추가합니다. - -```typescript -const DEFAULT_MENU_CONFIG: MenuConfigItem[] = [ - // ... 기존 10개 메뉴 ... - { id: 'monitoring', label: '실시간 모니터링', icon: '📡', enabled: true, order: 11 }, -] -``` - -`VALID_MENU_IDS`는 `DEFAULT_MENU_CONFIG`에서 자동 파생되므로 별도 수정 불필요합니다. - -```typescript -const VALID_MENU_IDS = DEFAULT_MENU_CONFIG.map(m => m.id) // 자동 포함됨 -``` - -> **주의**: `updateMenuConfig()`은 `VALID_MENU_IDS.length` 개수 전체가 포함되어야 저장을 허용합니다. -> 기존 운영 DB에 새 메뉴가 없는 상태에서도 `getMenuConfig()`의 fallback이 DEFAULT_MENU_CONFIG을 반환하므로 정상 동작합니다. - -## Step 4: DB 초기 데이터 업데이트 - -`database/auth_init.sql`의 `menu.config` 초기 JSON에 새 항목을 추가합니다. - -```sql -INSERT INTO AUTH_SETTING (SETTING_KEY, SETTING_VAL, SETTING_DC, MDFCN_DTM) VALUES -('menu.config', '[ - {"id":"prediction","label":"유출유 확산예측","icon":"🛢️","enabled":true,"order":1}, - ...기존 메뉴들... - {"id":"monitoring","label":"실시간 모니터링","icon":"📡","enabled":true,"order":11} -]', '메뉴 구성 설정', NOW()) -ON CONFLICT (SETTING_KEY) DO NOTHING; -``` - -> **참고**: 이 SQL은 신규 설치 시에만 적용됩니다. 기존 운영 DB는 관리자 UI에서 메뉴를 관리합니다. - -## Step 5: 관리자 메뉴 관리에서 활성화 - -코드 배포 후: -1. 관리자 계정으로 로그인 -2. 관리자 패널(⚙️) → 메뉴 관리 탭 -3. 새 메뉴가 목록에 표시됨 -4. 활성/비활성 토글, 순서, 라벨, 아이콘을 설정 -5. "변경사항 저장" 클릭 - -> 기존 DB에 새 메뉴 ID가 없으면 `getMenuConfig()`가 DEFAULT_MENU_CONFIG fallback을 사용하여 새 메뉴가 자동으로 목록에 나타납니다. - -## 실전 예시: "모니터링" 탭 추가 - -### 1. 뷰 컴포넌트 생성 - -```bash -# frontend/src/components/views/MonitoringView.tsx 파일 생성 -``` - -### 2. App.tsx 수정 (3곳) - -```diff -+ import { MonitoringView } from './components/views/MonitoringView' - -- export type MainTab = 'prediction' | 'hns' | 'rescue' | 'reports' | 'aerial' | 'assets' | 'scat' | 'incidents' | 'board' | 'weather' | 'admin' -+ export type MainTab = 'prediction' | 'hns' | 'rescue' | 'reports' | 'aerial' | 'assets' | 'scat' | 'incidents' | 'board' | 'weather' | 'monitoring' | 'admin' - - const renderView = () => { - switch (activeMainTab) { - // ... -+ case 'monitoring': -+ return - case 'admin': - return - } - } -``` - -### 3. settingsService.ts 수정 - -```diff - const DEFAULT_MENU_CONFIG: MenuConfigItem[] = [ - // ... 기존 메뉴들 ... - { id: 'incidents', label: '통합조회', icon: '🔍', enabled: true, order: 10 }, -+ { id: 'monitoring', label: '실시간 모니터링', icon: '📡', enabled: true, order: 11 }, - ] -``` - -### 4. auth_init.sql 수정 - -menu.config JSON에 새 항목 추가 (신규 설치용) - -### 5. 배포 후 관리자 UI에서 활성화 - -## 체크리스트 - -- [ ] 뷰 컴포넌트 생성 (`frontend/src/components/views/`) -- [ ] `MainTab` 타입 업데이트 (`App.tsx`) -- [ ] import 및 renderView switch case 추가 (`App.tsx`) -- [ ] `DEFAULT_MENU_CONFIG`에 추가 (`settingsService.ts`) -- [ ] `menu.config` 초기 JSON 업데이트 (`auth_init.sql`) -- [ ] TypeScript 컴파일 통과 (`cd frontend && npx tsc --noEmit`) -- [ ] ESLint 통과 (`cd frontend && npx eslint .`) -- [ ] 관리자 메뉴 관리에서 새 메뉴 표시 확인 diff --git a/docs/_backup_20260301/MOCK-TO-API-GUIDE.md b/docs/_backup_20260301/MOCK-TO-API-GUIDE.md deleted file mode 100644 index 684f201..0000000 --- a/docs/_backup_20260301/MOCK-TO-API-GUIDE.md +++ /dev/null @@ -1,435 +0,0 @@ -# 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) — 새 메뉴 탭 추가 절차 diff --git a/docs/_backup_20260301/README.md b/docs/_backup_20260301/README.md deleted file mode 100755 index 53f349e..0000000 --- a/docs/_backup_20260301/README.md +++ /dev/null @@ -1,247 +0,0 @@ -# WING-OPS (해양 방제 운영 지원 시스템) - -해양 오염 사고 대응을 위한 방제 운영 지원 시스템. -유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공합니다. - ---- - -## 빠른 시작 - -```bash -# 1. 저장소 복제 -git clone https://gitea.gc-si.dev/gc/wing-ops.git -cd wing-ops - -# 2. Claude Code 세션 열기 -claude - -# 3. 팀 워크플로우 초기화 -/init-project -``` - -`/init-project` 실행 시 자동으로 구성되는 항목: -- `.claude/` 디렉토리 (rules, skills, scripts, settings) -- `.githooks/` (pre-commit, commit-msg 자동 검증) -- Git hooks 경로 설정 (`core.hooksPath`) -- 메모리 디렉토리 초기화 - -> 상세 설치 절차(Docker, DB, 오프라인 환경 등)는 [INSTALL_GUIDE.md](INSTALL_GUIDE.md)를 참조하세요. - ---- - -## 기술 스택 - -| 영역 | 기술 | -|------|------| -| Frontend | React 19, Vite 7, TypeScript 5.9, Tailwind CSS 3 | -| Backend | Express 4, TypeScript, PostgreSQL (pg) | -| 상태 관리 | Zustand (클라이언트), TanStack Query (서버) | -| 지도 | Leaflet + react-leaflet | -| 실시간 | Socket.IO | -| 인증 | JWT (HttpOnly Cookie), Google OAuth | -| DB | PostgreSQL 16 + PostGIS (wing 운영DB + wing_auth 인증DB) | -| CI/CD | Gitea Actions | - ---- - -## 프로젝트 구조 - -``` -wing/ -├── frontend/ React 19 + Vite + TypeScript + Tailwind -│ └── src/ -│ ├── App.tsx 메인 (탭 라우팅, 감사 로그) -│ ├── common/ 공통 모듈 (@common/ alias) -│ │ ├── components/ auth/, layer/, layout/, map/, ui/ -│ │ ├── hooks/ useLayers, useSubMenu -│ │ ├── services/ api.ts, authApi.ts, layerService.ts -│ │ ├── store/ authStore, menuStore (Zustand) -│ │ ├── types/ backtrack, boomLine, hns, navigation -│ │ └── utils/ coordinates, geo, sanitize -│ └── tabs/ 탭 단위 패키지 (@tabs/ alias) -│ ├── prediction/ 확산 예측 (OilSpillView, LeftPanel 등) -│ ├── hns/ HNS 분석 (HNSView, HNSSubstanceView 등) -│ ├── rescue/ 구조 시나리오 -│ ├── aerial/ 항공 방제 -│ ├── weather/ 해양 기상 -│ └── ... incidents, board, reports, assets, scat, admin -├── backend/ Express + TypeScript -│ └── src/ -│ ├── server.ts 진입점 + 라우터 등록 -│ ├── auth/ 인증 (JWT, OAuth, 미들웨어) -│ ├── users/ 사용자 관리 -│ ├── roles/ 역할/권한 관리 -│ ├── settings/ 시스템 설정 -│ ├── menus/ 메뉴 설정 -│ ├── audit/ 감사 로그 -│ ├── hns/ HNS 물질 검색 API -│ ├── routes/ 레이어, 시뮬레이션 -│ ├── middleware/ 보안 (입력 살균, rate-limit) -│ └── db/ DB 연결 (wingDb, authDb), seed -├── database/ SQL 스크립트 + 마이그레이션 -├── docs/ 개발 문서 -├── .claude/ 팀 워크플로우 (rules, skills, scripts) -└── .githooks/ Git hooks (pre-commit, commit-msg) -``` - ---- - -## 개발 환경 실행 - -### 사전 요구사항 -- Node.js 20+ (`.node-version`, fnm 사용) -- PostgreSQL 16+ (운영 DB에 직접 연결) - -### 실행 - -```bash -# 백엔드 (터미널 1) -cd backend && npm install && npm run dev # localhost:3001 - -# 프론트엔드 (터미널 2) -cd frontend && npm install && npm run dev # localhost:5173 -``` - -### 빌드/검증 - -```bash -# TypeScript 타입 체크 -cd frontend && npx tsc --noEmit -cd backend && npx tsc --noEmit - -# ESLint -cd frontend && npx eslint . - -# 프로덕션 빌드 -cd frontend && npm run build # dist/ 생성 -cd backend && npm run build # dist/ 생성 -``` - ---- - -## 개발 워크플로우 - -``` -계획 → 브랜치 → 개발 → 커밋/푸시 → develop MR → main PR → 자동 배포 -``` - -### Claude Code 기반 개발 절차 - -| 단계 | 작업 | Claude 스킬 | -|------|------|-------------| -| 1. 계획 | 3개+ 파일 수정 시 Claude가 Plan Mode 진입 | (자동) | -| 2. 브랜치 | `feature/기능명` 으로 develop에서 분기 | - | -| 3. 개발 | Claude가 코드 작성 + 타입/린트 검증 | - | -| 4. 커밋/푸시 | pre-commit 자동 검증 후 푸시 | `/push` | -| 5. develop MR | feature → develop MR 생성 | `/mr` | -| 6. 릴리즈 | develop → main PR 생성 | `/release` | -| 7. 배포 | main 머지 시 Gitea Actions 자동 배포 | - | - -> 상세 워크플로우는 [DEVELOPMENT-GUIDE.md](DEVELOPMENT-GUIDE.md)를 참조하세요. - ---- - -## 문서 안내 - -### 개발 가이드 - -| 문서 | 설명 | 대상 | -|------|------|------| -| [DEVELOPMENT-GUIDE.md](DEVELOPMENT-GUIDE.md) | 개발 워크플로우 전체 흐름 (Plan → Branch → MR → Deploy) | 모든 개발자 | -| [COMMON-GUIDE.md](COMMON-GUIDE.md) | 공통 로직 개발 가이드 (인증, 감사로그, 메뉴, API 통신, 상태 관리) | 탭 개발자 | -| [MENU-TAB-GUIDE.md](MENU-TAB-GUIDE.md) | 새 메뉴 탭 추가 절차 (5단계) | 탭 개발자 | - -### 운영 가이드 - -| 문서 | 설명 | 대상 | -|------|------|------| -| [INSTALL_GUIDE.md](INSTALL_GUIDE.md) | 설치 매뉴얼 (온라인/오프라인, DB 초기화) | 운영/인프라 | -| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | 모든 개발자 | - -### 코드 컨벤션 (.claude/rules/) - -| 규칙 | 설명 | -|------|------| -| `team-policy.md` | 보안/품질 정책 (필수 준수) | -| `git-workflow.md` | 브랜치/커밋/MR 규칙 | -| `code-style.md` | TypeScript/React 코드 스타일 | -| `naming.md` | 네이밍 규칙 | -| `testing.md` | 테스트 규칙 | - ---- - -## 공통 기능 요약 - -개별 탭 개발 시 아래 공통 기능을 활용합니다. -상세 사용법은 [COMMON-GUIDE.md](COMMON-GUIDE.md)를 참조하세요. - -| 기능 | 프론트엔드 | 백엔드 | 상세 | -|------|-----------|--------|------| -| 인증/인가 | `authStore`, `api.ts` (자동 쿠키) | `requireAuth`, `requireRole` | [COMMON-GUIDE.md #1](COMMON-GUIDE.md#1-인증인가) | -| 감사 로그 | 탭 이동 자동 기록 (sendBeacon) | `audit/` 모듈 | [COMMON-GUIDE.md #2](COMMON-GUIDE.md#2-감사-로그-audit-log) | -| 메뉴 시스템 | `menuStore` | `menus/`, `settings/` | [COMMON-GUIDE.md #3](COMMON-GUIDE.md#3-메뉴-시스템) | -| API 통신 | `api.ts` (Axios + 인터셉터) | Express 라우터 | [COMMON-GUIDE.md #4](COMMON-GUIDE.md#4-api-통신-패턴) | -| 상태 관리 | Zustand, TanStack Query | - | [COMMON-GUIDE.md #5](COMMON-GUIDE.md#5-상태-관리) | - ---- - -## Claude Code 스킬 - -| 스킬 | 설명 | -|------|------| -| `/push` | 커밋 + 푸시 (한 번에) | -| `/mr` | 커밋 + 푸시 + develop MR (한 번에) | -| `/release` | develop → main 릴리즈 MR | -| `/create-mr` | MR만 생성 (세부 옵션) | -| `/fix-issue` | Gitea 이슈 분석 + 수정 브랜치 생성 | -| `/sync-team-workflow` | 팀 워크플로우 동기화 | -| `/changelog` | CHANGELOG.md 갱신 | - ---- - -## 환경 변수 - -### 프론트엔드 (.env) -``` -VITE_API_URL=http://localhost:3001/api -VITE_GOOGLE_CLIENT_ID=your-google-client-id -``` - -### 백엔드 (.env) -``` -PORT=3001 -NODE_ENV=development -JWT_SECRET=your-jwt-secret -AUTH_DB_HOST=localhost -AUTH_DB_PORT=5432 -AUTH_DB_NAME=wing_auth -AUTH_DB_USER=wing_auth -AUTH_DB_PASSWORD=WingAuth!2026 -GOOGLE_CLIENT_ID=your-google-client-id -``` - ---- - -## 배포 - -| 항목 | 값 | -|------|---| -| 프론트엔드 | https://wing-demo.gc-si.dev | -| 백엔드 API | https://wing-demo.gc-si.dev/api/ | -| CI/CD | Gitea Actions (main 머지 시 자동 배포) | - -배포 파이프라인 상세는 [DEVELOPMENT-GUIDE.md #7](DEVELOPMENT-GUIDE.md#7-자동-배포)을 참조하세요. - ---- - -## 문서 최신화 규칙 - -공통 기능(인증, 감사로그, 메뉴 시스템, API 통신 등)을 추가/변경할 때: -1. 해당 기능 코드 구현 -2. `docs/COMMON-GUIDE.md` 최신화 (필수) -3. 필요 시 `CLAUDE.md` 프로젝트 구조 갱신 - -매 기능 개발 완료 시: -``` -Claude에게: "memory 파일 최신화해줘" -``` diff --git a/docs/_backup_20260301/ROOT_CLAUDE.md b/docs/_backup_20260301/ROOT_CLAUDE.md deleted file mode 100644 index 42410c7..0000000 --- a/docs/_backup_20260301/ROOT_CLAUDE.md +++ /dev/null @@ -1,123 +0,0 @@ -# WING-OPS (해양 방제 운영 지원 시스템) - -## 프로젝트 개요 -해양 오염 사고 대응을 위한 방제 운영 지원 시스템. -유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공한다. - -- **프로젝트 타입**: react-ts (모노레포) -- **Frontend**: React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3 -- **Backend**: Express 4 + PostgreSQL (pg) + TypeScript -- **DB**: PostgreSQL 16 + PostGIS (wing 운영DB + wing_auth 인증DB) -- **상태관리**: Zustand (클라이언트), TanStack Query (서버) -- **지도**: Leaflet + react-leaflet -- **실시간**: Socket.IO - -## 빌드/실행 - -### Frontend -```bash -cd frontend -npm install -npm run dev # 개발 서버 (Vite, localhost:5173) -npm run build # 프로덕션 빌드 (tsc -b && vite build) -npm run lint # ESLint 검증 -npm run preview # 빌드 미리보기 -``` - -### Backend -```bash -cd backend -npm install -npm run dev # 개발 서버 (tsx watch, localhost:3001) -npm run build # TypeScript 컴파일 (tsc) -npm start # 프로덕션 실행 -npm run db:seed # DB 초기 데이터 -``` - -## 테스트 -테스트 프레임워크 미구성. 향후 Vitest + React Testing Library 도입 예정. - -## Lint/Format -```bash -cd frontend && npx eslint . # ESLint (flat config) -npx prettier --check . # Prettier 검증 -npx prettier --write . # Prettier 자동 수정 -``` - -## 프로젝트 구조 -``` -wing/ -├── frontend/ React 19 + Vite + TypeScript + Tailwind -│ └── src/ -│ ├── App.tsx 메인 (탭 라우팅, 감사 로그 자동 기록) -│ ├── common/ 공통 모듈 (@common/ alias) -│ │ ├── components/ auth/, layer/, layout/, map/, ui/ -│ │ ├── hooks/ useLayers, useSubMenu -│ │ ├── services/ api.ts, authApi.ts, layerService.ts -│ │ ├── store/ authStore, menuStore (Zustand) -│ │ ├── types/ backtrack, boomLine, hns, navigation -│ │ ├── utils/ coordinates, geo, sanitize -│ │ ├── data/ layerData.ts (UI 레이어 트리) -│ │ └── mock/ vesselMockData, backtrackMockData -│ └── tabs/ 탭 단위 패키지 (@tabs/ alias) -│ ├── prediction/ 확산 예측 (OilSpillView, LeftPanel 등) -│ ├── hns/ HNS 분석 (HNSView, HNSSubstanceView 등) -│ ├── rescue/ 구조 시나리오 -│ ├── aerial/ 항공 방제 -│ ├── weather/ 해양 기상 (오버레이, hooks, services) -│ ├── incidents/ 사건/사고 관리 -│ ├── board/ 게시판 -│ ├── reports/ 보고서 -│ ├── assets/ 자산 관리 -│ ├── scat/ Pre-SCAT 조사 -│ └── admin/ 관리자 (사용자/권한/메뉴/설정) -├── backend/ Express + TypeScript -│ └── src/ -│ ├── server.ts 진입점 + 라우터 등록 -│ ├── auth/ 인증 (JWT, OAuth, 미들웨어) -│ ├── users/ 사용자 관리 -│ ├── roles/ 역할/권한 관리 -│ ├── settings/ 시스템 설정 -│ ├── menus/ 메뉴 설정 -│ ├── audit/ 감사 로그 -│ ├── hns/ HNS 물질 검색 API -│ ├── routes/ 레이어, 시뮬레이션 -│ ├── middleware/ 보안 (입력 살균, rate-limit) -│ └── db/ DB 연결 (wingDb, authDb), seed -├── database/ SQL 스크립트 -│ ├── init.sql wing DB 초기 스키마 -│ ├── auth_init.sql wing_auth DB 초기 스키마 -│ └── migration/ 마이그레이션 (001_layer, 002_hns_substance) -├── docs/ 개발 문서 -├── .claude/ 팀 워크플로우 (rules, skills, scripts) -└── .githooks/ Git hooks (pre-commit, commit-msg) -``` - -### Path Alias -- `@common/*` → `src/common/*` (공통 모듈) -- `@tabs/*` → `src/tabs/*` (탭 패키지) - -## 팀 컨벤션 -`.claude/rules/` 디렉토리 참조: -- `team-policy.md` — 보안/품질 정책 -- `git-workflow.md` — 브랜치/커밋/MR 규칙 -- `code-style.md` — TypeScript/React 코드 스타일 -- `naming.md` — 네이밍 규칙 -- `testing.md` — 테스트 규칙 - -## 개발 문서 (`docs/`) -- `docs/README.md` — 프로젝트 개요, 초기 세팅, 워크플로우 요약, 문서 안내 -- `docs/DEVELOPMENT-GUIDE.md` — 개발 워크플로우 전체 흐름 (Plan → Branch → MR → Deploy) -- `docs/COMMON-GUIDE.md` — 공통 로직 개발 가이드 (인증, 감사로그, 메뉴, API 통신, 상태 관리) -- `docs/MENU-TAB-GUIDE.md` — 새 메뉴 탭 추가 절차 (5단계) -- `docs/INSTALL_GUIDE.md` — 설치 매뉴얼 (온라인/오프라인, DB) -- `docs/CHANGELOG.md` — 변경 이력 - -### 문서 최신화 규칙 -- 공통 기능(인증, 감사로그, 메뉴 시스템, API 통신 등)을 추가/변경할 때 반드시 `docs/COMMON-GUIDE.md`를 최신화할 것 -- 개별 탭 개발자는 이 문서를 참조하여 공통 영역과의 연동을 구현 - -## 환경 설정 -- Node.js 20 (`.node-version`, fnm 사용) -- npm registry: Nexus proxy (`.npmrc`) -- Git hooks: `.githooks/` (core.hooksPath 설정됨) diff --git a/docs/_backup_20260301/ROOT_README.md b/docs/_backup_20260301/ROOT_README.md deleted file mode 100644 index aab89dc..0000000 --- a/docs/_backup_20260301/ROOT_README.md +++ /dev/null @@ -1,223 +0,0 @@ -# WING-OPS (해양 방제 운영 지원 시스템) - -해양 오염 사고 대응을 위한 방제 운영 지원 시스템. -유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공합니다. - ---- - -## 1. 시작하기 - -### 1-1. 저장소 복제 - -```bash -git clone https://gitea.gc-si.dev/gc/wing-ops.git -cd wing-ops -``` - -### 1-2. Claude Code 초기화 - -```bash -# Claude Code 세션 열기 -claude - -# 팀 워크플로우 초기화 -/init-project -``` - -`/init-project` 실행 시 자동으로 구성되는 항목: -- `.claude/` 디렉토리 (rules, skills, scripts, settings) -- `.githooks/` (pre-commit, commit-msg 자동 검증) -- Git hooks 경로 설정 (`core.hooksPath`) -- 메모리 디렉토리 초기화 - -### 1-3. 의존성 설치 및 실행 - -```bash -# 백엔드 (터미널 1) -cd backend && npm install && npm run dev # localhost:3001 - -# 프론트엔드 (터미널 2) -cd frontend && npm install && npm run dev # localhost:5173 -``` - -> 사전 요구사항: Node.js 20+ (`.node-version`, fnm 사용), PostgreSQL 16+ (운영 DB 직접 연결) -> -> 상세 설치 절차(오프라인 환경, DB 초기화 등)는 [docs/INSTALL_GUIDE.md](docs/INSTALL_GUIDE.md)를 참조하세요. - ---- - -## 2. 개발 워크플로우 - -``` -계획 → 브랜치 → 개발 → 커밋/푸시 → develop MR → main PR → 자동 배포 -``` - -| 단계 | 작업 | Claude 스킬 | -|------|------|-------------| -| 1. 계획 | 3개+ 파일 수정 시 Claude가 Plan Mode 진입 | (자동) | -| 2. 브랜치 | `feature/기능명` 으로 develop에서 분기 | - | -| 3. 개발 | Claude가 코드 작성 + 타입/린트 검증 | - | -| 4. 커밋/푸시 | pre-commit 자동 검증 후 푸시 | `/push` | -| 5. develop MR | feature → develop MR 생성 | `/mr` | -| 6. 릴리즈 | develop → main PR 생성 | `/release` | -| 7. 배포 | main 머지 시 Gitea Actions 자동 배포 | - | - -> 상세 워크플로우(브랜치 규칙, 커밋 형식, MR 절차, 배포 확인, 실전 예시)는 [docs/DEVELOPMENT-GUIDE.md](docs/DEVELOPMENT-GUIDE.md)를 참조하세요. - ---- - -## 3. 탭 개발 - -개별 탭(기능 화면)을 개발할 때 아래 공통 기능을 활용합니다. - -| 기능 | 프론트엔드 | 백엔드 | 상세 | -|------|-----------|--------|------| -| 인증/인가 | `authStore`, `api.ts` (자동 쿠키) | `requireAuth`, `requireRole` | [COMMON-GUIDE.md #1](docs/COMMON-GUIDE.md#1-인증인가) | -| 감사 로그 | 탭 이동 자동 기록 (sendBeacon) | `audit/` 모듈 | [COMMON-GUIDE.md #2](docs/COMMON-GUIDE.md#2-감사-로그-audit-log) | -| 메뉴 시스템 | `menuStore` | `menus/`, `settings/` | [COMMON-GUIDE.md #3](docs/COMMON-GUIDE.md#3-메뉴-시스템) | -| API 통신 | `api.ts` (Axios + 인터셉터) | Express 라우터 | [COMMON-GUIDE.md #4](docs/COMMON-GUIDE.md#4-api-통신-패턴) | -| 상태 관리 | Zustand, TanStack Query | - | [COMMON-GUIDE.md #5](docs/COMMON-GUIDE.md#5-상태-관리) | - -> 공통 로직 전체 가이드: [docs/COMMON-GUIDE.md](docs/COMMON-GUIDE.md) -> -> 새 메뉴 탭 추가 절차 (5단계): [docs/MENU-TAB-GUIDE.md](docs/MENU-TAB-GUIDE.md) - ---- - -## 4. 프로젝트 구조 - -``` -wing/ -├── frontend/ React 19 + Vite + TypeScript + Tailwind -│ └── src/ -│ ├── App.tsx 메인 (탭 라우팅, 감사 로그) -│ ├── common/ 공통 모듈 (@common/ alias) -│ │ ├── components/ auth/, layer/, layout/, map/, ui/ -│ │ ├── hooks/ useLayers, useSubMenu -│ │ ├── services/ api.ts, authApi.ts, layerService.ts -│ │ ├── store/ authStore, menuStore (Zustand) -│ │ ├── types/ backtrack, boomLine, hns, navigation -│ │ └── utils/ coordinates, geo, sanitize -│ └── tabs/ 탭 단위 패키지 (@tabs/ alias) -│ ├── prediction/ 확산 예측 -│ ├── hns/ HNS 분석 -│ ├── rescue/ 구조 시나리오 -│ ├── aerial/ 항공 방제 -│ ├── weather/ 해양 기상 -│ ├── incidents/ 사건/사고 -│ ├── board/ 게시판 -│ ├── reports/ 보고서 -│ ├── assets/ 자산 관리 -│ ├── scat/ Pre-SCAT -│ └── admin/ 관리자 -├── backend/ Express + TypeScript -│ └── src/ -│ ├── server.ts 진입점 + 라우터 등록 -│ ├── auth/ 인증 (JWT, OAuth, 미들웨어) -│ ├── users/ 사용자 관리 -│ ├── roles/ 역할/권한 관리 -│ ├── settings/ 시스템 설정 -│ ├── menus/ 메뉴 설정 -│ ├── audit/ 감사 로그 -│ ├── hns/ HNS 물질 검색 API -│ ├── routes/ 레이어, 시뮬레이션 -│ ├── middleware/ 보안 (입력 살균, rate-limit) -│ └── db/ DB 연결 (wingDb, authDb), seed -├── database/ SQL 스크립트 + 마이그레이션 -├── docs/ 개발 문서 -├── .claude/ 팀 워크플로우 (rules, skills, scripts) -└── .githooks/ Git hooks (pre-commit, commit-msg) -``` - ---- - -## 5. 기술 스택 - -| 영역 | 기술 | -|------|------| -| Frontend | React 19, Vite 7, TypeScript 5.9, Tailwind CSS 3 | -| Backend | Express 4, TypeScript, PostgreSQL (pg) | -| 상태 관리 | Zustand (클라이언트), TanStack Query (서버) | -| 지도 | Leaflet + react-leaflet | -| 실시간 | Socket.IO | -| 인증 | JWT (HttpOnly Cookie), Google OAuth | -| DB | PostgreSQL 16 + PostGIS (wing 운영DB + wing_auth 인증DB) | -| CI/CD | Gitea Actions | - ---- - -## 6. 문서 안내 - -### 개발 가이드 - -| 문서 | 설명 | 대상 | -|------|------|------| -| [DEVELOPMENT-GUIDE.md](docs/DEVELOPMENT-GUIDE.md) | 개발 워크플로우 전체 흐름 (Plan → Branch → MR → Deploy) | 모든 개발자 | -| [COMMON-GUIDE.md](docs/COMMON-GUIDE.md) | 공통 로직 개발 가이드 (인증, 감사로그, 메뉴, API, 상태 관리) | 탭 개발자 | -| [MENU-TAB-GUIDE.md](docs/MENU-TAB-GUIDE.md) | 새 메뉴 탭 추가 절차 (5단계) | 탭 개발자 | - -### 운영 가이드 - -| 문서 | 설명 | 대상 | -|------|------|------| -| [INSTALL_GUIDE.md](docs/INSTALL_GUIDE.md) | 설치 매뉴얼 (온라인/오프라인, DB 초기화) | 운영/인프라 | -| [CHANGELOG.md](docs/CHANGELOG.md) | 변경 이력 | 모든 개발자 | - -### 코드 컨벤션 (.claude/rules/) - -| 규칙 | 설명 | -|------|------| -| `team-policy.md` | 보안/품질 정책 (필수 준수) | -| `git-workflow.md` | 브랜치/커밋/MR 규칙 | -| `code-style.md` | TypeScript/React 코드 스타일 | -| `naming.md` | 네이밍 규칙 | -| `testing.md` | 테스트 규칙 | - ---- - -## 7. 환경 변수 - -### 프론트엔드 (`frontend/.env`) -``` -VITE_API_URL=http://localhost:3001/api -VITE_GOOGLE_CLIENT_ID=your-google-client-id -``` - -### 백엔드 (`backend/.env`) -``` -PORT=3001 -NODE_ENV=development -JWT_SECRET=your-jwt-secret -AUTH_DB_HOST=localhost -AUTH_DB_PORT=5432 -AUTH_DB_NAME=wing_auth -AUTH_DB_USER=wing_auth -AUTH_DB_PASSWORD=<비밀번호> -GOOGLE_CLIENT_ID=your-google-client-id -``` - ---- - -## 8. 배포 - -| 항목 | 값 | -|------|---| -| 프론트엔드 | https://wing-demo.gc-si.dev | -| 백엔드 API | https://wing-demo.gc-si.dev/api/ | -| CI/CD | Gitea Actions (main 머지 시 자동 배포) | - -배포 파이프라인 상세는 [docs/DEVELOPMENT-GUIDE.md #7](docs/DEVELOPMENT-GUIDE.md#7-자동-배포)을 참조하세요. - ---- - -## 9. Claude Code 스킬 - -| 스킬 | 설명 | -|------|------| -| `/push` | 커밋 + 푸시 (한 번에) | -| `/mr` | 커밋 + 푸시 + develop MR (한 번에) | -| `/release` | develop → main 릴리즈 MR | -| `/create-mr` | MR만 생성 (세부 옵션) | -| `/fix-issue` | Gitea 이슈 분석 + 수정 브랜치 생성 | -| `/sync-team-workflow` | 팀 워크플로우 동기화 | -| `/changelog` | CHANGELOG.md 갱신 |