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회 연속 감지 · 카테고리 뱃지 · 설명
58 lines
1.8 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
};
|
|
}
|