import { useState, useEffect, useRef, useCallback } from 'react'; import { createVesselSignalClient, type VesselSignalClient, } from '@common/services/vesselSignalClient'; import { getInitialVesselSnapshot, isVesselInitEnabled, } from '@common/services/vesselApi'; import type { VesselPosition, MapBounds } from '@common/types/vessel'; /** * 선박 신호 실시간 수신 훅 * * 개발환경(VITE_VESSEL_SIGNAL_MODE=polling): * - 60초마다 백엔드 REST API(/api/vessels/in-area)를 현재 뷰포트 bbox로 호출 * * 운영환경(VITE_VESSEL_SIGNAL_MODE=websocket): * - 운영 WebSocket 서버(VITE_VESSEL_WS_URL)에 직접 연결하여 실시간 수신 * - 수신된 전체 데이터를 현재 뷰포트 bbox로 프론트에서 필터링 * * @param mapBounds MapView의 onBoundsChange로 전달받은 현재 뷰포트 bbox * @returns 현재 뷰포트 내 선박 목록 */ export function useVesselSignals(mapBounds: MapBounds | null): VesselPosition[] { const [vessels, setVessels] = useState([]); const boundsRef = useRef(mapBounds); const clientRef = useRef(null); useEffect(() => { boundsRef.current = mapBounds; }, [mapBounds]); const getViewportBounds = useCallback(() => boundsRef.current, []); useEffect(() => { const client = createVesselSignalClient(); clientRef.current = client; // 운영 환경: 로그인/새로고침 직후 최근 10분치 스냅샷을 먼저 1회 로드. // 이후 WebSocket 수신이 시작되면 최신 신호로 갱신된다. // VITE_VESSEL_INIT_ENABLED=true 일 때만 활성화(기본 비활성). if (isVesselInitEnabled()) { getInitialVesselSnapshot() .then((initial) => { const bounds = boundsRef.current; const filtered = bounds ? initial.filter( (v) => v.lon >= bounds.minLon && v.lon <= bounds.maxLon && v.lat >= bounds.minLat && v.lat <= bounds.maxLat, ) : initial; // WS 첫 메시지가 먼저 도착해 이미 채워졌다면 덮어쓰지 않음 setVessels((prev) => (prev.length === 0 ? filtered : prev)); }) .catch((e) => console.warn('[useVesselSignals] 초기 스냅샷 실패', e)); } client.start(setVessels, getViewportBounds); return () => { client.stop(); clientRef.current = null; }; }, [getViewportBounds]); // mapBounds가 바뀔 때마다(최초 채워질 때 + 이후 뷰포트 이동/줌마다) 즉시 1회 새로고침. // MapView의 onBoundsChange는 moveend/zoomend에서만 호출되므로 드래그 중 스팸은 없다. // 이후에도 60초 인터벌 폴링은 백그라운드에서 계속 동작. useEffect(() => { if (mapBounds && clientRef.current) { clientRef.current.refresh(); } }, [mapBounds]); return vessels; }