feat(export): Google Sheets Apps Script 템플릿 extras 추가\n\n- actionUrl 미지정 시 sheets/Code.gs, README.txt 포함\n- buildExtrasForPage 유닛/통합 테스트 추가
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import type { Page } from '@/lib/schema/page'
|
||||
|
||||
// Sheets 템플릿 등 ZIP 루트/서브 경로에 포함할 추가 파일(extras)을 구성한다.
|
||||
// - actionUrl이 비어있을 때만 Google Apps Script 템플릿을 포함한다.
|
||||
export function buildExtrasForPage(page: Page): Record<string, Uint8Array> {
|
||||
const extras: Record<string, Uint8Array> = {}
|
||||
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
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user