docs: v1.0.0 버저닝 릴리즈 노트 작성 #65

병합
htlee docs/version-history 에서 develop 로 33 commits 를 머지했습니다 2026-03-03 08:09:49 +09:00
19개의 변경된 파일9548개의 추가작업 그리고 2724개의 파일을 삭제
Showing only changes of commit 6fbb3fc249 - Show all commits

137
CLAUDE.md
파일 보기

@ -1,123 +1,132 @@
# WING-OPS (해양 방제 운영 지원 시스템)
## 프로젝트 개요
해양 오염 사고 대응을 위한 방제 운영 지원 시스템.
유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공한다.
해양 오염 사고 대응 방제 운영 지원 시스템. 유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공한다.
- **프로젝트 타입**: react-ts (모노레포)
- **타입**: react-ts 모노레포 (frontend + backend)
- **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
- **지도**: MapLibre GL JS 5.x + deck.gl 9.x (Leaflet 제거됨)
- **실시간**: Socket.IO
- **인증**: JWT (HttpOnly Cookie) + Google OAuth
- **CI/CD**: Gitea Actions
- **CSS**: Tailwind @layer 아키텍처 (base.css, components.css, wing.css)
- **HTTP 정책**: GET/POST만 사용 (PUT/DELETE/PATCH 금지, 한국 보안취약점 점검 가이드 준수)
- **RBAC**: AUTH_PERM OPER_CD (RCUD), permResolver 2차원 권한 엔진
## 빌드/실행
### 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 # 빌드 미리보기
cd frontend && npm install # Frontend
npm run dev # Vite dev (localhost:5173)
npm run build # tsc -b && vite build
npm run lint # ESLint
cd backend && npm install # Backend
npm run dev # tsx watch (localhost:3001)
npm run build && npm start # 프로덕션
npm run db:seed # DB 초기 데이터
```
### 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 도입 예정).
## 테스트
테스트 프레임워크 미구성. 향후 Vitest + React Testing Library 도입 예정.
## Lint/Format
```bash
cd frontend && npx eslint . # ESLint (flat config)
npx prettier --check . # Prettier 검증
npx prettier --write . # Prettier 자동 수정
cd frontend && npx eslint . # ESLint (flat config)
npx prettier --write . # Prettier 자동 수정
```
## 프로젝트 구조
```
wing/
├── frontend/ React 19 + Vite + TypeScript + Tailwind
├── frontend/ React 19 + Vite 7 + TypeScript + Tailwind
│ └── src/
│ ├── App.tsx 메인 (탭 라우팅, 감사 로그 자동 기록)
│ ├── index.css @tailwind + @import 엔트리포인트
│ ├── common/ 공통 모듈 (@common/ alias)
│ │ ├── components/ auth/, layer/, layout/, map/, ui/
│ │ ├── hooks/ useLayers, useSubMenu
│ │ ├── hooks/ useLayers, useSubMenu, useFeatureTracking
│ │ ├── services/ api.ts, authApi.ts, layerService.ts
│ │ ├── store/ authStore, menuStore (Zustand)
│ │ ├── styles/ base.css, components.css, wing.css
│ │ ├── constants/ featureIds.ts
│ │ ├── types/ backtrack, boomLine, hns, navigation
│ │ ├── utils/ coordinates, geo, sanitize
│ │ ├── data/ layerData.ts (UI 레이어 트리)
│ │ └── mock/ vesselMockData, backtrackMockData
│ │ ├── utils/ coordinates, geo, sanitize, cn.ts
│ │ └── data/ layerData.ts (UI 레이어 트리)
│ └── tabs/ 탭 단위 패키지 (@tabs/ alias)
│ ├── prediction/ 확산 예측 (OilSpillView, LeftPanel 등)
│ ├── hns/ HNS 분석 (HNSView, HNSSubstanceView 등)
│ ├── prediction/ 확산 예측 (OilSpillView, 역추적, 오일붐)
│ ├── hns/ HNS 분석 (시나리오, 물질 DB, 재계산)
│ ├── rescue/ 구조 시나리오
│ ├── aerial/ 항공 방제
│ ├── weather/ 해양 기상 (오버레이, hooks, services)
│ ├── aerial/ 항공 방제 (위성영상, 드론)
│ ├── weather/ 해양 기상 (KHOA API, 오버레이)
│ ├── incidents/ 사건/사고 관리
│ ├── board/ 게시판
│ ├── reports/ 보고서
│ ├── assets/ 자산 관리
│ ├── scat/ Pre-SCAT 조사
│ └── admin/ 관리자 (사용자/권한/메뉴/설정)
│ ├── assets/ 자산 관리 (기관, 장비, 선박보험)
│ ├── scat/ Pre-SCAT 해안조사
│ └── admin/ 관리자 (사용자/역할/권한/메뉴/설정)
├── backend/ Express + TypeScript
│ └── src/
│ ├── server.ts 진입점 + 라우터 등록
│ ├── auth/ 인증 (JWT, OAuth, 미들웨어)
│ ├── users/ 사용자 관리
│ ├── roles/ 역할/권한 관리
│ ├── roles/ 역할/권한 관리 (permResolver)
│ ├── settings/ 시스템 설정
│ ├── menus/ 메뉴 설정
│ ├── audit/ 감사 로그
│ ├── board/ 게시판 CRUD
│ ├── reports/ 보고서 CRUD
│ ├── assets/ 자산 관리 CRUD
│ ├── incidents/ 사건/사고 CRUD
│ ├── scat/ SCAT 조사 CRUD
│ ├── prediction/ 확산 예측 CRUD
│ ├── aerial/ 항공 방제 CRUD
│ ├── rescue/ 구조 시나리오 CRUD
│ ├── hns/ HNS 물질 검색 API
│ ├── routes/ 레이어, 시뮬레이션
│ ├── middleware/ 보안 (입력 살균, rate-limit)
│ ├── middleware/ 보안 (입력 살균, rate-limit, Helmet CORP cross-origin)
│ └── db/ DB 연결 (wingDb, authDb), seed
├── database/ SQL 스크립트
│ ├── init.sql wing DB 초기 스키마
│ ├── auth_init.sql wing_auth DB 초기 스키마
│ └── migration/ 마이그레이션 (001_layer, 002_hns_substance)
├── database/ SQL 스크립트 + 마이그레이션 (001~016)
├── docs/ 개발 문서
├── .claude/ 팀 워크플로우 (rules, skills, scripts)
├── .claude/ 팀 워크플로우 (rules, skills, scripts, agents)
└── .githooks/ Git hooks (pre-commit, commit-msg)
```
### Path Alias
- `@common/*``src/common/*` (공통 모듈)
- `@tabs/*``src/tabs/*` (탭 패키지)
- `@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` — 변경 이력
`.claude/rules/` 디렉토리 참조:
- `team-policy.md` -- 보안/품질 정책
- `git-workflow.md` -- 브랜치/커밋/MR 규칙
- `code-style.md` -- TypeScript/React 코드 스타일
- `naming.md` -- 네이밍 규칙
- `testing.md` -- 테스트 규칙
- `subagent-policy.md` -- 서브에이전트 활용 정책
## 개발 문서 (docs/)
- `docs/README.md` -- 프로젝트 아키텍처 상세
- `docs/DEVELOPMENT-GUIDE.md` -- 개발 워크플로우 전체 흐름
- `docs/COMMON-GUIDE.md` -- 공통 로직 가이드 (인증, 감사로그, 메뉴, API 통신, 상태관리, RBAC, 지도, CSS)
- `docs/MENU-TAB-GUIDE.md` -- 새 메뉴 탭 추가 절차 (5단계)
- `docs/CRUD-API-GUIDE.md` -- CRUD API 개발 가이드 (DB -> 백엔드 -> 프론트 End-to-End)
- `docs/MOCK-TO-API-GUIDE.md` -- Mock -> API 전환 프로세스
- `docs/INSTALL_GUIDE.md` -- 설치 매뉴얼 (온라인/오프라인, DB 초기화)
- `docs/CHANGELOG.md` -- 변경 이력
### 문서 최신화 규칙
- 공통 기능(인증, 감사로그, 메뉴 시스템, API 통신 등)을 추가/변경할 때 반드시 `docs/COMMON-GUIDE.md`를 최신화할 것
- 개별 탭 개발자는 이 문서를 참조하여 공통 영역과의 연동을 구현
- 공통 기능을 추가/변경할 때 반드시 `docs/COMMON-GUIDE.md`를 최신화할 것
- API 인터페이스 변경 시 `memory/api-types.md` 갱신
- 개별 탭 개발자는 공통 가이드를 참조하여 연동 구현
## 환경 설정
- Node.js 20 (`.node-version`, fnm 사용)
- npm registry: Nexus proxy (`.npmrc`)
- Git hooks: `.githooks/` (core.hooksPath 설정됨)

181
README.md
파일 보기

@ -1,167 +1,164 @@
# WING-OPS (해양 방제 운영 지원 시스템)
# WING-OPS
해양 오염 사고 대응을 위한 방제 운영 지원 시스템.
유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공합니다.
유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공다.
---
## 1. 시작하기
### 1-1. 저장소 복제
## 시작하기
```bash
git clone https://gitea.gc-si.dev/gc/wing-ops.git
cd wing-ops
```
# 1. 저장소 복제
git clone https://gitea.gc-si.dev/gc/wing-ops.git && cd wing-ops
### 1-2. Claude Code 초기화
```bash
# Claude Code 세션 열기
# 2. Claude Code 초기화 (.claude/, .githooks/, 메모리 디렉토리 자동 구성)
claude
# 팀 워크플로우 초기화
/init-project
```
`/init-project` 실행 시 자동으로 구성되는 항목:
- `.claude/` 디렉토리 (rules, skills, scripts, settings)
- `.githooks/` (pre-commit, commit-msg 자동 검증)
- Git hooks 경로 설정 (`core.hooksPath`)
- 메모리 디렉토리 초기화
### 1-3. 의존성 설치 및 실행
```bash
# 백엔드 (터미널 1)
# 3. 백엔드 (터미널 1)
cd backend && npm install && npm run dev # localhost:3001
# 프론트엔드 (터미널 2)
# 4. 프론트엔드 (터미널 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)를 참조하세요.
사전 요구사항: Node.js 20+ (`.node-version`, fnm 사용), PostgreSQL 16+ (원격 DB 직접 연결)
상세 설치 절차: [docs/INSTALL_GUIDE.md](docs/INSTALL_GUIDE.md)
### 빌드 및 검증
```bash
cd frontend && npm run build # tsc -b && vite build
cd frontend && npx eslint . # ESLint 검증
cd backend && npm run build # tsc
cd backend && npm run db:seed # DB 초기 데이터
```
---
## 2. 개발 워크플로우
## 개발 워크플로우
```
계획 → 브랜치 → 개발 → 커밋/푸시 → develop MR → main PR → 자동 배포
계획 -> 브랜치 -> 개발 -> 커밋/푸시 -> 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 자동 배포 | - |
| 계획 | 3개+ 파일 수정 시 Plan Mode 자동 진입 | (자동) |
| 브랜치 | `feature/기능명`으로 develop에서 분기 | - |
| 개발 | 코드 작성 + 타입/린트 검증 | - |
| 커밋/푸시 | pre-commit 자동 검증 후 푸시 | `/push` |
| develop MR | feature -> develop MR 생성 | `/mr` |
| 릴리즈 | develop -> main PR 생성 | `/release` |
| 배포 | main 머지 시 Gitea Actions 자동 배포 | - |
> 상세 워크플로우(브랜치 규칙, 커밋 형식, MR 절차, 배포 확인, 실전 예시)는 [docs/DEVELOPMENT-GUIDE.md](docs/DEVELOPMENT-GUIDE.md)를 참조하세요.
상세 워크플로우: [docs/DEVELOPMENT-GUIDE.md](docs/DEVELOPMENT-GUIDE.md)
---
## 3. 탭 개발
## 탭 개발
개별 탭(기능 화면)을 개발할 때 아래 공통 기능을 활용합니다.
11개 탭: prediction, hns, rescue, aerial, weather, incidents, board, reports, assets, scat, admin
| 기능 | 프론트엔드 | 백엔드 | 상세 |
|------|-----------|--------|------|
| 인증/인가 | `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-통신-패턴) |
| API 통신 | `api.ts` (Axios + 인터셉터) | Express 라우터 (GET/POST only) | [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)
- 새 메뉴 탭 추가 (5단계): [docs/MENU-TAB-GUIDE.md](docs/MENU-TAB-GUIDE.md)
- CRUD API 개발: [docs/CRUD-API-GUIDE.md](docs/CRUD-API-GUIDE.md)
- HTTP 정책: GET/POST만 사용 (PUT/DELETE/PATCH 금지, 보안취약점 가이드 준수)
---
## 4. 프로젝트 구조
## 프로젝트 구조
Path Alias: `@common/*` -> `src/common/*`, `@tabs/*` -> `src/tabs/*`
```
wing/
├── frontend/ React 19 + Vite + TypeScript + Tailwind
├── frontend/ React 19 + Vite 7 + TypeScript + Tailwind
│ └── src/
│ ├── App.tsx 메인 (탭 라우팅, 감사 로그)
│ ├── App.tsx 메인 (탭 라우팅, 감사 로그 자동 기록)
│ ├── index.css @tailwind + @import 엔트리포인트
│ ├── common/ 공통 모듈 (@common/ alias)
│ │ ├── components/ auth/, layer/, layout/, map/, ui/
│ │ ├── hooks/ useLayers, useSubMenu
│ │ ├── hooks/ useLayers, useSubMenu, useFeatureTracking
│ │ ├── services/ api.ts, authApi.ts, layerService.ts
│ │ ├── store/ authStore, menuStore (Zustand)
│ │ ├── styles/ base.css, components.css, wing.css
│ │ ├── constants/ featureIds.ts
│ │ ├── types/ backtrack, boomLine, hns, navigation
│ │ └── utils/ coordinates, geo, sanitize
│ │ ├── utils/ coordinates, geo, sanitize, cn.ts
│ │ └── data/ layerData.ts (UI 레이어 트리)
│ └── tabs/ 탭 단위 패키지 (@tabs/ alias)
│ ├── prediction/ 확산 예측
│ ├── hns/ HNS 분석
│ ├── prediction/ 확산 예측 (OilSpillView, 역추적, 오일붐)
│ ├── hns/ HNS 분석 (시나리오, 물질 DB, 재계산)
│ ├── rescue/ 구조 시나리오
│ ├── aerial/ 항공 방제
│ ├── weather/ 해양 기상
│ ├── incidents/ 사건/사고
│ ├── aerial/ 항공 방제 (위성영상, 드론)
│ ├── weather/ 해양 기상 (KHOA API, 오버레이)
│ ├── incidents/ 사건/사고 관리
│ ├── board/ 게시판
│ ├── reports/ 보고서
│ ├── assets/ 자산 관리
│ ├── scat/ Pre-SCAT
│ └── admin/ 관리자
│ ├── assets/ 자산 관리 (기관, 장비, 선박보험)
│ ├── scat/ Pre-SCAT 해안조사
│ └── admin/ 관리자 (사용자/역할/권한/메뉴/설정)
├── backend/ Express + TypeScript
│ └── src/
│ ├── server.ts 진입점 + 라우터 등록
│ ├── auth/ 인증 (JWT, OAuth, 미들웨어)
│ ├── users/ 사용자 관리
│ ├── roles/ 역할/권한 관리
│ ├── settings/ 시스템 설정
│ ├── menus/ 메뉴 설정
│ ├── users/, roles/ 사용자, 역할/권한 (permResolver)
│ ├── settings/, menus/ 시스템 설정, 메뉴 설정
│ ├── audit/ 감사 로그
│ ├── board/, reports/ 게시판, 보고서 CRUD
│ ├── assets/, incidents/, scat/ 자산, 사건/사고, SCAT CRUD
│ ├── prediction/, aerial/, rescue/ 예측, 항공, 구조 CRUD
│ ├── hns/ HNS 물질 검색 API
│ ├── routes/ 레이어, 시뮬레이션
│ ├── middleware/ 보안 (입력 살균, rate-limit)
│ └── db/ DB 연결 (wingDb, authDb), seed
├── database/ SQL 스크립트 + 마이그레이션
├── database/ SQL 스크립트 + 마이그레이션 (001~016)
├── docs/ 개발 문서
├── .claude/ 팀 워크플로우 (rules, skills, scripts)
├── .claude/ 팀 워크플로우 (rules, skills, scripts, agents)
└── .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) |
| Frontend | React 19, Vite 7.3, TypeScript 5.9, Tailwind CSS 3 |
| CSS 아키텍처 | Tailwind @layer (base.css, components.css, wing.css), cn() 유틸리티 |
| 상태 관리 | Zustand (클라이언트), TanStack Query (서버) |
| 지도 | Leaflet + react-leaflet |
| 실시간 | Socket.IO |
| 인증 | JWT (HttpOnly Cookie), Google OAuth |
| 지도 | MapLibre GL JS 5.x + @vis.gl/react-maplibre 8.1 + deck.gl 9.x |
| 실시간 | Socket.IO Client 4.8 |
| UI | lucide-react, @dnd-kit, emoji-mart |
| Backend | Express 4, TypeScript, pg (PostgreSQL) |
| 보안 | Helmet, CORS, Rate-limit, Input sanitization |
| 인증 | JWT (HttpOnly Cookie `WING_SESSION`), bcrypt, Google OAuth |
| DB | PostgreSQL 16 + PostGIS (wing 운영DB + wing_auth 인증DB) |
| CI/CD | Gitea Actions |
| CI/CD | Gitea Actions (.gitea/workflows/deploy.yml) |
---
## 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) | 변경 이력 | 모든 개발자 |
| [docs/README.md](docs/README.md) | 프로젝트 아키텍처 상세 | 모든 개발자 |
| [docs/DEVELOPMENT-GUIDE.md](docs/DEVELOPMENT-GUIDE.md) | 개발 워크플로우 전체 흐름 | 모든 개발자 |
| [docs/COMMON-GUIDE.md](docs/COMMON-GUIDE.md) | 공통 로직 가이드 (인증, 감사로그, 메뉴, API, 상태 관리) | 탭 개발자 |
| [docs/MENU-TAB-GUIDE.md](docs/MENU-TAB-GUIDE.md) | 새 메뉴 탭 추가 절차 (5단계) | 탭 개발자 |
| [docs/CRUD-API-GUIDE.md](docs/CRUD-API-GUIDE.md) | CRUD API 개발 가이드 | 탭 개발자 |
| [docs/MOCK-TO-API-GUIDE.md](docs/MOCK-TO-API-GUIDE.md) | Mock -> API 전환 프로세스 | 탭 개발자 |
| [docs/INSTALL_GUIDE.md](docs/INSTALL_GUIDE.md) | 설치 매뉴얼 (온라인/오프라인, DB 초기화) | 운영/인프라 |
| [docs/CHANGELOG.md](docs/CHANGELOG.md) | 변경 이력 | 모든 개발자 |
### 코드 컨벤션 (.claude/rules/)
@ -172,18 +169,21 @@ wing/
| `code-style.md` | TypeScript/React 코드 스타일 |
| `naming.md` | 네이밍 규칙 |
| `testing.md` | 테스트 규칙 |
| `subagent-policy.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
@ -198,26 +198,27 @@ 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-자동-배포)을 참조하세요.
| Gitea | https://gitea.gc-si.dev/gc/wing-ops |
| 보호 브랜치 | main, develop (MR 필수) |
---
## 9. Claude Code 스킬
## Claude Code 스킬
| 스킬 | 설명 |
|------|------|
| `/push` | 커밋 + 푸시 (한 번에) |
| `/mr` | 커밋 + 푸시 + develop MR (한 번에) |
| `/release` | develop main 릴리즈 MR |
| `/release` | develop -> main 릴리즈 MR |
| `/create-mr` | MR만 생성 (세부 옵션) |
| `/fix-issue` | Gitea 이슈 분석 + 수정 브랜치 생성 |
| `/sync-team-workflow` | 팀 워크플로우 동기화 |
| `/changelog` | CHANGELOG.md 갱신 |
| `/init-project` | 프로젝트 초기 설정 |

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -1,13 +1,63 @@
# WING 해양환경 위기대응 통합시스템 - 설치 매뉴얼
# WING-OPS 설치 매뉴얼
## 1. 필수 소프트웨어
## 목차
| 소프트웨어 | 최소 버전 | 용도 | 다운로드 |
|-----------|----------|------|---------|
| Node.js | v20 이상 (권장 v25) | 프론트엔드/백엔드 실행 | https://nodejs.org |
| npm | v10 이상 | 패키지 관리 | Node.js에 포함 |
1. [시스템 요구사항](#1-시스템-요구사항)
2. [프로젝트 구조](#2-프로젝트-구조)
3. [온라인 설치](#3-온라인-설치)
4. [오프라인 설치](#4-오프라인-설치)
5. [DB 초기화 및 마이그레이션](#5-db-초기화-및-마이그레이션)
6. [개발 서버 실행](#6-개발-서버-실행)
7. [운영 서버 배포](#7-운영-서버-배포)
8. [CI/CD 자동 배포](#8-cicd-자동-배포)
9. [접속 정보 요약](#9-접속-정보-요약)
10. [트러블슈팅](#10-트러블슈팅)
> **오프라인 환경**: 인터넷이 안 되는 망에서는 `node_modules`가 포함된 압축 파일을 사용하세요 (아래 "오프라인 설치" 참고).
---
## 1. 시스템 요구사항
### 필수 소프트웨어
| 소프트웨어 | 최소 버전 | 권장 버전 | 용도 |
|-----------|----------|----------|------|
| Node.js | v20 | v20 LTS | Frontend/Backend 실행 |
| npm | v10 | v10+ | 패키지 관리 (Node.js 포함) |
| PostgreSQL | v15 | v16 | 운영 DB + 인증 DB |
| PostGIS | v3.3 | v3.4 | 공간 데이터 처리 |
| Git | v2.30 | v2.40+ | 소스 코드 관리 |
### 하드웨어 권장 사양
| 항목 | 개발 환경 | 운영 환경 |
|------|----------|----------|
| CPU | 2 Core | 4 Core |
| RAM | 4 GB | 8 GB |
| Disk | 10 GB | 50 GB |
| OS | macOS / Linux / Windows | Rocky Linux 9 / CentOS 9 |
### Node.js 설치 (fnm 권장)
fnm(Fast Node Manager)으로 Node.js 버전을 관리한다. 프로젝트 루트의 `.node-version` 파일에 `20`이 지정되어 있다.
**macOS / Linux:**
```bash
# fnm 설치
curl -fsSL https://fnm.vercel.app/install | bash
# 셸 재시작 후
fnm install 20
fnm use 20
# 확인
node -v # v20.x.x
npm -v # v10.x.x
```
**수동 설치 (fnm 없이):**
https://nodejs.org 에서 Node.js 20 LTS를 다운로드하여 설치한다.
---
@ -15,151 +65,669 @@
```
wing/
├── frontend/ # React + Vite 프론트엔드 (포트 5173)
├── frontend/ React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3
│ ├── src/
│ │ ├── components/ # UI 컴포넌트
│ │ ├── data/ # 정적 데이터
│ │ ├── hooks/ # 커스텀 훅
│ │ ├── types/ # TypeScript 타입 정의
│ │ ├── utils/ # 유틸리티 함수
│ │ └── store/ # 상태관리
│ └── package.json
├── backend/ # Express 백엔드 API (포트 3001)
│ │ ├── App.tsx 메인 (탭 라우팅, 감사 로그 자동 기록)
│ │ ├── common/ 공통 모듈 (@common/ alias)
│ │ │ ├── components/ auth/, layer/, layout/, map/, ui/
│ │ │ ├── hooks/ useLayers, useSubMenu, useAuth
│ │ │ ├── services/ api.ts (Axios), authApi.ts, layerService.ts
│ │ │ ├── store/ authStore, menuStore (Zustand)
│ │ │ ├── types/ backtrack, boomLine, hns, navigation
│ │ │ └── utils/ coordinates, geo, sanitize
│ │ └── tabs/ 탭 단위 패키지 (11개)
│ │ ├── prediction/ 확산 예측
│ │ ├── hns/ HNS 분석
│ │ ├── rescue/ 구조 시나리오
│ │ ├── aerial/ 항공 방제
│ │ ├── weather/ 해양 기상
│ │ ├── incidents/ 사건/사고 관리
│ │ ├── board/ 게시판
│ │ ├── reports/ 보고서
│ │ ├── assets/ 자산 관리
│ │ ├── scat/ Pre-SCAT 조사
│ │ └── admin/ 관리자
│ ├── package.json
│ └── vite.config.ts
├── backend/ Express 4 + TypeScript + PostgreSQL
│ ├── src/
│ │ ├── routes/ # API 라우트
│ │ ├── middleware/ # 미들웨어 (보안 등)
│ │ ├── db/ # DB 연결
│ │ └── server.ts # 서버 엔트리
│ └── package.json
└── database/ # DB 초기화 SQL
├── database_init.sql
└── auth_init.sql
│ │ ├── server.ts 진입점 (보안 미들웨어 + 라우터 등록)
│ │ ├── auth/ 인증 (JWT, Google OAuth)
│ │ ├── users/ 사용자 관리
│ │ ├── roles/ 역할/권한 (RBAC 2차원 권한)
│ │ ├── db/ DB Pool (wingDb, authDb), seed
│ │ ├── middleware/ 보안 (입력 살균, rate-limit)
│ │ └── {도메인}/ 도메인별 모듈 (Router + Service)
│ ├── package.json
│ └── tsconfig.json
├── database/ SQL 스크립트
│ ├── init.sql wing DB 초기 스키마 (PostGIS)
│ ├── auth_init.sql wing_auth DB 초기 스키마
│ └── migration/ 마이그레이션 (001 ~ 016)
├── docs/ 개발 문서
├── .githooks/ Git Hooks (pre-commit, commit-msg)
├── .gitea/workflows/ CI/CD (Gitea Actions)
├── .node-version Node.js 20
└── .npmrc npm 레지스트리 (Nexus 프록시)
```
---
## 3. 온라인 설치 (인터넷 가능한 환경)
## 3. 온라인 설치
### 3-1. 의존성 설치
인터넷이 가능한 환경에서의 설치 절차.
### 3-1. 소스 코드 클론
```bash
# 프론트엔드
cd wing/frontend
git clone https://gitea.gc-si.dev/gc/wing-ops.git wing
cd wing
```
### 3-2. Git Hooks 설정
```bash
# pre-commit (TypeScript + ESLint 검증), commit-msg (Conventional Commits 검증)
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit .githooks/commit-msg
```
### 3-3. 의존성 설치
npm 레지스트리는 프로젝트 루트의 `.npmrc`에 Nexus 프록시로 설정되어 있다.
```bash
# Frontend
cd frontend
npm install
# 백엔드
# Backend
cd ../backend
npm install
```
### 3-2. 데이터베이스 설정
### 3-4. 환경변수 설정
운영 PostgreSQL에 직접 연결합니다. `backend/.env` 파일에서 DB 연결 정보를 설정하세요.
#### Backend (`backend/.env`)
```bash
# backend/.env
AUTH_DB_HOST=<PostgreSQL 호스트>
# ===========================================
# 서버 설정
# ===========================================
PORT=3001
NODE_ENV=development
# ===========================================
# wing DB (운영 데이터 - PostgreSQL + PostGIS)
# ===========================================
WING_DB_HOST=211.208.115.83
WING_DB_PORT=5432
WING_DB_USER=wing
WING_DB_PASS=<비밀번호>
WING_DB_NAME=wing
# ===========================================
# wing_auth DB (인증/권한 - PostgreSQL)
# ===========================================
AUTH_DB_HOST=211.208.115.83
AUTH_DB_PORT=5432
AUTH_DB_NAME=wing_auth
AUTH_DB_USER=wing_auth
AUTH_DB_PASSWORD=<비밀번호>
AUTH_DB_PASS=<비밀번호>
AUTH_DB_NAME=wing_auth
# ===========================================
# JWT 인증
# ===========================================
JWT_SECRET=<랜덤 시크릿 문자열>
JWT_EXPIRES_IN=24h
# ===========================================
# CORS (프론트엔드 출처)
# ===========================================
FRONTEND_URL=http://localhost:5173
# ===========================================
# Google OAuth (선택)
# ===========================================
GOOGLE_CLIENT_ID=<Google OAuth 클라이언트 ID>
```
> 신규 DB 초기화가 필요한 경우 `database/auth_init.sql`을 실행하세요.
### 3-3. 백엔드 실행
#### Frontend (`frontend/.env`)
```bash
cd wing/backend
npm run dev
# 백엔드 API URL
VITE_API_URL=http://localhost:3001/api
# Google OAuth (선택)
VITE_GOOGLE_CLIENT_ID=<Google OAuth 클라이언트 ID>
# 공공데이터 API 키 (해양기상, 선택)
VITE_DATA_GO_KR_API_KEY=<data.go.kr API >
VITE_WEATHER_API_KEY=<기상 API >
```
`http://localhost:3001` 에서 API 서버 시작
> `.env` 파일은 `.gitignore`에 포함된다. 실제 값은 팀 내부에서 공유한다.
### 3-4. 프론트엔드 실행
### 3-5. 실행
```bash
cd wing/frontend
# 터미널 1: 백엔드
cd backend
npm run dev
# 출력: 서버가 포트 3001에서 실행 중입니다.
# 터미널 2: 프론트엔드
cd frontend
npm run dev
# 출력: VITE vX.X.X ready in XXXms
# -> Local: http://localhost:5173/
```
`http://localhost:5173` 에서 웹 앱 시작
브라우저에서 http://localhost:5173 접속하여 확인한다.
---
## 4. 오프라인 설치 (폐쇄망/다른 망)
## 4. 오프라인 설치
인터넷이 안 되는 환경에서는 `npm install`이 불가능합니다.
이 경우 **node_modules 포함 압축 파일**을 사용하세요.
인터넷이 불가능한 폐쇄망 환경에서의 설치 절차.
### 4-1. 압축 해제
### 4-1. 온라인 환경에서 패키지 준비
인터넷이 되는 PC에서 다음을 실행한다:
```bash
# wing_full.tar.gz 파일을 작업 폴더에 복사한 뒤:
# 1. 프로젝트 클론 및 의존성 설치
git clone https://gitea.gc-si.dev/gc/wing-ops.git wing
cd wing/frontend && npm install
cd ../backend && npm install
# 2. 전체 프로젝트 압축 (node_modules 포함)
cd ../..
tar -czf wing_full.tar.gz wing/
```
### 4-2. Node.js 오프라인 설치 파일 준비
대상 OS에 맞는 설치 파일을 다운로드한다:
| OS | 파일 | 다운로드 |
|----|------|---------|
| Linux (x64) | `node-v20.x.x-linux-x64.tar.xz` | https://nodejs.org/dist/v20.x.x/ |
| macOS (arm64) | `node-v20.x.x-darwin-arm64.tar.gz` | https://nodejs.org/dist/v20.x.x/ |
| macOS (x64) | `node-v20.x.x-darwin-x64.tar.gz` | https://nodejs.org/dist/v20.x.x/ |
| Windows | `node-v20.x.x-x64.msi` | https://nodejs.org/dist/v20.x.x/ |
### 4-3. 대상 서버에 설치
```bash
# 1. Node.js 설치 (Linux 예시)
tar -xJf node-v20.x.x-linux-x64.tar.xz
export PATH=$PWD/node-v20.x.x-linux-x64/bin:$PATH
# .bashrc 또는 .profile에 PATH 영구 등록
# 2. 프로젝트 압축 해제
tar -xzf wing_full.tar.gz
cd wing
# 3. 환경변수 설정 (3-4 참조)
vi backend/.env
vi frontend/.env
# 4. 실행 (node_modules가 이미 포함되어 있으므로 npm install 불필요)
cd backend && npm run dev # 터미널 1
cd frontend && npm run dev # 터미널 2
```
### 4-2. Node.js 설치
### 4-4. 오프라인 환경 주의사항
대상 PC에 Node.js가 없으면 오프라인 설치 파일(.msi 또는 .pkg)을 미리 준비하여 설치합니다.
- `npm install` 불가하므로 패키지 추가 시 온라인 환경에서 설치 후 재배포해야 한다.
- DB는 대상 환경에서 접근 가능한 PostgreSQL을 사용해야 한다.
- 공공데이터 API (기상, 해양) 키가 필요한 기능은 외부 네트워크 접근이 필요하다.
- Windows: `node-v25.x.x-x64.msi`
- macOS: `node-v25.x.x.pkg`
---
### 4-3. DB 연결 설정
## 5. DB 초기화 및 마이그레이션
`backend/.env` 파일에서 연결 가능한 PostgreSQL 정보를 설정합니다.
### 5-1. DB 구성
### 4-4. 실행
프로젝트는 동일 PostgreSQL 서버에 2개의 데이터베이스를 사용한다:
node_modules가 이미 포함되어 있으므로 바로 실행 가능합니다.
| DB | 용도 | 확장 |
|----|------|------|
| `wing` | 운영 데이터 (사고, 예측, 자산 등) | PostGIS |
| `wing_auth` | 인증/권한 (사용자, 역할, 메뉴) | uuid-ossp, pgcrypto |
### 5-2. 신규 DB 초기화
PostgreSQL에 최초 설치 시 아래 순서로 실행한다.
```bash
# 터미널 1 - 백엔드
cd wing/backend
npm run dev
# 1. wing DB 초기화 (PostgreSQL superuser로 실행)
psql -U postgres -f database/init.sql
# 터미널 2 - 프론트엔드
cd wing/frontend
npm run dev
# 2. wing_auth DB 초기화
psql -U postgres -f database/auth_init.sql
```
`init.sql`에 포함된 내용:
- 사용자(wing) 및 데이터베이스(wing) 생성
- PostGIS 확장 설치
- 공통코드, 시뮬레이션, 사고, 예측 등 핵심 테이블 생성
`auth_init.sql`에 포함된 내용:
- 사용자(wing_auth) 및 데이터베이스(wing_auth) 생성
- uuid-ossp, pgcrypto 확장 설치
- 조직, 역할, 사용자, 권한, 메뉴, 설정, 감사로그 테이블 생성
### 5-3. 마이그레이션 적용
초기 스키마 이후 추가된 변경 사항은 `database/migration/` 디렉토리에 순번대로 관리된다.
```
database/migration/
├── 001_layer_table.sql 레이어 테이블
├── 002_hns_substance.sql HNS 물질 데이터
├── 003_perm_tree.sql 권한 트리
├── 004_oper_cd.sql 오퍼레이션 코드
├── 005_db_consolidation.sql DB 통합
├── 006_board.sql 게시판
├── 007_reports.sql 보고서
├── 008_assets.sql 자산
├── 008_assets_seed.sql 자산 시드 데이터
├── 009_incidents.sql 사건/사고
├── 010_postgis_geom.sql PostGIS 지오메트리
├── 011_scat.sql SCAT 조사
├── 012_board_ext.sql 게시판 확장
├── 013_hns_analysis.sql HNS 분석
├── 014_prediction.sql 확산 예측
├── 015_aerial.sql 항공 방제
└── 016_rescue.sql 구조 시나리오
```
**마이그레이션 실행 (수동):**
```bash
# wing DB에 순번대로 적용
psql -h <호스트> -p 5432 -U wing -d wing -f database/migration/001_layer_table.sql
psql -h <호스트> -p 5432 -U wing -d wing -f database/migration/002_hns_substance.sql
# ... 순번대로 계속
```
> 이미 적용된 마이그레이션을 다시 실행해도 대부분 `IF NOT EXISTS` 조건이 포함되어 있어 안전하다.
### 5-4. 시드 데이터
초기 데이터(관리자 계정, 공통코드 등)를 입력한다:
```bash
cd backend
npm run db:seed
```
---
## 5. 접속 정보 요약
## 6. 개발 서버 실행
| 서비스 | URL | 비고 |
|--------|-----|------|
| 프론트엔드 (WING) | http://localhost:5173 | Vite dev server |
| 백엔드 API | http://localhost:3001 | Express |
| PostgreSQL | 운영 DB 직접 연결 | `.env` 설정 참조 |
### 6-1. 명령어 요약
---
**Frontend:**
## 6. 주요 명령어
| 명령어 | 설명 |
|--------|------|
| `npm run dev` | Vite 개발 서버 (localhost:5173, HMR 지원) |
| `npm run build` | 프로덕션 빌드 (`tsc -b && vite build` -> `dist/`) |
| `npm run lint` | ESLint 검증 |
| `npm run preview` | 빌드 결과 미리보기 |
**Backend:**
| 명령어 | 설명 |
|--------|------|
| `npm run dev` | tsx watch 개발 서버 (localhost:3001, 파일 변경 시 자동 재시작) |
| `npm run build` | TypeScript 컴파일 (`tsc` -> `dist/`) |
| `npm start` | 프로덕션 실행 (`node dist/server.js`) |
| `npm run db:seed` | DB 시드 데이터 입력 |
### 6-2. 개발 서버 시작
```bash
# 프론트엔드 빌드 (배포용)
cd frontend && npm run build # dist/ 폴더에 정적 파일 생성
# 터미널 1: 백엔드 (먼저 실행)
cd backend
npm run dev
# 서버가 포트 3001에서 실행 중입니다.
# wing DB 연결 성공 (211.208.115.83:5432/wing)
# 백엔드 빌드
cd backend && npm run build # dist/ 폴더에 JS 생성
# 터미널 2: 프론트엔드
cd frontend
npm run dev
# VITE v7.x.x ready in XXXms
# -> Local: http://localhost:5173/
```
# DB 시드 데이터 입력
cd backend && npm run db:seed
### 6-3. 검증 명령어
```bash
# TypeScript 타입 체크
cd frontend && npx tsc --noEmit
cd backend && npx tsc --noEmit
# ESLint
cd frontend && npx eslint src/
# Prettier
cd frontend && npx prettier --check src/
cd frontend && npx prettier --write src/ # 자동 수정
```
---
## 7. 트러블슈팅
## 7. 운영 서버 배포
| 증상 | 해결 |
### 7-1. 서버 환경 준비
운영 서버(Rocky Linux 9 기준)에 다음을 설치한다:
```bash
# Node.js 20 설치 (fnm 또는 직접 설치)
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc
fnm install 20
fnm use 20
# PostgreSQL 16 + PostGIS 설치 (dnf 또는 직접 설치)
# 이미 설치된 경우 생략
```
### 7-2. 수동 배포 절차
#### Frontend
```bash
cd frontend
# 의존성 설치
npm ci
# 환경변수 설정 (빌드 시 Vite가 주입)
export VITE_API_URL=/api
export VITE_GOOGLE_CLIENT_ID=<클라이언트ID>
export VITE_DATA_GO_KR_API_KEY=<>
export VITE_WEATHER_API_KEY=<>
# 프로덕션 빌드
npx vite build
# 빌드 결과를 웹 서버 디렉토리로 복사
cp -r dist/* /deploy/wing-demo/
```
#### Backend
```bash
cd backend
# 의존성 설치
npm ci
# TypeScript 컴파일
npx tsc
# 프로덕션 의존성만 유지 (devDependencies 제거)
npm prune --omit=dev
# 배포 디렉토리로 복사
mkdir -p /deploy/wing-demo-backend/dist
cp -r dist/* /deploy/wing-demo-backend/dist/
cp -r node_modules /deploy/wing-demo-backend/
cp package.json /deploy/wing-demo-backend/
```
### 7-3. systemd 서비스 등록
Backend를 systemd 서비스로 등록하여 자동 시작/재시작을 설정한다.
**서비스 파일 생성** (`/etc/systemd/system/wing-demo-api.service`):
```ini
[Unit]
Description=WING-OPS Backend API
After=network.target postgresql.service
[Service]
Type=simple
User=deploy
WorkingDirectory=/deploy/wing-demo-backend
ExecStart=/home/deploy/.local/share/fnm/aliases/default/bin/node dist/server.js
Restart=on-failure
RestartSec=5
# 환경변수
Environment=NODE_ENV=production
Environment=PORT=3001
Environment=WING_DB_HOST=211.208.115.83
Environment=WING_DB_PORT=5432
Environment=WING_DB_USER=wing
Environment=WING_DB_PASS=<비밀번호>
Environment=WING_DB_NAME=wing
Environment=AUTH_DB_HOST=211.208.115.83
Environment=AUTH_DB_PORT=5432
Environment=AUTH_DB_USER=wing_auth
Environment=AUTH_DB_PASS=<비밀번호>
Environment=AUTH_DB_NAME=wing_auth
Environment=JWT_SECRET=<시크릿>
Environment=JWT_EXPIRES_IN=24h
Environment=FRONTEND_URL=https://wing-demo.gc-si.dev
Environment=GOOGLE_CLIENT_ID=<클라이언트ID>
[Install]
WantedBy=multi-user.target
```
**서비스 등록 및 시작:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable wing-demo-api
sudo systemctl start wing-demo-api
# 상태 확인
sudo systemctl status wing-demo-api
# 로그 확인
sudo journalctl -u wing-demo-api -f
```
### 7-4. 리버스 프록시 (Nginx)
Frontend 정적 파일과 Backend API를 하나의 도메인으로 서비스한다.
```nginx
server {
listen 443 ssl;
server_name wing-demo.gc-si.dev;
ssl_certificate /etc/ssl/certs/wing-demo.crt;
ssl_certificate_key /etc/ssl/private/wing-demo.key;
# Frontend 정적 파일
root /deploy/wing-demo;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Backend API 프록시
location /api/ {
proxy_pass http://127.0.0.1:3001/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 헬스 체크
location /health {
proxy_pass http://127.0.0.1:3001/health;
}
}
# HTTP -> HTTPS 리다이렉트
server {
listen 80;
server_name wing-demo.gc-si.dev;
return 301 https://$host$request_uri;
}
```
---
## 8. CI/CD 자동 배포
### 8-1. Gitea Actions 파이프라인
main 브랜치에 push(머지)되면 `.gitea/workflows/deploy.yml`이 자동 실행된다.
```
main 브랜치 push
|
[Checkout] 소스 코드 체크아웃
|
[Setup Node.js] Node.js 24 설정
|
[Configure npm] Nexus 프록시 레지스트리 설정
|
[Frontend Build] npm ci -> vite build -> /deploy/wing-demo/
|
[Backend Build] npm ci -> tsc -> npm prune --omit=dev
| -> /deploy/wing-demo-backend/
|
[Deploy Trigger] .deploy-trigger 파일 생성
-> cron/watchdog이 감지 -> 서비스 재시작
```
### 8-2. Gitea Secrets 설정
CI/CD에서 사용하는 시크릿을 Gitea에 등록해야 한다:
**등록 경로:** Settings -> Actions -> Secrets -> Add Secret
| Secret 이름 | 용도 |
|-------------|------|
| `NEXUS_NPM_AUTH` | npm Nexus 프록시 인증 토큰 |
| `GOOGLE_CLIENT_ID` | Google OAuth 클라이언트 ID |
| `DATA_GO_KR_API_KEY` | 공공데이터 API 키 |
| `WEATHER_API_KEY` | 기상 API 키 |
### 8-3. 배포 확인
```bash
# 프론트엔드 응답 확인
curl -s -o /dev/null -w '%{http_code}' https://wing-demo.gc-si.dev/
# 기대: 200
# 백엔드 헬스 체크
curl -s https://wing-demo.gc-si.dev/api/health
# 기대: {"status":"ok"}
# 백엔드 API 정보
curl -s https://wing-demo.gc-si.dev/api/
# 기대: {"name":"WING Backend API","version":"1.0.0","status":"running"}
```
---
## 9. 접속 정보 요약
### 개발 환경
| 서비스 | URL | 포트 |
|--------|-----|------|
| Frontend (Vite dev) | http://localhost:5173 | 5173 |
| Backend API | http://localhost:3001 | 3001 |
| Backend 헬스 체크 | http://localhost:3001/health | 3001 |
| PostgreSQL (wing) | 211.208.115.83:5432/wing | 5432 |
| PostgreSQL (wing_auth) | 211.208.115.83:5432/wing_auth | 5432 |
### 운영 환경
| 서비스 | URL |
|--------|-----|
| Frontend | https://wing-demo.gc-si.dev |
| Backend API | https://wing-demo.gc-si.dev/api/ |
| 헬스 체크 | https://wing-demo.gc-si.dev/api/health |
| Gitea | https://gitea.gc-si.dev/gc/wing-ops |
### API 엔드포인트
| 경로 | 용도 |
|------|------|
| `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 포함 압축본 사용 |
| `GET /` | API 정보 |
| `GET /health` | 헬스 체크 |
| `POST /api/auth/login` | 로그인 |
| `POST /api/auth/logout` | 로그아웃 |
| `GET /api/auth/me` | 현재 사용자 |
| `GET /api/users` | 사용자 목록 |
| `GET /api/roles` | 역할 목록 |
| `GET /api/menus` | 메뉴 목록 |
| `GET /api/settings` | 시스템 설정 |
| `GET /api/audit` | 감사 로그 |
| `GET /api/board` | 게시판 |
| `GET /api/layers` | 레이어 |
| `GET /api/simulation` | 시뮬레이션 |
| `GET /api/hns` | HNS 물질 |
| `GET /api/reports` | 보고서 |
| `GET /api/assets` | 자산 |
| `GET /api/incidents` | 사건/사고 |
| `GET /api/scat` | SCAT 조사 |
| `GET /api/prediction` | 확산 예측 |
| `GET /api/aerial` | 항공 방제 |
| `GET /api/rescue` | 구조 시나리오 |
---
## 10. 트러블슈팅
### 의존성 설치
| 증상 | 원인 | 해결 |
|------|------|------|
| `npm install` 실패 (ENETUNREACH) | Nexus 프록시 접근 불가 | `.npmrc`의 registry URL 확인, VPN/네트워크 상태 확인 |
| `npm install` 실패 (EAUTH) | Nexus 인증 토큰 만료 | `.npmrc``_auth` 값 갱신 |
| `npm install` 실패 (peer dependency) | 패키지 버전 충돌 | `npm install --legacy-peer-deps` 시도 |
| `MODULE_NOT_FOUND` | node_modules 누락/손상 | `rm -rf node_modules package-lock.json && npm install` |
### 서버 실행
| 증상 | 원인 | 해결 |
|------|------|------|
| 포트 5173/3001 충돌 | 이미 다른 프로세스가 사용 중 | `lsof -i :5173` 또는 `lsof -i :3001`로 PID 확인 후 `kill -9 <PID>` |
| 백엔드 시작 시 DB 연결 실패 | DB 접속 정보 오류 또는 네트워크 | `backend/.env`의 DB 호스트/포트/사용자/비밀번호 확인, `nc -zv <호스트> 5432`로 네트워크 확인 |
| 프론트엔드에서 API 호출 CORS 에러 | 백엔드 CORS 설정 불일치 | `backend/.env``FRONTEND_URL`이 프론트엔드 URL과 일치하는지 확인 |
| `env: tsx: No such file or directory` | tsx 미설치 | `cd backend && npm install` 재실행 |
### DB
| 증상 | 원인 | 해결 |
|------|------|------|
| `relation "XXX" does not exist` | 테이블 미생성 | `database/init.sql` 또는 해당 마이그레이션 SQL 실행 |
| PostGIS 함수 에러 | PostGIS 확장 미설치 | `psql -U postgres -d wing -c "CREATE EXTENSION IF NOT EXISTS postgis"` |
| 인코딩 문제 (한글 깨짐) | DB 인코딩 설정 | DB 생성 시 `ENCODING='UTF8' LC_COLLATE='ko_KR.UTF-8'` 지정 |
| seed 실패 | 이미 데이터 존재 또는 FK 제약 | 에러 메시지 확인 후 해당 테이블 데이터 정리 |
### Git Hooks
| 증상 | 원인 | 해결 |
|------|------|------|
| pre-commit이 실행되지 않음 | hooksPath 미설정 | `git config core.hooksPath .githooks` |
| pre-commit permission denied | 실행 권한 없음 | `chmod +x .githooks/pre-commit .githooks/commit-msg` |
| TypeScript 에러로 커밋 차단 | 타입 에러 존재 | `npx tsc --noEmit`으로 에러 확인 후 수정 |
| commit-msg 형식 오류 | Conventional Commits 미준수 | `type(scope): subject` 형식 준수 (type: feat, fix, docs 등) |
### 빌드
| 증상 | 원인 | 해결 |
|------|------|------|
| `vite build` 메모리 부족 | Node.js 힙 메모리 부족 | `export NODE_OPTIONS=--max-old-space-size=4096` 후 재시도 |
| `tsc` 타입 에러 | TypeScript strict 모드 위반 | `npx tsc --noEmit`으로 에러 목록 확인 후 수정 |
| 빌드 후 라우팅 404 | SPA 서버 설정 누락 | Nginx `try_files $uri $uri/ /index.html` 설정 확인 |

파일 보기

@ -1,16 +1,23 @@
# WING 메뉴 탭 추가 가이드
# 메뉴 탭 추가 가이드
새로운 메뉴 탭을 추가할 때 필요한 절차를 설명합니다.
새로운 메뉴 탭을 추가하는 전체 절차를 5단계로 설명한다.
board 탭을 기준 템플릿으로 사용하며, 각 단계별 실제 코드 예시를 제공한다.
## 메뉴 시스템 구조
> **소요 시간**: 약 20~30분 (기본 CRUD 탭 기준)
---
## 메뉴 시스템 아키텍처
```
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] AUTH_SETTING (menu.config JSON)
|
v GET /api/menus
[Backend] settingsService.ts (DEFAULT_MENU_CONFIG, VALID_MENU_IDS)
|
v API
[Frontend] menuStore.ts --> TopBar.tsx (탭 렌더링, enabled && hasPermission 필터링)
--> App.tsx (renderView 라우팅)
```
- **DB**가 메뉴 정의의 단일 소스 (id, label, icon, enabled, order)
@ -18,177 +25,648 @@ Frontend: menuStore.ts → TopBar.tsx (탭 렌더링)
- **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/{TabName}View.tsx` | 뷰 컴포넌트 생성 |
| | `frontend/src/tabs/{탭명}/services/{tabName}Api.ts` | API 서비스 생성 |
| | `frontend/src/tabs/{탭명}/index.ts` | re-export |
| **Step 2** | `frontend/src/common/types/navigation.ts` | MainTab 타입 추가 |
| | `frontend/src/App.tsx` | import + renderView case 추가 |
| | `frontend/src/common/hooks/useSubMenu.ts` | 서브메뉴 설정 (서브탭이 있는 경우) |
| **Step 3** | `frontend/src/common/constants/featureIds.ts` | FEATURE_ID 등록 |
| **Step 4** | `backend/src/{도메인}/{domain}Router.ts` | 라우터 생성 |
| | `backend/src/{도메인}/{domain}Service.ts` | 서비스 생성 |
| **Step 5** | `backend/src/server.ts` | 라우트 등록 |
| | `backend/src/settings/settingsService.ts` | DEFAULT_MENU_CONFIG 추가 |
| | `database/auth_init.sql` | menu.config 초기 JSON 추가 |
| | `database/migration/NNN_{domain}.sql` | DB 마이그레이션 |
## Step 1: 뷰 컴포넌트 생성
---
`frontend/src/tabs/{탭명}/components/` 에 새 뷰 컴포넌트를 생성합니다.
## Step 1: 프론트엔드 탭 패키지 생성
### 1-1. 디렉토리 구조
```
frontend/src/tabs/{탭명}/
components/
{TabName}View.tsx # 메인 뷰 컴포넌트
services/
{tabName}Api.ts # API 서비스
index.ts # re-export
```
### 1-2. 뷰 컴포넌트 (보일러플레이트)
서브탭이 **없는** 간단한 탭:
```tsx
// frontend/src/tabs/monitoring/components/MonitoringView.tsx
export function MonitoringView() {
return (
<div className="flex flex-1 overflow-hidden bg-bg-0">
<div className="flex-1 flex flex-col overflow-hidden p-6">
<h1 className="text-lg font-bold text-text-1 font-korean">실시간 모니터링</h1>
{/* 뷰 콘텐츠 */}
<div className="flex flex-1 overflow-hidden">
<div className="flex-1 relative overflow-hidden">
<div className="flex flex-col h-full bg-bg-0">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-border bg-bg-1">
<div className="text-sm font-bold text-text-1">실시간 모니터링</div>
</div>
{/* 본문 */}
<div className="flex-1 overflow-auto px-8 py-6">
<p className="text-text-3 text-sm">준비 중입니다.</p>
</div>
</div>
</div>
</div>
)
);
}
```
`index.ts`에서 re-export합니다:
서브탭이 **있는** 탭 (board 패턴):
```tsx
// frontend/src/tabs/monitoring/index.ts
export { MonitoringView } from './components/MonitoringView'
// frontend/src/tabs/monitoring/components/MonitoringView.tsx
import { useSubMenu } from '@common/hooks/useSubMenu';
export function MonitoringView() {
const { activeSubTab } = useSubMenu('monitoring');
const renderContent = () => {
switch (activeSubTab) {
case 'dashboard':
return <div>대시보드 컨텐츠</div>;
case 'alerts':
return <div>알림 컨텐츠</div>;
default:
return <div>준비 중입니다.</div>;
}
};
return (
<div className="flex flex-1 overflow-hidden">
<div className="flex-1 relative overflow-hidden">
{renderContent()}
</div>
</div>
);
}
```
기존 탭(`@tabs/prediction`, `@tabs/weather` 등)의 레이아웃 패턴을 참고하세요.
공통 모듈은 `@common/` alias로 import합니다.
### 1-3. API 서비스 (보일러플레이트)
## Step 2: App.tsx 탭 등록
```ts
// frontend/src/tabs/monitoring/services/monitoringApi.ts
3가지를 수정합니다.
import { api } from '@common/services/api';
// ============================================================
// 인터페이스
// ============================================================
export interface MonitoringItem {
sn: number;
title: string;
status: string;
regDtm: string;
}
export interface MonitoringListResponse {
items: MonitoringItem[];
totalCount: number;
page: number;
size: number;
}
export interface MonitoringListParams {
search?: string;
page?: number;
size?: number;
}
export interface CreateMonitoringInput {
title: string;
status?: string;
}
// ============================================================
// API 함수
// ============================================================
export async function fetchMonitoringList(
params?: MonitoringListParams,
): Promise<MonitoringListResponse> {
const response = await api.get<MonitoringListResponse>('/monitoring', { params });
return response.data;
}
export async function fetchMonitoringDetail(sn: number): Promise<MonitoringItem> {
const response = await api.get<MonitoringItem>(`/monitoring/${sn}`);
return response.data;
}
export async function createMonitoring(input: CreateMonitoringInput): Promise<{ sn: number }> {
const response = await api.post<{ sn: number }>('/monitoring', input);
return response.data;
}
```
### 1-4. index.ts (re-export)
```ts
// frontend/src/tabs/monitoring/index.ts
export { MonitoringView } from './components/MonitoringView';
```
> **참고**: 기존 탭의 index.ts 패턴과 동일하다. 모든 탭은 index.ts에서 메인 뷰만 export한다.
---
## Step 2: navigation.ts에 MainTab 추가 + App.tsx 라우팅
### 2-1. MainTab 타입에 ID 추가
```tsx
// frontend/src/App.tsx (line 20)
```ts
// frontend/src/common/types/navigation.ts
// Before
export type MainTab = 'prediction' | 'hns' | ... | 'admin'
export type MainTab = 'prediction' | 'hns' | 'rescue' | ... | 'admin';
// After
export type MainTab = 'prediction' | 'hns' | ... | 'monitoring' | 'admin'
// After (새 탭 ID를 admin 앞에 추가)
export type MainTab = 'prediction' | 'hns' | 'rescue' | ... | 'monitoring' | 'admin';
```
### 2-2. 뷰 컴포넌트 import
### 2-2. App.tsx에 import + renderView case 추가
```tsx
import { MonitoringView } from '@tabs/monitoring'
```
// frontend/src/App.tsx
### 2-3. renderView switch에 case 추가
// 1. import 추가
import { MonitoringView } from '@tabs/monitoring';
```tsx
// 2. renderView switch에 case 추가
const renderView = () => {
switch (activeMainTab) {
// ... 기존 case들 ...
case 'monitoring':
return <MonitoringView />
// ...
return <MonitoringView />;
// admin은 항상 마지막
case 'admin':
return <AdminView />;
default:
return <div className="flex items-center justify-center h-full text-text-3">준비 중입니다...</div>;
}
};
```
### 2-3. 서브메뉴 설정 (서브탭이 있는 경우)
서브탭이 있다면 `useSubMenu.ts`에 3곳을 수정한다:
```ts
// frontend/src/common/hooks/useSubMenu.ts
// 1. subMenuConfigs 에 서브탭 배열 추가
const subMenuConfigs: Record<MainTab, SubMenuItem[] | null> = {
// ... 기존 설정 ...
monitoring: [
{ id: 'dashboard', label: '대시보드', icon: '📊' },
{ id: 'alerts', label: '알림 관리', icon: '🔔' },
],
};
// 2. subMenuState 에 기본 서브탭 추가
const subMenuState: Record<MainTab, string> = {
// ... 기존 상태 ...
monitoring: 'dashboard',
};
```
서브탭이 **없으면** `null`과 빈 문자열을 설정한다:
```ts
monitoring: null, // subMenuConfigs
monitoring: '', // subMenuState
```
---
## Step 3: featureIds.ts에 FEATURE_ID 등록
FEATURE_ID는 RBAC 권한 검사와 감사 로그에 사용된다.
형식: `'{메인탭}:{서브탭}'`
```ts
// frontend/src/common/constants/featureIds.ts
export const FEATURE_IDS = {
// ... 기존 항목 ...
// monitoring
'monitoring:dashboard': '모니터링 대시보드',
'monitoring:alerts': '알림 관리',
} as const;
```
> **동기화 필수**: 여기에 등록한 키는 백엔드의 `AUTH_PERM.RSRC_CD`와 일치해야 한다.
> 서브탭이 없는 탭은 `'{탭명}:main'` 형태로 하나만 등록한다.
---
## Step 4: 백엔드 모듈 생성
### 4-1. 디렉토리 구조
```
backend/src/{도메인}/
{domain}Router.ts # Express 라우터 (요청 파싱, 응답 포맷)
{domain}Service.ts # 비즈니스 로직 + DB 쿼리
```
### 4-2. 라우터 (보일러플레이트)
```ts
// backend/src/monitoring/monitoringRouter.ts
import { Router } from 'express';
import { requireAuth, requirePermission } from '../auth/authMiddleware.js';
import { AuthError } from '../auth/authService.js';
import { listItems, getItem, createItem } from './monitoringService.js';
const router = Router();
// GET /api/monitoring -- 목록 조회
router.get('/', requireAuth, requirePermission('monitoring', 'READ'), async (req, res) => {
try {
const { search, page, size } = req.query;
const result = await listItems({
search: search as string | undefined,
page: page ? parseInt(page as string, 10) : undefined,
size: size ? parseInt(size as string, 10) : undefined,
});
res.json(result);
} catch (err) {
console.error('[monitoring] 목록 조회 오류:', err);
res.status(500).json({ error: '목록 조회 중 오류가 발생했습니다.' });
}
});
// GET /api/monitoring/:sn -- 상세 조회
router.get('/:sn', requireAuth, requirePermission('monitoring', 'READ'), async (req, res) => {
try {
const sn = parseInt(req.params.sn as string, 10);
if (isNaN(sn)) {
res.status(400).json({ error: '유효하지 않은 번호입니다.' });
return;
}
const item = await getItem(sn);
res.json(item);
} catch (err) {
if (err instanceof AuthError) {
res.status(err.status).json({ error: err.message });
return;
}
console.error('[monitoring] 상세 조회 오류:', err);
res.status(500).json({ error: '조회 중 오류가 발생했습니다.' });
}
});
// POST /api/monitoring -- 등록
router.post('/', requireAuth, requirePermission('monitoring', 'CREATE'), async (req, res) => {
try {
const { title, status } = req.body;
if (!title) {
res.status(400).json({ error: '제목은 필수입니다.' });
return;
}
const result = await createItem({
title,
status,
authorId: req.user!.sub,
});
res.status(201).json(result);
} catch (err) {
if (err instanceof AuthError) {
res.status(err.status).json({ error: err.message });
return;
}
console.error('[monitoring] 등록 오류:', err);
res.status(500).json({ error: '등록 중 오류가 발생했습니다.' });
}
});
export default router;
```
### 4-3. 서비스 (보일러플레이트)
```ts
// backend/src/monitoring/monitoringService.ts
import { wingPool } from '../db/wingDb.js';
import { AuthError } from '../auth/authService.js';
// ============================================================
// 인터페이스
// ============================================================
interface MonitoringItem {
sn: number;
title: string;
status: string;
authorId: string;
regDtm: string;
}
interface ListItemsInput {
search?: string;
page?: number;
size?: number;
}
interface ListItemsResult {
items: MonitoringItem[];
totalCount: number;
page: number;
size: number;
}
interface CreateItemInput {
title: string;
status?: string;
authorId: string;
}
// ============================================================
// CRUD 함수
// ============================================================
export async function listItems(input: ListItemsInput): Promise<ListItemsResult> {
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 USE_YN = 'Y'";
const params: (string | number)[] = [];
let paramIdx = 1;
if (input.search) {
whereClause += ` AND TITLE ILIKE $${paramIdx}`;
params.push(`%${input.search}%`);
paramIdx++;
}
// 전체 건수
const countResult = await wingPool.query(
`SELECT COUNT(*) as cnt FROM MONITORING ${whereClause}`,
params,
);
const totalCount = parseInt(countResult.rows[0].cnt, 10);
// 목록
const listParams = [...params, size, offset];
const listResult = await wingPool.query(
`SELECT SN, TITLE, STATUS, AUTHOR_ID, REG_DTM
FROM MONITORING
${whereClause}
ORDER BY REG_DTM DESC
LIMIT $${paramIdx++} OFFSET $${paramIdx}`,
listParams,
);
const items: MonitoringItem[] = listResult.rows.map((r: Record<string, unknown>) => ({
sn: r.sn as number,
title: r.title as string,
status: r.status as string,
authorId: r.author_id as string,
regDtm: r.reg_dtm as string,
}));
return { items, totalCount, page, size };
}
export async function getItem(sn: number): Promise<MonitoringItem> {
const result = await wingPool.query(
`SELECT SN, TITLE, STATUS, AUTHOR_ID, REG_DTM
FROM MONITORING
WHERE SN = $1 AND USE_YN = 'Y'`,
[sn],
);
if (result.rows.length === 0) {
throw new AuthError('데이터를 찾을 수 없습니다.', 404);
}
const r = result.rows[0];
return {
sn: r.sn,
title: r.title,
status: r.status,
authorId: r.author_id,
regDtm: r.reg_dtm,
};
}
export async function createItem(input: CreateItemInput): Promise<{ sn: number }> {
if (!input.title || input.title.trim().length === 0) {
throw new AuthError('제목은 필수입니다.', 400);
}
const result = await wingPool.query(
`INSERT INTO MONITORING (TITLE, STATUS, AUTHOR_ID)
VALUES ($1, $2, $3)
RETURNING SN`,
[input.title.trim(), input.status || 'ACTIVE', input.authorId],
);
return { sn: result.rows[0].sn };
}
```
## Step 3: 백엔드 메뉴 설정 등록
### 주요 패턴 요약
`backend/src/settings/settingsService.ts``DEFAULT_MENU_CONFIG` 배열에 항목을 추가합니다.
| 항목 | 패턴 |
|------|------|
| DB Pool | `wingPool` (wing DB) 또는 `authPool` (wing_auth DB) |
| 에러 처리 | `AuthError(message, status)` 활용 |
| 논리 삭제 | `USE_YN = 'Y'/'N'` 컬럼 사용, DELETE 대신 UPDATE |
| 페이징 | `LIMIT $N OFFSET $M`, 기본 size 20, 최대 100 |
| 인증 | `requireAuth` (JWT 검증) + `requirePermission(resource, operation)` |
| 작성자 | `req.user!.sub` (JWT payload에서 USER_ID 추출) |
---
## Step 5: server.ts 라우트 등록 + DB 마이그레이션
### 5-1. server.ts에 라우트 등록
```ts
// backend/src/server.ts
// 1. import 추가
import monitoringRouter from './monitoring/monitoringRouter.js';
// 2. 업무 API 라우트 등록 (기존 라우트 아래에)
app.use('/api/monitoring', monitoringRouter);
```
> **참고**: import 경로에 `.js` 확장자가 필요하다 (TypeScript ESM 빌드).
### 5-2. DEFAULT_MENU_CONFIG에 메뉴 항목 추가
```ts
// backend/src/settings/settingsService.ts
```typescript
const DEFAULT_MENU_CONFIG: MenuConfigItem[] = [
// ... 기존 10개 메뉴 ...
{ id: 'incidents', label: '통합조회', icon: '🔍', enabled: true, order: 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에 새 항목을 추가합니다.
### 5-3. 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;
-- database/auth_init.sql 의 menu.config INSERT 문에 새 항목 추가
-- (신규 설치 시에만 적용. 기존 운영 DB는 관리자 UI에서 관리)
```
> **참고**: 이 SQL은 신규 설치 시에만 적용됩니다. 기존 운영 DB는 관리자 UI에서 메뉴를 관리합니다.
### 5-4. DB 마이그레이션 작성
## Step 5: 관리자 메뉴 관리에서 활성화
```sql
-- database/migration/017_monitoring.sql
코드 배포 후:
1. 관리자 계정으로 로그인
2. 관리자 패널(⚙️) → 메뉴 관리 탭
3. 새 메뉴가 목록에 표시됨
4. 활성/비활성 토글, 순서, 라벨, 아이콘을 설정
5. "변경사항 저장" 클릭
-- ============================================================
-- 마이그레이션 017: 모니터링 (MONITORING)
-- ============================================================
> 기존 DB에 새 메뉴 ID가 없으면 `getMenuConfig()`가 DEFAULT_MENU_CONFIG fallback을 사용하여 새 메뉴가 자동으로 목록에 나타납니다.
CREATE TABLE IF NOT EXISTS MONITORING (
SN SERIAL PRIMARY KEY,
TITLE VARCHAR(200) NOT NULL,
STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
AUTHOR_ID UUID NOT NULL,
USE_YN CHAR(1) NOT NULL DEFAULT 'Y',
REG_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(),
MDFCN_DTM TIMESTAMPTZ,
## 실전 예시: "모니터링" 탭 추가
CONSTRAINT FK_MONITORING_AUTHOR FOREIGN KEY (AUTHOR_ID)
REFERENCES auth.AUTH_USER(USER_ID),
CONSTRAINT CK_MONITORING_USE CHECK (USE_YN IN ('Y','N'))
);
### 1. 뷰 컴포넌트 생성
COMMENT ON TABLE MONITORING IS '모니터링';
COMMENT ON COLUMN MONITORING.USE_YN IS '사용여부 (N=논리삭제)';
CREATE INDEX IF NOT EXISTS IDX_MONITORING_REG_DTM ON MONITORING(REG_DTM DESC);
```
> 마이그레이션 파일 네이밍: `NNN_{도메인}.sql` (NNN은 다음 순번).
> 자세한 마이그레이션 패턴은 `CRUD-API-GUIDE.md`를 참고한다.
---
## 실전 예시: "monitoring" 탭 추가 전체 흐름
### 1단계: 프론트엔드 파일 생성
```bash
# frontend/src/components/views/MonitoringView.tsx 파일 생성
mkdir -p frontend/src/tabs/monitoring/components
mkdir -p frontend/src/tabs/monitoring/services
```
### 2. App.tsx 수정 (3곳)
- `frontend/src/tabs/monitoring/components/MonitoringView.tsx` 생성
- `frontend/src/tabs/monitoring/services/monitoringApi.ts` 생성
- `frontend/src/tabs/monitoring/index.ts` 생성
### 2단계: 프론트엔드 기존 파일 수정
```diff
+ import { MonitoringView } from './components/views/MonitoringView'
--- frontend/src/common/types/navigation.ts
+ export type MainTab = '...' | 'monitoring' | 'admin';
- 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) {
// ...
--- frontend/src/App.tsx
+ import { MonitoringView } from '@tabs/monitoring';
// renderView switch 내:
+ case 'monitoring':
+ return <MonitoringView />
case 'admin':
return <AdminView />
}
}
+ return <MonitoringView />;
--- frontend/src/common/hooks/useSubMenu.ts
// subMenuConfigs:
+ monitoring: null,
// subMenuState:
+ monitoring: '',
```
### 3. settingsService.ts 수정
### 3단계: FEATURE_ID 등록
```diff
const DEFAULT_MENU_CONFIG: MenuConfigItem[] = [
// ... 기존 메뉴들 ...
{ id: 'incidents', label: '통합조회', icon: '🔍', enabled: true, order: 10 },
+ { id: 'monitoring', label: '실시간 모니터링', icon: '📡', enabled: true, order: 11 },
]
--- frontend/src/common/constants/featureIds.ts
+ // monitoring
+ 'monitoring:main': '모니터링',
```
### 4. auth_init.sql 수정
### 4단계: 백엔드 파일 생성
menu.config JSON에 새 항목 추가 (신규 설치용)
- `backend/src/monitoring/monitoringRouter.ts` 생성
- `backend/src/monitoring/monitoringService.ts` 생성
### 5. 배포 후 관리자 UI에서 활성화
### 5단계: 백엔드 기존 파일 수정 + DB
```diff
--- backend/src/server.ts
+ import monitoringRouter from './monitoring/monitoringRouter.js';
+ app.use('/api/monitoring', monitoringRouter);
--- backend/src/settings/settingsService.ts
+ { id: 'monitoring', label: '실시간 모니터링', icon: '📡', enabled: true, order: 11 },
```
- `database/migration/017_monitoring.sql` 생성
- `database/auth_init.sql` 의 menu.config JSON에 항목 추가
### 6단계: 검증
```bash
cd frontend && npx tsc --noEmit # TypeScript 컴파일 검증
cd frontend && npx eslint . # ESLint 검증
cd backend && npx tsc --noEmit # 백엔드 컴파일 검증
```
---
## 체크리스트
- [ ] 뷰 컴포넌트 생성 (`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 .`)
- [ ] 관리자 메뉴 관리에서 새 메뉴 표시 확인
### 프론트엔드
- [ ] `frontend/src/tabs/{탭명}/components/{TabName}View.tsx` 생성
- [ ] `frontend/src/tabs/{탭명}/services/{tabName}Api.ts` 생성
- [ ] `frontend/src/tabs/{탭명}/index.ts` re-export 생성
- [ ] `navigation.ts` MainTab 타입에 새 ID 추가
- [ ] `App.tsx` import + renderView switch case 추가
- [ ] `useSubMenu.ts` subMenuConfigs + subMenuState 추가 (서브탭 있는 경우)
- [ ] `featureIds.ts` FEATURE_ID 등록
- [ ] `npx tsc --noEmit` 통과
- [ ] `npx eslint .` 통과
### 백엔드
- [ ] `backend/src/{도메인}/{domain}Router.ts` 생성
- [ ] `backend/src/{도메인}/{domain}Service.ts` 생성
- [ ] `server.ts` import + `app.use()` 등록
- [ ] `settingsService.ts` DEFAULT_MENU_CONFIG에 항목 추가
- [ ] `npx tsc --noEmit` 통과
### DB
- [ ] `database/migration/NNN_{domain}.sql` 마이그레이션 작성
- [ ] `database/auth_init.sql` menu.config 초기 JSON 업데이트
- [ ] SQL 실행 검증
### 배포 후
- [ ] 관리자 로그인 -> 메뉴 관리에서 새 메뉴 표시 확인
- [ ] 메뉴 활성화/비활성화 토글 동작 확인
- [ ] 권한 미부여 사용자에게 메뉴가 보이지 않는지 확인

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -1,31 +1,7 @@
# WING-OPS (해양 방제 운영 지원 시스템)
# 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)를 참조하세요.
유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 해안평가, 기상/해상 정보를 통합 제공한다.
---
@ -34,13 +10,43 @@ claude
| 영역 | 기술 |
|------|------|
| 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 |
| 지도 | MapLibre GL JS 5.x + deck.gl 9.x (GPU 렌더링) |
| UI | lucide-react (아이콘), @dnd-kit (드래그앤드롭), emoji-mart (이모지) |
| 실시간 | Socket.IO Client 4.8 |
| Backend | Express 4, TypeScript, PostgreSQL 16 + PostGIS |
| 인증 | JWT HttpOnly Cookie (`WING_SESSION`) + Google OAuth + RBAC 2차원 권한 |
| 보안 | Helmet, CORS, Rate-limit, 입력 살균 (sanitize) |
| CI/CD | Gitea Actions (main 머지 시 자동 배포) |
---
## 아키텍처
```
[Browser]
|
| React 19 + MapLibre GL JS + deck.gl
| Zustand (로컬 상태) + TanStack Query (서버 상태)
|
|--- HTTP (Axios) ---> [Express 4 API]
|--- WebSocket ------> [Socket.IO]
|
[PostgreSQL 16 + PostGIS]
wing DB (운영) + wing_auth DB (인증)
```
### HTTP 정책
- **GET / POST only** (보안취약점 가이드 준수, PUT/DELETE 미사용)
- JWT는 HttpOnly Cookie로 전송, Axios 인터셉터에서 자동 처리
- Helmet CORP cross-origin 설정 (sendBeacon 허용)
### 권한 체계
- RBAC 2차원 권한: **리소스(FEATURE_ID)** x **오퍼레이션(RCUD)**
- `permResolver` 엔진이 AUTH_PERM 테이블의 OPER_CD 기반으로 권한 해석
- 백엔드: `requireAuth` > `requireRole` > `requirePermission` 미들웨어 체인
---
@ -48,53 +54,140 @@ claude
```
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)
├── frontend/src/
│ ├── App.tsx MainTab 라우팅, 감사 로그 자동 기록
│ ├── index.css @tailwind + @import 엔트리
│ ├── common/ @common/ alias
│ │ ├── components/ auth/, layer/, layout/, map/, ui/
│ │ ├── hooks/ useFeatureTracking, useLayers, useSubMenu
│ │ ├── services/ api.ts, authApi.ts, layerService.ts
│ │ ├── store/ authStore, menuStore (Zustand)
│ │ ├── types/ backtrack, boomLine, hns, navigation
│ │ ├── utils/ cn, coordinates, geo, sanitize
│ │ ├── styles/ base.css, components.css, wing.css (@layer)
│ │ └── constants/ featureIds.ts (FEATURE_ID 상수 체계)
│ └── tabs/ @tabs/ alias (11개 탭)
│ ├── prediction/ 유류 확산 예측
│ ├── hns/ HNS 분석
│ ├── rescue/ 구조 시나리오
│ ├── aerial/ 항공 방제
│ ├── weather/ 해양 기상
│ ├── incidents/ 사건/사고 관리
│ ├── board/ 게시판
│ ├── reports/ 보고서
│ ├── assets/ 자산 관리
│ ├── scat/ Pre-SCAT 해안평가
│ └── admin/ 관리자
├── backend/src/
│ ├── server.ts Express 진입점 + 보안 미들웨어
│ ├── auth/ JWT, OAuth, 미들웨어
│ ├── users/, roles/ 사용자/역할 관리, permResolver
│ ├── settings/, menus/ 시스템 설정, 메뉴
│ ├── audit/ 감사 로그
│ ├── board/, hns/, reports/ 업무 도메인
│ ├── assets/, incidents/ 업무 도메인
│ ├── scat/, prediction/ 업무 도메인
│ ├── aerial/, rescue/ 업무 도메인
│ ├── routes/ layers, simulation
│ ├── middleware/ security (sanitize, rate-limit)
│ └── db/ wingDb, authDb, seed
├── database/
│ ├── init.sql wing DB 스키마
│ ├── auth_init.sql wing_auth DB 스키마
│ └── migration/ 001~016 마이그레이션
└── .gitea/workflows/ CI/CD 파이프라인
```
### Path Alias
| Alias | 경로 |
|-------|------|
| `@common/*` | `src/common/*` |
| `@tabs/*` | `src/tabs/*` |
---
## 개발 환경 실행
## 탭 구성 (11개)
| 탭 | 패키지 | 설명 |
|---|---|---|
| prediction | 확산 예측 | 유류 확산 모델 시뮬레이션 (KOSPS/POSEIDON/OpenDrift 앙상블) |
| hns | HNS 분석 | 위험유해물질 거동 분석 + 물질 DB |
| rescue | 구조 시나리오 | 해상 구조 시나리오 모의 |
| aerial | 항공 방제 | 항공 탐색, 드론, CCTV, 위성 |
| weather | 해양 기상 | 실시간 해양기상 오버레이 |
| incidents | 사건/사고 | 사건 이력 관리, 미디어 |
| board | 게시판 | 공지/자료 게시판 |
| reports | 보고서 | 오염보고서 생성기 |
| assets | 자산 관리 | 방제 장비/기관/담당자/선박보험 |
| scat | 해안평가 | Pre-SCAT 해안 구간 조사 |
| admin | 관리자 | 사용자/역할/권한/메뉴/설정 |
---
## 백엔드 API 라우트
모든 API는 `/api/` 접두사 하위에 등록된다.
### 인증/관리
| 라우트 | 설명 |
|--------|------|
| `/api/auth` | JWT 로그인/로그아웃, OAuth, 토큰 갱신 |
| `/api/users` | 사용자 CRUD |
| `/api/roles` | 역할/권한 관리, RCUD 매트릭스 |
| `/api/settings` | 시스템 설정 |
| `/api/menus` | 메뉴 설정 |
| `/api/audit` | 감사 로그 |
### 업무 도메인
| 라우트 | 설명 |
|--------|------|
| `/api/board` | 게시판 CRUD + 첨부파일 |
| `/api/hns` | HNS 물질 검색/분석 |
| `/api/reports` | 보고서 템플릿/생성/조회 |
| `/api/assets` | 자산(장비/기관/선박/보험) 관리 |
| `/api/incidents` | 사건/사고 이력 관리 |
| `/api/scat` | SCAT 구역/구간/조사 |
| `/api/prediction` | 확산 분석/역추적/오일펜스 |
| `/api/aerial` | 항공 미디어/CCTV/위성 |
| `/api/rescue` | 구난 작전/시나리오 |
### 지도/시뮬레이션
| 라우트 | 설명 |
|--------|------|
| `/api/layers` | 지도 레이어 데이터 |
| `/api/simulation` | 시뮬레이션 실행/결과 |
---
## 데이터베이스
| DB | 용도 | 비고 |
|----|------|------|
| wing | 운영 데이터 | PostgreSQL 16 + PostGIS |
| wing_auth | 인증/권한 | 동일 서버, 별도 DB |
마이그레이션 파일: `database/migration/001~016`
---
## 퀵스타트
### 사전 요구사항
- Node.js 20+ (`.node-version`, fnm 사용)
- PostgreSQL 16+ (운영 DB에 직접 연결)
- Node.js 20+ (`.node-version` 파일, fnm 사용)
- PostgreSQL 16+ (운영 DB 접근 가능해야 함)
### 실행
```bash
# 저장소 복제
git clone https://gitea.gc-si.dev/gc/wing-ops.git
cd wing-ops
# 백엔드 (터미널 1)
cd backend && npm install && npm run dev # localhost:3001
@ -113,101 +206,19 @@ cd backend && npx tsc --noEmit
cd frontend && npx eslint .
# 프로덕션 빌드
cd frontend && npm run build # dist/ 생성
cd backend && npm run build # dist/ 생성
cd frontend && npm run build # tsc -b && vite build → dist/
cd backend && npm run build # tsc → 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)
프론트엔드 (`frontend/.env`):
```
VITE_API_URL=http://localhost:3001/api
VITE_GOOGLE_CLIENT_ID=your-google-client-id
```
### 백엔드 (.env)
백엔드 (`backend/.env`):
```
PORT=3001
NODE_ENV=development
@ -216,7 +227,7 @@ AUTH_DB_HOST=localhost
AUTH_DB_PORT=5432
AUTH_DB_NAME=wing_auth
AUTH_DB_USER=wing_auth
AUTH_DB_PASSWORD=WingAuth!2026
AUTH_DB_PASSWORD=your-password
GOOGLE_CLIENT_ID=your-google-client-id
```
@ -230,18 +241,44 @@ GOOGLE_CLIENT_ID=your-google-client-id
| 백엔드 API | https://wing-demo.gc-si.dev/api/ |
| CI/CD | Gitea Actions (main 머지 시 자동 배포) |
배포 파이프라인 상세는 [DEVELOPMENT-GUIDE.md #7](DEVELOPMENT-GUIDE.md#7-자동-배포)을 참조하세요.
---
## 개발 워크플로우
```
브랜치 분기 → 개발 → 커밋/푸시 → develop MR → main PR → 자동 배포
```
- `main`: 배포 가능한 안정 브랜치 (보호됨)
- `develop`: 개발 통합 브랜치 (보호됨)
- `feature/`, `bugfix/`, `hotfix/`: 작업 브랜치
- 직접 push 금지, MR을 통해서만 머지
---
## 문서 최신화 규칙
## 문서 안내
공통 기능(인증, 감사로그, 메뉴 시스템, API 통신 등)을 추가/변경할 때:
1. 해당 기능 코드 구현
2. `docs/COMMON-GUIDE.md` 최신화 (필수)
3. 필요 시 `CLAUDE.md` 프로젝트 구조 갱신
### 개발 가이드
매 기능 개발 완료 시:
```
Claude에게: "memory 파일 최신화해줘"
```
| 문서 | 설명 |
|------|------|
| [DEVELOPMENT-GUIDE.md](DEVELOPMENT-GUIDE.md) | 개발 워크플로우 전체 흐름 |
| [COMMON-GUIDE.md](COMMON-GUIDE.md) | 공통 로직 (인증, 감사로그, 메뉴, API, 상태관리) |
| [MENU-TAB-GUIDE.md](MENU-TAB-GUIDE.md) | 새 메뉴 탭 추가 절차 |
### 운영 가이드
| 문서 | 설명 |
|------|------|
| [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` | 테스트 규칙 |

파일 보기

@ -0,0 +1,42 @@
# 변경 이력
## [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 초기화

파일 보기

@ -0,0 +1,502 @@
# 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<MyData[]> {
const response = await api.get<MyData[]>('/my-endpoint')
return response.data
}
export async function createMyData(data: Omit<MyData, 'id'>): Promise<MyData> {
const response = await api.post<MyData>('/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<MyState>((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개)
```

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -0,0 +1,433 @@
# 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 <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 갱신 (스킬) |

파일 보기

@ -0,0 +1,165 @@
# 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=<PostgreSQL 호스트>
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 포함 압축본 사용 |

파일 보기

@ -0,0 +1,194 @@
# 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 (
<div className="flex flex-1 overflow-hidden bg-bg-0">
<div className="flex-1 flex flex-col overflow-hidden p-6">
<h1 className="text-lg font-bold text-text-1 font-korean">실시간 모니터링</h1>
{/* 뷰 콘텐츠 */}
</div>
</div>
)
}
```
`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 <MonitoringView />
// ...
}
}
```
## 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 <MonitoringView />
case 'admin':
return <AdminView />
}
}
```
### 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 .`)
- [ ] 관리자 메뉴 관리에서 새 메뉴 표시 확인

파일 보기

@ -0,0 +1,435 @@
# Mock → API 전환 개발 지침
이 문서는 각 탭의 mock 데이터를 PostgreSQL DB + REST API 기반으로 전환할 때 따라야 할 표준 프로세스를 정의한다.
CRUD API 작성 규칙은 [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) 참조.
---
## 1. 전환 프로세스 (탭당 반복)
### Step A. 브랜치 생성
`feature/{탭명}-crud` 형식으로 develop에서 분기한다.
```bash
git checkout develop
git pull
git checkout -b feature/{탭명}-crud
```
### Step B. Mock 전수 조사 (필수!)
탭 디렉토리 전체에서 mock/하드코딩 데이터를 빠짐없이 검색한다.
**검색 키워드**: `mock`, `Mock`, `MOCK`, `sample`, `initial`, `hardcod`, `localStorage`, 인라인 배열 상수
```bash
grep -rn "mock\|Mock\|MOCK\|sample\|initial\|hardcod\|localStorage" frontend/src/tabs/{탭}/
```
**체크리스트 형식**으로 정리한다:
```
□ 파일명:라인 — 변수명 (N건) — 전환방법
□ components/AssetList.tsx:12 — MOCK_ASSETS (30건) — DB 이전
□ services/assetService.ts:5 — INITIAL_FILTER — 프론트 상수 유지
□ hooks/useAsset.ts:88 — localStorage.getItem('draft') — DB 이전
```
board 전환 시 mock 참조 누락으로 런타임 에러가 발생한 경험이 있다. 전수 조사를 건너뛰지 말 것.
### Step C. 프론트 상수 vs DB 데이터 판단
조사한 mock/하드코딩 데이터를 아래 기준으로 분류한다.
| 분류 | 유지/이전 | 예시 |
|------|-----------|------|
| UI 전용 색상 매핑 | 프론트 상수 유지 | 상태별 뱃지 색, 심각도 색상 |
| 레이아웃/뷰 설정 | 프론트 상수 유지 | 기본 페이지 크기, 컬럼 너비 |
| 비즈니스 목록 데이터 | DB 이전 | 자산 목록, 사고 목록, 보고서 |
| 검색/필터 대상 데이터 | DB 이전 | 카테고리, 기관명, 상태값 |
| 유형/카테고리 코드 | DB 이전 또는 CHECK 제약 | 자산유형, 오염물질유형 |
### Step D. DB 스키마 설계 + 마이그레이션
DDL 규칙은 [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) (4. DB 설계 규칙) 참조.
1. 기존 테이블 활용 가능 여부 확인 (예: ACDNT, LAYER 등)
2. `database/migration/NNN_{탭명}.sql` 파일 작성 (번호는 기존 파일 다음 순번)
3. 초기 데이터 INSERT (mock 데이터를 SQL로 변환)
4. psql로 원격 DB에 직접 실행
```bash
# 원격 wing DB에 마이그레이션 실행
PGPASSWORD=Wing2026 psql -h 211.208.115.83 -U wing -d wing \
-f database/migration/NNN_{탭명}.sql
# 실행 결과 검증 (마이그레이션 파일 끝의 SELECT 확인)
```
마이그레이션 파일 규칙:
- 모든 DDL에 `IF NOT EXISTS` / `IF EXISTS` 사용 (재실행 안전)
- 파일 끝에 검증 SELECT 포함
### Step E. 백엔드 Service + Router 구현
Service/Router 패턴은 [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) (5. Service 레이어 패턴, 6. Router 레이어 패턴) 참조.
**HTTP 메소드 규칙** (보안취약점 가이드 준수):
| 메소드 | 용도 |
|--------|------|
| GET | 단순 조회 (민감하지 않은 경우) |
| POST | 생성/수정/삭제 및 복잡한 조회 파라미터 |
PUT, DELETE, PATCH는 사용하지 않는다. 자세한 내용은 [2. HTTP 메소드 정책](#2-http-메소드-정책-필독) 참조.
**URL 패턴**:
| URL | 설명 |
|-----|------|
| `GET /api/{domain}` | 목록 (간단한 파라미터) |
| `GET /api/{domain}/:sn` | 상세 |
| `POST /api/{domain}/list` | 목록 (복잡한 필터 파라미터) |
| `POST /api/{domain}/detail` | 상세 |
| `POST /api/{domain}/create` | 생성 |
| `POST /api/{domain}/update` | 수정 |
| `POST /api/{domain}/delete` | 삭제 |
| `GET /api/{domain}/templates` | 메타데이터/코드 조회 |
**인증 패턴**:
```typescript
// 현재 로그인 사용자 UUID 추출
const userId = req.user!.sub // JWT payload의 사용자 UUID
// ❌ 절대 사용 금지 (reports 전환 시 실제 발생한 버그)
const user = (req as unknown as { user: { id: string } }).user
const userId = user.id // undefined → DB NOT NULL 제약 위반
```
구현 후 `backend/src/server.ts`에 라우터를 등록한다.
### Step F. 프론트엔드 API 서비스 + 컴포넌트 전환
1. `frontend/src/tabs/{탭}/services/{탭}Api.ts` 생성
2. API 응답 타입 (`interface Api{탭명}Item` 등) 정의
3. API ↔ 프론트 모델 변환 함수 작성 (필요 시)
4. 정적 마스터 데이터 캐싱: 모듈 변수 또는 TanStack Query `staleTime: Infinity`
5. 컴포넌트에서 mock import → API 호출로 교체
6. `api.post()` 사용 (`api.put()`, `api.delete()` 사용 금지)
```typescript
// frontend/src/tabs/{탭}/services/{탭}Api.ts
import { api } from '@common/services/api'
export interface Api{탭명}Item {
sn: number
// ...
}
export async function fetch{탭명}List(params: {
search?: string
page?: number
size?: number
}): Promise<{ items: Api{탭명}Item[]; totalCount: number }> {
const response = await api.post('/{ 탭명}/list', params)
return response.data
}
// 수정 — POST /update 사용
export async function update{탭명}(sn: number, data: Update{탭명}Input): Promise<void> {
await api.post('/{탭명}/update', { sn, ...data })
}
// 삭제 — POST /delete 사용
export async function delete{탭명}(sn: number): Promise<void> {
await api.post('/{탭명}/delete', { sn })
}
```
### Step G. 빌드 검증
```bash
# 백엔드 TypeScript 컴파일
cd backend && npm run build
# 프론트엔드 타입 체크 + ESLint
cd frontend && npx tsc --noEmit && npx eslint .
```
빌드/린트 에러가 0건이어야 다음 단계로 진행한다.
### Step H. 로컬 API 동작 테스트
```bash
# 백엔드 개발 서버 시작
cd backend && npm run dev
# 로그인 — 쿠키 파일 획득
curl -s -c /tmp/wing.cookie -X POST http://localhost:3001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"account":"admin","password":"admin1234"}' | jq .
# 목록 조회
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/list \
-H 'Content-Type: application/json' \
-d '{"page":1,"size":10}' | jq .
# 생성
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/create \
-H 'Content-Type: application/json' \
-d '{...}' | jq .
# 수정
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/update \
-H 'Content-Type: application/json' \
-d '{"sn": 1, ...}' | jq .
# 삭제
curl -s -b /tmp/wing.cookie -X POST http://localhost:3001/api/{탭명}/delete \
-H 'Content-Type: application/json' \
-d '{"sn": 1}' | jq .
```
CRUD 전체 흐름(생성 → 조회 → 수정 → 삭제 → 필터)을 확인하고 테스트 데이터를 정리한다.
### Step I. Mock 잔여 확인
```bash
grep -rn "mock\|Mock\|MOCK\|localStorage" frontend/src/tabs/{탭}/
# → UI 상수(색상, 레이아웃) 외 결과 0건이어야 함
```
잔여가 있으면 Step F로 돌아가 처리한다.
### Step J. 커밋 + 푸시 + MR
```bash
# 커밋 (Conventional Commits 형식, 한국어)
git add -p
git commit -m "feat({탭명}): mock 데이터 DB + REST API 전환"
# 푸시
git push -u origin feature/{탭명}-crud
```
`feature/{탭명}-crud``develop` MR을 Gitea에서 생성한다.
`/push` 또는 `/mr` 스킬 활용 가능.
---
## 2. HTTP 메소드 정책 (필독)
한국 보안취약점 점검 가이드에 따라 GET/POST만 사용한다.
### 허용
| 메소드 | 용도 | 예시 |
|--------|------|------|
| GET | 단순 조회 (파라미터가 단순하고 민감하지 않은 경우) | `GET /api/reports`, `GET /api/reports/:sn` |
| POST | 생성/수정/삭제, 복잡한 필터 파라미터 조회 | `POST /api/reports/create`, `POST /api/reports/list` |
### 금지
| 메소드 | 이유 |
|--------|------|
| PUT | 보안취약점 가이드 위반 |
| DELETE | 보안취약점 가이드 위반 |
| PATCH | 보안취약점 가이드 위반 |
### 기존 API 현황
`boardRouter`, `userRouter`, `roleRouter` 등은 아직 PUT/DELETE를 사용 중이다.
별도 세션에서 POST 패턴으로 마이그레이션 예정.
**신규 탭 전환 시 반드시 POST 패턴을 적용한다.**
---
## 3. 전환 시 주의사항 (실전 교훈)
### 3.1 req.user 접근 패턴
```typescript
// 올바른 패턴
const userId = req.user!.sub // JWT payload의 사용자 UUID
// 잘못된 패턴 (런타임 에러 발생)
const user = (req as unknown as { user: { id: string } }).user
const userId = user.id // undefined → DB NOT NULL 제약 위반
```
Reports 전환 시 실제 발생한 버그. `boardRouter.ts`의 패턴을 확인하고 `req.user!.sub`을 사용한다.
JWT 페이로드 전체 구조:
```typescript
interface JwtPayload {
sub: string // 사용자 UUID (USER_ID)
acnt: string // 계정명 (USER_ACNT)
name: string // 사용자명 (USER_NM)
roles: string[] // 역할 코드 목록
}
// 사용 예시
const userId = req.user!.sub // UUID
const userName = req.user!.name // 이름
```
### 3.2 AUTH_USER 테이블 컬럼명
```sql
-- 올바른 컬럼명
SELECT u.USER_NM as author_name FROM AUTH_USER u
-- 잘못된 컬럼명 (500 에러 발생)
SELECT u.NM as author_name FROM AUTH_USER u
```
Reports 전환 시 실제 발생한 버그. 반드시 `USER_NM`을 사용한다.
`AUTH_USER` 주요 컬럼 참조:
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `USER_ID` | UUID PK | 사용자 UUID (`req.user!.sub`과 일치) |
| `USER_ACNT` | VARCHAR | 계정명 (`req.user!.acnt`와 일치) |
| `USER_NM` | VARCHAR | 사용자명 (`req.user!.name`와 일치) |
| `EMAIL` | VARCHAR | 이메일 |
### 3.3 Mock 전수 조사 누락
Board 전환 시 일부 mock 참조를 놓쳐 런타임 에러가 발생했다.
[Step B](#step-b-mock-전수-조사-필수)의 전수 조사를 건너뛰지 말 것.
특히 다음 위치를 반드시 확인한다:
- 컴포넌트 파일 내 인라인 배열 (`const ITEMS = [{ id: 1, ... }]`)
- 커스텀 훅 초기값 (`useState([{ ... }])`)
- `localStorage.getItem` / `localStorage.setItem` 호출
- 서비스 파일 내 하드코딩 반환값
### 3.4 프론트 api.put() / api.delete() 금지
```typescript
// 올바른 POST 패턴
await api.post(`/reports/update`, { sn, ...input })
await api.post(`/reports/delete`, { sn })
// 금지 — PUT/DELETE 사용 불가
await api.put(`/reports/${sn}`, input)
await api.delete(`/reports/${sn}`)
```
### 3.5 트랜잭션 사용 시점
- 단일 테이블 INSERT/UPDATE: 트랜잭션 불필요
- 다중 테이블 동시 변경 (예: 헤더 + 섹션, 보고서 + 첨부파일): 반드시 트랜잭션 사용
```typescript
const client = await wingPool.connect()
try {
await client.query('BEGIN')
// 헤더 INSERT
const headerResult = await client.query(
'INSERT INTO REPORT_HDR (...) VALUES ($1, $2) RETURNING HDR_SN',
[...]
)
const hdrSn = headerResult.rows[0].hdr_sn
// 섹션 INSERT (헤더 FK 참조)
for (const section of sections) {
await client.query(
'INSERT INTO REPORT_SECT (HDR_SN, ...) VALUES ($1, ...)',
[hdrSn, ...]
)
}
await client.query('COMMIT')
return { hdrSn }
} catch (err) {
await client.query('ROLLBACK')
throw err
} finally {
client.release()
}
```
### 3.6 에러 처리 일관성
모든 라우트 핸들러에서 동일한 에러 처리 구조를 사용한다.
```typescript
try {
// 비즈니스 로직
} catch (err) {
if (err instanceof AuthError) {
res.status(err.status).json({ error: err.message })
return
}
console.error('[{탭명}] 작업 오류:', err)
res.status(500).json({ error: '처리 중 오류가 발생했습니다.' })
}
```
Board의 GET 목록 라우트에서 `AuthError` 분기 누락 이슈가 있었다.
목록 조회처럼 평범해 보이는 라우트도 예외 없이 동일한 구조를 사용한다.
### 3.7 정적 마스터 데이터 캐싱
코드 목록, 기관 목록 등 변경이 드문 마스터 데이터는 매 호출마다 DB 조회하지 않는다.
```typescript
// 방법 1: 모듈 변수 캐싱 (서버 재시작 시까지 유지)
let cachedOrgList: OrgItem[] | null = null
export async function getOrgList(): Promise<OrgItem[]> {
if (cachedOrgList) return cachedOrgList
const result = await wingPool.query('SELECT * FROM ORG WHERE USE_YN = $1', ['Y'])
cachedOrgList = result.rows.map(mapOrg)
return cachedOrgList
}
// 방법 2: TanStack Query staleTime 설정 (프론트엔드)
const { data: orgList } = useQuery({
queryKey: ['orgList'],
queryFn: fetchOrgList,
staleTime: 1000 * 60 * 10, // 10분간 리패치 없음
})
```
---
## 4. 탭별 전환 우선순위
| # | 탭 | 난이도 | 상태 | 비고 |
|---|---|--------|------|------|
| 1 | Reports (보고서) | ★★★ | 완료 | 7개 DB 테이블, 섹션 단위 JSONB |
| 2 | Assets (방제자산) | ★★☆ | 대기 | mock 1파일 집중, ORG 테이블 활용 |
| 3 | Incidents (사고관리) | ★★★ | 대기 | mock 5파일 분산, ACDNT 테이블 존재 |
| 4 | SCAT (해안조사) | ★★★★ | 대기 | 1,084개 세그먼트, 스키마 격차 |
| 5 | Rescue (구조시나리오) | ★★★★ | 대기 | DB 미정의, 시뮬레이션 복잡 |
| 6 | Prediction (확산예측) | ★★★★★ | 대기 | 시뮬레이션 엔진 의존, 부분 API 연동 |
제외: Weather (KHOA API 연동 완료), HNS (API 연동 완료), Board (API 연동 완료), Aerial (스켈레톤 수준)
---
## 5. 완료 검증 체크리스트 (탭당)
- [ ] 백엔드 빌드 통과 (`cd backend && npm run build`)
- [ ] 프론트 타입 체크 통과 (`cd frontend && npx tsc --noEmit`)
- [ ] 프론트 ESLint 통과 (`cd frontend && npx eslint .`)
- [ ] API CRUD 전체 테스트 (curl: 생성, 조회, 수정, 삭제, 필터)
- [ ] Mock/localStorage 잔여 0건 (UI 상수 제외)
- [ ] PUT/DELETE 사용 0건 (프론트/백엔드 모두)
- [ ] 커밋 + 푸시 + MR 생성
---
## 관련 문서
- [CRUD-API-GUIDE.md](./CRUD-API-GUIDE.md) — CRUD API 표준 (DB 설계, Service/Router 패턴, 권한 모델)
- [COMMON-GUIDE.md](./COMMON-GUIDE.md) — 공통 로직 (인증, 감사 로그, 메뉴, API 통신)
- [MENU-TAB-GUIDE.md](./MENU-TAB-GUIDE.md) — 새 메뉴 탭 추가 절차

247
docs/_backup_20260301/README.md Executable file
파일 보기

@ -0,0 +1,247 @@
# 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 파일 최신화해줘"
```

파일 보기

@ -0,0 +1,123 @@
# 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 설정됨)

파일 보기

@ -0,0 +1,223 @@
# 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 갱신 |