kcg-ai-monitoring/frontend/src/lib/map/hooks/useMapLayers.ts
htlee 2ee8a0e7ff feat(detection): DAR-03 어구 탐지 워크플로우 + 모선 검토 UI + 24h 리플레이 통합
- prediction: G-01/G-04/G-05/G-06 위반 분류 + 쌍끌이 공조 탐지 추가
- backend: 모선 확정/제외 API + signal-batch 항적 프록시 + ParentResolution 점수 근거 필드 확장
- frontend: 어구 탐지 그리드 다중필터/지도 flyTo, 후보 검토 패널(점수 근거+확정/제외), 24h convex hull 리플레이 + TripsLayer 애니메이션
- gitignore: 루트 .venv/ 추가
2026-04-15 13:26:15 +09:00

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;
}