import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import type maplibregl from 'maplibre-gl'; import { BaseMap, STATIC_LAYERS, createMarkerLayer, createPolylineLayer, useMapLayers, type MapHandle } from '@lib/map'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { Navigation, Ship, MapPin, Clock, Wind, Anchor, Play, BarChart3, Target, Settings, CheckCircle, Share2 } from 'lucide-react'; import { usePatrolStore } from '@stores/patrolStore'; /* SFR-07: AI 경비함정 단일 함정 순찰·경로 추천 */ const RANGE_MAP: Record = { '태극급': '3,500NM', '참수리급': '800NM', '삼봉급': '5,000NM', }; const ROUTE_SHIP_IDS = ['P-3001', 'P-3005', 'P-3009', 'P-5001']; export function PatrolRoute() { const { t } = useTranslation('patrol'); const { ships, routes, scenarios, load } = usePatrolStore(); useEffect(() => { load(); }, [load]); const [selectedShip, setSelectedShip] = useState('P-3001'); const [selectedScenario, setSelectedScenario] = useState(1); const SHIPS = useMemo( () => ships .filter((s) => ROUTE_SHIP_IDS.includes(s.id)) .map((s) => ({ id: s.id, name: s.name, class: s.shipClass, speed: `${s.speed}kt`, range: RANGE_MAP[s.shipClass] ?? '-', status: ['추적중', '검문중', '초계중'].includes(s.status) ? '출동중' : s.status === '정비중' ? '정비중' : '가용', })), [ships], ); const mapRef = useRef(null); const route = routes[selectedShip] || routes['P-3001']; const currentShip = SHIPS.find(s => s.id === selectedShip) ?? SHIPS[0]; const wps = route?.waypoints ?? []; const buildLayers = useCallback(() => { if (wps.length === 0) return [...STATIC_LAYERS]; const routeCoords: [number, number][] = wps.map(w => [w.lat, w.lng]); const midMarkers = []; for (let i = 0; i < wps.length - 1; i++) { midMarkers.push({ lat: (wps[i].lat + wps[i + 1].lat) / 2, lng: (wps[i].lng + wps[i + 1].lng) / 2, color: '#06b6d4', radius: 500, }); } const waypointMarkers = wps.map((wp, i) => { const isStart = i === 0; const isEnd = i === wps.length - 1; const color = isStart || isEnd ? '#22c55e' : '#06b6d4'; return { lat: wp.lat, lng: wp.lng, color, radius: isStart || isEnd ? 1400 : 1000, label: `WP${i + 1}` }; }); return [ ...STATIC_LAYERS, createPolylineLayer('patrol-route', routeCoords, { color: '#06b6d4', width: 3, opacity: 0.8 }), createMarkerLayer('route-midpoints', midMarkers, '#06b6d4', 500), createMarkerLayer('waypoint-markers', waypointMarkers), ]; }, [wps]); useMapLayers(mapRef, buildLayers, [wps]); const handleMapReady = useCallback((map: maplibregl.Map) => { if (wps.length === 0) return; const lngs = wps.map(w => w.lng); const lats = wps.map(w => w.lat); map.fitBounds( [[Math.min(...lngs), Math.min(...lats)], [Math.max(...lngs), Math.max(...lats)]], { padding: 40 }, ); }, [wps]); const mapCenter: [number, number] = wps.length > 0 ? [wps.reduce((s, w) => s + w.lat, 0) / wps.length, wps.reduce((s, w) => s + w.lng, 0) / wps.length] : [36.0, 126.0]; if (!currentShip || !route) return null; return ( } />
{/* 함정 선택 */}
함정 선택
{SHIPS.map(s => (
s.status === '가용' && setSelectedShip(s.id)} className={`px-3 py-2 rounded-lg cursor-pointer transition-colors ${selectedShip === s.id ? 'bg-cyan-600/20 border border-cyan-500/30' : 'bg-surface-overlay border border-transparent hover:border-border'} ${s.status !== '가용' ? 'opacity-40 cursor-not-allowed' : ''}`}>
{s.name} {s.status}
{s.class} · {s.speed} · {s.range}
))}
{/* 경로 Waypoint */}
추천 순찰 경로 ({currentShip.name})
{route.waypoints.map((wp, i) => (
{i + 1}
{wp.name}
{wp.lat.toFixed(2)}°N, {wp.lng.toFixed(2)}°E
ETA {wp.eta}
))}
{[['총 거리', route.summary.dist], ['예상 시간', route.summary.time], ['연료 소모', route.summary.fuel], ['커버 격자', route.summary.grids]].map(([k, v]) => (
{k}
{v}
))}
{/* 시나리오 가중치 */}
시나리오 가중치
{scenarios.map((sc, i) => (
setSelectedScenario(i)} className={`px-3 py-2 rounded-lg cursor-pointer ${selectedScenario === i ? 'bg-yellow-500/10 border border-yellow-500/30' : 'bg-surface-overlay border border-transparent'}`}>
{sc.name} {sc.score}점
위험 {sc.weight.risk}% 연료 {sc.weight.fuel}% 시간 {sc.weight.time}%
))}
기존 방식 대비 효율성
커버리지+32%
연료 절감-18%
{/* 순찰 경로 지도 */} {/* 범례 */}
순찰 경로
출발/귀항
경유 초계점
순찰 경로
EEZ
NLL
{/* 함정 정보 */}
{currentShip.name} {currentShip.class} · {route.waypoints.length} waypoints · {route.summary.dist}
); }