[fix] solution/site: 사진 카드를 PhotoCard 하나로 통일 — 템플릿마다 제각각이던 축제·명소 카드

같은 템플릿 안에서도 축제 카드(흰 바탕·글 안쪽)와 명소 카드(바탕 없음·글 아래)가 따로 놀았다.
실측(9종 전수): 제목 굵기 400/500/700/900, 사진 모서리 0/12/18/22px 가 한 화면에 섞여 있었다.

- kit/Card.tsx·card.css: 사진 3:2 + 제목 1줄 + 거리 1줄 + 설명 2줄. 템플릿 차이는 --kc-* 변수만
- 9종 Around·Festival, 고택 인물: 사진 카드를 PhotoCard 로 교체. 맛집·홈 목록 행은 그대로
- 템플릿 CSS: 모서리·글꼴·강조색 변수와 그리드 간격. 일러스트는 상자형, 빅타이포는 선 격자
- 모바일 캐러셀 화살표가 카드를 가려 1024px 미만에서 숨김
- docs/TEMPLATE_DESIGN.md: 사진 카드 규칙

tsc·eslint 통과 · vitest 123 passed · 12개 사이트 굽기 · 카드 모양 템플릿당 1종 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mina Choi 2026-09-30 15:30:30 +09:00
parent 089886fe1e
commit 4831e2d25d
27 changed files with 282 additions and 178 deletions

View File

@ -49,6 +49,14 @@
- 같은 목록의 카드는 같은 크기: 사진은 고정 비율 + `object-fit:cover`, 글은 줄 수 말줄임
- 칸 수는 항목 수에 맞춘다(`repeat(auto-fit, minmax(…))`). 3개인데 4칸을 잡아 빈칸을 남기지 않는다
### 사진 카드는 하나다 — `layouts/kit/Card.tsx` `PhotoCard`
- 주변 안내 · 축제 · 명소 · 인물(사진 있을 때)은 모두 `PhotoCard` 를 쓴다. 템플릿마다 카드를 새로 짓지 않는다
- 모양: 사진(3:2) 아래에 제목 1줄 · 거리나 날짜 1줄 · 설명 2줄. 템플릿별로 다른 것은 `--kc-*` 변수만
(`--kc-r` 사진 모서리, `--kc-tf`·`--kc-tw` 제목 글꼴·굵기, `--kc-mc` 거리 색, 상자형은 `--kc-bg`·`--kc-box-r`)
- 사진 없는 목록(맛집 · 홈 가까운 곳 목록)은 행(row)으로 쓴다. 한 섹션 안에서 카드와 행을 섞지 않는다
- 모바일 캐러셀 화살표는 숨긴다(카드 위를 가린다). 1024px 이상에서만 사진 가운데 높이에 둔다
## 4. 섹션 머리
- **제목은 위, 내용은 아래.** 참고한 펜션 사이트들이 모두 이 방식이다

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {Idx} from './SectionHead';
import {Tabs} from './Tabs';
@ -66,6 +67,17 @@ export function Around() {
</li>
);
const spot = (place: LocalPlace) => (
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
onClick={() => setOpen(place)}
/>
);
return (
<>
{rail.length > 0 && (
@ -86,16 +98,14 @@ export function Around() {
<section id="walk" aria-labelledby="walk-heading">
<Idx id="walk-heading" title="주변 명소" aside={`${spots.length} Spots`} />
<Tabs items={rangeTabs(spots)} value={spotRange} onChange={setSpotRange} />
<ul className="guide">{shownSpots.slice(0, firstSpots).map((place, i) => item(place, i))}</ul>
<div className="kgrid">{shownSpots.slice(0, firstSpots).map(spot)}</div>
{shownSpots.length > firstSpots && (
<details>
<summary className="line">
<span>{shownSpots.length - firstSpots}곳 더 보기</span>
<span>+</span>
</summary>
<ul className="guide">
{shownSpots.slice(firstSpots).map((place, i) => item(place, i + firstSpots))}
</ul>
<div className="kgrid">{shownSpots.slice(firstSpots).map(spot)}</div>
</details>
)}
</section>

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Car} from '../paper/Car';
import {Sheet} from '../paper/Sheet';
import {Idx} from './SectionHead';
@ -34,17 +35,12 @@ export function Festival() {
arrows
label="계절별 축제"
slides={shown.map((f) => (
<button type="button" className="fc" onClick={() => setOpen(f)}>
{f.imageUrl ? (
<span className="ph">
<img src={f.imageUrl} alt={f.name} loading="lazy" />
</span>
) : (
<span className="noimg">{f.name}</span>
)}
<span className="t">{f.name}</span>
<span className="d">{[f.month, f.location].filter(Boolean).join(' · ')}</span>
</button>
<PhotoCard
image={f.imageUrl}
title={f.name}
meta={[f.month, f.location].filter(Boolean).join(' · ')}
onClick={() => setOpen(f)}
/>
))}
/>
</div>

View File

@ -284,3 +284,7 @@
.w4-bigtype .ppl{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
.w4-bigtype .amen{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}}
.w4-bigtype .lead+:is(.chips,.tabrow){margin-top:20px}
.w4-bigtype{--kc-r:0;--kc-tw:500;--kc-ts:17px;--kc-mc:var(--sub);--kc-sub:var(--sub);--kc-px:12px;--kc-pb:14px}
.w4-bigtype .kgrid{gap:0;margin-top:0;border-top:1px solid var(--line);width:calc(100% + 1px);clip-path:inset(0 1px 0 0)}
.w4-bigtype .kgrid>.kcard{border-right:1px solid var(--line);border-bottom:1px solid var(--line)}
@media (min-width:1024px){.w4-bigtype .kgrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:0}}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
const WALK = 80;
@ -70,6 +71,17 @@ export function Around() {
</button>
);
const spot = (item: LocalPlace) => (
<PhotoCard
key={item.name}
image={item.imageUrl}
title={item.name}
meta={item.distanceText}
desc={item.description}
onClick={() => setOpen(item)}
/>
);
return (
<>
{rail.length > 0 && (
@ -78,7 +90,7 @@ export function Around() {
<p className="label">AROUND</p>
<h2 id="around-heading">주변 안내</h2>
<p className="lead">걸어서 닿는 명소와 맛집입니다.</p>
<div className="guide">{rail.slice(0, HOME_ROWS).map((item) => place(item))}</div>
<div className="guide guide--card">{rail.slice(0, HOME_ROWS).map(spot)}</div>
<a className="link b-more--c" href="#/area">
지역 소개에서 모두 보기
</a>
@ -95,11 +107,11 @@ export function Around() {
</h2>
<p className="lead">걸어서 닿는 곳부터 차로 가는 곳까지 묶었습니다.</p>
<RangeTabs places={spots} value={spotRange} onChange={setSpotRange} />
<div className="guide">{shownSpots.slice(0, firstSpots).map((item) => place(item))}</div>
<div className="guide guide--card">{shownSpots.slice(0, firstSpots).map(spot)}</div>
{shownSpots.length > firstSpots && (
<details className="fold" key={spotRange}>
<summary className="link">{shownSpots.length - firstSpots}곳 더 보기</summary>
<div className="guide">{shownSpots.slice(firstSpots).map((item) => place(item))}</div>
<div className="guide guide--card">{shownSpots.slice(firstSpots).map(spot)}</div>
</details>
)}
</section>

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
const SEASONS = ['봄', '여름', '가을', '겨울'];
@ -40,15 +41,13 @@ export function Festival() {
)}
<div className="rail">
{shown.map((f) => (
<button key={f.name} type="button" className="card" onClick={() => setOpen(f)}>
{f.imageUrl ? (
<img src={f.imageUrl} alt={f.name} loading="lazy" />
) : (
<span className="noimg">{f.name}</span>
)}
<b className="nm">{f.name}</b>
<span className="ds">{[f.month, f.location].filter(Boolean).join(' · ')}</span>
</button>
<PhotoCard
key={f.name}
image={f.imageUrl}
title={f.name}
meta={[f.month, f.location].filter(Boolean).join(' · ')}
onClick={() => setOpen(f)}
/>
))}
</div>
</section>

View File

@ -345,3 +345,9 @@
.w4-boutique[data-page=story] main .b-rule{margin-left:0;margin-right:0}
.w4-boutique[data-page=story] main .shell{padding-inline:24px}
.w4-boutique[data-page=story] main #reviews{margin-top:var(--sec)}}
.w4-boutique{--kc-r:999px 999px 0 0;--kc-ar:4/3;--kc-tf:var(--mj);--kc-tw:400;--kc-ts:17px;--kc-mc:var(--rose);--kc-sub:var(--sub);--kc-soft:var(--bg2)}
.w4-boutique .rail>.kcard{flex:0 0 62%;height:auto}
.w4-boutique .guide--card{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:28px 14px}
.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}}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {useWide} from './useWide';
@ -71,6 +72,17 @@ export function Around() {
</button>
);
const spot = (item: LocalPlace) => (
<PhotoCard
key={item.name}
image={item.imageUrl}
title={item.name}
meta={item.distanceText}
desc={item.description}
onClick={() => setOpen(item)}
/>
);
return (
<>
{rail.length > 0 && (
@ -81,11 +93,13 @@ export function Around() {
<p className="lead">걸어서 닿는 명소와 맛집입니다.</p>
<div className="grid3">
{rail.slice(0, HOME_ROWS).map((item) => (
<button key={item.name} type="button" className="card" onClick={() => setOpen(item)}>
<img src={item.imageUrl} alt={item.name} loading="lazy" />
<b className="nm clamp">{item.name}</b>
{item.distanceText && <span className="ds">{item.distanceText}</span>}
</button>
<PhotoCard
key={item.name}
image={item.imageUrl}
title={item.name}
meta={item.distanceText}
onClick={() => setOpen(item)}
/>
))}
</div>
<a className="link more" href="#/area">
@ -104,11 +118,11 @@ export function Around() {
</h2>
<p className="lead">걸어서 닿는 곳부터 차로 가는 곳까지 묶었습니다.</p>
<RangeTabs places={spots} value={spotRange} onChange={setSpotRange} />
<div className="guide">{shownSpots.slice(0, firstSpots).map((item) => place(item))}</div>
<div className="guide guide--card">{shownSpots.slice(0, firstSpots).map(spot)}</div>
{shownSpots.length > firstSpots && (
<details className="fold">
<summary className="link">{shownSpots.length - firstSpots}곳 더 보기</summary>
<div className="guide">{shownSpots.slice(firstSpots).map((item) => place(item))}</div>
<div className="guide guide--card">{shownSpots.slice(firstSpots).map(spot)}</div>
</details>
)}
</section>

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
const SEASONS = ['봄', '여름', '가을', '겨울'];
@ -40,15 +41,13 @@ export function Festival() {
)}
<div className="rail">
{shown.map((f) => (
<button key={f.name} type="button" className="card" onClick={() => setOpen(f)}>
{f.imageUrl ? (
<img src={f.imageUrl} alt={f.name} loading="lazy" />
) : (
<span className="noimg">{f.name}</span>
)}
<b className="nm">{f.name}</b>
<span className="ds">{[f.month, f.location].filter(Boolean).join(' · ')}</span>
</button>
<PhotoCard
key={f.name}
image={f.imageUrl}
title={f.name}
meta={[f.month, f.location].filter(Boolean).join(' · ')}
onClick={() => setOpen(f)}
/>
))}
</div>
</section>

View File

@ -296,3 +296,9 @@
.w4-cinema .unit .btns{grid-template-columns:1fr 1fr}}
.w4-cinema .wrap>.label:has(+h2),.w4-cinema .book .label,.w4-cinema .rhead>.kicker{display:none}
.w4-cinema :is(h1,h2,.big,.hl){font-family:var(--tpl-font-heading)}
.w4-cinema{--kc-r:14px;--kc-tw:600;--kc-mc:var(--accent);--kc-sub:var(--sub);--kc-soft:rgba(244,239,230,.06)}
.w4-cinema .rail>.kcard{flex:0 0 72%;height:auto}
.w4-cinema .guide--card{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px 14px;border-top:0}
.w4-cinema .grid3:has(>.kcard){gap:20px 10px}
.w4-cinema .grid3>.kcard .kcard-t{font-size:15px}
@media (min-width:1024px){.w4-cinema .rail>.kcard{flex:0 0 calc((100% - 30px) / 4)}.w4-cinema .guide--card,.w4-cinema #walk .guide{grid-template-columns:repeat(4,minmax(0,1fr));gap:32px 16px}.w4-cinema #around .grid3:has(>.kcard){grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {useWide} from './useWide';
@ -66,18 +67,14 @@ export function Around() {
);
const card = (place: LocalPlace) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
{place.imageUrl && (
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
)}
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
{place.description && <p className="clamp">{place.description}</p>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
onClick={() => setOpen(place)}
/>
);
return (
@ -94,15 +91,13 @@ export function Around() {
</div>
<div className="guide guide--3">
{rail.slice(0, railMax).map((place) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
onClick={() => setOpen(place)}
/>
))}
</div>
<a href="#/area" className="more">

View File

@ -447,3 +447,5 @@
.w4-coral #about .amen li.off .ic{color:#c2410c}
.w4-coral #about .amen li.off{color:var(--sub)}
.w4-coral :is(h1,h2,h3){font-family:var(--tpl-font-heading)}
.w4-coral{--kc-r:0;--kc-tw:500;--kc-mc:var(--accent);--kc-sub:var(--sub);--kc-soft:var(--alt)}
.w4-coral .cards2,.w4-coral .guide--3{row-gap:24px}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {SpotIcon} from './Art';
import {useWide} from './useWide';
@ -70,6 +71,18 @@ export function Around() {
</button>
);
const spot = (place: LocalPlace) => (
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
fallback={<SpotIcon food={false} />}
onClick={() => setOpen(place)}
/>
);
return (
<>
<div data-pg="home">
@ -103,11 +116,11 @@ export function Around() {
const shown = spots.filter((p) => inRange(spotRange)(meters(p)));
return (
<>
<div className="guide guide--card">{shown.slice(0, firstSpots).map((p, i) => row(p, i))}</div>
<div className="guide guide--card">{shown.slice(0, firstSpots).map(spot)}</div>
{shown.length > firstSpots && (
<details>
<summary className="more">{shown.length - firstSpots}곳 더 보기</summary>
<div className="guide guide--card">{shown.slice(firstSpots).map((p, i) => row(p, i + firstSpots))}</div>
<div className="guide guide--card">{shown.slice(firstSpots).map(spot)}</div>
</details>
)}
</>

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
const SEASONS = ['봄', '여름', '가을', '겨울'];
@ -40,15 +41,13 @@ export function Festival() {
)}
<div className="rail">
{shown.map((f) => (
<button key={f.name} type="button" className="card" onClick={() => setOpen(f)}>
{f.imageUrl ? (
<img src={f.imageUrl} alt={f.name} loading="lazy" />
) : (
<span className="noimg">{f.name}</span>
)}
<b className="nm">{f.name}</b>
<span className="ds">{[f.month, f.location].filter(Boolean).join(' · ')}</span>
</button>
<PhotoCard
key={f.name}
image={f.imageUrl}
title={f.name}
meta={[f.month, f.location].filter(Boolean).join(' · ')}
onClick={() => setOpen(f)}
/>
))}
</div>
</section>

View File

@ -334,3 +334,7 @@
@media(max-width:1023px){.w4-graphic #location .btns{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.w4-graphic #location .btns>:first-child{grid-column:1/-1}
.w4-graphic #location .btns>*{white-space:nowrap;min-width:0}}
.w4-graphic{--kc-r:0;--kc-box-r:22px;--kc-bg:var(--card);--kc-shadow:0 2px 0 rgba(29,43,36,.08);--kc-px:12px;--kc-pb:14px;--kc-tf:var(--k-head);--kc-tw:400;--kc-ts:17px;--kc-mc:var(--accent);--kc-sub:var(--sub);--kc-soft:#fff1c9}
.w4-graphic .rail>.kcard{flex:0 0 72%;height:auto}
.w4-graphic .guide--card{grid-template-columns:repeat(2,minmax(0,1fr));gap:24px 14px}
@media (min-width:1024px){.w4-graphic .rail>.kcard{flex:0 0 calc((100% - 48px) / 4)}.w4-graphic .guide--card{grid-template-columns:repeat(4,minmax(0,1fr));gap:32px 16px}}

View File

@ -0,0 +1,31 @@
import type {ReactNode} from 'react';
import './card.css';
export function PhotoCard({
image,
title,
meta,
desc,
fallback,
ratio,
onClick,
}: {
image?: string | null;
title: string;
meta?: string | null;
desc?: string | null;
fallback?: ReactNode;
ratio?: string;
onClick: () => void;
}) {
return (
<button type="button" className="kcard" onClick={onClick}>
<span className="kcard-ph" style={ratio ? {aspectRatio: ratio} : undefined}>
{image ? <img src={image} alt={title} loading="lazy" /> : <span className="kcard-no">{fallback ?? title}</span>}
</span>
<span className="kcard-t">{title}</span>
{meta && <span className="kcard-m">{meta}</span>}
{desc && <span className="kcard-d">{desc}</span>}
</button>
);
}

View File

@ -0,0 +1,13 @@
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard{display:flex;flex-direction:column;width:100%;height:100%;min-width:0;margin:0;padding:0 0 var(--kc-pb,0);border:0;border-radius:var(--kc-box-r,0);overflow:hidden;background:var(--kc-bg,none);box-shadow:var(--kc-shadow,none);color:inherit;font:inherit;text-align:left;cursor:pointer;-webkit-appearance:none;appearance:none}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-ph{position:relative;display:block;width:100%;aspect-ratio:var(--kc-ar,3/2);border-radius:var(--kc-r,14px);overflow:hidden;background:var(--kc-soft,rgba(127,127,127,.1))}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-ph img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .5s ease}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard:hover .kcard-ph img{transform:scale(1.03)}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-no{display:grid;place-items:center;width:100%;height:100%;padding:0 16px;font-family:var(--kc-tf,inherit);font-size:17px;line-height:1.4;text-align:center;color:var(--kc-sub,inherit)}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-no svg{width:40px;height:40px}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-t{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;margin-top:12px;padding:0 var(--kc-px,2px);font-family:var(--kc-tf,inherit);font-size:var(--kc-ts,16px);font-weight:var(--kc-tw,600);line-height:1.45;letter-spacing:-.01em}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-m{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;margin-top:4px;padding:0 var(--kc-px,2px);font-size:14px;font-weight:500;line-height:1.5;color:var(--kc-mc,inherit)}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kcard-d{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:6px;padding:0 var(--kc-px,2px);font-size:15px;line-height:1.6;color:var(--kc-sub,inherit)}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px 14px}
@media (min-width:1024px){:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .kgrid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:32px 20px}}
:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .car--multi:has(.kcard) .arrow{top:calc(50% - 30px)}
@media (max-width:1023px){:is(.w4p,.w4-round,.w4-cinema,.w4-bigtype,.w4-boutique,.w4-graphic,.w4-coral,.w4-minimal,.w4-pine) .car--multi:has(.kcard) .arrow{display:none}}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {useWide} from './useWide';
@ -66,18 +67,14 @@ export function Around() {
);
const card = (place: LocalPlace) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
{place.imageUrl && (
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
)}
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
{place.description && <p className="clamp">{place.description}</p>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
onClick={() => setOpen(place)}
/>
);
return (
@ -94,15 +91,13 @@ export function Around() {
</div>
<div className="guide guide--3">
{rail.slice(0, railMax).map((place) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
onClick={() => setOpen(place)}
/>
))}
</div>
<a href="#/area" className="more">

View File

@ -450,3 +450,5 @@
.w4-minimal #about .amen li.off .ic{color:#c2410c}
.w4-minimal #about .amen li.off{color:var(--sub)}
.w4-minimal :is(h1,h2,.hl){font-family:var(--tpl-font-heading)}
.w4-minimal{--kc-r:0;--kc-tw:400;--kc-mc:var(--sub);--kc-sub:var(--sub);--kc-soft:var(--alt)}
.w4-minimal .cards2,.w4-minimal .guide--3{row-gap:28px}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Car} from './Car';
import {Sheet} from './Sheet';
@ -80,11 +81,7 @@ export function Around() {
arrows
label="주변 안내"
slides={rail.map((place) => (
<button type="button" className="itemcard" onClick={() => setOpen(place)}>
<img src={place.imageUrl} alt={place.name} loading="lazy" style={{aspectRatio: '3 / 2'}} />
<p className="cap">{place.name}</p>
{place.distanceText && <p className="cap dim">{place.distanceText}</p>}
</button>
<PhotoCard image={place.imageUrl} title={place.name} meta={place.distanceText} onClick={() => setOpen(place)} />
))}
/>
</section>
@ -104,17 +101,16 @@ export function Around() {
걸어서 닿는 곳부터 차로 가는 곳까지 묶었습니다.
</p>
<RangeTabs places={spots} value={spotRange} onChange={setSpotRange} />
<div className="grid2" style={{gap: 14}}>
<div className="grid2" style={{gap: '24px 14px'}}>
{shownSpots.map((place) => (
<button key={place.name} type="button" className="itemcard" onClick={() => setOpen(place)}>
{place.imageUrl && (
<img src={place.imageUrl} alt={place.name} loading="lazy" style={{aspectRatio: '3 / 2'}} />
)}
<p className="cap">
<b>{place.name}</b> {place.distanceText && <span className="dim">{place.distanceText}</span>}
</p>
{place.description && <p className="cap dim clamp">{place.description}</p>}
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
onClick={() => setOpen(place)}
/>
))}
</div>
</section>

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Car} from './Car';
import {Sheet} from './Sheet';
@ -48,19 +49,12 @@ export function Festival() {
arrows
label="계절별 축제"
slides={shown.map((f) => (
<button type="button" className="itemcard" onClick={() => setOpen(f)}>
{f.imageUrl ? (
<img src={f.imageUrl} alt={f.name} loading="lazy" style={{aspectRatio: '3 / 2'}} />
) : (
<span className="noimg" style={{aspectRatio: '3 / 2'}}>
{f.name}
</span>
)}
<p className="cap">
<b>{f.name}</b>
</p>
<p className="cap dim">{[f.month, f.location].filter(Boolean).join(' · ')}</p>
</button>
<PhotoCard
image={f.imageUrl}
title={f.name}
meta={[f.month, f.location].filter(Boolean).join(' · ')}
onClick={() => setOpen(f)}
/>
))}
/>
</section>

View File

@ -2,6 +2,7 @@ import {useState, type ReactNode} from 'react';
import type {ChronicleItem, PeopleItem, ReadingItem, SongItem} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {sectionItems, sectionName} from '@site/lib/derive';
import {PhotoCard} from '../kit/Card';
import {Sheet} from './Sheet';
const COUNT_WORD: Record<number, string> = {1: '한', 2: '두', 3: '세', 4: '네'};
@ -60,27 +61,35 @@ export function Story() {
{item.year && <span className="r">{item.year}</span>}
</button>
);
const person = (item: PeopleItem) => (
<button
key={item.name}
type="button"
className="itemcard"
onClick={() =>
setOpen({
title: item.name,
meta: [item.role, item.years].filter(Boolean).join(' · '),
body: item.oneLine,
image: item.imageUrl,
})
}
>
{item.imageUrl && <img src={item.imageUrl} alt={item.name} loading="lazy" style={{aspectRatio: '1 / 1'}} />}
<p className="cap">
<b>{item.name}</b> {item.role && <span className="dim">{item.role}</span>}
</p>
{item.oneLine && <p className="cap dim clamp">{item.oneLine}</p>}
</button>
);
const person = (item: PeopleItem) => {
const show = () =>
setOpen({
title: item.name,
meta: [item.role, item.years].filter(Boolean).join(' · '),
body: item.oneLine,
image: item.imageUrl,
});
if (item.imageUrl)
return (
<PhotoCard
key={item.name}
image={item.imageUrl}
title={item.name}
meta={item.role}
desc={item.oneLine}
ratio="1 / 1"
onClick={show}
/>
);
return (
<button key={item.name} type="button" className="itemcard" onClick={show}>
<p className="cap">
<b>{item.name}</b> {item.role && <span className="dim">{item.role}</span>}
</p>
{item.oneLine && <p className="cap dim clamp">{item.oneLine}</p>}
</button>
);
};
const event = (item: ChronicleItem) => (
<button
key={`${item.year}-${item.title}`}

View File

@ -338,3 +338,4 @@
.w4p #w4d-panel .w4d-eq i{background:#8c4a2f}
.w4p #w4d-lyrics-box{padding:14px 16px 18px;border-top:1px solid var(--line);font-family:'Noto Serif KR','AppleMyungjo',serif;font-size:15px;line-height:1.95;color:#39352e}
.w4p #w4d-panel .w4d-info{line-height:1.45}
.w4p{--kc-r:4px;--kc-tw:500;--kc-ts:16.5px;--kc-mc:var(--sub);--kc-sub:var(--sub);--kc-soft:var(--tint)}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {useWide} from './useWide';
@ -66,18 +67,14 @@ export function Around() {
);
const card = (place: LocalPlace) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
{place.imageUrl && (
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
)}
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
{place.description && <p className="clamp">{place.description}</p>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
onClick={() => setOpen(place)}
/>
);
return (

View File

@ -450,3 +450,5 @@
.w4-pine #location .loc .map{aspect-ratio:auto;height:100%;min-height:320px}
.w4-pine #location .loc .bd{align-self:center;padding:28px}
.w4-pine #festival .car--multi .slide{flex:0 0 260px}}
.w4-pine{--kc-r:14px;--kc-tw:600;--kc-mc:var(--pine);--kc-sub:var(--sub);--kc-soft:var(--alt)}
.w4-pine .cards2{row-gap:24px}

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import type {LocalPlace} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverMapSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
import {useWide} from './useWide';
@ -66,18 +67,14 @@ export function Around() {
);
const card = (place: LocalPlace) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
{place.imageUrl && (
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
)}
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
{place.description && <p className="clamp">{place.description}</p>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
desc={place.description}
onClick={() => setOpen(place)}
/>
);
return (
@ -94,15 +91,13 @@ export function Around() {
</div>
<div className="guide guide--3">
{rail.slice(0, railMax).map((place) => (
<button key={place.name} type="button" className="place" onClick={() => setOpen(place)}>
<div className="ph">
<img src={place.imageUrl} alt={place.name} loading="lazy" />
</div>
<div className="bd">
<h3>{place.name}</h3>
{place.distanceText && <small>{place.distanceText}</small>}
</div>
</button>
<PhotoCard
key={place.name}
image={place.imageUrl}
title={place.name}
meta={place.distanceText}
onClick={() => setOpen(place)}
/>
))}
</div>
<a href="#/area" className="more">

View File

@ -337,3 +337,5 @@
.w4-round #about .amen li.on .ic{color:#15803d}
.w4-round #about .amen li.off .ic{color:#c2410c}
.w4-round #about .amen li.off{color:var(--sub)}
.w4-round{--kc-r:var(--r-card);--kc-tw:700;--kc-mc:var(--accent);--kc-sub:var(--sub);--kc-soft:var(--alt)}
.w4-round .cards2,.w4-round .guide--3{row-gap:24px}