프로젝트

일반

사용자정보

Actions

이슈 #426

진행중
CP JL

이슈 #420: Redeem Code 시스템 — 사용권 발급/리딤/관리 (QR 코드 기반)

리딤 코드 SNS/문자 발송 기능

이슈 #426: 리딤 코드 SNS/문자 발송 기능

Chris Park이(가) 약 7시간 전에 추가함.

상태:
신규
우선순위:
높음
담당자:
목표버전:
시작일:
2026/04/30
완료일:
진척도:

0%

추정시간:

설명

개요

Redeem Code 시스템(#420)에서 발급된 코드를 SNS/문자 메시지로 자동 발송하는 기능을 구현한다.

발송 채널

채널 서비스 비고
SMS AWS SNS / Twilio 국내 + 국제 발송 지원
카카오톡 카카오 비즈메시지 API 템플릿 사전 승인 필요
이메일 AWS SES / SendGrid QR 코드 이미지 첨부

메시지 템플릿

[알페아] 사용권 코드가 도착했습니다!

리딤 코드: ALPHEA-XXXX-XXXX-XXXX
혜택: {reward_description}
유효기간: {expires_at}

앱에서 코드를 입력하시면 즉시 사용 가능합니다.
감사합니다.

발송 정책

  • 배치 발급 시 자동 발송 옵션 제공 (Admin UI에서 토글)
  • 발송 대상: 전화번호 또는 이메일 CSV 업로드
  • 발송 시점: 코드 발급 즉시 OR 예약 발송 (크론 스케줄)
  • 재발송: 수동 재발송 기능 제공 (발송 실패 시)
  • 발송 이력: 발송 성공/실패 상태, 발송 시각, 에러 메시지 기록

DB 스키마 확장 (migrations/0009_redeem_code_delivery.sql)

CREATE TABLE redeem_code_deliveries (
  id              BIGSERIAL PRIMARY KEY,
  code            TEXT NOT NULL REFERENCES redeem_codes(code),
  channel         TEXT NOT NULL,           -- 'sms' / 'kakao' / 'email'
  recipient       TEXT NOT NULL,           -- 전화번호 / 이메일
  template_id     TEXT,                    -- 템플릿 ID (카카오톡 등)
  status          TEXT NOT NULL,           -- 'pending' / 'sent' / 'failed'
  sent_at         TIMESTAMPTZ,
  error_message   TEXT,
  provider_msg_id TEXT,                    -- 외부 서비스 메시지 ID
  created_at      TIMESTAMPTZ DEFAULT NOW(),
  retry_count     INT DEFAULT 0
);

CREATE INDEX idx_deliveries_code ON redeem_code_deliveries(code);
CREATE INDEX idx_deliveries_status ON redeem_code_deliveries(status);

Admin REST API 확장

POST   /api/redeem-codes/batch/send       # 배치 발송 (CSV + 채널 선택)
POST   /api/redeem-codes/{code}/resend    # 개별 재발송
GET    /api/redeem-codes/{code}/deliveries # 발송 이력 조회
GET    /api/delivery-logs                 # 전체 발송 로그 (필터: status/channel/date)

Dev Console UI 확장

Redeem Codes 탭 내 추가 기능:

  • 발송 대상 CSV 업로드 폼 (columns: code, recipient, channel)
  • 발송 채널 선택 (SMS / 카카오톡 / 이메일)
  • 템플릿 프리뷰
  • 발송 진행 상태 표시 (progress bar)
  • 발송 이력 테이블 (코드별 발송 상태, 재발송 버튼)

외부 서비스 연동

AWS SNS (SMS)

// src/delivery/sns.rs
pub async fn send_sms(
    phone: &str,
    message: &str,
) -> Result<String, DeliveryError> {
    // AWS SNS SDK 호출
}

카카오 비즈메시지

// src/delivery/kakao.rs
pub async fn send_kakao(
    phone: &str,
    template_id: &str,
    params: HashMap<String, String>,
) -> Result<String, DeliveryError> {
    // 카카오 비즈메시지 API 호출
}

AWS SES (Email)

// src/delivery/ses.rs
pub async fn send_email(
    to: &str,
    subject: &str,
    body_html: &str,
    attachments: Vec<Attachment>,
) -> Result<String, DeliveryError> {
    // QR 코드 이미지를 첨부로 포함
}

구현 단계

  • Phase 1 — DB migration + store/deliveries.rs
  • Phase 2 — 외부 서비스 연동 모듈 (src/delivery/)
  • Phase 3 — Admin REST API 확장
  • Phase 4 — Dev Console UI 확장 (발송 폼 + 이력 테이블)
  • Phase 5 — 배치 발송 스케줄러 (옵션)

환경 변수

# AWS SNS
AWS_SNS_REGION=ap-northeast-2
AWS_SNS_ACCESS_KEY_ID=...
AWS_SNS_SECRET_ACCESS_KEY=...

# 카카오 비즈메시지
KAKAO_API_KEY=...
KAKAO_SENDER_KEY=...

# AWS SES
AWS_SES_REGION=ap-northeast-2
AWS_SES_FROM_EMAIL=noreply@alphea.io

보안 고려사항

  • 발송 대상 개인정보 (전화번호/이메일)는 암호화 저장 권장
  • 발송 실패 시 민감 정보 로깅 금지
  • Rate Limiting (분당 발송 건수 제한)

의존성

  • #420 (Redeem Code 시스템) 선행 필수
  • AWS SNS, Kakao, SES 계정 및 API 키 준비

JL James Lee이(가) 6달 전에 변경 Actions #1

SMS 채널 — AWS SNS provider 구현

commit:server|f6217a8

#426 의 SMS 발송 채널 (AWS SNS) 부분만 우선 구현. Kakao / SES / 배치 / 감사 테이블 등은 후속 작업.

이번 commit 에서 처리한 것

1. Provider abstraction (이미 #420 Phase A 에서 도입됨, 본 commit 에서 SNS 구현 추가)

  • SmsClient trait + OutgoingMms / SmsSendOutcome / SmsError
  • 신규 AwsSnsSmsClient (src/domain/sms/sns.rs)

2. AWS SNS Publish 호출

  • AWS SDK default credential chain (env / shared / IAM Role 모두 지원)
  • MessageAttribute 로 AWS.SNS.SMS.SMSType=Transactional 강제 — 한국 보상 코드 알림 용도라 거래성으로 발송
  • 응답의 message_id 를 provider_message_id 로 반환

3. SNS 의 MMS 미지원 대응

  • OutgoingMms.image_png 가 있으면 silently drop + debug 로그
  • send_sms handler 가 본문에 항상 QR: https://... URL 을 포함시켜 수신자가 탭하여 QR 이미지 확인 가능 (MMS 지원 provider 든 아니든 동일)
  • URL prefix 는 Config.public_admin_url (env PUBLIC_ADMIN_URL)

4. 한국 번호 → E.164 자동 변환

  • to_e164_korea("010-1234-5678") → "+821012345678"
  • 이미 + 로 시작하면 그대로 (국제번호 passthrough)
  • 그 외 형식은 InvalidPhone 에러

5. 환경변수

SMS_PROVIDER=sns                     # "stub" (default) | "sns"
PUBLIC_ADMIN_URL=https://edge-admin.alphea.ai
AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
# 또는 IAM Role 사용 시 위 두 개 비움

docker-compose.yml 의 app 컨테이너에 위 env forward 완료.

발송 본문 예시

[Alphea] $1000 사용권이 발급되었습니다.

코드: ALPHEA-XXXX-XXXX-XXXX
유효기간: 2026-12-31

QR: https://edge-admin.alphea.ai/api/redeem-codes/ALPHEA-XXXX-XXXX-XXXX/qr.png

운영 적용 방법

  1. AWS 콘솔에서 SNS SMS 활성화 + (운영 시) sender ID 사전 등록
  2. .env 에 SMS_PROVIDER=sns + AWS_* 자격 증명 + PUBLIC_ADMIN_URL=https://edge-admin.alphea.ai 설정
  3. docker compose up -d --build app
  4. Dev Console → Redeem 탭 → Detail 에서 phone 등록 + Send SMS 클릭 → 실제 SMS 발송 확인

검증

  • cargo build 성공 (AWS SDK ~30s)
  • cargo test --lib 45 passed (sms 신규 3: E.164 변환 / passthrough / reject)
  • cargo clippy --all-targets clean

본 commit 에서 미완료된 #426 범위 (후속 작업)

항목 비고
Kakao 비즈메시지 템플릿 사전 승인 + sender_key 등 별도 셋업 필요
AWS SES (이메일 + QR 첨부) 도메인 verification + DKIM 등 셋업
redeem_code_deliveries 감사 테이블 현재는 redeem_codes.last_sms_sent_at 만 stamp. status/error/retry/provider_msg_id 등 상세 추적은 별도 테이블 필요
배치 발송 (CSV 업로드) UI + endpoint 신설
개별 재발송 endpoint POST /api/redeem-codes/{code}/resend
발송 이력 조회 endpoint GET /api/redeem-codes/{code}/deliveries, GET /api/delivery-logs
발송 대상 개인정보 암호화 운영 정책 결정 후 적용
Rate limiting 분당 발송 건수 제한

Provider 추가 가이드 (Kakao / SES 도 같은 패턴)

  1. src/domain/sms/{kakao,ses}.rs 신설
  2. SmsClient trait 구현
  3. main::build_sms_client 의 match 에 분기 추가
  4. config / .env / docker-compose 에 provider 별 env 추가

JL James Lee이(가) 6달 전에 변경 Actions #2

Email 채널 — AWS SES provider 구현 (default channel)

commit:server|49543d4

#426 의 email 채널 부분 구현. redeem 코드의 기본 발송 채널 로 email 도입 (SMS 는 optional secondary).

변경 요약

DB (migrations/0012_redeem_codes_email.sql) — additive

  • redeem_codes.email TEXT (수령자 이메일)
  • redeem_codes.last_email_sent_at TIMESTAMPTZ

Domain (src/domain/email/)

  • EmailClient trait + OutgoingEmail (HTML / text / inline_png) + EmailSendOutcome / EmailError
  • StubEmailClient — dev 용, 로그만
  • AwsSesEmailClient (ses.rs):
    • SES v2 SendEmail with EmailContent::Raw
    • 직접 MIME 빌더: multipart/related → multipart/alternative (text/plain + text/html base64) + image/png inline (Content-ID: <qr@alphea>)
    • Subject 의 non-ASCII 는 RFC 2047 base64 encoded-word 로 감쌈 (=?UTF-8?B?...?=)
    • 본문은 모두 base64 인코딩하여 UTF-8 8bit 안전 회피
    • looks_like_email() 단순 검증

Config / 인프라

  • email_provider: String (env EMAIL_PROVIDER, default "stub")
  • email_from: String (env EMAIL_FROM, ses 일 때 필수)
  • AppState.email: Arc<dyn EmailClient>
  • main::build_email_client(provider, from) — ses/stub/unknown→stub fallback

Admin REST

  • IssueCodesReq.email, UpdateCodeReq.email
  • POST /api/redeem-codes/{code}/send-email:
    • Korean HTML body (코드 박스 + 만료일 + inline QR + fallback URL)
    • Plain text body (HTML 미지원 클라이언트용)
    • QR PNG attached as inline image (cid:qr@alphea)
    • 성공 시 stamp_email_sent

Dev Console UI

  • Issue 폼: email 필드를 default channel 로 상단 배치, phone 은 grid 하단으로 optional secondary 로 강등
  • Detail 편집기: email/note/phone 편집 + Save / Send Email / Send SMS 3 버튼 + last email/SMS 시각 표시
  • JS: rcSendEmail() 신규, rcFillEditor 가 last_email_sent_at 표시

Email 본문 (HTML, 한국어)

  • Subject: [Alphea] $1000 사용권 코드
  • Body: 코드 박스 + 유효기간 + inline QR (240px) + fallback link
  • Plain text fallback 동봉

Provider 선택

EMAIL_PROVIDER=ses        → AwsSesEmailClient (EMAIL_FROM 필수)
EMAIL_PROVIDER=stub       → StubEmailClient (default)
EMAIL_PROVIDER=<unknown>  → fallback to stub + warn 로그

운영 적용 체크리스트

  1. AWS SES 셋업 — sandbox 모드면 sender + recipient 모두 verify. Production access 별도 신청.
  2. DKIM / SPF — alphea.io DNS 레코드 (운영 권장)
  3. .env:
    EMAIL_PROVIDER=ses
    EMAIL_FROM=noreply@alphea.io
    AWS_REGION=ap-northeast-2
    AWS_ACCESS_KEY_ID=...
    AWS_SECRET_ACCESS_KEY=...
    
  4. docker compose up -d --build app
  5. Dev Console → Redeem 탭 → Issue 시 email 입력 → Detail 의 Send Email 클릭 → 실수신 확인

검증

  • cargo build 성공 (SES SDK 컴파일 ~17s)
  • cargo test --lib 53 passed (email 신규 8: trait 동작, MIME 빌드, RFC 2047, base64 line breaks 등)
  • cargo clippy --all-targets clean

#426 잔여 범위 (후속)

항목 상태
AWS SNS (SMS) ✅ 완료 (commit:server|f6217a8)
AWS SES (이메일 + QR 첨부) ✅ 본 commit
Kakao 비즈메시지 후속 — 템플릿 사전 승인 + sender_key 셋업 필요
redeem_code_deliveries 감사 테이블 후속 — 현재는 last_*_sent_at stamp 만, status/error/retry/provider_msg_id 상세 추적은 별도 테이블 필요
배치 발송 (CSV 업로드) 후속 — UI + endpoint 신설
개별 재발송 endpoint 후속 — POST /api/redeem-codes/{code}/resend
발송 이력 조회 endpoint 후속 — GET /api/redeem-codes/{code}/deliveries, GET /api/delivery-logs
발송 대상 개인정보 암호화 후속 — 운영 정책 결정 후 적용
Rate limiting 후속 — 분당 발송 건수 제한

JL James Lee이(가) 6달 전에 변경 Actions #3

운영 배포 가이드 — Office 365 + AWS SES dual-sender 셋업

본 노트는 코드 변경이 아닌 운영 환경 배포 가이드. alphea.ai 도메인이 이미 Office 365 (Exchange Online) 에서 메일을 받고 있는 상황에서, 외부 송신만 SES 로 추가하는 방법.

핵심: 공유 mailbox 발신은 그대로 가능

support@alphea.ai 가 O365 의 공유 mailbox (실제 사용자 계정 아님) 인 환경에서도 SES 발송 가능. SES 는 O365 권한 체계와 완전히 독립 — 도메인 자체를 인증 받으므로 local part 가 무엇이든 (또는 O365 에 mailbox 가 존재하지 않더라도) 발송 가능.

흐름 처리 주체
발송 (SES → 외부 수신자) SES 단독. O365 무관
수신자가 Reply DNS MX → O365 → 공유 mailbox 정상 수신
bounce SES MAIL FROM (default 는 Amazon-managed) → SES 콘솔 / SNS topic

→ 공유 mailbox 라는 점은 오히려 장점 — 답장은 자연스럽게 지원팀으로, bounce 노이즈는 SES 가 따로 관리.

Office 365 측 — 변경 거의 없음

  • 기존 SPF / DKIM (selector1._domainkey, selector2._domainkey) 그대로 유지. SES DKIM 은 다른 selector 라 충돌 없음.
  • 공유 mailbox support@alphea.ai 자체는 그대로 유지 (수신자 reply 받기 위해)
  • O365 admin 작업 불필요 — Send As / Send on behalf 같은 권한 부여도 무관

AWS SES 측 (서울 region 권장: ap-northeast-2)

  1. Identities → Create identity → Domain: alphea.ai
  2. SES 가 보여주는 DNS 레코드 Route 53 에 추가:
    • 소유권 확인 TXT: _amazonses.alphea.ai
    • DKIM CNAME × 3 (Easy DKIM)
  3. (선택, 운영 권장) MAIL FROM Domain: mail.alphea.ai 설정 → bounce/complaint 가 별도 처리
  4. Sandbox → Production access 신청 (SES 콘솔 → Account dashboard). 사용 사례 명시, 보통 24h 내 승인.
  5. (선택) Configuration set + SNS topic — bounce/complaint/delivery 이벤트 처리 자동화

Route 53 (Hosted Zone: alphea.ai) 변경

신규 추가:

Type Name Value
TXT _amazonses.alphea.ai (SES 가 알려준 토큰)
CNAME × 3 <token>._domainkey.alphea.ai <token>.dkim.amazonses.com

SPF 수정 — ⚠ 가장 흔한 함정:

# 변경 전 (O365 만)
alphea.ai. TXT  "v=spf1 include:spf.protection.outlook.com -all"

# 변경 후 (둘 다 — 한 줄에 합쳐야 함, 두 개 TXT 는 invalid)
alphea.ai. TXT  "v=spf1 include:spf.protection.outlook.com include:amazonses.com ~all"

테스트 단계는 ~all (softfail), 안정화 후 -all (hardfail) 권장.

DMARC — 기존 유지 OK. SES DKIM 이 d=alphea.ai 로 서명되어 자동 align.

alphea-ai-edge .env

EMAIL_PROVIDER=ses
EMAIL_FROM=support@alphea.ai
AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

또는 IAM Role 사용 시 access key 비우고 다음 정책 attach:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["ses:SendEmail", "ses:SendRawEmail"],
    "Resource": ["arn:aws:ses:ap-northeast-2:<account-id>:identity/alphea.ai"]
  }]
}

검증

# DNS propagation
dig +short TXT _amazonses.alphea.ai
dig +short TXT alphea.ai
dig +short CNAME <selector>._domainkey.alphea.ai

# SES 콘솔: domain status "Verified" + DKIM "Successful"

# Test 발송 (sandbox 동안은 verified address 만)
aws ses send-email --from support@alphea.ai \
  --destination ToAddresses=<verified> \
  --message Subject={Data="SES test"},Body={Text={Data="hi"}}

# Gmail "원본 보기" 또는 mail-tester.com — SPF/DKIM/DMARC 모두 PASS 확인

자주 빠뜨리는 것

  1. SPF 두 개로 분리 등록 (반드시 한 줄로 합쳐야 함)
  2. Production access 신청 안 함 → sandbox 에서 verified recipient 에만 발송
  3. MAIL FROM domain 미사용 → bounce 가 support@ 메일박스로 들어와 노이즈
  4. SES Region 불일치 — DKIM CNAME 은 SES active region 의 키여야 함
Actions

내보내기 PDF Atom