signal-batch/frontend/src/features/vessel-map/hooks/useMapInstance.ts
htlee ed0f3056b1 feat: Ship-GIS 기능 이관 — 최근위치/선박항적/뷰포트 리플레이
dark(ship-gis) 프로젝트의 맵 기반 3대 기능을 API 탐색기에 이관.
Feature 폴더 모듈화 구조로 타 프로젝트 재활용 가능하게 구성.

Phase 1: vessel-map 공유 모듈 (Deck.gl 9 + Zustand 5 + STOMP)
Phase 2: 최근 위치 (30초 폴링 + IconLayer + 선종 필터 + 팝업)
Phase 3: 선박 항적 (MMSI 조회 + PathLayer + 타임라인 보간)
Phase 4: 뷰포트 리플레이 (STOMP WebSocket 청크 + TripsLayer 애니메이션)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:19:21 +09:00

71 lines
1.7 KiB
TypeScript

import { useRef, useEffect, useCallback } from 'react'
import { MapboxOverlay } from '@deck.gl/mapbox'
import type { Layer } from '@deck.gl/core'
import type maplibregl from 'maplibre-gl'
/**
* MapLibre + Deck.gl MapboxOverlay 통합 관리 훅
*
* - MapboxOverlay 생성/제거 라이프사이클
* - setLayers()로 Deck.gl 레이어 동적 업데이트
* - pickObject()로 클릭/호버 인터랙션
*/
export function useMapInstance(map: maplibregl.Map | null) {
const overlayRef = useRef<MapboxOverlay | null>(null)
// Overlay 초기화 + 정리
useEffect(() => {
if (!map) return
const initOverlay = () => {
if (overlayRef.current) return
const overlay = new MapboxOverlay({
interleaved: false,
layers: [],
})
map.addControl(overlay)
overlayRef.current = overlay
}
if (map.loaded()) {
initOverlay()
} else {
map.on('load', initOverlay)
}
return () => {
map.off('load', initOverlay)
if (overlayRef.current) {
try {
map.removeControl(overlayRef.current)
} catch {
// 맵이 이미 제거된 경우
}
overlayRef.current.finalize()
overlayRef.current = null
}
}
}, [map])
/** Deck.gl 레이어 배열 업데이트 */
const setLayers = useCallback((layers: Layer[]) => {
overlayRef.current?.setProps({ layers })
}, [])
/** 특정 좌표의 객체 피킹 */
const pickObject = useCallback(
(x: number, y: number, layerIds?: string[]) => {
if (!overlayRef.current) return null
try {
return overlayRef.current.pickObject({ x, y, layerIds }) ?? null
} catch {
return null
}
},
[],
)
return { overlayRef, setLayers, pickObject }
}