gc-wing/apps/web/src/entities/weather/api/fetchWeather.ts

108 lines
3.6 KiB
TypeScript
Raw Normal View 히스토리

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<WeatherSnapshot> {
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<MarineResponse>),
fetch(`${WEATHER_BASE}?${coords}&${WEATHER_PARAMS}`, { signal: ac.signal })
.then((r) => r.json() as Promise<WeatherResponse>),
]);
// 단일 좌표면 배열이 아닌 단일 객체가 반환됨 → 통일
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);
}
}