form export 수정 및 통합테스트
CI / test (push) Successful in 49m26s
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-11-28 14:25:30 +09:00
parent c7b6097bd9
commit c9a62d479c
16 changed files with 234 additions and 5 deletions
+60 -3
View File
@@ -343,13 +343,24 @@ export const buildStaticHtml = (blocks: Block[], projectConfig?: ProjectConfig):
}
// 컨트롤러 전용 FormBlock: fieldIds 기반으로 개별 필드 블록과 연결되는 순수 컨트롤러 폼만 생성한다.
// - Export 에서는 레이아웃/스타일을 가지지 않고, id/메서드만 가지는 최소한의 <form> 요소만 만든다.
// - Export 에서는 레이아웃/스타일 대신 id/메서드/액션만 가지는 최소한의 <form> 요소만 만든다.
// - action 은 퍼블릭 페이지에서 우리 서버의 forms/submit 엔드포인트로 전송하기 위한 고정 경로다.
const formId = `form_${block.id}`;
const method = props.method ?? "POST";
const methodAttr = ` method="${method.toLowerCase()}"`;
const actionAttr = ` action="/api/forms/submit"`;
// 정적 Export 에서도 FormBlock 설정(전송 대상/웹훅 설정 등)을 함께 전달할 수 있도록
// preview 렌더러와 동일하게 __config hidden 필드에 FormBlockProps 전체를 JSON 으로 담아둔다.
const configJson = JSON.stringify(props ?? {});
const escapedConfig = escapeAttr(configJson);
const parts: string[] = [];
parts.push(`<form id="${escapeAttr(formId)}" class="pb-form-controller"${methodAttr}></form>`);
parts.push(
`<form id="${escapeAttr(formId)}" class="pb-form-controller"${methodAttr}${actionAttr}>`,
);
parts.push(`<input type="hidden" name="__config" value="${escapedConfig}" />`);
parts.push("</form>");
return parts.join("");
}
@@ -618,7 +629,53 @@ export async function POST(request: Request) {
zip.file("builder.css", "/* builder.css 를 찾을 수 없습니다. */\n");
}
const mainJs = `console.log("Page Builder static export loaded");`;
const mainJs = [
"(function(){",
" function pbInitFormControllers(){",
" var forms = document.querySelectorAll(\"form.pb-form-controller\");",
" if (!forms || !forms.length) { return; }",
" forms.forEach(function(form){",
" form.addEventListener(\"submit\", function(event){",
" if (!form) { return; }",
" if (event && typeof event.preventDefault === 'function') { event.preventDefault(); }",
" var formData = new FormData(form);",
" var configInput = form.querySelector('input[name=\"__config\"]');",
" var config = null;",
" if (configInput && configInput.value) {",
" try { config = JSON.parse(configInput.value); } catch (e) { config = null; }",
" }",
" var action = form.getAttribute('action') || '/api/forms/submit';",
" fetch(action, { method: 'POST', body: formData })",
" .then(function(res){ return res.json().catch(function(){ return {}; }); })",
" .then(function(data){",
" var ok = data && data.ok;",
" var message = data && data.message;",
" if (ok) {",
" var success = message || (config && config.successMessage) || '성공적으로 전송되었습니다.';",
" if (typeof alert === 'function') { alert(success); }",
" try { form.reset(); } catch (e) {}",
" } else {",
" var errorMsg = message || (config && config.errorMessage) || '전송 중 오류가 발생했습니다.';",
" if (typeof alert === 'function') { alert(errorMsg); }",
" }",
" })",
" .catch(function(){",
" var errorMsg = (config && config.errorMessage) || '전송 중 오류가 발생했습니다.';",
" if (typeof alert === 'function') { alert(errorMsg); }",
" });",
" });",
" });",
" }",
" if (typeof document !== 'undefined') {",
" if (document.readyState === 'loading') {",
" document.addEventListener('DOMContentLoaded', pbInitFormControllers);",
" } else {",
" pbInitFormControllers();",
" }",
" }",
" console.log('Page Builder static export loaded');",
"})();",
].join("\n");
zip.file("main.js", mainJs);
const zipArrayBuffer = await zip.generateAsync({ type: "arraybuffer" });
+5 -1
View File
@@ -31,8 +31,12 @@ export async function POST(req: Request) {
return NextResponse.json({ ok: true, message: successMessage });
}
if (submitTarget === "both") {
console.log("[forms/submit][internal]", { name, email, message });
}
// 2) webhook: destinationUrl 로 서버에서 POST (Google Sheets 등 포함)
if (submitTarget === "webhook") {
if (submitTarget === "webhook" || submitTarget === "both") {
const destinationUrl = config?.destinationUrl;
if (!destinationUrl) {
return NextResponse.json(