MapTiler Weather SDK 6종 기상 타일 오버레이: - 바람/기온/강수/기압/레이더/구름 라디오 토글 - 3시간 단위 step 스냅 타임라인 + 드래그 실시간 seek - 색상 범례, 배속 제어, 투명도 조절 - ServiceWorker 타일 캐시 (cache-first, 최대 2000장) - SDK 시간 단위(epoch 초) 정합성 보장 Open-Meteo 수역별 기상 패널: - 4개 수역 centroid 기반 해양/기상 데이터 5분 폴링 - 파고/풍속/수온/너울 카드 UI + 경고 하이라이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
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);
|
|
}
|
|
}
|