"use client"; import { useState, type CSSProperties } from "react"; import type { Block, TextBlockProps, ButtonBlockProps, ImageBlockProps, VideoBlockProps, SectionBlockProps, DividerBlockProps, ListBlockProps, ListItemNode, FormBlockProps, FormInputBlockProps, FormSelectBlockProps, FormCheckboxBlockProps, FormRadioBlockProps, FormSelectOption, FormRadioOption, FormCheckboxOption, } from "@/features/editor/state/editorStore"; import { normalizeVideoSourceUrl, resolveVideoPlatform, buildVideoEmbedUrl, computeVideoPublicTokens, } from "@/features/editor/utils/videoHelpers"; import { computeButtonPublicTokens, computeButtonPbTokens } from "@/features/editor/utils/buttonHelpers"; import { computeTextPublicTokens } from "@/features/editor/utils/textHelpers"; import { computeDividerPublicTokens } from "@/features/editor/utils/dividerHelpers"; import { computeImagePublicTokens } from "@/features/editor/utils/imageHelpers"; import { computeListPublicTokens } from "@/features/editor/utils/listHelpers"; import { computeSectionPublicTokens } from "@/features/editor/utils/sectionHelpers"; import { computeFormInputPublicTokens, computeFormSelectPublicTokens, computeFormCheckboxPublicTokens, computeFormRadioPublicTokens, computeFormControllerPublicTokens, } from "@/features/editor/utils/formHelpers"; // 섹션 레이아웃/스타일 토큰을 실제 Tailwind 클래스 조합으로 변환하는 유틸 // (에디터/퍼블릭 렌더러에서 동일한 규칙을 재사용하기 위해 export 한다.) export function getSectionLayoutConfig(props: SectionBlockProps) { const backgroundClass = props.background === "muted" ? "bg-slate-900" : props.background === "primary" ? "bg-sky-900" : "bg-slate-950"; // 섹션 수직 패딩: builder.css 의 pb-section-py-sm/md/lg (1rem/1.5rem/2.5rem)에 대응 // - sm → 1rem → py-4 // - md → 1.5rem → py-6 // - lg → 2.5rem → py-10 const paddingYClass = props.paddingY === "sm" ? "py-4" : props.paddingY === "lg" ? "py-10" : "py-6"; const maxWidthClass = props.maxWidthMode === "narrow" ? "max-w-3xl" : props.maxWidthMode === "wide" ? "max-w-6xl" : "max-w-5xl"; const gapXClass = props.gapX === "sm" ? "gap-4" : props.gapX === "lg" ? "gap-10" : "gap-8"; const alignItemsClass = props.alignItems === "center" ? "items-center" : props.alignItems === "bottom" ? "items-end" : "items-start"; return { backgroundClass, paddingYClass, maxWidthClass, gapXClass, alignItemsClass, } as const; } // 에디터 크롬 없이 실제 랜딩 페이지처럼 블록들을 렌더링하는 컴포넌트 interface PublicPageRendererProps { blocks: Block[]; } const pxToEm = (px: number, base = 16) => `${px / base}em`; const convertPxStringToEm = (value?: string | null) => { if (!value) return null; const match = value.trim().match(/-?\d+(?:\.\d+)?/); if (!match) return null; const px = parseFloat(match[0]); if (!Number.isFinite(px)) return null; return pxToEm(px); }; export function PublicPageRenderer({ blocks }: PublicPageRendererProps) { const sectionBlocks = blocks.filter((b) => b.type === "section"); const rootBlocks = blocks.filter((b) => !b.sectionId && b.type !== "section"); const [formStatus, setFormStatus] = useState<"idle" | "submitting" | "success" | "error">("idle"); const [formMessage, setFormMessage] = useState(""); const renderBlock = (block: Block) => { if ((block as any).type === "form") { return null; } if (block.type === "text") { const props = block.props as TextBlockProps; const tokens = computeTextPublicTokens(props); const textStyle: CSSProperties = { ...tokens.styleOverrides }; return (

{props.text}

); } if (block.type === "formInput") { const props = block.props as FormInputBlockProps; const tokens = computeFormInputPublicTokens(props); const baseInputClass = "rounded border px-3 py-2 text-xs outline-none focus:border-sky-500"; return (