`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로 베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import {useEffect, useState} from 'react';
|
|
|
|
export interface WeatherData {
|
|
temperature: number;
|
|
condition: string;
|
|
recommendation: string;
|
|
observedAt?: string;
|
|
isFallback?: boolean;
|
|
cached?: boolean;
|
|
stale?: boolean;
|
|
}
|
|
|
|
/** Open-Meteo weathercode → 한국어 상태 + 안내 문구. 코드표는 WMO 4677 기준. */
|
|
function describe(code: number): {condition: string; recommendation: string} {
|
|
if (code === 0) {
|
|
return {
|
|
condition: '맑음',
|
|
recommendation: '청명한 하늘 아래 해안도로 드라이브와 산책로를 추천합니다.',
|
|
};
|
|
}
|
|
if (code >= 1 && code <= 3) {
|
|
return {
|
|
condition: '구름많음',
|
|
recommendation: '부드러운 햇살과 구름이 어우러져 야외에서 노을을 감상하기 좋습니다.',
|
|
};
|
|
}
|
|
if (code >= 50 && code <= 69) {
|
|
return {
|
|
condition: '비',
|
|
recommendation: '빗소리를 들으며 실내 공간에서 따뜻하게 머무르기 좋은 날입니다.',
|
|
};
|
|
}
|
|
if (code >= 70 && code <= 79) {
|
|
return {
|
|
condition: '눈',
|
|
recommendation: '설경을 바라보며 따뜻한 차 한 잔과 함께 실내에서 쉬어가기 좋습니다.',
|
|
};
|
|
}
|
|
return {
|
|
condition: '흐림',
|
|
recommendation: '운치 있는 산책 후 실내에서 티타임을 가져보세요.',
|
|
};
|
|
}
|
|
|
|
const FALLBACK: WeatherData = {
|
|
temperature: 24.5,
|
|
condition: '맑음',
|
|
recommendation: '맑고 선선한 바람이 부는, 산책하기 좋은 날씨입니다.',
|
|
isFallback: true,
|
|
};
|
|
|
|
/**
|
|
* 실시간 날씨. Open-Meteo 는 API 키가 필요 없다 — 좌표만 있으면 된다.
|
|
*
|
|
* ★ 실패해도 사용자에게 에러를 보이지 않는다. 날씨는 부가 정보라
|
|
* 못 받아오면 조용히 폴백값을 쓴다(isFallback 으로 구분 가능).
|
|
*/
|
|
export function useWeather(regionCode?: string, lat?: number, lon?: number): WeatherData {
|
|
const [weather, setWeather] = useState<WeatherData>(FALLBACK);
|
|
|
|
useEffect(() => {
|
|
if (!regionCode || lat == null || lon == null) return;
|
|
let alive = true;
|
|
const controller = new AbortController();
|
|
|
|
async function fetchWeather() {
|
|
try {
|
|
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:9800';
|
|
const query = new URLSearchParams({
|
|
region_code: regionCode!,
|
|
latitude: String(lat),
|
|
longitude: String(lon),
|
|
});
|
|
const res = await fetch(`${baseUrl}/v1/local/weather?${query}`, {signal: controller.signal});
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const current = data?.weather;
|
|
if (!data?.result?.success || !current || !alive) return;
|
|
const {condition, recommendation} = describe(Number(current.weather_code));
|
|
setWeather({
|
|
temperature: Math.round(Number(current.temperature) * 10) / 10,
|
|
condition,
|
|
recommendation,
|
|
observedAt: current.observed_at,
|
|
cached: Boolean(data.cached),
|
|
stale: Boolean(data.stale),
|
|
});
|
|
} catch {
|
|
// 폴백 유지 — 화면에 에러를 띄우지 않는다.
|
|
}
|
|
}
|
|
|
|
void fetchWeather();
|
|
const timer = setInterval(() => void fetchWeather(), 10 * 60 * 1000);
|
|
return () => {
|
|
alive = false;
|
|
controller.abort();
|
|
clearInterval(timer);
|
|
};
|
|
}, [regionCode, lat, lon]);
|
|
|
|
return weather;
|
|
}
|