import { useState, useMemo, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useLocalStorage, useLocalStorageSet } from '../../hooks/useLocalStorage'; import { KoreaMap } from './KoreaMap'; import { FieldAnalysisModal } from './FieldAnalysisModal'; import { ReportModal } from './ReportModal'; import { OpsGuideModal } from './OpsGuideModal'; import type { OpsRoute } from './OpsGuideModal'; import { LayerPanel, type LayerTreeNode } from '../common/LayerPanel'; import { EventLog } from '../common/EventLog'; import { LiveControls } from '../common/LiveControls'; import { ReplayControls } from '../common/ReplayControls'; import { TimelineSlider } from '../common/TimelineSlider'; import { useKoreaData } from '../../hooks/useKoreaData'; import { useVesselAnalysis } from '../../hooks/useVesselAnalysis'; import { useGroupPolygons } from '../../hooks/useGroupPolygons'; import { useKoreaFilters } from '../../hooks/useKoreaFilters'; import { useSharedFilters } from '../../hooks/useSharedFilters'; import { EAST_ASIA_PORTS } from '../../data/ports'; import { KOREAN_AIRPORTS } from '../../services/airports'; import { MILITARY_BASES } from '../../data/militaryBases'; import { GOV_BUILDINGS } from '../../data/govBuildings'; import { KOREA_WIND_FARMS } from '../../data/windFarms'; import { NK_LAUNCH_SITES } from '../../data/nkLaunchSites'; import { NK_MISSILE_EVENTS } from '../../data/nkMissileEvents'; import { COAST_GUARD_FACILITIES } from '../../services/coastGuard'; import { NAV_WARNINGS } from '../../services/navWarning'; import { PIRACY_ZONES } from '../../services/piracy'; import { KOREA_SUBMARINE_CABLES } from '../../services/submarineCable'; import { HAZARD_FACILITIES } from '../../data/hazardFacilities'; import { CN_POWER_PLANTS, CN_MILITARY_FACILITIES } from '../../data/cnFacilities'; import { JP_POWER_PLANTS, JP_MILITARY_FACILITIES } from '../../data/jpFacilities'; type DashboardTab = 'iran' | 'korea'; interface ReplayState { isPlaying: boolean; speed: number; startTime: number; endTime: number; currentTime: number; } interface ReplayControls { state: ReplayState; play: () => void; pause: () => void; reset: () => void; setSpeed: (s: number) => void; setRange: (s: number, e: number) => void; seek: (t: number) => void; } interface MonitorState { currentTime: number; historyMinutes: number; } interface MonitorControls { state: MonitorState; setHistoryMinutes: (m: number) => void; } export interface KoreaDashboardProps { currentTime: number; isLive: boolean; refreshKey: number; replay: ReplayControls; monitor: MonitorControls; timeZone: 'KST' | 'UTC'; onTimeZoneChange: (tz: 'KST' | 'UTC') => void; } export const KoreaDashboard = ({ currentTime, isLive, refreshKey, replay, monitor, timeZone, onTimeZoneChange, }: KoreaDashboardProps) => { const [showFieldAnalysis, setShowFieldAnalysis] = useState(false); const [showReport, setShowReport] = useState(false); const [showOpsGuide, setShowOpsGuide] = useState(false); const [opsRoute, setOpsRoute] = useState(null); const [externalFlyTo, setExternalFlyTo] = useState<{ lat: number; lng: number; zoom: number } | null>(null); const { t } = useTranslation(); const { hiddenAcCategories, hiddenShipCategories, toggleAcCategory, toggleShipCategory } = useSharedFilters(); const [koreaLayers, setKoreaLayers] = useLocalStorage>('koreaLayers', { ships: true, aircraft: true, satellites: true, infra: true, cables: true, cctv: true, airports: true, coastGuard: true, navWarning: true, osint: true, eez: true, piracy: true, windFarm: true, ports: true, militaryBases: true, govBuildings: true, nkLaunch: true, nkMissile: true, cnFishing: false, militaryOnly: false, overseasChina: false, overseasJapan: false, cnPower: false, cnMilitary: false, jpPower: false, jpMilitary: false, hazardPetrochemical: false, hazardLng: false, hazardOilTank: false, hazardPort: false, energyNuclear: false, energyThermal: false, industryShipyard: false, industryWastewater: false, industryHeavy: false, }); const toggleKoreaLayer = useCallback((key: string) => { setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] })); }, [setKoreaLayers]); const batchToggleKoreaLayer = useCallback((keys: string[], value: boolean) => { setKoreaLayers(prev => { const next = { ...prev }; for (const k of keys) next[k] = value; return next; }); }, [setKoreaLayers]); const [hiddenNationalities, setHiddenNationalities] = useLocalStorageSet('hiddenNationalities', new Set()); const toggleNationality = useCallback((nat: string) => { setHiddenNationalities(prev => { const next = new Set(prev); if (next.has(nat)) { next.delete(nat); } else { next.add(nat); } return next; }); }, [setHiddenNationalities]); const [hiddenFishingNats, setHiddenFishingNats] = useLocalStorageSet('hiddenFishingNats', new Set()); const toggleFishingNat = useCallback((nat: string) => { setHiddenFishingNats(prev => { const next = new Set(prev); if (next.has(nat)) { next.delete(nat); } else { next.add(nat); } return next; }); }, [setHiddenFishingNats]); const koreaData = useKoreaData({ currentTime, isLive, hiddenAcCategories, hiddenShipCategories, hiddenNationalities, refreshKey, }); const vesselAnalysis = useVesselAnalysis(true); const groupPolygons = useGroupPolygons(true); const largestGearGroup = useMemo(() => { const gears = groupPolygons.allGroups.filter(g => g.groupType !== 'FLEET'); if (gears.length === 0) return undefined; const max = gears.reduce((a, b) => a.memberCount > b.memberCount ? a : b); return { name: max.groupLabel, count: max.memberCount }; }, [groupPolygons.allGroups]); const koreaFiltersResult = useKoreaFilters( koreaData.ships, koreaData.visibleShips, vesselAnalysis.analysisMap, ); const handleTabChange = useCallback((_tab: DashboardTab) => { // Tab switching is managed by parent (App.tsx); no-op here }, []); const layerTree = useMemo((): LayerTreeNode[] => [ { key: 'ships', label: t('layers.ships'), color: '#fb923c', count: koreaData.ships.length, specialRenderer: 'shipCategories' }, { key: 'nationality', label: '국적 분류', color: '#8b5cf6', count: koreaData.ships.length, specialRenderer: 'nationalityCategories' }, { key: 'aviation', label: '항공망', color: '#22d3ee', children: [ { key: 'aircraft', label: t('layers.aircraft'), color: '#22d3ee', count: koreaData.aircraft.length, specialRenderer: 'aircraftCategories' }, { key: 'satellites', label: t('layers.satellites'), color: '#ef4444', count: koreaData.satPositions.length }, ], }, { key: 'maritime-safety', label: '해양안전', color: '#3b82f6', children: [ { key: 'cables', label: t('layers.cables'), color: '#00e5ff', count: KOREA_SUBMARINE_CABLES.length }, { key: 'coastGuard', label: t('layers.coastGuard'), color: '#4dabf7', count: COAST_GUARD_FACILITIES.length }, { key: 'navWarning', label: t('layers.navWarning'), color: '#eab308', count: NAV_WARNINGS.length }, { key: 'osint', label: t('layers.osint'), color: '#ef4444', count: koreaData.osintFeed.length }, { key: 'eez', label: t('layers.eez'), color: '#3b82f6', count: 1 }, { key: 'piracy', label: t('layers.piracy'), color: '#ef4444', count: PIRACY_ZONES.length }, { key: 'nkMissile', label: '미사일 낙하', color: '#ef4444', count: NK_MISSILE_EVENTS.length }, { key: 'nkLaunch', label: '발사/포병진지', color: '#dc2626', count: NK_LAUNCH_SITES.length }, ], }, { key: 'govt-infra', label: '국가기관망', color: '#f59e0b', children: [ { key: 'ports', label: '항구', color: '#3b82f6', count: EAST_ASIA_PORTS.length }, { key: 'airports', label: t('layers.airports'), color: '#a78bfa', count: KOREAN_AIRPORTS.length }, { key: 'militaryBases', label: '군사시설', color: '#ef4444', count: MILITARY_BASES.length }, { key: 'govBuildings', label: '정부기관', color: '#f59e0b', count: GOV_BUILDINGS.length }, ], }, { key: 'energy', label: '에너지/발전시설', color: '#a855f7', children: [ { key: 'infra', label: t('layers.infra'), color: '#ffc107' }, { key: 'windFarm', label: '풍력단지', color: '#00bcd4', count: KOREA_WIND_FARMS.length }, { key: 'energyNuclear', label: '원자력발전', color: '#a855f7', count: HAZARD_FACILITIES.filter(f => f.type === 'nuclear').length }, { key: 'energyThermal', label: '화력발전소', color: '#64748b', count: HAZARD_FACILITIES.filter(f => f.type === 'thermal').length }, ], }, { key: 'hazard', label: '위험시설', color: '#ef4444', children: [ { key: 'hazardPetrochemical', label: '해안인접석유화학단지', color: '#f97316', count: HAZARD_FACILITIES.filter(f => f.type === 'petrochemical').length }, { key: 'hazardLng', label: 'LNG저장기지', color: '#06b6d4', count: HAZARD_FACILITIES.filter(f => f.type === 'lng').length }, { key: 'hazardOilTank', label: '유류저장탱크', color: '#eab308', count: HAZARD_FACILITIES.filter(f => f.type === 'oilTank').length }, { key: 'hazardPort', label: '위험물항만하역시설', color: '#ef4444', count: HAZARD_FACILITIES.filter(f => f.type === 'hazardPort').length }, ], }, { key: 'industry', label: '산업공정/제조시설', color: '#0ea5e9', children: [ { key: 'industryShipyard', label: '조선소 도장시설', color: '#0ea5e9', count: HAZARD_FACILITIES.filter(f => f.type === 'shipyard').length }, { key: 'industryWastewater', label: '폐수/하수처리장', color: '#10b981', count: HAZARD_FACILITIES.filter(f => f.type === 'wastewater').length }, { key: 'industryHeavy', label: '시멘트/제철소', color: '#94a3b8', count: HAZARD_FACILITIES.filter(f => f.type === 'heavyIndustry').length }, ], }, { key: 'overseas', label: '해외시설', color: '#f97316', children: [ { key: 'overseasChina', label: '🇨🇳 중국', color: '#ef4444', children: [ { key: 'cnPower', label: '발전소', color: '#a855f7', count: CN_POWER_PLANTS.length }, { key: 'cnMilitary', label: '주요군사시설', color: '#ef4444', count: CN_MILITARY_FACILITIES.length }, ], }, { key: 'overseasJapan', label: '🇯🇵 일본', color: '#f472b6', children: [ { key: 'jpPower', label: '발전소', color: '#a855f7', count: JP_POWER_PLANTS.length }, { key: 'jpMilitary', label: '주요군사시설', color: '#ef4444', count: JP_MILITARY_FACILITIES.length }, ], }, ], }, ], [koreaData, t]); // 헤더 슬롯 Portal — 한국 필터 버튼 + 카운트 const headerSlot = document.getElementById('dashboard-header-slot'); const countsSlot = document.getElementById('dashboard-counts-slot'); return ( <> {headerSlot && createPortal(
, headerSlot, )} {countsSlot && createPortal(
{koreaData.aircraft.length} AC {koreaData.militaryCount} MIL {koreaData.ships.length} SHIP {koreaData.satPositions.length} SAT
, countsSlot, )}
{showFieldAnalysis && ( setShowFieldAnalysis(false)} onShowReport={() => setShowReport(v => !v)} /> )} {showReport && ( setShowReport(false)} largestGearGroup={largestGearGroup} /> )} {showOpsGuide && ( { setShowOpsGuide(false); setOpsRoute(null); }} onFlyTo={(lat, lng, zoom) => setExternalFlyTo({ lat, lng, zoom })} onRouteSelect={setOpsRoute} /> )} setExternalFlyTo(null)} opsRoute={opsRoute} />
); };