wing-ops/backend/src/vessels/vesselService.ts
jeonghyo.k 29c5293ce7 feat(vessels): 실시간 선박 신호 지도 표출 및 폴링 스케줄러 추가
- 백엔드 vessels 라우터/서비스/스케줄러 추가 (1분 주기 한국 해역 폴링)
- 공통 useVesselSignals 훅 + vesselApi/vesselSignalClient 서비스 추가
- MapView에 VesselLayer/VesselInteraction/MapBoundsTracker 통합 (호버·팝업·상세 모달)
- OilSpill/HNS/Rescue/Incidents 뷰에 선박 신호 연동
- vesselMockData 정리, aerial IMAGE_API_URL 기본값 변경
2026-04-15 14:40:28 +09:00

56 lines
1.5 KiB
TypeScript

import type { VesselPosition, BoundingBox } from './vesselTypes.js';
const VESSEL_TTL_MS = 10 * 60 * 1000; // 10분
const cachedVessels = new Map<string, VesselPosition>();
let lastUpdated: Date | null = null;
// lastUpdate가 TTL을 초과한 선박을 캐시에서 제거.
// lastUpdate 파싱이 불가능한 경우 보수적으로 유지한다.
function evictStale(): void {
const now = Date.now();
for (const [mmsi, vessel] of cachedVessels) {
const ts = Date.parse(vessel.lastUpdate);
if (Number.isNaN(ts)) continue;
if (now - ts > VESSEL_TTL_MS) {
cachedVessels.delete(mmsi);
}
}
}
export function updateVesselCache(vessels: VesselPosition[]): void {
for (const vessel of vessels) {
if (!vessel.mmsi) continue;
cachedVessels.set(vessel.mmsi, vessel);
}
evictStale();
lastUpdated = new Date();
}
export function getVesselsInBounds(bounds: BoundingBox): VesselPosition[] {
const result: VesselPosition[] = [];
for (const v of cachedVessels.values()) {
if (
v.lon >= bounds.minLon &&
v.lon <= bounds.maxLon &&
v.lat >= bounds.minLat &&
v.lat <= bounds.maxLat
) {
result.push(v);
}
}
return result;
}
export function getCacheStatus(): {
count: number;
bangjeCount: number;
lastUpdated: Date | null;
} {
let bangjeCount = 0;
for (const v of cachedVessels.values()) {
if (v.shipNm && v.shipNm.toUpperCase().includes('BANGJE')) bangjeCount++;
}
return { count: cachedVessels.size, bangjeCount, lastUpdated };
}