- 백엔드 vessels 라우터/서비스/스케줄러 추가 (1분 주기 한국 해역 폴링) - 공통 useVesselSignals 훅 + vesselApi/vesselSignalClient 서비스 추가 - MapView에 VesselLayer/VesselInteraction/MapBoundsTracker 통합 (호버·팝업·상세 모달) - OilSpill/HNS/Rescue/Incidents 뷰에 선박 신호 연동 - vesselMockData 정리, aerial IMAGE_API_URL 기본값 변경
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import { updateVesselCache } from './vesselService.js';
|
|
import type { VesselPosition } from './vesselTypes.js';
|
|
|
|
const VESSEL_TRACK_API_URL =
|
|
process.env.VESSEL_TRACK_API_URL ?? 'https://guide.gc-si.dev/signal-batch';
|
|
const POLL_INTERVAL_MS = 60_000;
|
|
|
|
// 개별 쿠키 환경변수를 조합하여 Cookie 헤더 문자열 생성
|
|
function buildVesselCookie(): string {
|
|
const entries: [string, string | undefined][] = [
|
|
['apt.uid', process.env.VESSEL_COOKIE_APT_UID],
|
|
['g_state', process.env.VESSEL_COOKIE_G_STATE],
|
|
['gc_proxy_auth', process.env.VESSEL_COOKIE_GC_PROXY_AUTH],
|
|
['GC_SESSION', process.env.VESSEL_COOKIE_GC_SESSION],
|
|
// 기존 단일 쿠키 변수 폴백 (레거시 지원)
|
|
];
|
|
const parts = entries
|
|
.filter(([, v]) => v)
|
|
.map(([k, v]) => `${k}=${v}`);
|
|
|
|
// 기존 VESSEL_TRACK_COOKIE 폴백 (단일 문자열로 설정된 경우)
|
|
if (parts.length === 0 && process.env.VESSEL_TRACK_COOKIE) {
|
|
return process.env.VESSEL_TRACK_COOKIE;
|
|
}
|
|
return parts.join('; ');
|
|
}
|
|
|
|
// 한국 전 해역 고정 폴리곤 (124~132°E, 32~38°N)
|
|
const KOREA_WATERS_POLYGON = [
|
|
[120, 31],
|
|
[132, 31],
|
|
[132, 41],
|
|
[120, 41],
|
|
[120, 31],
|
|
];
|
|
|
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
|
|
async function pollVesselSignals(): Promise<void> {
|
|
const url = `${VESSEL_TRACK_API_URL}/api/v1/vessels/recent-positions-detail`;
|
|
const body = {
|
|
minutes: 5,
|
|
coordinates: KOREA_WATERS_POLYGON,
|
|
polygonFilter: true,
|
|
};
|
|
|
|
const cookie = buildVesselCookie();
|
|
const requestHeaders: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(cookie ? { Cookie: cookie } : {}),
|
|
};
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: requestHeaders,
|
|
body: JSON.stringify(body),
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
console.error(`[vesselScheduler] 선박 신호 API 오류: ${res.status}`, text.substring(0, 200));
|
|
return;
|
|
}
|
|
|
|
const contentType = res.headers.get('content-type') ?? '';
|
|
if (!contentType.includes('application/json')) {
|
|
const text = await res.text().catch(() => '');
|
|
console.error('[vesselScheduler] 선박 신호 API가 JSON이 아닌 응답 반환:', text);
|
|
return;
|
|
}
|
|
|
|
const data = (await res.json()) as VesselPosition[];
|
|
updateVesselCache(data);
|
|
} catch (err) {
|
|
console.error('[vesselScheduler] 선박 신호 폴링 실패:', err);
|
|
}
|
|
}
|
|
|
|
export function startVesselScheduler(): void {
|
|
if (intervalId !== null) return;
|
|
|
|
// 서버 시작 시 즉시 1회 실행 후 주기적 폴링
|
|
pollVesselSignals();
|
|
intervalId = setInterval(pollVesselSignals, POLL_INTERVAL_MS);
|
|
console.log('[vesselScheduler] 선박 신호 스케줄러 시작 (1분 간격)');
|
|
}
|
|
|
|
export function stopVesselScheduler(): void {
|
|
if (intervalId !== null) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
console.log('[vesselScheduler] 선박 신호 스케줄러 중지');
|
|
}
|
|
}
|