Tr_Code/EMAIL_CODE_AUTH_GUIDE.md

11 KiB

이메일 코드 인증 가이드

📧 개요

링크 클릭 방식 대신 6자리 숫자 코드 입력 방식의 이메일 인증을 사용합니다. 이는 스팸/피싱 의심을 줄이고 더 안전한 인증 경험을 제공합니다.

주요 기능

1. 회원가입 이메일 인증

  • 회원가입 시 입력한 이메일로 6자리 인증 코드 발송
  • 코드 입력 및 검증 후 회원가입 완료
  • 코드 유효 시간: 5분
  • 최대 5회 시도 가능

2. 비밀번호 재설정 이메일 인증

  • 비밀번호 찾기 시 이메일로 6자리 인증 코드 발송
  • 코드 검증 후 비밀번호 재설정 페이지로 이동
  • 코드 유효 시간: 5분
  • 재설정 페이지 접근 유효 시간: 10분

3. 코드 재전송

  • 코드가 만료되거나 받지 못한 경우 재전송 가능
  • 재전송은 첫 발송 후 30초 경과 시 가능

🎨 UI/UX 특징

1. 6자리 코드 입력

  • 각 자리별 개별 입력 필드
  • 자동으로 다음 칸으로 포커스 이동
  • 붙여넣기 지원 (6자리 숫자 자동 분배)
  • 백스페이스로 이전 칸으로 이동

2. 실시간 타이머

  • 남은 시간을 시각적으로 표시 (분:초)
  • 시간 만료 시 경고 메시지
  • 재전송 버튼 활성화 타이밍 표시

3. 에러 처리

  • 잘못된 코드 입력 시 shake 애니메이션
  • 남은 시도 횟수 표시
  • 명확한 에러 메시지

🏗️ 아키텍처

Frontend Component: EmailCodeVerification.vue

Props

interface Props {
  email: string          // 인증 대상 이메일
  title?: string         // 제목 (기본값: '이메일 인증')
  expirySeconds?: number // 만료 시간 (기본값: 300초 = 5분)
}

Events

// 코드 검증 요청
emit('verify', code: string)

// 코드 재전송 요청
emit('resend')

사용 예시

<EmailCodeVerification
  :email="formData.email"
  title="회원가입 이메일 인증"
  @verify="handleCodeVerify"
  @resend="handleCodeResend"
  ref="codeVerificationRef"
/>

Backend API

1. 코드 발송

POST /api/email/send-code
Content-Type: application/json

{
  "email": "user@humetro.busan.kr",
  "type": "signup"  // 또는 "password_reset"
}

Response:

{
  "success": true,
  "message": "인증 코드를 이메일로 전송했습니다.",
  "debug_code": "123456"  // 개발 환경에서만
}

2. 코드 검증

POST /api/email/verify-code
Content-Type: application/json

{
  "email": "user@humetro.busan.kr",
  "code": "123456",
  "type": "signup"  // 또는 "password_reset"
}

Response (성공):

{
  "success": true,
  "message": "인증이 완료되었습니다."
}

Response (실패):

{
  "success": false,
  "error": "인증 코드가 올바르지 않습니다. (남은 시도: 3회)"
}

🔐 보안 기능

1. 코드 생성

  • 6자리 무작위 숫자 생성 (random.choices(string.digits, k=6))
  • 각 이메일/타입별 고유 세션 키 사용
  • 코드는 세션에 암호화되어 저장

2. 검증 제한

  • 시도 횟수 제한: 5회까지만 시도 가능
  • 시간 제한: 5분 후 자동 만료
  • 일회성: 한 번 검증되면 코드 즉시 삭제

3. 도메인 제한

  • @humetro.busan.kr 도메인만 허용
  • 백엔드에서 이메일 도메인 검증

4. 세션 관리

# 코드 저장 (5분간 유효)
session[f"email_code_{email}_{type}"] = {
    "code": "123456",
    "expiry": timestamp + 300,
    "attempts": 0
}

# 검증 완료 표시 (10분간 유효)
session[f"email_verified_{email}_{type}"] = {
    "verified_at": timestamp
}

📱 사용 흐름

회원가입 흐름

sequenceDiagram
    participant U as User
    participant F as Frontend
    participant B as Backend
    participant E as Email

    U->>F: 회원가입 정보 입력
    F->>B: POST /api/email/send-code
    B->>B: 6자리 코드 생성
    B->>E: 이메일 발송 (TODO)
    B->>F: {success: true}
    F->>U: 코드 입력 화면 표시
    U->>F: 코드 입력
    F->>B: POST /api/email/verify-code
    B->>B: 코드 검증
    B->>F: {success: true}
    F->>B: POST /auth/signup
    B->>F: 회원가입 완료
    F->>U: 로그인 페이지로 이동

비밀번호 재설정 흐름

sequenceDiagram
    participant U as User
    participant F as Frontend
    participant B as Backend

    U->>F: 이메일 입력
    F->>B: POST /api/email/send-code (type: password_reset)
    B->>F: {success: true}
    F->>U: 코드 입력 화면
    U->>F: 코드 입력
    F->>B: POST /api/email/verify-code
    B->>B: 세션에 인증 완료 표시
    B->>F: {success: true}
    F->>F: /reset-password?email=...&verified=true
    U->>F: 새 비밀번호 입력
    F->>B: POST /api/auth/reset-password
    B->>B: 세션에서 인증 확인
    B->>B: 비밀번호 업데이트
    B->>F: {success: true}
    F->>U: 로그인 페이지로 이동

🎯 회원가입 통합

SignupView.vue

<template>
  <!-- 이메일 코드 인증 단계 -->
  <EmailCodeVerification
    v-if="showCodeVerification"
    :email="formData.email"
    title="회원가입 이메일 인증"
    @verify="handleCodeVerify"
    @resend="handleCodeResend"
    ref="codeVerificationRef"
  />

  <!-- 회원가입 폼 -->
  <form v-else @submit.prevent="handleSignup">
    <!-- 폼 필드들... -->
  </form>
</template>

<script setup lang="ts">
const showCodeVerification = ref(false)

async function handleSignup() {
  // 유효성 검증...
  
  // 이메일 코드 발송
  const sent = await sendEmailCode()
  
  if (sent) {
    // 코드 인증 화면으로 전환
    showCodeVerification.value = true
  }
}

async function handleCodeVerify(code: string) {
  const response = await fetch('/api/email/verify-code', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: formData.value.email,
      code,
      type: 'signup'
    })
  })

  const data = await response.json()

  if (data.success) {
    // 회원가입 완료
    await completeSignup()
  } else {
    // 에러 표시
    codeVerificationRef.value?.setError(data.error)
  }
}
</script>

🔄 비밀번호 재설정 통합

ForgotPasswordView.vue → ResetPasswordView.vue

<!-- ForgotPasswordView.vue -->
<template>
  <EmailCodeVerification
    v-if="showCodeVerification"
    :email="email"
    title="비밀번호 재설정 인증"
    @verify="handleCodeVerify"
    @resend="handleCodeResend"
  />

  <form v-else @submit.prevent="handleSubmit">
    <input v-model="email" type="email" />
    <button type="submit">코드 전송</button>
  </form>
</template>

<script setup lang="ts">
async function handleCodeVerify(code: string) {
  const response = await fetch('/api/email/verify-code', {
    method: 'POST',
    body: JSON.stringify({
      email: email.value,
      code,
      type: 'password_reset'
    })
  })

  const data = await response.json()

  if (data.success) {
    // 비밀번호 재설정 페이지로 이동
    router.push({
      path: '/reset-password',
      query: { email: email.value, verified: 'true' }
    })
  }
}
</script>
<!-- ResetPasswordView.vue -->
<script setup lang="ts">
onMounted(() => {
  const queryEmail = route.query.email as string
  const verified = route.query.verified as string

  if (!queryEmail || verified !== 'true') {
    // 인증되지 않은 접근 차단
    errorMessage.value = '잘못된 접근입니다.'
    router.push('/forgot-password')
  }
})

async function handleSubmit() {
  const response = await fetch('/api/auth/reset-password', {
    method: 'POST',
    body: JSON.stringify({
      email: email.value,
      password: password.value
    })
  })
  
  // 백엔드에서 세션의 인증 여부를 확인
}
</script>

🔧 개발 환경 설정

1. 이메일 발송 (TODO)

현재는 콘솔에만 코드를 출력하고, 개발 환경에서는 API 응답에 코드를 포함시킵니다:

# 개발 환경에서만
return {
    "success": True,
    "message": "인증 코드를 이메일로 전송했습니다.",
    "debug_code": code if app.debug else None
}

프로덕션에서는 실제 이메일 발송 서비스 연동 필요:

  • SMTP 서버
  • SendGrid / AWS SES
  • Supabase Email (Docker 환경에서는 제한적)

2. 콘솔에서 코드 확인

백엔드 콘솔:

=== 이메일 인증 코드 ===
To: user@humetro.busan.kr
Code: 123456
Type: signup
Expiry: 2025-10-14 15:30:00
=======================

프론트엔드 콘솔:

=== 개발 환경 인증 코드 ===
코드: 123456
=======================

📊 에러 처리

Frontend 에러 메시지

상황 메시지
잘못된 코드 "인증 코드가 올바르지 않습니다. (남은 시도: N회)"
시도 횟수 초과 "인증 시도 횟수를 초과했습니다. 코드를 재전송해주세요."
코드 만료 "인증 코드가 만료되었습니다. 코드를 재전송해주세요."
코드 미존재 "인증 코드가 존재하지 않거나 만료되었습니다."

Backend 에러 응답

# 시도 횟수 초과
if stored_data["attempts"] >= 5:
    session.pop(session_key, None)
    return {
        "success": False,
        "error": "인증 시도 횟수를 초과했습니다. 코드를 재전송해주세요."
    }, 400

# 만료
if datetime.now().timestamp() > stored_data["expiry"]:
    session.pop(session_key, None)
    return {
        "success": False,
        "error": "인증 코드가 만료되었습니다. 코드를 재전송해주세요."
    }, 400

# 불일치
if stored_data["code"] != code:
    stored_data["attempts"] += 1
    session[session_key] = stored_data
    return {
        "success": False,
        "error": f"인증 코드가 올바르지 않습니다. (남은 시도: {5 - stored_data['attempts']}회)"
    }, 400

🎨 UI 커스터마이징

다크모드 지원

:root.dark .code-digit {
  background: #1a202c;
  border-color: #4a5568;
  color: #f7fafc;
}

:root.dark .timer {
  color: #a78bfa;
}

애니메이션

/* Shake 애니메이션 (에러 시) */
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}

.code-digit.error {
  border-color: #dc2626;
  animation: shake 0.3s;
}

📝 TODO (향후 개선사항)

1. 이메일 발송 구현

# SMTP 또는 이메일 서비스 연동
import smtplib
from email.mime.text import MIMEText

def send_verification_email(email, code):
    msg = MIMEText(f"인증 코드: {code}")
    msg['Subject'] = '부산교통공사 1호선 - 이메일 인증'
    msg['From'] = 'noreply@humetro.busan.kr'
    msg['To'] = email
    
    # SMTP 발송...

2. 이메일 템플릿

  • HTML 이메일 템플릿 디자인
  • 회사 로고 및 브랜딩 추가
  • 다국어 지원

3. 보안 강화

  • Rate limiting (IP별 요청 제한)
  • Captcha 추가 (무차별 대입 공격 방지)
  • 로그 기록 (audit_logs)

4. 사용자 경험 개선

  • 이메일 자동 완성
  • 코드 자동 감지 (SMS/Email OTP)
  • 음성 안내 (접근성)

🔗 참고 자료