안녕하세요!
아래 인증 코드를 입력하여 계정을 확인해주세요:
이 코드는 {valid_minutes}분간 유효합니다.
Aislo는 절대로 귀하의 인증 코드를 요청하지 않습니다.
# 이메일 인프라 구현 계획서 (v1.0) ## 1. 개요 ### 1.1 목표 - B2B SaaS 환경에서 **자동 이메일 발송 시스템** 구축 - Google Workspace (개인용 Gmail 대체) 기반 SMTP 연동 - 비동기 태스크 기반 논블로킹 발송 - Phase 1: Google SMTP → Phase 2: AWS SES 마이그레이션 준비 ### 1.2 현재 상황 - **개발 단계**: 개인 Gmail 계정 (umsangdon@gmail.com) 사용 - **상용화 단계**: Google Workspace Business 또는 AWS SES로 전환 예정 - **타겟**: 회원가입, 로그인, 팀원 승인/거부 등 자동 메일 --- ## 2. 이메일 발송 유형 및 우선순위 ### 2.1 필수 (🔴) - Phase 1에서 구현 | 메일 유형 | 수신자 | 발송 타이밍 | 용도 | |----------|--------|-----------|------| | **OTP 인증** | 가입자/로그인자 | 가입/로그인 직후 | 이메일 소유권 확인 | | **가입 확인** | 신규 가입자 | 가입 완료 직후 | 가입 성공 안내 | | **팀원 승인 요청** | 마스터 | 팀원 가입 신청 시 | 승인/거부 요청 | | **팀원 승인/거부 결과** | 팀원 | 마스터 승인/거부 직후 | 결과 안내 | ### 2.2 중요 (🟠) - Phase 2에서 구현 | 메일 유형 | 수신자 | 발송 타이밍 | 용도 | |----------|--------|-----------|------| | **3개월 재인증 알림** | 모든 사용자 | 주기적 (크론) | 보안 점검 | | **비밀번호 변경 알림** | 해당 사용자 | 변경 직후 | 보안 알림 | | **Q&A 답변 회신** | 문의자 | 관리자 처리 후 | 고객 지원 | ### 2.3 선택사항 (🟡) - Phase 3 이후 | 메일 유형 | 수신자 | 발송 타이밍 | 용도 | |----------|--------|-----------|------| | **세션 만료 알림** | 활동 중인 사용자 | 만료 5분 전 | UX 개선 | | **주간 활동 리포트** | 마스터 | 매주 월요일 | 분석 정보 | | **결제/구독 안내** | 마스터 | 결제 시기 | 구독 관리 | --- ## 3. 아키텍처 설계 ### 3.1 전체 흐름도 ``` ┌─────────────────────────────────────────────────────┐ │ FastAPI 웹 서버 (Uvicorn) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ API 라우터 │ │ │ │ - POST /api/auth/register │ │ │ │ - POST /api/auth/login │ │ │ │ - POST /api/auth/join-request │ │ │ │ - POST /api/admin/approve-join │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ ↓ (즉시 응답) │ │ ┌──────────────────────────────────────────────┐ │ │ │ 백그라운드 비동기 작업 │ │ │ │ (asyncio.create_task) │ │ │ │ │ │ │ │ ├─ OTP 생성 + 암호화 저장 │ │ │ │ ├─ 이메일 템플릿 렌더링 │ │ │ │ └─ SMTP 발송 시작 │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 이메일 발송 모듈 │ │ │ │ (common_util/common_util_email.py) │ │ │ │ │ │ │ │ ├─ SMTP 클라이언트 (Google) │ │ │ │ ├─ 재시도 로직 (최대 3회) │ │ │ │ ├─ 로깅 및 모니터링 │ │ │ │ └─ 발송 이력 DB 저장 │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ └────────────────────┼────────────────────────────────┘ ↓ ┌──────────────────────────────┐ │ Google Gmail SMTP │ │ (smtp.gmail.com:587) │ │ + TLS 암호화 │ └──────────────────────────────┘ │ ↓ ┌──────────────────────────────┐ │ 수신자 이메일 서버 │ │ (Gmail, Naver, etc) │ └──────────────────────────────┘ ``` ### 3.2 개발 단계 상세 흐름 ``` 1단계: 사용자가 회원가입 버튼 클릭 ↓ 2단계: FastAPI 라우터 수신 - 이메일/비밀번호 검증 (Pydantic) - 중복 확인 - DB에 임시 사용자 생성 (status = PENDING) ↓ 3단계: 백그라운드 비동기 작업 시작 (즉시 응답) - OTP 코드 생성 (6자리 난수) - OTP 해시값 DB 저장 + 만료시간 기록 (5분) - 이메일 HTML 템플릿 렌더링 ↓ 4단계: 이메일 발송 (재시도 로직 포함) - 시도 1: SMTP 연결 및 발송 - 실패 시 2초 대기 후 재시도 - 3회 모두 실패 → 로그 기록 + 관리자 알림 ↓ 5단계: 클라이언트는 즉시 응답 받음 - "인증 코드를 이메일로 발송했습니다" - 사용자는 이메일 확인하여 OTP 입력 ``` --- ## 4. 개발 단계 설정 (Phase 1) ### 4.1 Google Gmail SMTP 설정 #### 단계 1: 앱 비밀번호 생성 ``` Google 계정 (umsangdon@gmail.com) 접속 ↓ Google 계정 관리 페이지 (myaccount.google.com) ↓ 왼쪽 메뉴: "보안" (Security) ↓ 2단계 인증 활성화 (이미 되어있으면 스킵) ↓ 앱 비밀번호 (App passwords) ├─ 앱: "메일(Mail)" ├─ 기기: "Windows" └─ [생성] 버튼 ↓ 16자리 비밀번호 생성 (예: abcd efgh ijkl mnop) ↓ .env 파일에 저장 ``` #### 단계 2: .env 파일 작성 ```bash # config/.env (개발용, .gitignore에 포함) # ===== Email Infrastructure (Phase 1) ===== SMTP_PROVIDER=google SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USERNAME=umsangdon@gmail.com SMTP_PASSWORD=abcd efgh ijkl mnop # 위에서 생성한 앱 비밀번호 SMTP_USE_TLS=true # 발송 이메일 주소 EMAIL_FROM_ADDRESS=umsangdon@gmail.com EMAIL_FROM_NAME=Aislo Support EMAIL_NOREPLY_ADDRESS=umsangdon+noreply@gmail.com # 답장 불가 계정 # 이메일 발송 설정 EMAIL_RETRY_COUNT=3 EMAIL_RETRY_DELAY_SECONDS=2 EMAIL_OTP_VALID_MINUTES=5 EMAIL_NOTIFICATION_ENABLED=true # 관리자 이메일 (에러 발생 시 수신) ADMIN_EMAIL=umsangdon@gmail.com ``` #### 단계 3: config_system.py 수정 ```python # config/config_system.py import os from dotenv import load_dotenv load_dotenv() # ===== Email Configuration ===== SMTP_PROVIDER = os.getenv("SMTP_PROVIDER", "google") SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com") SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) SMTP_USERNAME = os.getenv("SMTP_USERNAME", "umsangdon@gmail.com") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true" EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS", "umsangdon@gmail.com") EMAIL_FROM_NAME = os.getenv("EMAIL_FROM_NAME", "Aislo Support") EMAIL_NOREPLY_ADDRESS = os.getenv("EMAIL_NOREPLY_ADDRESS", "umsangdon+noreply@gmail.com") EMAIL_RETRY_COUNT = int(os.getenv("EMAIL_RETRY_COUNT", "3")) EMAIL_RETRY_DELAY_SECONDS = int(os.getenv("EMAIL_RETRY_DELAY_SECONDS", "2")) EMAIL_OTP_VALID_MINUTES = int(os.getenv("EMAIL_OTP_VALID_MINUTES", "5")) EMAIL_NOTIFICATION_ENABLED = os.getenv("EMAIL_NOTIFICATION_ENABLED", "true").lower() == "true" ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "umsangdon@gmail.com") ``` --- ## 5. 이메일 발송 모듈 (common_util_email.py) ### 5.1 기본 구조 ```python # common_util/common_util_email.py import smtplib import asyncio import logging from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime from config.config_system import ( SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS, EMAIL_FROM_ADDRESS, EMAIL_FROM_NAME, EMAIL_RETRY_COUNT, EMAIL_RETRY_DELAY_SECONDS, ) logger = logging.getLogger(__name__) class EmailService: """Google SMTP를 통한 비동기 이메일 발송""" @staticmethod async def send_email( to_email: str, subject: str, body_html: str, body_text: str = None, retry_count: int = None ) -> bool: """ 비동기 이메일 발송 (재시도 로직 포함) Args: to_email: 수신자 이메일 subject: 제목 body_html: HTML 본문 body_text: 텍스트 본문 (선택) retry_count: 재시도 횟수 (기본값: 환경변수) Returns: 발송 성공 여부 (True/False) """ if retry_count is None: retry_count = EMAIL_RETRY_COUNT for attempt in range(retry_count): try: # 메시지 구성 msg = MIMEMultipart("alternative") msg["From"] = f"{EMAIL_FROM_NAME} <{EMAIL_FROM_ADDRESS}>" msg["To"] = to_email msg["Subject"] = subject msg["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") # 텍스트 본문 추가 (메일 클라이언트가 HTML 미지원 시) if body_text: msg.attach(MIMEText(body_text, "plain", "utf-8")) # HTML 본문 추가 msg.attach(MIMEText(body_html, "html", "utf-8")) # 별도 스레드에서 SMTP 발송 (논블로킹) await asyncio.to_thread( EmailService._send_smtp_blocking, msg, to_email ) logger.info(f"✅ 이메일 발송 성공: {to_email} (제목: {subject})") return True except Exception as e: logger.warning( f"⚠️ 이메일 발송 실패 (시도 {attempt+1}/{retry_count}): " f"{to_email} - {str(e)}" ) # 마지막 시도가 아니면 대기 후 재시도 (지수 백오프) if attempt < retry_count - 1: delay = EMAIL_RETRY_DELAY_SECONDS * (2 ** attempt) await asyncio.sleep(delay) continue logger.error(f"❌ 이메일 발송 최종 실패: {to_email} (제목: {subject})") return False @staticmethod def _send_smtp_blocking(msg, to_email): """ 동기 SMTP 발송 (asyncio.to_thread에서 실행) Args: msg: MIMEMultipart 메시지 객체 to_email: 수신자 이메일 """ with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: if SMTP_USE_TLS: server.starttls() server.login(SMTP_USERNAME, SMTP_PASSWORD) server.send_message(msg) ``` ### 5.2 이메일 템플릿 모듈 (common_util_email_templates.py) ```python # common_util/common_util_email_templates.py def get_otp_email_html(otp_code: str, valid_minutes: int = 5) -> tuple[str, str]: """ OTP 인증 이메일 반환 (HTML + Text) Returns: (html_body, text_body) """ html_body = f"""
이메일 인증
안녕하세요!
아래 인증 코드를 입력하여 계정을 확인해주세요:
이 코드는 {valid_minutes}분간 유효합니다.
Aislo는 절대로 귀하의 인증 코드를 요청하지 않습니다.
안녕하세요 관리자님,
{user_email}이 {company_name}에 가입을 신청했습니다.
아래 버튼을 클릭하여 승인 또는 거부해주세요:
승인/거부 하기
버튼이 작동하지 않으면 아래 링크를 직접 열어주세요:
{approval_url}