wing-gis/frontend/wing-gis-web/src/features/vesselLayer/hooks/useVesselDeckLayer.ts
htlee b9d924e81e chore: 팀 워크플로우 기반 초기 프로젝트 구성
WING-GIS 해양경찰 통합 GIS 위치정보시스템.
모노레포: frontend(React 19 + MapLibre + deck.gl) + services(Spring Boot + Gradle).

- npm + Nexus 프록시 레지스트리 설정
- 팀 워크플로우 v1.6.1 부트스트랩 파일 배치
- .githooks (commit-msg, post-checkout)
- custom_pre_commit: true (모노레포 pre-commit 별도 관리)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 12:36:38 +09:00

130 lines
4.3 KiB
TypeScript

import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import type { LayersList } from '@deck.gl/core';
import type maplibregl from 'maplibre-gl';
import { useStore } from '../../../hooks/useStore';
import { toVesselFeatures, type VesselFeature } from '../lib/vesselAdapter';
import { buildVesselDeckLayers } from '../lib/vesselDeckLayers';
import { toAisFeatures, aisTargetToVessel, type AisFeature } from '../lib/aisAdapter';
import { buildAisDeckLayers } from '../lib/aisDeckLayers';
import { ShipBatchRenderer, type ViewportBounds } from '../lib/ShipBatchRenderer';
export function useVesselDeckLayer(
mapRef: React.MutableRefObject<maplibregl.Map | null>,
mapSyncEpoch: number,
): LayersList {
const vessels = useStore((s) => s.vessels);
const aisTargets = useStore((s) => s.aisTargets);
const signalState = useStore((s) => s.signalState);
const selectedVessel = useStore((s) => s.selectedVessel);
const setSelectedVessel = useStore((s) => s.setSelectedVessel);
// Non-AIS dummy vessel renderer
const nonAisRendererRef = useRef<ShipBatchRenderer<VesselFeature> | null>(null);
const [renderedNonAis, setRenderedNonAis] = useState<VesselFeature[]>([]);
useEffect(() => {
const renderer = new ShipBatchRenderer<VesselFeature>();
renderer.initialize((ships) => setRenderedNonAis(ships));
nonAisRendererRef.current = renderer;
return () => { renderer.dispose(); nonAisRendererRef.current = null; };
}, []);
useEffect(() => {
const nonAis = vessels.filter((v) => v.source !== 'AIS');
nonAisRendererRef.current?.setData(toVesselFeatures(nonAis));
}, [vessels]);
// AIS renderer
const aisRendererRef = useRef<ShipBatchRenderer<AisFeature> | null>(null);
const [renderedAis, setRenderedAis] = useState<AisFeature[]>([]);
useEffect(() => {
const renderer = new ShipBatchRenderer<AisFeature>();
renderer.initialize((ships) => setRenderedAis(ships));
aisRendererRef.current = renderer;
return () => { renderer.dispose(); aisRendererRef.current = null; };
}, []);
useEffect(() => {
const features = toAisFeatures(aisTargets);
const renderer = aisRendererRef.current;
if (renderer) {
renderer.setData(features);
} else {
// Renderer not ready yet, set directly
setRenderedAis(features);
}
}, [aisTargets]);
// Viewport sync for both renderers
useEffect(() => {
const map = mapRef.current;
if (!map || mapSyncEpoch === 0) return;
const updateViewport = () => {
const bounds = map.getBounds();
const vp: ViewportBounds = {
minLon: bounds.getWest(),
maxLon: bounds.getEast(),
minLat: bounds.getSouth(),
maxLat: bounds.getNorth(),
};
const zoom = map.getZoom();
for (const r of [nonAisRendererRef.current, aisRendererRef.current]) {
if (!r) continue;
r.setViewportBounds(vp);
r.setZoom(zoom);
r.requestRender();
}
};
map.on('moveend', updateViewport);
updateViewport();
return () => { map.off('moveend', updateViewport); };
}, [mapRef, mapSyncEpoch]);
// Click handlers
const handleNonAisClick = useCallback(
(info: { object?: VesselFeature }) => {
if (info.object) setSelectedVessel(info.object.raw);
},
[setSelectedVessel],
);
const handleAisClick = useCallback(
(info: { object?: AisFeature }) => {
if (info.object) setSelectedVessel(aisTargetToVessel(info.object.raw));
},
[setSelectedVessel],
);
const selectedMmsi = selectedVessel?.mmsi ?? null;
const highlightedMmsis = useMemo(() => new Set<string>(), []);
const nonAisLayers = useMemo(
() => buildVesselDeckLayers({
ships: renderedNonAis,
signalState,
selectedMmsi,
highlightedMmsis,
onClick: handleNonAisClick,
}),
[renderedNonAis, signalState, selectedMmsi, highlightedMmsis, handleNonAisClick],
);
const selectedMmsiNum = selectedMmsi ? Number(selectedMmsi) : null;
const aisLayers = useMemo(
() => buildAisDeckLayers({
ships: renderedAis,
aisVisible: signalState['AIS'] !== false,
selectedMmsi: Number.isFinite(selectedMmsiNum) ? selectedMmsiNum : null,
onClick: handleAisClick,
}),
[renderedAis, signalState, selectedMmsiNum, handleAisClick],
);
return useMemo(() => [...aisLayers, ...nonAisLayers], [aisLayers, nonAisLayers]);
}