/** * prediction 분석 결과 조회 API (레거시 proxy 경로). * 새 화면은 @/services/analysisApi 의 /api/analysis/* 를 직접 사용한다. */ const API_BASE = import.meta.env.VITE_API_URL ?? '/api'; export interface VesselAnalysisStats { total: number; dark: number; spoofing: number; critical: number; high: number; medium: number; low: number; clusterCount: number; gearGroups: number; gearCount: number; } export interface VesselAnalysisItem { mmsi: string; timestamp: string; classification: { vesselType: string; confidence: number; fishingPct: number; clusterId: number; season: string; }; algorithms: { location: { zone: string; distToBaselineNm: number }; activity: { state: string; ucafScore: number; ucftScore: number }; darkVessel: { isDark: boolean; gapDurationMin: number }; gpsSpoofing: { spoofingScore: number; bd09OffsetM: number; speedJumpCount: number }; cluster: { clusterId: number; clusterSize: number }; fleetRole: { isLeader: boolean; role: string }; riskScore: { score: number; level: string }; transship: { isSuspect: boolean; pairMmsi: string; durationMin: number }; }; features?: Record; } export interface VesselAnalysisResponse { serviceAvailable: boolean; count: number; stats: VesselAnalysisStats; items: VesselAnalysisItem[]; } export interface GearGroupItem { groupType: 'FLEET' | 'GEAR_IN_ZONE' | 'GEAR_OUT_ZONE'; groupKey: string; groupLabel?: string; subClusterId: number; snapshotTime: string; polygon: unknown; // GeoJSON geometry centerLat: number; centerLon: number; areaSqNm: number; memberCount: number; members: { mmsi: string; name?: string; lat?: number; lon?: number }[]; color?: string; resolution: { status: string; selectedParentMmsi: string | null; selectedParentName: string | null; topScore: number | null; confidence: number | null; secondScore: number | null; scoreMargin: number | null; decisionSource: string | null; stableCycles: number | null; approvedAt: string | null; manualComment: string | null; } | null; candidateCount?: number; liveTopScore?: number; // correlation_scores 실시간 최고 점수 } export interface GroupsResponse { serviceAvailable: boolean; count: number; items: GearGroupItem[]; } async function apiGet(path: string): Promise { const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' }); if (!res.ok) throw new Error(`API ${res.status}: ${path}`); return res.json(); } /** * @deprecated 백엔드 /api/vessel-analysis 는 빈 스텁 응답만 반환한다. * vessel_analysis_results 조회는 {@link import('./analysisApi').getAnalysisVessels} / * {@link import('./analysisApi').getAnalysisStats} 를 사용한다. * 본 함수는 호출처 제거 후 제거 예정. */ export function fetchVesselAnalysis() { return apiGet('/vessel-analysis'); } export function fetchGroups(groupType?: string) { const qs = groupType ? `?groupType=${groupType}` : ''; return apiGet(`/vessel-analysis/groups${qs}`); } export function fetchGroupDetail(groupKey: string) { return apiGet(`/vessel-analysis/groups/${encodeURIComponent(groupKey)}/detail`); } export function fetchGroupCorrelations(groupKey: string, minScore?: number) { const qs = minScore ? `?minScore=${minScore}` : ''; return apiGet(`/vessel-analysis/groups/${encodeURIComponent(groupKey)}/correlations${qs}`); } // ─── 후보 상세 메트릭 + 모선 확정/제외 ───────────── export interface CandidateMetricItem { observedAt: string; proximityRatio: number | null; visitScore: number | null; activitySync: number | null; dtwSimilarity: number | null; speedCorrelation: number | null; headingCoherence: number | null; driftSimilarity: number | null; shadowStay: boolean; shadowReturn: boolean; gearGroupActiveRatio: number | null; } export function fetchCandidateMetrics(groupKey: string, targetMmsi: string) { return apiGet<{ items: CandidateMetricItem[]; count: number }>( `/vessel-analysis/groups/${encodeURIComponent(groupKey)}/candidates/${encodeURIComponent(targetMmsi)}/metrics`, ); } export async function resolveParent( groupKey: string, action: 'confirm' | 'reject', targetMmsi: string, comment = '', ): Promise<{ ok: boolean; message?: string }> { const res = await fetch(`${API_BASE}/vessel-analysis/groups/${encodeURIComponent(groupKey)}/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ action, targetMmsi, comment }), }); return res.json(); } // ─── 선박 항적 조회 (signal-batch) ───────────── /** CompactVesselTrack 응답 */ export interface VesselTrack { vesselId: string; shipName: string; geometry: [number, number][]; // [lon, lat][] timestamps: string[]; // Unix timestamp (초) 문자열 배열 speeds: number[]; pointCount: number; } /** * 선박 항적 일괄 조회. * POST /api/prediction/v2/tracks/vessels (백엔드 프록시 → signal-batch) */ export async function fetchVesselTracks( vessels: string[], startTime: string, endTime: string, ): Promise { const res = await fetch(`${API_BASE}/vessel-analysis/tracks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ startTime, endTime, vessels }), }); if (!res.ok) throw new Error(`tracks API ${res.status}`); return res.json(); } // ─── 필터/유틸 ───────────────────────────────── /** * Dark Vessel만 필터. */ export function filterDarkVessels(items: VesselAnalysisItem[]): VesselAnalysisItem[] { return items.filter((i) => i.algorithms.darkVessel.isDark); } /** * GPS 스푸핑 의심 (score >= 0.3). */ export function filterSpoofingVessels(items: VesselAnalysisItem[], threshold = 0.3): VesselAnalysisItem[] { return items.filter((i) => i.algorithms.gpsSpoofing.spoofingScore >= threshold); } /** * 전재 의심. */ export function filterTransshipSuspects(items: VesselAnalysisItem[]): VesselAnalysisItem[] { return items.filter((i) => i.algorithms.transship.isSuspect); } /** * 위험도 레벨 필터. */ export function filterByRiskLevel(items: VesselAnalysisItem[], levels: string[]): VesselAnalysisItem[] { return items.filter((i) => levels.includes(i.algorithms.riskScore.level)); }