Compare commits

...

9 커밋

작성자 SHA1 메시지 날짜
2e674ccc5b Merge pull request 'fix(db): candidate_source 컬럼 VARCHAR(30)→(100) 확장 (V031)' (#84) from fix/candidate-source-length into develop 2026-04-17 11:53:08 +09:00
2b25dc1c92 docs: 릴리즈 노트 업데이트 2026-04-17 11:52:08 +09:00
0f4a9cb7d6 fix(db): candidate_source 컬럼 VARCHAR(30)→(100) 확장 (V031)
gear_group_parent_candidate_snapshots.candidate_source 의 VARCHAR(30) 제약
때문에 prediction gear_correlation 스테이지가 매 사이클 실패하던 문제 해소.

원인:
- prediction/algorithms/gear_parent_inference.py:875 의
  candidate_source = ','.join(sorted(meta['sources']))
  가 복수 source 라벨 (CORRELATION/EPISODE/LABEL/LINEAGE/MATCH) 을 쉼표 join
  하며 최대 약 39자. VARCHAR(30) 초과 시 psycopg2.errors.StringDataRightTruncation
  을 유발해 _insert_candidate_snapshots 전체 ROLLBACK.

발견 경위:
- Phase 0-1 (PR #83) 의 stage_runner + logger.exception 전환 후 journal 에
  찍힌 풀 스택트레이스로 드러남. 기존에는 logger.warning 한 줄 ("gear
  correlation failed: ...") 만 남아 원인 특정 불가.

영향 범위:
- 백엔드 JPA 엔티티 미참조 → 재빌드·재배포 불필요
- Flyway 자동 적용 (백엔드 기동 시)
- prediction 재기동만 필요 (기존 코드 그대로, 이제 INSERT 성공 기대)

검증:
- 재배포 후 journalctl 에서 'gear correlation failed' 로그 사라짐 확인
- kcg.gear_group_parent_candidate_snapshots 에 최근 15분 건수 증가 확인
2026-04-17 11:51:35 +09:00
3e29bc9995 Merge pull request 'refactor(prediction): 사이클 스테이지 에러 경계 도입 (Phase 0-1)' (#83) from feature/phase0-cycle-error-boundary into develop 2026-04-17 11:30:17 +09:00
a32d09f75a docs: 릴리즈 노트 업데이트 2026-04-17 11:28:58 +09:00
197da13826 refactor(prediction): 사이클 스테이지 에러 경계 도입 (Phase 0-1)
docs/prediction-analysis.md P1 권고 반영. 5분 사이클의 각 스테이지를
한 try/except 로 뭉친 기존 구조를 스테이지 단위로 분리해 실패 지점을
명시적으로 특정하고 부분 실패 시에도 후속 스테이지가 계속 돌아가도록 개선.

- prediction/pipeline/stage_runner.py 신설
  - run_stage(name, fn, *args, required=False, **kwargs) 유틸
  - required=True 면 예외 re-raise (상위 사이클 try/except 가 잡도록)
  - required=False 면 logger.exception 으로 stacktrace 보존 + None 반환
  - 지속시간 로깅 포함

- prediction/scheduler.py run_analysis_cycle() 수정
  - 출력 단계 6모듈을 각각 run_stage() 로 분리:
    violation_classifier / event_generator / kpi_writer /
    stats_aggregate_hourly / stats_aggregate_daily / alert_dispatcher
  - upsert_results / cleanup_old 도 run_stage 로 래핑 (upsert 는 required=True)
  - 내부 try/except 의 logger.warning → logger.exception 으로 업그레이드
    (fetch_dark_history, gear collision event promotion, group polygon,
     gear correlation, pair detection, chat cache)
  - 스테이지 실패 시 journalctl -u kcg-ai-prediction 에서 stacktrace 로
    원인 바로 특정 가능 (기존은 "failed: X" 한 줄만 남아 디버깅 불가)

검증:
- python3 -c "import ast; ast.parse(...)" scheduler.py / stage_runner.py 통과
- run_stage smoke test (정상/실패 흡수/required 재raise 3가지) 통과

범위 밖 (후속):
- Phase 0-2 ILLEGAL_FISHING_PATTERN 전용 페이지 (다음 MR)
- Phase 0-3 Transshipment 전용 페이지 (다음 MR)
2026-04-17 11:28:30 +09:00
bae2dbde08 Merge pull request 'docs: prediction-analysis 신규 + 루트/SFR 문서 drift 해소' (#82) from feature/docs-refresh-2026-04-17 into develop 2026-04-17 11:22:41 +09:00
451f38036a docs: 릴리즈 노트 업데이트 2026-04-17 11:21:56 +09:00
b37e18d952 docs: prediction-analysis 신규 + 루트/SFR 문서 drift 해소
- docs/prediction-analysis.md 신설 — opus 4.7 독립 리뷰 기반 prediction 구조/방향 심층 분석
  (9개 섹션: 아키텍처·5분 사이클·17 알고리즘·4대 도메인 커버리지·6축 구조 평가·개선 제안 P1~P4·임계값 전수표)
- AGENTS.md / README.md — V001~V016→V030, Python 3.9→3.11+, 14→17 알고리즘 모듈
- docs/architecture.md — /gear-collision 라우트 추가 (26→27 보호 경로)
- docs/sfr-traceability.md — V029→V030, 48→51 테이블, SFR-10 에 GEAR_IDENTITY_COLLISION 추가
- docs/sfr-user-guide.md — 어구 정체성 충돌 페이지 섹션 신설
- docs/system-flow-guide.md — 노드 수 102→115, V030 manifest 미반영 경고
- backend/README.md — "Phase 2 예정" 상태 → 실제 운영 구성 + PR #79 hotfix 요구사항 전면 재작성
2026-04-17 11:20:53 +09:00
12개의 변경된 파일753개의 추가작업 그리고 56개의 파일을 삭제

248
AGENTS.md Normal file
파일 보기

@ -0,0 +1,248 @@
# KCG AI Monitoring (모노레포)
해양경찰청 AI 기반 불법어선 탐지 및 단속 지원 플랫폼
## 모노레포 구조
```
kcg-ai-monitoring/
├── frontend/ # React 19 + TypeScript + Vite (UI)
├── backend/ # Spring Boot 3.x + Java 21 (인증/권한/감사 + 분석 API)
├── prediction/ # Python 3.11+ + FastAPI (AIS 분석 엔진, 5분 주기)
├── database/ # PostgreSQL 마이그레이션 참조용 README (실제 Flyway 파일은 backend/src/main/resources/db/migration/ V001~V030, 51 테이블)
│ └── migration/
├── deploy/ # 배포 가이드 + 서버 설정 문서
├── docs/ # 프로젝트 문서 (SFR, 아키텍처)
├── .gitea/ # Gitea Actions CI/CD (프론트 자동배포)
├── .Codex/ # Codex 워크플로우
├── .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
- 17개 알고리즘 모듈 (다크베셀, 스푸핑, 환적, 어구 상관·부모·정체성 충돌, 쌍끌이, 위험도, 어선 분류 등)
- 7단계 분류 파이프라인 (전처리→행동→리샘플→특징→분류→클러스터→계절)
- AIS 원본: SNPDB (5분 증분), 결과: kcgaidb (직접 write)
- prediction과 backend는 DB만 공유 (HTTP 호출 X, 단 실시간 상태 조회용 FastAPI 프록시 `/api/prediction/*` 예외)
### 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 단위 개별 제어.
상세는 `.Codex/plans/vast-tinkering-knuth.md` 참조.
## 팀 컨벤션
- 팀 규칙: `.Codex/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 버전이 결정되면, Codex는 이어서 다음 작업을 **자동으로** 수행한다 (`/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'`

파일 보기

@ -51,6 +51,7 @@ src/
| [docs/page-workflow.md](docs/page-workflow.md) | 31개 페이지 역할 + 4개 업무 파이프라인 |
| [docs/data-sharing-analysis.md](docs/data-sharing-analysis.md) | 데이터 공유 분석 + mock 통합 결과 |
| [docs/next-refactoring.md](docs/next-refactoring.md) | 다음 단계 TODO (API 연동, 실시간, 코드 스플리팅) |
| [docs/prediction-analysis.md](docs/prediction-analysis.md) | Prediction 모듈 구조/방향 심층 분석 (2026-04-17, opus 4.7 독립 리뷰) |
## SFR 요구사항 대응 현황
@ -67,7 +68,7 @@ src/
| SFR-07 | 단일함정 순찰경로 | `/patrol-route` | UI 완료 |
| SFR-08 | 다함정 경로최적화 | `/fleet-optimization` | UI 완료 |
| SFR-09 | Dark Vessel 탐지 | `/dark-vessel` | UI 완료 |
| SFR-10 | 어구 탐지 | `/gear-detection` | UI 완료 |
| SFR-10 | 어구 탐지 | `/gear-detection`, `/gear-collision`(V030) | UI 완료 |
| SFR-11 | 단속·탐지 이력 | `/enforcement-history` | UI 완료 |
| SFR-12 | 모니터링 대시보드 | `/dashboard`, `/monitoring` | UI 완료 |
| SFR-13 | 통계·성과 분석 | `/statistics` | UI 완료 |

파일 보기

@ -1,18 +1,88 @@
# Backend (Spring Boot)
Phase 2에서 초기화 예정.
운영 배포 중 — rocky-211 `:18080` (`kcg-ai-backend` systemd).
## 계획된 구성
- Spring Boot 3.x + Java 21
- PostgreSQL + Flyway
- Spring Security + JWT
- Caffeine 캐시
- 트리 기반 RBAC 권한 체계 (wing 패턴)
## 구성
- **Runtime**: Spring Boot 3.5.7 + Java 21
- **DB**: PostgreSQL (kcgaidb) + Flyway V001~V030 (51 테이블)
- **Auth**: Spring Security + JWT 쿠키 + BCrypt
- **Cache**: Caffeine (권한 트리 10분 TTL)
- **Permission**: 트리 기반 RBAC (47 리소스 × 5 operation)
- **HTTP client**: `RestClient` + 명시적 `@Bean` 주입 (`predictionRestClient`, `signalBatchRestClient`)
## 책임
- 자체 인증/권한/감사로그
- 운영자 의사결정 (모선 확정/제외/학습)
- prediction 분석 결과 조회 API (`/api/analysis/*`)
- 관리자 화면 API
상세 설계: `.claude/plans/vast-tinkering-knuth.md`
- 자체 인증/권한/감사 로그 + 관리자 API
- 운영자 의사결정 (모선 확정·제외·학습, 어구 정체성 충돌 분류)
- prediction 분석 결과 조회 (`/api/analysis/*`) + 이벤트 허브 (`/api/events`, `/api/alerts`)
- prediction 실시간 상태 프록시 (`/api/prediction/*`)
## 빌드 · 배포
```bash
# 로컬 실행
./mvnw spring-boot:run
# 프로덕션 빌드
./mvnw clean package -DskipTests
# → target/kcg-ai-backend-*.jar
# 운영 배포 (수동)
scp target/kcg-ai-backend-*.jar rocky-211:/opt/kcg-ai-backend/
ssh rocky-211 "sudo systemctl restart kcg-ai-backend"
```
## 필수 컴파일 설정 (PR #79 hotfix)
Spring 6.1 의 parameter-level `@Qualifier` 주입이 동작하려면 두 가지가 **필수**:
1. **`pom.xml`** — `maven-compiler-plugin``default-compile``default-testCompile` 양쪽 execution 에
`<parameters>true</parameters>` 설정. 파라미터 이름을 바이트코드에 보존해야 `@Qualifier` resolve 가 가능.
2. **`src/main/java/lombok.config`** — `lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier`
설정. Lombok `@RequiredArgsConstructor` 가 필드의 `@Qualifier` 를 생성자 파라미터에 복사해야 Spring 이 인식.
둘 중 하나라도 누락되면 `PredictionProxyController` 같은 다중 `RestClient` bean 주입 컨트롤러가 기동 시점에
`NoUniqueBeanDefinitionException` 으로 크래시 루프에 빠진다. 로컬에서 `./mvnw spring-boot:run` 실패는
운영 restart 시 동일하게 재현되므로 **MR 범위 밖이어도 우선 해결**.
## 주요 패키지 구조
```
src/main/java/gc/mda/kcg/
├── config/ # RestClientConfig, SecurityConfig, OpenApi, CorsConfig, CaffeineConfig 등
├── auth/ # LoginController, JWT, 비밀번호 정책
├── permission/ # 트리 RBAC, @RequirePermission AOP, 캐시
├── audit/ # @Auditable AOP, AuditLogService, AccessLogFilter
├── domain/
│ ├── analysis/ # VesselAnalysisController, GearCollisionController (V030), PredictionProxyController 등
│ ├── fleet/ # ParentInferenceWorkflowController, FleetService
│ ├── event/ # EventController, AlertController
│ ├── enforcement/ # EnforcementController, EnforcementPlanController
│ ├── master/ # MasterDataController (CodeMaster, GearTypeMaster, VesselPermit 등)
│ ├── admin/ # AdminLogController, AdminStatsController, UserManagementController
│ └── stats/ # StatsController (KPI 집계)
└── Application.java
```
모든 컨트롤러는 `controller → service → repository` 계층을 준수하며, 쓰기 액션은
`@RequirePermission` + `@Auditable` 로 권한·감사 일관 적용.
## Flyway 마이그레이션
- 경로: [src/main/resources/db/migration/V001__*.sql ~ V030__gear_identity_collision.sql](src/main/resources/db/migration/)
- 최신: **V030** (2026-04-17) — `gear_identity_collisions` 테이블 + `detection:gear-collision` 권한 트리
- Flyway 자동 적용: Spring Boot 기동 시점
## 디렉토리 밖 의존성
- **prediction → kcgaidb**: prediction 이 직접 write. backend 는 HTTP 호출 없이 DB 조회로만 연동
- **signal-batch**: `http://192.168.1.18:18090/signal-batch` (정적정보 보강, `signalBatchRestClient` 주입)
- **prediction (FastAPI)**: `http://redis-211:18092` (실시간 상태/채팅 프록시 전용)
## 운영 체크리스트
1. 빌드 성공 → local `./mvnw spring-boot:run` 기동 확인 → curl `/api/auth/me` 200 확인
2. scp 배포 → `systemctl restart kcg-ai-backend``journalctl -u kcg-ai-backend -n 100`
3. 응답 확인: `curl -k https://kcg-ai-monitoring.gc-si.dev/api/analysis/vessels?hours=1`
4. Flyway 에러 시: `backend.application` 로그에서 `Migration ... failed` 확인

파일 보기

@ -0,0 +1,20 @@
-- V031: gear_group_parent_candidate_snapshots.candidate_source VARCHAR(30) → VARCHAR(100)
--
-- 원인: prediction/algorithms/gear_parent_inference.py 의 _rank_candidates 가
-- candidate_source = ','.join(sorted(meta['sources'])) (line 875)
-- 로 여러 source 라벨(CORRELATION / EPISODE / LABEL / LINEAGE / MATCH 등) 을 쉼표로
-- join 해 저장한다. 모든 source 가 맞춰지면 약 39자(5개 라벨 + 쉼표) 에 달해
-- VARCHAR(30) 를 초과하며 psycopg2.errors.StringDataRightTruncation 을 유발.
--
-- 영향: prediction 사이클의 gear_correlation 스테이지에서 `_insert_candidate_snapshots`
-- 실패 → `gear_group_parent_candidate_snapshots` 테이블 갱신 전체 스킵.
-- Phase 0-1 의 사이클 스테이지 에러 격리 덕분에 후속 스테이지(pair_detection /
-- per-vessel analysis / upsert_results / 출력 모듈들)는 정상 완주.
--
-- 대안 검토:
-- (A) VARCHAR(100) — 여유치. 채택.
-- (B) TEXT — 제약 없음. 컬럼 의미가 "짧은 라벨 집합" 이라 VARCHAR 가 자연스러움.
-- (C) 코드에서 [:30] trim — 의미 손실(source 목록 절단). 부적합.
ALTER TABLE kcg.gear_group_parent_candidate_snapshots
ALTER COLUMN candidate_source TYPE VARCHAR(100);

파일 보기

@ -4,6 +4,16 @@
## [Unreleased]
### 수정
- **gear_group_parent_candidate_snapshots.candidate_source VARCHAR(30)→(100) 확장 (V031)** — prediction `gear_parent_inference` 가 여러 source 라벨을 쉼표로 join 한 값(최대 ~39자)이 VARCHAR(30) 제약을 넘어 매 사이클 `StringDataRightTruncation` 으로 gear correlation 스테이지 전체가 실패하던 기존 버그. Phase 0-1 (PR #83) 의 `logger.exception` 전환으로 풀 stacktrace 가 journal 에 찍히면서 원인 특정. backend JPA 엔티티 미참조로 재빌드 불필요, Flyway 자동 적용, prediction 재기동만으로 해소
### 변경
- **Prediction 5분 사이클 스테이지 에러 경계 도입 (Phase 0-1)**`prediction/pipeline/stage_runner.py` 신설해 `run_stage(name, fn, required=False)` 유틸 제공. `scheduler.py run_analysis_cycle()` 의 출력 6모듈(violation_classifier / event_generator / kpi_writer / stats_aggregate_hourly / stats_aggregate_daily / alert_dispatcher)을 한 try/except 로 묶던 구조를 스테이지별 독립 실행으로 분리, 한 모듈이 깨져도 다른 모듈이 계속 돌아가도록 개선. `upsert_results` 는 required=True 로 실패 시 사이클 abort. 내부 try/except 의 `logger.warning``logger.exception` 으로 업그레이드(fetch_dark_history / gear collision event promotion / group polygon / gear correlation / pair detection / chat cache)하여 `journalctl -u kcg-ai-prediction` 에서 stacktrace 로 실패 지점 즉시 특정 가능. (docs/prediction-analysis.md P1 권고)
### 문서
- **Prediction 모듈 심층 분석 리포트 신설**`docs/prediction-analysis.md` (9개 섹션, 250 라인). opus 4.7 독립 리뷰 관점에서 현재 17 알고리즘의 레이어 분리·5분 사이클 시퀀스·4대 도메인 커버리지를 평가하고, 6축(관심사 분리/재사용성/테스트 가능성/에러 격리/동시성/설정 가능성)으로 구조 채점 + P1~P4 개선 제안·임계값 전수표 제공
- **루트·SFR 문서 drift 해소** — V001~V016 → V030 + 51 테이블, Python 3.9 → 3.11+, 14 → 17 알고리즘 모듈 실측 반영. SFR-10 에 GEAR_IDENTITY_COLLISION 패턴 + GearCollisionDetection 페이지 섹션 추가 (sfr-traceability/sfr-user-guide), `/gear-collision` 라우트 architecture.md 포함, system-flow-guide 노드 수 102→115 + V030 manifest 미반영 경고, backend/README "Phase 2 예정" 상태 → 실제 운영 구성 전면 재작성 (PR #79 hotfix 요구사항 명시)
## [2026-04-17.4]
### 수정

파일 보기

@ -318,7 +318,7 @@ deps 변경 → useMapLayers → RAF → overlay.setProps() (React 리렌
---
## 라우팅 구조 (26 보호 경로 + login)
## 라우팅 구조 (27 보호 경로 + login)
`App.tsx`에서 `BrowserRouter` > `AuthProvider` > `Routes`로 구성된다.
@ -331,6 +331,7 @@ deps 변경 → useMapLayers → RAF → overlay.setProps() (React 리렌
- `/enforcement-plan` — 단속계획 (SFR-06)
- `/dark-vessel` — 무등화 선박 탐지 (SFR-09)
- `/gear-detection` — 어구 탐지 (SFR-10)
- `/gear-collision` — 어구 정체성 충돌 (SFR-10, V030 — 동일 어구 이름 × 복수 MMSI 공존 감지)
- `/china-fishing` — 중국어선 탐지
- `/patrol-route` — 순찰경로 (SFR-07)
- `/fleet-optimization` — 함대 최적화 (SFR-08)

250
docs/prediction-analysis.md Normal file
파일 보기

@ -0,0 +1,250 @@
# Prediction 모듈 심층 분석 — 구조·방향 리뷰
**대상:** `prediction/` (Python 3.11+, FastAPI, APScheduler, 59 `.py` 파일)
**작성일:** 2026-04-17
**작성 관점:** opus 4.7 독립 리뷰 — 정밀도 튜닝이 아닌 **방향성·코드 구조**
**전제:** 프로토타입·데모 단계. 정밀도 미흡은 인지된 상태.
---
## TL;DR — 3줄 요약
1. **뼈대는 튼튼하다.** 레이어 분리(algorithms/pipeline/output/db/cache), 순수함수 위주 알고리즘, 카테고리별 dedup 윈도우 분리까지 프로토타입치고는 일관된 설계.
2. **약한 고리는 오케스트레이터.** [scheduler.py run_analysis_cycle()](../prediction/scheduler.py) 한 함수가 700+ 라인, 지역 try/except + `logger.warning` 로 흡수된 실패가 많아 "어디서 깨졌는지 조용히 묻힌다". 상태 누적(모듈 전역 `_transship_pair_history`)도 여기 묶여 있음.
3. **커버리지 매트릭스는 4/4 이지만 UI 비대칭.** prediction 이 생산하는 결과 중 `ILLEGAL_FISHING_PATTERN` 이벤트·환적 의심은 DB·백엔드까지 도달하지만 전용 detection UI 가 없다. prediction 품질 개선과 무관하게 운영자가 쓸 수 없는 상태.
**권고 최우선 3가지** — 신규 알고리즘 추가보다 아래가 선행:
- **P1:** 사이클 스테이지 단위 에러 경계(`_stage(...)` 유틸)로 교체해 실패 스테이지 명시 로깅 + 부분 실패 시에도 후속 단계 진행
- **P1:** 하드코딩 임계값(MID prefix, 커버리지 박스, SOG band, 11 pattern 점수) 을 `correlation_param_models` 패턴처럼 DB/config 로 외부화
- **P1:** 환적 전용 + ILLEGAL_FISHING_PATTERN 전용 프론트 페이지 추가 — 이미 DB·API 는 있음
---
## 1. 아키텍처 레이어 — 책임과 결합도
```
prediction/
├── config.py Pydantic Settings + qualified_table() — SSOT 설정
├── scheduler.py APScheduler + run_analysis_cycle() (단일 엔트리)
├── main.py FastAPI app + /health /status /chat 등
├── fleet_tracker.py 상태 보유 (선단 레지스트리, 어구 정체성)
├── time_bucket.py 안전 지연 12분 + backfill 3 버킷
├── algorithms/ 17개 모듈 — 순수함수 중심
├── pipeline/ 8개 모듈 — 7단계 분류 파이프라인
├── output/ 5개 모듈 — event/violation/kpi/stats/alert
├── db/ 4개 모듈 — snpdb / kcgdb / signal_api / partition_manager
├── cache/ vessel_store.py — 24h sliding window 인메모리
├── chat/ Ollama + RAG 스텁
├── models/ result.py — AnalysisResult dataclass
├── data/ monitoring zones JSON 정적 설정
└── tests/ time_bucket / gear_parent_episode / gear_parent_inference 3종
```
| 레이어 | 책임 | 결합도 평가 |
|---|---|---|
| `config.py` | 환경 + SQL identifier 검증 | ✅ SSOT, `qualified_table()` 로 스키마 주입 공격 방지 |
| `algorithms/` | 탐지 로직 (순수) | ✅ 대부분 `df, params -> dict/tuple`. 상호 의존 적음 |
| `pipeline/` | 7단계 sequential | ✅ `orchestrator.ChineseFishingVesselPipeline.run()` 이 DF 를 그대로 파이프 |
| `output/` | 룰 엔진 + DB write | ✅ 룰을 lambda 리스트(event_generator.RULES)로 선언적 관리 |
| `db/` | Connection pool + SQL | ⚠️ `kcgdb.upsert_results(results)` 가 한 트랜잭션에 전부 묶임 (파티션 unique index 활용은 적절) |
| `cache/vessel_store.py` | 전역 싱글턴, 24h 궤적 인메모리 | ⚠️ 모듈 싱글턴 → 테스트 시 mock 어려움 |
| `fleet_tracker.py` | 레지스트리·어구 정체성 상태 | ⚠️ 싱글턴 + 모듈 전역 캐시 |
| `scheduler.py` | 전체 오케스트레이션 | ❌ **700+ 라인 모놀리식** — 아래 §2 상세 |
---
## 2. 5분 사이클 시퀀스 — [scheduler.py:80-804](../prediction/scheduler.py#L80-L804)
사이클 전체가 **한 함수** 안에서 9단계로 진행된다.
| 단계 | 라인 | 역할 | 실패 처리 |
|---|---|---|---|
| 1. 증분 로드 | [97-106](../prediction/scheduler.py#L97-L106) | `snpdb.fetch_incremental()` → vessel_store merge | 전체 try/except 포함 |
| 2. 정적 보강 | [108-112](../prediction/scheduler.py#L108-L112) | signal-batch API 호출 | 전체 try/except |
| 3. 대상 선별 | [114-128](../prediction/scheduler.py#L114-L128) | SOG/COG 계산 + 0건 시 조기 return | ✅ 조기 반환 |
| 4. 파이프라인 | [122-128](../prediction/scheduler.py#L122-L128) | `ChineseFishingVesselPipeline.run()` | 전체 try/except |
| 5. 선단 분석 | [131-177](../prediction/scheduler.py#L131-L177) | fleet_tracker + gear_identity 충돌 감지 | ⚠️ 내부 try/except 로 warning, 전진 |
| 5.5. 어구 그룹·상관·부모 추론 | [181-229](../prediction/scheduler.py#L181-L229) | polygon_builder + gear_correlation | ⚠️ 내부 try/except, 결과 없이 진행 |
| 5.9. 쌍끌이 후보·판정 | [231-303](../prediction/scheduler.py#L231-L303) | pair_trawl STRONG/PROBABLE/SUSPECT | ⚠️ 내부 try/except |
| 6. 개별 선박 분석 | [305-515](../prediction/scheduler.py#L305-L515) | AnalysisResult 생성 (파이프라인 통과자) | 루프 내 continue |
| 6.5. 경량 분석 | [523-682](../prediction/scheduler.py#L523-L682) | 파이프라인 미통과 중국 MID — `compute_lightweight_risk_score` | 루프 내 try/except |
| 7. 환적 의심 | [685-713](../prediction/scheduler.py#L685-L713) | `detect_transshipment` + `_transship_pair_history` 누적 | 전체 try/except |
| 8. DB upsert | [716-717](../prediction/scheduler.py#L716-L717) | `kcgdb.upsert_results()` + `cleanup_old(48h)` | 전체 try/except |
| 9. 출력 모듈 | [720-745](../prediction/scheduler.py#L720-L745) | violation_classifier → event_generator → kpi_writer → aggregate_hourly/daily → alert_dispatcher | ⚠️ **5개 단계를 한 try/except 로 묶음** → 어디서 실패했는지 단일 warning 으로 흡수 |
| 10. 채팅 컨텍스트 캐싱 | [748-791](../prediction/scheduler.py#L748-L791) | Redis | 전체 try/except |
### 구조적 관찰
- **전체 try/except 는 있다** ([97](../prediction/scheduler.py#L97) ~ [803](../prediction/scheduler.py#L803)) — 치명 실패가 다음 사이클을 막지는 않음
- **그러나 내부 스테이지가 너무 무겁다.** 9번째 출력 단계가 5개 모듈을 한 덩어리로 묶어 `logger.warning('output modules failed (non-fatal): %s', e)` 로 흡수. 어느 모듈이 깨졌는지 디버깅하려면 stacktrace 를 `logger.exception` 으로 바꿔야 함
- **Lazy import** 가 스테이지마다 반복 (`from output.event_generator import ...` 등). 시작 시간 단축에는 도움이지만 import 오류가 **첫 사이클 실행 시점에만 드러남** — 배포 후 5분 지연 발견 경험 다수
### 권고 (사이클 구조 재정비)
```python
def _stage(name: str, fn, *args, required=False, **kwargs):
t0 = time.time()
try:
result = fn(*args, **kwargs)
logger.info('stage %s ok in %.2fs', name, time.time() - t0)
return result
except Exception as e:
logger.exception('stage %s failed: %s', name, e)
if required:
raise
return None
```
- 각 스테이지를 `_stage('pair_detection', _run_pair_detection, ...)` 로 감싸면 실패 스테이지 명시 로깅 + stacktrace + 부분 실패 허용 정책을 일관화.
- 9번 단계의 5개 모듈은 각각 별도 `_stage(...)` 호출로 쪼갤 것.
---
## 3. 알고리즘 카탈로그 (17 모듈 × 담당 도메인)
| 파일 | 주 역할 | 입력 | 출력 | 주요 상수 |
|---|---|---|---|---|
| [dark_vessel.py](../prediction/algorithms/dark_vessel.py) | AIS gap + 11 패턴 점수화 | 선박 DF(timestamp, lat, lon, sog, cog) | (score 0~100, patterns[], tier) | GAP_SUSPICIOUS=6000s, VIOLATION=86400s, KR_COVERAGE box |
| [spoofing.py](../prediction/algorithms/spoofing.py) | 텔레포트·극속·BD09 오프셋 | 선박 DF | spoofing_score 0~1 | EXTREME_SPEED=50kn (주석 기준), fishing max=25kn |
| [risk.py](../prediction/algorithms/risk.py) | 종합 risk + 경량 risk 2종 | DF, zone, is_permitted, 외부 점수 | (risk 0~100, level) | tier: 70+=CRITICAL, 50+=HIGH, 30+=MEDIUM |
| [fishing_pattern.py](../prediction/algorithms/fishing_pattern.py) | UCAF/UCFT gear SOG band | DF, gear | (ucaf, ucft) | PT 2.5~4.5, OT 2.0~4.0, GN 0.5~2.5 |
| [transshipment.py](../prediction/algorithms/transshipment.py) | 5단계 필터 파이프라인 | DF targets, pair_history, zone_fn | list of dict | PROXIMITY ~220m, RENDEZVOUS 90min, WATCH 제외 |
| [location.py](../prediction/algorithms/location.py) | zone 분류, haversine_nm, BD09 | (lat, lon) | zone, dist_nm | 12/24 NM 밴드 |
| [gear_correlation.py](../prediction/algorithms/gear_correlation.py) | 멀티모델 EMA + streak | vessel_store, gear_groups, conn | UPDATE gear_correlation_scores | α_base=0.30, polygon=0.70 |
| [gear_identity.py](../prediction/algorithms/gear_identity.py) | 공존 쌍 추출 (V030/PR #73) | gear_signals | collisions[] | CRITICAL_DIST=50km, COEXIST=3회 |
| [gear_parent_inference.py](../prediction/algorithms/gear_parent_inference.py) | 어구 → 모선 assignment | gear_groups + tracks | parent 후보 + confidence | 2-pass: direct-match → candidates |
| [gear_parent_episode.py](../prediction/algorithms/gear_parent_episode.py) | 에피소드 delineation (first_seen~last_seen) | gear_signals 시계열 | episodes[] | gap tolerance |
| [gear_violation.py](../prediction/algorithms/gear_violation.py) | G-01~G-06 통합 판정 (DAR-03) | DF + zone + pair_result + permits | g_codes[], evidence, score | G-06=20pts, G-02=18pts, G-01=15pts |
| [gear_name_rules.py](../prediction/algorithms/gear_name_rules.py) | 어구 이름 정규표현식 | string | parent_code (Optional) | regex set |
| [pair_trawl.py](../prediction/algorithms/pair_trawl.py) | 쌍끌이 tier 분류 | DF×2, 6h | (pair_detected, tier, pair_type) | PROXIMITY=0.27NM, MIN_SYNC=2h |
| [track_similarity.py](../prediction/algorithms/track_similarity.py) | DTW 궤적 유사도 | DF×2 | 0~1 | - |
| [fleet.py](../prediction/algorithms/fleet.py) | leader/follower/independent | DF, tracker | role | - |
| [polygon_builder.py](../prediction/algorithms/polygon_builder.py) | gear group convex hull | vessel_store, companies | 스냅샷[] | 시간버킷 |
| [vessel_type_mapping.py](../prediction/algorithms/vessel_type_mapping.py) | fishery_code → vessel_type 폴백 | fishery_code | 'TRAWL'/'PT'/... | - |
**관찰:**
- 대부분 **순수 함수** → 재사용·단위테스트 용이. 단 `gear_correlation.run_gear_correlation``conn` 을 받아 DB 를 직접 UPDATE 함 (알고리즘 + I/O 혼재)
- **상수가 모듈 상단에 모여 있는 것은 좋으나 config 외부화는 안 됨**. 현장 운영자가 임계값을 바꾸려면 코드 배포 필요
---
## 4. 4대 도메인 커버리지 매트릭스
| 도메인 | 담당 파일 | 접근 방식 | 구조적 강점 | 구조적 공백 |
|---|---|---|---|---|
| **Dark Vessel** | `dark_vessel.py` + `risk.py` + `compute_dark_suspicion` | gap(100분 임계) + 11 패턴 점수 + 4-tier (CRITICAL≥70) | 패턴 분리(P1~P11) + 한국 커버리지 감점(50pt)으로 자연 gap 구분 | MID/커버리지 박스 하드코딩. 감점 로직은 탐지 누락 위험(coverage box 바깥=자연 gap 가정) — 중국 EEZ 해역 근접 공해에서 의도적 OFF 가 감점 맞을 수 있음 |
| **Gear Tracking** | `gear_correlation` + `gear_identity` + `gear_parent_inference` + `polygon_builder` | 어구 이름 regex → 공존/교체 분기 + 멀티모델 EMA + 모선 추론 + 폴리곤 | 공존(GEAR_IDENTITY_COLLISION)을 1순위 증거로 재정의 (V030). SAVEPOINT 로 PK 충돌 격리 | 어구 신호는 **이름 regex 의존**. 이름이 GEAR_PATTERN 에 맞지 않으면 파이프라인 전체 진입 실패 |
| **Chinese Fishing** | `pipeline/` 7단계 + MID 412/413/414 필터 | preprocess → behavior → resample → feature → classify → cluster → seasonal + rule-based 분류 | 룰 + 군집으로 해석 가능 | MID prefix 하드코딩 2곳 중복. 전체 인구(55k)대비 500척만 파이프라인 통과 — 경량 분석(§6)으로 보완하나 여전히 <10% |
| **Illegal Pattern** | `gear_violation` (G-01~G-06) + `event_generator.RULES` (15+ 룰) + `transshipment` 5단계 | 임계값 기반 독립 룰 × 카테고리별 dedup 윈도우 | 룰이 lambda 리스트로 선언적 | **UI 미노출** — DB `prediction_events` 카테고리 `ILLEGAL_*` 는 생산되나 전용 detection UI 없음. 운영자는 EventList(`/event-list`) 에서만 조회. 환적도 동일 문제 |
---
## 5. 코드 구조 평가 (6축)
| 축 | 평가 | 근거 |
|---|---|---|
| **관심사 분리** | B+ | algorithms / pipeline / output / db 레이어는 깔끔. 단 scheduler 는 오케스트레이터가 아니라 **메가 함수** |
| **재사용성** | A- | 17 알고리즘 모듈 중 ~15개가 순수함수. `run_gear_correlation` 만 conn 혼재 |
| **테스트 가능성** | C+ | unit test 3개만 (time_bucket / gear_parent_episode / gear_parent_inference). `vessel_store` / `fleet_tracker` 싱글턴 → integration test 어려움 |
| **에러 격리** | C | 사이클 전체 try/except + 내부 지역 try/except 혼재. 출력 5모듈이 한 덩어리 → 실패 지점 특정 불가 |
| **동시성** | A- | `ThreadedConnectionPool(1,5)`, `max_instances=1` 스케줄러 — 단일 프로세스 가정 하에서 안전 |
| **설정 가능성** | C- | 임계값 대부분 파일 상수. `correlation_param_models` 패턴만 DB 기반 (예외) |
### 주목할 잘된 점
- **Dedup 윈도우 카테고리별 차등** ([event_generator.py:26-39](../prediction/output/event_generator.py#L26-L39)) — 5분 boundary 집단 만료를 피하기 위해 33/67/89/127/131/181/367분 등 **의도적으로 5의 배수 회피**. 룰 기반 탐지의 대표적인 "튜닝 knob" 이 코드에 명시.
- **gear_identity 공존/교체 분기** ([fleet_tracker.py](../prediction/fleet_tracker.py) 트랜잭션 설계) — SAVEPOINT 로 부분 실패를 사이클 전체 abort 와 분리. 이전 13h 공백 사고의 재발 방지 설계가 구조에 반영됨.
- **Lightweight path** — 파이프라인 통과 못한 중국 MID 를 경량 경로로 계속 커버 ([scheduler.py:523-682](../prediction/scheduler.py#L523-L682)). "정밀 vs 커버리지" 를 두 경로로 나누는 의사결정 자체는 탁월.
### 구조적 채무
- **환경 분기 부재**: `config.py` 에 dev/prod 분기가 없음. `.env` 파일 하나에 의존. 로컬 실행 시 운영 DB 를 건드리는 위험 ([config.py:8-22](../prediction/config.py#L8-L22))
- **상태 있는 모듈 전역 변수**: `_transship_pair_history`, `_last_run`, `_scheduler` ([scheduler.py:16-26](../prediction/scheduler.py#L16-L26)). 테스트 격리 어렵고, 재시작 시 pair 누적 상태 증발
- **DB 쓰기 산재**: `kcgdb.upsert_results` / `save_group_snapshots` / `gear_correlation UPDATE` / `gear_identity UPSERT` / `prediction_events INSERT` 가 서로 다른 트랜잭션. 한 사이클 원자성 X — 의도적일 수 있으나 명시 설계 문서 없음
---
## 6. 방향성 진단 — 프로토타입 → MVP → 운영
### 지금 강점
- **룰 기반 탐지를 탄탄히 다져둔 토대** — 임계값이 드러나 있고 dedup 설계가 명시적. 향후 ML overlay 를 얹을 때 "어디에 얹을지" 가 명확 (dark_suspicion score, transship score, gear_violation score 가 모두 연속값으로 산출)
- **운영자 의사결정 통합 설계** — V030 GEAR_IDENTITY_COLLISION 에서 status(OPEN/REVIEWED/CONFIRMED_ILLEGAL/FALSE_POSITIVE) 가 severity 재계산을 억제하는 패턴 — 사람 loop back 이 설계된 유일한 자리. 다른 도메인에도 이 패턴 확장 가능
### 지금 약점
- **ML 부재** — sklearn/torch 없음. 2026 기준 프로토타입으로는 적절하나, sequence anomaly (dark gap 의 시계열 반복 패턴) 나 behavioral classifier 는 룰만으론 한계
- **하드코딩 지대**: MID prefix(4개소), KR coverage box, SOG band, 11 패턴 점수, 5단계 transship 임계 — 모두 "이 프로젝트에서 튜닝해야 할 핫스팟" 인데 DB/config 분리 안 됨
- **UI 비대칭**: `prediction_events.category='ILLEGAL_FISHING_PATTERN'` 이 생산되지만 전용 UI 없음. 환적도 동일. 결과적으로 **prediction 이 만드는 가치의 일부가 운영자에게 도달하지 못함**
- **테스트 빈곤**: 17 알고리즘 중 3개만 유닛테스트. 사이클 단위 integration test 전혀 없음 — 사이클 회귀가 항상 운영 로그로만 드러남 (13h 공백 사고가 대표 사례)
---
## 7. 구조적 개선 제안 (우선순위별)
### P1 — 지금 해야 할 것 (운영 안정성)
1. **사이클 스테이지 단위 에러 경계**`_stage(name, fn, required=False)` 유틸로 9번 출력 5모듈을 쪼갤 것. `logger.exception` 으로 stacktrace 보존. `required=True``fetch_incremental` 같은 fatal 에만 적용 → 실패 시 조기 반환
2. **임계값 외부화**`correlation_param_models` 패턴을 확장해 `detection_params` 테이블 신설 (algo_name, param_key, value, active_from, active_to). 배포 없이 해상도 튜닝 가능. 운영자 권한으로 접근 시 감사 로그
3. **ILLEGAL_FISHING_PATTERN 전용 페이지** + **환적 전용 페이지** — 백엔드 API·DB 는 이미 존재. 프론트만 GearCollisionDetection 패턴으로 추가 (`PageContainer` + `DataTable` + `Badge intent`)
4. **사이클 부분 원자성 명시** — DB 쓰기 경계 문서화 (어디까지가 한 트랜잭션인지). 최소한 [architecture.md](architecture.md) 또는 신설 `docs/prediction-transactions.md` 에 다이어그램
### P2 — 다음 (품질 확보)
5. **알고리즘 유닛테스트 커버리지** — 17 모듈 중 최소 10 개 (dark_vessel 11 패턴 / transshipment 5단계 / gear_violation 6 G-code / spoofing / risk) 에 fixture 기반 테스트. `tests/fixtures/` 에 AIS DF CSV 샘플
6. **DB fixture integration test** — testcontainers-python 으로 PostgreSQL 띄워 한 사이클 실행 + 결과 테이블 assert. CI 에서 돌릴 수 있도록 데이터 10 척 x 1h 정도 경량
7. **`vessel_store` / `fleet_tracker` 의존성 주입 개편** — 모듈 싱글턴 → `AnalysisContext` dataclass 로 명시 주입. 테스트 mock 가능
8. **MID prefix·커버리지 box 를 `monitoring_zones` JSON 연장** — 이미 `data/monitoring_zones.json` 이 있음. 동일 포맷으로 `mid_prefixes.json` / `kr_coverage.json` 추가
### P3 — 중기 (가치 확장)
9. **ML overlay 타겟 설정** — dark_suspicion score (11 패턴 합산) 은 classifier training target 으로 최적. GCN/Transformer 로 **"gap 시퀀스가 의도적인가"** 를 학습. 룰 유지 + 게이트만 ML 로 대체 (shadow mode 로 비교)
10. **correlation 파라미터 MLOps 연동**`correlation_param_models` 를 MLflow 로 실험 기록 → 성능 좋은 모델 자동 active 전환
11. **AIS 벤치마크 데이터셋** — 한중어업협정 906척 중 과거 단속 이력 있는 선박을 positive label. 현재 매칭률 53%+ 이므로 샘플 확보 가능. tier 별 precision/recall 산출
### P4 — 장기 (스케일)
12. **multi-process / async** — APScheduler + 단일 스레드 한계. 현 55k 선박 / 2.3M points / 110초 사이클에서 8k 중국 증가 + 한국 확장 시 5분 주기 내 완료 불가 예측. asyncio + ray / dask 로 스테이지 병렬
13. **Event bus 분리** — 지금은 `prediction_events` INSERT 가 동기. outbox 패턴으로 비동기 분리 시 백엔드/프론트 실시간 push 기반 (WebSocket) 로 진화 가능
---
## 8. 부록 — 임계값 전수표 (외부화 우선순위)
| 위치 | 상수 | 현재값 | P1 외부화 | 비고 |
|---|---|---|---|---|
| [scheduler.py:28](../prediction/scheduler.py#L28) | `_KR_DOMESTIC_PREFIXES` | `('440','441')` | ✅ | 한국 MID |
| [scheduler.py:140,247](../prediction/scheduler.py#L140) | 중국 MID prefix | `'412' '413' '414'` 하드코딩 2곳 | ✅ | `mid_prefixes.json` |
| [scheduler.py:256](../prediction/scheduler.py#L256) | pair pool SOG band | `1.5 <= mean_sog <= 5.0` | ✅ | 조업 속력대 |
| [dark_vessel.py:6-8](../prediction/algorithms/dark_vessel.py#L6-L8) | GAP 임계 3종 | 6000/10800/86400s | ✅ | 100분/3h/24h |
| [dark_vessel.py:11-12](../prediction/algorithms/dark_vessel.py#L11-L12) | `_KR_COVERAGE_LAT/LON` | 32.0~39.5 / 124.0~132.0 | ✅ | AIS 수신 박스 |
| [dark_vessel.py:257-359](../prediction/algorithms/dark_vessel.py#L257-L359) | 11 패턴 점수 P1~P11 | 10~30 pt 분산 | ⚠️ | 탐지 정책 튜닝 대상 |
| [event_generator.py:26-39](../prediction/output/event_generator.py#L26-L39) | DEDUP_WINDOWS 12 카테고리 | 33~367분 | ⚠️ | 이미 의도적 |
| [config.py:25-37](../prediction/config.py#L25-L37) | 파이프라인 주기 등 | 5/24/60/30/12/3 | ✅ env | `.env` 로 이미 가능 |
| [transshipment.py](../prediction/algorithms/transshipment.py) | PROXIMITY / RENDEZVOUS | 0.002deg / 90min | ✅ | 환적 민감도 |
| [pair_trawl.py](../prediction/algorithms/pair_trawl.py) | PROXIMITY / SOG_Δ / COG_Δ / MIN_SYNC | 0.27NM / 0.5kn / 10° / 2h | ⚠️ | tier 재분류 기준 |
**P1:** 배포 없이 튜닝 가능해야 할 것
**⚠️:** 튜닝 자체가 탐지 정책 변경 → 릴리즈 노트 필요
---
## 9. 관련 파일 인덱스
- 오케스트레이터: [scheduler.py](../prediction/scheduler.py)
- 설정: [config.py](../prediction/config.py)
- 상태 컴포넌트: [fleet_tracker.py](../prediction/fleet_tracker.py), [cache/vessel_store.py](../prediction/cache/vessel_store.py)
- 알고리즘: [algorithms/](../prediction/algorithms/)
- 파이프라인: [pipeline/orchestrator.py](../prediction/pipeline/orchestrator.py)
- 출력: [output/event_generator.py](../prediction/output/event_generator.py), [output/violation_classifier.py](../prediction/output/violation_classifier.py), [output/kpi_writer.py](../prediction/output/kpi_writer.py), [output/stats_aggregator.py](../prediction/output/stats_aggregator.py), [output/alert_dispatcher.py](../prediction/output/alert_dispatcher.py)
- DB: [db/kcgdb.py](../prediction/db/kcgdb.py), [db/snpdb.py](../prediction/db/snpdb.py), [db/partition_manager.py](../prediction/db/partition_manager.py)
연관 운영 문서:
- [architecture.md](architecture.md) — 프론트엔드 아키텍처
- [sfr-traceability.md](sfr-traceability.md) — SFR 매트릭스
- [system-flow-guide.md](system-flow-guide.md) — system-flow 노드 명세
- `backend/README.md` — 백엔드 구성 (V030 + PR #79 hotfix 요구사항 명시)
---
## 변경 이력
| 일자 | 내용 |
|---|---|
| 2026-04-17 | 초판 — opus 4.7 독립 리뷰. 구조/방향 중심 + 우선순위별 개선 제안 |

파일 보기

@ -13,9 +13,9 @@
| 레이어 | 기술 | 상태 |
|-------|------|------|
| Frontend | React 19 + TypeScript 5.9 + Vite 8 + Tailwind CSS 4 + Zustand 5 + MapLibre GL 5 + deck.gl 9 + ECharts 6 + react-i18next | 운영 배포 (rocky-211 nginx) |
| Backend | Spring Boot 3.5.7 + Java 21 + PostgreSQL 14.19 + Flyway V001~V029 + Spring Security + JWT + Caffeine + 트리 RBAC | 운영 배포 (rocky-211 :18080) |
| Prediction | Python 3.11+ + FastAPI + APScheduler, 14 알고리즘, 7단계 분류 파이프라인 | 운영 배포 (redis-211 :18092, 5분 주기) |
| Database | PostgreSQL `kcgaidb` / 48 테이블 / schema `kcg` + snpdb(AIS 원천) | 운영 |
| Backend | Spring Boot 3.5.7 + Java 21 + PostgreSQL 14.19 + Flyway V001~V030 + Spring Security + JWT + Caffeine + 트리 RBAC | 운영 배포 (rocky-211 :18080) |
| Prediction | Python 3.11+ + FastAPI + APScheduler, 17 알고리즘 모듈 + 7단계 분류 파이프라인 + 5 출력/룰 모듈 | 운영 배포 (redis-211 :18092, 5분 주기) |
| Database | PostgreSQL `kcgaidb` / 51 테이블 / schema `kcg` + snpdb(AIS 원천) | 운영 |
| Design System | `/design-system.html` 쇼케이스 SSOT + `shared/constants/` 25개 카탈로그 + `shared/components/ui/` 9개 공통 컴포넌트 | SSOT 전영역 준수 (2026-04-17 PR #C 완료) |
| i18n | 10 네임스페이스 × ko/en, `common.json` 에 aria/error/dialog/message 54키 추가 | alert/confirm/aria-label 하드코딩 제거 완료 (2026-04-17 PR #B) |
@ -87,7 +87,7 @@ Frontend ← Backend /api/analysis/* + /api/events + /api/alerts + ... (65+ API)
| SFR-07 | AI 경비함정 단일 함정 순찰·경로 | PatrolRoute | 🔲 Mock | - |
| SFR-08 | AI 경비함정 다함정 협력형 경로 | FleetOptimization | 🔲 Mock | - |
| SFR-09 | 불법 어선 패턴 탐지 (Dark Vessel) | DarkVesselDetection, TransferDetection | ✅ /api/analysis/* | ✅ Dark 11패턴 + Transship 5단계 |
| SFR-10 | 불법 어망·어구 탐지 및 관리 | GearDetection, GearIdentification | ✅ /api/vessel-analysis/groups + /api/analysis/gear-detections | ✅ DAR-03 G-01~G-06 + pair tier |
| SFR-10 | 불법 어망·어구 탐지 및 관리 | GearDetection, GearIdentification, GearCollisionDetection(V030) | ✅ /api/vessel-analysis/groups + /api/analysis/gear-detections + /api/analysis/gear-collisions | ✅ DAR-03 G-01~G-06 + pair tier + GEAR_IDENTITY_COLLISION(PR #73) |
| SFR-11 | 단속·탐지 이력 관리 | EnforcementHistory, EventList | ✅ /api/events + /api/enforcement/records | ✅ prediction_events |
| SFR-12 | 모니터링 및 경보 현황판 | Dashboard, MonitoringDashboard, ChinaFishing | ✅ /api/stats + /api/alerts + /api/analysis/* | ✅ prediction_kpi_realtime + stats |
| SFR-13 | 통계·지표·성과 분석 | Statistics | ✅ /api/stats (daily/monthly/hourly) | ✅ prediction_stats_* |
@ -257,14 +257,16 @@ Frontend ← Backend /api/analysis/* + /api/events + /api/alerts + ... (65+ API)
- G-06 쌍끌이 공조 — **tier 분류**: STRONG / PROBABLE / SUSPECT
- 페어 탐색 `find_pair_candidates` (bbox + 궤적 유사도 2차)
- 한중어업협정 906척 NAME_EXACT + NAME_FUZZY 매칭 53%+
- **GEAR_IDENTITY_COLLISION (V030/PR #73)** — 동일 어구 이름이 서로 다른 MMSI 로 동일 사이클 내 공존 감지 → `gear_identity_collisions` UPSERT(name, mmsi_lo, mmsi_hi), CRITICAL/HIGH/MEDIUM/LOW severity 분류. 이전 "교체(sequential)" 로 오해하던 케이스를 "어구 복제/스푸핑 증거" 로 재정의. SAVEPOINT + try/except 로 `gear_correlation_scores_pkey` 충돌 격리
**백엔드 연동 ✅**:
- `/api/vessel-analysis/groups` + `/groups/{key}/detail|correlations|candidates/{mmsi}/metrics|resolve` — 모선 워크플로우 (VesselAnalysisGroupService, 2026-04-17 PARENT_RESOLVE @Auditable 추가)
- `/api/analysis/gear-detections` (2026-04-16.6 신설) — 자동 탐지 결과 MMSI 중복 제거
- `/api/analysis/gear-collisions` (V030, 4 엔드포인트) — GET list/stats/{id} + POST {id}/resolve (REVIEWED/CONFIRMED_ILLEGAL/FALSE_POSITIVE/REOPEN), `@Auditable GEAR_COLLISION_RESOLVE`
- 24h 궤적 리플레이 (gearReplayStore + useGearReplayLayers + TripsLayer)
- GearDetailPanel: 후보 클릭 → 모선 확정/제외 UI
**마스터**: fishery_permit_cn (V029, 29 컬럼) + fleet_vessels 확장 (permit_year/fishery_code)
**마스터**: fishery_permit_cn (V029, 29 컬럼) + fleet_vessels 확장 (permit_year/fishery_code) + gear_identity_collisions (V030)
---
@ -412,7 +414,7 @@ Frontend ← Backend /api/analysis/* + /api/events + /api/alerts + ... (65+ API)
| SFR-07 | `features/patrol/PatrolRoute.tsx` |
| SFR-08 | `features/patrol/FleetOptimization.tsx` |
| SFR-09 | `features/detection/DarkVesselDetection.tsx`, `features/detection/components/DarkDetailPanel.tsx`, `features/vessel/TransferDetection.tsx`, `prediction/algorithms/dark_vessel.py`, `spoofing.py`, `transship.py`, `risk.py` |
| SFR-10 | `features/detection/GearDetection.tsx`, `GearIdentification.tsx`, `features/detection/components/GearDetailPanel.tsx`, `GearReplayController.tsx`, `prediction/algorithms/pair_trawl.py`, `gear_violation.py`, `vessel_type_mapping.py`, `backend/.../analysis/VesselAnalysisGroupService.java` |
| SFR-10 | `features/detection/GearDetection.tsx`, `GearIdentification.tsx`, `GearCollisionDetection.tsx`(V030), `features/detection/components/GearDetailPanel.tsx`, `GearReplayController.tsx`, `prediction/algorithms/pair_trawl.py`, `gear_violation.py`, `gear_identity.py`(V030), `gear_correlation.py`, `gear_parent_inference.py`, `vessel_type_mapping.py`, `backend/.../analysis/VesselAnalysisGroupService.java`, `GearCollisionController+Service.java`(V030) |
| SFR-11 | `features/enforcement/EnforcementHistory.tsx`, `EventList.tsx`, `backend/.../event/EventController+Service.java`, `AlertService.java`, `enforcement/EnforcementService.java` |
| SFR-12 | `features/dashboard/Dashboard.tsx`, `features/monitoring/MonitoringDashboard.tsx`, `features/detection/ChinaFishing.tsx`, `features/detection/components/VesselMiniMap.tsx`, `VesselAnomalyPanel.tsx`, `backend/.../analysis/VesselAnalysisController+Service.java` |
| SFR-13 | `features/statistics/Statistics.tsx`, `ReportManagement.tsx`, `backend/.../stats/`, `admin/AdminStatsService.java` |
@ -483,6 +485,7 @@ Frontend ← Backend /api/analysis/* + /api/events + /api/alerts + ... (65+ API)
| AdminLogController | (직접 Repository) | SFR-01 |
| VesselAnalysisController | VesselAnalysisService | SFR-09, 12 |
| VesselAnalysisProxyController | VesselAnalysisGroupService (+`@Auditable PARENT_RESOLVE`) | SFR-10 |
| GearCollisionController (V030) | GearCollisionService (+`@Auditable GEAR_COLLISION_RESOLVE`) | SFR-10 |
| PredictionProxyController | RestClient `@Bean predictionRestClient` | SFR-03, 20 |
| EventController | EventService | SFR-11, 12 |
| AlertController | AlertService | SFR-12, 17 |

파일 보기

@ -450,6 +450,37 @@ AIS(선박자동식별장치) 신호를 의도적으로 끈 의심 선박(Dark V
---
### 어구 정체성 충돌 (V030, 2026-04-17 추가)
**메뉴 위치:** 탐지/분석 > 어구 정체성 충돌
**URL:** `/gear-collision`
**접근 권한:** ADMIN, OPERATOR, ANALYST, FIELD, VIEWER (READ) / ADMIN, OPERATOR (UPDATE)
**화면 설명:**
동일한 어구 이름이 서로 다른 MMSI 로 같은 5분 사이클에 동시 송출되는 공존 케이스를 탐지해
어구 복제/스푸핑 증거로 기록·분류하는 화면입니다. 이전에는 "MMSI 교체"로 오해해 덮어쓰는 바람에
`gear_correlation_scores_pkey` 제약 충돌로 prediction 사이클 전체가 실패하던 이슈(PR #73)를
"공존 패턴"으로 재정의하면서 신설되었습니다.
**주요 기능:**
- 충돌 쌍(name, MMSI 저/고) 목록 + 공존 누적 횟수 + 양 위치 최대 거리 + severity tier
- 운영자 분류 워크플로우: OPEN → REVIEWED / CONFIRMED_ILLEGAL / FALSE_POSITIVE (REOPEN 가능)
- severity 자동 산정: CRITICAL(거리≥50km OR 공존≥3회) / HIGH(거리≥10km OR 공존≥2회) / MEDIUM / LOW
- 상세 패널 — 양 MMSI 최근 궤적, 어구 이름 파싱(선박명+어구코드), 매칭된 모선(fleet_vessels)
**구현 완료 (V030 / PR #73, 2026-04-17):**
- ✅ `gear_identity_collisions` 테이블 + UNIQUE(name, mmsi_lo, mmsi_hi) 제약 + 5 인덱스
- ✅ prediction `algorithms/gear_identity.py` 순수함수 + `fleet_tracker.track_gear_identity()` 공존/교체 분기
- ✅ 백엔드 4 엔드포인트 `/api/analysis/gear-collisions` + `GEAR_COLLISION_RESOLVE` @Auditable
- ✅ 프론트 페이지 `GearCollisionDetection.tsx` (DataTable + KPI 5장 + SidePanel + 상태/심각도 필터)
- ✅ 이벤트 허브 연동 — HIGH/CRITICAL 은 `prediction_events` 에 자동 등록 (dedup 367분)
**향후 구현 예정:**
- 🔲 FALSE_POSITIVE 반복 쌍 자동 화이트리스트 (v2 학습 피드백 루프)
- 🔲 KPI 테이블 (`prediction_stats_*`) 에 severity 별 집계 반영
---
## SFR-11: 단속/탐지 이력 관리
**메뉴 위치:** 단속/이력 > 단속/탐지 이력

파일 보기

@ -6,10 +6,15 @@ KCG AI Monitoring 시스템 워크플로우 플로우차트 뷰어 사용법.
`/system-flow.html`은 snpdb 5분 원천 궤적 수집부터 prediction 분석, 이벤트 생성, 운영자 의사결정까지 시스템 전체 데이터 흐름을 노드/엣지로 시각화한 **개발 단계 활용 페이지**입니다.
- 102개 노드 + 133개 엣지 (v1.0.0 기준)
- 115개 노드 + 133개 엣지 (manifest 현재 상태, `meta.json` 은 아직 v1.0.0/2026-04-07 로 미갱신)
- 메인 SPA(`/`)와 완전 분리된 별도 React 앱
- 메뉴/링크 노출 없음 — 직접 URL 접근만
> ⚠️ **V030 미반영 경고**: 2026-04-17 V030 로 추가된 GEAR_IDENTITY_COLLISION 파이프라인 (
> `algo.gear_identity_collision`, `storage.gear_identity_collisions`, `api.gear_collisions_*`,
> `ui.gear_collision`, `decision.gear_collision_resolve`) 노드가 아직 manifest 에 등록되지
> 않았다. 다음 `/version` 릴리즈 시 매니페스트 동기화 필요.
## 접근 URL
- **운영**: https://kcg-ai-monitoring.gc-si.dev/system-flow.html

파일 보기

@ -0,0 +1,58 @@
"""사이클 스테이지 에러 경계 유틸.
`run_analysis_cycle` 내부의 스테이지를 지점에서 감싸서
실패 스테이지를 명시적으로 로깅하고, 부분 실패가 후속 스테이지를
막지 않도록 한다.
설계 원칙:
- 비필수 스테이지는 예외를 흡수하고 None 반환 호출자는
`if result is None` 건너뛰기 선택 가능
- 필수 스테이지(`required=True`) 예외를 그대로 올려 상위
`run_analysis_cycle` top-level try/except 잡도록 한다
- `logger.exception` 사용으로 stacktrace 저널에 남도록 하여
원격 서버(journalctl) 에서 실패 지점 특정 가능
"""
from __future__ import annotations
import logging
import time
from typing import Any, Callable, TypeVar
logger = logging.getLogger(__name__)
T = TypeVar('T')
def run_stage(
name: str,
fn: Callable[..., T],
*args: Any,
required: bool = False,
**kwargs: Any,
) -> T | None:
"""스테이지 실행 + 지속시간 로깅 + 실패 격리.
Args:
name: 스테이지 이름 (로그 라벨). 'fleet_tracking', 'pair_detection'
fn: 실행할 호출 가능 객체
*args, **kwargs: fn 전달
required: True 실패 예외를 re-raise. False None 반환하고 계속
Returns:
fn 반환값, 또는 실패 None (required=False )
Raises:
fn 던진 예외 (required=True 때만)
"""
t0 = time.time()
try:
result = fn(*args, **kwargs)
elapsed = time.time() - t0
logger.info('stage %s ok in %.2fs', name, elapsed)
return result
except Exception as e:
elapsed = time.time() - t0
logger.exception('stage %s failed after %.2fs: %s', name, elapsed, e)
if required:
raise
return None

파일 보기

@ -8,6 +8,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from config import settings
from fleet_tracker import GEAR_PATTERN
from pipeline.stage_runner import run_stage
logger = logging.getLogger(__name__)
@ -69,7 +70,7 @@ def _fetch_dark_history(kcg_conn, mmsi_list: list[str]) -> dict[str, dict]:
for m, n7, n24, t in cur.fetchall()
}
except Exception as e:
logger.warning('fetch_dark_history failed: %s', e)
logger.exception('fetch_dark_history failed: %s', e)
return {}
@ -170,7 +171,7 @@ def run_analysis_cycle():
collision_events['skipped_low'],
)
except Exception as e:
logger.warning('gear collision event promotion failed: %s', e)
logger.exception('gear collision event promotion failed: %s', e)
fleet_roles = fleet_tracker.build_fleet_clusters(vessel_dfs)
@ -193,7 +194,7 @@ def run_analysis_cycle():
logger.info('group polygons: %d saved, %d cleaned, %d gear groups',
saved, cleaned, len(gear_groups))
except Exception as e:
logger.warning('group polygon generation failed: %s', e)
logger.exception('group polygon generation failed: %s', e)
# 4.7 어구 연관성 분석 (멀티모델 패턴 추적)
try:
@ -226,7 +227,7 @@ def run_analysis_cycle():
inference_result['skipped'],
)
except Exception as e:
logger.warning('gear correlation failed: %s', e)
logger.exception('gear correlation failed: %s', e)
# 4.9 페어 후보 탐색 (bbox 1차 + 궤적 유사도 2차 → G-06 pair_trawl 판정)
pair_results: dict[str, dict] = {}
@ -300,7 +301,7 @@ def run_analysis_cycle():
REJECT_COUNTERS['insufficient_aligned'], REJECT_COUNTERS['no_sync_at_any_tier'],
)
except Exception as e:
logger.warning('pair detection failed: %s', e)
logger.exception('pair detection failed: %s', e)
# 5. 선박별 추가 알고리즘 → AnalysisResult 생성
# dark 이력 일괄 조회 (7일 history) — 사이클당 1회
@ -712,37 +713,36 @@ def run_analysis_cycle():
'transship_score': item['score'],
}
# 7. 결과 저장
upserted = kcgdb.upsert_results(results)
kcgdb.cleanup_old(hours=48)
# 7. 결과 저장 (필수 — 실패 시 사이클 abort)
upserted = run_stage('upsert_results', kcgdb.upsert_results, results, required=True)
run_stage('cleanup_old', kcgdb.cleanup_old, hours=48)
# 8. 출력 모듈 (이벤트 생성, 위반 분류, KPI 갱신, 통계 집계, 경보)
try:
from output.violation_classifier import run_violation_classifier
from output.event_generator import run_event_generator
from output.kpi_writer import run_kpi_writer
from output.stats_aggregator import aggregate_hourly, aggregate_daily
from output.alert_dispatcher import run_alert_dispatcher
# 8. 출력 모듈 — 각 단계를 독립적으로 실행해 실패 지점을 명시적으로 기록.
# 한 모듈이 깨져도 다른 모듈은 계속 돌아가야 한다 (예: event_generator 는 실패했어도
# kpi_writer / stats_aggregator / alert_dispatcher 는 이전 사이클 결과로 동작 가능).
from output.violation_classifier import run_violation_classifier
from output.event_generator import run_event_generator
from output.kpi_writer import run_kpi_writer
from output.stats_aggregator import aggregate_hourly, aggregate_daily
from output.alert_dispatcher import run_alert_dispatcher
from dataclasses import asdict
results_dicts = [asdict(r) for r in results]
# 필드명 매핑 (AnalysisResult → 출력 모듈 기대 형식)
for d in results_dicts:
d['zone_code'] = d.pop('zone', None)
d['gap_duration_min'] = d.get('gap_duration_min', 0)
d['transship_suspect'] = d.pop('is_transship_suspect', False)
d['fleet_is_leader'] = d.pop('is_leader', False)
d['fleet_cluster_id'] = d.pop('cluster_id', None)
d['speed_kn'] = None # 분석 결과에 속도 없음
run_violation_classifier(results_dicts)
run_event_generator(results_dicts)
run_kpi_writer()
aggregate_hourly()
aggregate_daily()
run_alert_dispatcher()
logger.info('output modules completed')
except Exception as e:
logger.warning('output modules failed (non-fatal): %s', e)
from dataclasses import asdict
results_dicts = [asdict(r) for r in results]
# 필드명 매핑 (AnalysisResult → 출력 모듈 기대 형식)
for d in results_dicts:
d['zone_code'] = d.pop('zone', None)
d['gap_duration_min'] = d.get('gap_duration_min', 0)
d['transship_suspect'] = d.pop('is_transship_suspect', False)
d['fleet_is_leader'] = d.pop('is_leader', False)
d['fleet_cluster_id'] = d.pop('cluster_id', None)
d['speed_kn'] = None # 분석 결과에 속도 없음
run_stage('violation_classifier', run_violation_classifier, results_dicts)
run_stage('event_generator', run_event_generator, results_dicts)
run_stage('kpi_writer', run_kpi_writer)
run_stage('stats_aggregate_hourly', aggregate_hourly)
run_stage('stats_aggregate_daily', aggregate_daily)
run_stage('alert_dispatcher', run_alert_dispatcher)
# 9. Redis에 분석 컨텍스트 캐싱 (채팅용)
try:
@ -788,7 +788,7 @@ def run_analysis_cycle():
'polygon_summary': kcgdb.fetch_polygon_summary(),
})
except Exception as e:
logger.warning('failed to cache analysis context for chat: %s', e)
logger.exception('failed to cache analysis context for chat: %s', e)
elapsed = round(time.time() - start, 2)
_last_run['duration_sec'] = elapsed