/* * 리드 수신용 Google Apps Script — 데모 요청·상담 신청을 스프레드시트에 적재한다. * * 이 파일은 랜딩 번들에 포함되지 않는다. Google Apps Script 편집기에 붙여넣을 원본이고, * 여기 두는 이유는 배포 후 이 코드가 어디에도 남지 않으면 나중에 아무도 손댈 수 없기 때문이다. * * ── 설치 (약 3분) ──────────────────────────────────────────────────────────── * 1. 새 Google 스프레드시트를 만든다. 시트 이름은 그대로 둬도 된다. * 2. 확장 프로그램 → Apps Script → 기본 코드를 지우고 이 파일 전체를 붙여넣는다. * 3. 배포 → 새 배포 → 유형 "웹 앱" * 실행 계정 : 나 * 액세스 권한 : 모든 사용자 ← 이걸 "나"로 두면 랜딩에서 호출이 막힌다 * 4. 배포하면 나오는 URL(https://script.google.com/macros/s/…/exec)을 복사한다. * 5. Vercel → negotium-landing → Settings → Environment Variables 에 * 이름 LEAD_WEBHOOK_URL / 값 복사한 URL / Production 체크 * 6. 다시 배포한다. 환경변수는 배포 시점에 주입되므로 재배포 전에는 적용되지 않는다. * * ── 동작 ──────────────────────────────────────────────────────────────────── * api/lead.ts 가 아래 형태로 POST 한다. 필드가 늘어도 헤더를 자동으로 확장하므로 * 이 스크립트를 다시 고칠 일은 거의 없다. * { text, source, name, email, company, phone, message, submittedAt, userAgent } * * 주의: 웹 앱은 POST 에 302 를 돌려주고 script.googleusercontent.com 으로 넘긴다. * api/lead.ts 의 fetch 는 리다이렉트를 따라가므로 최종 200 을 받는다 — 정상이다. */ /** 리드가 쌓일 시트 이름. 없으면 자동 생성한다. */ var SHEET_NAME = 'leads' /** 항상 이 순서로 왼쪽부터 채운다. 나머지 필드는 뒤에 자동으로 붙는다. */ var PREFERRED = ['submittedAt', 'source', 'name', 'email', 'company', 'phone', 'message', 'userAgent'] function doPost(e) { try { var payload = JSON.parse((e && e.postData && e.postData.contents) || '{}') // text 는 Slack 전용 요약이라 시트에는 넣지 않는다. 같은 내용이 개별 필드에 이미 있다. delete payload.text var lock = LockService.getScriptLock() lock.waitLock(20000) // 동시 제출이 같은 행에 겹쳐 쓰는 것을 막는다 try { var sheet = getSheet_() var header = ensureHeader_(sheet, payload) var row = header.map(function (key) { return payload[key] === undefined ? '' : payload[key] }) sheet.appendRow(row) } finally { lock.releaseLock() } return json_({ ok: true }) } catch (err) { // 실패해도 랜딩 쪽 api/lead.ts 가 이미 로그를 남겼으므로 리드 자체는 보존된다. return json_({ ok: false, error: String(err) }) } } /** 브라우저로 URL 을 열었을 때 살아있는지 확인용. 배포 직후 점검에 쓴다. */ function doGet() { return json_({ ok: true, service: 'negotium lead sink' }) } function getSheet_() { var ss = SpreadsheetApp.getActiveSpreadsheet() return ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME) } /** 헤더가 없으면 만들고, 처음 보는 필드가 오면 열을 덧붙인다. */ function ensureHeader_(sheet, payload) { var lastCol = sheet.getLastColumn() var header = lastCol ? sheet.getRange(1, 1, 1, lastCol).getValues()[0].filter(String) : [] if (!header.length) { header = PREFERRED.filter(function (k) { return k in payload }) Object.keys(payload).forEach(function (k) { if (header.indexOf(k) === -1) header.push(k) }) sheet.getRange(1, 1, 1, header.length).setValues([header]).setFontWeight('bold') sheet.setFrozenRows(1) return header } var added = Object.keys(payload).filter(function (k) { return header.indexOf(k) === -1 }) if (added.length) { sheet.getRange(1, header.length + 1, 1, added.length).setValues([added]).setFontWeight('bold') header = header.concat(added) } return header } function json_(obj) { return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON) }