kcg-ai-monitoring/frontend/src/services/analysisAdapter.ts
htlee d82eaf7e79 feat(frontend): 중국어선 감시 실데이터 연동 + 특이운항 미니맵/판별 패널
Tab 1 AI 감시 대시보드 / Tab 2 환적탐지 / Tab 3 어구판별 3개 탭을
deprecated iran proxy 에서 자체 /api/analysis/* 로 전환하고, 특이운항
선박의 24h 항적과 판별 구간 상세를 지도와 패널로 제공한다.

서비스 계층
- analysisApi.ts 확장: getAnalysisStats / getAnalysisVessels(필터 3종) /
  getGearDetections 추가. VesselAnalysis 에 violationCategories /
  bd09OffsetM / ucafScore / ucftScore / clusterId 필드 노출
- analysisAdapter.ts: flat VesselAnalysis → nested VesselAnalysisItem
  변환으로 기존 컴포넌트 재사용
- vesselAnalysisApi.ts fetchVesselAnalysis @deprecated 마킹

Tab 1 (ChinaFishing)
- 서버 집계(stats) 기준 카운터 재구성. 중국어선 / Dark / 환적 / 고위험
  모두 mmsiPrefix=412 로 서버 필터
- 선박 리스트 vessel_type UNKNOWN 인 경우 "중국어선" + "미분류" 로 표시
- 특이운항 row 클릭 → 아래 행에 미니맵 + 판별 패널 배치
- 관심영역 / VIIRS / 기상 / VTS 카드에 "데모 데이터" 뱃지. 비허가 /
  제재 / 관심 탭 disabled + "준비중" 뱃지

Tab 2 (RealVesselAnalysis)
- /analysis/dark / /analysis/transship / /analysis/vessels mode별 분기
- 상단 통계 카드를 items 클라이언트 집계로 전환해 하단 테이블과 정합

Tab 3 (GearIdentification)
- 최하단 "최근 자동탐지 결과" 섹션 추가. row 클릭 시 상단 입력 폼
  프리필 + 결과 패널에 자동탐지 근거 프리셋

특이운항 판별 시각화 (VesselMiniMap / VesselAnomalyPanel /
vesselAnomaly 유틸)
- 24h getAnalysisHistory 로드 → classifyAnomaly 로 DARK/SPOOFING/
  TRANSSHIP/GEAR_VIOLATION/HIGH_RISK 5개 카테고리 판별. 좌표는
  top-level lat/lon 우선, features.gap_start_* fallback
- groupAnomaliesToSegments: 5분 주기 반복되는 동일 신호를 시작~종료
  구간으로 병합
- 미니맵: 전체 궤적은 연한 파랑, segment 시간범위와 매칭되는 AIS
  궤적 서브구간을 severity 색(CRITICAL 빨강 / WARNING 주황 / INFO
  파랑) 으로 하이라이트. 이벤트 기준 좌표는 작은 흰 점
- 판별 패널: 시작→종료 · 지속 · N회 연속 감지 · 카테고리 뱃지 · 설명
2026-04-16 14:31:26 +09:00

58 lines
1.8 KiB
TypeScript

/**
* analysisApi (flat shape) → vesselAnalysisApi (nested VesselAnalysisItem) 변환.
* 기존 컴포넌트가 nested shape에 의존하므로 서비스 교체 후에도 컴포넌트 최소 수정으로 재사용할 수 있게 어댑터를 둔다.
*/
import type { VesselAnalysis } from './analysisApi';
import type { VesselAnalysisItem } from './vesselAnalysisApi';
export function toVesselItem(v: VesselAnalysis): VesselAnalysisItem {
return {
mmsi: v.mmsi,
timestamp: v.analyzedAt,
classification: {
vesselType: v.vesselType ?? 'UNKNOWN',
confidence: Number(v.confidence ?? 0),
fishingPct: Number(v.fishingPct ?? 0),
clusterId: v.clusterId ?? 0,
season: v.season ?? 'UNKNOWN',
},
algorithms: {
location: {
zone: v.zoneCode ?? 'EEZ_OR_BEYOND',
distToBaselineNm: Number(v.distToBaselineNm ?? 0),
},
activity: {
state: v.activityState ?? 'UNKNOWN',
ucafScore: Number(v.ucafScore ?? 0),
ucftScore: Number(v.ucftScore ?? 0),
},
darkVessel: {
isDark: v.isDark ?? false,
gapDurationMin: v.gapDurationMin ?? 0,
},
gpsSpoofing: {
spoofingScore: Number(v.spoofingScore ?? 0),
bd09OffsetM: Number(v.bd09OffsetM ?? 0),
speedJumpCount: v.speedJumpCount ?? 0,
},
cluster: {
clusterId: v.fleetClusterId ?? 0,
clusterSize: 0,
},
fleetRole: {
isLeader: v.fleetIsLeader ?? false,
role: v.fleetRole ?? 'NONE',
},
riskScore: {
score: v.riskScore ?? 0,
level: v.riskLevel ?? 'LOW',
},
transship: {
isSuspect: v.transshipSuspect ?? false,
pairMmsi: v.transshipPairMmsi ?? '',
durationMin: v.transshipDurationMin ?? 0,
},
},
};
}