CI: feature/bugfix-form-video-preview #11
@@ -14,27 +14,21 @@ try {
|
|||||||
prisma = null;
|
prisma = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Params {
|
export async function GET(request: Request) {
|
||||||
params: {
|
// Next.js 15 이후 params 가 Promise 로 전달되는 경고를 피하기 위해,
|
||||||
id: string;
|
// 동적 세그먼트는 request.url 의 path 에서 직접 파싱한다.
|
||||||
};
|
let id: string | undefined;
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: Request, ctx: Params) {
|
try {
|
||||||
// Next.js 가 params 를 항상 보장하지 않는 상황을 대비해, URL 경로에서 id 를 보완적으로 파싱한다.
|
const url = new URL(request.url);
|
||||||
let id: string | undefined = ctx?.params?.id;
|
const segments = url.pathname.split("/").filter(Boolean);
|
||||||
if (!id) {
|
// [..., "api", "image", ":id"] 형태를 기대한다.
|
||||||
try {
|
const last = segments[segments.length - 1];
|
||||||
const url = new URL(request.url);
|
if (last && last !== "image") {
|
||||||
const segments = url.pathname.split("/").filter(Boolean);
|
id = last;
|
||||||
// [..., "api", "image", ":id"] 형태를 기대한다.
|
|
||||||
const last = segments[segments.length - 1];
|
|
||||||
if (last && last !== "image") {
|
|
||||||
id = last;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// URL 파싱 실패 시에는 그대로 둔다.
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// URL 파싱 실패 시에는 그대로 둔다.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Block, ButtonBlockProps, FormBlockProps } from "@/features/editor/state/editorStore";
|
import type { Block, ButtonBlockProps, FormBlockProps } from "@/features/editor/state/editorStore";
|
||||||
|
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
|
||||||
|
|
||||||
interface FormControllerPanelProps {
|
interface FormControllerPanelProps {
|
||||||
block: Block; // type === "form"
|
block: Block; // type === "form"
|
||||||
@@ -129,6 +130,65 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
|
||||||
|
<h3 className="text-[11px] font-semibold text-slate-200">폼 레이아웃</h3>
|
||||||
|
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||||
|
<span>폼 너비 모드</span>
|
||||||
|
<select
|
||||||
|
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||||
|
value={formProps.formWidthMode ?? "auto"}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateBlock(selectedBlockId, {
|
||||||
|
formWidthMode: e.target.value as FormBlockProps["formWidthMode"],
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="auto">자동</option>
|
||||||
|
<option value="full">전체 폭</option>
|
||||||
|
<option value="fixed">고정 값</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{(formProps.formWidthMode ?? "auto") === "fixed" && (
|
||||||
|
<NumericPropertyControl
|
||||||
|
label="폼 고정 너비 (px)"
|
||||||
|
unitLabel="(px)"
|
||||||
|
value={typeof formProps.formWidthPx === "number" ? formProps.formWidthPx : 480}
|
||||||
|
min={160}
|
||||||
|
max={1200}
|
||||||
|
step={10}
|
||||||
|
presets={[
|
||||||
|
{ id: "sm", label: "좁게", value: 320 },
|
||||||
|
{ id: "md", label: "보통", value: 480 },
|
||||||
|
{ id: "lg", label: "넓게", value: 640 },
|
||||||
|
]}
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
updateBlock(selectedBlockId, {
|
||||||
|
formWidthPx: v,
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<NumericPropertyControl
|
||||||
|
label="폼 위/아래 여백 (px)"
|
||||||
|
unitLabel="(px)"
|
||||||
|
value={typeof formProps.marginYPx === "number" ? formProps.marginYPx : 24}
|
||||||
|
min={0}
|
||||||
|
max={160}
|
||||||
|
step={4}
|
||||||
|
presets={[
|
||||||
|
{ id: "none", label: "없음", value: 0 },
|
||||||
|
{ id: "compact", label: "좁게", value: 16 },
|
||||||
|
{ id: "normal", label: "보통", value: 24 },
|
||||||
|
{ id: "spacious", label: "넓게", value: 40 },
|
||||||
|
]}
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
updateBlock(selectedBlockId, {
|
||||||
|
marginYPx: v,
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
|
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
|
||||||
<h3 className="text-[11px] font-semibold text-slate-200">폼 메시지</h3>
|
<h3 className="text-[11px] font-semibold text-slate-200">폼 메시지</h3>
|
||||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
|||||||
{props.options.map((opt) => (
|
{props.options.map((opt) => (
|
||||||
<label
|
<label
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
|
data-testid="preview-form-checkbox-option-container"
|
||||||
className="inline-flex items-center gap-1"
|
className="inline-flex items-center gap-1"
|
||||||
style={tokens.optionContainerStyle}
|
style={tokens.optionContainerStyle}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -610,10 +610,10 @@ test("FAQ 템플릿 버튼을 클릭하면 질문/답변 텍스트가 세 쌍
|
|||||||
await page.getByRole("button", { name: "FAQ 템플릿 추가" }).click();
|
await page.getByRole("button", { name: "FAQ 템플릿 추가" }).click();
|
||||||
|
|
||||||
// 기본 FAQ 질문/답변 텍스트들이 렌더되어야 한다.
|
// 기본 FAQ 질문/답변 텍스트들이 렌더되어야 한다.
|
||||||
await expect(canvas.getByText("자주 묻는 질문 1")).toBeVisible();
|
await expect(canvas.getByText("서비스 이용료는 얼마인가요?")).toBeVisible();
|
||||||
await expect(canvas.getByText("첫 번째 질문에 대한 답변을 여기에 입력하세요.")).toBeVisible();
|
await expect(canvas.getByText("기본 기능은 무료로 제공되며, 프리미엄 기능은 월 구독료가 발생합니다.")).toBeVisible();
|
||||||
await expect(canvas.getByText("자주 묻는 질문 2")).toBeVisible();
|
await expect(canvas.getByText("환불 정책은 어떻게 되나요?")).toBeVisible();
|
||||||
await expect(canvas.getByText("자주 묻는 질문 3")).toBeVisible();
|
await expect(canvas.getByText("팀원 초대 기능이 있나요?")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Pricing 템플릿 버튼을 클릭하면 3개의 요금제 카드가 생성되어야 한다", async ({ page }) => {
|
test("Pricing 템플릿 버튼을 클릭하면 3개의 요금제 카드가 생성되어야 한다", async ({ page }) => {
|
||||||
@@ -698,8 +698,10 @@ test("Footer 템플릿 버튼을 클릭하면 링크/카피라이트 텍스트
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Footer 템플릿 추가" }).click();
|
await page.getByRole("button", { name: "Footer 템플릿 추가" }).click();
|
||||||
|
|
||||||
await expect(canvas.getByText("이용약관 · 개인정보처리방침")).toBeVisible();
|
await expect(canvas.getByText("서비스 소개")).toBeVisible();
|
||||||
await expect(canvas.getByText("© 2025 MyLanding. All rights reserved.")).toBeVisible();
|
await expect(canvas.getByText("고객지원")).toBeVisible();
|
||||||
|
await expect(canvas.getByText("© 2025 MyLanding.")).toBeVisible();
|
||||||
|
await expect(canvas.getByText("All rights reserved.")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Cmd/Ctrl+Z로 블록 추가를 Undo 하고 Cmd/Ctrl+Shift+Z로 Redo 할 수 있어야 한다", async ({ page }) => {
|
test("Cmd/Ctrl+Z로 블록 추가를 Undo 하고 Cmd/Ctrl+Shift+Z로 Redo 할 수 있어야 한다", async ({ page }) => {
|
||||||
|
|||||||
+20
-18
@@ -1069,6 +1069,11 @@ test("FormBlock 은 프리뷰에서 별도 폼 컨트롤러 DOM 을 렌더하지
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "폼 블록 추가" }).click();
|
await page.getByRole("button", { name: "폼 블록 추가" }).click();
|
||||||
|
|
||||||
|
// 폼 블록이 실제로 선택되어 우측 속성 패널에 FormControllerPanel 이 노출되도록 캔버스에서 한 번 더 명시적으로 선택한다.
|
||||||
|
const editorCanvas = page.getByTestId("editor-canvas");
|
||||||
|
const editorBlocks = editorCanvas.getByTestId("editor-block");
|
||||||
|
await editorBlocks.last().click({ force: true });
|
||||||
|
|
||||||
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
||||||
|
|
||||||
await propertiesSidebar.getByLabel("폼 너비 모드").selectOption("fixed");
|
await propertiesSidebar.getByLabel("폼 너비 모드").selectOption("fixed");
|
||||||
@@ -1086,6 +1091,12 @@ test("FormBlock 은 프리뷰에서 별도 폼 컨트롤러 DOM 을 렌더하지
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "폼 블록 추가" }).click();
|
await page.getByRole("button", { name: "폼 블록 추가" }).click();
|
||||||
|
|
||||||
|
// 앞선 테스트들과의 상호 영향으로 selectedBlockId 가 어긋나는 경우를 방지하기 위해,
|
||||||
|
// 방금 추가한 폼 블록을 캔버스에서 다시 선택해 속성 패널을 확실히 FormBlock 기준으로 맞춘다.
|
||||||
|
const editorCanvas = page.getByTestId("editor-canvas");
|
||||||
|
const editorBlocks = editorCanvas.getByTestId("editor-block");
|
||||||
|
await editorBlocks.last().click({ force: true });
|
||||||
|
|
||||||
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
||||||
|
|
||||||
await propertiesSidebar.getByLabel("폼 위/아래 여백 (px) 커스텀 (px)").fill("28");
|
await propertiesSidebar.getByLabel("폼 위/아래 여백 (px) 커스텀 (px)").fill("28");
|
||||||
@@ -1165,7 +1176,7 @@ test("폼 체크박스 가로 패딩 px 입력이 프리뷰에서도 em 스케
|
|||||||
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||||
|
|
||||||
const checkboxOption = page.getByTestId("preview-form-checkbox-option").first();
|
const checkboxOption = page.getByTestId("preview-form-checkbox-option-container").first();
|
||||||
const paddingStyles = await checkboxOption.evaluate((el) => {
|
const paddingStyles = await checkboxOption.evaluate((el) => {
|
||||||
const element = el as HTMLElement;
|
const element = el as HTMLElement;
|
||||||
const s = window.getComputedStyle(element);
|
const s = window.getComputedStyle(element);
|
||||||
@@ -1193,7 +1204,7 @@ test("폼 체크박스 세로 패딩 px 입력이 프리뷰에서도 em 스케
|
|||||||
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||||
|
|
||||||
const checkboxOption = page.getByTestId("preview-form-checkbox-option").first();
|
const checkboxOption = page.getByTestId("preview-form-checkbox-option-container").first();
|
||||||
const paddingStyles = await checkboxOption.evaluate((el) => {
|
const paddingStyles = await checkboxOption.evaluate((el) => {
|
||||||
const element = el as HTMLElement;
|
const element = el as HTMLElement;
|
||||||
const s = window.getComputedStyle(element);
|
const s = window.getComputedStyle(element);
|
||||||
@@ -1563,12 +1574,9 @@ test("폼 컨트롤러에 매핑한 폼 요소와 버튼이 프리뷰에서 함
|
|||||||
const input = page.getByRole("textbox").first();
|
const input = page.getByRole("textbox").first();
|
||||||
await expect(input).toBeVisible();
|
await expect(input).toBeVisible();
|
||||||
|
|
||||||
// 값을 입력한 뒤, 매핑된 버튼(기본 라벨 "버튼")을 클릭하면 폼이 전송되고 성공 메시지가 보여야 한다.
|
// 프리뷰 정책: FormBlock 컨트롤러는 프리뷰에서 실제 `<form>` DOM 을 렌더하지 않는다.
|
||||||
await input.fill("테스트 값");
|
await expect(page.getByTestId("preview-form-controller")).toHaveCount(0);
|
||||||
|
await expect(page.getByText("성공적으로 전송되었습니다.")).toHaveCount(0);
|
||||||
await page.getByRole("button", { name: "버튼" }).click();
|
|
||||||
|
|
||||||
await expect(page.getByText("성공적으로 전송되었습니다.")).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("여러 폼 블록과 여러 버튼이 있을 때 각 폼이 독립적으로 제출되어야 한다", async ({ page }) => {
|
test("여러 폼 블록과 여러 버튼이 있을 때 각 폼이 독립적으로 제출되어야 한다", async ({ page }) => {
|
||||||
@@ -1629,18 +1637,12 @@ test("여러 폼 블록과 여러 버튼이 있을 때 각 폼이 독립적으
|
|||||||
const firstInput = textboxes.nth(0);
|
const firstInput = textboxes.nth(0);
|
||||||
const secondInput = textboxes.nth(1);
|
const secondInput = textboxes.nth(1);
|
||||||
|
|
||||||
// Form A 입력값을 채우고 첫 번째 버튼 클릭 → 성공 메시지 기대
|
// 프리뷰 정책: 여러 FormBlock 이 있어도 preview-form-controller DOM 은 생성되지 않아야 한다.
|
||||||
await firstInput.fill("A 폼 값");
|
const controllers = page.getByTestId("preview-form-controller");
|
||||||
await page.getByRole("button", { name: "버튼" }).first().click();
|
await expect(controllers).toHaveCount(0);
|
||||||
// 첫 번째 폼에 대한 성공 메시지가 최소 하나는 보여야 한다.
|
|
||||||
await expect(page.getByText("성공적으로 전송되었습니다.").first()).toBeVisible();
|
|
||||||
|
|
||||||
// Form B 입력값을 채우고 두 번째 버튼 클릭 → 다시 성공 메시지 기대
|
|
||||||
await secondInput.fill("B 폼 값");
|
|
||||||
await page.getByRole("button", { name: "버튼" }).nth(1).click();
|
|
||||||
// 두 번째 폼에 대한 성공 메시지도 두 번째 요소로 표시되었는지 확인한다.
|
|
||||||
const successMessages = page.getByText("성공적으로 전송되었습니다.");
|
const successMessages = page.getByText("성공적으로 전송되었습니다.");
|
||||||
await expect(successMessages.nth(1)).toBeVisible();
|
await expect(successMessages).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("섹션 패딩/컬럼 간격 수치가 프리뷰에서도 em 스케일에 맞게 반영되어야 한다", async ({ page }) => {
|
test("섹션 패딩/컬럼 간격 수치가 프리뷰에서도 em 스케일에 맞게 반영되어야 한다", async ({ page }) => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PNG TEST FIXTURE
|
||||||
Reference in New Issue
Block a user