o2o-site-AEO/solution/site/src/layouts/coral/Weather.tsx
Mina Choi 013cdcfc9f [feat] solution/site,shared: 숙박 템플릿 8종 추가 — 라운드·시네마·빅타이포·부티크·일러스트·코랄·미니멀·솔숲
사장님이 고를 수 있는 숙박 템플릿이 고택 하나뿐이었다. 모두 stay2 콘텐츠(날씨·미니 블로그·
지역 이야기·객실·축제·일정·후기·엽서·노래)를 그대로 싣고 모양만 다르다.

- layouts/<id>/: 템플릿별 Frame·섹션·CSS. 데스크톱은 넓은 배치(1024px 이상), 모바일 우선
- layouts/kit/kit.css: 탭 전환·시트·예약 폼·접기 등 공통 구조
- shared templates.json · builder.ts · catalog.ts: 템플릿·LayoutId 등록
- seo/head.ts: 템플릿별 한글 웹폰트 — 서로 겹치지 않게 배정
- paper Frame·Intro: 같은 탭을 다시 눌러도 맨 위로, 미니 블로그를 글 여러 개로
- docs/TEMPLATES.md · TEMPLATE_DESIGN.md: 목록과 화면 규칙(간격·접기·✓ 표시·글꼴)

eslint·tsc 통과, vitest 123 passed, 템플릿 9개 굽기 성공 · 간격 검사 0건
2026-09-30 13:45:10 +09:00

79 lines
2.6 KiB
TypeScript

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 {CloudIcon, RainIcon, SnowIcon, SunIcon} from './icons';
const MOOD_ICON = {
맑음: SunIcon,
흐림: CloudIcon,
비: RainIcon,
눈: SnowIcon,
} as const;
const OBSERVED = new Intl.DateTimeFormat('ko-KR', {
timeZone: 'Asia/Seoul',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
function observed(iso: string): string | undefined {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return undefined;
const parts = Object.fromEntries(OBSERVED.formatToParts(date).map((p) => [p.type, p.value]));
return `${parts.month} ${parts.day}일 ${parts.hour}:${parts.minute} 관측`;
}
export function Weather() {
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;
const condition = (weather?.condition ?? '') as WeatherSky;
const skyKey: WeatherSky = notes?.noteSets?.[condition] ? condition : mood;
const [line, bandLine] = useWeatherNotes(
notes?.noteSets?.[skyKey],
notes?.notes?.[mood] ?? weather?.note,
notes?.tempNoteSets?.[band],
notes?.tempNotes?.[band],
);
if (!weather) return null;
const Icon = MOOD_ICON[mood];
const region = payload.place.addressLocality?.replace(/(시|군)$/, '');
const at = observed(weather.observedAt);
return (
<section className="sec" id="weather" data-pg="home" aria-labelledby="weather-heading">
<p className="kicker">Today</p>
<h2 id="weather-heading">{region ? `지금 ${region}은` : '오늘의 날씨'}</h2>
<div className="weather" data-band={band}>
<Icon />
<div className="t">
<span className="deg">{Math.round(weather.temperature * 10) / 10}°</span>
<span className="cond">{weather.condition}</span>
</div>
{at && <p className="obs">{at}</p>}
{line && (
<p id="wnote" key={line} className="note">
{line}
</p>
)}
<p className="band">
<span>{band}</span>
{bandLine && <em key={bandLine}>{bandLine}</em>}
</p>
</div>
</section>
);
}