type TemplateParams = { sheetId: string allowedOrigins: string[] minSubmitSeconds: number honeypotFieldName: string sheetName?: string } export function generateAppsScriptTemplate(params: TemplateParams) { const { sheetId, allowedOrigins, minSubmitSeconds, honeypotFieldName, sheetName = 'Responses' } = params const code = `/** * Apps Script Web App for accepting HTML form submissions and writing to Google Sheets. * Deployed as Web App with Anyone (even anonymous) or Anyone within domain access, as needed. */ const CONFIG = { SHEET_ID: '${sheetId}', SHEET_NAME: '${sheetName}', ALLOWED_ORIGINS: ${JSON.stringify(allowedOrigins)}, minSubmitSeconds: ${minSubmitSeconds}, HONEYPOT: '${honeypotFieldName}', }; function doOptions(e) { return corsResponse('', 204); } function doPost(e) { try { const origin = (e?.parameter?.origin) || (e?.headers && e.headers['Origin']) || ''; const now = Date.now(); const params = parseBody(e); // CORS check enforceCors(origin); // Spam protection: honeypot must be empty if (params[CONFIG.HONEYPOT]) { return corsResponse(JSON.stringify({ ok: true }), 200, origin); // pretend success } // Spam protection: minimum submit time const ts = Number(params['_ts'] || 0); if (!isFinite(ts) || now - ts < CONFIG.minSubmitSeconds * 1000) { return corsResponse(JSON.stringify({ ok: true }), 200, origin); // pretend success } // Write to sheet const ss = SpreadsheetApp.openById(CONFIG.SHEET_ID); const sh = ss.getSheetByName(CONFIG.SHEET_NAME) || ss.getSheets()[0]; const entries = Object.keys(params).filter(k => k !== CONFIG.HONEYPOT); const values = entries.map(k => String(params[k])); sh.appendRow([new Date()].concat(values)); return corsResponse(JSON.stringify({ ok: true }), 200, origin); } catch (err) { return corsResponse(JSON.stringify({ ok: false, error: String(err) }), 500); } } function parseBody(e) { // Accept form-urlencoded or JSON if (e?.postData?.type === 'application/json') { try { return JSON.parse(e.postData.contents || '{}'); } catch (_) { return {}; } } // Parse form-urlencoded const raw = (e?.postData?.contents || ''); const out = {}; raw.split('&').forEach(p => { const [k, v] = p.split('='); if (k) out[decodeURIComponent(k)] = decodeURIComponent((v || '').replace(/\+/g, ' ')); }); // Also include e.parameter for safety if (e?.parameter) { Object.keys(e.parameter).forEach(k => { if (!(k in out)) out[k] = e.parameter[k]; }); } return out; } function enforceCors(origin) { if (!CONFIG.ALLOWED_ORIGINS || CONFIG.ALLOWED_ORIGINS.length === 0) return; if (CONFIG.ALLOWED_ORIGINS.indexOf(origin) === -1) { throw new Error('CORS: Origin not allowed: ' + origin); } } function corsResponse(body, status, origin) { const output = ContentService.createTextOutput(body).setMimeType(ContentService.MimeType.JSON); const resp = HtmlService.createHtmlOutput().getResponse(); resp.setContent(output.getContent()); resp.setResponseCode(status || 200); resp.setHeader('Access-Control-Allow-Origin', origin || '*'); resp.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); resp.setHeader('Access-Control-Allow-Headers', 'Content-Type'); return resp; } `; const readme = `# Google Apps Script Web App (Google Sheets 저장) 이 스크립트는 HTML 폼 제출을 받아 Google 스프레드시트에 행을 추가합니다. ## 준비물 - Google 스프레드시트 생성 후 시트 ID 확인(주소창의 /d/ 와 /edit 사이) - Apps Script 에디터 열기(스프레드시트 -> 확장 프로그램 -> Apps Script) ## 사용 방법 1. 새 프로젝트 생성 후, Code.gs 에 아래 코드 붙여넣기 2. CONFIG 내 값을 편집 - SHEET_ID: ${sheetId} - SHEET_NAME: ${sheetName} - ALLOWED_ORIGINS: ${JSON.stringify(allowedOrigins)} - MIN_SUBMIT_SECONDS: ${minSubmitSeconds} - HONEYPOT: ${honeypotFieldName} 3. 배포 - 상단 메뉴: Deploy -> Deploy as Web app - 새 배포 -> Entry point: doPost - Access: Anyone (권장) 또는 필요 정책에 맞게 설정 - Deploy를 눌러 Web app URL 획득 4. 빌더에서 폼 action URL 에 위 Web app URL 입력 ## CORS 및 스팸 방지 - Access-Control-Allow-Origin 헤더가 설정됩니다. - 허용 오리진만 통과하도록 enforceCors를 사용합니다. - honeypot 필드와 제출 타임스탬프(_ts)로 기본 스팸 방지 동작을 합니다. ## 테스트 - doPost, SpreadsheetApp 관련 호출은 Apps Script 런타임에서 동작합니다. ` return { code, readme } }