o2o-site-AEO/solution/frontend/src/hooks/useWeather.ts
Mina Choi c85c577349 이름: solution/front → solution/frontend
`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
2026-08-31 15:27:16 +09:00

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;
}