o2o-site-AEO/solution/site/src/sections/WeatherSection.tsx
Mina Choi 4260e20a70 [fix] site: 모달 닫기 스크롤 버그 수정 — 이용안내 체크인·체크아웃 병합, UI 다듬기
모달 onClose 가 inline 함수라 부모 리렌더마다 useEffect 가 다시 걸려
scrollY 를 0으로 덮어써 X 버튼으로 닫으면 페이지가 맨 위로 튀었다.

- lib/ui/Modal.tsx: onClose 를 ref 로 들고 [open] 에만 의존하도록 수정
  (BlogSection·ReviewSection 등 Modal 쓰는 7곳 전부 적용)
- sections/EssentialInfoSection.tsx: 체크인·체크아웃 행을 한 줄로 병합
- sections/GallerySection.tsx, UnitsSection.tsx, MobileTabBar.tsx, SiteFooter.tsx,
  items/*: 표시 폭·간격·라벨 정리
- lib/ui/Carousel.tsx: loop 이음매 간격 재점검
- scripts/prerender.ts: countUniqueContent 가 socialPosts(자체 출력)를 세지
  않도록 — 콘텐츠 0건 사이트가 게이트를 우회하던 경로

tsc 통과, vitest 100/102 passed (use-live-weather 실패 2건은 기존·무관)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 17:23:20 +09:00

126 lines
5.6 KiB
TypeScript

/**
* 오늘의 날씨.
*
* ★ 온도 두 조각으로는 아무 값이 없다 (2026-09-04, 사장님: "컨텐츠가 있어야 하지 않나")
* `27.9°C 구름많음` 은 손님 폰 잠금화면에 이미 있다. 이 섹션이 답해야 하는 건
* 날씨가 아니라 **"그래서 오늘 이 집에서 뭘 하지"** 다. 그래서 셋을 얹는다 —
* ① 날씨별 한 줄(사장님이 미리 적어 둔 것) ② 관측 시각 ③ 그 날씨의 그림.
* ★ 문장을 하나만 두지 않는다. 발행본은 정적인데 날씨는 브라우저에서 갱신되므로,
* 굽는 순간의 문장을 박으면 비 오는 날 "마당에 앉기 좋습니다"가 뜬다.
* ★ 관측 시각을 반드시 낸다. 시각 없는 숫자는 못 믿는다 — 캐시가 어긋나면 어제 값이다.
*/
import {Cloud, CloudRain, CloudSnow, Sun} from 'lucide-react';
import type {WeatherSky} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {useLiveWeather} from '@site/lib/use-live-weather';
import {useWeatherNotes} from '@site/lib/use-weather-note';
import {weatherBand, weatherMood} from '@site/lib/derive';
import {Card, Section} from '@site/lib/ui';
const MOOD_ICON = {맑음: Sun, 흐림: Cloud, : CloudRain, : CloudSnow} as const;
/** "2026-09-03T12:30" → "9월 3일 12:30". 날짜가 오늘이면 시각만 남긴다. */
function observedText(iso: string | undefined): string | undefined {
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(iso ?? '');
if (!m) return undefined;
const [, , month, day, hour, minute] = m;
return `${Number(month)}${Number(day)}${hour}:${minute} 관측`;
}
export function WeatherSection() {
const payload = useSite();
const weather = useLiveWeather({
initial: payload.local.weather,
regionCode: payload.place.regionCode,
latitude: payload.place.latitude,
longitude: payload.place.longitude,
});
const mood = weatherMood(weather?.condition ?? '');
const band = weatherBand(weather?.temperature ?? 0);
const notes = payload.local.weather;
// 하늘 문구는 아홉 갈래로 고른다(WeatherSky). 옛 payload 처럼 그 칸이 없으면 그림 넷(mood)으로 떨어진다.
const condition = (weather?.condition ?? '') as WeatherSky;
const skyKey: WeatherSky = notes?.noteSets?.[condition] ? condition : mood;
/*
* ★ 기온 줄 (2026-09-09, 사장님: "기온에 따라서 문구 다르게, 펜션지역의 관광에 맞춰서")
* 하늘 줄은 집 안 이야기(마당·CAFÉ)라 "그래서 오늘 어디를 가지"에는 답하지 않았다.
* 34도의 맑음과 3도의 맑음이 같은 문장을 받는 것도 이 때문이었다.
* ★ 없으면 줄 자체를 그리지 않는다 — 기온대는 항상 계산되지만 문장은 사장님이 적은
* 것뿐이라, 빈 칸을 우리가 채우면 지어낸 관광 안내가 된다.
* ★ 둘을 한 타이머로 같이 뽑는다 — 따로 뽑으면 두 줄이 각자 다른 순간에 바뀐다.
*/
const [line, bandLine] = useWeatherNotes(
notes?.noteSets?.[skyKey], notes?.notes?.[mood] ?? weather?.note,
notes?.tempNoteSets?.[band], notes?.tempNotes?.[band],
);
if (!weather) return null;
const observed = observedText(weather.observedAt);
const SkyIcon = MOOD_ICON[mood];
return (
<Section id="weather" tone="alt" title="오늘의 날씨">
<Card className="relative min-h-64 overflow-hidden">
{/* 아이콘은 글 뒤에 깔린다. aria-hidden — 낭독기가 읽을 것이 없다. */}
<SkyIcon
aria-hidden
strokeWidth={1}
className="pointer-events-none absolute -bottom-10 -right-10 size-64 opacity-[0.08]"
/>
<div className="relative flex flex-col gap-5 p-6 sm:p-8">
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-1">
<p className="flex items-baseline gap-2.5">
<span
className="serif tabular-nums leading-none"
style={{fontSize: 'var(--fs-display)'}}
>
{Math.round(weather.temperature)}
</span>
<span className="text-[length:var(--fs-lead)] opacity-100">°C</span>
<span className="text-[length:var(--fs-lead)] font-semibold">
{weather.condition}
</span>
</p>
{observed && (
<span className="text-[length:var(--fs-sm)] tabular-nums">
{observed}
{weather.stale && ' · 최근 관측값'}
</span>
)}
</div>
{/* 사장님이 자기 집을 두고 쓴 한 줄. 날씨 예보가 아니다. */}
{line && (
<p
key={line}
className="w4-note-fade measure serif text-[length:var(--fs-lead)] leading-loose opacity-100"
>
{line}
</p>
)}
{/*
기온 줄은 하늘 줄 아래에 둔다 — 손님이 먼저 보는 건 "비 오나"이고
"그럼 어디 가지"는 그다음이다. 활자를 한 급 낮춰 둘의 순서를 눈에도 보이게 한다.
*/}
{bandLine && (
<p
key={bandLine}
className="w4-note-fade measure flex items-start gap-2.5 text-[length:var(--fs-sm)] leading-relaxed opacity-100"
>
<span
className="border-line mt-0.5 shrink-0 rounded-full border px-2 py-0.5 text-[length:var(--fs-sm)] font-bold"
aria-hidden
>
{band}
</span>
<span>{bandLine}</span>
</p>
)}
</div>
</Card>
</Section>
);
}