Cloudflare 환경 OTP·Passcode 이메일 발송 현황 및 Frappe 불경유 발송 기획서
작성일: 2026-02-27 목적: (1) Cloudflare(D1, KV, Worker, Pages) 환경에서 OTP·passcode 등 이메일을 보내는 로직 현황을 확인하고, (2) Frappe를 거치지 않고KV·Pages에 텍스트 기반·간단·심플한 이메일 템플릿을 두고, Worker가 템플릿을 가져와 **Zuplo 이메일 API(POST /email)**를 직접 호출해 메일을 보내는 기획을 정리한다. 코드 생성 없음 — 조사 결과·템플릿 설계·옵션 A(Zuplo) 반영·보안·Rate Limit·tenant_id·passcode 재설정 이메일 포함. (Auth Worker용 템플릿은 Frappe 고급 HTML과 동일 디자인을 쓰지 않고 텍스트 기반 심플 스타일로 통일.)
Rate Limit: Zuplo 이메일 API에 50/분 등 기존 정책이 적용되며, Auth 전용 키를 쓰면 OTP·passcode 메일만 별도 제한 가능.
tenant_id: 모든 발송 요청에 tenant_id를 포함해 테넌트별 추적·제한에 활용.
3.4 Passcode 재설정 이메일 (구현 완료)
구현: Auth Worker에서 POST /auth/passcode/request-reset(이메일 수신 → 토큰 생성·저장·재설정 링크 이메일 발송), POST /auth/passcode/reset(토큰·신규 6자리 패스코드 수신 → 토큰 소비·패스코드 저장·JWT·쿠키 반환) 라우트 및 Zuplo를 통한 PASSCODE 재설정 이메일 발송이 반영되어 있다. 본문은 getPasscodeResetEmailBody / getPasscodeResetEmailSubject(텍스트 기반 심플)로 생성 후 Zuplo POST /email 호출. client-web에 /{slug}/forgot-passcode, /{slug}/reset-passcode?token=... 페이지가 있으며, signin 시 “Forgot passcode?” 링크로 진입 가능.
3.5 정리
질문
답
OTP·passcode 이메일을 Frappe 없이 보낼 수 있나?
가능. Auth Worker가 Zuplo POST /email(to, subject, content, tenant_id + API Key) 호출.
Gateway: POST 받아 Queue에만 넣음(발송 안 함). Mail-sender: Queue 소비 후 Resend로만 발송.
Passcode 재설정 이메일 발송 로직
구현 완료. request-reset·reset 라우트 및 Zuplo 발송·client-web 페이지 반영.
4. KV·Pages에서 텍스트 기반 심플 템플릿을 가져와 메일 보내는 기획
4.1 개요
목표: Frappe를 거치지 않고, KV 또는 Pages에 저장한 텍스트 기반·간단·심플한 이메일 템플릿을 Worker가 가져와 변수만 치환한 뒤 content로 Zuplo POST /email에 넘겨 메일을 보낸다.
스타일 원칙: Frappe 측 고급 HTML 템플릿(카드·배경·버튼 등)과 동일 디자인을 쓰지 않고, 텍스트 중심의 단순한 서식만 사용한다. 가독성 위주, 복잡한 레이아웃·인라인 스타일 최소화.
적용 대상: 기존 auth-worker(OTP), 추후 passcode 재설정 등. pregoi-email-queue-gateway는 POST 받아 Queue에만 넣고, pregoi-mail-sender는 Queue 소비 후 Resend로만 발송하는 구조는 그대로 둔다.
4.2 템플릿 저장 위치 (KV vs Pages)
저장소
장점
비고
KV
Worker가 동일 계정에서 바로 get. 버전·테넌트별 키 가능(예: email_template:security, email_template:security:v2).
템플릿(텍스트 또는 최소 HTML)을 KV 값으로 저장. 배포·갱신 시 wrangler kv key put 또는 API로 업데이트.
Pages
정적 에셋으로 배포. URL로 fetch. 수정 후 빌드만 하면 반영 가능.
Worker가 fetch(templateUrl)로 가져옴. 캐시·네트워크 의존 고려.
기획 권장: OTP·PASSCODE 등 보안 메일용 단일 텍스트 기반 심플 템플릿을 KV 한 건(예: email_template:security) 또는 Pages 한 URL에 두고, Worker에서 로드 후 변수 치환해 사용.
4.3 OTP·PASSCODE용 이메일 템플릿 구조 (텍스트 기반·심플)
스타일: 텍스트 기반의 간단하고 심플한 이메일로 통일한다. 플레인 텍스트 또는 최소한의 HTML(줄바꿈·단락·코드 한 줄 강조·URL 링크 정도). 카드·배경색·버튼·복잡한 인라인 스타일은 사용하지 않는다.
숫자 기반 보안 정보(OTP, PASSCODE 재설정)용 공통 템플릿을 KV 또는 Pages에 둔다. Worker·KV 환경에서는 Jinja가 없으므로 문자열 치환(예: {{ otp_code }} → 실제 코드) 및 mode별 분기(OTP 전용 / PASSCODE_RESET 전용 본문을 선택하거나, OTP용·PASSCODE_RESET용 두 개 템플릿으로 분리)로 구현한다.
mode == “OTP”: 인사(안녕하세요 [user_name]님.) → OTP 안내 문구 → 인증 코드 한 줄(otp_code) → 유효시간/만료시각 → (선택) 요청 정보 한 줄씩 → 보안 안내(본인이 요청하지 않았다면 무시·비밀번호 변경 권고).
mode == “PASSCODE_RESET”: 인사 → 재설정 안내 문구 → (선택) reset_code 한 줄 → “PASSCODE 재설정” 재설정 링크 한 줄(reset_url, 클릭 가능한 URL만) → 본인이 요청하지 않았다면 무시.
푸터: “본 이메일은 자동 발송되었습니다. 회신하지 마세요.” + 문의(support_email 또는 관리자 연락) 한 줄.
4.4 Worker에서의 처리 순서 (기획)
템플릿 로드: KV get('email_template:security') 또는 Pages URL fetch(...) 로 텍스트 기반 심플 템플릿 문자열 획득.
변수 치환: mode, otp_code, user_name, brand_name, otp_expires_minutes, otp_expires_at, reset_url, reset_code, support_email 등을 실제 값으로 치환. mode에 따라 OTP 블록 / PASSCODE_RESET 블록만 사용하거나, OTP 전용·PASSCODE_RESET 전용 템플릿 두 개 중 하나 선택.
subject 생성: 동일 변수로 제목 문자열 생성(예: “로그인 인증 코드”, “PASSCODE 재설정 안내”).
Zuplo POST /email: to, subject, content(치환된 텍스트 또는 최소 HTML), tenant_id, API Key로 옵션 A 호출.
4.5 Pages 역할
client-web(Pages): Auth Worker API만 호출. 이메일 발송 로직 없음. 템플릿을 저장·서빙할 경우, 정적 경로(예: /templates/email-security.txt 또는 .html)에 텍스트 기반 심플 템플릿을 두고 Worker가 fetch로 가져올 수 있다.
클라이언트: /{companyId}/{lang}/forgot-passcode(이메일 입력 → request-reset), /{companyId}/{lang}/reset-passcode?token=...(신규 패스코드 입력 → reset).
7. 후속 단계 (구현 순서 제안)
구현 시 아래 순서를 권장한다. 코드 생성은 별도 단계에서 진행한다.
단계
작업
산출·확인
1
Zuplo 이메일 API URL·Auth 전용 API Key 발급·Rate Limit 정책 확인
URL·Key·정책 문서
2
Auth Worker Secret: ZUPLO_EMAIL_API_URL, ZUPLO_EMAIL_API_KEY 설정
wrangler secret put 등
3
OTP·PASSCODE용 텍스트 기반 심플 템플릿 확정. Worker용 치환 규칙 정리(mode별 블록 또는 OTP/PASSCODE_RESET 두 파일)
템플릿(텍스트 또는 최소 HTML)·변수 목록
4
템플릿 저장: KV 네임스페이스에 email_template:security put 또는 Pages 정적 경로에 배포
KV 키 또는 Pages URL
5
Auth Worker: 템플릿 로드( KV get 또는 fetch)·변수 치환·subject 생성 로직 추가
sendVerificationEmail 호출 전 content 준비
6
Auth Worker: sendVerificationEmail 내부에서 Zuplo POST /email(to, subject, content, tenant_id) + API Key 호출, 202 처리
OTP 요청 시 실제 메일 발송
7
(선택) user_name 등 USER_MAP 또는 요청에서 보강. otp_expires_minutes, otp_expires_at 계산·치환
본문 품질·보안 안내 완성
8
(추후)완료 Passcode 재설정 라우트·로직·client-web 페이지
passcode 재설정 이메일·forgot-passcode·reset-passcode
통합 템플릿 기획과의 관계: Auth Worker용 OTP·passcode 메일은 Frappe 템플릿과 같은 디자인을 사용하지 않고, 텍스트 기반의 간단·심플한 스타일로만 구성한다. Frappe 측(email-templates-unified-design-plan.md) 고급 HTML 템플릿과는 별도로, KV/Pages에는 텍스트 중심·최소 서식의 템플릿만 둔다.
8. 문서 이력
2026-02-27: 최초 작성. Cloudflare D1/KV/Worker/Pages에서 OTP·passcode 이메일 로직 조사, Zuplo/게이트웨이 직접 호출로 동일 워크플로우 가능함 정리.
2026-02-27: Frappe 불경유 기획으로 확장. KV·Pages에서 디자인된 이메일 템플릿을 가져와 Worker가 Zuplo(POST /email) 직접 호출하도록 옵션 A만 반영. 보안·Rate Limit·tenant_id·passcode 재설정 이메일 반영. pregoi-email-queue-gateway(Queue에만 넣음)·pregoi-mail-sender(Resend로만 발송)·Pages(이메일 발송 로직 없음) 역할 명시. OTP·PASSCODE HTML 템플릿 구조·변수·Worker 처리 순서 기획 추가.
2026-02-27: §7 후속 단계(구현 순서 제안) 추가. Zuplo 설정 → Secret → 템플릿 확정·저장 → Worker 로드·치환·POST 호출 순서 및 통합 템플릿 기획과의 관계 명시.
2026-02-27: Frappe 동일 디자인 사용 중단. Auth Worker용 이메일을 텍스트 기반·간단·심플 스타일로 변경. §4 제목·개요·템플릿 구조·변수·모드별 본문·푸터·처리 순서·Pages 역할, §5·§6·§7 관련 문구 및 체크리스트 수정.