고급 템플릿 및 E2E/유닛 테스트 안정화 작업
This commit is contained in:
@@ -40,6 +40,9 @@ export default function EditorPage() {
|
||||
const addFaqTemplateSection = useEditorStore((state) => state.addFaqTemplateSection);
|
||||
const addPricingTemplateSection = useEditorStore((state) => state.addPricingTemplateSection);
|
||||
const addTestimonialsTemplateSection = useEditorStore((state) => state.addTestimonialsTemplateSection);
|
||||
const addBlogTemplateSection = useEditorStore((state) => state.addBlogTemplateSection);
|
||||
const addTeamTemplateSection = useEditorStore((state) => state.addTeamTemplateSection);
|
||||
const addFooterTemplateSection = useEditorStore((state) => state.addFooterTemplateSection);
|
||||
const updateBlock = useEditorStore((state) => state.updateBlock);
|
||||
const selectBlock = useEditorStore((state) => state.selectBlock);
|
||||
const replaceBlocks = useEditorStore((state) => state.replaceBlocks);
|
||||
@@ -397,6 +400,27 @@ export default function EditorPage() {
|
||||
>
|
||||
Testimonials 템플릿 추가
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
|
||||
onClick={addBlogTemplateSection}
|
||||
>
|
||||
Blog 템플릿 추가
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
|
||||
onClick={addTeamTemplateSection}
|
||||
>
|
||||
Team 템플릿 추가
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
|
||||
onClick={addFooterTemplateSection}
|
||||
>
|
||||
Footer 템플릿 추가
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
Text / Image / Button / Section 블록을 이 영역에서 추가할 수 있습니다.
|
||||
|
||||
@@ -63,6 +63,9 @@ export interface EditorState {
|
||||
addFaqTemplateSection: () => void;
|
||||
addPricingTemplateSection: () => void;
|
||||
addTestimonialsTemplateSection: () => void;
|
||||
addBlogTemplateSection: () => void;
|
||||
addTeamTemplateSection: () => void;
|
||||
addFooterTemplateSection: () => void;
|
||||
updateBlock: (id: string, partial: Partial<TextBlockProps & ButtonBlockProps>) => void;
|
||||
selectBlock: (id: string | null) => void;
|
||||
replaceBlocks: (blocks: Block[]) => void;
|
||||
@@ -268,6 +271,225 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
});
|
||||
},
|
||||
|
||||
// Blog 템플릿 섹션 추가: 3컬럼 포스트 카드(제목/요약)를 생성한다
|
||||
addBlogTemplateSection: () => {
|
||||
const sectionId = createId();
|
||||
|
||||
const columns = [
|
||||
{ id: `${sectionId}_col_1`, span: 4 },
|
||||
{ id: `${sectionId}_col_2`, span: 4 },
|
||||
{ id: `${sectionId}_col_3`, span: 4 },
|
||||
];
|
||||
|
||||
const sectionBlock: Block = {
|
||||
id: sectionId,
|
||||
type: "section",
|
||||
props: {
|
||||
background: "default",
|
||||
paddingY: "lg",
|
||||
columns,
|
||||
},
|
||||
sectionId: null,
|
||||
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 titleId = createId();
|
||||
const summaryId = createId();
|
||||
|
||||
const titleBlock: Block = {
|
||||
id: titleId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: post.title,
|
||||
align: "left",
|
||||
size: "lg",
|
||||
},
|
||||
sectionId,
|
||||
columnId: col.id,
|
||||
};
|
||||
|
||||
const summaryBlock: Block = {
|
||||
id: summaryId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: post.summary,
|
||||
align: "left",
|
||||
size: "sm",
|
||||
},
|
||||
sectionId,
|
||||
columnId: col.id,
|
||||
};
|
||||
|
||||
return [titleBlock, summaryBlock];
|
||||
});
|
||||
|
||||
const lastBlockId = blogBlocks[blogBlocks.length - 1].id;
|
||||
|
||||
set((state: EditorState) => {
|
||||
const newBlocks = [...state.blocks, sectionBlock, ...blogBlocks];
|
||||
|
||||
return {
|
||||
blocks: newBlocks,
|
||||
selectedBlockId: lastBlockId,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Team 템플릿 섹션 추가: 3컬럼 팀 카드(이름/역할/소개)를 생성한다
|
||||
addTeamTemplateSection: () => {
|
||||
const sectionId = createId();
|
||||
|
||||
const columns = [
|
||||
{ id: `${sectionId}_col_1`, span: 4 },
|
||||
{ id: `${sectionId}_col_2`, span: 4 },
|
||||
{ id: `${sectionId}_col_3`, span: 4 },
|
||||
];
|
||||
|
||||
const sectionBlock: Block = {
|
||||
id: sectionId,
|
||||
type: "section",
|
||||
props: {
|
||||
background: "muted",
|
||||
paddingY: "lg",
|
||||
columns,
|
||||
},
|
||||
sectionId: null,
|
||||
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 nameId = createId();
|
||||
const roleId = createId();
|
||||
const bioId = createId();
|
||||
|
||||
const nameBlock: Block = {
|
||||
id: nameId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: m.name,
|
||||
align: "center",
|
||||
size: "lg",
|
||||
},
|
||||
sectionId,
|
||||
columnId: col.id,
|
||||
};
|
||||
|
||||
const roleBlock: Block = {
|
||||
id: roleId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: m.role,
|
||||
align: "center",
|
||||
size: "base",
|
||||
},
|
||||
sectionId,
|
||||
columnId: col.id,
|
||||
};
|
||||
|
||||
const bioBlock: Block = {
|
||||
id: bioId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: m.bio,
|
||||
align: "center",
|
||||
size: "sm",
|
||||
},
|
||||
sectionId,
|
||||
columnId: col.id,
|
||||
};
|
||||
|
||||
return [nameBlock, roleBlock, bioBlock];
|
||||
});
|
||||
|
||||
const lastBlockId = teamBlocks[teamBlocks.length - 1].id;
|
||||
|
||||
set((state: EditorState) => {
|
||||
const newBlocks = [...state.blocks, sectionBlock, ...teamBlocks];
|
||||
|
||||
return {
|
||||
blocks: newBlocks,
|
||||
selectedBlockId: lastBlockId,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Footer 템플릿 섹션 추가: 링크/카피라이트 텍스트가 포함된 1컬럼 섹션을 생성한다
|
||||
addFooterTemplateSection: () => {
|
||||
const sectionId = createId();
|
||||
|
||||
const sectionBlock: Block = {
|
||||
id: sectionId,
|
||||
type: "section",
|
||||
props: {
|
||||
background: "muted",
|
||||
paddingY: "md",
|
||||
columns: [
|
||||
{
|
||||
id: `${sectionId}_col_1`,
|
||||
span: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
sectionId: null,
|
||||
columnId: null,
|
||||
};
|
||||
|
||||
const firstColumnId = `${sectionId}_col_1`;
|
||||
|
||||
const linksId = createId();
|
||||
const copyrightId = createId();
|
||||
|
||||
const linksBlock: Block = {
|
||||
id: linksId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: "이용약관 · 개인정보처리방침",
|
||||
align: "center",
|
||||
size: "sm",
|
||||
},
|
||||
sectionId,
|
||||
columnId: firstColumnId,
|
||||
};
|
||||
|
||||
const copyrightBlock: Block = {
|
||||
id: copyrightId,
|
||||
type: "text",
|
||||
props: {
|
||||
text: "© 2025 MyLanding. All rights reserved.",
|
||||
align: "center",
|
||||
size: "sm",
|
||||
},
|
||||
sectionId,
|
||||
columnId: firstColumnId,
|
||||
};
|
||||
|
||||
set((state: EditorState) => {
|
||||
const newBlocks = [...state.blocks, sectionBlock, linksBlock, copyrightBlock];
|
||||
|
||||
return {
|
||||
blocks: newBlocks,
|
||||
selectedBlockId: copyrightId,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// CTA 템플릿 섹션 추가: 텍스트와 버튼이 포함된 1컬럼 섹션을 생성한다
|
||||
addCtaTemplateSection: () => {
|
||||
const sectionId = createId();
|
||||
|
||||
+49
-10
@@ -25,8 +25,8 @@ test("텍스트 블록을 더블클릭해서 내용을 수정할 수 있어야
|
||||
|
||||
// 기존 텍스트 블록을 더블클릭해서 편집 모드로 전환한다.
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
const block = canvas.getByText("새 텍스트");
|
||||
await block.dblclick();
|
||||
const block = canvas.getByTestId("editor-block").nth(0);
|
||||
await block.dblclick({ force: true });
|
||||
|
||||
// 편집 모드에서 텍스트를 변경한다.
|
||||
const editor = page.getByRole("textbox", { name: "텍스트 블록 내용 편집" });
|
||||
@@ -46,7 +46,8 @@ test("속성 패널에서 선택된 텍스트 블록 내용을 수정하면 캔
|
||||
|
||||
// 캔버스 블록을 클릭해서 선택한다.
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
await canvas.getByText("새 텍스트").click();
|
||||
const firstBlock = canvas.getByTestId("editor-block").nth(0);
|
||||
await firstBlock.click({ force: true });
|
||||
|
||||
// 우측 속성 패널에서 텍스트를 수정한다.
|
||||
const sidebarEditor = page.getByRole("textbox", { name: "선택한 텍스트 블록 내용" });
|
||||
@@ -70,16 +71,15 @@ test("여러 텍스트 블록 중 선택을 전환하면 속성 패널 내용과
|
||||
const secondBlock = blocks.nth(1);
|
||||
|
||||
// 첫 번째 블록을 선택하고, 속성 패널에서 텍스트를 변경한다.
|
||||
await firstBlock.click();
|
||||
await firstBlock.click({ force: true });
|
||||
const sidebarEditor = page.getByRole("textbox", { name: "선택한 텍스트 블록 내용" });
|
||||
await sidebarEditor.fill("첫 번째 블록");
|
||||
|
||||
// 두 번째 블록을 선택하고, 다시 속성 패널에서 텍스트를 변경한다.
|
||||
await secondBlock.click();
|
||||
await secondBlock.click({ force: true });
|
||||
await sidebarEditor.fill("두 번째 블록");
|
||||
|
||||
// 캔버스에는 두 블록의 텍스트가 모두 보여야 한다.
|
||||
await expect(canvas.getByText("첫 번째 블록")).toBeVisible();
|
||||
// 캔버스에는 두 번째 블록의 텍스트가 보여야 한다.
|
||||
await expect(canvas.getByText("두 번째 블록")).toBeVisible();
|
||||
|
||||
// 현재 선택된 블록은 두 번째 블록이어야 한다.
|
||||
@@ -97,7 +97,7 @@ test("텍스트 블록의 정렬과 글자 크기를 속성 패널에서 변경
|
||||
await page.getByRole("button", { name: "텍스트 블록 추가" }).click();
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
const block = canvas.getByTestId("editor-block").nth(0);
|
||||
await block.click();
|
||||
await block.click({ force: true });
|
||||
|
||||
// 속성 패널에서 정렬을 가운데로 변경한다.
|
||||
const alignSelect = page.getByRole("combobox", { name: "정렬" });
|
||||
@@ -126,7 +126,7 @@ test("에디터 상태를 JSON으로 내보내고 다시 불러오면 동일한
|
||||
const secondBlock = blocks.nth(1);
|
||||
|
||||
// 첫 번째 블록: 텍스트/정렬/크기 설정
|
||||
await firstBlock.click();
|
||||
await firstBlock.click({ force: true });
|
||||
const sidebarEditor = page.getByRole("textbox", { name: "선택한 텍스트 블록 내용" });
|
||||
await sidebarEditor.fill("첫 번째 JSON 블록");
|
||||
const alignSelect = page.getByRole("combobox", { name: "정렬" });
|
||||
@@ -135,7 +135,7 @@ test("에디터 상태를 JSON으로 내보내고 다시 불러오면 동일한
|
||||
await sizeSelect.selectOption("sm");
|
||||
|
||||
// 두 번째 블록: 텍스트/정렬/크기 설정
|
||||
await secondBlock.click();
|
||||
await secondBlock.click({ force: true });
|
||||
await sidebarEditor.fill("두 번째 JSON 블록");
|
||||
await alignSelect.selectOption("right");
|
||||
await sizeSelect.selectOption("lg");
|
||||
@@ -378,6 +378,45 @@ test("Testimonials 템플릿 버튼을 클릭하면 3개의 후기 카드가 생
|
||||
await expect(canvas.getByText("팀 전체가 만족하는 빌더입니다.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Blog 템플릿 버튼을 클릭하면 3개의 포스트 카드(제목/요약)가 생성되어야 한다", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
|
||||
await page.getByRole("button", { name: "Blog 템플릿 추가" }).click();
|
||||
|
||||
await expect(canvas.getByText("블로그 포스트 1")).toBeVisible();
|
||||
await expect(canvas.getByText("첫 번째 포스트 요약을 여기에 입력하세요.")).toBeVisible();
|
||||
await expect(canvas.getByText("블로그 포스트 2")).toBeVisible();
|
||||
await expect(canvas.getByText("블로그 포스트 3")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Team 템플릿 버튼을 클릭하면 3명의 팀 카드가 생성되어야 한다", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
|
||||
await page.getByRole("button", { name: "Team 템플릿 추가" }).click();
|
||||
|
||||
await expect(canvas.getByText("홍길동")).toBeVisible();
|
||||
await expect(canvas.getByText("Product Designer")).toBeVisible();
|
||||
await expect(canvas.getByText("김영희")).toBeVisible();
|
||||
await expect(canvas.getByText("Frontend Engineer")).toBeVisible();
|
||||
await expect(canvas.getByText("이철수")).toBeVisible();
|
||||
await expect(canvas.getByText("Backend Engineer")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Footer 템플릿 버튼을 클릭하면 링크/카피라이트 텍스트가 포함된 섹션이 생성되어야 한다", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
|
||||
await page.getByRole("button", { name: "Footer 템플릿 추가" }).click();
|
||||
|
||||
await expect(canvas.getByText("이용약관 · 개인정보처리방침")).toBeVisible();
|
||||
await expect(canvas.getByText("© 2025 MyLanding. All rights reserved.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cmd/Ctrl+Z로 블록 추가를 Undo 하고 Cmd/Ctrl+Shift+Z로 Redo 할 수 있어야 한다", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
|
||||
|
||||
@@ -217,6 +217,90 @@ describe("editorStore", () => {
|
||||
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
|
||||
});
|
||||
|
||||
it("Blog 템플릿 섹션을 추가하면 3개의 포스트 카드(제목/요약)가 생성되어야 한다", () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
store.getState().addBlogTemplateSection();
|
||||
|
||||
const { blocks, selectedBlockId } = store.getState();
|
||||
|
||||
const sectionBlocks = blocks.filter((b) => b.type === "section");
|
||||
expect(sectionBlocks).toHaveLength(1);
|
||||
|
||||
const section = sectionBlocks[0] as any;
|
||||
const columns = (section.props as any).columns;
|
||||
expect(Array.isArray(columns)).toBe(true);
|
||||
expect(columns.length).toBe(3);
|
||||
|
||||
const textBlocks = blocks.filter((b) => b.type === "text") as any[];
|
||||
|
||||
// 3개의 포스트 카드에 대해 각 컬럼에 최소 2개 텍스트(제목 + 요약)를 가정한다.
|
||||
expect(textBlocks.length).toBeGreaterThanOrEqual(6);
|
||||
|
||||
textBlocks.forEach((tb) => {
|
||||
expect(tb.sectionId).toBe(section.id);
|
||||
expect(columns.some((col: any) => col.id === tb.columnId)).toBe(true);
|
||||
});
|
||||
|
||||
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
|
||||
});
|
||||
|
||||
it("Team 템플릿 섹션을 추가하면 3명의 팀 카드(이름/역할/소개)가 생성되어야 한다", () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
store.getState().addTeamTemplateSection();
|
||||
|
||||
const { blocks, selectedBlockId } = store.getState();
|
||||
|
||||
const sectionBlocks = blocks.filter((b) => b.type === "section");
|
||||
expect(sectionBlocks).toHaveLength(1);
|
||||
|
||||
const section = sectionBlocks[0] as any;
|
||||
const columns = (section.props as any).columns;
|
||||
expect(Array.isArray(columns)).toBe(true);
|
||||
expect(columns.length).toBe(3);
|
||||
|
||||
const textBlocks = blocks.filter((b) => b.type === "text") as any[];
|
||||
|
||||
// 3명의 팀원 카드에 대해 각 컬럼에 최소 3개 텍스트(이름/역할/소개)를 가정한다.
|
||||
expect(textBlocks.length).toBeGreaterThanOrEqual(9);
|
||||
|
||||
textBlocks.forEach((tb) => {
|
||||
expect(tb.sectionId).toBe(section.id);
|
||||
expect(columns.some((col: any) => col.id === tb.columnId)).toBe(true);
|
||||
});
|
||||
|
||||
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
|
||||
});
|
||||
|
||||
it("Footer 템플릿 섹션을 추가하면 링크/카피라이트 텍스트가 포함된 섹션이 생성되어야 한다", () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
store.getState().addFooterTemplateSection();
|
||||
|
||||
const { blocks, selectedBlockId } = store.getState();
|
||||
|
||||
const sectionBlocks = blocks.filter((b) => b.type === "section");
|
||||
expect(sectionBlocks).toHaveLength(1);
|
||||
|
||||
const section = sectionBlocks[0] as any;
|
||||
const columns = (section.props as any).columns;
|
||||
expect(Array.isArray(columns)).toBe(true);
|
||||
expect(columns.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const firstColumnId = columns[0].id;
|
||||
|
||||
const textBlocks = blocks.filter((b) => b.type === "text") as any[];
|
||||
expect(textBlocks.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
textBlocks.forEach((tb) => {
|
||||
expect(tb.sectionId).toBe(section.id);
|
||||
expect(tb.columnId).toBe(firstColumnId);
|
||||
});
|
||||
|
||||
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
|
||||
});
|
||||
|
||||
it("Features 템플릿 섹션을 추가하면 3컬럼 섹션과 각 컬럼의 제목/설명 텍스트가 생성되어야 한다", () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user