1차 싱크 완료
CI / test (push) Failing after 41m13s
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-11-24 21:32:37 +09:00
parent 7a8ad7c057
commit 672cca5271
83 changed files with 6681 additions and 325 deletions
+220
View File
@@ -708,6 +708,161 @@ describe("editorStore", () => {
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
});
it("선택된 섹션이 있을 때 Team 템플릿 섹션 액션을 호출하면 해당 섹션이 새 Team 템플릿으로 교체되어야 한다", () => {
const store = createEditorStore();
store.getState().addTeamTemplateSection();
let { blocks, selectedBlockId } = store.getState();
const sectionBlocks = blocks.filter((b) => b.type === "section");
expect(sectionBlocks).toHaveLength(1);
const section = sectionBlocks[0] as any;
const sectionId = section.id;
const oldTextIds = blocks.filter((b) => b.type === "text").map((b) => b.id);
const originalLastSelectedId = selectedBlockId;
store.getState().selectBlock(sectionId);
store.getState().addTeamTemplateSection();
({ blocks, selectedBlockId } = store.getState());
const sectionBlocksAfter = blocks.filter((b) => b.type === "section");
expect(sectionBlocksAfter).toHaveLength(1);
const replacedSection = sectionBlocksAfter[0] as any;
expect(replacedSection.id).toBe(sectionId);
const textBlocksAfter = blocks.filter((b) => b.type === "text") as any[];
expect(textBlocksAfter.length).toBeGreaterThanOrEqual(1);
textBlocksAfter.forEach((tb) => {
expect(tb.sectionId).toBe(sectionId);
});
const allIdsAfter = blocks.map((b) => b.id);
oldTextIds.forEach((id) => {
expect(allIdsAfter).not.toContain(id);
});
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
expect(selectedBlockId).not.toBe(originalLastSelectedId);
});
it("선택된 섹션이 있을 때 Blog 템플릿 섹션 액션을 호출하면 해당 섹션이 새 Blog 템플릿으로 교체되어야 한다", () => {
const store = createEditorStore();
store.getState().addBlogTemplateSection();
let { blocks, selectedBlockId } = store.getState();
const sectionBlocks = blocks.filter((b) => b.type === "section");
expect(sectionBlocks).toHaveLength(1);
const section = sectionBlocks[0] as any;
const sectionId = section.id;
const oldTextIds = blocks.filter((b) => b.type === "text").map((b) => b.id);
const originalLastSelectedId = selectedBlockId;
store.getState().selectBlock(sectionId);
store.getState().addBlogTemplateSection();
({ blocks, selectedBlockId } = store.getState());
const sectionBlocksAfter = blocks.filter((b) => b.type === "section");
expect(sectionBlocksAfter).toHaveLength(1);
const replacedSection = sectionBlocksAfter[0] as any;
expect(replacedSection.id).toBe(sectionId);
const textBlocksAfter = blocks.filter((b) => b.type === "text") as any[];
expect(textBlocksAfter.length).toBeGreaterThanOrEqual(1);
textBlocksAfter.forEach((tb) => {
expect(tb.sectionId).toBe(sectionId);
});
const allIdsAfter = blocks.map((b) => b.id);
oldTextIds.forEach((id) => {
expect(allIdsAfter).not.toContain(id);
});
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
expect(selectedBlockId).not.toBe(originalLastSelectedId);
});
it("선택된 섹션이 있을 때 Hero 템플릿 섹션 액션을 호출하면 해당 섹션이 새 Hero 템플릿으로 교체되어야 한다", () => {
const store = createEditorStore();
// 1) 먼저 Hero 템플릿 섹션을 하나 추가한다.
store.getState().addHeroTemplateSection();
let { blocks, selectedBlockId } = store.getState();
const sectionBlocks = blocks.filter((b) => b.type === "section");
expect(sectionBlocks).toHaveLength(1);
const section = sectionBlocks[0] as any;
const sectionId = section.id;
// 기존 텍스트/버튼 블록들의 id 를 기록해 둔다.
const oldTextIds = blocks.filter((b) => b.type === "text").map((b) => b.id);
const oldButtonIds = blocks.filter((b) => b.type === "button").map((b) => b.id);
const originalLastSelectedId = selectedBlockId;
// 2) 해당 섹션을 선택한 상태에서 다시 Hero 템플릿 섹션 액션을 호출한다.
store.getState().selectBlock(sectionId);
store.getState().addHeroTemplateSection();
({ blocks, selectedBlockId } = store.getState());
const sectionBlocksAfter = blocks.filter((b) => b.type === "section");
expect(sectionBlocksAfter).toHaveLength(1);
const replacedSection = sectionBlocksAfter[0] as any;
// 교체 후에도 섹션 id 는 동일하게 유지되어야 한다 (섹션 컨테이너 재사용).
expect(replacedSection.id).toBe(sectionId);
const columns = (replacedSection.props as any).columns;
expect(Array.isArray(columns)).toBe(true);
expect(columns.length).toBeGreaterThanOrEqual(1);
const firstColumnId = columns[0].id;
const textBlocksAfter = blocks.filter((b) => b.type === "text") as any[];
const buttonBlocksAfter = blocks.filter((b) => b.type === "button") as any[];
// 새 텍스트/버튼 블록이 최소 1개 이상 존재해야 한다.
expect(textBlocksAfter.length).toBeGreaterThanOrEqual(1);
expect(buttonBlocksAfter.length).toBeGreaterThanOrEqual(1);
// 새 텍스트/버튼 블록은 모두 동일 섹션의 첫 컬럼에 배치되어야 한다.
textBlocksAfter.forEach((tb) => {
expect(tb.sectionId).toBe(sectionId);
expect(tb.columnId).toBe(firstColumnId);
});
buttonBlocksAfter.forEach((bb) => {
expect(bb.sectionId).toBe(sectionId);
expect(bb.columnId).toBe(firstColumnId);
});
// 기존 텍스트/버튼 블록 id 들은 모두 제거되어야 한다.
const allIdsAfter = blocks.map((b) => b.id);
[...oldTextIds, ...oldButtonIds].forEach((id) => {
expect(allIdsAfter).not.toContain(id);
});
// 선택 상태는 새로 생성된 템플릿의 마지막 블록을 가리켜야 한다.
expect(selectedBlockId).toBe(blocks[blocks.length - 1].id);
expect(selectedBlockId).not.toBe(originalLastSelectedId);
});
it("Blog 템플릿 섹션을 추가하면 3개의 포스트 카드(제목/요약)가 생성되어야 한다", () => {
const store = createEditorStore();
@@ -1191,6 +1346,71 @@ describe("editorStore", () => {
expect(updatedFormProps.submitButtonId).toBe(submitButtonId);
});
it("addHeroTemplateSection 호출 시 Hero 템플릿 섹션과 블록들이 추가되고 마지막 블록이 선택되어야 한다", () => {
const store = createEditorStore();
const stateAny = store.getState() as any;
stateAny.addHeroTemplateSection();
const { blocks, selectedBlockId } = store.getState();
expect(blocks.length).toBeGreaterThanOrEqual(4);
const section = blocks.find((b) => b.type === "section");
expect(section).toBeTruthy();
const heroBlocks = blocks.filter((b) => b.sectionId === section!.id);
// 헤드라인/서브텍스트/버튼 3개가 섹션 내부에 있어야 한다.
expect(heroBlocks).toHaveLength(3);
expect(heroBlocks.map((b) => b.type)).toEqual(["text", "text", "button"]);
// 마지막 블록이 선택 상태여야 한다.
const lastHeroBlock = heroBlocks[heroBlocks.length - 1];
expect(selectedBlockId).toBe(lastHeroBlock.id);
});
it("addFeaturesTemplateSection 호출 시 3컬럼 Feature 텍스트 블록들이 추가되고 마지막 블록이 선택되어야 한다", () => {
const store = createEditorStore();
const stateAny = store.getState() as any;
stateAny.addFeaturesTemplateSection();
const { blocks, selectedBlockId } = store.getState();
// 섹션 1개 + 각 컬럼당 제목/설명 2개씩, 총 7개 블록이 생성된다.
expect(blocks.length).toBe(7);
const section = blocks.find((b) => b.type === "section");
expect(section).toBeTruthy();
const featureBlocks = blocks.filter((b) => b.sectionId === section!.id);
expect(featureBlocks).toHaveLength(6);
expect(featureBlocks.every((b) => b.type === "text")).toBe(true);
// 마지막 Feature 블록이 선택되어야 한다.
const lastFeature = featureBlocks[featureBlocks.length - 1];
expect(selectedBlockId).toBe(lastFeature.id);
});
it("addCtaTemplateSection 호출 시 CTA 섹션과 텍스트/버튼이 추가되고 버튼이 선택되어야 한다", () => {
const store = createEditorStore();
const stateAny = store.getState() as any;
stateAny.addCtaTemplateSection();
const { blocks, selectedBlockId } = store.getState();
expect(blocks.length).toBe(3);
const section = blocks.find((b) => b.type === "section");
expect(section).toBeTruthy();
const ctaBlocks = blocks.filter((b) => b.sectionId === section!.id);
expect(ctaBlocks).toHaveLength(2);
expect(ctaBlocks.map((b) => b.type).sort()).toEqual(["button", "text"].sort());
const buttonBlock = ctaBlocks.find((b) => b.type === "button");
expect(buttonBlock).toBeTruthy();
expect(selectedBlockId).toBe(buttonBlock!.id);
});
it("projectConfig 기본값과 updateProjectConfig 액션으로 프로젝트 설정을 관리할 수 있어야 한다", () => {
const store = createEditorStore();
const stateAny = store.getState() as any;