Skip to content

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과 동일 디자인을 쓰지 않고 텍스트 기반 심플 스타일로 통일.)

참조:

  • docs/planning/pregoi-mail-sender-worker-and-frappe-integration-plan.md — 이메일 아키텍처 (Frappe → Zuplo → Queue → Worker → Resend)
  • docs/planning/employee-web-login-passcode-test-plan.md — OTP·패스코드 플로우
  • docs/planning/email-templates-unified-design-plan.md — 통합 이메일 템플릿·메시지 표준화
  • apps/auth-worker/README.md — Auth Worker 라우트·시크릿

1. 조사 범위

환경용도OTP/Passcode/이메일 관련 여부
D1RAG 등 (prego-rag-ingestion)이메일·OTP·passcode 미사용
KVUSER_MAP, AUTH_STORE, DEVICE_LIST, ACCESS_LOG, ONBOARDING_COMPANIES (auth-worker 등)OTP 저장(otp:email), passcode 저장(auth:dev:…) — 발송 로직은 Worker에 있음
Workerauth-worker, pregoi-email-queue-gateway, pregoi-mail-sender, 기타 workersauth-worker에서 OTP 이메일 발송 로직 존재(현재 스텁)
Pagesclient-web(Next.js) 등이메일 발송 없음. Auth Worker API 호출만 함

2. Cloudflare 환경별 이메일·OTP·Passcode 로직 정리

2.1 D1

  • 사용처: workers/rag-ingestion — RAG 문서 저장·벡터화용 DB. OTP·passcode·이메일과 무관.
  • 결론: D1에는 OTP/passcode/이메일 발송 로직 없음.

2.2 KV

  • 역할: Auth Worker가 OTP·패스코드·디바이스 목록·접근 로그 등을 저장.
    • AUTH_STORE: otp:{email}(OTP 코드), auth:dev:{eid}:{deviceId}(패스코드 해시 등)
    • USER_MAP: user:map:{email} → tenant_id, slug, employee_id, status
  • 결론: KV는 저장소일 뿐, 이메일을 보내는 코드는 Worker에 있음.

2.3 Worker

2.3.1 auth-worker (Prego/apps/auth-worker)

  • OTP 요청 (POST /auth/otp/request):
    1. USER_MAP에서 이메일 조회.
    2. OTP 생성 후 AUTH_STORE.put('otp:' + email, otp, { expirationTtl }) 저장.
    3. env.EMAIL_API_KEY가 있으면 sendVerificationEmail(email, otp, env.EMAIL_API_KEY) 호출.
  • sendVerificationEmail (현재 구현):
    • 스텁. 실제 발송 없음. console.log('Send OTP email', email, code) 만 수행.
    • 주석: “Stub: integrate with Resend/SendGrid etc. using apiKey”.
  • 패스코드 (POST /auth/passcode/set): AUTH_STORE·DEVICE_LIST에만 저장. Passcode 관련 이메일 발송 로직은 없음.

정리: Auth Worker에는 OTP를 이메일로 보내는 진입로만 있고, 실제 발송은 미구현(스텁) 상태다. 본 기획에서는 Frappe 불경유로 Zuplo 이메일 API(옵션 A) 호출을 전제로 한다.

2.3.2 pregoi-email-queue-gateway (Prego/pregoi-email-queue-gateway)

  • 역할: POST (to, subject, content, tenant_id) 수신 → **Cloudflare Queue(saas-email-queue)**에 푸시 → 202 반환. 발송은 하지 않음(Queue에만 넣음).
  • 인증: GATEWAY_API_KEY 설정 시 X-Gateway-Api-Key 또는 Authorization: Bearer 검사.
  • 호출 주체: 기획상 Zuplo가 인증·Rate Limit 후 이 Worker를 호출하거나, Zuplo가 Queue에 직접 푸시하는 구성.

2.3.3 pregoi-mail-sender (Prego/pregoi-mail-sender)

  • 트리거: Queue Consumer 전용. HTTP fetch 비활성화 권장.
  • 역할: Queue에서 메시지 수신 후 Resend API로만 발송. 요청 생성·템플릿 처리 없음.

2.4 Pages

  • client-web: Employee 웹. 로그인 시 POST /auth/otp/requestAuth Worker API만 호출. 이메일 발송 로직 없음.
  • 결론: Pages에는 OTP/passcode 이메일 발송 로직 없음.

3. Frappe 불경유: Zuplo 이메일 API 직접 호출 (옵션 A)

3.1 현재 이메일 워크플로우 (Frappe 경유)

Frappe (prego_saas sendmail 패치)
→ POST Zuplo (POST /email, API Key, 50/min)
→ Zuplo가 Queue에 푸시 (또는 Gateway 호출)
→ saas-email-queue
→ pregoi-mail-sender (Queue Consumer)
→ Resend API → 수신자 메일함

3.2 Auth Worker에서 Zuplo POST /email 직접 호출 (권장 방식)

  • 목표: OTP(및 추후 passcode 재설정) 이메일을 Frappe 없이 보내고, 동일하게 Queue → pregoi-mail-sender → Resend 경로를 사용하는 것.
  • 방식(옵션 A)
    Auth Worker가 Zuplo 이메일 API POST /email에 아래를 전달한다.
    • to: 수신자 이메일
    • subject: 제목 (예: 로그인 인증 코드, PASSCODE 재설정)
    • content: HTML 본문 (아래 §4에서 KV·Pages 템플릿을 가져와 치환한 결과)
    • tenant_id: USER_MAP에서 조회한 tenant_id (로그·Rate Limit용)
    • 인증: Zuplo에서 요구하는 API Key (헤더에 Bearer 또는 동일 방식)
  • 흐름: Auth Worker → Zuplo (인증·Rate Limit) → Queue 푸시 → 202 수신. 이후 pregoi-mail-sender가 Queue를 소비해 Resend로만 발송.

3.3 보안·Rate Limit·tenant_id

  • 보안: API Key는 Auth Worker Secret으로만 관리. 노출 금지.
  • 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) 호출.
pregoi-email-queue-gateway / pregoi-mail-sender 역할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)

저장소장점비고
KVWorker가 동일 계정에서 바로 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용 두 개 템플릿으로 분리)로 구현한다.

주요 변수:

변수용도예시
subject제목 (title·subject)로그인 인증 코드를 보내드립니다
brand_name서비스명 표기Prego
user_name인사 대상 이름홍길동
modeOTP / PASSCODE_RESETOTP
otp_code6자리 OTP123456
otp_expires_minutes유효 시간(분)10
otp_expires_at만료 시각2026-02-27 15:30
login_device / login_location / login_ip요청 정보(선택)
reset_code재설정 코드(선택)
reset_expires_minutes재설정 유효 분
reset_urlPASSCODE 재설정 링크(URL)https://…
support_email푸터 문의 메일support@pregoi.com
  • 모드별 본문(텍스트·심플):
    • mode == “OTP”: 인사(안녕하세요 [user_name]님.) → OTP 안내 문구 → 인증 코드 한 줄(otp_code) → 유효시간/만료시각 → (선택) 요청 정보 한 줄씩 → 보안 안내(본인이 요청하지 않았다면 무시·비밀번호 변경 권고).
    • mode == “PASSCODE_RESET”: 인사 → 재설정 안내 문구 → (선택) reset_code 한 줄 → “PASSCODE 재설정” 재설정 링크 한 줄(reset_url, 클릭 가능한 URL만) → 본인이 요청하지 않았다면 무시.
  • 푸터: “본 이메일은 자동 발송되었습니다. 회신하지 마세요.” + 문의(support_email 또는 관리자 연락) 한 줄.

4.4 Worker에서의 처리 순서 (기획)

  1. 템플릿 로드: KV get('email_template:security') 또는 Pages URL fetch(...)텍스트 기반 심플 템플릿 문자열 획득.
  2. 변수 치환: 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 전용 템플릿 두 개 중 하나 선택.
  3. subject 생성: 동일 변수로 제목 문자열 생성(예: “로그인 인증 코드”, “PASSCODE 재설정 안내”).
  4. 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로 가져올 수 있다.

5. 구현 시 고려사항 (기획 수준)

5.1 Auth Worker 설정

  • Zuplo 이메일 API(옵션 A): ZUPLO_EMAIL_API_URL, ZUPLO_EMAIL_API_KEY(Secret) 설정. sendVerificationEmail(및 추후 passcode 재설정 발송)에서 fetch로 POST (to, subject, content, tenant_id).
  • 템플릿: KV 키(예: email_template:security) 또는 Pages 템플릿 URL을 환경 변수/Secret로 두고, Worker가 로드 후 위 변수 치환 로직 적용.

5.2 보안·Rate Limit·tenant_id (재확인)

  • 보안: API Key·템플릿 URL 등 Secret/환경만 Worker에 두고, 코드/저장소에 노출하지 않음.
  • Rate Limit: Zuplo 이메일 API 정책(50/분 등) 및 필요 시 Auth 전용 키로 OTP·passcode 메일만 제한.
  • tenant_id: USER_MAP에서 취득해 모든 POST /email 요청에 포함.

5.3 Passcode 재설정 이메일

  • 구현 완료: Auth Worker POST /auth/passcode/request-reset(이메일 → 토큰·재설정 링크 이메일), POST /auth/passcode/reset(토큰·신규 패스코드 → 저장·JWT). 템플릿은 Worker 내 getPasscodeResetEmailBody/getPasscodeResetEmailSubject(텍스트 기반). pregoi-email-queue-gateway·pregoi-mail-sender 역할 변경 없음.

6. 체크리스트 (구현 전 확인용)

  • D1: OTP/passcode/이메일 로직 없음 확인 (rag-ingestion만 사용)
  • KV: 저장만 담당, 발송 로직 없음 확인 (템플릿 저장 시 KV 사용 가능)
  • Auth Worker: OTP 시 sendVerificationEmail 호출부 있음, 현재 스텁 확인
  • Auth Worker: passcode/set 시 이메일 발송 없음(Passcode 관련 이메일 발송 로직 없음)
  • pregoi-email-queue-gateway: POST 받아 Queue에만 넣음, 발송 안 함
  • pregoi-mail-sender: Queue 소비 후 Resend로만 발송
  • Pages(client-web): Auth Worker API만 호출, 이메일 발송 로직 없음
  • (구현 시) Auth Worker에서 OTP 이메일: Zuplo POST /email(옵션 A), to/subject/content/tenant_id + API Key
  • (구현 시) 템플릿: Worker 내 OTP·PASSCODE 텍스트 기반 심플 템플릿(이메일-otp-template.ts) 로드·치환 후 content로 전달
  • (구현 시) 보안·Rate Limit·tenant_id 반영
  • Passcode 재설정 이메일: request-reset·reset 라우트 및 동일 Zuplo 플로우·client-web 페이지 반영

6.1 API: Passcode 재설정 (구현 완료)

엔드포인트메서드Body설명
/auth/passcode/request-resetPOST{ "email": "user@example.com" }이메일로 USER_MAP 조회 후 재설정 토큰 생성·AUTH_STORE 저장(TTL 15분), 재설정 URL 포함 이메일 Zuplo 발송. 계정 없어도 200 + 동일 메시지(이메일 유출 방지).
/auth/passcode/resetPOST{ "token": "<from email>", "passcode": "123456" }토큰 검증 후 삭제, 신규 6자리 패스코드 저장·새 디바이스 등록, JWT·쿠키 반환. redirect_url, token 포함.
  • 환경: Auth Worker CLIENT_WEB_BASE_URL(재설정 링크 베이스), ZUPLO_EMAIL_API_URL, ZUPLO_EMAIL_API_KEY 필요.
  • 클라이언트: /{companyId}/{lang}/forgot-passcode(이메일 입력 → request-reset), /{companyId}/{lang}/reset-passcode?token=...(신규 패스코드 입력 → reset).

7. 후속 단계 (구현 순서 제안)

구현 시 아래 순서를 권장한다. 코드 생성은 별도 단계에서 진행한다.

단계작업산출·확인
1Zuplo 이메일 API URL·Auth 전용 API Key 발급·Rate Limit 정책 확인URL·Key·정책 문서
2Auth Worker Secret: ZUPLO_EMAIL_API_URL, ZUPLO_EMAIL_API_KEY 설정wrangler secret put 등
3OTP·PASSCODE용 텍스트 기반 심플 템플릿 확정. Worker용 치환 규칙 정리(mode별 블록 또는 OTP/PASSCODE_RESET 두 파일)템플릿(텍스트 또는 최소 HTML)·변수 목록
4템플릿 저장: KV 네임스페이스에 email_template:security put 또는 Pages 정적 경로에 배포KV 키 또는 Pages URL
5Auth Worker: 템플릿 로드( KV get 또는 fetch)·변수 치환·subject 생성 로직 추가sendVerificationEmail 호출 전 content 준비
6Auth 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 관련 문구 및 체크리스트 수정.
Help