/s/stay 목업은 캐치프레이즈(tagline) 한 줄이 고정이고 그 아래 줄에서 문구가 돈다 (2026-09-11 "메인 모토는 고정, 그 아래 변동되는 모토"). 템플릿은 큰 줄에 공통 문구 10개를 돌리고 있었고, 앞 커밋의 소문구는 "명소까지 거리" 로 목업과 달랐다. - sections/HeroCatchphrase.tsx: HeroCatchphrase = tagline 고정(없으면 공통 문구 중 사이트별 하나), HeroRotator = narrative.catchphrases 를 일반→계절→일반→월→일반→날씨 순서로 6초마다, 줄 높이는 가장 긴 문구 - 9종 Hero: 소문구 자리를 HeroRotator 로. kit/heroSub.ts 삭제 - shared: catchphrases 항목에 season·month·weather - 테스트: 고정·폴백·비숙박·빈 순환·순서·날씨 분류 tsc·eslint 통과 · vitest 127 passed · 9종 모바일 캡처, 라운드·부티크 6초 간격 순환·높이 고정 확인 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
import {PlaceCategory} from '@o2o/shared';
|
|
import {HeroCatchphrase, HeroRotator} from '@site/sections/HeroCatchphrase';
|
|
import {useSite} from '@site/lib/site-context';
|
|
import {galleryImages, isSectionEnabled} from '@site/lib/derive';
|
|
import {useLiveWeather} from '@site/lib/use-live-weather';
|
|
import {Car} from '../paper/Car';
|
|
import {MoodIcon, Weather} from './Weather';
|
|
|
|
const MAX_SLIDES = 5;
|
|
|
|
export function Hero() {
|
|
const payload = useSite();
|
|
const {place, narrative} = payload;
|
|
const slides = [...payload.media]
|
|
.filter((image) => image.alt?.trim())
|
|
.sort((a, b) => Number(b.isPrimary) - Number(a.isPrimary))
|
|
.slice(0, MAX_SLIDES);
|
|
const fixed = narrative.tagline ?? narrative.heroSubline;
|
|
const lodging = place.category === PlaceCategory.LODGING;
|
|
const where = place.addressLocality ?? place.addressRegion;
|
|
const sub = [where, fixed].filter(Boolean).join(' · ');
|
|
const showWeather = isSectionEnabled(payload, 'weather');
|
|
const weather = useLiveWeather({
|
|
initial: payload.local.weather,
|
|
regionCode: payload.place.regionCode,
|
|
latitude: payload.place.latitude,
|
|
longitude: payload.place.longitude,
|
|
});
|
|
const backdrop = galleryImages(payload).find((image) => !image.isPrimary);
|
|
|
|
return (
|
|
<>
|
|
<section className="hero" id="top" data-pg="home">
|
|
{slides.length > 0 && (
|
|
<Car
|
|
label={`${place.name} 사진`}
|
|
autoplay={6000}
|
|
style={{height: '100%'}}
|
|
slides={slides.map((image, i) => (
|
|
<img
|
|
key={image.mediaId}
|
|
src={image.url}
|
|
alt={image.alt}
|
|
fetchPriority={i === 0 ? 'high' : 'low'}
|
|
loading={i === 0 ? undefined : 'lazy'}
|
|
decoding="async"
|
|
/>
|
|
))}
|
|
/>
|
|
)}
|
|
<div className="veil" />
|
|
<div className="hero-copy">
|
|
{lodging ? (
|
|
<>
|
|
<p className="big">
|
|
<HeroCatchphrase />
|
|
</p>
|
|
<h1 className="nm">{place.name}</h1>
|
|
{where && <p className="sub">{where}</p>}
|
|
<HeroRotator className="sub" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<h1 className="big">{place.name}</h1>
|
|
{sub && <p className="sub">{sub}</p>}
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="hero-meta">
|
|
{showWeather && weather ? (
|
|
<a className="wx-chip glass" href="#weather">
|
|
<MoodIcon condition={weather.condition} />
|
|
{Math.round(weather.temperature)}° {weather.condition}
|
|
</a>
|
|
) : (
|
|
<span />
|
|
)}
|
|
<span className="scroll" aria-hidden>
|
|
SCROLL
|
|
</span>
|
|
</div>
|
|
</section>
|
|
{showWeather && weather && (
|
|
<div data-editor-id="weather" style={{display: 'contents'}}>
|
|
<Weather weather={weather} backdrop={backdrop} />
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|