import { useState, useEffect, useMemo, useRef, useCallback, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { BaseMap, STATIC_LAYERS, createMarkerLayer, createHeatmapLayer, useMapLayers, type MapHandle } from '@lib/map'; import type { HeatPoint, MarkerData } from '@lib/map'; import { AlertTriangle, Ship, Anchor, Eye, Navigation, Crosshair, Shield, Waves, Wind, Thermometer, MapPin, ChevronRight, Activity, Zap, Target, ArrowUpRight, ArrowDownRight, Radar, TrendingUp, BarChart3 } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { AreaChart, PieChart } from '@lib/charts'; import { useKpiStore } from '@stores/kpiStore'; import { useEventStore } from '@stores/eventStore'; import { usePatrolStore } from '@stores/patrolStore'; import { useVesselStore } from '@stores/vesselStore'; // ─── 작전 경보 등급 ───────────────────── type AlertLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; const ALERT_COLORS: Record = { CRITICAL: { bg: 'bg-red-500/15', text: 'text-red-400', border: 'border-red-500/30', dot: 'bg-red-500' }, HIGH: { bg: 'bg-orange-500/15', text: 'text-orange-400', border: 'border-orange-500/30', dot: 'bg-orange-500' }, MEDIUM: { bg: 'bg-yellow-500/15', text: 'text-yellow-400', border: 'border-yellow-500/30', dot: 'bg-yellow-500' }, LOW: { bg: 'bg-blue-500/15', text: 'text-blue-400', border: 'border-blue-500/30', dot: 'bg-blue-500' }, }; // ─── KPI UI 매핑 (icon, color는 store에 없으므로 라벨 기반 매핑) ───────── const KPI_UI_MAP: Record = { '실시간 탐지': { icon: Radar, color: '#3b82f6' }, 'EEZ 침범': { icon: AlertTriangle, color: '#ef4444' }, '다크베셀': { icon: Eye, color: '#f97316' }, '불법환적 의심': { icon: Anchor, color: '#a855f7' }, '추적 중': { icon: Crosshair, color: '#06b6d4' }, '나포/검문': { icon: Shield, color: '#10b981' }, }; const AREA_RISK_DATA = [ { area: '서해 NLL', vessels: 8, risk: 95, trend: 'up' }, { area: 'EEZ 북부', vessels: 14, risk: 91, trend: 'up' }, { area: '서해 5도', vessels: 11, risk: 88, trend: 'stable' }, { area: 'EEZ 서부', vessels: 6, risk: 72, trend: 'down' }, { area: '동해 중부', vessels: 4, risk: 58, trend: 'up' }, { area: 'EEZ 남부', vessels: 3, risk: 45, trend: 'down' }, { area: '남해 서부', vessels: 1, risk: 22, trend: 'stable' }, ]; const HOURLY_DETECTION = [ { hour: '00', count: 5, eez: 2 }, { hour: '01', count: 4, eez: 1 }, { hour: '02', count: 6, eez: 3 }, { hour: '03', count: 8, eez: 4 }, { hour: '04', count: 12, eez: 6 }, { hour: '05', count: 18, eez: 8 }, { hour: '06', count: 28, eez: 12 }, { hour: '07', count: 35, eez: 15 }, { hour: '08', count: 47, eez: 18 }, ]; const VESSEL_TYPE_DATA = [ { name: 'EEZ 침범', value: 18, color: '#ef4444' }, { name: '다크베셀', value: 12, color: '#f97316' }, { name: '불법환적', value: 8, color: '#a855f7' }, { name: 'MMSI변조', value: 5, color: '#eab308' }, { name: '고속도주', value: 4, color: '#06b6d4' }, ]; const WEATHER_DATA = { wind: { speed: 12, direction: 'NW', gust: 18 }, wave: { height: 1.8, period: 6 }, temp: { air: 8, water: 11 }, visibility: 12, seaState: 3, }; // ─── 서브 컴포넌트 ───────────────────── function PulsingDot({ color }: { color: string }) { return ( ); } function RiskBar({ value, size = 'default' }: { value: number; size?: 'default' | 'sm' }) { const pct = value * 100; const color = pct > 90 ? 'bg-red-500' : pct > 80 ? 'bg-orange-500' : pct > 70 ? 'bg-yellow-500' : 'bg-blue-500'; const textColor = pct > 90 ? 'text-red-400' : pct > 80 ? 'text-orange-400' : pct > 70 ? 'text-yellow-400' : 'text-blue-400'; const barW = size === 'sm' ? 'w-16' : 'w-24'; return (
{pct.toFixed(0)}
); } interface KpiCardProps { label: string; value: number; prev: number; icon: LucideIcon; color: string; desc: string } function KpiCard({ label, value, prev, icon: Icon, color, desc }: KpiCardProps) { const diff = value - prev; const isUp = diff > 0; return (
{isUp ? : } {Math.abs(diff)}
{value}
{label}
{desc}
); } interface TimelineEvent { time: string; level: AlertLevel; title: string; detail: string; vessel: string; area: string } function TimelineItem({ event }: { event: TimelineEvent }) { const c = ALERT_COLORS[event.level]; return (
{event.time}
{event.title} {event.level}

{event.detail}

{event.vessel} {event.area}
); } function PatrolStatusBadge({ status }: { status: string }) { const styles: Record = { '추적 중': 'bg-red-500/20 text-red-400 border-red-500/30', '검문 중': 'bg-orange-500/20 text-orange-400 border-orange-500/30', '초계 중': 'bg-blue-500/20 text-blue-400 border-blue-500/30', '귀항 중': 'bg-muted text-muted-foreground border-slate-500/30', '대기': 'bg-green-500/20 text-green-400 border-green-500/30', }; return {status}; } function FuelGauge({ percent }: { percent: number }) { const color = percent > 60 ? 'bg-green-500' : percent > 30 ? 'bg-yellow-500' : 'bg-red-500'; return (
{percent}%
); } // ─── 해역 위협 미니맵 (Leaflet) ─────────────────── const THREAT_AREAS = [ { name: '서해 NLL', lat: 37.80, lng: 124.90, risk: 95, vessels: 8 }, { name: 'EEZ 북부', lat: 37.20, lng: 124.63, risk: 91, vessels: 14 }, { name: '서해 5도', lat: 37.50, lng: 124.60, risk: 88, vessels: 11 }, { name: '서해 중부', lat: 36.50, lng: 124.80, risk: 65, vessels: 6 }, { name: 'EEZ 서부', lat: 36.00, lng: 123.80, risk: 72, vessels: 6 }, { name: '동해 중부', lat: 37.00, lng: 130.50, risk: 58, vessels: 4 }, { name: 'EEZ 남부', lat: 34.50, lng: 127.50, risk: 45, vessels: 3 }, { name: '남해 서부', lat: 34.20, lng: 126.00, risk: 22, vessels: 1 }, ]; // 히트맵용 위협 포인트 생성 function generateThreatHeat(): [number, number, number][] { const pts: [number, number, number][] = []; THREAT_AREAS.forEach((a) => { const count = Math.round(a.vessels * 2.5); const intensity = a.risk / 100; for (let i = 0; i < count; i++) { pts.push([ a.lat + (Math.random() - 0.5) * 0.8, a.lng + (Math.random() - 0.5) * 1.0, intensity * (0.6 + Math.random() * 0.4), ]); } }); return pts; } const THREAT_HEAT = generateThreatHeat(); // 모듈 스코프: static mock data → 마커도 module-level const THREAT_MARKERS: MarkerData[] = THREAT_AREAS.map((a) => ({ lat: a.lat, lng: a.lng, color: a.risk > 85 ? '#ef4444' : a.risk > 60 ? '#f97316' : a.risk > 40 ? '#eab308' : '#3b82f6', radius: Math.max(a.vessels, 5) * 150, label: a.name, })); function SeaAreaMap() { const mapRef = useRef(null); const buildLayers = useCallback(() => [ ...STATIC_LAYERS, createHeatmapLayer('threat-heat', THREAT_HEAT as HeatPoint[], { radiusPixels: 22 }), createMarkerLayer('threat-markers', THREAT_MARKERS), ], []); useMapLayers(mapRef, buildLayers, []); return (
{/* 범례 */}
위협 등급
낮음
높음
{/* LIVE 인디케이터 */}
실시간 해역 위협도
); } // ─── 실시간 시계 (격리된 리렌더) ───────────────── /** 실시간 시계 — React setState/render 완전 우회, DOM 직접 조작 */ function LiveClock() { const spanRef = useRef(null); useEffect(() => { const fmt: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }; const update = () => { if (spanRef.current) { spanRef.current.textContent = new Date().toLocaleString('ko-KR', fmt); } }; update(); const timer = setInterval(update, 1000); return () => clearInterval(timer); }, []); return ; } // ─── 해역 미니맵 (memo로 불필요 리렌더 방지) ───── const MemoSeaAreaMap = memo(SeaAreaMap); // ─── 메인 대시보드 ───────────────────── export function Dashboard() { const { t } = useTranslation('dashboard'); const [defconLevel] = useState(2); const kpiStore = useKpiStore(); const eventStore = useEventStore(); const vesselStore = useVesselStore(); const patrolStore = usePatrolStore(); useEffect(() => { if (!kpiStore.loaded) kpiStore.load(); }, [kpiStore.loaded, kpiStore.load]); useEffect(() => { if (!eventStore.loaded) eventStore.load(); }, [eventStore.loaded, eventStore.load]); useEffect(() => { if (!vesselStore.loaded) vesselStore.load(); }, [vesselStore.loaded, vesselStore.load]); useEffect(() => { if (!patrolStore.loaded) patrolStore.load(); }, [patrolStore.loaded, patrolStore.load]); const KPI_DATA = useMemo(() => kpiStore.metrics.map((m) => ({ label: m.label, value: m.value, prev: m.prev ?? 0, icon: KPI_UI_MAP[m.label]?.icon ?? Radar, color: KPI_UI_MAP[m.label]?.color ?? '#3b82f6', desc: m.description ?? '', })), [kpiStore.metrics]); const TIMELINE_EVENTS: TimelineEvent[] = useMemo(() => eventStore.events.slice(0, 10).map((e) => ({ time: e.time.includes(' ') ? e.time.split(' ')[1].slice(0, 5) : e.time, level: e.level, title: e.title, detail: e.detail, vessel: e.vesselName ?? '-', area: e.area ?? '-', })), [eventStore.events]); const TOP_RISK_VESSELS = useMemo(() => vesselStore.suspects.slice(0, 8).map((v) => ({ id: v.id, name: v.name, risk: v.risk / 100, type: v.pattern ?? v.type, flag: v.flag === 'CN' ? '중국' : v.flag === 'KR' ? '한국' : '미상', tonnage: v.tonnage ?? null, speed: v.speed != null ? `${v.speed}kt` : '-', heading: v.heading != null ? `${v.heading}°` : '-', lastAIS: v.lastSignal ?? '-', location: `N${v.lat.toFixed(2)} E${v.lng.toFixed(2)}`, pattern: v.status, })), [vesselStore.suspects]); const PATROL_SHIPS = useMemo(() => patrolStore.ships.map((s) => ({ name: s.name, class: s.shipClass, status: s.status, target: s.target ?? '-', area: s.zone ?? '-', speed: `${s.speed}kt`, fuel: s.fuel, })), [patrolStore.ships]); const defconColors = ['', 'bg-red-600', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500', 'bg-blue-500']; const defconLabels = ['', 'DEFCON 1', 'DEFCON 2', 'DEFCON 3', 'DEFCON 4', 'DEFCON 5']; return (
{/* ── 상단 헤더 바 ── */}

{t('dashboard.title')}

{t('dashboard.desc')}

{defconLabels[defconLevel]} 경계강화
AI 감시체계 정상
{/* ── KPI 카드 6개 ── */}
{KPI_DATA.map((kpi) => ( ))}
{/* ── 메인 3단 레이아웃 ── */}
{/* ── 좌측: 해역 미니맵 + 해역별 위험도 ── */}
해역별 위험 선박 분포
{AREA_RISK_DATA.map((area) => (
{area.area}
85 ? 'linear-gradient(90deg, #ef4444, #dc2626)' : area.risk > 60 ? 'linear-gradient(90deg, #f97316, #ea580c)' : area.risk > 40 ? 'linear-gradient(90deg, #eab308, #ca8a04)' : 'linear-gradient(90deg, #3b82f6, #2563eb)', }} /> {area.vessels}척
85 ? '#ef4444' : area.risk > 60 ? '#f97316' : area.risk > 40 ? '#eab308' : '#3b82f6' }}>{area.risk} {area.trend === 'up' && } {area.trend === 'down' && } {area.trend === 'stable' && }
))}
{/* ── 중앙: 이벤트 타임라인 ── */}
실시간 상황 타임라인
긴급 {TIMELINE_EVENTS.filter(e => e.level === 'CRITICAL').length} 경고 {TIMELINE_EVENTS.filter(e => e.level === 'HIGH').length}
{TIMELINE_EVENTS.map((event, i) => ( ))}
{/* ── 우측: 함정 배치 + 기상 + 유형별 차트 ── */}
{/* 함정 배치 현황 */} 함정 배치 현황 {PATROL_SHIPS.length}척 운용 중
{PATROL_SHIPS.map((ship) => (
{ship.name}
{ship.class}
{ship.target !== '-' ? ship.target : ship.area}
{ship.speed}
))}
{/* 기상/해상 정보 */} 해상 기상 현황
{WEATHER_DATA.wind.speed}m/s
{WEATHER_DATA.wind.direction} 풍
돌풍 {WEATHER_DATA.wind.gust}m/s
{WEATHER_DATA.wave.height}m
파고
주기 {WEATHER_DATA.wave.period}s
{WEATHER_DATA.temp.air}°C
기온
수온 {WEATHER_DATA.temp.water}°C
{WEATHER_DATA.visibility}km
시정
해상{WEATHER_DATA.seaState}급
{/* 유형별 탐지 비율 */} 위반 유형별 탐지 현황
{VESSEL_TYPE_DATA.map((item) => (
{item.name} {item.value}건
))}
{/* ── 시간대별 탐지 추이 차트 ── */}
금일 시간대별 탐지 추이
전체 탐지 EEZ 침범
{/* ── 고위험 선박 추적 테이블 ── */}
고위험 선박 추적 현황 (AI 우선순위) {TOP_RISK_VESSELS.length}척 감시 중
{/* 테이블 헤더 */}
# 선박명 / ID 위반 유형 국적/지역 속력/침로 AIS 상태 행동패턴 위치 위험도
{TOP_RISK_VESSELS.map((vessel, index) => (
#{index + 1}
0.9 ? 'bg-red-500' : vessel.risk > 0.8 ? 'bg-orange-500' : 'bg-yellow-500'} /> {vessel.name}
{vessel.id}
{vessel.type} {vessel.flag}
{vessel.speed}
{vessel.heading}
{vessel.lastAIS} {vessel.pattern} {vessel.location}
))}
); }