뷰포트에 관계없이 백엔드 캐시의 전체 선박을 검색 가능하도록 개선.
- backend: GET /api/vessels/all 엔드포인트 추가 (getAllVessels)
- vesselSignalClient: onAllVessels? 콜백 추가; PollingClient는 3분마다 pollAll(), WS Client는 필터링 전 전송
- useVesselSignals: { vessels, allVessels } 반환, 초기 스냅샷도 allVessels에 반영
- MapView: allVessels prop 추가, VesselSearchBar에 우선 전달
- OilSpillView/HNSView/RescueView/IncidentsView: allVessels 구조분해 후 MapView/VesselSearchBar에 전달
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
import type { VesselPosition, MapBounds } from '@/types/vessel';
|
|
import { getVesselsInArea, getAllVessels } from './vesselApi';
|
|
|
|
export interface VesselSignalClient {
|
|
start(
|
|
onVessels: (vessels: VesselPosition[]) => void,
|
|
getViewportBounds: () => MapBounds | null,
|
|
onAllVessels?: (vessels: VesselPosition[]) => void,
|
|
): void;
|
|
stop(): void;
|
|
/**
|
|
* 즉시 1회 새로고침. 폴링 모드에선 현재 bbox로 REST 호출,
|
|
* WebSocket 모드에선 no-op(서버 push에 의존).
|
|
*/
|
|
refresh(): void;
|
|
}
|
|
|
|
// 개발환경: setInterval(60s) → 백엔드 REST API 호출
|
|
class PollingVesselClient implements VesselSignalClient {
|
|
private intervalId: ReturnType<typeof setInterval> | undefined = undefined;
|
|
private allIntervalId: ReturnType<typeof setInterval> | undefined = undefined;
|
|
private onVessels: ((vessels: VesselPosition[]) => void) | null = null;
|
|
private onAllVessels: ((vessels: VesselPosition[]) => void) | undefined = undefined;
|
|
private getViewportBounds: (() => MapBounds | null) | null = null;
|
|
|
|
private async poll(): Promise<void> {
|
|
const bounds = this.getViewportBounds?.();
|
|
if (!bounds || !this.onVessels) return;
|
|
try {
|
|
const vessels = await getVesselsInArea(bounds);
|
|
this.onVessels(vessels);
|
|
} catch {
|
|
// 폴링 실패 시 무시 (다음 인터벌에 재시도)
|
|
}
|
|
}
|
|
|
|
private async pollAll(): Promise<void> {
|
|
if (!this.onAllVessels) return;
|
|
try {
|
|
const vessels = await getAllVessels();
|
|
this.onAllVessels(vessels);
|
|
} catch {
|
|
// 무시
|
|
}
|
|
}
|
|
|
|
start(
|
|
onVessels: (vessels: VesselPosition[]) => void,
|
|
getViewportBounds: () => MapBounds | null,
|
|
onAllVessels?: (vessels: VesselPosition[]) => void,
|
|
): void {
|
|
this.onVessels = onVessels;
|
|
this.onAllVessels = onAllVessels;
|
|
this.getViewportBounds = getViewportBounds;
|
|
|
|
this.poll();
|
|
this.pollAll();
|
|
this.intervalId = setInterval(() => this.poll(), 60_000);
|
|
this.allIntervalId = setInterval(() => this.pollAll(), 3 * 60_000);
|
|
}
|
|
|
|
stop(): void {
|
|
clearInterval(this.intervalId);
|
|
clearInterval(this.allIntervalId);
|
|
this.intervalId = undefined;
|
|
this.allIntervalId = undefined;
|
|
this.onVessels = null;
|
|
this.onAllVessels = undefined;
|
|
this.getViewportBounds = null;
|
|
}
|
|
|
|
refresh(): void {
|
|
this.poll();
|
|
}
|
|
}
|
|
|
|
// 운영환경: 실시간 WebSocket 서버에 직접 연결
|
|
class DirectWebSocketVesselClient implements VesselSignalClient {
|
|
private ws: WebSocket | null = null;
|
|
private readonly wsUrl: string;
|
|
|
|
constructor(wsUrl: string) {
|
|
this.wsUrl = wsUrl;
|
|
}
|
|
|
|
start(
|
|
onVessels: (vessels: VesselPosition[]) => void,
|
|
getViewportBounds: () => MapBounds | null,
|
|
onAllVessels?: (vessels: VesselPosition[]) => void,
|
|
): void {
|
|
this.ws = new WebSocket(this.wsUrl);
|
|
|
|
this.ws.onmessage = (event) => {
|
|
try {
|
|
const allVessels = JSON.parse(event.data as string) as VesselPosition[];
|
|
|
|
onAllVessels?.(allVessels);
|
|
|
|
const bounds = getViewportBounds();
|
|
|
|
if (!bounds) {
|
|
onVessels(allVessels);
|
|
return;
|
|
}
|
|
|
|
const filtered = allVessels.filter(
|
|
(v) =>
|
|
v.lon >= bounds.minLon &&
|
|
v.lon <= bounds.maxLon &&
|
|
v.lat >= bounds.minLat &&
|
|
v.lat <= bounds.maxLat,
|
|
);
|
|
onVessels(filtered);
|
|
} catch {
|
|
// 파싱 실패 무시
|
|
}
|
|
};
|
|
|
|
this.ws.onerror = () => {
|
|
console.error('[vesselSignalClient] WebSocket 연결 오류');
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
console.warn('[vesselSignalClient] WebSocket 연결 종료');
|
|
};
|
|
}
|
|
|
|
stop(): void {
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
}
|
|
|
|
refresh(): void {
|
|
// 운영 WS 모드에선 서버 push에 의존하므로 별도 새로고침 동작 없음
|
|
}
|
|
}
|
|
|
|
export function createVesselSignalClient(): VesselSignalClient {
|
|
if (import.meta.env.VITE_VESSEL_SIGNAL_MODE === 'websocket') {
|
|
const wsUrl = import.meta.env.VITE_VESSEL_WS_URL as string;
|
|
return new DirectWebSocketVesselClient(wsUrl);
|
|
}
|
|
return new PollingVesselClient();
|
|
}
|