diff --git a/frontend/src/hooks/useGearReplayLayers.ts b/frontend/src/hooks/useGearReplayLayers.ts index cfd29b9..d7b0c82 100644 --- a/frontend/src/hooks/useGearReplayLayers.ts +++ b/frontend/src/hooks/useGearReplayLayers.ts @@ -84,21 +84,8 @@ export function useGearReplayLayers( const ct = state.currentTime; const st = state.startTime; - // 디버그: 첫 프레임에서 데이터 상태 출력 - if (!debugLoggedRef.current) { - debugLoggedRef.current = true; - console.log('[GearReplay] renderFrame 시작:', { - historyFrames: state.historyFrames.length, - correlationByModel: state.correlationByModel.size, - modelNames: [...state.correlationByModel.keys()], - correlationTripsData: correlationTripsData.length, - enabledModels: [...enabledModels], - enabledVessels: enabledVessels.size, - }); - for (const [mn, items] of state.correlationByModel) { - console.log(` [${mn}] ${items.length}건 (vessels: ${items.filter(c => c.targetType === 'VESSEL').length}, gear: ${items.filter(c => c.targetType !== 'VESSEL').length})`); - } - } + const shouldLog = !debugLoggedRef.current; + if (shouldLog) debugLoggedRef.current = true; // Find current frame const { index: frameIdx, cursor } = findFrameAtTime(state.frameTimes, ct, cursorRef.current); @@ -264,10 +251,11 @@ export function useGearReplayLayers( })); } - // 6. Correlation vessel positions (트랙 보간 우선, 없으면 live 위치 fallback) + // 6. Correlation vessel positions (트랙 보간 → 끝점 clamp → live fallback) const corrPositions: CorrPosition[] = []; const corrTrackMap = new Map(correlationTripsData.map(d => [d.id, d])); const liveShips = shipsRef.current; + const relTime = ct - st; for (const [mn, items] of correlationByModel) { if (!enabledModels.has(mn)) continue; @@ -281,13 +269,31 @@ export function useGearReplayLayers( let lat: number | undefined; let cog = 0; - // 방법 1: 트랙 데이터 보간 + // 방법 1: 트랙 데이터 (보간 + 범위 밖은 끝점 clamp) const tripData = corrTrackMap.get(c.targetMmsi); - if (tripData && tripData.timestamps.length > 0) { - const relTime = ct - st; + if (tripData && tripData.path.length > 0) { const ts = tripData.timestamps; const path = tripData.path; - if (relTime >= ts[0] && relTime <= ts[ts.length - 1]) { + + if (relTime <= ts[0]) { + // 트랙 시작 전 → 첫 점 사용 + lon = path[0][0]; lat = path[0][1]; + if (path.length > 1) { + const dx = path[1][0] - path[0][0]; + const dy = path[1][1] - path[0][1]; + cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360; + } + } else if (relTime >= ts[ts.length - 1]) { + // 트랙 종료 후 → 마지막 점 사용 + const last = path.length - 1; + lon = path[last][0]; lat = path[last][1]; + if (last > 0) { + const dx = path[last][0] - path[last - 1][0]; + const dy = path[last][1] - path[last - 1][1]; + cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360; + } + } else { + // 범위 내 → 보간 let lo = 0; let hi = ts.length - 1; while (lo < hi - 1) { @@ -327,6 +333,33 @@ export function useGearReplayLayers( } } + // 디버그: 첫 프레임에서 전체 상태 출력 + if (shouldLog) { + const trackHit = corrPositions.filter(p => corrTrackMap.has(p.mmsi)).length; + const liveHit = corrPositions.length - trackHit; + console.log('[GearReplay] renderFrame:', { + historyFrames: state.historyFrames.length, + correlationByModel: state.correlationByModel.size, + modelNames: [...state.correlationByModel.keys()], + corrTripsData: correlationTripsData.length, + corrTrackMap: corrTrackMap.size, + enabledModels: [...enabledModels], + enabledVessels: enabledVessels.size, + relTime: Math.round(relTime / 60000) + 'min', + members: members.length, + corrPositions: corrPositions.length, + posSource: `track:${trackHit} live:${liveHit}`, + }); + // 모델별 상세 + for (const [mn, items] of state.correlationByModel) { + const modEnabled = enabledModels.has(mn); + const modPositions = corrPositions.filter(p => { + return items.some(c => c.targetMmsi === p.mmsi); + }).length; + console.log(` [${mn}] ${modEnabled ? 'ON' : 'OFF'} ${items.length}건 → 위치확인 ${modPositions}`); + } + } + if (corrPositions.length > 0) { layers.push(new IconLayer({ id: 'replay-corr-vessels',