test(ci): add exporter heading levels and a11y typography smoke; update MAIN_PLAN
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user