import { useEffect, useState, useCallback, useMemo } from 'react'; import { Loader2, RefreshCw, EyeOff, AlertTriangle, Radar } from 'lucide-react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { getAlertLevelIntent } from '@shared/constants/alertLevels'; import { fetchVesselAnalysis, type VesselAnalysisItem, type VesselAnalysisStats, } from '@/services/vesselAnalysisApi'; /** * iran 백엔드의 실시간 vessel analysis 결과를 표시. * - mode: 'dark' (Dark Vessel만) / 'spoofing' (스푸핑 의심) / 'transship' (전재) / 'all' * - 위험도 통계 + 필터링된 선박 테이블 */ interface Props { mode: 'dark' | 'spoofing' | 'transship' | 'all'; title: string; icon?: React.ReactNode; } // 위험도 색상은 alertLevels 카탈로그 (intent prop) 사용 const ZONE_LABELS: Record = { TERRITORIAL_SEA: '영해', CONTIGUOUS_ZONE: '접속수역', EEZ_OR_BEYOND: 'EEZ 외', ZONE_I: '특정해역 I', ZONE_II: '특정해역 II', ZONE_III: '특정해역 III', ZONE_IV: '특정해역 IV', }; export function RealVesselAnalysis({ mode, title, icon }: Props) { const [items, setItems] = useState([]); const [stats, setStats] = useState(null); const [available, setAvailable] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [zoneFilter, setZoneFilter] = useState(''); const load = useCallback(async () => { setLoading(true); setError(''); try { const res = await fetchVesselAnalysis(); setItems(res.items); setStats(res.stats); setAvailable(res.serviceAvailable); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const filtered = useMemo(() => { let result = items; if (mode === 'dark') result = result.filter((i) => i.algorithms.darkVessel.isDark); else if (mode === 'spoofing') result = result.filter((i) => i.algorithms.gpsSpoofing.spoofingScore >= 0.3); else if (mode === 'transship') result = result.filter((i) => i.algorithms.transship.isSuspect); if (zoneFilter) result = result.filter((i) => i.algorithms.location.zone === zoneFilter); return result; }, [items, mode, zoneFilter]); const sortedByRisk = useMemo( () => [...filtered].sort((a, b) => b.algorithms.riskScore.score - a.algorithms.riskScore.score), [filtered], ); return (
{icon} {title} {!available && 미연결}
GET /api/vessel-analysis · iran 백엔드 실시간 분석 결과
{/* 통계 카드 */} {stats && (
)} {error &&
에러: {error}
} {loading &&
} {!loading && (
{sortedByRisk.length === 0 && ( )} {sortedByRisk.slice(0, 100).map((v) => ( ))}
MMSI 선박 유형 위험도 점수 해역 활동 Dark Spoofing 전재 갱신
필터된 데이터가 없습니다.
{v.mmsi} {v.classification.vesselType} ({(v.classification.confidence * 100).toFixed(0)}%) {v.algorithms.riskScore.level} {v.algorithms.riskScore.score} {ZONE_LABELS[v.algorithms.location.zone] || v.algorithms.location.zone} ({v.algorithms.location.distToBaselineNm.toFixed(1)}NM) {v.algorithms.activity.state} {v.algorithms.darkVessel.isDark ? ( {v.algorithms.darkVessel.gapDurationMin}분 ) : -} {v.algorithms.gpsSpoofing.spoofingScore > 0 ? ( {v.algorithms.gpsSpoofing.spoofingScore.toFixed(2)} ) : -} {v.algorithms.transship.isSuspect ? ( {v.algorithms.transship.durationMin}분 ) : -} {v.timestamp ? new Date(v.timestamp).toLocaleTimeString('ko-KR') : '-'}
{sortedByRisk.length > 100 && (
상위 100건만 표시 (전체 {sortedByRisk.length}건, 위험도순)
)}
)}
); } function StatBox({ label, value, color }: { label: string; value: number; color: string }) { return (
{label}
{value.toLocaleString()}
); } // 편의 export: 모드별 default props export const RealDarkVessels = () => } />; export const RealSpoofingVessels = () => } />; export const RealTransshipSuspects = () => } />; export const RealAllVessels = () => } />;