import { useState, useEffect, useCallback } from 'react'; 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 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 './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] = useState<'flat' | 'globe' | 'satellite'>('satellite'); const [dashboardTab, setDashboardTab] = useState<'iran' | 'korea'>('iran'); const [layers, setLayers] = useState({ events: true, aircraft: true, satellites: true, ships: true, koreanShips: true, airports: true, sensorCharts: true, oilFacilities: true, militaryOnly: false, }); // Korea tab layer visibility (lifted from KoreaMap) const [koreaLayers, setKoreaLayers] = useState>({ ships: true, aircraft: true, satellites: true, infra: true, cables: true, cctv: true, airports: true, coastGuard: true, navWarning: true, osint: true, eez: true, piracy: true, militaryOnly: false, }); const toggleKoreaLayer = useCallback((key: string) => { setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] })); }, []); // Category filter state (shared across tabs) const [hiddenAcCategories, setHiddenAcCategories] = useState>(new Set()); const [hiddenShipCategories, setHiddenShipCategories] = useState>(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; }); }, []); 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; }); }, []); 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 [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, refreshKey, }); // Korea filters hook const koreaFiltersResult = useKoreaFilters( koreaData.ships, koreaData.visibleShips, currentTime, ); const toggleLayer = useCallback((key: keyof LayerVisibility) => { setLayers(prev => ({ ...prev, [key]: !prev[key] })); }, []); // 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: 'sensorCharts', label: t('layers.sensorCharts'), color: '#22c55e' }, ]} 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' && ( <>
{isLive ? ( ) : ( <> )}
)} {showCollectorMonitor && ( setShowCollectorMonitor(false)} /> )}
); } export default App;