kcg-ai-monitoring/CLAUDE.md

288 lines
14 KiB
Markdown
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# KCG AI Monitoring (모노레포)
해양경찰청 AI 기반 불법어선 탐지 및 단속 지원 플랫폼
## 🚨 절대 지침 (Absolute Rules)
아래 두 지침은 모든 작업에 우선 적용된다. 사용자가 명시적으로 해제하지 않는 한 우회 금지.
### 1. 신규 기능 설계·구현 착수 시: 원격 develop 동기화 필수
신규 기능/버그 수정/리팩터 등 **어떤 작업이든 브랜치를 새로 만들기 전**에는 아래 절차를 반드시 수행한다.
```bash
git fetch origin --prune
# origin/develop이 로컬 develop보다 앞서 있는지 확인
git log --oneline develop..origin/develop | head
```
- **로컬 develop이 뒤처진 경우** → 사용자에게 다음을 권유하고 동의를 받은 후 진행:
> "`origin/develop`이 로컬보다 N개 커밋 앞서 있습니다. 최신화 후 신규 브랜치를 생성하는 것을 권장합니다. 진행할까요?"
승인 시: `git checkout develop && git pull --ff-only origin develop` → 그 위에서 `git checkout -b <new-branch>`
- **로컬 develop이 최신인 경우** → 그대로 develop에서 신규 브랜치 분기
- **로컬 develop이 없는 경우** → `git checkout -b develop origin/develop`로 tracking branch 먼저 생성
- **로컬에 unstaged/uncommitted 변경이 있을 때** → 사용자에게 먼저 알리고 stash/commit 여부 확인 후 진행. 임의로 폐기 금지.
**이유**: 오래된 develop 위에서 작업하면 머지 충돌·리베이스 비용이 커지고, 이미 해결된 이슈를 중복 해결할 위험이 있다. 브랜치 분기 시점의 기반을 항상 최신으로 맞춘다.
**적용 범위**: `/push`, `/mr`, `/create-mr`, `/release`, `/fix-issue` 스킬 실행 시, 그리고 Claude가 자발적으로 새 브랜치를 만들 때 모두.
### 2. 프론트엔드 개발 시: `design-system.html` 쇼케이스 규칙 전면 준수
`frontend/` 하위의 모든 페이지·컴포넌트·스타일 작성은 `design-system.html`(쇼케이스)에 정의된 컴포넌트·토큰·카탈로그만 사용한다. 이 문서 하단 **"디자인 시스템 (필수 준수)"** 섹션의 규칙을 **예외 없이** 따른다.
핵심 요약 (상세는 하단 섹션 참조):
- 공통 컴포넌트 우선 사용: `Badge`, `Button`, `Input`, `Select`, `TabBar`, `Card`, `PageContainer`, `PageHeader`, `Section`
- 라벨/색상은 `shared/constants/` 카탈로그 API(`getAlertLevelIntent` 등) 경유, ad-hoc 문자열 매핑 금지
- **인라인 색상·하드코딩 Tailwind 색상·`!important` 전면 금지**
- 접근성: `<button type="button">`, 아이콘 전용은 `aria-label`, 폼 요소는 `aria-label`/`<label>` 필수
위반 시 리뷰 단계에서 반려 대상. 신규 페이지는 하단의 **"페이지 작성 표준 템플릿"** 을 시작점으로 사용한다.
---
## 모노레포 구조
```
kcg-ai-monitoring/
├── frontend/ # React 19 + TypeScript + Vite (UI)
├── backend/ # Spring Boot 3.x + Java 21 (인증/권한/감사 + 분석 API)
├── prediction/ # Python 3.9 + FastAPI (AIS 분석 엔진, 5분 주기)
├── database/ # PostgreSQL 마이그레이션 (Flyway V001~V016, 48 테이블)
│ └── migration/
├── deploy/ # 배포 가이드 + 서버 설정 문서
├── docs/ # 프로젝트 문서 (SFR, 아키텍처)
├── .gitea/ # Gitea Actions CI/CD (프론트 자동배포)
├── .claude/ # Claude Code 워크플로우
├── .githooks/ # Git hooks
└── Makefile # 통합 dev/build 명령
```
## 시스템 구성
```
[Frontend Vite :5173] ──→ [Backend Spring :8080] ──→ [PostgreSQL kcgaidb]
↑ write
[Prediction FastAPI :18092] ─────┘ (5분 주기 분석 결과 저장)
↑ read
[SNPDB PostgreSQL] (AIS 원본)
```
- **자체 백엔드**: 인증/권한/감사로그/관리자 + 운영자 의사결정 (확정/제외/학습) + prediction 분석 결과 조회 API (`/api/analysis/*`)
- **Prediction**: AIS → 분석 결과를 kcgaidb 에 직접 write (백엔드 호출 없음)
- **DB 공유 아키텍처**: 백엔드와 prediction 은 HTTP 호출 없이 kcgaidb 를 통해서만 연동
## 명령어
```bash
make install # 전체 의존성 설치
make dev # 프론트 + 백엔드 동시 실행
make dev-all # 프론트 + 백엔드 + prediction 동시 실행
make dev-frontend # 프론트만
make dev-backend # 백엔드만
make dev-prediction # prediction 분석 엔진만 (FastAPI :8001)
make build # 전체 빌드
make lint # 프론트 lint
make format # 프론트 prettier
```
## 기술 스택
### Frontend (`frontend/`)
- React 19, TypeScript 5.9, Vite 8
- Tailwind CSS 4 + CVA
- MapLibre GL 5 + deck.gl 9 (지도)
- ECharts 6 (차트)
- Zustand 5 (상태관리)
- i18next (ko/en)
- React Router 7
- ESLint 10 + Prettier
### Prediction (`prediction/`) — 분석 엔진
- Python 3.11+, FastAPI, APScheduler
- 14개 알고리즘 (어구 추론, 다크베셀, 스푸핑, 환적, 위험도 등)
- 7단계 분류 파이프라인 (전처리→행동→리샘플→특징→분류→클러스터→계절)
- AIS 원본: SNPDB (5분 증분), 결과: kcgaidb (직접 write)
- prediction과 backend는 DB만 공유 (HTTP 호출 X)
### Backend (`backend/`)
- Spring Boot 3.x + Java 21
- Spring Security + JWT
- PostgreSQL + Flyway
- Caffeine (권한 캐싱)
- 트리 기반 RBAC (wing 패턴)
### Database (`kcgaidb`)
- PostgreSQL
- 사용자: `kcg-app`
- 스키마: `kcg`
## 배포 환경
| 서비스 | 서버 (SSH) | 포트 | 관리 |
|---|---|---|---|
| 프론트엔드 | rocky-211 | nginx 443 | Gitea Actions 자동배포 |
| 백엔드 | rocky-211 | 18080 | `systemctl restart kcg-ai-backend` |
| prediction | redis-211 | 18092 | `systemctl restart kcg-ai-prediction` |
- **URL**: https://kcg-ai-monitoring.gc-si.dev
- **배포 상세**: `deploy/README.md` 참조
- **CI/CD**: `.gitea/workflows/deploy.yml` (프론트만 자동, 백엔드/prediction 수동)
## 권한 체계
좌측 탭(메뉴) = 권한 그룹, 내부 패널/액션 = 자식 자원, CRUD 단위 개별 제어.
상세는 `.claude/plans/vast-tinkering-knuth.md` 참조.
## 팀 컨벤션
- 팀 규칙: `.claude/rules/`
- 커밋: Conventional Commits (한국어), `.githooks/commit-msg` 검증
- pre-commit: `frontend/` 디렉토리 기준 TypeScript + ESLint 검증
## 디자인 시스템 (필수 준수)
프론트엔드 UI는 **`/design-system.html` 쇼케이스를 단일 진실 공급원(SSOT)** 으로 한다.
모든 페이지/컴포넌트는 쇼케이스에 정의된 컴포넌트와 토큰만 사용한다.
### 쇼케이스 진입
- **URL**: https://kcg-ai-monitoring.gc-si.dev/design-system.html (메인 SPA와 별개)
- **소스**: `frontend/design-system.html` + `frontend/src/designSystemMain.tsx` + `frontend/src/design-system/`
- **추적 ID 체계**: `TRK-<카테고리>-<슬러그>` (예: `TRK-BADGE-critical-sm`)
- 호버 시 툴팁, "ID 복사 모드"에서 클릭 시 클립보드 복사
- URL 해시 딥링크: `#trk=TRK-BUTTON-primary-md`
- **단축키 `A`**: 다크/라이트 테마 토글
### 공통 컴포넌트 (반드시 사용)
| 컴포넌트 | 위치 | 용도 |
|---|---|---|
| `Badge` | `@shared/components/ui/badge` | 8 intent × 4 size, **className으로 색상 override 금지** |
| `Button` | `@shared/components/ui/button` | 5 variant × 3 size (primary/secondary/ghost/outline/destructive) |
| `Input` / `Select` / `Textarea` / `Checkbox` / `Radio` | `@shared/components/ui/` | 폼 요소 (Select는 aria-label 타입 강제) |
| `TabBar` / `TabButton` | `@shared/components/ui/tabs` | underline / pill / segmented 3 variant |
| `Card` / `CardHeader` / `CardTitle` / `CardContent` | `@shared/components/ui/card` | 4 variant |
| `PageContainer` | `@shared/components/layout` | 페이지 루트 (size sm/md/lg + fullBleed) |
| `PageHeader` | `@shared/components/layout` | 페이지 헤더 (icon + title + description + demo + actions) |
| `Section` | `@shared/components/layout` | Card + CardHeader + CardTitle + CardContent 단축 |
### 카탈로그 기반 라벨/색상
분류 데이터는 `frontend/src/shared/constants/`의 19+ 카탈로그를 참조한다.
중앙 레지스트리는 `catalogRegistry.ts`이며, 쇼케이스가 자동 열거한다.
```tsx
import { Badge } from '@shared/components/ui/badge';
import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels';
<Badge intent={getAlertLevelIntent(event.level)} size="sm">
{getAlertLevelLabel(event.level, t, lang)}
</Badge>
```
ad-hoc 한글/영문 상태 문자열은 `getStatusIntent()` (statusIntent.ts) 사용.
숫자 위험도는 `getRiskIntent(0~100)` 사용.
### CSS 작성 규칙
1. **인라인 색상 금지**`style={{ backgroundColor: '#ef4444' }}` 같은 정적 색상은 작성 금지
- 예외: 동적 데이터 기반 (`backgroundColor: meta.hex`, progress width `${value}%`)
2. **하드코딩 Tailwind 색상 금지**`bg-red-500/20 text-red-400` 같은 직접 작성 금지
- 반드시 Badge intent 또는 카탈로그 API 호출
3. **className override 정책**
- ✅ 레이아웃/위치 보정: `<Badge intent="info" className="w-full justify-center">`
- ❌ 색상/글자 크기 override: `<Badge intent="info" className="bg-red-500 text-xs">`
4. **시맨틱 토큰 우선**`theme.css @layer utilities`의 토큰 사용
- `text-heading` / `text-label` / `text-hint` / `text-on-vivid` / `text-on-bright`
- `bg-surface-raised` / `bg-surface-overlay` / `bg-card` / `bg-background`
5. **!important 절대 금지** — `cn()` + `tailwind-merge`로 충돌 해결
6. **`-webkit-` 벤더 prefix** — 수동 작성 CSS는 `backdrop-filter` 등 prefix 직접 추가 (Tailwind는 자동)
### 페이지 작성 표준 템플릿
```tsx
import { PageContainer, PageHeader, Section } from '@shared/components/layout';
import { Button } from '@shared/components/ui/button';
import { Badge } from '@shared/components/ui/badge';
import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels';
import { Shield, Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export function MyPage() {
const { t, i18n } = useTranslation('common');
const lang = i18n.language as 'ko' | 'en';
return (
<PageContainer>
<PageHeader
icon={Shield}
iconColor="text-blue-400"
title="페이지 제목"
description="페이지 설명"
actions={
<Button variant="primary" icon={<Plus className="w-4 h-4" />}>
추가
</Button>
}
/>
<Section title="데이터 목록">
<Badge intent={getAlertLevelIntent('HIGH')} size="sm">
{getAlertLevelLabel('HIGH', t, lang)}
</Badge>
</Section>
</PageContainer>
);
}
```
### 접근성 (a11y) 필수
- **`<button>`**: `type="button"` 명시 + 아이콘 전용은 `aria-label` 필수
- **`<input>` / `<textarea>` / `<select>`**: `aria-label` 또는 `<label htmlFor>` 필수
- **`Select` 컴포넌트**: TypeScript union type으로 `aria-label`/`aria-labelledby`/`title` 중 하나 컴파일 타임 강제
- 위반 시 WCAG 2.1 Level A 위반 + axe DevTools 경고
### 변경 사이클
1. 디자인 변경이 필요하면 → **쇼케이스에서 먼저 미세조정** → 시각 검증
2. 카탈로그 라벨/색상 변경 → `shared/constants/*` 또는 `variantMeta.ts`만 수정
3. 컴포넌트 변형 추가 → `lib/theme/variants.ts` CVA에만 추가
4. 실 페이지는 **컴포넌트만 사용**, 변경 시 자동 반영
### 금지 패턴 체크리스트
-`<Badge className="bg-red-500/20 text-red-400">``<Badge intent="critical">`
-`<button className="bg-blue-600 ...">``<Button variant="primary">`
-`<input className="bg-surface ...">``<Input>`
-`<div className="p-5 space-y-4">` 페이지 루트 → `<PageContainer>`
-`-m-4` negative margin 해킹 → `<PageContainer fullBleed>`
-`style={{ color: '#ef4444' }}` 정적 색상 → 시맨틱 토큰 또는 카탈로그
-`!important``cn()` 활용
- ❌ 페이지 내 `const STATUS_COLORS = {...}` 로컬 재정의 → shared/constants 카탈로그
## System Flow 뷰어 (개발 단계용)
- **URL**: https://kcg-ai-monitoring.gc-si.dev/system-flow.html (메인 SPA와 별개)
- **소스**: `frontend/system-flow.html` + `frontend/src/systemFlowMain.tsx` + `frontend/src/flow/`
- **매니페스트**: `frontend/src/flow/manifest/` (10개 카테고리 JSON + meta.json + edges.json)
- **노드 ID 명명**: `<category>.<snake_case>` (예: `output.event_generator`, `ui.parent_review`)
- **딥링크**: `/system-flow.html#node=<node_id>` — 산출문서에서 노드 직접 참조
- **가이드**: `docs/system-flow-guide.md` 참조
### `/version` 스킬 사후 처리 (필수)
`/version` 스킬을 실행하여 새 SemVer 버전이 결정되면, Claude는 이어서 다음 작업을 **자동으로** 수행한다 (`/version` 스킬 자체는 팀 공통 파일이라 직접 수정하지 않음):
1. **manifest 동기화**: `/version`이 결정한 새 버전을 `frontend/src/flow/manifest/meta.json`에 반영
- `version`: 새 SemVer (예: `"1.2.0"`)
- `updatedAt`: 현재 ISO datetime (`new Date().toISOString()`)
- `releaseDate`: 오늘 날짜 (`YYYY-MM-DD`)
2. **같은 커밋에 포함**: `frontend/src/flow/manifest/meta.json``/version` 스킬이 만든 커밋에 amend하거나, `docs: VERSION-HISTORY 갱신 + system-flow manifest 동기화`로 통합 커밋
3. **서버 archive는 CI/CD가 자동 처리**: 별도 작업 불필요. main 머지 후 Gitea Actions가 빌드 + dist 배포 + `/deploy/kcg-ai-monitoring-archive/system-flow/v{version}_{date}/`에 스냅샷 영구 보존
### 노드 ID 안정성
- **노드 ID는 절대 변경 금지** (산출문서가 참조하므로 깨짐)
- 노드 제거 시 `status: 'deprecated'`로 마킹 (1~2 릴리즈 유지 후 삭제)
- 새 노드 추가 시 `status: 'implemented'` 또는 `'planned'`