연락처 통일 - 사이트 안에 이메일이 contact@o2o.kr · o2oteam@o2o.kr 두 갈래로 갈라져 있던 것을 sales@o2o.kr 하나로 통일 (lib/contact.ts 단일 출처) - 전화번호 010.2755.6463 을 처음으로 명시. discoveryContact.ts 는 기존 import 경로를 깨지 않도록 재내보내기만 한다 푸터·문의 폼 - 푸터에 전화·이메일과 문의하기 버튼 추가 (사업자명·주소는 확인된 값이 없어 넣지 않는다) - ContactModal 신규: 성함·회사·전화·이메일·문의내용. 화면 구조는 기존 DiscoveryLeadModal 을 그대로 따라 모달이 두 종류로 보이지 않게 한다 - Navbar 문의하기를 mailto 에서 모달로 교체. 메일 앱이 없는 사용자가 mailto 에서 이탈하던 것을 막는다 수신 경로 - submitLead: Apps Script 웹앱으로 POST. 이 사이트는 서버 없는 SPA 라 중간 서버를 두지 않았다. Apps Script 는 CORS 프리플라이트에 응답하지 않아 Content-Type 을 text/plain 으로 보내 simple request 로 맞춘다 - scripts/apps-script/lead-intake.gs: 구글시트 행 추가와 알림 메일을 한 실행에서 처리. 시트 트리거는 스크립트가 쓴 변경에 늦거나 누락되는 경우가 있어 doPost 안에서 바로 보낸다 - 엔드포인트 미설정·전송 실패 시 접수된 것처럼 보이지 않게 하고 메일 폴백을 띄운다 (기존 DiscoveryLeadModal 과 같은 규칙) 토큰 실값은 저장소에 넣지 않는다. 배포본 Apps Script 와 Vercel 환경변수(VITE_LEAD_ENDPOINT · VITE_LEAD_TOKEN)에만 둔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
5.4 KiB
JavaScript
147 lines
5.4 KiB
JavaScript
/**
|
|
* webforai.kr 문의 폼 수신기 (Google Apps Script 웹앱)
|
|
*
|
|
* 하는 일
|
|
* 1. 폼 POST 를 받아 구글시트에 행 추가
|
|
* 2. 같은 실행 안에서 알림 메일 발송 (sales@o2o.kr, haewonkam@o2o.kr)
|
|
*
|
|
* 왜 트리거가 아니라 여기서 메일을 보내는가
|
|
* 시트 onChange 트리거는 스크립트가 쓴 변경에는 늦거나 누락되는 경우가 있다.
|
|
* 행을 추가한 직후 같은 실행에서 보내면 "시트에 들어오면 알림" 을 가장 확실하게 지킨다.
|
|
*
|
|
* ─────────────────────────────────────────────────────────────
|
|
* 설치 순서 (haewon 이 직접)
|
|
*
|
|
* 1. 구글시트를 새로 만든다. 이름 예: "webforai 문의 접수"
|
|
* 2. 시트에서 확장프로그램 → Apps Script 를 연다.
|
|
* 3. 기본 Code.gs 내용을 지우고 이 파일 전체를 붙여넣는다.
|
|
* 4. 아래 SHARED_TOKEN 을 임의의 문자열로 바꾼다 (예: 영문+숫자 24자).
|
|
* 빈 문자열로 두면 토큰 검사를 하지 않는다.
|
|
* 5. 배포 → 새 배포 → 유형 "웹 앱"
|
|
* - 실행 계정: 나
|
|
* - 액세스 권한: 모든 사용자 ← 이 값이어야 사이트에서 호출된다
|
|
* 배포하면 https://script.google.com/macros/s/AKfyc.../exec 주소가 나온다.
|
|
* 6. 첫 배포 때 권한 승인을 한 번 묻는다. 승인한다
|
|
* (시트 쓰기 + 메일 발송 권한).
|
|
* 7. 그 주소를 Vercel 환경변수에 넣는다 (infinith-discovery 프로젝트):
|
|
* VITE_LEAD_ENDPOINT = https://script.google.com/macros/s/AKfyc.../exec
|
|
* VITE_LEAD_TOKEN = 4번에서 정한 문자열 (토큰을 쓸 때만)
|
|
* 넣은 뒤 재배포해야 반영된다 (Vite 는 빌드 시점에 값을 넣는다).
|
|
*
|
|
* 코드를 고친 뒤에는 반드시 "배포 → 배포 관리 → 편집 → 버전: 새 버전" 으로
|
|
* 다시 배포해야 반영된다. 저장만 하면 기존 배포는 옛 코드 그대로다.
|
|
* ─────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
/** 4번에서 정한 값. 사이트의 VITE_LEAD_TOKEN 과 같아야 한다. 빈 문자열이면 검사 안 함. */
|
|
var SHARED_TOKEN = '';
|
|
|
|
/** 알림 받을 주소. */
|
|
var NOTIFY_TO = 'sales@o2o.kr,haewonkam@o2o.kr';
|
|
|
|
/** 행이 쌓일 시트 탭 이름. 없으면 만든다. */
|
|
var SHEET_NAME = '문의';
|
|
|
|
var HEADERS = ['접수시각', '성함', '회사', '전화', '이메일', '문의내용', '유입', 'User-Agent'];
|
|
|
|
function doPost(e) {
|
|
try {
|
|
var body = {};
|
|
if (e && e.postData && e.postData.contents) {
|
|
body = JSON.parse(e.postData.contents);
|
|
}
|
|
|
|
if (SHARED_TOKEN && body.token !== SHARED_TOKEN) {
|
|
return json_({ ok: false, error: 'unauthorized' });
|
|
}
|
|
|
|
// 이름이 비어 있으면 받지 않는다. 사이트 폼도 같은 규칙으로 막는다.
|
|
var name = trim_(body.name);
|
|
if (!name) {
|
|
return json_({ ok: false, error: 'name required' });
|
|
}
|
|
|
|
var row = [
|
|
new Date(),
|
|
name,
|
|
trim_(body.company),
|
|
trim_(body.phone),
|
|
trim_(body.email),
|
|
trim_(body.message),
|
|
trim_(body.source),
|
|
e && e.parameter ? trim_(e.parameter.ua) : '',
|
|
];
|
|
|
|
appendRow_(row);
|
|
notify_(row);
|
|
|
|
return json_({ ok: true });
|
|
} catch (err) {
|
|
// 실패를 삼키지 않는다. 실행 로그에 남기고 사이트에도 실패를 알린다.
|
|
console.error('doPost 실패: ' + err);
|
|
return json_({ ok: false, error: String(err) });
|
|
}
|
|
}
|
|
|
|
/** 브라우저로 주소를 열었을 때 살아 있는지 확인용. */
|
|
function doGet() {
|
|
return json_({ ok: true, service: 'webforai lead intake' });
|
|
}
|
|
|
|
function appendRow_(row) {
|
|
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
var sh = ss.getSheetByName(SHEET_NAME);
|
|
if (!sh) {
|
|
sh = ss.insertSheet(SHEET_NAME);
|
|
}
|
|
if (sh.getLastRow() === 0) {
|
|
sh.appendRow(HEADERS);
|
|
sh.getRange(1, 1, 1, HEADERS.length).setFontWeight('bold');
|
|
sh.setFrozenRows(1);
|
|
}
|
|
sh.appendRow(row);
|
|
}
|
|
|
|
function notify_(row) {
|
|
var subject = '[webforai 문의] ' + row[1] + (row[2] ? ' · ' + row[2] : '');
|
|
var lines = [
|
|
'성함: ' + (row[1] || '-'),
|
|
'회사: ' + (row[2] || '-'),
|
|
'전화: ' + (row[3] || '-'),
|
|
'이메일: ' + (row[4] || '-'),
|
|
'유입: ' + (row[6] || '-'),
|
|
'',
|
|
'문의 내용:',
|
|
row[5] || '-',
|
|
'',
|
|
'시트: ' + SpreadsheetApp.getActiveSpreadsheet().getUrl(),
|
|
];
|
|
MailApp.sendEmail({
|
|
to: NOTIFY_TO,
|
|
subject: subject,
|
|
body: lines.join('\n'),
|
|
// 회신하면 문의한 사람에게 바로 가게 한다. 이메일을 안 남겼으면 설정하지 않는다.
|
|
replyTo: row[4] || undefined,
|
|
});
|
|
}
|
|
|
|
function json_(obj) {
|
|
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(
|
|
ContentService.MimeType.JSON
|
|
);
|
|
}
|
|
|
|
function trim_(v) {
|
|
return v === null || v === undefined ? '' : String(v).trim();
|
|
}
|
|
|
|
/**
|
|
* 설치 확인용. Apps Script 편집기에서 이 함수를 한 번 실행하면
|
|
* 시트에 테스트 행이 들어가고 알림 메일이 오는지 바로 볼 수 있다.
|
|
*/
|
|
function testRun() {
|
|
var row = [new Date(), '테스트', '설치 확인', '010-0000-0000', 'test@example.com', '설치 확인용 테스트입니다.', 'test', ''];
|
|
appendRow_(row);
|
|
notify_(row);
|
|
}
|