diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 0dc38be..59c8fa8 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -238,13 +238,21 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF // ── deck.gl 리플레이 레이어 (Zustand → imperative setProps, React 렌더 우회) ── const reactLayersRef = useRef([]); + type ShipPos = { lng: number; lat: number; course?: number }; + const shipsRef = useRef(new globalThis.Map()); + // live 선박 위치를 ref에 동기화 (리플레이 fallback용) + const allShipsList = allShips ?? ships; + const shipPosMap = new globalThis.Map(); + for (const s of allShipsList) shipPosMap.set(s.mmsi, { lng: s.lng, lat: s.lat, course: s.course }); + shipsRef.current = shipPosMap; + const requestRender = useCallback(() => { if (!overlayRef.current) return; overlayRef.current.setProps({ layers: [...reactLayersRef.current, ...replayLayerRef.current], }); }, []); - useGearReplayLayers(replayLayerRef, requestRender); + useGearReplayLayers(replayLayerRef, requestRender, shipsRef); useEffect(() => { fetchKoreaInfra().then(setInfra).catch(() => {}); diff --git a/frontend/src/hooks/useGearReplayLayers.ts b/frontend/src/hooks/useGearReplayLayers.ts index 6fbd91f..a420228 100644 --- a/frontend/src/hooks/useGearReplayLayers.ts +++ b/frontend/src/hooks/useGearReplayLayers.ts @@ -52,6 +52,7 @@ interface CorrPosition { export function useGearReplayLayers( replayLayerRef: React.MutableRefObject, requestRender: () => void, + shipsRef: React.MutableRefObject>, ): void { // ── React selectors (infrequent changes only) ──────────────────────────── const historyFrames = useGearReplayStore(s => s.historyFrames); @@ -263,9 +264,10 @@ export function useGearReplayLayers( })); } - // 6. Correlation vessel positions (interpolated from correlationTripsData) + // 6. Correlation vessel positions (트랙 보간 우선, 없으면 live 위치 fallback) const corrPositions: CorrPosition[] = []; const corrTrackMap = new Map(correlationTripsData.map(d => [d.id, d])); + const liveShips = shipsRef.current; for (const [mn, items] of correlationByModel) { if (!enabledModels.has(mn)) continue; @@ -275,29 +277,43 @@ export function useGearReplayLayers( for (const c of items as GearCorrelationItem[]) { if (corrPositions.some(p => p.mmsi === c.targetMmsi)) continue; + let lon: number | undefined; + let lat: number | undefined; + let cog = 0; + + // 방법 1: 트랙 데이터 보간 const tripData = corrTrackMap.get(c.targetMmsi); - if (!tripData) continue; - - const relTime = ct - st; - const ts = tripData.timestamps; - const path = tripData.path; - if (ts.length === 0) continue; - if (relTime < ts[0] || relTime > ts[ts.length - 1]) continue; - - // Binary search in timestamps - let lo = 0; - let hi = ts.length - 1; - while (lo < hi - 1) { - const mid = (lo + hi) >> 1; - if (ts[mid] <= relTime) lo = mid; else hi = mid; + if (tripData && tripData.timestamps.length > 0) { + const relTime = ct - st; + const ts = tripData.timestamps; + const path = tripData.path; + if (relTime >= ts[0] && relTime <= ts[ts.length - 1]) { + let lo = 0; + let hi = ts.length - 1; + while (lo < hi - 1) { + const mid = (lo + hi) >> 1; + if (ts[mid] <= relTime) lo = mid; else hi = mid; + } + const ratio = ts[hi] !== ts[lo] ? (relTime - ts[lo]) / (ts[hi] - ts[lo]) : 0; + lon = path[lo][0] + (path[hi][0] - path[lo][0]) * ratio; + lat = path[lo][1] + (path[hi][1] - path[lo][1]) * ratio; + const dx = path[hi][0] - path[lo][0]; + const dy = path[hi][1] - path[lo][1]; + cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360; + } } - const ratio = ts[hi] !== ts[lo] ? (relTime - ts[lo]) / (ts[hi] - ts[lo]) : 0; - const lon = path[lo][0] + (path[hi][0] - path[lo][0]) * ratio; - const lat = path[lo][1] + (path[hi][1] - path[lo][1]) * ratio; - // heading from segment direction - const dx = path[hi][0] - path[lo][0]; - const dy = path[hi][1] - path[lo][1]; - const cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360; + + // 방법 2: live 선박 위치 fallback + if (lon === undefined) { + const ship = liveShips.get(c.targetMmsi); + if (ship) { + lon = ship.lng; + lat = ship.lat; + cog = ship.course ?? 0; + } + } + + if (lon === undefined || lat === undefined) continue; corrPositions.push({ mmsi: c.targetMmsi,