import { useEffect, useState, useMemo, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Activity, AlertTriangle, Ship, Eye, Anchor, Radar, Shield, Bell, Clock, Target, ChevronRight, Filter, MapPin, Calendar, Volume2, Search, RotateCcw, } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { AreaChart, PieChart } from '@lib/charts'; import { useKpiStore } from '@stores/kpiStore'; import { useEventStore } from '@stores/eventStore'; import { getHourlyStats, type PredictionStatsHourly } from '@/services/kpi'; import { SystemStatusPanel } from './SystemStatusPanel'; import { BaseMap, createMarkerLayer, useMapLayers, type MapHandle, type MarkerData } from '@lib/map'; import maplibregl from 'maplibre-gl'; /** * SFR-12: 모니터링 및 경보 현황판(대시보드) * * RFP 정의: * 관할 구역별 위험도, 의심 선박·어구, 단속 계획·실적, 경보 상태 등을 * 한눈에 확인할 수 있는 상황실 전용 대시보드 기능 * * 기능①: 메인 통합 대시보드 (위험도 지도 + 의심선박/어구 + 경보 + 단속이력) * 기능②: 시각·청각적 경보 강조 (5등급 색상·깜박임·알림음) * 기능③: 필터링·Drill-down (관할해역·기간·경보유형·위험등급) */ // ─── Mock 데이터 ─── const MOCK_ALERTS = [ { time: '14:28', type: 'Dark Vessel', level: 'CRITICAL', area: '서해 NLL', vessel: '미상선박', mmsi: '-' }, { time: '14:12', type: 'AIS 조작', level: 'HIGH', area: '서해 5도', vessel: '冀黄港渔05001', mmsi: '412876001' }, { time: '13:55', type: '공조 조업', level: 'CRITICAL', area: '서해 NLL', vessel: '鲁荣渔56555', mmsi: '412999888' }, { time: '13:30', type: '협정선 접근', level: 'MEDIUM', area: '동해 EEZ', vessel: 'LIAO DONG 77', mmsi: '412345678' }, { time: '12:44', type: '불법 어구', level: 'HIGH', area: '제주 남방', vessel: '-', mmsi: '-' }, { time: '12:10', type: '금어기 조업', level: 'MEDIUM', area: '남해', vessel: '대한호', mmsi: '440987654' }, { time: '11:35', type: 'Dark Vessel', level: 'HIGH', area: '서해 NLL', vessel: '미상선박-B', mmsi: '-' }, ]; const MOCK_VESSELS = [ { mmsi: '412999888', name: '鲁荣渔56555', type: 'Dark Vessel', lat: 37.20, lng: 124.63, risk: 5, time: '14:28', status: '경보발령' }, { mmsi: '412876001', name: '冀黄港渔05001', type: 'AIS 조작', lat: 37.75, lng: 125.02, risk: 4, time: '14:12', status: '모니터링' }, { mmsi: '412345678', name: 'LIAO DONG 77', type: '협정선 접근', lat: 36.80, lng: 129.50, risk: 3, time: '13:30', status: '모니터링' }, { mmsi: '440987654', name: '대한호', type: '불법어구', lat: 34.50, lng: 128.20, risk: 3, time: '12:44', status: '확인중' }, { mmsi: '-', name: '미상선박-B', type: 'Dark Vessel', lat: 37.50, lng: 124.80, risk: 4, time: '11:35', status: '추적중' }, { mmsi: '412234567', name: 'MIN RONG 8', type: '공조 조업', lat: 36.20, lng: 125.30, risk: 5, time: '13:55', status: '경보발령' }, ]; const RISK_COLORS: Record = { 5: '#ef4444', 4: '#f97316', 3: '#eab308', 2: '#3b82f6', 1: '#06b6d4' }; const RISK_LABELS: Record = { 5: '5등급(적)', 4: '4등급(주황)', 3: '3등급(황)', 2: '2등급(청)', 1: '1등급(녹)' }; const LV: Record = { CRITICAL: 'text-red-400 bg-red-500/15 border-red-500/30', HIGH: 'text-orange-400 bg-orange-500/15 border-orange-500/30', MEDIUM: 'text-yellow-400 bg-yellow-500/15 border-yellow-500/30', LOW: 'text-blue-400 bg-blue-500/15 border-blue-500/30', }; const LV_LABEL: Record = { CRITICAL: '긴급', HIGH: '고위험', MEDIUM: '주의', LOW: '관심' }; // PIE 색상 const PIE_COLOR_MAP: Record = { 'EEZ 침범': '#ef4444', '다크베셀': '#f97316', 'MMSI 변조': '#eab308', '불법환적': '#a855f7', '어구 불법': '#6b7280', }; export function MonitoringDashboard() { const { t } = useTranslation('dashboard'); const kpiStore = useKpiStore(); const eventStore = useEventStore(); const [hourlyStats, setHourlyStats] = useState([]); // ─── 필터 상태 ─── const [filterArea, setFilterArea] = useState('전국'); const [filterPeriod, setFilterPeriod] = useState('금일'); const [filterAlertType, setFilterAlertType] = useState('전체'); const [filterRisk, setFilterRisk] = useState('전체'); useEffect(() => { if (!kpiStore.loaded) kpiStore.load(); }, [kpiStore.loaded, kpiStore.load]); useEffect(() => { if (!eventStore.loaded) eventStore.load(); }, [eventStore.loaded, eventStore.load]); useEffect(() => { getHourlyStats(24).then(setHourlyStats).catch(() => setHourlyStats([])); }, []); // 24시간 추이 차트 데이터 const TREND = useMemo(() => hourlyStats.map((h) => { const hourLabel = h.statHour ? `${new Date(h.statHour).getHours()}시` : ''; let riskScore = 0, total = 0; if (h.byRiskLevel) { const weights: Record = { CRITICAL: 100, HIGH: 70, MEDIUM: 40, LOW: 10 }; Object.entries(h.byRiskLevel).forEach(([k, v]) => { const cnt = Number(v) || 0; riskScore += (weights[k.toUpperCase()] ?? 0) * cnt; total += cnt; }); } return { h: hourLabel, risk: total > 0 ? Math.round(riskScore / total) : 0, alarms: (h.eventCount ?? 0) + (h.criticalCount ?? 0) }; }), [hourlyStats]); // PIE 데이터 const PIE = kpiStore.violationTypes.map((v) => ({ name: v.type, value: v.pct, color: PIE_COLOR_MAP[v.type] ?? '#6b7280' })); // ─── 지도: MapLibre 네이티브 마커 ─── const mapRef = useRef(null); const onMapReady = useCallback((map: maplibregl.Map) => { MOCK_VESSELS.forEach(v => { const color = RISK_COLORS[v.risk] ?? '#3b82f6'; const el = document.createElement('div'); el.style.cssText = 'display:flex;flex-direction:column;align-items:center;z-index:10;'; const label = document.createElement('div'); label.style.cssText = `font-size:9px;font-weight:800;color:${color};white-space:nowrap;text-shadow:-1px -1px 0 rgba(0,0,0,0.8),1px -1px 0 rgba(0,0,0,0.8),-1px 1px 0 rgba(0,0,0,0.8),1px 1px 0 rgba(0,0,0,0.8);margin-bottom:2px;`; label.textContent = `🚢 ${v.name}`; el.appendChild(label); const dot = document.createElement('div'); const isAlert = v.risk >= 4; dot.style.cssText = `width:${isAlert ? 14 : 10}px;height:${isAlert ? 14 : 10}px;border-radius:50%;background:${color};border:2px solid rgba(255,255,255,0.8);box-shadow:0 0 8px ${color};${isAlert ? 'animation:pulse 1.5s infinite;' : ''}`; el.appendChild(dot); new maplibregl.Marker({ element: el, anchor: 'bottom' }).setLngLat([v.lng, v.lat]).addTo(map); }); }, []); // 경보 깜박임 CSS const alertFlash = (level: string) => level === 'CRITICAL' ? 'animate-pulse' : ''; const filterCls = "bg-surface-overlay border border-slate-700/50 rounded-lg px-2.5 py-1.5 text-[10px] text-heading focus:outline-none focus:border-blue-500/60 appearance-none"; return (
{/* ════════ 지도 중심 영역 (전체 폭, 높이 큼) ════════ */}
{/* 지도 위 오버레이: 헤더 + KPI */}
{/* 헤더 바 */}

{t('monitoring.title')}

{new Date().toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })} | {filterArea}
{/* KPI 4개 — 지도 위 상단 */}
{[ { label: '오늘 경보', value: `${MOCK_ALERTS.length}건`, sub: '▲2', icon: Bell, color: '#ef4444' }, { label: '의심 선박', value: `${MOCK_VESSELS.length}척`, sub: `고위험 ${MOCK_VESSELS.filter(v => v.risk >= 4).length}`, icon: Ship, color: '#f97316' }, { label: '의심 어구', value: '12개', sub: '위반 4', icon: Anchor, color: '#eab308' }, { label: '단속 완료', value: '3건', sub: '오늘', icon: Shield, color: '#10b981' }, ].map(k => (
{k.value} {k.label} ({k.sub})
))}
{/* 지도 위 오버레이: 필터 바 (하단) */}
{MOCK_VESSELS.length}척 표시
{/* 지도 위 오버레이: 범례 (좌측 하단 필터 위) */}
위험 등급
{[5, 4, 3, 2, 1].map(r => (
{RISK_LABELS[r]}
))}
{/* 지도 위 오버레이: 최근 경보 (우측) */}
최근 경보
{MOCK_ALERTS.slice(0, 5).map((a, i) => (
{a.time} {LV_LABEL[a.level]} {a.type}
))}
{/* ════════ 지도 아래 정보 영역 ════════ */}
{/* 시스템 상태 */} {/* ── 기능① 단속 계획·실적 요약 (SFR-06 연계) + 경보 설정 요약 ── */}
{/* 단속 계획·실적 */}
단속 계획·실적 요약 (SFR-06 연계)
{[ { label: '금일 계획', value: '5건', color: 'text-blue-400' }, { label: '경보 발령', value: '2건', color: 'text-red-400' }, { label: '투입 함정', value: '10척', color: 'text-cyan-400' }, { label: '단속 완료', value: '3건', color: 'text-green-400' }, ].map(s => (
{s.value}
{s.label}
))}
{[ { zone: '서해 NLL 인근', risk: 85, status: 'APPROVED', ships: '2척' }, { zone: '동해 EEZ 인근', risk: 65, status: 'DRAFT', ships: '1척' }, { zone: '제주 남방 해역', risk: 78, status: 'APPROVED', ships: '2척' }, ].map(p => (
{p.zone} = 80 ? 'text-red-400' : 'text-orange-400'}`}>{p.risk}점 {p.ships} {p.status}
))}
{/* 기능② 시각·청각적 경보 강조 설정 현황 */}
경보 강조 설정 현황 (기능②)
{[ { label: '5등급(긴급)', desc: '빨강 깜박임 + 3회 연속 경고음 + 팝업', color: 'bg-red-500', active: true }, { label: '4등급(고위험)', desc: '주황 강조 + 단일 알림음 + 팝업', color: 'bg-orange-500', active: true }, { label: '3등급(주의)', desc: '노랑 테두리 + 무음', color: 'bg-yellow-500', active: true }, { label: '2등급(관심)', desc: '파랑 테두리 + 무음', color: 'bg-blue-500', active: false }, { label: '1등급(참고)', desc: '회색 표시 + 로그만', color: 'bg-gray-500', active: false }, ].map(s => (
{s.label} {s.desc}
))}
팝업 토스트 30초 자동 소멸
야간 모드 밝기 자동 + 고대비
SFR-17 연계 경보 발령 즉시 현장 함정 AI 알림 자동 전송
{/* 의심 선박 목록 */}
의심 선박 목록 {MOCK_VESSELS.length}척
{['MMSI', '선박명', '탐지유형', '위치', '위험도', '시각', '상태'].map(h => ( ))} {MOCK_VESSELS.map(v => ( ))}
{h}
{v.mmsi} {v.name} {v.type} {v.lat.toFixed(1)}°N {v.lng.toFixed(1)}°E {RISK_LABELS[v.risk]} {v.time} {v.status}
{/* ── 기능③ Drill-down 안내 ── */}
Drill-down 기능 (기능③)
지도 격자 클릭 → 구역 상세
특정 격자 선택 시 해당 구역의 의심 선박·어구 목록, 위험도 이력, 최근 단속 현황을 팝업으로 표시합니다.
선박 클릭 → 탐지 근거(SHAP) + 항적 재생
의심 선박 선택 시 AI 탐지 근거(SHAP 기여도), 과거 항적 재생, 관련 경보 이력을 상세 패널로 제공합니다.
{/* 차트: 24시간 추이 + 탐지 유형 분포 */}
24시간 위험도·경보 추이
탐지 유형 분포
{PIE.map(d => (
{d.name}
{d.value}%
))}
{/* 실시간 이벤트 타임라인 */}
실시간 이벤트 타임라인
{eventStore.events.slice(0, 6).map((e, i) => (
{e.time.includes(' ') ? e.time.split(' ')[1].slice(0, 5) : e.time} {e.level} {e.title} {e.detail}
))}
); }