From 058b5009e71cdd7c33fff06a77f379edfb541173 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sat, 15 Nov 2025 01:00:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(export):=20Google=20Sheets=20Apps=20Script?= =?UTF-8?q?=20=ED=85=9C=ED=94=8C=EB=A6=BF=20extras=20=EC=B6=94=EA=B0=80\n\?= =?UTF-8?q?n-=20actionUrl=20=EB=AF=B8=EC=A7=80=EC=A0=95=20=EC=8B=9C=20shee?= =?UTF-8?q?ts/Code.gs,=20README.txt=20=ED=8F=AC=ED=95=A8\n-=20buildExtrasF?= =?UTF-8?q?orPage=20=EC=9C=A0=EB=8B=9B/=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/exporter/extras.ts | 44 ++++++++++++++++++++++++++ lib/exporter/sheets.template.test.ts | 47 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 lib/exporter/extras.ts create mode 100644 lib/exporter/sheets.template.test.ts diff --git a/lib/exporter/extras.ts b/lib/exporter/extras.ts new file mode 100644 index 0000000..a29b7fa --- /dev/null +++ b/lib/exporter/extras.ts @@ -0,0 +1,44 @@ +import type { Page } from '@/lib/schema/page' + +// Sheets 템플릿 등 ZIP 루트/서브 경로에 포함할 추가 파일(extras)을 구성한다. +// - actionUrl이 비어있을 때만 Google Apps Script 템플릿을 포함한다. +export function buildExtrasForPage(page: Page): Record { + const extras: Record = {} + const te = new TextEncoder() + + const hasAction = !!(page.form?.actionUrl && page.form.actionUrl.trim().length > 0) + if (!hasAction) { + const code = `// Google Apps Script (Code.gs) +// - Web App 으로 배포 후, 아래 doPost 핸들러가 폼 데이터를 수신합니다. +// - 시트에 저장하거나 이메일 전송 로직을 추가하세요. +function doPost(e) { + try { + var data = e.parameter || {} + // TODO: 시트에 저장 예시 + // var ss = SpreadsheetApp.getActiveSpreadsheet() + // var sheet = ss.getSheetByName('Responses') || ss.insertSheet('Responses') + // sheet.appendRow([new Date(), JSON.stringify(data)]) + return ContentService.createTextOutput(JSON.stringify({ ok: true })) + .setMimeType(ContentService.MimeType.JSON) + } catch (err) { + return ContentService.createTextOutput(JSON.stringify({ ok: false, error: String(err) })) + .setMimeType(ContentService.MimeType.JSON) + } +} +` + const readme = `README (Google Sheets Apps Script 템플릿) + +1) Google Drive에서 새 스크립트(Apps Script) 프로젝트를 생성합니다. +2) Code.gs 파일 내용을 붙여넣습니다. +3) Deploy > New deployment > Web app 으로 배포합니다. + - Execute as: Me + - Who has access: Anyone +4) 배포 URL을 복사하여 빌더의 Form action URL에 넣습니다. +5) 필요한 경우 doPost에서 스프레드시트 저장 로직을 추가하세요. +` + extras['sheets/Code.gs'] = te.encode(code) + extras['sheets/README.txt'] = te.encode(readme) + } + + return extras +} diff --git a/lib/exporter/sheets.template.test.ts b/lib/exporter/sheets.template.test.ts new file mode 100644 index 0000000..3a756a3 --- /dev/null +++ b/lib/exporter/sheets.template.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest' +import { buildZip } from '@/lib/exporter/zip' +import { exportPage } from '@/lib/exporter/html' +import { pageSchema, type Page } from '@/lib/schema/page' +import { buildExtrasForPage } from '@/lib/exporter/extras' + +function makePageNoAction(): Page { + return pageSchema.parse({ + title: 'Sheets Tpl', + locale: 'en', + theme: { primaryColor: '#2563eb', fontFamily: 'Inter' }, + sections: [ { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'https://img/h.png' } } ], + form: { actionUrl: '', fields: [ { type: 'text', name: 'email', label: 'Email', required: true } ], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + seo: { title: 't', description: 'd' }, + }) +} + +function makePageWithAction(): Page { + return pageSchema.parse({ + title: 'Sheets Off', + locale: 'en', + theme: { primaryColor: '#2563eb', fontFamily: 'Inter' }, + sections: [ { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'https://img/h.png' } } ], + form: { actionUrl: 'https://example.com/submit', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + seo: { title: 't', description: 'd' }, + }) +} + +describe('Sheets 템플릿 Export', () => { + it('actionUrl 미지정 시 extras에 Sheets 템플릿 파일을 포함한다', async () => { + const page = makePageNoAction() + const extras = buildExtrasForPage(page) + expect(Object.keys(extras)).toContain('sheets/Code.gs') + expect(Object.keys(extras)).toContain('sheets/README.txt') + + const out = exportPage(page) + const zipBytes = await buildZip({ html: out.html, css: out.css, js: out.js, assets: {}, extras }) + expect(zipBytes.byteLength).toBeGreaterThan(0) + }) + + it('actionUrl 지정 시 extras에는 Sheets 템플릿이 포함되지 않는다', () => { + const page = makePageWithAction() + const extras = buildExtrasForPage(page) + expect(Object.keys(extras)).not.toContain('sheets/Code.gs') + expect(Object.keys(extras)).not.toContain('sheets/README.txt') + }) +}) -- 2.52.0