diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts index aa26aaf..cefdcca 100644 --- a/src/app/api/export/route.ts +++ b/src/app/api/export/route.ts @@ -343,13 +343,24 @@ export const buildStaticHtml = (blocks: Block[], projectConfig?: ProjectConfig): } // 컨트롤러 전용 FormBlock: fieldIds 기반으로 개별 필드 블록과 연결되는 순수 컨트롤러 폼만 생성한다. - // - Export 에서는 레이아웃/스타일을 가지지 않고, id/메서드만 가지는 최소한의
`); + parts.push( + `"); 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" }); diff --git a/src/app/api/forms/submit/route.ts b/src/app/api/forms/submit/route.ts index e105738..8e70f5c 100644 --- a/src/app/api/forms/submit/route.ts +++ b/src/app/api/forms/submit/route.ts @@ -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( diff --git a/src/features/editor/state/editorStore.ts b/src/features/editor/state/editorStore.ts index ad2c461..c6c39a2 100644 --- a/src/features/editor/state/editorStore.ts +++ b/src/features/editor/state/editorStore.ts @@ -643,7 +643,7 @@ export interface FormFieldConfig { } // 폼 블록 속성 -export type FormSubmitTarget = "internal" | "webhook"; +export type FormSubmitTarget = "internal" | "webhook" | "both"; export type FormPayloadFormat = "form" | "json"; export interface FormBlockProps { diff --git a/tests/api/export.spec.ts b/tests/api/export.spec.ts index e3c6300..c440a90 100644 --- a/tests/api/export.spec.ts +++ b/tests/api/export.spec.ts @@ -65,6 +65,11 @@ describe("/api/export", () => { expect(cssEntry).toBeTruthy(); expect(jsEntry).toBeTruthy(); + const mainJs = await jsEntry!.async("string"); + expect(mainJs).toContain("pbInitFormControllers"); + expect(mainJs).toContain('querySelectorAll("form.pb-form-controller")'); + expect(mainJs).toContain("fetch("); + const html = await indexEntry!.async("string"); expect(html).toContain("