푸터 (haewon 2026-10-01) - aio2o.webforai.kr 푸터와 같은 내용으로 맞춘다: 법인명·본사·연구소 주소와 전화·팩스·이메일. 값의 원본은 그쪽 data/facts.json 이며 지어내지 않았다. - 전화 표기를 점에서 하이픈으로 바꿔 두 사이트를 같게 한다. 문의 폼 보호 장치 - 허니팟: 사람 눈에 안 보이는 입력칸(website)을 두고, 값이 차 있으면 스크립트가 저장하지 않는다. display:none 을 건너뛰는 봇이 있어 화면 밖으로 밀어내고 aria-hidden·tabIndex -1 로 보조기기와 탭 이동에서는 뺀다. - 속도 제한: Apps Script 가 한 시간 30건까지만 받는다. VITE_LEAD_TOKEN 은 빌드 시점에 번들로 들어가 공개값이라 보호 장치가 못 된다. 주소와 토큰이 알려져도 피해가 상한 안에 묶이게 한다. - 알림 실패 분리: 메일 발송이 실패해도 행은 남기고 접수로 응답한다. 지금까지는 메일이 막히면(한도 소진·구글 장애) 저장됐는데도 고객 화면에 "전송 실패"가 떠 접수된 줄 모르고 돌아가게 되어 있었다. 시트에 '알림' 열을 추가해 '보냄' / '실패(메일만)' 을 적어 메일이 안 간 문의를 찾을 수 있게 한다. 토큰 실값은 저장소에 넣지 않는다. 배포본 Apps Script 와 Vercel 환경변수에만 둔다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59 lines
2.6 KiB
TypeScript
59 lines
2.6 KiB
TypeScript
/**
|
|
* 문의 폼 전송.
|
|
*
|
|
* 받는 쪽은 Google Apps Script 웹앱이다(scripts/apps-script/lead-intake.gs).
|
|
* 그 스크립트 하나가 구글시트에 행을 추가하고 알림 메일까지 보낸다.
|
|
* 이 사이트는 서버가 없는 SPA 라 중간 서버를 두지 않았다.
|
|
*
|
|
* Apps Script 웹앱은 CORS 프리플라이트에 응답하지 않는다. 그래서 단순 요청(simple request)
|
|
* 조건을 지켜 Content-Type 을 text/plain 으로 보낸다. 스크립트가 본문을 JSON 으로 파싱한다.
|
|
* 헤더를 application/json 으로 바꾸면 프리플라이트가 발생해 브라우저에서 차단된다.
|
|
*/
|
|
|
|
/** Vercel 환경변수 VITE_LEAD_ENDPOINT (Apps Script 배포 URL). 없으면 전송하지 않는다. */
|
|
const ENDPOINT = import.meta.env.VITE_LEAD_ENDPOINT as string | undefined;
|
|
|
|
/** 선택. 스크립트 쪽 SHARED_TOKEN 과 같은 값을 넣으면 외부의 무작위 호출을 걸러낸다. */
|
|
const TOKEN = import.meta.env.VITE_LEAD_TOKEN as string | undefined;
|
|
|
|
export interface LeadInput {
|
|
name: string;
|
|
company: string;
|
|
phone: string;
|
|
email: string;
|
|
message: string;
|
|
/** 어느 화면에서 열었는지 (footer · navbar) */
|
|
source: string;
|
|
/**
|
|
* 허니팟. 사람에게는 보이지 않는 칸이라 항상 빈 문자열이어야 한다.
|
|
* 값이 차 있으면 스크립트가 봇으로 보고 저장하지 않는다.
|
|
* 이름은 Apps Script 의 HONEYPOT_FIELD 와 같아야 한다.
|
|
*/
|
|
website: string;
|
|
}
|
|
|
|
export type LeadResult = { ok: true } | { ok: false; reason: 'unconfigured' | 'failed' };
|
|
|
|
export async function submitLead(input: LeadInput): Promise<LeadResult> {
|
|
if (!ENDPOINT) {
|
|
// 엔드포인트가 없는데 성공했다고 말하지 않는다. 화면은 메일 폴백을 보여준다.
|
|
console.warn('[submitLead] VITE_LEAD_ENDPOINT 가 설정되지 않았습니다.');
|
|
return { ok: false, reason: 'unconfigured' };
|
|
}
|
|
try {
|
|
const res = await fetch(ENDPOINT, {
|
|
method: 'POST',
|
|
// 프리플라이트를 피하기 위한 simple request. 위 주석 참고.
|
|
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
|
|
body: JSON.stringify({ ...input, token: TOKEN ?? '', submittedAt: new Date().toISOString() }),
|
|
});
|
|
if (!res.ok) return { ok: false, reason: 'failed' };
|
|
const data = await res.json().catch(() => null);
|
|
// 스크립트는 { ok: true } 를 돌려준다. 형식이 다르면 실패로 본다.
|
|
return data && data.ok ? { ok: true } : { ok: false, reason: 'failed' };
|
|
} catch (e) {
|
|
console.error('[submitLead] 전송 실패', e);
|
|
return { ok: false, reason: 'failed' };
|
|
}
|
|
}
|