122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
import { getRecentObservation, OBS_STATION_CODES } from './khoaApi';
|
|
import type { WeatherSnapshot } from '@common/store/weatherSnapshotStore';
|
|
|
|
const CARDINAL_LABELS = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] as const;
|
|
|
|
export function degreesToCardinal(deg: number): string {
|
|
const idx = Math.round(((deg % 360) + 360) % 360 / 22.5) % 16;
|
|
return CARDINAL_LABELS[idx];
|
|
}
|
|
|
|
const BASE_STATIONS = [
|
|
{ id: 'incheon', name: '인천', location: { lat: 37.45, lon: 126.43 } },
|
|
{ id: 'ulsan', name: '울산', location: { lat: 35.52, lon: 129.38 } },
|
|
{ id: 'yeosu', name: '여수', location: { lat: 34.74, lon: 127.75 } },
|
|
{ id: 'jeju', name: '제주', location: { lat: 33.51, lon: 126.53 } },
|
|
{ id: 'pohang', name: '포항', location: { lat: 36.03, lon: 129.38 } },
|
|
{ id: 'mokpo', name: '목포', location: { lat: 34.78, lon: 126.38 } },
|
|
{ id: 'gunsan', name: '군산', location: { lat: 35.97, lon: 126.7 } },
|
|
{ id: 'sokcho', name: '속초', location: { lat: 38.21, lon: 128.59 } },
|
|
{ id: 'tongyeong', name: '통영', location: { lat: 34.83, lon: 128.43 } },
|
|
{ id: 'donghae', name: '동해', location: { lat: 37.52, lon: 129.14 } },
|
|
];
|
|
|
|
export async function fetchWeatherSnapshotForCoord(
|
|
lat: number,
|
|
lon: number,
|
|
): Promise<WeatherSnapshot> {
|
|
const nearest = BASE_STATIONS.reduce((best, s) => {
|
|
const d = (s.location.lat - lat) ** 2 + (s.location.lon - lon) ** 2;
|
|
const bd = (best.location.lat - lat) ** 2 + (best.location.lon - lon) ** 2;
|
|
return d < bd ? s : best;
|
|
});
|
|
|
|
const r = (n: number) => Math.round(n * 10) / 10;
|
|
|
|
const obsCode = OBS_STATION_CODES[nearest.id];
|
|
const obs = obsCode ? await getRecentObservation(obsCode) : null;
|
|
|
|
const windIcon = (spd: number) => spd > 12 ? '🌧️' : spd > 8 ? '🌦️' : spd > 5 ? '⛅' : '☀️';
|
|
const mockAstronomy = {
|
|
sunrise: '07:12', sunset: '17:58',
|
|
moonrise: '19:35', moonset: '01:50',
|
|
moonPhase: '상현달 14일', tidalRange: 6.7,
|
|
};
|
|
|
|
if (obs) {
|
|
const windSpeed = r(obs.wind_speed ?? 8.0);
|
|
const windDir = obs.wind_dir ?? 315;
|
|
const waterTemp = r(obs.water_temp ?? 8.0);
|
|
const airTemp = r(obs.air_temp ?? waterTemp);
|
|
const pressure = Math.round(obs.air_pres ?? 1013);
|
|
const waveHeight = r(1.0 + windSpeed * 0.1);
|
|
|
|
return {
|
|
stationName: nearest.name,
|
|
capturedAt: new Date().toISOString(),
|
|
wind: {
|
|
speed: windSpeed,
|
|
direction: windDir,
|
|
directionLabel: degreesToCardinal(windDir),
|
|
speed_1k: r(windSpeed * 0.8),
|
|
speed_3k: r(windSpeed * 1.2),
|
|
},
|
|
wave: {
|
|
height: waveHeight,
|
|
maxHeight: r(waveHeight * 1.6),
|
|
period: Math.floor(4 + windSpeed * 0.3),
|
|
direction: degreesToCardinal(windDir + 45),
|
|
},
|
|
temperature: { current: waterTemp, feelsLike: r(airTemp - windSpeed * 0.3) },
|
|
pressure,
|
|
visibility: pressure > 1010 ? 15 : 10,
|
|
salinity: 31.2,
|
|
forecast: [0, 3, 6, 9, 12].map((h, i) => ({
|
|
time: `${h}시`,
|
|
icon: windIcon(windSpeed + (i * 0.3 - 0.3)),
|
|
temperature: r(airTemp - i * 0.3),
|
|
windSpeed: r(windSpeed + (i * 0.2 - 0.2)),
|
|
})),
|
|
astronomy: mockAstronomy,
|
|
alert: windSpeed > 14 ? '풍랑주의보 예상' : undefined,
|
|
};
|
|
}
|
|
|
|
// Fallback: 시드 기반 더미
|
|
const seed = nearest.location.lat * 100 + nearest.location.lon;
|
|
const windSpeed = r(6 + (seed % 7));
|
|
const windDir = [0, 45, 90, 135, 180, 225, 270, 315][Math.floor(seed) % 8];
|
|
const waveHeight = r(0.8 + (seed % 20) / 10);
|
|
const temp = r(5 + (seed % 8));
|
|
|
|
return {
|
|
stationName: nearest.name,
|
|
capturedAt: new Date().toISOString(),
|
|
wind: {
|
|
speed: windSpeed,
|
|
direction: windDir,
|
|
directionLabel: degreesToCardinal(windDir),
|
|
speed_1k: r(windSpeed * 0.8),
|
|
speed_3k: r(windSpeed * 1.2),
|
|
},
|
|
wave: {
|
|
height: waveHeight,
|
|
maxHeight: r(waveHeight * 1.6),
|
|
period: 4 + (Math.floor(seed) % 3),
|
|
direction: degreesToCardinal(windDir + 45),
|
|
},
|
|
temperature: { current: temp, feelsLike: r(temp - windSpeed * 0.3) },
|
|
pressure: 1010 + (Math.floor(seed) % 12),
|
|
visibility: 12 + (Math.floor(seed) % 10),
|
|
salinity: 31.2,
|
|
forecast: [0, 3, 6, 9, 12].map((h, i) => ({
|
|
time: `${h}시`,
|
|
icon: windIcon(windSpeed + (i * 0.3 - 0.3)),
|
|
temperature: r(temp - i * 0.3),
|
|
windSpeed: r(windSpeed + (i * 0.2 - 0.2)),
|
|
})),
|
|
astronomy: mockAstronomy,
|
|
alert: windSpeed > 14 ? '풍랑주의보 예상' : undefined,
|
|
};
|
|
}
|