o2o-site-AEO/solution/frontend/src/features/builder/canvas/primitives/SectionHeading.tsx
Mina Choi 4debeaed3a [feat] solution/frontend: 템플릿이 색만 바꾸던 걸 끝낸다 — 업종당 5개에서 3개로
업종마다 템플릿이 다섯인데 넷이 "흰 바탕 + 고딕 + 둥근 모서리"에 색조만 달랐다. 고르는 화면의
미리보기도 회색 막대 세 줄 + 색 동그라미라 다섯 장이 전부 같은 그림이었다 — 뭐가 다른지 알 수
없으니 아무거나 골랐다.

- shared/TemplateItem.look: 제목·본문 서체, 모서리, 테두리 두께, 그림자, 제목 자간·굵기, 섹션 여백.
  CSS 에 그대로 들어가는 문자열로 들고 있다 — 숫자로 두면 쓰는 쪽에서 단위를 빠뜨린 곳이 조용히 0 이 된다
- CanvasView: Tailwind v4 의 --radius-* · --shadow-* 를 캔버스 안에서만 덮는다. 변이 파일 40여 개에
  흩어진 rounded-* · shadow-* 를 한 줄도 안 고치고 전부 템플릿을 따르게 된다. 배수는 Tailwind
  기본 비율 그대로라 기준값 0.75rem 이면 지금까지와 픽셀 단위로 같다
- index.css: .site-canvas 는 --tpl-font-heading/body 를 이미 읽고 있었는데 아무도 넣지 않았다.
  서체가 안 갈리던 진짜 이유가 이 빠진 고리다. 제목 굵기도 토큰으로 뺐다 — 간판체(Gugi)는 굵기가
  한 벌뿐이라 700 을 주면 브라우저가 가짜 볼드를 씌워 획이 뭉갠다
- industryData: templatesFor() 팩토리 하나로 심플·매거진·레트로 셋. 업종은 accent 하나만 바꾼다 —
  생김새는 업종이 아니라 취향의 문제다. 537줄 → 237줄
- Step4Template: 미리보기를 그 템플릿의 서체·모서리·테두리·그림자로 실제로 그린다
- index.html: Noto Serif KR 추가. 매거진 제목이 Batang 으로 떨어지는데 맥에는 그 서체가 없다
- SectionFrame: 세로 여백을 --tpl-section-space 로. 이 값 하나로 페이지의 호흡이 바뀐다

옛 템플릿 id 가 DB 에 남아 있어도 resolveTemplate 이 첫 템플릿으로 떨어뜨린다.

tsc·eslint·vite build 통과(frontend·admin·site), 템플릿 12벌 look 전량 대조 + 폴백 확인
2026-09-02 08:49:38 +09:00

127 lines
3.9 KiB
TypeScript

/**
* 섹션 제목 — 그 자체로 배리에이션을 갖는 원자.
*
* 섹션 배리에이션을 바꿔도 제목 스타일은 따로 고를 수 있어야 한다
* ("갤러리는 격자로, 제목은 가운데 정렬" 같은 조합이 실제로 나온다).
*/
import type {ReactNode} from 'react';
import type {TemplateItem} from '@o2o/shared';
import {cn} from '@/lib/utils';
export type HeadingStyle = 'underline' | 'centered' | 'eyebrow' | 'minimal';
interface SectionHeadingProps {
title: string;
subtitle?: string;
/** 제목 위에 얹는 작은 라벨. eyebrow · centered 스타일에서 쓴다. */
eyebrow?: string;
variant?: HeadingStyle;
/** 제목 오른쪽에 붙는 것(경고 배지, 추가 버튼 등). */
trailing?: ReactNode;
/**
* 템플릿 색 토큰. 넘기면 제목은 text, eyebrow 는 accent 를 따른다.
*
* ★ 부제·밑줄은 일부러 중립 회색으로 남긴다 — 여기까지 토큰을 먹이면
* accent 가 밝은 템플릿에서 부제가 배경에 묻힌다.
*/
colors?: TemplateItem['colors'];
className?: string;
}
export function SectionHeading({
title,
subtitle,
eyebrow,
variant = 'underline',
trailing,
colors,
className,
}: SectionHeadingProps) {
const titleStyle = colors ? {color: colors.text} : undefined;
const eyebrowStyle = colors ? {color: colors.accent} : undefined;
if (variant === 'centered') {
return (
<div className={cn('space-y-2 text-center', className)}>
{eyebrow && (
<span
className="text-[11px] font-semibold uppercase tracking-[0.2em] text-stone-400"
style={eyebrowStyle}
>
{eyebrow}
</span>
)}
<h2
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
style={titleStyle}
>
{title}
</h2>
{subtitle && <p className="text-xs text-stone-500 sm:text-sm">{subtitle}</p>}
{trailing && <div className="flex justify-center pt-1">{trailing}</div>}
</div>
);
}
if (variant === 'eyebrow') {
return (
<div className={cn('flex items-start justify-between gap-3', className)}>
<div className="space-y-1">
{eyebrow && (
<span
className="block text-[11px] font-bold uppercase tracking-wider text-amber-700"
style={eyebrowStyle}
>
{eyebrow}
</span>
)}
<h2
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
style={titleStyle}
>
{title}
</h2>
{subtitle && <p className="text-xs text-stone-500">{subtitle}</p>}
</div>
{trailing}
</div>
);
}
if (variant === 'minimal') {
// 여기서 제목은 사실상 작은 라벨이라 text 가 아니라 secondary 를 쓴다.
return (
<div className={cn('flex items-baseline justify-between gap-3', className)}>
<h2
className="tpl-title text-sm font-bold uppercase tracking-[0.18em] text-stone-500"
style={colors ? {color: colors.secondary} : undefined}
>
{title}
</h2>
{trailing}
</div>
);
}
// underline — 기본. 아래 실선이 섹션의 시작을 확실히 끊어준다.
return (
<div
className={cn(
'flex flex-col justify-between gap-2 border-b border-stone-200/80 pb-4 sm:flex-row sm:items-baseline',
className,
)}
>
<div>
<h2
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
style={titleStyle}
>
{title}
</h2>
{subtitle && <p className="mt-0.5 text-xs text-stone-500">{subtitle}</p>}
</div>
{trailing && <div className="self-start sm:self-auto">{trailing}</div>}
</div>
);
}