- SharedFilterContext + SharedFilterProvider: 카테고리 필터 공유 상태 - useSharedFilters: Context 소비 훅 (react-refresh 호환 분리) - IranDashboard: 이란 전용 상태/JSX 추출 (274줄) - KoreaDashboard: 한국 전용 상태/JSX 추출 (323줄) - App.tsx 통합은 후속 커밋 (헤더 상태 참조 리팩토링 필요)
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { ReplayMap } from './ReplayMap';
|
|
import type { FlyToTarget } from './ReplayMap';
|
|
import { GlobeMap } from './GlobeMap';
|
|
import { SatelliteMap } from './SatelliteMap';
|
|
import { SensorChart } from '../common/SensorChart';
|
|
import { EventLog } from '../common/EventLog';
|
|
import { LayerPanel } from '../common/LayerPanel';
|
|
import { LiveControls } from '../common/LiveControls';
|
|
import { ReplayControls } from '../common/ReplayControls';
|
|
import { TimelineSlider } from '../common/TimelineSlider';
|
|
import { useIranData } from '../../hooks/useIranData';
|
|
import { useSharedFilters } from '../../hooks/useSharedFilters';
|
|
import type { GeoEvent, LayerVisibility, AppMode } from '../../types';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface IranDashboardProps {
|
|
currentTime: number;
|
|
isLive: boolean;
|
|
refreshKey: number;
|
|
replay: {
|
|
state: {
|
|
isPlaying: boolean;
|
|
speed: number;
|
|
startTime: number;
|
|
endTime: number;
|
|
currentTime: number;
|
|
};
|
|
play: () => void;
|
|
pause: () => void;
|
|
reset: () => void;
|
|
setSpeed: (s: number) => void;
|
|
setRange: (s: number, e: number) => void;
|
|
seek: (t: number) => void;
|
|
};
|
|
monitor: {
|
|
state: { currentTime: number; historyMinutes: number };
|
|
setHistoryMinutes: (m: number) => void;
|
|
};
|
|
timeZone: 'KST' | 'UTC';
|
|
onTimeZoneChange: (tz: 'KST' | 'UTC') => void;
|
|
}
|
|
|
|
const INITIAL_LAYERS: LayerVisibility = {
|
|
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,
|
|
};
|
|
|
|
const appModeFromIsLive = (isLive: boolean): AppMode => (isLive ? 'live' : 'replay');
|
|
|
|
const IranDashboard = ({
|
|
currentTime,
|
|
isLive,
|
|
refreshKey,
|
|
replay,
|
|
monitor,
|
|
timeZone,
|
|
onTimeZoneChange,
|
|
}: IranDashboardProps) => {
|
|
const { t } = useTranslation();
|
|
|
|
const [mapMode, _setMapMode] = useState<'flat' | 'globe' | 'satellite'>('satellite');
|
|
const [layers, setLayers] = useState<LayerVisibility>(INITIAL_LAYERS);
|
|
const [seismicMarker, setSeismicMarker] = useState<{
|
|
lat: number;
|
|
lng: number;
|
|
magnitude: number;
|
|
place: string;
|
|
} | null>(null);
|
|
const [flyToTarget, setFlyToTarget] = useState<FlyToTarget | null>(null);
|
|
const [hoveredShipMmsi, setHoveredShipMmsi] = useState<string | null>(null);
|
|
const [focusShipMmsi, setFocusShipMmsi] = useState<string | null>(null);
|
|
|
|
const { hiddenAcCategories, hiddenShipCategories, toggleAcCategory, toggleShipCategory } =
|
|
useSharedFilters();
|
|
|
|
const iranData = useIranData({
|
|
appMode: appModeFromIsLive(isLive),
|
|
currentTime,
|
|
isLive,
|
|
hiddenAcCategories,
|
|
hiddenShipCategories,
|
|
refreshKey,
|
|
dashboardTab: 'iran',
|
|
});
|
|
|
|
const toggleLayer = useCallback((key: keyof LayerVisibility) => {
|
|
setLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
|
}, []);
|
|
|
|
const handleEventFlyTo = useCallback((event: GeoEvent) => {
|
|
setFlyToTarget({ lat: event.lat, lng: event.lng, zoom: 8 });
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<main className="app-main">
|
|
<div className="map-panel">
|
|
{mapMode === 'flat' ? (
|
|
<ReplayMap
|
|
key="map-iran"
|
|
events={isLive ? [] : iranData.mergedEvents}
|
|
currentTime={currentTime}
|
|
aircraft={iranData.visibleAircraft}
|
|
satellites={iranData.satPositions}
|
|
ships={iranData.visibleShips}
|
|
layers={layers}
|
|
flyToTarget={flyToTarget}
|
|
onFlyToDone={() => setFlyToTarget(null)}
|
|
hoveredShipMmsi={hoveredShipMmsi}
|
|
focusShipMmsi={focusShipMmsi}
|
|
onFocusShipClear={() => setFocusShipMmsi(null)}
|
|
seismicMarker={seismicMarker}
|
|
/>
|
|
) : mapMode === 'globe' ? (
|
|
<GlobeMap
|
|
events={isLive ? [] : iranData.mergedEvents}
|
|
currentTime={currentTime}
|
|
aircraft={iranData.visibleAircraft}
|
|
satellites={iranData.satPositions}
|
|
ships={iranData.visibleShips}
|
|
layers={layers}
|
|
/>
|
|
) : (
|
|
<SatelliteMap
|
|
events={isLive ? [] : iranData.mergedEvents}
|
|
currentTime={currentTime}
|
|
aircraft={iranData.visibleAircraft}
|
|
satellites={iranData.satPositions}
|
|
ships={iranData.visibleShips}
|
|
layers={layers}
|
|
hoveredShipMmsi={hoveredShipMmsi}
|
|
focusShipMmsi={focusShipMmsi}
|
|
onFocusShipClear={() => setFocusShipMmsi(null)}
|
|
flyToTarget={flyToTarget}
|
|
onFlyToDone={() => setFlyToTarget(null)}
|
|
seismicMarker={seismicMarker}
|
|
/>
|
|
)}
|
|
<div className="map-overlay-left">
|
|
<LayerPanel
|
|
layers={layers as unknown as Record<string, boolean>}
|
|
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}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<aside className="side-panel">
|
|
<EventLog
|
|
events={isLive ? [] : iranData.mergedEvents}
|
|
currentTime={currentTime}
|
|
totalShipCount={iranData.ships.length}
|
|
koreanShips={iranData.koreanShips}
|
|
koreanShipsByCategory={iranData.koreanShipsByCategory}
|
|
osintFeed={iranData.osintFeed}
|
|
isLive={isLive}
|
|
dashboardTab="iran"
|
|
ships={iranData.ships}
|
|
highlightKoreanShips={layers.koreanShips}
|
|
onToggleHighlightKorean={() => setLayers(prev => ({ ...prev, koreanShips: !prev.koreanShips }))}
|
|
onShipHover={setHoveredShipMmsi}
|
|
onShipClick={(mmsi) => {
|
|
setFocusShipMmsi(mmsi);
|
|
const ship = iranData.ships.find(s => s.mmsi === mmsi);
|
|
if (ship) setFlyToTarget({ lat: ship.lat, lng: ship.lng, zoom: 10 });
|
|
}}
|
|
/>
|
|
</aside>
|
|
</main>
|
|
|
|
{layers.sensorCharts && (
|
|
<section className="charts-panel">
|
|
<SensorChart
|
|
seismicData={iranData.seismicData}
|
|
pressureData={iranData.pressureData}
|
|
currentTime={currentTime}
|
|
historyMinutes={monitor.state.historyMinutes}
|
|
onSeismicClick={(lat, lng, magnitude, place) => {
|
|
setFlyToTarget({ lat, lng, zoom: 8 });
|
|
setSeismicMarker({ lat, lng, magnitude, place });
|
|
}}
|
|
/>
|
|
</section>
|
|
)}
|
|
|
|
<footer className="app-footer">
|
|
{isLive ? (
|
|
<LiveControls
|
|
currentTime={monitor.state.currentTime}
|
|
historyMinutes={monitor.state.historyMinutes}
|
|
onHistoryChange={monitor.setHistoryMinutes}
|
|
aircraftCount={iranData.aircraft.length}
|
|
shipCount={iranData.ships.length}
|
|
satelliteCount={iranData.satPositions.length}
|
|
timeZone={timeZone}
|
|
onTimeZoneChange={onTimeZoneChange}
|
|
/>
|
|
) : (
|
|
<>
|
|
<ReplayControls
|
|
isPlaying={replay.state.isPlaying}
|
|
speed={replay.state.speed}
|
|
startTime={replay.state.startTime}
|
|
endTime={replay.state.endTime}
|
|
onPlay={replay.play}
|
|
onPause={replay.pause}
|
|
onReset={replay.reset}
|
|
onSpeedChange={replay.setSpeed}
|
|
onRangeChange={replay.setRange}
|
|
/>
|
|
<TimelineSlider
|
|
currentTime={replay.state.currentTime}
|
|
startTime={replay.state.startTime}
|
|
endTime={replay.state.endTime}
|
|
events={iranData.mergedEvents}
|
|
onSeek={replay.seek}
|
|
onEventFlyTo={handleEventFlyTo}
|
|
/>
|
|
</>
|
|
)}
|
|
</footer>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export { IranDashboard };
|
|
export type { IranDashboardProps };
|