이슈 #426
진행중이슈 #420: Redeem Code 시스템 — 사용권 발급/리딤/관리 (QR 코드 기반)
리딤 코드 SNS/문자 발송 기능
설명
개요¶
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달 전에 변경
SMS 채널 — AWS SNS provider 구현¶
commit:server|f6217a8
#426 의 SMS 발송 채널 (AWS SNS) 부분만 우선 구현. Kakao / SES / 배치 / 감사 테이블 등은 후속 작업.
이번 commit 에서 처리한 것¶
1. Provider abstraction (이미 #420 Phase A 에서 도입됨, 본 commit 에서 SNS 구현 추가)
-
SmsClienttrait +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_smshandler 가 본문에 항상QR: https://...URL 을 포함시켜 수신자가 탭하여 QR 이미지 확인 가능 (MMS 지원 provider 든 아니든 동일) - URL prefix 는
Config.public_admin_url(envPUBLIC_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
운영 적용 방법¶
- AWS 콘솔에서 SNS SMS 활성화 + (운영 시) sender ID 사전 등록
-
.env에SMS_PROVIDER=sns+AWS_*자격 증명 +PUBLIC_ADMIN_URL=https://edge-admin.alphea.ai설정 docker compose up -d --build app- Dev Console → Redeem 탭 → Detail 에서 phone 등록 + Send SMS 클릭 → 실제 SMS 발송 확인
검증¶
-
cargo build성공 (AWS SDK ~30s) -
cargo test --lib45 passed (sms 신규 3: E.164 변환 / passthrough / reject) -
cargo clippy --all-targetsclean
본 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 도 같은 패턴)¶
-
src/domain/sms/{kakao,ses}.rs신설 -
SmsClienttrait 구현 -
main::build_sms_client의 match 에 분기 추가 - config / .env / docker-compose 에 provider 별 env 추가
JL James Lee이(가) 6달 전에 변경
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/)
-
EmailClienttrait +OutgoingEmail(HTML / text / inline_png) +EmailSendOutcome/EmailError -
StubEmailClient— dev 용, 로그만 -
AwsSesEmailClient(ses.rs):- SES v2
SendEmailwithEmailContent::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()단순 검증
- SES v2
Config / 인프라
-
email_provider: String(envEMAIL_PROVIDER, default"stub") -
email_from: String(envEMAIL_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 로그
운영 적용 체크리스트¶
- AWS SES 셋업 — sandbox 모드면 sender + recipient 모두 verify. Production access 별도 신청.
-
DKIM / SPF —
alphea.ioDNS 레코드 (운영 권장) -
.env:EMAIL_PROVIDER=ses EMAIL_FROM=noreply@alphea.io AWS_REGION=ap-northeast-2 AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... docker compose up -d --build app- Dev Console → Redeem 탭 → Issue 시 email 입력 → Detail 의 Send Email 클릭 → 실수신 확인
검증¶
-
cargo build성공 (SES SDK 컴파일 ~17s) -
cargo test --lib53 passed (email 신규 8: trait 동작, MIME 빌드, RFC 2047, base64 line breaks 등) -
cargo clippy --all-targetsclean
#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달 전에 변경
운영 배포 가이드 — 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)¶
-
Identities → Create identity → Domain:
alphea.ai - SES 가 보여주는 DNS 레코드 Route 53 에 추가:
- 소유권 확인 TXT:
_amazonses.alphea.ai - DKIM CNAME × 3 (Easy DKIM)
- 소유권 확인 TXT:
- (선택, 운영 권장) MAIL FROM Domain:
mail.alphea.ai설정 → bounce/complaint 가 별도 처리 - Sandbox → Production access 신청 (SES 콘솔 → Account dashboard). 사용 사례 명시, 보통 24h 내 승인.
- (선택) 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 확인
자주 빠뜨리는 것¶
- SPF 두 개로 분리 등록 (반드시 한 줄로 합쳐야 함)
- Production access 신청 안 함 → sandbox 에서 verified recipient 에만 발송
-
MAIL FROM domain 미사용 → bounce 가
support@메일박스로 들어와 노이즈 - SES Region 불일치 — DKIM CNAME 은 SES active region 의 키여야 함