From 017d5f128e5be9b9f62d5b31e631c792eec978ad Mon Sep 17 00:00:00 2001 From: Jaybe Date: Wed, 10 Dec 2025 17:04:56 +0900 Subject: [PATCH] =?UTF-8?q?ci=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/editor/templates/blogTemplate.ts | 12 +- src/app/editor/templates/ctaTemplate.ts | 8 +- src/app/editor/templates/faqTemplate.ts | 25 +-- src/app/editor/templates/featuresTemplate.ts | 8 +- src/app/editor/templates/footerTemplate.ts | 8 +- src/app/editor/templates/heroTemplate.ts | 10 +- src/app/editor/templates/teamTemplate.ts | 12 +- src/features/editor/state/editorStore.ts | 26 ++- src/features/i18n/LocaleSwitcher.tsx | 26 +-- tests/api/export.spec.ts | 156 +++++------------- tests/unit/EditorSectionLayout.spec.tsx | 9 +- .../unit/ProjectPropertiesPanel.seo.spec.tsx | 8 +- tests/unit/PublicPageRendererFooter.spec.tsx | 14 +- .../unit/PublicPageRendererTemplates.spec.tsx | 23 ++- uploads/3cd68ac4-d85d-44d3-8e68-97086b341895 | 1 + uploads/43987d79-2417-4bdf-bb00-c064f948115c | 1 + uploads/5abc6415-e25d-4a8c-a791-f3cb6cf1263d | 1 + uploads/fbf099b4-65c8-4db6-92ab-7f7f494016a9 | 1 + uploads/test-image-1765353749297 | 1 + uploads/test-image-1765353782946 | 1 + uploads/test-section-bg-1765353749344 | 1 + uploads/test-section-bg-1765353782975 | 1 + uploads/test-section-bg-video-1765353749481 | 1 + uploads/test-section-bg-video-1765353783013 | 1 + uploads/test-video-1765353749466 | 1 + uploads/test-video-1765353783005 | 1 + uploads/test-video-poster-1765353749304 | 1 + uploads/test-video-poster-1765353782956 | 1 + 28 files changed, 167 insertions(+), 192 deletions(-) create mode 100644 uploads/3cd68ac4-d85d-44d3-8e68-97086b341895 create mode 100644 uploads/43987d79-2417-4bdf-bb00-c064f948115c create mode 100644 uploads/5abc6415-e25d-4a8c-a791-f3cb6cf1263d create mode 100644 uploads/fbf099b4-65c8-4db6-92ab-7f7f494016a9 create mode 100644 uploads/test-image-1765353749297 create mode 100644 uploads/test-image-1765353782946 create mode 100644 uploads/test-section-bg-1765353749344 create mode 100644 uploads/test-section-bg-1765353782975 create mode 100644 uploads/test-section-bg-video-1765353749481 create mode 100644 uploads/test-section-bg-video-1765353783013 create mode 100644 uploads/test-video-1765353749466 create mode 100644 uploads/test-video-1765353783005 create mode 100644 uploads/test-video-poster-1765353749304 create mode 100644 uploads/test-video-poster-1765353782956 diff --git a/src/app/editor/templates/blogTemplate.ts b/src/app/editor/templates/blogTemplate.ts index 8a2f1fa..7e52dc8 100644 --- a/src/app/editor/templates/blogTemplate.ts +++ b/src/app/editor/templates/blogTemplate.ts @@ -1,12 +1,14 @@ "use client"; import type { Block, SectionBlockProps, TextBlockProps, ImageBlockProps } from "@/features/editor/state/editorStore"; +import type { EditorBlogTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createBlogTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorBlogTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const columns: SectionBlockProps["columns"] = [ { id: `${sectionId}_col_1`, span: 4 }, @@ -33,14 +35,8 @@ export function createBlogTemplateBlocks(opts: { columnId: null, }; - const postDefinitions = [ - { title: "블로그 포스트 1", summary: "첫 번째 포스트 요약을 여기에 입력하세요." }, - { title: "블로그 포스트 2", summary: "두 번째 포스트 요약을 여기에 입력하세요." }, - { title: "블로그 포스트 3", summary: "세 번째 포스트 요약을 여기에 입력하세요." }, - ]; - const blogBlocks: Block[] = columns.flatMap((col, index) => { - const post = postDefinitions[index] ?? postDefinitions[0]; + const post = messages.posts[index] ?? messages.posts[0]; const imageId = createId(); const titleId = createId(); diff --git a/src/app/editor/templates/ctaTemplate.ts b/src/app/editor/templates/ctaTemplate.ts index 5d757e3..d4b79f9 100644 --- a/src/app/editor/templates/ctaTemplate.ts +++ b/src/app/editor/templates/ctaTemplate.ts @@ -1,12 +1,14 @@ "use client"; import type { Block, SectionBlockProps, TextBlockProps, ButtonBlockProps } from "@/features/editor/state/editorStore"; +import type { EditorCtaTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createCtaTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorCtaTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const columns: SectionBlockProps["columns"] = [ { id: `${sectionId}_col_1`, span: 8 }, @@ -37,7 +39,7 @@ export function createCtaTemplateBlocks(opts: { const textId = createId(); const textProps: TextBlockProps = { - text: "지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.\n망설이지 말고 시작하세요!", + text: messages.body, align: "left", size: "lg", } as any; @@ -51,7 +53,7 @@ export function createCtaTemplateBlocks(opts: { const buttonId = createId(); const buttonProps: ButtonBlockProps = { - label: "CTA 버튼", + label: messages.buttonLabel, href: "#", align: "center", size: "lg", diff --git a/src/app/editor/templates/faqTemplate.ts b/src/app/editor/templates/faqTemplate.ts index 15d9e3c..80f8b3d 100644 --- a/src/app/editor/templates/faqTemplate.ts +++ b/src/app/editor/templates/faqTemplate.ts @@ -1,12 +1,14 @@ "use client"; import type { Block, SectionBlockProps, TextBlockProps } from "@/features/editor/state/editorStore"; +import type { EditorFaqTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createFaqTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorFaqTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const columns: SectionBlockProps["columns"] = [ { id: `${sectionId}_col_1`, span: 3 }, @@ -38,7 +40,7 @@ export function createFaqTemplateBlocks(opts: { // Left Column: Title const titleId = createId(); const titleProps: TextBlockProps = { - text: "FAQ", + text: messages.title, align: "left", size: "xl", bold: true, @@ -53,7 +55,7 @@ export function createFaqTemplateBlocks(opts: { const subTitleId = createId(); const subTitleProps: TextBlockProps = { - text: "자주 묻는 질문", + text: messages.subtitle, align: "left", size: "sm", color: "muted", @@ -68,22 +70,7 @@ export function createFaqTemplateBlocks(opts: { // Right Column: Q&A List - const faqPairs = [ - { - question: "서비스 이용료는 얼마인가요?", - answer: "기본 기능은 무료로 제공되며, 프리미엄 기능은 월 구독료가 발생합니다.", - }, - { - question: "환불 정책은 어떻게 되나요?", - answer: "결제 후 7일 이내에 사용 이력이 없는 경우 전액 환불 가능합니다.", - }, - { - question: "팀원 초대 기능이 있나요?", - answer: "네, 프로 요금제 이상부터 팀원 초대가 가능합니다.", - }, - ]; - - const faqBlocks: Block[] = faqPairs.flatMap((pair) => { + const faqBlocks: Block[] = messages.items.flatMap((pair) => { const qId = createId(); const aId = createId(); diff --git a/src/app/editor/templates/featuresTemplate.ts b/src/app/editor/templates/featuresTemplate.ts index 006fa51..648be6c 100644 --- a/src/app/editor/templates/featuresTemplate.ts +++ b/src/app/editor/templates/featuresTemplate.ts @@ -1,12 +1,14 @@ "use client"; import type { Block, SectionBlockProps, TextBlockProps } from "@/features/editor/state/editorStore"; +import type { EditorFeaturesTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createFeaturesTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorFeaturesTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const columns = [ { id: `${sectionId}_col_1`, span: 4 }, @@ -38,13 +40,13 @@ export function createFeaturesTemplateBlocks(opts: { const descId = createId(); const titleProps: TextBlockProps = { - text: `Feature ${index + 1} 제목`, + text: `${messages.itemTitlePrefix} ${index + 1}${messages.itemTitleSuffix}`.trim(), align: "left", size: "lg", } as any; const descProps: TextBlockProps = { - text: "해당 기능을 간단히 설명하는 텍스트입니다.", + text: messages.itemDescription, align: "left", size: "sm", } as any; diff --git a/src/app/editor/templates/footerTemplate.ts b/src/app/editor/templates/footerTemplate.ts index 317711a..52e0d2a 100644 --- a/src/app/editor/templates/footerTemplate.ts +++ b/src/app/editor/templates/footerTemplate.ts @@ -1,12 +1,14 @@ "use client"; import type { Block, SectionBlockProps, TextBlockProps } from "@/features/editor/state/editorStore"; +import type { EditorFooterTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createFooterTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorFooterTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const columns: SectionBlockProps["columns"] = [ { id: `${sectionId}_col_1`, span: 4 }, @@ -54,7 +56,7 @@ export function createFooterTemplateBlocks(opts: { const descId = createId(); const descProps: TextBlockProps = { - text: "더 나은 웹사이트를 위한 최고의 선택.", + text: messages.description, align: "left", size: "sm", color: "muted", @@ -70,7 +72,7 @@ export function createFooterTemplateBlocks(opts: { // Col 2: Links const linksId = createId(); const linksProps: TextBlockProps = { - text: "서비스 소개\n요금제\n고객지원\n문의하기", + text: messages.linksText, align: "center", size: "sm", color: "muted", diff --git a/src/app/editor/templates/heroTemplate.ts b/src/app/editor/templates/heroTemplate.ts index 5922d12..b220dde 100644 --- a/src/app/editor/templates/heroTemplate.ts +++ b/src/app/editor/templates/heroTemplate.ts @@ -6,12 +6,14 @@ import type { TextBlockProps, ButtonBlockProps, } from "@/features/editor/state/editorStore"; +import type { EditorHeroTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createHeroTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorHeroTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const sectionProps: SectionBlockProps = { background: "default", @@ -41,7 +43,7 @@ export function createHeroTemplateBlocks(opts: { const heroHeadlineId = createId(); const heroHeadlineProps: TextBlockProps = { - text: "Hero 제목을 여기에 입력하세요", + text: messages.headline, align: "center", size: "lg", } as any; @@ -55,7 +57,7 @@ export function createHeroTemplateBlocks(opts: { const heroSubId = createId(); const heroSubProps: TextBlockProps = { - text: "제품이나 서비스를 한 문장으로 설명하는 서브텍스트입니다.", + text: messages.subheadline, align: "center", size: "base", } as any; @@ -69,7 +71,7 @@ export function createHeroTemplateBlocks(opts: { const heroButtonId = createId(); const heroButtonProps: ButtonBlockProps = { - label: "지금 시작하기", + label: messages.primaryButtonLabel, href: "#", align: "center", size: "md", diff --git a/src/app/editor/templates/teamTemplate.ts b/src/app/editor/templates/teamTemplate.ts index 5cd5e53..b3394fb 100644 --- a/src/app/editor/templates/teamTemplate.ts +++ b/src/app/editor/templates/teamTemplate.ts @@ -1,12 +1,14 @@ "use client"; import type { Block, SectionBlockProps, TextBlockProps, ImageBlockProps } from "@/features/editor/state/editorStore"; +import type { EditorTeamTemplateMessages } from "@/features/i18n/messages/editorTemplates"; export function createTeamTemplateBlocks(opts: { sectionId: string; createId: () => string; + messages: EditorTeamTemplateMessages; }): { blocks: Block[]; lastSelectedId: string } { - const { sectionId, createId } = opts; + const { sectionId, createId, messages } = opts; const columns: SectionBlockProps["columns"] = [ { id: `${sectionId}_col_1`, span: 4 }, @@ -33,14 +35,8 @@ export function createTeamTemplateBlocks(opts: { columnId: null, }; - const memberDefinitions = [ - { name: "홍길동", role: "Product Designer", bio: "사용자 경험을 설계합니다." }, - { name: "김영희", role: "Frontend Engineer", bio: "깔끔한 UI를 구현합니다." }, - { name: "이철수", role: "Backend Engineer", bio: "안정적인 인프라를 책임집니다." }, - ]; - const teamBlocks: Block[] = columns.flatMap((col, index) => { - const m = memberDefinitions[index] ?? memberDefinitions[0]; + const m = messages.members[index] ?? messages.members[0]; const imageId = createId(); const nameId = createId(); diff --git a/src/features/editor/state/editorStore.ts b/src/features/editor/state/editorStore.ts index 23c264e..da903b5 100644 --- a/src/features/editor/state/editorStore.ts +++ b/src/features/editor/state/editorStore.ts @@ -9,6 +9,8 @@ import { createCtaTemplateBlocks } from "@/app/editor/templates/ctaTemplate"; import { createFaqTemplateBlocks } from "@/app/editor/templates/faqTemplate"; import { createPricingTemplateBlocks } from "@/app/editor/templates/pricingTemplate"; import { createTestimonialsTemplateBlocks } from "@/app/editor/templates/testimonialsTemplate"; +import { getEditorTemplatesMessages } from "@/features/i18n/messages/editorTemplates"; +import { DEFAULT_LOCALE } from "@/features/i18n/locale"; // 블록 타입 정의: 텍스트/버튼/이미지/비디오/섹션/구분선/리스트/폼 블록/폼 요소 블록 export type BlockType = @@ -914,9 +916,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createHeroTemplateBlocks({ sectionId, createId, + messages: tpl.hero, }); pushHistoryImpl(set, get); @@ -937,11 +941,6 @@ const createEditorState = (set: any, get: any): EditorState => { }); }, - // 리스트 아이템 선택 상태를 업데이트한다. - selectListItem: (itemId) => { - set({ selectedListItemId: itemId ?? null }); - }, - // Features 템플릿 섹션 추가: 3컬럼(4/4/4) 섹션과 각 컬럼의 제목/설명 텍스트를 생성한다 addFeaturesTemplateSection: () => { const { selectedBlockId, blocks } = get(); @@ -956,9 +955,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createFeaturesTemplateBlocks({ sectionId, createId, + messages: tpl.features, }); pushHistoryImpl(set, get); @@ -993,9 +994,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createBlogTemplateBlocks({ sectionId, createId, + messages: tpl.blog, }); pushHistoryImpl(set, get); @@ -1030,9 +1033,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createTeamTemplateBlocks({ sectionId, createId, + messages: tpl.team, }); pushHistoryImpl(set, get); @@ -1067,9 +1072,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createFooterTemplateBlocks({ sectionId, createId, + messages: tpl.footer, }); pushHistoryImpl(set, get); @@ -1104,9 +1111,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createCtaTemplateBlocks({ sectionId, createId, + messages: tpl.cta, }); pushHistoryImpl(set, get); @@ -1141,9 +1150,11 @@ const createEditorState = (set: any, get: any): EditorState => { } } + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); const { blocks: templateBlocks, lastSelectedId } = createFaqTemplateBlocks({ sectionId, createId, + messages: tpl.faq, }); pushHistoryImpl(set, get); @@ -1733,6 +1744,11 @@ const createEditorState = (set: any, get: any): EditorState => { })); }, + // 현재 선택된 리스트 아이템 ID 를 상태에 저장한다. + selectListItem: (itemId) => { + set({ selectedListItemId: itemId }); + }, + // 선택된 리스트 아이템을 들여쓰기한다. indentSelectedListItem: (blockId) => { const { blocks, selectedListItemId } = get(); diff --git a/src/features/i18n/LocaleSwitcher.tsx b/src/features/i18n/LocaleSwitcher.tsx index be6e541..7150396 100644 --- a/src/features/i18n/LocaleSwitcher.tsx +++ b/src/features/i18n/LocaleSwitcher.tsx @@ -7,24 +7,24 @@ export function LocaleSwitcher() { const locale = useAppLocale(); const { setLocale } = useLocaleActions(); - const handleChange = (event: React.ChangeEvent) => { - const nextLocale = event.target.value as AppLocale; + const handleToggle = () => { + const nextLocale: AppLocale = locale === "en" ? "ko" : "en"; setLocale(nextLocale); }; + const labelText = locale === "en" ? "EN" : "KO"; + return (
- +
); } + diff --git a/tests/api/export.spec.ts b/tests/api/export.spec.ts index 82141f0..1fd78e4 100644 --- a/tests/api/export.spec.ts +++ b/tests/api/export.spec.ts @@ -8,6 +8,8 @@ import { createHeroTemplateBlocks } from "@/app/editor/templates/heroTemplate"; import { createFeaturesTemplateBlocks } from "@/app/editor/templates/featuresTemplate"; import { createCtaTemplateBlocks } from "@/app/editor/templates/ctaTemplate"; import { createFooterTemplateBlocks } from "@/app/editor/templates/footerTemplate"; +import { getEditorTemplatesMessages } from "@/features/i18n/messages/editorTemplates"; +import { DEFAULT_LOCALE } from "@/features/i18n/locale"; import { buildStaticHtml } from "@/app/api/export/route"; const BASE_URL = "http://localhost"; @@ -2013,13 +2015,12 @@ describe("/api/export", () => { expect(html).toContain("Pro"); }); - describe("템플릿 섹션 정적 export", () => { - function createIdFactory() { - let i = 0; - return () => `tpl_${++i}`; - } + function createIdFactory() { + let i = 0; + return () => `tpl_${++i}`; + } - async function exportTemplateHtml(blocks: Block[]): Promise { + async function exportTemplateHtml(blocks: Block[]): Promise { const projectConfig: ProjectConfig = { title: "템플릿 export 테스트", slug: "template-export-test", @@ -2050,20 +2051,32 @@ describe("/api/export", () => { return html; } - it("Hero 템플릿 섹션은 export HTML 에 기본 헤드라인/서브텍스트/버튼 텍스트를 포함해야 한다", async () => { + it("Hero 템플릿 섹션은 export HTML 에 헤드라인/서브텍스트/버튼 텍스트를 포함해야 한다", async () => { const sectionId = "hero_section_export"; - const { blocks } = createHeroTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createHeroTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.hero, + }); const html = await exportTemplateHtml(blocks as Block[]); expect(html).toContain("Your hero headline goes here"); - expect(html).toContain("Describe your product or service in a single, clear sentence."); + expect(html).toContain( + "Describe your product or service in a single, clear sentence.", + ); expect(html).toContain("Get started now"); }); it("Features 템플릿 섹션은 export HTML 에 3개의 Feature 제목과 설명 텍스트를 포함해야 한다", async () => { const sectionId = "features_section_export"; - const { blocks } = createFeaturesTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createFeaturesTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.features, + }); const html = await exportTemplateHtml(blocks as Block[]); @@ -2072,126 +2085,46 @@ describe("/api/export", () => { expect(html).toContain("Feature 3"); const descText = "A short description of this feature."; - const occurrences = html.split(descText).length - 1; - expect(occurrences).toBeGreaterThanOrEqual(3); + const count = html.split(descText).length - 1; + expect(count).toBeGreaterThanOrEqual(3); }); - it("CTA 템플릿 섹션은 export HTML 에 CTA 본문 텍스트와 버튼 라벨을 포함해야 한다", async () => { + it("CTA 템플릿 섹션은 export HTML 에 CTA 텍스트와 버튼 라벨을 포함해야 한다", async () => { const sectionId = "cta_section_export"; - const { blocks } = createCtaTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createCtaTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.cta, + }); const html = await exportTemplateHtml(blocks as Block[]); expect(html).toContain("Write a compelling CTA message here."); + // 작은따옴표는 HTML 이스케이프되어 Don't 형태로 렌더된다. + expect(html).toContain("Don't hesitate to get started!"); expect(html).toContain("Call to action"); }); - it("Footer 템플릿 섹션은 export HTML 의 마지막 섹션으로 렌더되고 푸터 텍스트를 포함해야 한다", async () => { + it("Footer 템플릿 섹션은 export HTML 에 푸터 설명과 링크 텍스트를 포함해야 한다", async () => { const sectionId = "footer_section_export"; - const { blocks } = createFooterTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createFooterTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.footer, + }); const html = await exportTemplateHtml(blocks as Block[]); - expect(html).toContain("MyLanding"); expect(html).toContain("The best choice for building better websites."); expect(html).toContain("Product"); + expect(html).toContain("Pricing"); + expect(html).toContain("Support"); expect(html).toContain("Contact"); - expect(html).toContain(" 2025 MyLanding."); - - const lastSectionIndex = html.lastIndexOf(" { - const blocks: Block[] = [ - { - id: "nav_btn_1", - type: "button", - props: { - label: "요금제", - href: "/pricing", - align: "left", - }, - } as any, - ]; - - const projectConfig: ProjectConfig = { - title: "내비게이션 링크 테스트", - slug: "nav-link-test", - canvasPreset: "full", - }; - - const html = buildStaticHtml(blocks, projectConfig); - - expect(html).toContain('요금제"); - }); - }); - - describe("buildStaticHtml", () => { - it("text 블록은 pb 텍스트 스타일 클래스를 포함해야 한다", async () => { - const blocks: Block[] = [ - { - id: "text_styled", - type: "text", - props: { - text: "스타일 테스트", - align: "center", - size: "base", - fontSizeMode: "scale", - fontSizeScale: "lg", - lineHeightMode: "scale", - lineHeightScale: "relaxed", - fontWeightMode: "scale", - fontWeightScale: "semibold", - colorMode: "palette", - colorPalette: "strong", - underline: true, - italic: true, - }, - } as any, - ]; - - const projectConfig: ProjectConfig = { - title: "텍스트 스타일 테스트", - slug: "text-style-test", - canvasPreset: "full", - }; - - const payload = { blocks, projectConfig }; - - const { POST: handleExport } = await import("@/app/api/export/route"); - - const res = await handleExport( - new Request(`${BASE_URL}/api/export`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }), - ); - - expect(res.status).toBe(200); - - const arrayBuffer = await res.arrayBuffer(); - const zip = await JSZip.loadAsync(arrayBuffer); - - const indexEntry = zip.file("index.html"); - expect(indexEntry).toBeTruthy(); - - const html = await indexEntry!.async("string"); - expect(html).toContain("스타일 테스트"); - expect(html).toContain("pb-text-center"); - expect(html).toContain("pb-text-lg"); - expect(html).toContain("pb-leading-relaxed"); - expect(html).toContain("pb-font-semibold"); - expect(html).toContain("pb-text-color-strong"); - expect(html).toContain("pb-underline"); - expect(html).toContain("pb-italic"); - }); - - it("기본 텍스트 블록은 프리뷰처럼 흰 글자와 줄바꿈 유지 스타일을 사용해야 한다", async () => { + it("기본 텍스트 블록은 프리뷰처럼 흰 글자와 줄바꿈 유지 스타일을 사용해야 한다", async () => { const blocks: Block[] = [ { id: "text_default", @@ -3244,4 +3177,3 @@ describe("/api/export", () => { expect(css).toContain(".pb-section-column"); }); }); -}); diff --git a/tests/unit/EditorSectionLayout.spec.tsx b/tests/unit/EditorSectionLayout.spec.tsx index 6755322..6e6b21f 100644 --- a/tests/unit/EditorSectionLayout.spec.tsx +++ b/tests/unit/EditorSectionLayout.spec.tsx @@ -3,6 +3,8 @@ import { render, screen, cleanup } from "@testing-library/react"; import type { Block, ProjectConfig } from "@/features/editor/state/editorStore"; import EditorPage from "@/app/editor/page"; import { createHeroTemplateBlocks } from "@/app/editor/templates/heroTemplate"; +import { getEditorTemplatesMessages } from "@/features/i18n/messages/editorTemplates"; +import { DEFAULT_LOCALE } from "@/features/i18n/locale"; let mockState: any; @@ -191,7 +193,12 @@ describe("EditorPage - 섹션 레이아웃 속성", () => { return () => `hero_${++i}`; })(); - const { blocks } = createHeroTemplateBlocks({ sectionId, createId }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createHeroTemplateBlocks({ + sectionId, + createId, + messages: tpl.hero, + }); mockState.blocks = blocks as Block[]; mockState.selectedBlockId = sectionId; diff --git a/tests/unit/ProjectPropertiesPanel.seo.spec.tsx b/tests/unit/ProjectPropertiesPanel.seo.spec.tsx index 5e815f4..1d7457c 100644 --- a/tests/unit/ProjectPropertiesPanel.seo.spec.tsx +++ b/tests/unit/ProjectPropertiesPanel.seo.spec.tsx @@ -41,7 +41,7 @@ describe("ProjectPropertiesPanel SEO 메타", () => { it("SEO 타이틀 입력을 변경하면 seoTitle 이 updateProjectConfig 로 전달되어야 한다", () => { render(); - const input = screen.getByLabelText("SEO 타이틀") as HTMLInputElement; + const input = screen.getByLabelText("SEO title") as HTMLInputElement; fireEvent.change(input, { target: { value: "새 SEO 타이틀" } }); @@ -51,7 +51,7 @@ describe("ProjectPropertiesPanel SEO 메타", () => { it("메타 디스크립션 입력을 변경하면 seoDescription 이 updateProjectConfig 로 전달되어야 한다", () => { render(); - const textarea = screen.getByLabelText("메타 디스크립션") as HTMLTextAreaElement; + const textarea = screen.getByLabelText("Meta description") as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "SEO 설명" } }); @@ -63,7 +63,7 @@ describe("ProjectPropertiesPanel SEO 메타", () => { it("OG/Twitter 이미지 URL 입력을 변경하면 seoOgImageUrl 이 updateProjectConfig 로 전달되어야 한다", () => { render(); - const input = screen.getByLabelText("OG/Twitter 이미지 URL") as HTMLInputElement; + const input = screen.getByLabelText("OG/Twitter image URL") as HTMLInputElement; fireEvent.change(input, { target: { value: "https://example.com/og.png" } }); @@ -88,7 +88,7 @@ describe("ProjectPropertiesPanel SEO 메타", () => { render(); const checkbox = screen.getByLabelText( - "검색 엔진에 노출하지 않기 (noindex)", + "Hide from search engines (noindex)", ) as HTMLInputElement; expect(checkbox.checked).toBe(false); diff --git a/tests/unit/PublicPageRendererFooter.spec.tsx b/tests/unit/PublicPageRendererFooter.spec.tsx index ed657b3..f08bc97 100644 --- a/tests/unit/PublicPageRendererFooter.spec.tsx +++ b/tests/unit/PublicPageRendererFooter.spec.tsx @@ -3,6 +3,8 @@ import { render, screen, cleanup } from "@testing-library/react"; import { PublicPageRenderer } from "@/features/editor/components/PublicPageRenderer"; import { createFooterTemplateBlocks } from "@/app/editor/templates/footerTemplate"; import type { Block } from "@/features/editor/state/editorStore"; +import { getEditorTemplatesMessages } from "@/features/i18n/messages/editorTemplates"; +import { DEFAULT_LOCALE } from "@/features/i18n/locale"; // PublicPageRenderer 푸터 템플릿 TDD // - footerTemplate 로 생성한 섹션이 프리뷰에서 섹션/텍스트 구조로 올바르게 렌더되는지 검증한다. @@ -16,8 +18,8 @@ describe("PublicPageRenderer - 푸터 템플릿", () => { const sectionId = "footer_preview_section"; let i = 0; const createId = () => `footer_${++i}`; - - const { blocks } = createFooterTemplateBlocks({ sectionId, createId }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createFooterTemplateBlocks({ sectionId, createId, messages: tpl.footer }); render(); @@ -25,9 +27,9 @@ describe("PublicPageRenderer - 푸터 템플릿", () => { expect(section.getAttribute("data-section-id")).toBe(sectionId); expect(screen.getByText("MyLanding")).toBeTruthy(); - expect(screen.getByText("더 나은 웹사이트를 위한 최고의 선택.")).toBeTruthy(); - expect(screen.getByText(/서비스 소개/)).toBeTruthy(); - expect(screen.getByText(/문의하기/)).toBeTruthy(); - expect(screen.getByText(/© 2025 MyLanding\./)).toBeTruthy(); + expect(screen.getByText("The best choice for building better websites.")).toBeTruthy(); + expect(screen.getByText(/Product/)).toBeTruthy(); + expect(screen.getByText(/Contact/)).toBeTruthy(); + expect(screen.getByText(/ 2025 MyLanding\./)).toBeTruthy(); }); }); diff --git a/tests/unit/PublicPageRendererTemplates.spec.tsx b/tests/unit/PublicPageRendererTemplates.spec.tsx index b7b6c8c..0d8ceae 100644 --- a/tests/unit/PublicPageRendererTemplates.spec.tsx +++ b/tests/unit/PublicPageRendererTemplates.spec.tsx @@ -4,6 +4,8 @@ import { PublicPageRenderer } from "@/features/editor/components/PublicPageRende import { createHeroTemplateBlocks } from "@/app/editor/templates/heroTemplate"; import { createFeaturesTemplateBlocks } from "@/app/editor/templates/featuresTemplate"; import { createCtaTemplateBlocks } from "@/app/editor/templates/ctaTemplate"; +import { getEditorTemplatesMessages } from "@/features/i18n/messages/editorTemplates"; +import { DEFAULT_LOCALE } from "@/features/i18n/locale"; import type { Block } from "@/features/editor/state/editorStore"; // PublicPageRenderer 템플릿 TDD @@ -21,7 +23,12 @@ describe("PublicPageRenderer - 섹션 템플릿", () => { it("Hero 템플릿 섹션은 섹션과 헤드라인/서브텍스트/버튼을 렌더해야 한다", () => { const sectionId = "hero_section_1"; - const { blocks } = createHeroTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createHeroTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.hero, + }); render(); @@ -34,7 +41,12 @@ describe("PublicPageRenderer - 섹션 템플릿", () => { it("Features 템플릿 섹션은 3컬럼 Feature 제목/설명 텍스트를 렌더해야 한다", () => { const sectionId = "features_section_1"; - const { blocks } = createFeaturesTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createFeaturesTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.features, + }); render(); @@ -53,7 +65,12 @@ describe("PublicPageRenderer - 섹션 템플릿", () => { it("CTA 템플릿 섹션은 텍스트와 CTA 버튼을 렌더해야 한다", () => { const sectionId = "cta_section_1"; - const { blocks } = createCtaTemplateBlocks({ sectionId, createId: createIdFactory() }); + const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE); + const { blocks } = createCtaTemplateBlocks({ + sectionId, + createId: createIdFactory(), + messages: tpl.cta, + }); render(); diff --git a/uploads/3cd68ac4-d85d-44d3-8e68-97086b341895 b/uploads/3cd68ac4-d85d-44d3-8e68-97086b341895 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/3cd68ac4-d85d-44d3-8e68-97086b341895 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/43987d79-2417-4bdf-bb00-c064f948115c b/uploads/43987d79-2417-4bdf-bb00-c064f948115c new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/43987d79-2417-4bdf-bb00-c064f948115c @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/5abc6415-e25d-4a8c-a791-f3cb6cf1263d b/uploads/5abc6415-e25d-4a8c-a791-f3cb6cf1263d new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/5abc6415-e25d-4a8c-a791-f3cb6cf1263d @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/fbf099b4-65c8-4db6-92ab-7f7f494016a9 b/uploads/fbf099b4-65c8-4db6-92ab-7f7f494016a9 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/fbf099b4-65c8-4db6-92ab-7f7f494016a9 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/test-image-1765353749297 b/uploads/test-image-1765353749297 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-image-1765353749297 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-image-1765353782946 b/uploads/test-image-1765353782946 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-image-1765353782946 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-section-bg-1765353749344 b/uploads/test-section-bg-1765353749344 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-section-bg-1765353749344 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-section-bg-1765353782975 b/uploads/test-section-bg-1765353782975 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-section-bg-1765353782975 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-section-bg-video-1765353749481 b/uploads/test-section-bg-video-1765353749481 new file mode 100644 index 0000000..badc796 --- /dev/null +++ b/uploads/test-section-bg-video-1765353749481 @@ -0,0 +1 @@ +dummy-section-video \ No newline at end of file diff --git a/uploads/test-section-bg-video-1765353783013 b/uploads/test-section-bg-video-1765353783013 new file mode 100644 index 0000000..badc796 --- /dev/null +++ b/uploads/test-section-bg-video-1765353783013 @@ -0,0 +1 @@ +dummy-section-video \ No newline at end of file diff --git a/uploads/test-video-1765353749466 b/uploads/test-video-1765353749466 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/test-video-1765353749466 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/test-video-1765353783005 b/uploads/test-video-1765353783005 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/test-video-1765353783005 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/test-video-poster-1765353749304 b/uploads/test-video-poster-1765353749304 new file mode 100644 index 0000000..7285c92 --- /dev/null +++ b/uploads/test-video-poster-1765353749304 @@ -0,0 +1 @@ +dummy-poster \ No newline at end of file diff --git a/uploads/test-video-poster-1765353782956 b/uploads/test-video-poster-1765353782956 new file mode 100644 index 0000000..7285c92 --- /dev/null +++ b/uploads/test-video-poster-1765353782956 @@ -0,0 +1 @@ +dummy-poster \ No newline at end of file