import { NextResponse } from "next/server"; import { promises as fs } from "fs"; import path from "path"; import JSZip from "jszip"; import type { Block, ProjectConfig, FormBlockProps, FormSelectOption, TextBlockProps, ButtonBlockProps, SectionBlockProps, VideoBlockProps, ListBlockProps, } from "@/features/editor/state/editorStore"; import { normalizeVideoSourceUrl, resolveVideoPlatform, buildVideoEmbedUrl, computeVideoExportTokens, } from "@/features/editor/utils/videoHelpers"; import { computeTextPbTokens } from "@/features/editor/utils/textHelpers"; import { computeButtonPbTokens } from "@/features/editor/utils/buttonHelpers"; import { computeImageExportStyles } from "@/features/editor/utils/imageHelpers"; import { computeSectionExportTokens } from "@/features/editor/utils/sectionHelpers"; import { computeListExportTokens } from "@/features/editor/utils/listHelpers"; import { computeFormInputExportTokens, computeFormSelectExportTokens, computeFormCheckboxExportTokens, computeFormRadioExportTokens, computeFormBlockExportTokens, } from "@/features/editor/utils/formHelpers"; import { computeDividerExportTokens } from "@/features/editor/utils/dividerHelpers"; type ExportRequestBody = { blocks: Block[]; projectConfig?: ProjectConfig; }; const BUILDER_CSS_PATH = path.join(process.cwd(), "src", "styles", "builder.css"); const UPLOAD_DIR = path.join(process.cwd(), "uploads"); const escapeHtml = (value: string): string => value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); const escapeAttr = (value: string): string => escapeHtml(value); export const buildStaticHtml = (blocks: Block[], projectConfig?: ProjectConfig): string => { const baseTitleRaw = (projectConfig?.title ?? "").trim() || "Page Builder Export"; const seoTitleRaw = (projectConfig?.seoTitle ?? "").trim(); const pageTitleRaw = seoTitleRaw || baseTitleRaw; const pageTitle = escapeHtml(pageTitleRaw); const headExtraRaw = (projectConfig?.headHtml ?? "").trim(); const headExtra = headExtraRaw ? `\n${headExtraRaw}\n` : ""; const preset = projectConfig?.canvasPreset ?? "full"; let maxWidth: string | null = null; const widthPx = typeof projectConfig?.canvasWidthPx === "number" && projectConfig.canvasWidthPx > 0 ? projectConfig.canvasWidthPx : null; if (widthPx != null) { maxWidth = `${widthPx}px`; } else if (preset === "mobile") { maxWidth = "390px"; } else if (preset === "tablet") { maxWidth = "768px"; } else if (preset === "desktop") { maxWidth = "1200px"; } const bgColor = (projectConfig?.canvasBgColorHex ?? "").trim(); const widthPart = maxWidth != null ? `max-width:${maxWidth};` : ""; const bgPart = bgColor ? `background-color:${bgColor};` : ""; const basePart = "margin:0 auto;padding:24px;box-sizing:border-box;"; const canvasStyle = `${widthPart}${bgPart}${basePart}`; const bodyBgRaw = (projectConfig?.bodyBgColorHex ?? "#020617").trim() || "#020617"; const bodyStyle = `background-color:${bodyBgRaw};margin:0;padding:0;`; const trackingRaw = (projectConfig?.trackingScript ?? "").trim(); const trackingHtml = trackingRaw ? `\n${trackingRaw}\n` : ""; const seoDescriptionRaw = (projectConfig?.seoDescription ?? "").trim(); const seoOgImageRaw = (projectConfig?.seoOgImageUrl ?? "").trim(); const seoCanonicalRaw = (projectConfig?.seoCanonicalUrl ?? "").trim(); const seoNoIndex = projectConfig?.seoNoIndex === true; const seoHeadParts: string[] = []; if (seoDescriptionRaw) { seoHeadParts.push( ` `, ); } if (pageTitleRaw) { seoHeadParts.push( ` `, ); } if (seoDescriptionRaw) { seoHeadParts.push( ` `, ); } seoHeadParts.push(` `); if (seoOgImageRaw) { seoHeadParts.push( ` `, ); } if (seoCanonicalRaw) { seoHeadParts.push( ` `, ); } // Twitter 카드 메타 태그 seoHeadParts.push( ` `, ); if (pageTitleRaw) { seoHeadParts.push( ` `, ); } if (seoDescriptionRaw) { seoHeadParts.push( ` `, ); } if (seoOgImageRaw) { seoHeadParts.push( ` `, ); } if (seoCanonicalRaw) { seoHeadParts.push( ` `, ); } if (seoNoIndex) { seoHeadParts.push( ` `, ); } const seoHeadHtml = seoHeadParts.length > 0 ? `\n${seoHeadParts.join("\n")}` : ""; const sectionBlocks = blocks.filter((b) => b.type === "section"); const rootBlocks = blocks.filter((b) => !b.sectionId && b.type !== "section"); // Form 컨트롤러(FormBlock)와 개별 필드 블록(formInput/formSelect/...) 를 연결하기 위한 맵. // - key: 필드 블록 id (fieldIds 에 포함된 id) // - value: 해당 필드가 속한