/** * DarkDetailPanel — Dark Vessel 판정 상세 사이드 패널 * * 테이블 행 클릭 시 우측에 슬라이드 표시. * 점수 산출 내역, 선박 정보, GAP 상세, 과거 이력을 종합 표시. */ import { useEffect, useState, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { ScoreBreakdown } from '@shared/components/common/ScoreBreakdown'; import { buildScoreBreakdown } from '@shared/constants/darkVesselPatterns'; import { getRiskIntent } from '@shared/constants/statusIntent'; import { getAlertLevelIntent } from '@shared/constants/alertLevels'; import { formatDateTime } from '@shared/utils/dateFormat'; import { getAnalysisHistory, type VesselAnalysis } from '@/services/analysisApi'; import { X, Ship, MapPin, Clock, AlertTriangle, TrendingUp, ExternalLink, ShieldAlert } from 'lucide-react'; import { BarChart as EcBarChart } from '@lib/charts'; interface DarkDetailPanelProps { vessel: VesselAnalysis | null; onClose: () => void; } export function DarkDetailPanel({ vessel, onClose }: DarkDetailPanelProps) { const navigate = useNavigate(); const { t: tc } = useTranslation('common'); const [history, setHistory] = useState([]); const features = vessel?.features ?? {}; const darkTier = (features.dark_tier as string) ?? 'NONE'; const darkScore = (features.dark_suspicion_score as number) ?? 0; const darkPatterns = (features.dark_patterns as string[]) ?? []; const darkHistory7d = (features.dark_history_7d as number) ?? 0; const darkHistory24h = (features.dark_history_24h as number) ?? 0; const gapStartLat = features.gap_start_lat as number | undefined; const gapStartLon = features.gap_start_lon as number | undefined; const gapStartSog = features.gap_start_sog as number | undefined; const gapStartState = features.gap_start_state as string | undefined; // 점수 산출 내역 const breakdown = useMemo(() => buildScoreBreakdown(darkPatterns), [darkPatterns]); // 7일 이력 조회 const loadHistory = useCallback(async () => { if (!vessel?.mmsi) return; try { const res = await getAnalysisHistory(vessel.mmsi, 168); // 7일 setHistory(res); } catch { setHistory([]); } }, [vessel?.mmsi]); useEffect(() => { loadHistory(); }, [loadHistory]); // 일별 dark 건수 집계 (차트용) const dailyDarkData = useMemo(() => { const dayMap: Record = {}; for (const h of history) { if (!h.isDark) continue; const day = (h.analyzedAt ?? '').slice(0, 10); if (day) dayMap[day] = (dayMap[day] || 0) + 1; } return Object.entries(dayMap) .sort(([a], [b]) => a.localeCompare(b)) .map(([day, count]) => ({ name: day.slice(5), value: count })); }, [history]); if (!vessel) return null; return (
{/* 헤더 */}
판정 상세 {darkTier} {darkScore}점
{/* 선박 기본 정보 */}
선박 정보
MMSI 선종 {vessel.vesselType || 'UNKNOWN'} 국적 {vessel.mmsi?.startsWith('412') ? 'CN (중국)' : vessel.mmsi?.slice(0, 3)} 해역 {vessel.zoneCode || '-'} 활동상태 {vessel.activityState || '-'} 위험도 {vessel.riskLevel} ({vessel.riskScore})
{/* 점수 산출 내역 */}
점수 산출 내역 ({breakdown.items.length}개 패턴 적용)
{/* GAP 상세 */}
GAP 상세
GAP 길이 {vessel.gapDurationMin ? `${vessel.gapDurationMin}분 (${(vessel.gapDurationMin / 60).toFixed(1)}h)` : '-'} 시작 위치 {gapStartLat != null ? `${gapStartLat.toFixed(4)}°N ${gapStartLon?.toFixed(4)}°E` : '-'} 시작 SOG {gapStartSog != null ? `${gapStartSog.toFixed(1)}kn` : '-'} 시작 상태 {gapStartState || '-'} 분석시각 {formatDateTime(vessel.analyzedAt)}
{/* 과거 이력 */}
과거 이력 (7일)
7일 dark 일수 {darkHistory7d}일 24시간 dark {darkHistory24h}일
{dailyDarkData.length > 0 && (
)} {dailyDarkData.length === 0 && (
이력 없음
)}
{/* 액션 버튼 */}
); }