19 KiB
메인 플랜 (Page Builder)
현재 완료된 단계
- builder-1 ~ builder-8: 기본 에디터, 텍스트/버튼/섹션 블록, DnD, Undo/Redo, 블록 삭제/복제 등 핵심 UX 구현
- builder-9-templates-advanced (현재 브랜치):
- Hero / Features / CTA / FAQ / Pricing / Testimonials / Blog / Team / Footer 템플릿 섹션
- 프리뷰 페이지 및 모바일 뷰포트 E2E
- CI 환경에서 Prisma Client generate + API 테스트용 DATABASE_URL 설정
- API 테스트에서 Prisma를 메모리 기반 목으로 교체하여 DB 없이 테스트 통과
다음 단계 개요
-
builder-10-block-types
- 더 다양한 블록 타입 추가 (예: 이미지, 아이콘이 있는 카드, 구분선, 리스트 등)
- Zustand store에 타입/props 정의 및 액션 추가
- 에디터 UI에 새 블록 버튼 및 속성 패널 연동
- 새 블록 타입에 대한 유닛 테스트 + E2E 테스트 추가
-
builder-11-editor-ux-advanced
- 드래그앤드롭 UX 고도화 (섹션/컬럼 이동 UX 개선, 드롭 힌트 강화)
- 키보드 숏컷 확장 및 포커스 관리 개선
- 인라인 편집 UX 다듬기
-
**## builder-12-theme-output 상세 플랜
- Tailwind 기반 디자인 시스템 정리 (색상/타이포/간격 스케일)
- Public/Preview 렌더링 품질 개선 (반응형, 여백, 타이포그래피)
- 샘플 테마 프리셋 추가
3) 텍스트 스타일 시스템 정리 (에디터 쪽 선행 작업)
- 에디터 텍스트 블록 속성 구조와 렌더링 규칙 정리
TextBlockProps에서 정렬/폰트 크기/줄 간격/자간/색상/최대 너비/장식 등 타이포 관련 속성 확장TextPropertiesPanel에서 위 속성들을 슬라이더/프리셋/컬러 피커로 제어하도록 구현SortableEditorBlock에서 텍스트 블록 렌더링 시align→pb-text-left/center/rightfontSizeMode/fontSizeScale/fontSizeCustom→pb-text-*또는style.fontSizelineHeightMode/lineHeightScale/lineHeightCustom→pb-leading-*또는style.lineHeightletterSpacingCustom→style.letterSpacingcolorPalette/colorCustom→pb-text-color-*또는style.colormaxWidthMode/maxWidthScale/maxWidthCustom→pb-text-maxw-*또는style.maxWidthunderline/strike/italic→pb-underline/pb-line-through/pb-italic
- 텍스트 편집 모드가 아닐 때, 실제 텍스트 요소(
<div>)에도 위 클래스와textStyleOverrides를 직접 적용해 시각적 일관성 확보
- 테스트 정렬
tests/e2e/editor.spec.ts에서 텍스트/구분선/리스트 블록 관련 정렬/크기 기대값을 Tailwindtext-*가 아니라pb-text-*유틸리티 네이밍에 맞게 조정- JSON 내보내기/불러오기 시나리오에서도
pb-text-left/right/sm/lg클래스가 복원되는지 검증
- builder-13-forms
- 폼 전송 구조 설계 및 구현 (internal API + Webhook/Google Sheets)
- 폼 전송 설정(헤더/토큰/추가 파라미터) 및 다중 폼 컨트롤러 구조 설계
- 폼 요소 블록(input/select/radio/checkbox) 추가 및 자유 레이아웃 배치 지원
builder-10-block-types 상세 플랜
1) 대상 블록 타입 정의
- 이미지 블록(image)
- props:
src,alt,align,width,borderRadius등
- props:
- 구분선(divider)
- props:
style(solid/dashed),thickness,color,marginY
- props:
- 리스트(list)
- props:
items(문자열 배열),ordered(true/false),align
- props:
- (필요시) 카드(card)
- props:
title,description,imageSrc,align
- props:
※ 실제 구현 범위는 TDD를 진행하면서 우선순위 높은 것부터 차례대로 확장한다.
2) TDD 순서
- 유닛 테스트부터 작성 (실패 상태로 시작)
tests/unit/editorStore.spec.tsaddImageBlock,addDividerBlock,addListBlock등 새 액션 테스트 추가- 블록이 올바른 기본 props 와 함께
blocks배열에 추가되는지 확인 - 섹션/컬럼 내부에 배치되는 경우 위치 정보(
sectionId,columnId) 테스트
- 스토어 구현 (editorStore.ts)
Block타입에 새type과props구조 추가EditorState에 새 액션 시그니처 추가createEditorState내부에 실제 로직 구현 (createId 활용, 기존 텍스트/버튼/섹션 패턴 재사용)
- 에디터 UI 연동 (editor/page.tsx)
- 사이드바에 새 블록 버튼 추가 (이미지, 구분선, 리스트 등)
- 캔버스 렌더링 분기에 새 블록 타입별 렌더링 로직 추가
- 속성 패널에서 최소한의 필수 props 편집 가능하도록 구현
- E2E 테스트 추가 (Playwright)
tests/e2e/editor.spec.ts- 새 블록 타입 버튼 클릭 → 캔버스에 올바르게 렌더링되는지 확인
- 속성 패널에서 값 수정 시 캔버스 반영 확인 (예: 이미지 src, 리스트 아이템 수정)
- 리팩터링 & 안정화
- 중복되는 렌더링/props 처리 로직 정리
- 필요시 추가 유닛 테스트/E2E로 회귀 방지
builder-11-editor-ux-advanced 상세 플랜
1) DnD UX 고도화
-
목표
- 블록/섹션 드래그앤드롭이 시각적으로 명확하고, 의도한 위치로 안정적으로 이동하도록 개선
- 2컬럼 섹션 컬럼 이동, 블록 순서 변경 관련 E2E를 CI에서도 신뢰할 수 있게 만들기
-
작업 항목 (TDD)
tests/e2e/editor.spec.ts"블록 드래그앤드롭으로 순서를 변경할 수 있어야 한다"시나리오를 현재 UX에 맞게 단순/안정화"2컬럼 섹션에서 블록을 다른 컬럼 영역으로 드래그하면 컬럼이 변경되어야 한다"시나리오를 실제 드롭 영역 구조에 맞춰 조정
EditorPage/SortableEditorBlock- 드롭 대상 컬럼/섹션의 hit-area를 넓히거나, 불필요한 overlay가 pointer events를 가로채지 않게 레이아웃 조정
- DnD Kit 설정(sensors, collision detection 등) 필요 시 보정
- 필요 시
editorStore유닛 테스트에서reorderBlocks,moveBlock조합 케이스(섹션/컬럼 간 이동)를 명시적으로 테스트해 회귀 방지
2) 키보드/포커스 UX 개선
-
목표
- ArrowUp/ArrowDown 으로 선택 블록이 예측 가능하게 이동
- Cmd/Ctrl+Z / Cmd/Ctrl+Shift+Z / Cmd/Ctrl+D 와 같은 단축키가 OS에 관계없이 일관되게 동작
-
작업 항목 (TDD)
tests/e2e/editor.spec.ts"ArrowUp/ArrowDown 키로 선택된 블록을 위/아래로 이동하며 순차적으로 선택할 수 있어야 한다"테스트를 CI에서도 통과하도록 재정의- 초기 선택 상태, 포커스 위치, 기대되는
data-selected/aria-selected값을 명확히 기술
- 초기 선택 상태, 포커스 위치, 기대되는
EditorPage의 전역 keydown 핸들러- ArrowUp/Down 선택 이동 로직을 단순/명확하게 정리 (현재 선택이 없을 때 첫 블록 선택 등)
- OS별 Cmd/Ctrl 판별 로직(
navigator.platform)과 E2E에서 보내는 키 조합을 일치시켜 유지
3) 인라인 편집 UX 정리
-
목표
- 텍스트 블록 더블클릭 → 인라인 편집 진입
- Enter → 커밋, Esc → 취소, 포커스 아웃 시 안전하게 커밋
-
작업 항목 (TDD)
tests/e2e/editor.spec.ts"텍스트 블록을 더블클릭해서 내용을 수정할 수 있어야 한다"시나리오를- 더블클릭 → 편집 모드 진입
- Enter 커밋 / Esc 취소 흐름까지 포함하도록 보완
EditorPage의 인라인 편집 상태(editingBlockId,editingText)- 더블클릭/포커스 아웃/Enter/Esc 이벤트에 대한 상태 전이와
updateBlock호출 타이밍을 명확히 정의
- 더블클릭/포커스 아웃/Enter/Esc 이벤트에 대한 상태 전이와
이 모든 작업도 builder-10과 동일하게 실패하는 테스트 추가 → 최소 구현 → 리팩터링 → CI/E2E 확인 순서로 진행한다.
builder-13-forms 상세 플랜
1) 폼 전송 구조 (v1: 단일 폼 + 필드 배열)
-
목표
- SPA 스타일로 페이지 리로드 없이 폼을 전송하고, CORS 문제 없이 외부 API/Google Sheets 로 연동
-
현재 구현 상태 요약
FormBlockProps에 전송 설정 추가submitTarget: "internal" | "webhook"destinationUrl,method,headers,extraParamssuccessMessage,errorMessage
- 퍼블릭 렌더러: 폼 블록을
<form>으로 렌더링하고/api/forms/submit으로FormData전송 /api/forms/submit라우트:__confighidden 필드로 넘어온FormBlockProps를 파싱submitTarget === "internal"일 때 서버 내부 처리 (향후 DB 저장/메일 발송용)submitTarget === "webhook"일 때destinationUrl로 서버사이드 POST (x-www-form-urlencoded)headers/extraParams를 함께 전송하여 토큰/고정 파라미터 전달
- 폼 필드 정의:
FormFieldConfig(id,name,label,type,required)FormBlockProps.fields?: FormFieldConfig[]로 필드 배열 관리- 에디터 우측 패널에서 필드 추가/삭제/순서/라벨/name/type/필수 여부 편집 UI 제공
-
TDD 순서 (v1)
tests/unit/editorStore.spec.tsFormBlockProps/FormFieldConfig초기값,addFormBlock동작 테스트
editorStore.tsFormFieldConfig,FormBlockProps정의 및addFormBlock구현
PublicPageRenderer.tsxfields기반 폼 렌더링 및/api/forms/submit호출 로직 구현
src/app/api/forms/submit/route.ts- internal/webhook 분기 및 Webhook/Google Sheets 전송 로직 구현
tests/e2e/editor.spec.ts/tests/e2e/preview.spec.ts- 폼 추가 → 필드 수정 → 프리뷰에서 폼 전송 → 성공/에러 메시지 확인 시나리오 추가
2) 폼 요소 블록 + 폼 컨트롤러 구조 (v2: 자유 레이아웃)
-
목표
- 폼 요소(input/select/radio/checkbox)를 텍스트 블록처럼 섹션/컬럼 안에 자유롭게 배치
- 폼 블록은 전송 설정 + 어떤 요소(id)들을 묶어서 어디로 보낼지 제어하는 컨트롤러 역할만 수행
- 페이지 내에 여러 폼 블록이 있어도 각기 다른 필드/submit 버튼/전송 설정을 사용 가능
-
설계 개요
- 새 블록 타입 추가:
formInput,formSelect,formRadio,formCheckbox등- 각 props 예시:
FormInputBlockProps:label,placeholder,formFieldName?,formFieldType,required?FormSelectBlockProps:label,formFieldName?,options[],required?FormRadioBlockProps:groupLabel,formFieldName?,options[],required?FormCheckboxBlockProps:label,formFieldName?,required?
FormBlockProps확장:fieldIds: string[](이 폼이 담당하는 폼 요소 블록들의 id)submitButtonId?: string(이 버튼이 눌리면 이 폼을 전송)
- 좌측 사이드바에 "폼 요소" 섹션 추가:
- 텍스트 입력/셀렉트/라디오 그룹/체크박스 블록 추가 버튼
editorStore에addFormInputBlock/addFormSelectBlock등 액션 추가
- 런타임 동작:
- 버튼 클릭 → 해당 버튼 id 를
submitButtonId로 가진 FormBlock 검색 - FormBlock.fieldIds 에 포함된 블록들 중
formFieldName이 설정된 요소만 수집 - 현재 값(value)을 모아
FormData또는 JSON 으로/api/forms/submit에 전송
- 버튼 클릭 → 해당 버튼 id 를
- 새 블록 타입 추가:
-
TDD 순서 (v2)
tests/unit/editorStore.spec.ts- 새 폼 요소 블록 타입 추가 액션(addFormInputBlock 등) 테스트
- FormBlock 의
fieldIds/submitButtonId기본값 및 업데이트 동작 테스트
editorStore.tsBlockType에 폼 요소 타입 추가, 각 props/interface 정의addFormInputBlock등 구현 (섹션/컬럼 내 자유 배치)
src/app/editor/forms/elements/*.tsxInputElement/SelectElement/RadioGroupElement/CheckboxElement컴포넌트 구현 및 에디터/퍼블릭 렌더링 연계
src/app/editor/page.tsx- 좌측 사이드바에 "폼 요소" 섹션 추가 및 액션 연결
- 블록 렌더링 분기에 새 폼 요소 블록 타입 처리 추가
- 우측 폼 패널에 FormBlock 의
fieldIds/submitButtonId매핑 UI 추가
tests/e2e/editor.spec.ts/tests/e2e/preview.spec.ts- 섹션/컬럼 안에 폼 요소들을 자유 배치 → FormBlock 에 id 매핑 → 버튼 클릭 시 해당 요소 값만 전송되는지 검증
폼 기능 역시 다른 단계와 동일하게 실패하는 테스트 → 최소 구현 → 리팩터링 순서를 유지하며, Webhook/Google Sheets 연동 시 CORS 문제를 피하기 위해 항상 /api/forms/submit 경유 구조를 사용한다.
3) 진행 현황 (2025-11-20)
-
v2 TDD 1단계(editorStore)
tests/unit/editorStore.spec.tsformInput블록 추가 액션(addFormInputBlock)에 대한 유닛 테스트 작성- FormBlock 컨트롤러 기본 속성(
fieldIds,submitButtonId) 초기값 유닛 테스트 작성
src/features/editor/state/editorStore.tsBlockType에"formInput"타입 추가FormInputBlockProps정의 및 공통Block.props유니온에 포함FormBlockProps에fieldIds?: string[],submitButtonId?: string | null추가addFormBlock에서fieldIds: [],submitButtonId: null기본값 설정addFormInputBlock액션 구현 (루트에 기본 label/formFieldName 으로 추가, 선택 상태 업데이트)
- 결과:
editorStore.spec.ts전체 25개 테스트 모두 통과
-
v2 TDD 2단계(editorStore + Editor UI)
tests/unit/editorStore.spec.tsformSelect/formRadio/formCheckbox블록 추가 액션 및 FormBlockfieldIds/submitButtonId업데이트 테스트 추가
src/features/editor/state/editorStore.tsBlockType에"formSelect","formRadio","formCheckbox"추가FormSelectBlockProps/FormRadioBlockProps/FormCheckboxBlockProps정의 및Block.props/updateBlock에 반영addFormSelectBlock/addFormRadioBlock/addFormCheckboxBlock구현 (루트에 기본 label/options 로 추가)
src/app/editor/page.tsx- 좌측 사이드바에 "폼 요소" 섹션 추가
- 우측 속성 패널에서 FormBlock 선택 시 "폼 컨트롤러" 섹션 추가
- 체크박스/셀렉트 변경 시
updateBlock으로 FormBlock 의fieldIds/submitButtonId업데이트
tests/e2e/editor.spec.ts- "폼 요소 사이드바에서 폼 입력/셀렉트/라디오/체크박스 블록을 추가할 수 있어야 한다" 시나리오 추가
- "폼 블록을 선택하면 우측 속성 패널에서 폼 필드/버튼 매핑 UI가 보여야 한다" 시나리오 추가
- 결과: 관련 유닛/E2E 테스트 모두 통과
-
v2 TDD 3단계(Preview/Public 렌더러 폼 제출 v2)
tests/e2e/preview.spec.ts- "폼 컨트롤러에 매핑한 폼 요소와 버튼이 프리뷰에서 함께 동작해야 한다" 시나리오 추가
- 에디터에서
formInput/button/form블록을 추가 - FormBlock 의
fieldIds/submitButtonId매핑 설정 후/preview이동 - 프리뷰에서 입력값을 채우고 "버튼" 클릭 시 성공 메시지("성공적으로 전송되었습니다.")가 표시되는지 검증
- 에디터에서
- "폼 컨트롤러에 매핑한 폼 요소와 버튼이 프리뷰에서 함께 동작해야 한다" 시나리오 추가
src/features/editor/components/PublicPageRenderer.tsx- FormBlock 렌더링 시 v2 컨트롤러 우선 사용
fieldIds가 설정된 경우 해당 id 를 가진formInput/formSelect/formCheckbox/formRadio블록들만 수집- 각 폼 요소 블록의
formFieldName/label/options를 사용해<input>/<select>/<textarea>/checkbox/radio필드 렌더링 fieldIds가 없거나 매핑된 요소가 없으면 기존 v1FormBlockProps.fields또는 기본 name/email/message 폼을 fallback 으로 사용
submitButtonId가 설정된 경우 해당 버튼 블록의label을 폼의 submit 버튼 라벨로 사용- submit 시
/api/forms/submit으로 FormData 를 전송하고, 응답이 ok 이면successMessage(기본값 "성공적으로 전송되었습니다.") 를 표시
- FormBlock 렌더링 시 v2 컨트롤러 우선 사용
- 결과: 프리뷰 폼 컨트롤러 v2 E2E 테스트 통과
-
v2 TDD 4단계(radio/checkbox 라벨 구조 정리)
- 목표: 라디오/체크박스 필드에서
groupLabel은 항상 그룹 타이틀(제목 텍스트)로만 사용하고, 각 옵션의label이 실제 라벨 역할을 하도록 분리 - 에디터 속성 패널(
EditorPage)- 폼 요소 선택 시 라디오/체크박스의 상단 라벨 인풋을
그룹 타이틀로 표기 - 라벨 모드 셀렉트를 라디오/체크박스의 경우
옵션 라벨 타입으로 표기하여 그룹 제목과 옵션 라벨 역할을 명확히 분리
- 폼 요소 선택 시 라디오/체크박스의 상단 라벨 인풋을
- 에디터 캔버스 렌더링(
EditorPage)formRadio/formCheckbox블록에서 상단 헤더는 항상groupLabel텍스트로만 렌더링labelMode === "image"인 경우, 라디오/체크박스 옆 옵션 라벨 위치에labelImageUrl/labelImageAlt를 사용해 이미지 라벨을 표시하고, 기본 모드에서는 옵션의label텍스트를 그대로 사용
- E2E 테스트(
tests/e2e/editor.spec.ts)- "폼 셀렉트/라디오/체크박스 블록도 우측 패널에서 기본 필드 속성을 편집할 수 있어야 한다" 시나리오를 groupLabel/옵션 라벨 타입 분리에 맞게 업데이트
- 라디오/체크박스 선택 시 라벨 입력 필드 이름을
그룹 타이틀로 기대 - 라디오/체크박스 선택 시
옵션 라벨 타입콤보박스 존재 여부를 검증
- 라디오/체크박스 선택 시 라벨 입력 필드 이름을
- "폼 셀렉트/라디오/체크박스 블록도 우측 패널에서 기본 필드 속성을 편집할 수 있어야 한다" 시나리오를 groupLabel/옵션 라벨 타입 분리에 맞게 업데이트
- 목표: 라디오/체크박스 필드에서
-
디버깅 기록
tests/unit/editorStore.spec.ts하단에 템플릿/undo/redo/remove/duplicate 관련 테스트 블록이 중복 삽입되어 중괄호 정렬이 깨짐- 증상: Vitest 실행 시
Unexpected "}"파싱 에러 발생 - 조치: 중복된 it 블록들 제거, 기존 describe 내부의
removeBlock/duplicateBlock테스트를 정리하고 그 뒤에 폼 관련 2개 테스트만 배치하여 정상 종료
- 증상: Vitest 실행 시
tests/unit/PropertyControls.spec.ts에는 별도의 TS/JSX 문법 및 타입 오류(onChange, label 등)가 존재하지만, 현재 폼 기능 흐름과 직접 관련 없으므로 후속 단계에서 별도 TDD 리팩터링 대상으로 남겨둠
디버깅/CI 관련 기록 (요약)
-
CI에서
prisma generate단계가DATABASE_URL없음으로 실패 → 워크플로에서 더미DATABASE_URL주입으로 해결 -
유닛 테스트에서 API 테스트가 실제 DB 접속 시도 →
- CI:
Run unit tests스텝에도 더미DATABASE_URL추가 - 테스트 코드: PrismaClient를 메모리 기반 목으로 교체, 라우트를 동적 import 하도록 수정
- CI:
-
Playwright E2E (에디터):
- 레이아웃 상 사이드바/속성 패널이 캔버스 블록 클릭을 가로채면서
locator.click타임아웃이 발생하는 이슈 확인 - divider/list 블록용 E2E는 블록 추가 직후 자동 선택 상태와 속성 패널 조작만 검증하도록 작성해 부분적으로 우회
- 기존 테스트들(블록 삭제, 복제, 키보드 이동 등)은 동일한 클릭 간섭 문제 영향 범위에 있어 후속 단계에서 레이아웃/테스트 전략 리팩터링 필요
- 레이아웃 상 사이드바/속성 패널이 캔버스 블록 클릭을 가로채면서
이후 단계(builder-11-editor-ux-advanced, builder-12-theme-output)에서도 모든 기능 추가는 TDD(실패 테스트 → 구현 → 리팩터링) 순서를 유지한다.