- prediction: G-01/G-04/G-05/G-06 위반 분류 + 쌍끌이 공조 탐지 추가 - backend: 모선 확정/제외 API + signal-batch 항적 프록시 + ParentResolution 점수 근거 필드 확장 - frontend: 어구 탐지 그리드 다중필터/지도 flyTo, 후보 검토 패널(점수 근거+확정/제외), 24h convex hull 리플레이 + TripsLayer 애니메이션 - gitignore: 루트 .venv/ 추가
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
/**
|
|
* useMapLayers — React 리렌더 없이 deck.gl 레이어를 업데이트하는 hook
|
|
*
|
|
* 원리: RAF(requestAnimationFrame) 배치로 overlay.setProps() 직접 호출
|
|
* React의 setState/render cycle을 완전 우회
|
|
*/
|
|
import { useRef, useEffect } from 'react';
|
|
import type { MapboxOverlay } from '@deck.gl/mapbox';
|
|
import type { Layer } from 'deck.gl';
|
|
import type maplibregl from 'maplibre-gl';
|
|
|
|
export interface MapHandle {
|
|
overlay: MapboxOverlay | null;
|
|
map: maplibregl.Map | null;
|
|
}
|
|
|
|
/**
|
|
* 레이어를 RAF 기반으로 업데이트
|
|
* @param handleRef — BaseMap의 overlay를 담은 ref
|
|
* @param buildLayers — 레이어 빌드 함수 (호출될 때만 실행)
|
|
* @param deps — 변경 감지 대상 (shallow 비교)
|
|
*/
|
|
export function useMapLayers(
|
|
handleRef: React.RefObject<MapHandle | null>,
|
|
buildLayers: () => Layer[],
|
|
deps: unknown[],
|
|
) {
|
|
const prevRef = useRef<unknown[]>([]);
|
|
const rafRef = useRef(0);
|
|
|
|
// deps 변경 시에만 레이어 갱신 (매 렌더 아닌 deps diff 기반)
|
|
useEffect(() => {
|
|
if (shallowEqual(prevRef.current, deps)) return;
|
|
prevRef.current = deps;
|
|
|
|
cancelAnimationFrame(rafRef.current);
|
|
rafRef.current = requestAnimationFrame(() => {
|
|
handleRef.current?.overlay?.setProps({ layers: buildLayers() });
|
|
});
|
|
});
|
|
|
|
// 언마운트 시에만 레이어 초기화 — stale WebGL 참조 방지
|
|
useEffect(() => {
|
|
return () => {
|
|
cancelAnimationFrame(rafRef.current);
|
|
try { handleRef.current?.overlay?.setProps({ layers: [] }); } catch { /* finalized */ }
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
}
|
|
|
|
/**
|
|
* Zustand store.subscribe + RAF 기반 레이어 업데이트
|
|
* store 변경 → RAF 배치 → overlay.setProps (React 리렌더 0회)
|
|
*/
|
|
export function useStoreLayerSync<T>(
|
|
handleRef: React.RefObject<MapHandle | null>,
|
|
subscribe: (callback: (state: T) => void) => () => void,
|
|
buildLayers: (state: T) => Layer[],
|
|
) {
|
|
const rafRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
const unsub = subscribe((state) => {
|
|
cancelAnimationFrame(rafRef.current);
|
|
rafRef.current = requestAnimationFrame(() => {
|
|
handleRef.current?.overlay?.setProps({ layers: buildLayers(state) });
|
|
});
|
|
});
|
|
return () => {
|
|
unsub();
|
|
cancelAnimationFrame(rafRef.current);
|
|
// 언마운트 시 레이어 초기화 — stale WebGL 참조 방지
|
|
try { handleRef.current?.overlay?.setProps({ layers: [] }); } catch { /* finalized */ }
|
|
};
|
|
// buildLayers는 안정적 참조여야 함 (useCallback으로 감싸거나 모듈 스코프)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [handleRef, subscribe]);
|
|
}
|
|
|
|
function shallowEqual(a: unknown[], b: unknown[]): boolean {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (!Object.is(a[i], b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|