[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 전량 대조 + 폴백 확인
This commit is contained in:
parent
e6ffdad888
commit
4debeaed3a
@ -5,6 +5,41 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-02 — 템플릿이 색만 바꾸던 걸 끝냈다 (5개 → 3개)
|
||||
|
||||
**왜**
|
||||
업종마다 템플릿이 다섯이었는데 넷이 "흰 바탕 + 고딕 + 둥근 모서리"에 색조만 달랐다.
|
||||
고르는 화면의 미리보기도 회색 막대 세 줄 + 색 동그라미라 다섯 장이 전부 같은 그림이었다 —
|
||||
사장님은 뭐가 다른지 알 수 없으니 아무거나 골랐다. 사용자 말: "가라 UI 로 되어 있어서 뭐가뭔지 모르겠음".
|
||||
|
||||
**한 일**
|
||||
- `TemplateItem.look`(`TemplateLook`) 신설: 제목·본문 서체, 모서리, 테두리 두께, 그림자,
|
||||
제목 자간·굵기, 섹션 여백. **CSS 에 그대로 들어가는 문자열**로 들고 있다 — 숫자로 두면
|
||||
쓰는 쪽에서 단위를 빠뜨린 곳이 조용히 0 이 된다.
|
||||
- 업종당 **3개**로 정리: 심플(고딕·둥근·그림자) · 매거진(명조 제목·각짐·그림자 없음·여백 큼) ·
|
||||
레트로(간판체·2px 테두리·오프셋 그림자·갱지). `templatesFor()` 팩토리 하나가 찍어내고
|
||||
**업종은 accent 하나만 바꾼다** — 생김새는 업종이 아니라 취향의 문제다.
|
||||
`industryData.ts` 537줄 → 237줄.
|
||||
- 고르는 화면의 미리보기를 **그 템플릿의 서체·모서리·테두리·그림자로 실제로 그린다**(`TemplatePreview`).
|
||||
|
||||
**핵심 수법 — Tailwind 테마 변수를 캔버스 안에서만 덮는다**
|
||||
`.site-canvas` 에 `--radius-*` · `--shadow-*` 를 내려보내면, 변이 파일 40여 개에 흩어진
|
||||
`rounded-*` · `shadow-*` 를 **한 줄도 안 고치고** 전부 템플릿을 따르게 된다.
|
||||
배수는 Tailwind 기본 비율을 그대로 옮겨, 기준값 0.75rem 이면 지금까지와 픽셀 단위로 같고 0 이면 전부 각진다.
|
||||
|
||||
**밟은 함정**
|
||||
- `.site-canvas` 는 이미 `--tpl-font-heading/body` 를 **읽고 있었는데 아무도 넣지 않았다.**
|
||||
서체가 갈리지 않던 진짜 이유가 이 빠진 고리였다.
|
||||
- 간판체(Gugi)는 굵기가 한 벌뿐이라 `font-weight:700` 을 주면 브라우저가 가짜 볼드를 씌워 획이 뭉갠다.
|
||||
→ `--tpl-heading-weight` 로 템플릿이 400 을 지정할 수 있게 했다.
|
||||
- `Noto Serif KR` 을 안 불러오고 있었다. 매거진 제목이 Batang 으로 떨어지는데 맥에는 그 서체가 없다.
|
||||
- 옛 템플릿 id(`stay-warm-wood` 등)가 DB 에 남아 있어도 `resolveTemplate` 이 첫 템플릿으로 떨어뜨린다.
|
||||
|
||||
**검증** — `tsc·eslint·vite build` 통과(frontend·admin·site). 템플릿 12벌의 look 전량 대조,
|
||||
옛 id 폴백·레트로만 아이템을 데려오는지 확인.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-01 — 붙여넣기 아이템 셋: 가요 다방 · 오늘의 한 장 · 반나절 산책
|
||||
|
||||
**한 일**
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300..900&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300..900&family=Noto+Serif+KR:wght@400;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- 7080 아이템(가요 다방·일력·승차권) 전용. 이 셋이 없으면 간판/명조가 고딕으로 떨어져 감성이 사라진다 -->
|
||||
|
||||
@ -1,4 +1,113 @@
|
||||
import type {IndustryData, IndustryType} from '@o2o/shared';
|
||||
import type {IndustryData, IndustryType, TemplateItem} from '@o2o/shared';
|
||||
/**
|
||||
* 템플릿 세 벌.
|
||||
*
|
||||
* ★ 예전엔 업종마다 다섯 벌이었는데 넷이 "흰 바탕 + 고딕 + 둥근 모서리"에 색조만 달랐다 —
|
||||
* 골라도 뭐가 달라지는지 알 수 없었다. 지금은 서체·모서리·테두리·그림자·여백까지 갈린다.
|
||||
* 같은 얼굴을 색만 바꿔 늘리지 않는다.
|
||||
* ★ 업종이 바꾸는 건 accent 하나다. 생김새는 업종이 아니라 취향의 문제다.
|
||||
*/
|
||||
const LOOK = {
|
||||
simple: {
|
||||
fontHeading: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
|
||||
fontBody: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
|
||||
radius: '0.75rem',
|
||||
borderWidth: '1px',
|
||||
shadow: '0 1px 2px rgb(0 0 0 / 0.06)',
|
||||
headingTracking: '-0.02em',
|
||||
headingWeight: '700',
|
||||
sectionSpace: '3.5rem',
|
||||
},
|
||||
magazine: {
|
||||
fontHeading: "'Noto Serif KR', 'Batang', 'Times New Roman', serif",
|
||||
fontBody: "'Pretendard Variable', 'Noto Sans KR', system-ui, sans-serif",
|
||||
radius: '0px',
|
||||
borderWidth: '1px',
|
||||
shadow: 'none',
|
||||
headingTracking: '-0.03em',
|
||||
headingWeight: '700',
|
||||
sectionSpace: '5rem',
|
||||
},
|
||||
retro: {
|
||||
fontHeading: "'Gugi', 'Noto Sans KR', sans-serif",
|
||||
fontBody: "'Gowun Batang', 'Noto Serif KR', serif",
|
||||
radius: '0px',
|
||||
borderWidth: '2px',
|
||||
shadow: '4px 4px 0 rgb(27 26 21 / 0.16)',
|
||||
// 간판체는 자간을 벌리면 글자가 흩어진다. 굵기도 한 벌뿐이라 700 을 주면 가짜 볼드가 씌워진다.
|
||||
headingTracking: '0em',
|
||||
headingWeight: '400',
|
||||
sectionSpace: '4rem',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** 업종 하나의 템플릿 세 벌. accent 만 업종이 정한다. */
|
||||
function templatesFor(
|
||||
industryId: IndustryType,
|
||||
accent: string,
|
||||
retro: {name: string; description: string},
|
||||
): TemplateItem[] {
|
||||
return [
|
||||
{
|
||||
id: `${industryId}-simple`,
|
||||
industryId,
|
||||
name: '심플',
|
||||
tone: 'info',
|
||||
toneLabel: '깔끔한 기본',
|
||||
description: '흰 바탕에 고딕. 읽기 쉽고 어디에도 어울립니다. 무엇을 고를지 모르겠으면 이것.',
|
||||
colors: {
|
||||
primary: '#18181b',
|
||||
secondary: '#52525b',
|
||||
bg: '#ffffff',
|
||||
card: '#fafafa',
|
||||
text: '#09090b',
|
||||
accent,
|
||||
},
|
||||
fontStyle: '고딕',
|
||||
look: LOOK.simple,
|
||||
},
|
||||
{
|
||||
id: `${industryId}-magazine`,
|
||||
industryId,
|
||||
name: '매거진',
|
||||
tone: 'photo',
|
||||
toneLabel: '잡지 편집',
|
||||
description: '명조 제목에 각진 모서리, 그림자 없이 선으로만. 사진과 글이 많을 때 품이 납니다.',
|
||||
colors: {
|
||||
primary: '#111111',
|
||||
secondary: '#57534e',
|
||||
bg: '#ffffff',
|
||||
card: '#f7f6f4',
|
||||
text: '#111111',
|
||||
accent,
|
||||
},
|
||||
fontStyle: '명조 제목',
|
||||
look: LOOK.magazine,
|
||||
},
|
||||
{
|
||||
id: `${industryId}-retro`,
|
||||
industryId,
|
||||
name: retro.name,
|
||||
tone: 'photo',
|
||||
toneLabel: '레트로 감성',
|
||||
description: retro.description,
|
||||
colors: {
|
||||
primary: '#1b1a15',
|
||||
secondary: '#4c4739',
|
||||
bg: '#e4dac0',
|
||||
card: '#f2ebd9',
|
||||
text: '#1b1a15',
|
||||
// 레트로의 정체성이 이 주(朱) 잉크다 — 업종 accent 로 갈아끼우지 않는다.
|
||||
accent: '#bf2f1b',
|
||||
},
|
||||
fontStyle: '옛 간판체',
|
||||
look: LOOK.retro,
|
||||
// 이 템플릿이 팔려는 게 바로 이 아이템들이다.
|
||||
defaultSectionTypes: ['songs', 'daily', 'course'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
export const INDUSTRY_CONFIGS: Record<IndustryType, IndustryData> = {
|
||||
stay: {
|
||||
@ -24,112 +133,10 @@ export const INDUSTRY_CONFIGS: Record<IndustryType, IndustryData> = {
|
||||
{ id: 'local', type: 'local', name: '지역 정보', isLocked: false, isEnabled: true, description: '주변 관광지 및 맛집 추천' },
|
||||
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '고객들이 자주 묻는 질문과 답변' },
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
id: 'stay-o2o-editorial',
|
||||
industryId: 'stay',
|
||||
name: 'O2O 에디토리얼',
|
||||
tone: 'photo',
|
||||
toneLabel: '차분한 모던',
|
||||
description: '절제된 타이포그래피와 여백, 차분한 슬레이트 톤으로 신뢰감과 감성을 함께 전합니다.',
|
||||
colors: {
|
||||
primary: '#18181b',
|
||||
secondary: '#52525b',
|
||||
bg: '#ffffff',
|
||||
card: '#fafafa',
|
||||
text: '#09090b',
|
||||
accent: '#2563eb',
|
||||
},
|
||||
fontStyle: 'Modern Editorial',
|
||||
},
|
||||
{
|
||||
id: 'stay-quiet-margin',
|
||||
industryId: 'stay',
|
||||
name: '조용한 여백',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '넓은 여백과 감성적인 사진 배치로 여유로운 휴식 분위기를 전달합니다.',
|
||||
colors: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#64748b',
|
||||
bg: '#ffffff',
|
||||
card: '#f8fafc',
|
||||
text: '#0f172a',
|
||||
accent: '#0284c7',
|
||||
},
|
||||
fontStyle: 'Clean Minimalist',
|
||||
},
|
||||
{
|
||||
id: 'stay-warm-wood',
|
||||
industryId: 'stay',
|
||||
name: '따뜻한 목조',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '아늑하고 내추럴한 톤으로 자연 친화적인 공간의 따스함을 극대화합니다.',
|
||||
colors: {
|
||||
primary: '#43302b',
|
||||
secondary: '#786055',
|
||||
bg: '#faf8f5',
|
||||
card: '#f2ece4',
|
||||
text: '#29201d',
|
||||
accent: '#c27847',
|
||||
},
|
||||
fontStyle: 'Warm Natural',
|
||||
},
|
||||
{
|
||||
id: 'stay-clear-info',
|
||||
industryId: 'stay',
|
||||
name: '또렷한 정보',
|
||||
tone: 'info',
|
||||
toneLabel: '정보 중심',
|
||||
description: '객실 옵션, 구비 시설, 체크인 규정을 한눈에 파악할 수 있는 실용적인 구조입니다.',
|
||||
colors: {
|
||||
primary: '#0f172a',
|
||||
secondary: '#475569',
|
||||
bg: '#ffffff',
|
||||
card: '#f1f5f9',
|
||||
text: '#020617',
|
||||
accent: '#2563eb',
|
||||
},
|
||||
fontStyle: 'Crisp Modern',
|
||||
},
|
||||
{
|
||||
id: 'stay-booking-focus',
|
||||
industryId: 'stay',
|
||||
name: '예약 중심',
|
||||
tone: 'book',
|
||||
toneLabel: '예약·신청 강조',
|
||||
description: '객실별 예약 버튼과 실시간 문의 CTA를 전면에 배치하여 전환율을 높입니다.',
|
||||
colors: {
|
||||
primary: '#18181b',
|
||||
secondary: '#52525b',
|
||||
bg: '#ffffff',
|
||||
card: '#fafafa',
|
||||
text: '#09090b',
|
||||
accent: '#059669',
|
||||
},
|
||||
fontStyle: 'Conversion Driven',
|
||||
},
|
||||
{
|
||||
id: 'stay-retro-port',
|
||||
industryId: 'stay',
|
||||
name: '옛 항구',
|
||||
tone: 'photo',
|
||||
toneLabel: '레트로 감성',
|
||||
description: '도넛판과 일력, 승차권. 오래된 항구 도시의 인상으로 묵는 곳을 소개합니다.',
|
||||
colors: {
|
||||
primary: '#1b1a15',
|
||||
secondary: '#4c4739',
|
||||
bg: '#e4dac0',
|
||||
card: '#f2ebd9',
|
||||
text: '#1b1a15',
|
||||
accent: '#bf2f1b',
|
||||
},
|
||||
fontStyle: 'Retro Signage',
|
||||
// 레트로 템플릿을 고르면 7080 아이템이 함께 들어온다 — 이 템플릿이 팔려는 게 바로 그것이다.
|
||||
defaultSectionTypes: ['songs', 'daily', 'course'],
|
||||
},
|
||||
],
|
||||
templates: templatesFor('stay', '#2563eb', {
|
||||
name: '옛 항구',
|
||||
description: '갱지 바탕에 간판체. 도넛판·일력·승차권이 함께 들어옵니다. 오래된 항구 도시의 인상으로 묵는 곳을 소개합니다.',
|
||||
}),
|
||||
},
|
||||
|
||||
cafe: {
|
||||
@ -155,95 +162,10 @@ export const INDUSTRY_CONFIGS: Record<IndustryType, IndustryData> = {
|
||||
{ id: 'local', type: 'local', name: '주변 나들이', isLocked: false, isEnabled: true, description: '양평 드라이브 코스 및 명소' },
|
||||
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '반려견 동반, 주차, 케어키즈존 안내' },
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
id: 'cafe-modern-espresso',
|
||||
industryId: 'cafe',
|
||||
name: '모던 에스프레소',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '다크 차콜 톤과 고해상도 커피 샷으로 프로페셔널한 스페셜티 이미지를 완성합니다.',
|
||||
colors: {
|
||||
primary: '#1c1917',
|
||||
secondary: '#78716c',
|
||||
bg: '#ffffff',
|
||||
card: '#fafaf9',
|
||||
text: '#0c0a09',
|
||||
accent: '#b45309',
|
||||
},
|
||||
fontStyle: 'Sleek Roast',
|
||||
},
|
||||
{
|
||||
id: 'cafe-sun-terrace',
|
||||
industryId: 'cafe',
|
||||
name: '햇살과 테라스',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '산뜻한 화이트와 그린 포인트로 자연 채광 가득한 힐링 테라스 분위기를 연출합니다.',
|
||||
colors: {
|
||||
primary: '#14532d',
|
||||
secondary: '#4b5563',
|
||||
bg: '#ffffff',
|
||||
card: '#f0fdf4',
|
||||
text: '#052e16',
|
||||
accent: '#16a34a',
|
||||
},
|
||||
fontStyle: 'Garden Terrace',
|
||||
},
|
||||
{
|
||||
id: 'cafe-roastery-story',
|
||||
industryId: 'cafe',
|
||||
name: '감성 로스터리',
|
||||
tone: 'info',
|
||||
toneLabel: '정보 중심',
|
||||
description: '원두 컵노트, 로스팅 프로파일, 브루잉 가이드를 체계적으로 정리해 전달합니다.',
|
||||
colors: {
|
||||
primary: '#332924',
|
||||
secondary: '#6c584c',
|
||||
bg: '#fcfaf8',
|
||||
card: '#f4ede6',
|
||||
text: '#201815',
|
||||
accent: '#8c5e3d',
|
||||
},
|
||||
fontStyle: 'Artisan Roast',
|
||||
},
|
||||
{
|
||||
id: 'cafe-quick-order',
|
||||
industryId: 'cafe',
|
||||
name: '퀵 오더 & 예약',
|
||||
tone: 'book',
|
||||
toneLabel: '예약·신청 강조',
|
||||
description: '시그니처 원두 패키지 스마트스토어 링크 및 단체 대관 접수를 최우선으로 노출합니다.',
|
||||
colors: {
|
||||
primary: '#0f172a',
|
||||
secondary: '#475569',
|
||||
bg: '#ffffff',
|
||||
card: '#f8fafc',
|
||||
text: '#020617',
|
||||
accent: '#ea580c',
|
||||
},
|
||||
fontStyle: 'Dynamic Commerce',
|
||||
},
|
||||
{
|
||||
id: 'cafe-retro-dabang',
|
||||
industryId: 'cafe',
|
||||
name: '옛 다방',
|
||||
tone: 'photo',
|
||||
toneLabel: '레트로 감성',
|
||||
description: 'LP 와 갱지, 손글씨. 다방 시절의 인상으로 지금의 커피를 이야기합니다.',
|
||||
colors: {
|
||||
primary: '#1b1a15',
|
||||
secondary: '#4c4739',
|
||||
bg: '#e4dac0',
|
||||
card: '#f2ebd9',
|
||||
text: '#1b1a15',
|
||||
accent: '#bf2f1b',
|
||||
},
|
||||
fontStyle: 'Retro Signage',
|
||||
// 레트로 템플릿을 고르면 7080 아이템이 함께 들어온다 — 이 템플릿이 팔려는 게 바로 그것이다.
|
||||
defaultSectionTypes: ['songs', 'daily', 'course'],
|
||||
},
|
||||
],
|
||||
templates: templatesFor('cafe', '#b45309', {
|
||||
name: '옛 다방',
|
||||
description: '갱지 바탕에 간판체. LP 와 손글씨로, 다방 시절의 인상으로 지금의 커피를 이야기합니다.',
|
||||
}),
|
||||
},
|
||||
|
||||
restaurant: {
|
||||
@ -269,95 +191,10 @@ export const INDUSTRY_CONFIGS: Record<IndustryType, IndustryData> = {
|
||||
{ id: 'local', type: 'local', name: '주변 안내', isLocked: false, isEnabled: true, description: '청담 명품거리 및 갤러리 안내' },
|
||||
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '콜키지 정책, 알러지 케어, 주차 안내' },
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
id: 'rest-neat-table',
|
||||
industryId: 'restaurant',
|
||||
name: '정갈한 한상',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '단아한 여백과 요리 고유의 색감을 살려 프리미엄 다이닝의 격을 높입니다.',
|
||||
colors: {
|
||||
primary: '#1c1917',
|
||||
secondary: '#57534e',
|
||||
bg: '#ffffff',
|
||||
card: '#fafaf9',
|
||||
text: '#0c0a09',
|
||||
accent: '#b45309',
|
||||
},
|
||||
fontStyle: 'Sophisticated Table',
|
||||
},
|
||||
{
|
||||
id: 'rest-modern-dining',
|
||||
industryId: 'restaurant',
|
||||
name: '모던 다이닝',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '세련된 다크 모던 톤으로 주류 페어링과 비스트로의 트렌디함을 강조합니다.',
|
||||
colors: {
|
||||
primary: '#09090b',
|
||||
secondary: '#52525b',
|
||||
bg: '#ffffff',
|
||||
card: '#f4f4f5',
|
||||
text: '#09090b',
|
||||
accent: '#e11d48',
|
||||
},
|
||||
fontStyle: 'Chic Bistro',
|
||||
},
|
||||
{
|
||||
id: 'rest-taste-origin',
|
||||
industryId: 'restaurant',
|
||||
name: '맛과 원산지',
|
||||
tone: 'info',
|
||||
toneLabel: '정보 중심',
|
||||
description: '계절 코스 구성, 식재료 산지 지도, 조리 과정을 상세히 기록하여 신뢰도를 높입니다.',
|
||||
colors: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#475569',
|
||||
bg: '#ffffff',
|
||||
card: '#f1f5f9',
|
||||
text: '#0f172a',
|
||||
accent: '#0d9488',
|
||||
},
|
||||
fontStyle: 'Artisan Story',
|
||||
},
|
||||
{
|
||||
id: 'rest-booking-type',
|
||||
industryId: 'restaurant',
|
||||
name: '테이블 예약형',
|
||||
tone: 'book',
|
||||
toneLabel: '예약·신청 강조',
|
||||
description: '시간대별 룸 예약 위젯과 잔여석 상태를 직관적으로 노출합니다.',
|
||||
colors: {
|
||||
primary: '#0f172a',
|
||||
secondary: '#334155',
|
||||
bg: '#ffffff',
|
||||
card: '#f8fafc',
|
||||
text: '#020617',
|
||||
accent: '#2563eb',
|
||||
},
|
||||
fontStyle: 'Direct Reserve',
|
||||
},
|
||||
{
|
||||
id: 'restaurant-retro-nopo',
|
||||
industryId: 'restaurant',
|
||||
name: '노포',
|
||||
tone: 'photo',
|
||||
toneLabel: '레트로 감성',
|
||||
description: '간판 활자와 성냥갑 톤. 오래 해온 집이라는 사실 자체가 메뉴판입니다.',
|
||||
colors: {
|
||||
primary: '#1b1a15',
|
||||
secondary: '#4c4739',
|
||||
bg: '#e4dac0',
|
||||
card: '#f2ebd9',
|
||||
text: '#1b1a15',
|
||||
accent: '#bf2f1b',
|
||||
},
|
||||
fontStyle: 'Retro Signage',
|
||||
// 레트로 템플릿을 고르면 7080 아이템이 함께 들어온다 — 이 템플릿이 팔려는 게 바로 그것이다.
|
||||
defaultSectionTypes: ['songs', 'daily', 'course'],
|
||||
},
|
||||
],
|
||||
templates: templatesFor('restaurant', '#16a34a', {
|
||||
name: '노포',
|
||||
description: '갱지 바탕에 간판체. 오래 해온 집이라는 사실 자체가 메뉴판이 됩니다.',
|
||||
}),
|
||||
},
|
||||
|
||||
tour: {
|
||||
@ -383,95 +220,10 @@ export const INDUSTRY_CONFIGS: Record<IndustryType, IndustryData> = {
|
||||
{ id: 'local', type: 'local', name: '주변 관광 코스', isLocked: false, isEnabled: true, description: '도예마을 맛집 및 주변 힐링 스팟' },
|
||||
{ id: 'faq', type: 'faq', name: '자주 묻는 질문', isLocked: false, isEnabled: true, description: '작품 수령 기간, 복장, 환불 규정' },
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
id: 'tour-visual-tour',
|
||||
industryId: 'tour',
|
||||
name: '비주얼 투어',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '체험 현장의 생생한 사진과 감각적인 그리드로 방문 욕구를 자극합니다.',
|
||||
colors: {
|
||||
primary: '#0f172a',
|
||||
secondary: '#475569',
|
||||
bg: '#ffffff',
|
||||
card: '#f8fafc',
|
||||
text: '#020617',
|
||||
accent: '#0284c7',
|
||||
},
|
||||
fontStyle: 'Visual Journey',
|
||||
},
|
||||
{
|
||||
id: 'tour-gallery-archive',
|
||||
industryId: 'tour',
|
||||
name: '갤러리 아카이브',
|
||||
tone: 'photo',
|
||||
toneLabel: '사진 중심',
|
||||
description: '단아한 미술관 도록 스타일로 공예품과 전시 작품의 미학을 돋보이게 합니다.',
|
||||
colors: {
|
||||
primary: '#27272a',
|
||||
secondary: '#71717a',
|
||||
bg: '#fafafa',
|
||||
card: '#f4f4f5',
|
||||
text: '#18181b',
|
||||
accent: '#d97706',
|
||||
},
|
||||
fontStyle: 'Art Museum',
|
||||
},
|
||||
{
|
||||
id: 'tour-detailed-guide',
|
||||
industryId: 'tour',
|
||||
name: '상세 안내형',
|
||||
tone: 'info',
|
||||
toneLabel: '정보 중심',
|
||||
description: '연령대별 추천 코스, 단계별 커리큘럼, 준비물 안내를 명확하게 정리합니다.',
|
||||
colors: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#64748b',
|
||||
bg: '#ffffff',
|
||||
card: '#f1f5f9',
|
||||
text: '#0f172a',
|
||||
accent: '#4f46e5',
|
||||
},
|
||||
fontStyle: 'Curriculum Guide',
|
||||
},
|
||||
{
|
||||
id: 'tour-ticket-booking',
|
||||
industryId: 'tour',
|
||||
name: '티켓 예매형',
|
||||
tone: 'book',
|
||||
toneLabel: '예약·신청 강조',
|
||||
description: '클래스 날짜 선택과 즉시 티켓 결제 연동에 최적화된 고전환 레이아웃입니다.',
|
||||
colors: {
|
||||
primary: '#0f172a',
|
||||
secondary: '#334155',
|
||||
bg: '#ffffff',
|
||||
card: '#f8fafc',
|
||||
text: '#020617',
|
||||
accent: '#059669',
|
||||
},
|
||||
fontStyle: 'Instant Booking',
|
||||
},
|
||||
{
|
||||
id: 'tour-retro-timetrip',
|
||||
industryId: 'tour',
|
||||
name: '시간여행',
|
||||
tone: 'photo',
|
||||
toneLabel: '레트로 감성',
|
||||
description: '연표와 승차권, 일력. 지역의 시간을 걸어 다니는 체험으로 엮습니다.',
|
||||
colors: {
|
||||
primary: '#1b1a15',
|
||||
secondary: '#4c4739',
|
||||
bg: '#e4dac0',
|
||||
card: '#f2ebd9',
|
||||
text: '#1b1a15',
|
||||
accent: '#bf2f1b',
|
||||
},
|
||||
fontStyle: 'Retro Signage',
|
||||
// 레트로 템플릿을 고르면 7080 아이템이 함께 들어온다 — 이 템플릿이 팔려는 게 바로 그것이다.
|
||||
defaultSectionTypes: ['songs', 'daily', 'course'],
|
||||
},
|
||||
],
|
||||
templates: templatesFor('tour', '#7c3aed', {
|
||||
name: '시간여행',
|
||||
description: '갱지 바탕에 간판체. 일력과 승차권으로 지역의 시간을 걸어 다니게 만듭니다.',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -60,6 +60,8 @@ export function CanvasView() {
|
||||
// 템플릿을 바꿔도 화면 면적의 대부분이 그대로다 — 쇼케이스와 같은 식(lib/color.ts)을 쓴다.
|
||||
const surfaces = deriveSurfaces(template.colors);
|
||||
|
||||
const look = template.look;
|
||||
|
||||
const themeVars = {
|
||||
'--tpl-surface': surfaces.surface,
|
||||
'--tpl-surface-alt': surfaces.surfaceAlt,
|
||||
@ -71,6 +73,28 @@ export function CanvasView() {
|
||||
'--tpl-card': template.colors.card,
|
||||
'--tpl-text': template.colors.text,
|
||||
'--tpl-accent': template.colors.accent,
|
||||
|
||||
// 색만 바꾸면 템플릿이 다 같아 보인다 — 생김새를 가르는 건 아래쪽이다.
|
||||
'--tpl-font-heading': look.fontHeading,
|
||||
'--tpl-font-body': look.fontBody,
|
||||
'--tpl-heading-tracking': look.headingTracking,
|
||||
'--tpl-heading-weight': look.headingWeight,
|
||||
'--tpl-section-space': look.sectionSpace,
|
||||
'--tpl-border-width': look.borderWidth,
|
||||
|
||||
// ★ Tailwind v4 의 테마 변수를 캔버스 안에서만 덮는다. 이러면 변이 파일 40여 개에 흩어진
|
||||
// rounded-* · shadow-* 를 하나도 안 고치고 전부 템플릿을 따르게 된다.
|
||||
// 배수는 Tailwind 기본 비율(0.25/0.375/0.5/0.75/1/1.5rem)을 그대로 옮긴 것이라,
|
||||
// 기준값이 0.75rem 이면 지금까지와 픽셀 단위로 같고 0 이면 전부 각진다.
|
||||
'--radius-sm': `calc(${look.radius} * 0.34)`,
|
||||
'--radius-md': `calc(${look.radius} * 0.5)`,
|
||||
'--radius-lg': `calc(${look.radius} * 0.67)`,
|
||||
'--radius-xl': look.radius,
|
||||
'--radius-2xl': `calc(${look.radius} * 1.34)`,
|
||||
'--radius-3xl': `calc(${look.radius} * 2)`,
|
||||
'--shadow-xs': look.shadow,
|
||||
'--shadow-sm': look.shadow,
|
||||
'--shadow-md': look.shadow,
|
||||
} as CSSProperties;
|
||||
|
||||
/**
|
||||
|
||||
@ -54,13 +54,18 @@ export function SectionFrame({
|
||||
onClick={onSelect}
|
||||
className={cn(
|
||||
'relative cursor-pointer transition-all',
|
||||
padded && 'px-4 py-12 sm:px-6 sm:py-16 md:px-8',
|
||||
padded && 'px-4 sm:px-6 md:px-8',
|
||||
isSelected
|
||||
? 'ring-2 ring-stone-900 ring-offset-2'
|
||||
: 'hover:outline-1 hover:outline-dashed hover:outline-stone-300',
|
||||
className,
|
||||
)}
|
||||
style={{backgroundColor: TONE_BG[tone]}}
|
||||
style={{
|
||||
backgroundColor: TONE_BG[tone],
|
||||
// 세로 여백은 템플릿이 정한다 — 이 값 하나로 페이지의 호흡이 바뀐다.
|
||||
// 폴백은 걷어낸 py-12/py-16 과 같은 값이라, 토큰이 없는 곳에서는 지금까지와 동일하다.
|
||||
...(padded ? {paddingBlock: 'var(--tpl-section-space, 3.5rem)'} : {}),
|
||||
}}
|
||||
>
|
||||
{isSelected && (
|
||||
<span
|
||||
|
||||
@ -52,7 +52,7 @@ export function SectionHeading({
|
||||
</span>
|
||||
)}
|
||||
<h2
|
||||
className="serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
||||
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
||||
style={titleStyle}
|
||||
>
|
||||
{title}
|
||||
@ -76,7 +76,7 @@ export function SectionHeading({
|
||||
</span>
|
||||
)}
|
||||
<h2
|
||||
className="serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
||||
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
||||
style={titleStyle}
|
||||
>
|
||||
{title}
|
||||
@ -93,7 +93,7 @@ export function SectionHeading({
|
||||
return (
|
||||
<div className={cn('flex items-baseline justify-between gap-3', className)}>
|
||||
<h2
|
||||
className="text-sm font-bold uppercase tracking-[0.18em] text-stone-500"
|
||||
className="tpl-title text-sm font-bold uppercase tracking-[0.18em] text-stone-500"
|
||||
style={colors ? {color: colors.secondary} : undefined}
|
||||
>
|
||||
{title}
|
||||
@ -113,7 +113,7 @@ export function SectionHeading({
|
||||
>
|
||||
<div>
|
||||
<h2
|
||||
className="serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
||||
className="tpl-title serif-title text-xl font-bold tracking-tight text-stone-900 sm:text-2xl"
|
||||
style={titleStyle}
|
||||
>
|
||||
{title}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {Check} from 'lucide-react';
|
||||
import type {TemplateItem} from '@o2o/shared';
|
||||
import {INDUSTRY_CONFIGS} from '@/data/industryData';
|
||||
import {queueSiteTemplateSave} from '@/features/publish/siteTemplate';
|
||||
import {cn} from '@/lib/utils';
|
||||
@ -6,6 +7,79 @@ import {useBuilderStore} from '@/stores/builder';
|
||||
import {WizardFooter} from './WizardFooter';
|
||||
import {WizardSteps} from './WizardSteps';
|
||||
|
||||
|
||||
/**
|
||||
* 템플릿 미리보기 — 그 템플릿의 서체·모서리·테두리·그림자로 **실제로** 그린다.
|
||||
*
|
||||
* ★ 예전에는 회색 막대 세 줄과 색 동그라미였다. 다섯 템플릿이 전부 같은 그림이라
|
||||
* 무엇을 고르는지 알 수 없었고, 그래서 아무거나 골랐다.
|
||||
*/
|
||||
function TemplatePreview({template}: {template: TemplateItem}) {
|
||||
const {look, colors} = template;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="my-3 overflow-hidden p-3.5"
|
||||
style={{
|
||||
backgroundColor: colors.bg,
|
||||
color: colors.text,
|
||||
borderRadius: look.radius,
|
||||
border: `${look.borderWidth} solid ${colors.secondary}22`,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[9px] uppercase"
|
||||
style={{color: colors.accent, letterSpacing: '0.22em', fontFamily: look.fontBody}}
|
||||
>
|
||||
Section
|
||||
</p>
|
||||
<p
|
||||
className="mt-1.5 text-base leading-tight"
|
||||
style={{
|
||||
fontFamily: look.fontHeading,
|
||||
fontWeight: look.headingWeight,
|
||||
letterSpacing: look.headingTracking,
|
||||
}}
|
||||
>
|
||||
오래 머무는 자리
|
||||
</p>
|
||||
<p
|
||||
className="mt-1.5 text-[10px] leading-relaxed"
|
||||
style={{fontFamily: look.fontBody, color: colors.secondary}}
|
||||
>
|
||||
제목은 {template.fontStyle}, 본문은 이 서체로 나갑니다.
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="mt-3 p-2"
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: `calc(${look.radius} * 0.67)`,
|
||||
border: `${look.borderWidth} solid ${colors.secondary}22`,
|
||||
boxShadow: look.shadow,
|
||||
}}
|
||||
>
|
||||
<span className="text-[9px]" style={{fontFamily: look.fontBody, color: colors.secondary}}>
|
||||
카드 · 모서리 {look.radius === '0px' ? '각짐' : '둥금'} · 그림자{' '}
|
||||
{look.shadow === 'none' ? '없음' : '있음'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="mt-2.5 inline-block px-2.5 py-1 text-[9px] font-semibold"
|
||||
style={{
|
||||
backgroundColor: colors.accent,
|
||||
color: colors.bg,
|
||||
borderRadius: `calc(${look.radius} * 0.5)`,
|
||||
fontFamily: look.fontBody,
|
||||
}}
|
||||
>
|
||||
예약 문의
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Step4Template() {
|
||||
const industry = useBuilderStore((s) => s.industry);
|
||||
const templateId = useBuilderStore((s) => s.templateId);
|
||||
@ -79,31 +153,7 @@ export function Step4Template() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 색·레이아웃 미리보기 와이어프레임 */}
|
||||
<div className="my-3 space-y-2.5 rounded-xl border border-border bg-muted/50 p-3.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{(['primary', 'secondary', 'card'] as const).map((key) => (
|
||||
<span
|
||||
key={key}
|
||||
className="size-3.5 rounded-full border border-black/10"
|
||||
style={{backgroundColor: template.colors[key]}}
|
||||
title={key}
|
||||
/>
|
||||
))}
|
||||
<span className="ml-1 font-mono text-[10px] text-muted-foreground">
|
||||
{template.colors.primary}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 pt-1">
|
||||
<div
|
||||
className="h-2.5 w-2/3 rounded"
|
||||
style={{backgroundColor: template.colors.primary, opacity: 0.85}}
|
||||
/>
|
||||
<div className="h-1.5 w-full rounded bg-border" />
|
||||
<div className="h-1.5 w-4/5 rounded bg-border" />
|
||||
</div>
|
||||
</div>
|
||||
<TemplatePreview template={template} />
|
||||
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{template.description}
|
||||
|
||||
@ -60,8 +60,19 @@ body {
|
||||
★ 폴백이 inherit 이라 --tpl-font-heading 이 없으면 예전과 똑같이 본문 서체를 그대로 물려받는다. */
|
||||
.site-canvas h1,
|
||||
.site-canvas h2,
|
||||
.site-canvas h3 {
|
||||
.site-canvas h3,
|
||||
.site-canvas h4 {
|
||||
font-family: var(--tpl-font-heading, inherit);
|
||||
letter-spacing: var(--tpl-heading-tracking, normal);
|
||||
}
|
||||
/* ★ 굵기는 폴백을 inherit 로 두지 않는다. 간판체(Gugi)는 굵기가 한 벌뿐이라 700 을 주면
|
||||
브라우저가 가짜 볼드를 씌워 획이 뭉갠다 — 템플릿이 400 을 지정할 수 있어야 한다. */
|
||||
.site-canvas :is(h1, h2, h3, h4).tpl-title {
|
||||
font-weight: var(--tpl-heading-weight, 700);
|
||||
}
|
||||
/* 카드·패널 테두리 두께도 템플릿이 정한다. 레트로는 2px 라야 인쇄물처럼 보인다. */
|
||||
.site-canvas .border {
|
||||
border-width: var(--tpl-border-width, 1px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -67,7 +67,15 @@ export interface TemplateItem {
|
||||
text: string;
|
||||
accent: string;
|
||||
};
|
||||
/** 사람에게 보이는 서체 이름('고딕' · '명조 제목'). 실제 서체는 look 이 정한다. */
|
||||
fontStyle: string;
|
||||
/**
|
||||
* 실제로 화면을 가르는 값.
|
||||
*
|
||||
* ★ 예전엔 색 여섯 개뿐이라 템플릿을 바꿔도 색조만 달라졌다 — 다섯 개가 다 같아 보였다.
|
||||
* 서체·모서리·테두리·그림자·여백이 여기 있고, 캔버스가 CSS 변수로 그대로 내려보낸다.
|
||||
*/
|
||||
look: TemplateLook;
|
||||
/**
|
||||
* 이 템플릿을 고르면 함께 들어오는 섹션(canvas/addable 의 타입).
|
||||
*
|
||||
@ -77,6 +85,29 @@ export interface TemplateItem {
|
||||
defaultSectionTypes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿의 생김새. 값은 전부 CSS 에 그대로 들어가는 문자열이다 —
|
||||
* 숫자로 들고 있다가 쓰는 쪽에서 단위를 붙이면, 빠뜨린 곳이 조용히 0 이 된다.
|
||||
*/
|
||||
export interface TemplateLook {
|
||||
/** 제목 서체 스택. `--tpl-font-heading` */
|
||||
fontHeading: string;
|
||||
/** 본문 서체 스택. `--tpl-font-body` */
|
||||
fontBody: string;
|
||||
/** 모서리. Tailwind 의 `--radius-*` 를 캔버스 안에서만 덮어 rounded-* 전부가 따라온다. */
|
||||
radius: string;
|
||||
/** 카드·패널 테두리 두께. */
|
||||
borderWidth: string;
|
||||
/** 카드 그림자. Tailwind 의 `--shadow-*` 를 덮는다. 'none' 이면 평평해진다. */
|
||||
shadow: string;
|
||||
/** 제목 자간. */
|
||||
headingTracking: string;
|
||||
/** 제목 굵기. 간판체처럼 한 굵기뿐인 서체는 400 이어야 한다. */
|
||||
headingWeight: string;
|
||||
/** 섹션 세로 여백. 이 값 하나로 페이지의 호흡이 바뀐다. */
|
||||
sectionSpace: string;
|
||||
}
|
||||
|
||||
export interface PhotoItem {
|
||||
id: string;
|
||||
url: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user