import type { WeatherQueryPoint, WeatherPoint, WeatherSnapshot } from '../model/types'; const MARINE_BASE = 'https://marine-api.open-meteo.com/v1/marine'; const WEATHER_BASE = 'https://api.open-meteo.com/v1/forecast'; const MARINE_PARAMS = 'current=wave_height,wave_direction,wave_period,swell_wave_height,swell_wave_direction,sea_surface_temperature'; const WEATHER_PARAMS = 'current=wind_speed_10m,wind_direction_10m,wind_gusts_10m,temperature_2m,weather_code'; const TIMEOUT_MS = 10_000; /* Open-Meteo 다중 좌표 응답 타입 */ interface MarineCurrentItem { wave_height?: number; wave_direction?: number; wave_period?: number; swell_wave_height?: number; swell_wave_direction?: number; sea_surface_temperature?: number; } interface WeatherCurrentItem { wind_speed_10m?: number; wind_direction_10m?: number; wind_gusts_10m?: number; temperature_2m?: number; weather_code?: number; } /* 단일 좌표 응답 */ interface SingleMarineResponse { current?: MarineCurrentItem; } /* 다중 좌표 응답 — 배열 */ type MarineResponse = SingleMarineResponse | SingleMarineResponse[]; interface SingleWeatherResponse { current?: WeatherCurrentItem; } type WeatherResponse = SingleWeatherResponse | SingleWeatherResponse[]; function buildMultiCoordParams(points: WeatherQueryPoint[]): string { const lats = points.map((p) => p.lat.toFixed(2)).join(','); const lons = points.map((p) => p.lon.toFixed(2)).join(','); return `latitude=${lats}&longitude=${lons}`; } function n(v: number | undefined): number | null { return v != null && Number.isFinite(v) ? v : null; } /** * Open-Meteo Marine + Weather API를 병렬 호출하여 기상 스냅샷 반환. * 좌표 배열 기반이므로 수역 centroid 외에도 임의 좌표에 활용 가능. */ export async function fetchWeatherForPoints( points: WeatherQueryPoint[], ): Promise { if (points.length === 0) { return { points: [], fetchedAt: Date.now() }; } const coords = buildMultiCoordParams(points); const ac = new AbortController(); const timer = setTimeout(() => ac.abort(), TIMEOUT_MS); try { const [marineRaw, weatherRaw] = await Promise.all([ fetch(`${MARINE_BASE}?${coords}&${MARINE_PARAMS}`, { signal: ac.signal }) .then((r) => r.json() as Promise), fetch(`${WEATHER_BASE}?${coords}&${WEATHER_PARAMS}`, { signal: ac.signal }) .then((r) => r.json() as Promise), ]); // 단일 좌표면 배열이 아닌 단일 객체가 반환됨 → 통일 const marines: SingleMarineResponse[] = Array.isArray(marineRaw) ? marineRaw : [marineRaw]; const weathers: SingleWeatherResponse[] = Array.isArray(weatherRaw) ? weatherRaw : [weatherRaw]; const result: WeatherPoint[] = points.map((pt, i) => { const mc = marines[i]?.current; const wc = weathers[i]?.current; return { label: pt.label, color: pt.color, lat: pt.lat, lon: pt.lon, zoneId: pt.zoneId, waveHeight: n(mc?.wave_height), waveDirection: n(mc?.wave_direction), wavePeriod: n(mc?.wave_period), swellHeight: n(mc?.swell_wave_height), swellDirection: n(mc?.swell_wave_direction), seaSurfaceTemp: n(mc?.sea_surface_temperature), windSpeed: n(wc?.wind_speed_10m), windDirection: n(wc?.wind_direction_10m), windGusts: n(wc?.wind_gusts_10m), temperature: n(wc?.temperature_2m), weatherCode: n(wc?.weather_code), }; }); return { points: result, fetchedAt: Date.now() }; } finally { clearTimeout(timer); } }