[fix] solution/site: 첫 화면 태그 삭제·소문구 공통화 — 부티크 모바일도 문 모양

- 라운드·코랄·미니멀·솔숲: 히어로의 ‘주차 가능’ 칩 삭제. 일러스트: 지역 말풍선 삭제
- kit/heroSub.ts: 소문구 = 사장님 문구, 없으면 가장 가까운 명소까지 거리. 솔숲에만 있던 규칙을 9종 공통으로
- 고택·부티크: 캐치프레이즈 아래로 소문구 순서 변경
- 부티크 모바일: 전체화면 사진(664px)을 데스크톱과 같은 아치로 — 첫 화면 593px
- docs/TEMPLATE_DESIGN.md: 첫 화면 규칙

tsc·eslint 통과 · vitest 123 passed · 9종 모바일·1440px 첫 화면 캡처 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mina Choi 2026-09-30 15:47:57 +09:00
parent 865eb0cc77
commit 7a7f00dd15
12 changed files with 56 additions and 60 deletions

View File

@ -85,6 +85,10 @@
## 6. 값 보여주기
- **첫 화면은 캐치프레이즈 → 그 아래 소문구.** 소문구는 사장님 문구, 없으면 가장 가까운 명소까지 거리(`kit/heroSub.ts`)
- 첫 화면에 태그·칩(‘주차 가능’, 지역 말풍선)을 두지 않는다
- 부티크 첫 화면은 모바일도 문(아치) 모양이다. 전체화면 사진으로 되돌리지 않는다
- **‘가능 · 있음’은 ✓, ‘불가 · 없음’은 ✕ 목록으로 쓴다.** `주차 / 가능` 같은 표로 쓰지 않는다
- 마크업: `<ul class="amen"><li class="on"><i class="ic">✓</i><span><b>주차</b> 가능</span></li>`
- 색: ✓ `#15803d`, ✕ `#c2410c`. 가능한 것을 먼저 놓는다

View File

@ -2,6 +2,7 @@ import {PlaceCategory} from '@o2o/shared';
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {BOOK_EVENT} from '../paper/Rooms';
import {Weather} from './Weather';
@ -12,6 +13,7 @@ export function Hero() {
.filter((image) => image.alt?.trim())
.sort((a, b) => Number(b.isPrimary) - Number(a.isPrimary));
const fixed = narrative.tagline ?? narrative.heroSubline;
const sub = heroSub(payload);
const lodging = place.category === PlaceCategory.LODGING;
const stay = lodging ? stayBookingView(payload) : null;
const link = bookingLinks(payload)[0];
@ -29,7 +31,7 @@ export function Hero() {
<span className="no" /> 오늘의 문장
</p>
{(lodging || fixed) && <p className="rot">{lodging ? <HeroCatchphrase /> : fixed}</p>}
{lodging && fixed && <p className="m sub">{fixed}</p>}
{lodging && sub && <p className="m sub">{sub}</p>}
<div className="hx">
{stay ? (
<button type="button" className="line line--ink" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>

View File

@ -3,6 +3,7 @@ import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {PlaceCategory} from '@o2o/shared';
import {heroSub} from '../kit/heroSub';
import {BOOK_EVENT} from '../paper/Rooms';
import {pad2} from './ui';
import {Weather} from './Weather';
@ -26,7 +27,7 @@ export function Hero() {
}, 5000);
return () => window.clearInterval(timer);
}, [total]);
const fixed = narrative.tagline ?? narrative.heroSubline;
const sub = heroSub(payload);
const english = place.englishName?.trim();
const locality = [place.addressLocality, place.addressRegion].find(Boolean);
const stayBooking = place.category === PlaceCategory.LODGING ? stayBookingView(payload) : null;
@ -57,10 +58,10 @@ export function Hero() {
{english && <p className="b-hero-en">{english}</p>}
<h1 className={english ? 'b-hero-name' : 'b-hero-name b-hero-name--solo'}>{place.name}</h1>
{locality && <p className="b-hero-loc">{locality}</p>}
{fixed && <p className="b-hero-ko">{fixed}</p>}
<p className="b-hero-cp">
<HeroCatchphrase />
</p>
{sub && <p className="b-hero-ko">{sub}</p>}
<div className="b-hero-act">
{stayBooking ? (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>

View File

@ -351,3 +351,16 @@
.w4-boutique #around .guide--card{grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 10px}
.w4-boutique #around .kcard-t{font-size:15px}
@media (min-width:1024px){.w4-boutique .rail>.kcard{flex-basis:calc((100% - 48px) / 3)}.w4-boutique .guide--card{grid-template-columns:repeat(3,minmax(0,1fr));gap:36px 24px}}
@media (max-width:1023px){
.w4-boutique{--aw:min(250px,calc(100vw - 88px));--ah:calc(var(--aw) * 1.12)}
.w4-boutique[data-page=home] .b-hd:not(.solid){color:var(--brown)}
.w4-boutique .b-hero{height:auto;min-height:0;padding:calc(84px + var(--ah) + 24px) var(--g) 8px;background:transparent;overflow:visible}
.w4-boutique .b-hero:before{content:"";position:absolute;top:100px;left:50%;width:var(--aw);height:var(--ah);margin-left:calc(var(--aw) / -2 + 14px);border-radius:999px 999px 0 0;background:var(--bg2)}
.w4-boutique .b-hero:after{display:none}
.w4-boutique .b-slide{inset:84px auto auto 50%;width:var(--aw);height:var(--ah);margin-left:calc(var(--aw) / -2);border-radius:999px 999px 0 0;overflow:hidden;z-index:1}
.w4-boutique .b-hero-copy{position:relative;left:auto;right:auto;bottom:auto;padding:0;color:var(--brown)}
.w4-boutique .b-hero-ko{color:var(--ink)}
.w4-boutique .b-hero-count{top:calc(84px + var(--ah) - 34px);bottom:auto}
.w4-boutique .b-hero--plain{padding-top:120px}
.w4-boutique .b-hero--plain:before{display:none}
}

View File

@ -3,6 +3,7 @@ import {HeroCatchphrase} 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 {heroSub} from '../kit/heroSub';
import {Car} from '../paper/Car';
import {MoodIcon, Weather} from './Weather';
@ -18,7 +19,7 @@ export function Hero() {
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 sub = [where, heroSub(payload)].filter(Boolean).join(' · ');
const showWeather = isSectionEnabled(payload, 'weather');
const weather = useLiveWeather({
initial: payload.local.weather,

View File

@ -1,7 +1,8 @@
import {PlaceCategory} from '@o2o/shared';
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, heroFacts, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {Car} from '../paper/Car';
import {BOOK_EVENT} from '../paper/Rooms';
import {Weather} from './Weather';
@ -17,10 +18,10 @@ export function Hero() {
.slice(0, MAX_SLIDES);
const fixed = narrative.tagline ?? narrative.heroSubline;
const lodging = place.category === PlaceCategory.LODGING;
const sub = heroSub(payload);
const region = place.addressLocality ?? place.addressRegion;
const booking = bookingLinks(payload)[0];
const stay = lodging ? stayBookingView(payload) : null;
const chips = heroFacts(payload).map((row) => `${row.label} ${row.value}`);
return (
<>
@ -56,16 +57,7 @@ export function Hero() {
) : (
fixed && <p className="hl in">{fixed}</p>
)}
{lodging && fixed && <p className="tagline">{fixed}</p>}
{chips.length > 0 && (
<div className="chips">
{chips.map((chip) => (
<span key={chip} className="chip">
{chip}
</span>
))}
</div>
)}
{lodging && sub && <p className="tagline">{sub}</p>}
<div className="hero-cta">
{stay ? (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>

View File

@ -4,6 +4,7 @@ import {useSite} from '@site/lib/site-context';
import {useLiveWeather} from '@site/lib/use-live-weather';
import {useWeatherNotes} from '@site/lib/use-weather-note';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView, weatherBand, weatherMood} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {BOOK_EVENT} from '../paper/Rooms';
import lines from '@site/lib/lodging-catchphrases.json';
import {Hills, Sky} from './Art';
@ -103,7 +104,7 @@ export function Hero() {
const {place, narrative} = payload;
const phrase = useCatchphrase();
const region = [place.addressRegion, place.addressLocality].filter(Boolean).join(' ');
const fixed = narrative.tagline ?? narrative.heroSubline;
const sub = heroSub(payload);
const stay = place.category === PlaceCategory.LODGING ? stayBookingView(payload) : null;
const link = bookingLinks(payload)[0];
@ -111,7 +112,6 @@ export function Hero() {
<section className="scene" id="top" data-pg="home" aria-labelledby="hero-heading">
<Sky />
<div className="hero-copy">
{region && <span className="tag">{region}</span>}
<h1 id="hero-heading" className="disp">
<Pop text={place.name} />
</h1>
@ -120,7 +120,7 @@ export function Hero() {
<Pop text={phrase} />
</p>
)}
{fixed && <p className="sub">{fixed}</p>}
{sub && <p className="sub">{sub}</p>}
{(stay || link || place.phone) && (
<div className="hgo">
{stay ? (

View File

@ -0,0 +1,10 @@
import type {SitePayload} from '@o2o/shared';
export function heroSub(payload: SitePayload): string | undefined {
const fixed = payload.narrative.tagline ?? payload.narrative.heroSubline;
if (fixed) return fixed;
const nearest = [...payload.local.attractions]
.filter((spot) => spot.distanceText)
.sort((a, b) => (a.distanceMeters ?? Infinity) - (b.distanceMeters ?? Infinity))[0];
return nearest ? `${nearest.name}까지 ${nearest.distanceText}` : undefined;
}

View File

@ -1,7 +1,8 @@
import {PlaceCategory} from '@o2o/shared';
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, heroFacts, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {Car} from '../paper/Car';
import {BOOK_EVENT} from '../paper/Rooms';
import {Weather} from './Weather';
@ -17,10 +18,10 @@ export function Hero() {
.slice(0, MAX_SLIDES);
const fixed = narrative.tagline ?? narrative.heroSubline;
const lodging = place.category === PlaceCategory.LODGING;
const sub = heroSub(payload);
const region = place.addressLocality ?? place.addressRegion;
const booking = bookingLinks(payload)[0];
const stay = lodging ? stayBookingView(payload) : null;
const chips = heroFacts(payload).map((row) => `${row.label} ${row.value}`);
return (
<>
@ -56,16 +57,7 @@ export function Hero() {
) : (
fixed && <p className="hl in">{fixed}</p>
)}
{lodging && fixed && <p className="tagline">{fixed}</p>}
{chips.length > 0 && (
<div className="chips">
{chips.map((chip) => (
<span key={chip} className="chip">
{chip}
</span>
))}
</div>
)}
{lodging && sub && <p className="tagline">{sub}</p>}
<div className="hero-cta">
{stay ? (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>

View File

@ -1,6 +1,7 @@
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {isSectionEnabled} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {Weather} from './Weather';
import {Car} from './Car';
@ -13,7 +14,7 @@ export function Hero() {
.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 sub = heroSub(payload);
return (
<>
@ -41,10 +42,10 @@ export function Hero() {
<p className="eyebrow">{place.addressLocality ?? place.addressRegion}</p>
)}
<h1 className="mj">{place.name}</h1>
{fixed && <p className="tagline">{fixed}</p>}
<p className="cp">
<HeroCatchphrase />
</p>
{sub && <p className="tagline">{sub}</p>}
</div>
</section>
{isSectionEnabled(payload, 'weather') && (

View File

@ -1,7 +1,8 @@
import {PlaceCategory} from '@o2o/shared';
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, heroFacts, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {Car} from '../paper/Car';
import {BOOK_EVENT} from '../paper/Rooms';
import {Weather} from './Weather';
@ -18,13 +19,9 @@ export function Hero() {
const fixed = narrative.tagline ?? narrative.heroSubline;
const lodging = place.category === PlaceCategory.LODGING;
const region = place.addressLocality ?? place.addressRegion;
const nearest = [...payload.local.attractions]
.filter((spot) => spot.distanceText)
.sort((a, b) => (a.distanceMeters ?? Infinity) - (b.distanceMeters ?? Infinity))[0];
const sub = fixed ?? (nearest ? `${nearest.name}까지 ${nearest.distanceText}` : undefined);
const sub = heroSub(payload);
const booking = bookingLinks(payload)[0];
const stay = lodging ? stayBookingView(payload) : null;
const chips = heroFacts(payload).map((row) => `${row.label} ${row.value}`);
return (
<>
@ -61,15 +58,6 @@ export function Hero() {
fixed && <p className="hl in">{fixed}</p>
)}
{lodging && sub && <p className="tagline">{sub}</p>}
{chips.length > 0 && (
<div className="chips">
{chips.map((chip) => (
<span key={chip} className="chip">
{chip}
</span>
))}
</div>
)}
<div className="hero-cta">
{stay ? (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>

View File

@ -1,7 +1,8 @@
import {PlaceCategory} from '@o2o/shared';
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, heroFacts, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {heroSub} from '../kit/heroSub';
import {Car} from '../paper/Car';
import {BOOK_EVENT} from '../paper/Rooms';
import {Weather} from './Weather';
@ -17,10 +18,10 @@ export function Hero() {
.slice(0, MAX_SLIDES);
const fixed = narrative.tagline ?? narrative.heroSubline;
const lodging = place.category === PlaceCategory.LODGING;
const sub = heroSub(payload);
const region = place.addressLocality ?? place.addressRegion;
const booking = bookingLinks(payload)[0];
const stay = lodging ? stayBookingView(payload) : null;
const chips = heroFacts(payload).map((row) => `${row.label} ${row.value}`);
return (
<>
@ -56,16 +57,7 @@ export function Hero() {
) : (
fixed && <p className="hl in">{fixed}</p>
)}
{lodging && fixed && <p className="tagline">{fixed}</p>}
{chips.length > 0 && (
<div className="chips">
{chips.map((chip) => (
<span key={chip} className="chip">
{chip}
</span>
))}
</div>
)}
{lodging && sub && <p className="tagline">{sub}</p>}
<div className="hero-cta">
{stay ? (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>