From c519055e90bc82524fe83b6a7b93768352155cb1 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sun, 16 Nov 2025 18:29:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Sheets=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EB=B3=B4=EA=B0=95(TDD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Code.gs에 @tool/@version 헤더, 필드 매핑 TODO, try/catch 에러 처리 추가 - README.txt에 Web App 배포/필드 매핑 가이드 추가 - 테스트 추가: sheets.template.enhance.test.ts - 전체 테스트 그린 유지 --- README.md | 4 +- lib/exporter/extras.ts | 18 ++++++-- lib/exporter/sheets.template.enhance.test.ts | 46 ++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 lib/exporter/sheets.template.enhance.test.ts diff --git a/README.md b/README.md index 9b178af..ce13530 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,15 @@ Simple index to quickly find assets by group: ### assets.integrity.index.json -- entries: array of `{ path, integrity }` +- entries: array of `{ path, integrity, size }` - `path`: asset `zipPath` (e.g. `assets/abcd1234.png` or grouped `assets/images/...`) - `integrity`: `sha256-` (real SHA-256 of the asset bytes) + - `size`: byte length of the asset - bundleHash: `sha256-` over a deterministic join of `path` and `integrity` for all entries Usage: - Verify file-level integrity by recomputing SHA-256(base64) of each asset and comparing with `entries[].integrity`. +- Verify file size matches `entries[].size`. - Verify bundle integrity by recomputing `bundleHash` from sorted entries; useful for quick tamper detection. ### Google Sheets template (optional) diff --git a/lib/exporter/extras.ts b/lib/exporter/extras.ts index 65bb975..052f19e 100644 --- a/lib/exporter/extras.ts +++ b/lib/exporter/extras.ts @@ -68,13 +68,25 @@ export function buildExtrasForPage(page: Page, overrides?: ExtrasOverrides): Rec const code = `/** * Google Apps Script Web App for form submissions * - Deploy as Web App and set URL as form action if needed. + * @tool: landing-builder + * @version: 1.0.0 */ function doPost(e){ - // TODO: handle submission to Google Sheets - return ContentService.createTextOutput('OK') + try { + // TODO: map incoming fields to sheet columns + // Example: + // const payload = JSON.parse(e.postData.contents || '{}'); + // const row = [payload.name, payload.email, new Date()]; + // SpreadsheetApp.openById('YOUR_SHEET_ID').getSheetByName('Sheet1').appendRow(row); + return ContentService.createTextOutput('OK') + } catch (e) { + return ContentService + .createTextOutput('ERROR: ' + (e && (e as any).message ? (e as any).message : 'unknown')) + .setMimeType(ContentService.MimeType.TEXT) + } } ` - const readme = `Google Sheets Apps Script Template\n\n1) Apps Script에서 새 프로젝트를 만들고 아래 Code.gs를 붙여넣으세요.\n2) 배포 > 웹 앱으로 배포 후 URL을 복사해 사용하세요.` + const readme = `Google Sheets Apps Script Template\n\n1) Apps Script에서 새 프로젝트를 만들고 아래 Code.gs를 붙여넣으세요.\n2) Deploy as Web App로 배포 후 URL을 복사해 사용하세요.\n3) Map fields to columns: 제출 payload의 필드를 시트 컬럼에 매핑하세요.` out['sheets/Code.gs'] = toBytes(code) out['sheets/README.txt'] = toBytes(readme) } diff --git a/lib/exporter/sheets.template.enhance.test.ts b/lib/exporter/sheets.template.enhance.test.ts new file mode 100644 index 0000000..c757451 --- /dev/null +++ b/lib/exporter/sheets.template.enhance.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest' +import { buildExtrasForPage } from '@/lib/exporter/extras' +import type { Page } from '@/lib/schema/page' + +function td(b: Uint8Array){ return new TextDecoder().decode(b) } + +const basePage: Page = { + title: 'Test', + seo: { title: 'T', description: 'D' }, + theme: { primaryColor: '#0ea5e9', fontFamily: 'Inter' }, + analytics: { ga4MeasurementId: '', metaPixelId: '', customHeadHtml: '' }, + sections: [], + form: { + fields: [], + actionUrl: '', + spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 }, + }, + locale: 'en', +} + +describe('Sheets template enhance', () => { + it('includes version/meta and mapping/error guide in Code.gs and README', () => { + const extras = buildExtrasForPage(basePage, { sheetsMode: 'include' }) + const code = td(extras['sheets/Code.gs']) + const readme = td(extras['sheets/README.txt']) + + // version/meta header + expect(code).toMatch(/@version:\s*\d+\.\d+\.\d+/) + expect(code).toMatch(/@tool:\s*landing-builder/i) + + // field mapping and error handling guides + expect(code).toMatch(/TODO:\s*map incoming fields to sheet columns/i) + expect(code).toMatch(/try\s*\{/) + expect(code).toMatch(/catch\s*\(e\)/) + + // README includes setup and mapping hint + expect(readme).toMatch(/Apps Script/i) + expect(readme).toMatch(/Deploy as Web App/i) + expect(readme).toMatch(/Map fields to columns/i) + }) + + it('respects exclude mode', () => { + const extras = buildExtrasForPage(basePage, { sheetsMode: 'exclude' }) + expect(Object.keys(extras).some(k => k.startsWith('sheets/'))).toBe(false) + }) +})