CI: feature/i18n-browser-locale #34

Merged
jaybe merged 3 commits from feature/i18n-browser-locale into main 2025-12-10 08:11:49 +00:00
28 changed files with 167 additions and 192 deletions
Showing only changes of commit 017d5f128e - Show all commits
+4 -8
View File
@@ -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();
+5 -3
View File
@@ -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",
+6 -19
View File
@@ -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();
+5 -3
View File
@@ -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;
+5 -3
View File
@@ -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",
+6 -4
View File
@@ -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",
+4 -8
View File
@@ -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();
+21 -5
View File
@@ -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();
+12 -12
View File
@@ -7,24 +7,24 @@ export function LocaleSwitcher() {
const locale = useAppLocale();
const { setLocale } = useLocaleActions();
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<div className="fixed bottom-4 right-4 z-50 text-xs">
<label className="inline-flex items-center gap-1 rounded border border-slate-300 bg-white/80 px-2 py-1 text-slate-700 shadow-sm backdrop-blur dark:border-slate-700 dark:bg-slate-900/80 dark:text-slate-100">
<span>Language</span>
<select
className="bg-transparent text-xs outline-none"
value={locale}
onChange={handleChange}
<button
type="button"
aria-label="Toggle locale"
className="inline-flex items-center justify-center rounded-full border border-slate-300 bg-white/80 px-2 py-1 text-slate-700 shadow-sm backdrop-blur dark:border-slate-700 dark:bg-slate-900/80 dark:text-slate-100"
onClick={handleToggle}
>
<option value="en">English</option>
<option value="ko"></option>
</select>
</label>
{labelText}
</button>
</div>
);
}
+38 -106
View File
@@ -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,7 +2015,6 @@ describe("/api/export", () => {
expect(html).toContain("Pro");
});
describe("템플릿 섹션 정적 export", () => {
function createIdFactory() {
let i = 0;
return () => `tpl_${++i}`;
@@ -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,123 +2085,43 @@ 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&#39;t 형태로 렌더된다.
expect(html).toContain("Don&#39;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("<section");
expect(lastSectionIndex).toBeGreaterThan(-1);
const lastSectionHtml = html.slice(lastSectionIndex);
expect(lastSectionHtml).toContain(" 2025 MyLanding.");
});
it("루트 버튼 내비게이션 링크는 Export HTML 에서 동일한 라벨과 href 로 렌더되어야 한다", async () => {
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('<a href="/pricing"');
expect(html).toContain(">요금제</a>");
});
});
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 () => {
@@ -3244,4 +3177,3 @@ describe("/api/export", () => {
expect(css).toContain(".pb-section-column");
});
});
});
+8 -1
View File
@@ -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;
@@ -41,7 +41,7 @@ describe("ProjectPropertiesPanel SEO 메타", () => {
it("SEO 타이틀 입력을 변경하면 seoTitle 이 updateProjectConfig 로 전달되어야 한다", () => {
render(<ProjectPropertiesPanel />);
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(<ProjectPropertiesPanel />);
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(<ProjectPropertiesPanel />);
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(<ProjectPropertiesPanel />);
const checkbox = screen.getByLabelText(
"검색 엔진에 노출하지 않기 (noindex)",
"Hide from search engines (noindex)",
) as HTMLInputElement;
expect(checkbox.checked).toBe(false);
+8 -6
View File
@@ -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(<PublicPageRenderer blocks={blocks as Block[]} />);
@@ -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();
});
});
@@ -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(<PublicPageRenderer blocks={blocks as Block[]} />);
@@ -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(<PublicPageRenderer blocks={blocks as Block[]} />);
@@ -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(<PublicPageRenderer blocks={blocks as Block[]} />);
@@ -0,0 +1 @@
dummy-video
@@ -0,0 +1 @@
dummy-video
@@ -0,0 +1 @@
dummy-video
@@ -0,0 +1 @@
dummy-video
+1
View File
@@ -0,0 +1 @@
dummy-image
+1
View File
@@ -0,0 +1 @@
dummy-image
+1
View File
@@ -0,0 +1 @@
dummy-image
+1
View File
@@ -0,0 +1 @@
dummy-image
@@ -0,0 +1 @@
dummy-section-video
@@ -0,0 +1 @@
dummy-section-video
+1
View File
@@ -0,0 +1 @@
dummy-video
+1
View File
@@ -0,0 +1 @@
dummy-video
+1
View File
@@ -0,0 +1 @@
dummy-poster
+1
View File
@@ -0,0 +1 @@
dummy-poster