공식채널 단일화, 한일옥 거리 반영 날씨 조건을 7종으로 세분화, 축제 종료 여부와 무관하게 상시 노출, '지역 읽기'갈래 축소, 야놀자(NOL) 브랜드명 제거.
90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
import {useEffect, useState} from 'react';
|
|
import type {WeatherSnapshot} from '@o2o/shared';
|
|
|
|
export interface LiveWeather extends WeatherSnapshot {
|
|
stale?: boolean;
|
|
}
|
|
|
|
/** WMO weather code → 한 줄. ★ solution/backend/services/site_payload.py 의 _weather_condition() 과 같은 구간이어야 한다. */
|
|
export function condition(code: number): string {
|
|
if (code === 0) return '맑음';
|
|
if (code >= 1 && code <= 3) return '구름많음';
|
|
if (code === 45 || code === 48) return '안개';
|
|
if (code === 51 || code === 53 || code === 55) return '이슬비';
|
|
if (code === 56 || code === 57 || code === 66 || code === 67) return '어는비';
|
|
if (code === 61 || code === 63) return '비';
|
|
if (code === 65) return '강한비';
|
|
if (code >= 70 && code <= 79) return '눈';
|
|
if (code === 80 || code === 81 || code === 82 || code === 85 || code === 86) return '소나기';
|
|
if (code >= 95 && code <= 99) return '뇌우';
|
|
return '흐림';
|
|
}
|
|
|
|
/** 프리렌더 스냅샷으로 시작하고, 하이드레이션이 끝난 뒤에만 최신 캐시를 반영한다. */
|
|
export function useLiveWeather({
|
|
initial,
|
|
regionCode,
|
|
latitude,
|
|
longitude,
|
|
}: {
|
|
initial?: WeatherSnapshot;
|
|
regionCode?: string;
|
|
latitude?: number;
|
|
longitude?: number;
|
|
}): LiveWeather | undefined {
|
|
const [weather, setWeather] = useState<LiveWeather | undefined>(initial);
|
|
|
|
useEffect(() => {
|
|
if (!regionCode || latitude == null || longitude == null) return;
|
|
const controller = new AbortController();
|
|
|
|
async function refresh() {
|
|
try {
|
|
/*
|
|
* ★ **같은 오리진의 상대 주소로 부른다.**
|
|
* 전에는 `import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:9800'` 이었다.
|
|
* `VITE_*` 는 번들에 구워지므로 그 값이 없는 자리에서 구우면 **발행본이 방문자
|
|
* 브라우저에서 `localhost:9800` 을 부른다** — 실측(2026-09-09, `/s/stay`):
|
|
* CORS 로 전부 막혀 실시간 날씨가 한 번도 반영되지 않았고, 화면에는 굽던 날의
|
|
* 기온(6일 전 27.9°C)이 그대로 떠 있었다. 관측 시각을 같이 내도록 해 둔 덕에
|
|
* 거짓말은 아니었지만, 손님에게는 오늘 날씨로 읽힌다.
|
|
* 애초에 이 렌더러의 규칙이 **런타임 환경변수를 쓰지 않는 것**이다(site/.env.example) —
|
|
* env 를 채워 다시 굽는 건 같은 함정을 다음 사람에게 물려주는 것이다.
|
|
* ★ 상대 주소면 nginx 가 앱·발행본·API 를 한 오리진으로 주므로(CLAUDE.md) 그대로 닿고,
|
|
* CORS 도 없다. 오리진이 다른 곳(블롭 단독)에 올라가면 404 로 실패하는데,
|
|
* 그때는 아래 catch 가 구운 스냅샷을 유지한다 — 날씨가 사이트를 깨지 않는다.
|
|
*/
|
|
const query = new URLSearchParams({
|
|
region_code: regionCode!,
|
|
latitude: String(latitude),
|
|
longitude: String(longitude),
|
|
});
|
|
const response = await fetch(`/v1/local/weather?${query}`, {
|
|
signal: controller.signal,
|
|
});
|
|
if (!response.ok) return;
|
|
const body = await response.json();
|
|
if (!body?.result?.success || !body.weather) return;
|
|
setWeather((previous) => ({
|
|
temperature: Number(body.weather.temperature),
|
|
condition: condition(Number(body.weather.weather_code)),
|
|
note: previous?.note,
|
|
observedAt: body.weather.observed_at,
|
|
stale: Boolean(body.stale),
|
|
}));
|
|
} catch {
|
|
// 정적 payload의 날씨를 유지한다. 날씨 장애가 사이트 렌더를 깨면 안 된다.
|
|
}
|
|
}
|
|
|
|
void refresh();
|
|
const timer = window.setInterval(() => void refresh(), 10 * 60 * 1000);
|
|
return () => {
|
|
controller.abort();
|
|
window.clearInterval(timer);
|
|
};
|
|
}, [latitude, longitude, regionCode]);
|
|
|
|
return weather;
|
|
}
|