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') + }) +})