- 백엔드 vessels 라우터/서비스/스케줄러 추가 (1분 주기 한국 해역 폴링) - 공통 useVesselSignals 훅 + vesselApi/vesselSignalClient 서비스 추가 - MapView에 VesselLayer/VesselInteraction/MapBoundsTracker 통합 (호버·팝업·상세 모달) - OilSpill/HNS/Rescue/Incidents 뷰에 선박 신호 연동 - vesselMockData 정리, aerial IMAGE_API_URL 기본값 변경
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
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<VesselPosition[]>([]);
|
|
const boundsRef = useRef<MapBounds | null>(mapBounds);
|
|
const clientRef = useRef<VesselSignalClient | null>(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;
|
|
}
|