diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index bd61939..e017d1f 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -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 — 붙여넣기 아이템 셋: 가요 다방 · 오늘의 한 장 · 반나절 산책 **한 일** diff --git a/solution/frontend/index.html b/solution/frontend/index.html index 1e9c809..f5ab125 100644 --- a/solution/frontend/index.html +++ b/solution/frontend/index.html @@ -9,7 +9,7 @@ diff --git a/solution/frontend/src/data/industryData.ts b/solution/frontend/src/data/industryData.ts index f25f07a..5e2498c 100644 --- a/solution/frontend/src/data/industryData.ts +++ b/solution/frontend/src/data/industryData.ts @@ -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 = { stay: { @@ -24,112 +133,10 @@ export const INDUSTRY_CONFIGS: Record = { { 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 = { { 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 = { { 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 = { { 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: '갱지 바탕에 간판체. 일력과 승차권으로 지역의 시간을 걸어 다니게 만듭니다.', + }), }, }; diff --git a/solution/frontend/src/features/builder/CanvasView.tsx b/solution/frontend/src/features/builder/CanvasView.tsx index 7588037..4ed1789 100644 --- a/solution/frontend/src/features/builder/CanvasView.tsx +++ b/solution/frontend/src/features/builder/CanvasView.tsx @@ -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; /** diff --git a/solution/frontend/src/features/builder/canvas/primitives/SectionFrame.tsx b/solution/frontend/src/features/builder/canvas/primitives/SectionFrame.tsx index 9599299..ddce39d 100644 --- a/solution/frontend/src/features/builder/canvas/primitives/SectionFrame.tsx +++ b/solution/frontend/src/features/builder/canvas/primitives/SectionFrame.tsx @@ -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 && ( )}

{title} @@ -76,7 +76,7 @@ export function SectionHeading({ )}

{title} @@ -93,7 +93,7 @@ export function SectionHeading({ return (

{title} @@ -113,7 +113,7 @@ export function SectionHeading({ >

{title} diff --git a/solution/frontend/src/features/onboarding/Step4Template.tsx b/solution/frontend/src/features/onboarding/Step4Template.tsx index 45b3216..089e3c6 100644 --- a/solution/frontend/src/features/onboarding/Step4Template.tsx +++ b/solution/frontend/src/features/onboarding/Step4Template.tsx @@ -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 ( +
+

+ Section +

+

+ 오래 머무는 자리 +

+

+ 제목은 {template.fontStyle}, 본문은 이 서체로 나갑니다. +

+ +
+ + 카드 · 모서리 {look.radius === '0px' ? '각짐' : '둥금'} · 그림자{' '} + {look.shadow === 'none' ? '없음' : '있음'} + +
+ + + 예약 문의 + +
+ ); +} + export function Step4Template() { const industry = useBuilderStore((s) => s.industry); const templateId = useBuilderStore((s) => s.templateId); @@ -79,31 +153,7 @@ export function Step4Template() {

- {/* 색·레이아웃 미리보기 와이어프레임 */} -
-
- {(['primary', 'secondary', 'card'] as const).map((key) => ( - - ))} - - {template.colors.primary} - -
- -
-
-
-
-
-
+

{template.description} diff --git a/solution/frontend/src/index.css b/solution/frontend/src/index.css index c644c6d..92c927d 100644 --- a/solution/frontend/src/index.css +++ b/solution/frontend/src/index.css @@ -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); } } diff --git a/solution/shared/src/types/builder.ts b/solution/shared/src/types/builder.ts index 1f4487c..8899131 100644 --- a/solution/shared/src/types/builder.ts +++ b/solution/shared/src/types/builder.ts @@ -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;