import { useState, useEffect, useCallback } from 'react'; import { useLocalStorage, useLocalStorageSet } from './hooks/useLocalStorage'; import { ReplayMap } from './components/iran/ReplayMap'; import type { FlyToTarget } from './components/iran/ReplayMap'; import { GlobeMap } from './components/iran/GlobeMap'; import { SatelliteMap } from './components/iran/SatelliteMap'; import { KoreaMap } from './components/korea/KoreaMap'; import { TimelineSlider } from './components/common/TimelineSlider'; import { ReplayControls } from './components/common/ReplayControls'; import { LiveControls } from './components/common/LiveControls'; import { SensorChart } from './components/common/SensorChart'; import { EventLog } from './components/common/EventLog'; import { LayerPanel } from './components/common/LayerPanel'; import { useReplay } from './hooks/useReplay'; import { useMonitor } from './hooks/useMonitor'; import { useIranData } from './hooks/useIranData'; import { useKoreaData } from './hooks/useKoreaData'; import { useKoreaFilters } from './hooks/useKoreaFilters'; import { useVesselAnalysis } from './hooks/useVesselAnalysis'; import type { GeoEvent, LayerVisibility, AppMode } from './types'; import { useTheme } from './hooks/useTheme'; import { useAuth } from './hooks/useAuth'; import { useTranslation } from 'react-i18next'; import LoginPage from './components/auth/LoginPage'; import CollectorMonitor from './components/common/CollectorMonitor'; import { FieldAnalysisModal } from './components/korea/FieldAnalysisModal'; // 정적 데이터 카운트용 import (이미 useStaticDeckLayers에서 번들 포함됨) 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'; import './App.css'; function App() { const { user, isLoading: authLoading, isAuthenticated, login, devLogin, logout } = useAuth(); if (authLoading) { return (
Loading...
); } if (!isAuthenticated) { return ; } return ; } interface AuthenticatedAppProps { user: { email: string; name: string; picture?: string } | null; onLogout: () => Promise; } function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { const [appMode, setAppMode] = useState('live'); const [mapMode, setMapMode] = useLocalStorage<'flat' | 'globe' | 'satellite'>('mapMode', 'satellite'); const [dashboardTab, setDashboardTab] = useLocalStorage<'iran' | 'korea'>('dashboardTab', 'iran'); const [layers, setLayers] = useLocalStorage('iranLayers', { events: true, aircraft: true, satellites: true, ships: true, koreanShips: true, airports: true, sensorCharts: false, oilFacilities: true, meFacilities: true, militaryOnly: false, overseasUS: false, overseasUK: false, overseasIran: false, overseasUAE: false, overseasSaudi: false, overseasOman: false, overseasQatar: false, overseasKuwait: false, overseasIraq: false, overseasBahrain: false, }); // Korea tab layer visibility (lifted from KoreaMap) 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]); // Category filter state (shared across tabs) const [hiddenAcCategories, setHiddenAcCategories] = useLocalStorageSet('hiddenAcCategories', new Set()); const [hiddenShipCategories, setHiddenShipCategories] = useLocalStorageSet('hiddenShipCategories', new Set()); const toggleAcCategory = useCallback((cat: string) => { setHiddenAcCategories(prev => { const next = new Set(prev); if (next.has(cat)) { next.delete(cat); } else { next.add(cat); } return next; }); }, [setHiddenAcCategories]); const toggleShipCategory = useCallback((cat: string) => { setHiddenShipCategories(prev => { const next = new Set(prev); if (next.has(cat)) { next.delete(cat); } else { next.add(cat); } return next; }); }, [setHiddenShipCategories]); // Nationality filter state (Korea tab) 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]); // Fishing vessel nationality filter state 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 [flyToTarget, setFlyToTarget] = useState(null); // 1시간마다 전체 데이터 강제 리프레시 const [refreshKey, setRefreshKey] = useState(0); useEffect(() => { const HOUR_MS = 3600_000; const interval = setInterval(() => { setRefreshKey(k => k + 1); }, HOUR_MS); return () => clearInterval(interval); }, []); const [showCollectorMonitor, setShowCollectorMonitor] = useState(false); const [showFieldAnalysis, setShowFieldAnalysis] = useState(false); const [timeZone, setTimeZone] = useState<'KST' | 'UTC'>('KST'); const [hoveredShipMmsi, setHoveredShipMmsi] = useState(null); const [focusShipMmsi, setFocusShipMmsi] = useState(null); const [seismicMarker, setSeismicMarker] = useState<{ lat: number; lng: number; magnitude: number; place: string } | null>(null); const replay = useReplay(); const monitor = useMonitor(); const { theme, toggleTheme } = useTheme(); const { t, i18n } = useTranslation(); const toggleLang = useCallback(() => { i18n.changeLanguage(i18n.language === 'ko' ? 'en' : 'ko'); }, [i18n]); const isLive = appMode === 'live'; // Unified time values based on current mode const currentTime = appMode === 'live' ? monitor.state.currentTime : replay.state.currentTime; // Iran data hook const iranData = useIranData({ appMode, currentTime, isLive, hiddenAcCategories, hiddenShipCategories, refreshKey, dashboardTab, }); // Korea data hook const koreaData = useKoreaData({ currentTime, isLive, hiddenAcCategories, hiddenShipCategories, hiddenNationalities, refreshKey, }); // Vessel analysis (Python prediction 결과) const vesselAnalysis = useVesselAnalysis(dashboardTab === 'korea'); // Korea filters hook const koreaFiltersResult = useKoreaFilters( koreaData.ships, koreaData.visibleShips, currentTime, vesselAnalysis.analysisMap, koreaLayers.cnFishing, ); const toggleLayer = useCallback((key: keyof LayerVisibility) => { setLayers(prev => ({ ...prev, [key]: !prev[key] })); }, [setLayers]); // Handle event card click from timeline: fly to location on map const handleEventFlyTo = useCallback((event: GeoEvent) => { setFlyToTarget({ lat: event.lat, lng: event.lng, zoom: 8 }); }, []); return (
{/* Dashboard Tabs (replaces title) */}
{/* Mode Toggle */} {dashboardTab === 'iran' && (
⚔️ D+{Math.floor((currentTime - new Date('2026-03-01T00:00:00Z').getTime()) / (1000 * 60 * 60 * 24))}
)} {dashboardTab === 'korea' && (
)} {dashboardTab === 'iran' && (
)}
{dashboardTab === 'iran' ? iranData.aircraft.length : koreaData.aircraft.length} AC {dashboardTab === 'iran' ? iranData.militaryCount : koreaData.militaryCount} MIL {dashboardTab === 'iran' ? iranData.ships.length : koreaData.ships.length} SHIP {dashboardTab === 'iran' ? iranData.satPositions.length : koreaData.satPositions.length} SAT
{isLive ? t('header.live') : replay.state.isPlaying ? t('header.replaying') : t('header.paused')}
{user && (
{user.picture && ( )} {user.name}
)}
{/* ═══════════════════════════════════════ IRAN DASHBOARD ═══════════════════════════════════════ */} {dashboardTab === 'iran' && ( <>
{mapMode === 'flat' ? ( setFlyToTarget(null)} hoveredShipMmsi={hoveredShipMmsi} focusShipMmsi={focusShipMmsi} onFocusShipClear={() => setFocusShipMmsi(null)} seismicMarker={seismicMarker} /> ) : mapMode === 'globe' ? ( ) : ( setFocusShipMmsi(null)} flyToTarget={flyToTarget} onFlyToDone={() => setFlyToTarget(null)} seismicMarker={seismicMarker} /> )}
} onToggle={toggleLayer as (key: string) => void} aircraftByCategory={iranData.aircraftByCategory} aircraftTotal={iranData.aircraft.length} shipsByMtCategory={iranData.shipsByCategory} shipTotal={iranData.ships.length} satelliteCount={iranData.satPositions.length} extraLayers={[ { key: 'events', label: t('layers.events'), color: '#a855f7' }, { key: 'koreanShips', label: `\u{1F1F0}\u{1F1F7} ${t('layers.koreanShips')}`, color: '#00e5ff', count: iranData.koreanShips.length }, { key: 'airports', label: t('layers.airports'), color: '#f59e0b' }, { key: 'oilFacilities', label: t('layers.oilFacilities'), color: '#d97706' }, { key: 'meFacilities', label: '주요시설/군사', color: '#ef4444', count: 35 }, { key: 'sensorCharts', label: t('layers.sensorCharts'), color: '#22c55e' }, ]} overseasItems={[ { key: 'overseasUS', label: '🇺🇸 미국', color: '#3b82f6' }, { key: 'overseasUK', label: '🇬🇧 영국', color: '#dc2626' }, { key: 'overseasIran', label: '🇮🇷 이란', color: '#22c55e' }, { key: 'overseasUAE', label: '🇦🇪 UAE', color: '#f59e0b' }, { key: 'overseasSaudi', label: '🇸🇦 사우디아라비아', color: '#84cc16' }, { key: 'overseasOman', label: '🇴🇲 오만', color: '#e11d48' }, { key: 'overseasQatar', label: '🇶🇦 카타르', color: '#8b5cf6' }, { key: 'overseasKuwait', label: '🇰🇼 쿠웨이트', color: '#f97316' }, { key: 'overseasIraq', label: '🇮🇶 이라크', color: '#65a30d' }, { key: 'overseasBahrain', label: '🇧🇭 바레인', color: '#e11d48' }, ]} hiddenAcCategories={hiddenAcCategories} hiddenShipCategories={hiddenShipCategories} onAcCategoryToggle={toggleAcCategory} onShipCategoryToggle={toggleShipCategory} />
{layers.sensorCharts && (
{ setFlyToTarget({ lat, lng, zoom: 8 }); setSeismicMarker({ lat, lng, magnitude, place }); }} />
)}
{isLive ? ( ) : ( <> )}
)} {/* ═══════════════════════════════════════ KOREA DASHBOARD ═══════════════════════════════════════ */} {dashboardTab === 'korea' && ( <>
{showFieldAnalysis && ( setShowFieldAnalysis(false)} /> )}
f.type === 'nuclear').length, group: '에너지/발전시설' }, { key: 'energyThermal', label: '화력발전소', color: '#64748b', count: HAZARD_FACILITIES.filter(f => f.type === 'thermal').length, group: '에너지/발전시설' }, // 위험시설 { key: 'hazardPetrochemical', label: '해안인접석유화학단지', color: '#f97316', count: HAZARD_FACILITIES.filter(f => f.type === 'petrochemical').length, group: '위험시설' }, { key: 'hazardLng', label: 'LNG저장기지', color: '#06b6d4', count: HAZARD_FACILITIES.filter(f => f.type === 'lng').length, group: '위험시설' }, { key: 'hazardOilTank', label: '유류저장탱크', color: '#eab308', count: HAZARD_FACILITIES.filter(f => f.type === 'oilTank').length, group: '위험시설' }, { key: 'hazardPort', label: '위험물항만하역시설', color: '#ef4444', count: HAZARD_FACILITIES.filter(f => f.type === 'hazardPort').length, group: '위험시설' }, // 산업공정/제조시설 { key: 'industryShipyard', label: '조선소 도장시설', color: '#0ea5e9', count: HAZARD_FACILITIES.filter(f => f.type === 'shipyard').length, group: '산업공정/제조시설' }, { key: 'industryWastewater', label: '폐수/하수처리장', color: '#10b981', count: HAZARD_FACILITIES.filter(f => f.type === 'wastewater').length, group: '산업공정/제조시설' }, { key: 'industryHeavy', label: '시멘트/제철소', color: '#94a3b8', count: HAZARD_FACILITIES.filter(f => f.type === 'heavyIndustry').length, group: '산업공정/제조시설' }, ]} overseasItems={[ { key: 'overseasChina', label: '🇨🇳 중국', color: '#ef4444', count: CN_POWER_PLANTS.length + CN_MILITARY_FACILITIES.length, 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', count: JP_POWER_PLANTS.length + JP_MILITARY_FACILITIES.length, children: [ { key: 'jpPower', label: '발전소', color: '#a855f7', count: JP_POWER_PLANTS.length }, { key: 'jpMilitary', label: '주요군사시설', color: '#ef4444', count: JP_MILITARY_FACILITIES.length }, ], }, ]} hiddenAcCategories={hiddenAcCategories} hiddenShipCategories={hiddenShipCategories} onAcCategoryToggle={toggleAcCategory} onShipCategoryToggle={toggleShipCategory} shipsByNationality={koreaData.shipsByNationality} hiddenNationalities={hiddenNationalities} onNationalityToggle={toggleNationality} fishingByNationality={koreaData.fishingByNationality} hiddenFishingNats={hiddenFishingNats} onFishingNatToggle={toggleFishingNat} />
{isLive ? ( ) : ( <> )}
)} {showCollectorMonitor && ( setShowCollectorMonitor(false)} /> )}
); } export default App;