diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cf63c90..0de1de1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,6 +26,7 @@ import CollectorMonitor from './components/common/CollectorMonitor'; import { FieldAnalysisModal } from './components/korea/FieldAnalysisModal'; import { ReportModal } from './components/korea/ReportModal'; import { OpsGuideModal } from './components/korea/OpsGuideModal'; +import type { OpsRoute } from './components/korea/OpsGuideModal'; import { filterFacilities } from './data/meEnergyHazardFacilities'; // 정적 데이터 카운트용 import (이미 useStaticDeckLayers에서 번들 포함됨) import { EAST_ASIA_PORTS } from './data/ports'; @@ -196,6 +197,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { const [showFieldAnalysis, setShowFieldAnalysis] = useState(false); const [showReport, setShowReport] = useState(false); const [showOpsGuide, setShowOpsGuide] = useState(false); + const [opsRoute, setOpsRoute] = useState(null); const [timeZone, setTimeZone] = useState<'KST' | 'UTC'>('KST'); const [hoveredShipMmsi, setHoveredShipMmsi] = useState(null); const [focusShipMmsi, setFocusShipMmsi] = useState(null); @@ -668,8 +670,9 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { {showOpsGuide && ( setShowOpsGuide(false)} + onClose={() => { setShowOpsGuide(false); setOpsRoute(null); }} onFlyTo={(lat, lng, zoom) => setFlyToTarget({ lat, lng, zoom })} + onRouteSelect={setOpsRoute} /> )} setFlyToTarget(null)} + opsRoute={opsRoute} />
void; + opsRoute?: { from: { lat: number; lng: number; name: string }; to: { lat: number; lng: number; name: string }; distanceNM: number; riskLevel: string } | null; } // MarineTraffic-style: satellite + dark ocean + nautical overlay @@ -134,7 +135,7 @@ const FILTER_I18N_KEY: Record = { ferryWatch: 'filters.ferryWatchMonitor', }; -export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis, externalFlyTo, onExternalFlyToDone }: Props) { +export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis, externalFlyTo, onExternalFlyToDone, opsRoute }: Props) { const { t } = useTranslation(); const mapRef = useRef(null); const [infra, setInfra] = useState([]); @@ -942,6 +943,61 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF onExpandedChange={setAnalysisPanelOpen} /> )} + + {/* 작전가이드 임검침로 점선 */} + {opsRoute && (() => { + const riskColor = opsRoute.riskLevel === 'CRITICAL' ? '#ef4444' : opsRoute.riskLevel === 'HIGH' ? '#f59e0b' : '#3b82f6'; + const routeGeoJson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [ + [opsRoute.from.lng, opsRoute.from.lat], + [opsRoute.to.lng, opsRoute.to.lat], + ], + }, + }], + }; + return ( + <> + + + + +
+
+ +
+ + +
+ {opsRoute.distanceNM.toFixed(1)} NM +
{opsRoute.from.name} → {opsRoute.to.name}
+
+
+ + ); + })()} ); } diff --git a/frontend/src/components/korea/OpsGuideModal.tsx b/frontend/src/components/korea/OpsGuideModal.tsx index aba5af5..d17ba21 100644 --- a/frontend/src/components/korea/OpsGuideModal.tsx +++ b/frontend/src/components/korea/OpsGuideModal.tsx @@ -5,10 +5,18 @@ import type { CoastGuardFacility } from '../../services/coastGuard'; import { getMarineTrafficCategory } from '../../utils/marineTraffic'; import { analyzeFishing, classifyFishingZone } from '../../utils/fishingAnalysis'; +export interface OpsRoute { + from: { lat: number; lng: number; name: string }; + to: { lat: number; lng: number; name: string; mmsi: string }; + distanceNM: number; + riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM'; +} + interface Props { ships: Ship[]; onClose: () => void; onFlyTo?: (lat: number, lng: number, zoom: number) => void; + onRouteSelect?: (route: OpsRoute | null) => void; } interface SuspectVessel { @@ -29,7 +37,7 @@ function haversineNM(lat1: number, lng1: number, lat2: number, lng2: number): nu const RISK_COLOR = { CRITICAL: '#ef4444', HIGH: '#f59e0b', MEDIUM: '#3b82f6' }; const RISK_ICON = { CRITICAL: '🔴', HIGH: '🟡', MEDIUM: '🔵' }; -export function OpsGuideModal({ ships, onClose, onFlyTo }: Props) { +export function OpsGuideModal({ ships, onClose, onFlyTo, onRouteSelect }: Props) { const [selectedKCG, setSelectedKCG] = useState(null); const [searchRadius, setSearchRadius] = useState(30); // NM const [pos, setPos] = useState({ x: 60, y: 60 }); @@ -212,7 +220,17 @@ export function OpsGuideModal({ ships, onClose, onFlyTo }: Props) { borderLeft: `3px solid ${RISK_COLOR[s.riskLevel]}`, cursor: onFlyTo ? 'pointer' : 'default', }} - onClick={() => onFlyTo?.(s.ship.lat, s.ship.lng, 12)} + onClick={() => { + onFlyTo?.(s.ship.lat, s.ship.lng, 10); + if (selectedKCG) { + onRouteSelect?.({ + from: { lat: selectedKCG.lat, lng: selectedKCG.lng, name: selectedKCG.name }, + to: { lat: s.ship.lat, lng: s.ship.lng, name: s.ship.name || s.ship.mmsi, mmsi: s.ship.mmsi }, + distanceNM: s.distance, + riskLevel: s.riskLevel, + }); + } + }} >
#{i + 1}