사장님이 고를 수 있는 숙박 템플릿이 고택 하나뿐이었다. 모두 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건
79 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|