From 4e4c9cd37a8ac1ba65fbaf4f9f2f875bea9260a2 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sat, 22 Nov 2025 15:18:32 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=B9=EC=85=98=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=ED=9B=84=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=B8=94=EB=A1=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B2=84=ED=8A=BC/=EA=B5=AC=EB=B6=84=EC=84=A0=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC-=EC=86=8D=EC=84=B1=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forms/FormCheckboxPropertiesPanel.tsx | 169 +++ src/app/editor/forms/FormControllerPanel.tsx | 59 + .../editor/forms/FormInputPropertiesPanel.tsx | 136 +- .../editor/forms/FormRadioPropertiesPanel.tsx | 169 +++ .../forms/FormSelectPropertiesPanel.tsx | 149 ++ src/app/editor/page.tsx | 21 +- src/app/editor/panels/BlocksSidebar.tsx | 8 +- .../editor/panels/ButtonPropertiesPanel.tsx | 107 +- src/app/editor/panels/ListPropertiesPanel.tsx | 4 +- src/app/editor/panels/PropertiesSidebar.tsx | 1 + .../editor/panels/SectionPropertiesPanel.tsx | 4 +- src/app/editor/templates/ctaTemplate.ts | 3 + src/app/editor/templates/heroTemplate.ts | 5 +- .../editor/components/PublicPageRenderer.tsx | 513 ++++++- src/features/editor/state/editorStore.ts | 57 +- tests/e2e/editor.spec.ts | 180 ++- tests/e2e/preview.spec.ts | 1225 ++++++++++++++++- 17 files changed, 2658 insertions(+), 152 deletions(-) diff --git a/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx b/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx index 0cf056b..1903a46 100644 --- a/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx +++ b/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx @@ -183,6 +183,175 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc

필드 스타일

+ {/* px 기반 체크박스 타이포 입력값을 노출하여 em 스케일 변환을 위한 근거를 남긴다 */} + { + const raw = checkboxProps.fontSizeCustom ?? ""; + const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 14; + })()} + min={8} + max={40} + step={1} + presets={[ + { id: "xs", label: "XS", value: 12 }, + { id: "sm", label: "S", value: 14 }, + { id: "md", label: "M", value: 16 }, + { id: "lg", label: "L", value: 18 }, + { id: "xl", label: "XL", value: 20 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + fontSizeCustom: `${v}px`, + } as any); + }} + /> + {/* 줄간격 px 입력값을 노출해 preview em 변환과 TDD 케이스를 연동한다 */} + { + const raw = checkboxProps.lineHeightCustom ?? ""; + const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 20; + })()} + min={8} + max={60} + step={1} + presets={[ + { id: "tight", label: "타이트", value: 18 }, + { id: "normal", label: "보통", value: 24 }, + { id: "loose", label: "루즈", value: 32 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + lineHeightCustom: `${v}px`, + } as any); + }} + /> + {/* 자간 px 입력을 통한 폼 체크박스 미세 조정값을 노출 */} + { + const raw = checkboxProps.letterSpacingCustom ?? ""; + const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 0; + })()} + min={-10} + max={20} + step={0.5} + presets={[ + { id: "tight", label: "좁게", value: -1 }, + { id: "normal", label: "보통", value: 0 }, + { id: "wide", label: "넓게", value: 2 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + letterSpacingCustom: `${v}px`, + } as any); + }} + /> + + {(checkboxProps.widthMode ?? "full") === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} + + updateBlock(selectedBlockId, { + paddingX: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + paddingY: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + optionGapPx: v, + } as any) + } + /> +
+

폼 레이아웃

+ + {(formProps.formWidthMode ?? "auto") === "fixed" && ( + + updateBlock(selectedBlockId, { + formWidthPx: v, + } as any) + } + /> + )} + + updateBlock(selectedBlockId, { + marginYPx: v, + } as any) + } + /> +
+

폼 컨트롤러

diff --git a/src/app/editor/forms/FormInputPropertiesPanel.tsx b/src/app/editor/forms/FormInputPropertiesPanel.tsx index cd4a670..e6861b0 100644 --- a/src/app/editor/forms/FormInputPropertiesPanel.tsx +++ b/src/app/editor/forms/FormInputPropertiesPanel.tsx @@ -130,6 +130,79 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }

필드 스타일

+ { + const raw = inputProps.fontSizeCustom ?? ""; + const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 14; + })()} + min={8} + max={40} + step={1} + presets={[ + { id: "xs", label: "XS", value: 12 }, + { id: "sm", label: "S", value: 14 }, + { id: "md", label: "M", value: 16 }, + { id: "lg", label: "L", value: 18 }, + { id: "xl", label: "XL", value: 20 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + fontSizeCustom: `${v}px`, + } as any); + }} + /> + {/* 줄간격 px 입력을 노출하여 preview em 변환의 근거를 제공 */} + { + const raw = inputProps.lineHeightCustom ?? ""; + const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 20; + })()} + min={8} + max={60} + step={1} + presets={[ + { id: "tight", label: "타이트", value: 18 }, + { id: "normal", label: "보통", value: 24 }, + { id: "loose", label: "루즈", value: 32 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + lineHeightCustom: `${v}px`, + } as any); + }} + /> + {/* 자간 px 입력으로 폼 입력 타이포를 세밀하게 조정 */} + { + const raw = inputProps.letterSpacingCustom ?? ""; + const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 0; + })()} + min={-10} + max={20} + step={0.5} + presets={[ + { id: "tight", label: "좁게", value: -1 }, + { id: "normal", label: "보통", value: 0 }, + { id: "wide", label: "넓게", value: 2 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + letterSpacingCustom: `${v}px`, + } as any); + }} + /> + {(inputProps.labelLayout ?? "stacked") === "inline" && ( + + updateBlock(selectedBlockId, { + labelGapPx: v, + } as any) + } + /> + )} + {(radioProps.widthMode ?? "full") === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} + + updateBlock(selectedBlockId, { + paddingX: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + paddingY: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + optionGapPx: v, + } as any) + } + />

필드 스타일

+ { + const raw = selectProps.fontSizeCustom ?? ""; + const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 14; + })()} + min={8} + max={40} + step={1} + presets={[ + { id: "xs", label: "XS", value: 12 }, + { id: "sm", label: "S", value: 14 }, + { id: "md", label: "M", value: 16 }, + { id: "lg", label: "L", value: 18 }, + { id: "xl", label: "XL", value: 20 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + fontSizeCustom: `${v}px`, + } as any); + }} + /> + {/* 줄간격 px 입력을 받아 preview 렌더러의 em 변환 근거로 사용한다 */} + { + const raw = selectProps.lineHeightCustom ?? ""; + const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 20; + })()} + min={8} + max={60} + step={1} + presets={[ + { id: "tight", label: "타이트", value: 18 }, + { id: "normal", label: "보통", value: 24 }, + { id: "loose", label: "루즈", value: 32 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + lineHeightCustom: `${v}px`, + } as any); + }} + /> + {/* 자간 px 입력을 제공하여 TDD 시나리오와 동기화한다 */} + { + const raw = selectProps.letterSpacingCustom ?? ""; + const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/); + if (match) return Number(match[1]); + return 0; + })()} + min={-10} + max={20} + step={0.5} + presets={[ + { id: "tight", label: "좁게", value: -1 }, + { id: "normal", label: "보통", value: 0 }, + { id: "wide", label: "넓게", value: 2 }, + ]} + onChangeValue={(v) => { + updateBlock(selectedBlockId, { + letterSpacingCustom: `${v}px`, + } as any); + }} + /> + + {(selectProps.widthMode ?? "full") === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} + + updateBlock(selectedBlockId, { + paddingX: v, + } as any) + } + /> + + updateBlock(selectedBlockId, { + paddingY: v, + } as any) + } + /> { + if (block.type === "text") { + // 더블클릭 시 텍스트 블록 인라인 편집 모드로 진입한다. + event.stopPropagation(); + startEditing(block.id, (block.props as TextBlockProps).text); + } + }} >
- -

- Text / Image / Button / Divider / List / Section 블록을 이 영역에서 추가할 수 있습니다. -

); } diff --git a/src/app/editor/panels/ButtonPropertiesPanel.tsx b/src/app/editor/panels/ButtonPropertiesPanel.tsx index 5ca8b9e..b84b6f4 100644 --- a/src/app/editor/panels/ButtonPropertiesPanel.tsx +++ b/src/app/editor/panels/ButtonPropertiesPanel.tsx @@ -26,6 +26,50 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc />
+
+ { + updateBlock(selectedBlockId, { + paddingX: v, + } as any); + }} + /> +
+
+ { + updateBlock(selectedBlockId, { + paddingY: v, + } as any); + }} + /> +
- { - const s = buttonProps.size ?? "md"; - return s === "xs" ? 1 : s === "sm" ? 2 : s === "lg" ? 4 : s === "xl" ? 5 : 3; - })()} - min={1} - max={5} - step={1} - presets={[ - { id: "xs", label: "아주 작게", value: 1 }, - { id: "sm", label: "작게", value: 2 }, - { id: "md", label: "보통", value: 3 }, - { id: "lg", label: "크게", value: 4 }, - { id: "xl", label: "아주 크게", value: 5 }, - ]} - onChangeValue={(v) => { - const next = v <= 1 ? "xs" : v === 2 ? "sm" : v === 4 ? "lg" : v >= 5 ? "xl" : "md"; - updateBlock(selectedBlockId, { size: next } as any); - }} - /> + {/* 버튼 크기 스케일 컨트롤은 paddingX/paddingY 및 폰트 크기(px) 컨트롤로 충분하므로 제거했다. */}
+
+ + {(buttonProps.widthMode ?? (buttonProps.fullWidth ? "full" : "auto")) === "fixed" && ( + { + updateBlock(selectedBlockId, { + widthPx: v, + } as any); + }} + /> + )} +
{ const raw = buttonProps.fontSizeCustom ?? ""; diff --git a/src/app/editor/panels/ListPropertiesPanel.tsx b/src/app/editor/panels/ListPropertiesPanel.tsx index fb083ce..13f5a16 100644 --- a/src/app/editor/panels/ListPropertiesPanel.tsx +++ b/src/app/editor/panels/ListPropertiesPanel.tsx @@ -105,7 +105,7 @@ export function ListPropertiesPanel({ {/* 글자 크기 */} { const raw = listProps.fontSizeCustom ?? ""; @@ -204,7 +204,7 @@ export function ListPropertiesPanel({ {/* 아이템 간 여백 (슬라이더) */} { if (typeof listProps.gapYPx === "number") { diff --git a/src/app/editor/panels/PropertiesSidebar.tsx b/src/app/editor/panels/PropertiesSidebar.tsx index dbebceb..ff7df0b 100644 --- a/src/app/editor/panels/PropertiesSidebar.tsx +++ b/src/app/editor/panels/PropertiesSidebar.tsx @@ -48,6 +48,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) { return (