fix: 연관 선박 위치 live fallback — 트랙 데이터 없을 때 ships 배열 사용

- useGearReplayLayers에 shipsRef 파라미터 추가
- corrPositions 계산: 트랙 보간 우선 → live 선박 위치 fallback
- KoreaMap: allShips → shipsRef에 매 렌더 동기화 (ref로 re-render 방지)
- globalThis.Map으로 react-map-gl Map 타입 충돌 해결

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
htlee 2026-03-31 08:26:07 +09:00
부모 6ba3db5cee
커밋 c4186a327d
2개의 변경된 파일47개의 추가작업 그리고 23개의 파일을 삭제

파일 보기

@ -238,13 +238,21 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
// ── deck.gl 리플레이 레이어 (Zustand → imperative setProps, React 렌더 우회) ──
const reactLayersRef = useRef<DeckLayer[]>([]);
type ShipPos = { lng: number; lat: number; course?: number };
const shipsRef = useRef(new globalThis.Map<string, ShipPos>());
// live 선박 위치를 ref에 동기화 (리플레이 fallback용)
const allShipsList = allShips ?? ships;
const shipPosMap = new globalThis.Map<string, ShipPos>();
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(() => {});

파일 보기

@ -52,6 +52,7 @@ interface CorrPosition {
export function useGearReplayLayers(
replayLayerRef: React.MutableRefObject<Layer[]>,
requestRender: () => void,
shipsRef: React.MutableRefObject<Map<string, { lng: number; lat: number; course?: number }>>,
): 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,