Django Ninja API로 구축하는 고급 User 모델: 이메일 인증부터 관리자 승인까지
Django로 사용자 관리 시스템을 구축할 때 기본 User 모델로는 한계가 있습니다. 실제 서비스에서는 이메일 인증, 관리자 승인, 계정 정지/활성화 등의 기능이 필요하며, 이를 API로 제공해야 하는 경우가 많습니다. 이 글에서는...
Django로 사용자 관리 시스템을 구축할 때 기본 User 모델로는 한계가 있습니다. 실제 서비스에서는 이메일 인증, 관리자 승인, 계정 정지/활성화 등의 기능이 필요하며, 이를 API로 제공해야 하는 경우가 많습니다. 이 글에서는 Django Ninja를 사용하여 고급 User 모델을 API로 효율적으로 구현하는 방법을 단계별로 알아보겠습니다.
📚 고급 User 모델 설계 개요
구현할 기능들
- 이메일 인증 시스템: 회원가입 후 이메일로 인증 링크 발송
- 관리자 승인 시스템: 관리자가 수동으로 계정 승인
- 계정 정지 기능: 관리자가 사용자 계정을 정지
- 계정 재활성화: 정지된 계정을 다시 활성화
User 상태 관리 전략
사용자의 상태를 체계적으로 관리하기 위해 다음과 같은 상태를 정의합니다:
from django.db import models
class UserStatus(models.TextChoices):
PENDING_EMAIL = 'pending_email', '이메일 인증 대기'
PENDING_APPROVAL = 'pending_approval', '관리자 승인 대기'
ACTIVE = 'active', '활성'
SUSPENDED = 'suspended', '정지'
DEACTIVATED = 'deactivated', '비활성'
🔧 Custom User 모델 구현
1. BaseUserManager 커스터마이징
먼저 사용자 생성 로직을 관리하는 매니저를 구현합니다:
# accounts/managers.py
from django.contrib.auth.models import BaseUserManager
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.utils import timezone
class CustomUserManager(BaseUserManager):
def _create_user(self, email, password, **extra_fields):
"""공통 사용자 생성 로직"""
if not email:
raise ValueError('이메일은 필수입니다.')
try:
validate_email(email)
except ValidationError:
raise ValueError('유효한 이메일 주소를 입력해주세요.')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
return user
def create_user(self, email, password=None, **extra_fields):
"""일반 사용자 생성"""
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
extra_fields.setdefault('status', 'pending_email')
user = self._create_user(email, password, **extra_fields)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
"""슈퍼유저 생성"""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('status', 'active')
extra_fields.setdefault('is_approved', True)
extra_fields.setdefault('email_verified', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('슈퍼유저는 is_staff=True여야 합니다.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('슈퍼유저는 is_superuser=True여야 합니다.')
user = self._create_user(email, password, **extra_fields)
user.save(using=self._db)
return user
def get_active_users(self):
"""활성 사용자만 조회"""
return self.filter(status='active', is_active=True)
def get_pending_approval_users(self):
"""승인 대기 사용자 조회"""
return self.filter(status='pending_approval')
2. Custom User 모델 정의
# accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from django.core.mail import send_mail
from django.utils.crypto import get_random_string
from .managers import CustomUserManager
class UserStatus(models.TextChoices):
PENDING_EMAIL = 'pending_email', '이메일 인증 대기'
PENDING_APPROVAL = 'pending_approval', '관리자 승인 대기'
ACTIVE = 'active', '활성'
SUSPENDED = 'suspended', '정지'
DEACTIVATED = 'deactivated', '비활성'
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField('이메일', unique=True)
first_name = models.CharField('이름', max_length=30, blank=True)
last_name = models.CharField('성', max_length=30, blank=True)
# 계정 상태 관리
status = models.CharField(
'계정 상태',
max_length=20,
choices=UserStatus.choices,
default=UserStatus.PENDING_EMAIL
)
# 기본 Django 필드
is_staff = models.BooleanField('스태프 여부', default=False)
is_active = models.BooleanField('활성 여부', default=True)
# 추가 인증 필드
email_verified = models.BooleanField('이메일 인증 여부', default=False)
email_verification_token = models.CharField(
'이메일 인증 토큰',
max_length=64,
blank=True
)
email_verification_sent_at = models.DateTimeField(
'이메일 인증 발송 시간',
null=True,
blank=True
)
# 관리자 승인 관련
is_approved = models.BooleanField('관리자 승인 여부', default=False)
approved_by = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approved_users',
verbose_name='승인자'
)
approved_at = models.DateTimeField('승인 시간', null=True, blank=True)
# 정지 관련
suspended_by = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='suspended_users',
verbose_name='정지 처리자'
)
suspended_at = models.DateTimeField('정지 시간', null=True, blank=True)
suspension_reason = models.TextField('정지 사유', blank=True)
# 날짜 필드
date_joined = models.DateTimeField('가입일', default=timezone.now)
last_login_ip = models.GenericIPAddressField('마지막 로그인 IP', null=True, blank=True)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = '사용자'
verbose_name_plural = '사용자들'
db_table = 'auth_user'
def __str__(self):
return self.email
def get_full_name(self):
"""전체 이름 반환"""
return f'{self.last_name} {self.first_name}'.strip()
def get_short_name(self):
"""짧은 이름 반환"""
return self.first_name or self.email.split('@')[0]
def email_user(self, subject, message, from_email=None, **kwargs):
"""이메일 발송"""
send_mail(subject, message, from_email, [self.email], **kwargs)
def can_login(self):
"""로그인 가능 여부 확인"""
return (
self.is_active and
self.email_verified and
self.is_approved and
self.status == UserStatus.ACTIVE
)
def generate_email_verification_token(self):
"""이메일 인증 토큰 생성"""
self.email_verification_token = get_random_string(64)
self.email_verification_sent_at = timezone.now()
self.save(update_fields=['email_verification_token', 'email_verification_sent_at'])
return self.email_verification_token
def verify_email(self):
"""이메일 인증 처리"""
self.email_verified = True
self.email_verification_token = ''
# 이메일 인증 후 관리자 승인 대기 상태로 변경
if self.status == UserStatus.PENDING_EMAIL:
self.status = UserStatus.PENDING_APPROVAL
self.save(update_fields=['email_verified', 'email_verification_token', 'status'])
def approve_user(self, approved_by_user):
"""관리자 승인 처리"""
if not self.email_verified:
raise ValueError('이메일 인증이 필요합니다.')
self.is_approved = True
self.approved_by = approved_by_user
self.approved_at = timezone.now()
self.status = UserStatus.ACTIVE
self.save(update_fields=[
'is_approved', 'approved_by', 'approved_at', 'status'
])
def suspend_user(self, suspended_by_user, reason=''):
"""사용자 정지"""
self.status = UserStatus.SUSPENDED
self.suspended_by = suspended_by_user
self.suspended_at = timezone.now()
self.suspension_reason = reason
self.save(update_fields=[
'status', 'suspended_by', 'suspended_at', 'suspension_reason'
])
def reactivate_user(self):
"""사용자 재활성화 (이메일 인증 상태에 따른 차별적 처리)"""
if self.status == UserStatus.SUSPENDED:
if self.email_verified:
# 이메일 인증이 완료된 사용자는 관리자 승인 대기 상태로
self.status = UserStatus.PENDING_APPROVAL
self.is_approved = False
self.approved_by = None
self.approved_at = None
else:
# 이메일 인증이 안된 사용자는 이메일 인증 대기 상태로
self.status = UserStatus.PENDING_EMAIL
self.is_approved = False
self.approved_by = None
self.approved_at = None
# 새로운 이메일 인증 토큰 생성
self.generate_email_verification_token()
# 정지 관련 정보 초기화
self.suspended_by = None
self.suspended_at = None
self.suspension_reason = ''
self.save(update_fields=[
'status', 'suspended_by', 'suspended_at', 'suspension_reason',
'is_approved', 'approved_by', 'approved_at'
])
🚀 Django Ninja API 구현
1. 패키지 설치 및 설정
먼저 필요한 패키지들을 설치합니다:
pip install django-ninja
pip install pydantic[email]
2. Pydantic 스키마 정의
# accounts/schemas.py
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
from .models import UserStatus
class UserRegistrationSchema(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
first_name: str = Field(..., max_length=30)
last_name: str = Field(..., max_length=30)
class UserLoginSchema(BaseModel):
email: EmailStr
password: str
class EmailVerificationSchema(BaseModel):
token: str
class UserApprovalSchema(BaseModel):
user_id: int
approval_note: Optional[str] = None
class UserSuspensionSchema(BaseModel):
user_id: int
reason: str = Field(..., min_length=1, max_length=500)
class UserReactivationSchema(BaseModel):
user_id: int
class UserResponseSchema(BaseModel):
id: int
email: str
first_name: str
last_name: str
status: str
email_verified: bool
is_approved: bool
date_joined: datetime
last_login: Optional[datetime]
class Config:
from_attributes = True
class UserListResponseSchema(BaseModel):
users: list[UserResponseSchema]
total: int
page: int
per_page: int
class UserStatusResponseSchema(BaseModel):
total_users: int
recent_registrations: int
pending_email: int
pending_approval: int
active: int
suspended: int
deactivated: int
class MessageResponseSchema(BaseModel):
message: str
success: bool
class ErrorResponseSchema(BaseModel):
error: str
detail: Optional[str] = None
class TokenResponseSchema(BaseModel):
access_token: str
token_type: str = "bearer"
user: UserResponseSchema
3. 인증 헬퍼 함수
# accounts/auth.py
from django.contrib.auth import authenticate
from django.contrib.auth.models import AnonymousUser
from ninja.security import HttpBearer
from ninja import NinjaAPI
from django.contrib.sessions.models import Session
from django.utils import timezone
from .models import CustomUser
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
try:
# 간단한 세션 기반 인증 (실제로는 JWT 등을 사용 권장)
session = Session.objects.get(session_key=token)
if session.expire_date < timezone.now():
return None
user_id = session.get_decoded().get('_auth_user_id')
if user_id:
user = CustomUser.objects.get(id=user_id)
if user.can_login():
return user
except (Session.DoesNotExist, CustomUser.DoesNotExist):
pass
return None
class AdminRequired(AuthBearer):
def authenticate(self, request, token):
user = super().authenticate(request, token)
if user and user.is_staff:
return user
return None
# 인증 인스턴스
auth = AuthBearer()
admin_auth = AdminRequired()
def get_current_user(request):
"""현재 로그인한 사용자 반환"""
if hasattr(request, 'auth') and request.auth:
return request.auth
return AnonymousUser()
📱 사용자 관련 API 엔드포인트
1. 사용자 인증 API
# accounts/api.py
from ninja import NinjaAPI, Query
from ninja.pagination import paginate, PageNumberPagination
from django.shortcuts import get_object_or_404
from django.contrib.auth import authenticate, login, logout
from django.contrib import messages
from django.core.mail import send_mail
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from django.db.models import Q, Count
from typing import List
from .models import CustomUser, UserStatus
from .schemas import *
from .auth import auth, admin_auth, get_current_user
api = NinjaAPI(title="User Management API", version="1.0.0")
@api.post("/auth/register", response={201: MessageResponseSchema, 400: ErrorResponseSchema})
def register_user(request, data: UserRegistrationSchema):
"""사용자 회원가입"""
try:
# 이메일 중복 확인
if CustomUser.objects.filter(email=data.email).exists():
return 400, {"error": "이미 존재하는 이메일입니다."}
# 사용자 생성
user = CustomUser.objects.create_user(
email=data.email,
password=data.password,
first_name=data.first_name,
last_name=data.last_name
)
# 이메일 인증 토큰 생성 및 발송
token = user.generate_email_verification_token()
send_verification_email(user, token, request)
return 201, {
"message": "회원가입이 완료되었습니다. 이메일을 확인하여 인증을 완료해주세요.",
"success": True
}
except Exception as e:
return 400, {"error": "회원가입 처리 중 오류가 발생했습니다.", "detail": str(e)}
@api.post("/auth/login", response={200: TokenResponseSchema, 401: ErrorResponseSchema})
def login_user(request, data: UserLoginSchema):
"""사용자 로그인"""
user = authenticate(request, username=data.email, password=data.password)
if user is None:
return 401, {"error": "이메일 또는 비밀번호가 잘못되었습니다."}
# 사용자 상태별 처리
if user.status == UserStatus.PENDING_EMAIL:
return 401, {"error": "이메일 인증이 필요합니다. 이메일을 확인해주세요."}
elif user.status == UserStatus.PENDING_APPROVAL:
return 401, {"error": "관리자 승인을 기다리고 있습니다. 잠시만 기다려주세요."}
elif user.status == UserStatus.SUSPENDED:
return 401, {"error": f"계정이 정지되었습니다. 정지 사유: {user.suspension_reason}"}
elif user.status == UserStatus.DEACTIVATED:
return 401, {"error": "비활성화된 계정입니다. 고객센터에 문의해주세요."}
elif user.status == UserStatus.ACTIVE:
# 정상 로그인
login(request, user)
# 로그인 IP 저장
save_login_ip(request, user)
return 200, {
"access_token": request.session.session_key,
"token_type": "bearer",
"user": UserResponseSchema.from_orm(user)
}
return 401, {"error": "로그인할 수 없는 계정 상태입니다."}
@api.post("/auth/logout", auth=auth, response={200: MessageResponseSchema})
def logout_user(request):
"""사용자 로그아웃"""
logout(request)
return {"message": "로그아웃되었습니다.", "success": True}
@api.get("/auth/me", auth=auth, response=UserResponseSchema)
def get_current_user_info(request):
"""현재 로그인한 사용자 정보"""
return UserResponseSchema.from_orm(request.auth)
def send_verification_email(user, token, request):
"""이메일 인증 메일 발송"""
verification_url = request.build_absolute_uri(
f"/api/auth/verify-email?token={token}"
)
subject = '[사이트명] 이메일 인증을 완료해주세요'
message = f"""
안녕하세요 {user.get_short_name()}님!
회원가입을 완료하려면 아래 링크를 클릭하여 이메일 인증을 완료해주세요.
{verification_url}
이 링크는 24시간 후 만료됩니다.
감사합니다.
"""
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[user.email],
fail_silently=False,
)
def save_login_ip(request, user):
"""로그인 IP 저장"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
user.last_login_ip = ip
user.save(update_fields=['last_login_ip'])
2. 이메일 인증 API
@api.get("/auth/verify-email", response={200: MessageResponseSchema, 400: ErrorResponseSchema})
def verify_email(request, token: str = Query(...)):
"""이메일 인증 처리"""
try:
user = CustomUser.objects.get(
email_verification_token=token,
email_verified=False
)
# 토큰 만료 확인 (24시간)
if user.email_verification_sent_at:
expiry_time = user.email_verification_sent_at + timezone.timedelta(hours=24)
if timezone.now() > expiry_time:
return 400, {"error": "인증 링크가 만료되었습니다."}
# 이메일 인증 처리
user.verify_email()
return 200, {
"message": "이메일 인증이 완료되었습니다. 관리자 승인을 기다려주세요.",
"success": True
}
except CustomUser.DoesNotExist:
return 400, {"error": "유효하지 않은 인증 토큰입니다."}
@api.post("/auth/resend-verification", response={200: MessageResponseSchema, 400: ErrorResponseSchema})
def resend_verification_email(request, email: EmailStr):
"""이메일 인증 재발송"""
try:
user = CustomUser.objects.get(
email=email,
email_verified=False,
status=UserStatus.PENDING_EMAIL
)
# 인증 메일 재발송
token = user.generate_email_verification_token()
send_verification_email(user, token, request)
return 200, {
"message": "인증 이메일이 재발송되었습니다.",
"success": True
}
except CustomUser.DoesNotExist:
return 400, {"error": "해당 이메일로 가입된 계정을 찾을 수 없습니다."}
👨💼 관리자 API 엔드포인트
1. 사용자 목록 및 통계 API
@api.get("/admin/users", auth=admin_auth, response=UserListResponseSchema)
@paginate(PageNumberPagination, page_size=20)
def get_users_list(
request,
status: Optional[str] = Query(None),
email_verified: Optional[bool] = Query(None),
is_approved: Optional[bool] = Query(None),
search: Optional[str] = Query(None)
):
"""사용자 목록 조회 (관리자용)"""
queryset = CustomUser.objects.all().select_related('approved_by', 'suspended_by')
# 필터링
if status:
queryset = queryset.filter(status=status)
if email_verified is not None:
queryset = queryset.filter(email_verified=email_verified)
if is_approved is not None:
queryset = queryset.filter(is_approved=is_approved)
if search:
queryset = queryset.filter(
Q(email__icontains=search) |
Q(first_name__icontains=search) |
Q(last_name__icontains=search)
)
queryset = queryset.order_by('-date_joined')
return queryset
@api.get("/admin/users/statistics", auth=admin_auth, response=UserStatusResponseSchema)
def get_user_statistics(request):
"""사용자 통계 조회"""
total_users = CustomUser.objects.count()
status_counts = CustomUser.objects.aggregate(
pending_email=Count('id', filter=Q(status=UserStatus.PENDING_EMAIL)),
pending_approval=Count('id', filter=Q(status=UserStatus.PENDING_APPROVAL)),
active=Count('id', filter=Q(status=UserStatus.ACTIVE)),
suspended=Count('id', filter=Q(status=UserStatus.SUSPENDED)),
deactivated=Count('id', filter=Q(status=UserStatus.DEACTIVATED)),
)
# 최근 가입자 (7일)
week_ago = timezone.now() - timezone.timedelta(days=7)
recent_registrations = CustomUser.objects.filter(
date_joined__gte=week_ago
).count()
return {
'total_users': total_users,
'recent_registrations': recent_registrations,
**status_counts
}
@api.get("/admin/users/pending-approval", auth=admin_auth, response=List[UserResponseSchema])
def get_pending_approval_users(request):
"""승인 대기 사용자 목록"""
users = CustomUser.objects.filter(
status=UserStatus.PENDING_APPROVAL,
email_verified=True
).order_by('date_joined')
return [UserResponseSchema.from_orm(user) for user in users]
2. 사용자 승인 API
@api.post("/admin/users/approve", auth=admin_auth, response={200: MessageResponseSchema, 400: ErrorResponseSchema})
def approve_user(request, data: UserApprovalSchema):
"""사용자 승인"""
try:
user = get_object_or_404(CustomUser, id=data.user_id)
if user.status != UserStatus.PENDING_APPROVAL:
return 400, {"error": "승인 대기 상태가 아닙니다."}
if not user.email_verified:
return 400, {"error": "이메일 인증이 완료되지 않았습니다."}
# 사용자 승인
user.approve_user(request.auth)
# 승인 알림 이메일 발송
send_approval_email(user, request)
return 200, {
"message": f"{user.email} 사용자가 승인되었습니다.",
"success": True
}
except Exception as e:
return 400, {"error": "승인 처리 중 오류가 발생했습니다.", "detail": str(e)}
@api.post("/admin/users/approve-bulk", auth=admin_auth, response={200: MessageResponseSchema})
def approve_users_bulk(request, user_ids: List[int]):
"""사용자 일괄 승인"""
count = 0
for user_id in user_ids:
try:
user = CustomUser.objects.get(
id=user_id,
status=UserStatus.PENDING_APPROVAL,
email_verified=True
)
user.approve_user(request.auth)
send_approval_email(user, request)
count += 1
except CustomUser.DoesNotExist:
continue
return {
"message": f"{count}명의 사용자가 승인되었습니다.",
"success": True
}
def send_approval_email(user, request):
"""승인 알림 이메일 발송"""
subject = '[사이트명] 계정이 승인되었습니다'
message = f"""
안녕하세요 {user.get_short_name()}님!
귀하의 계정이 관리자에 의해 승인되었습니다.
이제 모든 서비스를 이용하실 수 있습니다.
로그인: {request.build_absolute_uri('/api/auth/login')}
감사합니다.
"""
user.email_user(subject, message)
3. 사용자 정지 API
@api.post("/admin/users/suspend", auth=admin_auth, response={200: MessageResponseSchema, 400: ErrorResponseSchema})
def suspend_user(request, data: UserSuspensionSchema):
"""사용자 정지"""
try:
user = get_object_or_404(CustomUser, id=data.user_id)
if user.status != UserStatus.ACTIVE:
return 400, {"error": "활성 상태가 아닙니다."}
if user.is_superuser:
return 400, {"error": "슈퍼유저는 정지할 수 없습니다."}
# 사용자 정지
user.suspend_user(request.auth, data.reason)
# 정지 알림 이메일 발송
send_suspension_email(user, data.reason)
return 200, {
"message": f"{user.email} 사용자가 정지되었습니다.",
"success": True
}
except Exception as e:
return 400, {"error": "정지 처리 중 오류가 발생했습니다.", "detail": str(e)}
@api.post("/admin/users/suspend-bulk", auth=admin_auth, response={200: MessageResponseSchema})
def suspend_users_bulk(request, user_ids: List[int], reason: str):
"""사용자 일괄 정지"""
count = 0
for user_id in user_ids:
try:
user = CustomUser.objects.get(
id=user_id,
status=UserStatus.ACTIVE,
is_superuser=False
)
user.suspend_user(request.auth, reason)
send_suspension_email(user, reason)
count += 1
except CustomUser.DoesNotExist:
continue
return {
"message": f"{count}명의 사용자가 정지되었습니다.",
"success": True
}
def send_suspension_email(user, reason):
"""정지 알림 이메일 발송"""
subject = '[사이트명] 계정이 정지되었습니다'
message = f"""
안녕하세요 {user.get_short_name()}님!
귀하의 계정이 다음 사유로 정지되었습니다:
정지 사유: {reason}
정지 시간: {user.suspended_at}
문의사항이 있으시면 고객센터로 연락해주세요.
감사합니다.
"""
user.email_user(subject, message)
4. 사용자 재활성화 API (이메일 인증 상태별 처리)
@api.post("/admin/users/reactivate", auth=admin_auth, response={200: MessageResponseSchema, 400: ErrorResponseSchema})
def reactivate_user(request, data: UserReactivationSchema):
"""사용자 재활성화 (이메일 인증 상태에 따른 차별적 처리)"""
try:
user = get_object_or_404(CustomUser, id=data.user_id)
if user.status != UserStatus.SUSPENDED:
return 400, {"error": "정지 상태가 아닙니다."}
# 이메일 인증 상태에 따른 차별적 처리
old_email_verified = user.email_verified
user.reactivate_user()
if old_email_verified:
# 이메일 인증이 완료된 사용자 -> 관리자 승인 대기 상태
message_text = f"{user.email} 사용자가 재활성화되었습니다. (관리자 승인 대기 상태)"
send_reactivation_approval_needed_email(user, request)
else:
# 이메일 인증이 안된 사용자 -> 이메일 인증 대기 상태
message_text = f"{user.email} 사용자가 재활성화되었습니다. (이메일 인증 필요)"
# 새로운 인증 토큰으로 이메일 발송
send_verification_email(user, user.email_verification_token, request)
return 200, {
"message": message_text,
"success": True
}
except Exception as e:
return 400, {"error": "재활성화 처리 중 오류가 발생했습니다.", "detail": str(e)}
@api.get("/admin/users/{user_id}/reactivation-info", auth=admin_auth)
def get_reactivation_info(request, user_id: int):
"""재활성화 시 처리 방식 미리보기"""
try:
user = get_object_or_404(CustomUser, id=user_id)
if user.status != UserStatus.SUSPENDED:
return {"error": "정지 상태가 아닙니다."}
if user.email_verified:
next_status = "관리자 승인 대기"
description = "이메일 인증이 완료된 사용자이므로 관리자 승인 대기 상태로 변경됩니다."
else:
next_status = "이메일 인증 대기"
description = "이메일 인증이 완료되지 않은 사용자이므로 이메일 인증 대기 상태로 변경되고 새로운 인증 이메일이 발송됩니다."
return {
"user_email": user.email,
"current_status": user.get_status_display(),
"email_verified": user.email_verified,
"next_status": next_status,
"description": description
}
except Exception as e:
return {"error": str(e)}
def send_reactivation_approval_needed_email(user, request):
"""재활성화 후 승인 필요 알림 이메일"""
subject = '[사이트명] 계정이 재활성화되었습니다 (승인 대기)'
message = f"""
안녕하세요 {user.get_short_name()}님!
귀하의 계정이 재활성화되었습니다.
현재 관리자 승인을 기다리고 있는 상태입니다.
승인이 완료되면 다시 모든 서비스를 이용하실 수 있습니다.
감사합니다.
"""
user.email_user(subject, message)
🔧 URL 및 설정
URL 구성
# urls.py (프로젝트 메인)
from django.contrib import admin
from django.urls import path
from accounts.api import api
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls),
]
settings.py 설정
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'ninja', # Django Ninja 추가
'accounts',
]
AUTH_USER_MODEL = 'accounts.CustomUser'
# 이메일 설정
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
DEFAULT_FROM_EMAIL = 'your-email@gmail.com'
# 인증 백엔드
AUTHENTICATION_BACKENDS = [
'accounts.backends.CustomAuthenticationBackend',
]
# 세션 설정
SESSION_COOKIE_AGE = 86400 * 7 # 7일
SESSION_SAVE_EVERY_REQUEST = True
� API 문서 및 테스트
Swagger UI 접근
Django Ninja는 자동으로 API 문서를 생성합니다:
http://localhost:8000/api/docs
API 엔드포인트 요약
| 엔드포인트 | 메소드 | 설명 | 인증 필요 |
|---|---|---|---|
/api/auth/register |
POST | 회원가입 | No |
/api/auth/login |
POST | 로그인 | No |
/api/auth/logout |
POST | 로그아웃 | Yes |
/api/auth/me |
GET | 현재 사용자 정보 | Yes |
/api/auth/verify-email |
GET | 이메일 인증 | No |
/api/auth/resend-verification |
POST | 인증 이메일 재발송 | No |
/api/admin/users |
GET | 사용자 목록 | Admin |
/api/admin/users/statistics |
GET | 사용자 통계 | Admin |
/api/admin/users/pending-approval |
GET | 승인 대기 목록 | Admin |
/api/admin/users/approve |
POST | 사용자 승인 | Admin |
/api/admin/users/approve-bulk |
POST | 일괄 승인 | Admin |
/api/admin/users/suspend |
POST | 사용자 정지 | Admin |
/api/admin/users/suspend-bulk |
POST | 일괄 정지 | Admin |
/api/admin/users/reactivate |
POST | 사용자 재활성화 | Admin |
/api/admin/users/{id}/reactivation-info |
GET | 재활성화 정보 | Admin |
프론트엔드 연동 예시
// JavaScript 클라이언트 예시
class UserAPI {
constructor(baseURL = '/api') {
this.baseURL = baseURL;
this.token = localStorage.getItem('access_token');
}
async register(userData) {
const response = await fetch(`${this.baseURL}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
return response.json();
}
async login(email, password) {
const response = await fetch(`${this.baseURL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password })
});
if (response.ok) {
const data = await response.json();
this.token = data.access_token;
localStorage.setItem('access_token', this.token);
return data;
}
throw new Error('Login failed');
}
async getCurrentUser() {
const response = await fetch(`${this.baseURL}/auth/me`, {
headers: {
'Authorization': `Bearer ${this.token}`,
}
});
return response.json();
}
async approveUser(userId, approvalNote = null) {
const response = await fetch(`${this.baseURL}/admin/users/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
},
body: JSON.stringify({
user_id: userId,
approval_note: approvalNote
})
});
return response.json();
}
async suspendUser(userId, reason) {
const response = await fetch(`${this.baseURL}/admin/users/suspend`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
},
body: JSON.stringify({
user_id: userId,
reason: reason
})
});
return response.json();
}
async reactivateUser(userId) {
const response = await fetch(`${this.baseURL}/admin/users/reactivate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
},
body: JSON.stringify({ user_id: userId })
});
return response.json();
}
}
// 사용 예시
const userAPI = new UserAPI();
// 사용자 등록
userAPI.register({
email: 'user@example.com',
password: 'securepassword123',
first_name: '홍길동',
last_name: '홍'
}).then(result => {
console.log('Registration result:', result);
});
// 로그인
userAPI.login('user@example.com', 'securepassword123')
.then(result => {
console.log('Login successful:', result.user);
})
.catch(error => {
console.error('Login failed:', error);
});
🔐 커스텀 인증 백엔드 구현
사용자 상태 확인 백엔드
# accounts/backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from .models import UserStatus
User = get_user_model()
class CustomAuthenticationBackend(ModelBackend):
"""커스텀 인증 백엔드"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""사용자 인증"""
if username is None:
username = kwargs.get(User.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = User.objects.get(**{User.USERNAME_FIELD: username})
except User.DoesNotExist:
# 타이밍 공격 방지를 위한 더미 비밀번호 검증
User().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
def user_can_authenticate(self, user):
"""사용자 로그인 가능 여부 확인"""
# 기본 is_active 확인
if not super().user_can_authenticate(user):
return False
# 커스텀 로그인 가능 조건 확인
return user.can_login()
def get_user(self, user_id):
"""사용자 ID로 사용자 조회"""
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
🚨 로그인 시 상태별 처리
커스텀 로그인 뷰
# accounts/views.py (추가)
from django.contrib.auth.views import LoginView
from django.contrib.auth import authenticate, login
from django.contrib import messages
from .models import UserStatus
class CustomLoginView(LoginView):
"""커스텀 로그인 뷰"""
template_name = 'accounts/login.html'
def form_valid(self, form):
"""폼 유효성 검사 통과 시"""
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(
self.request,
username=username,
password=password
)
if user is not None:
# 사용자 상태별 처리
if user.status == UserStatus.PENDING_EMAIL:
messages.error(
self.request,
'이메일 인증이 필요합니다. 이메일을 확인해주세요.'
)
return self.form_invalid(form)
elif user.status == UserStatus.PENDING_APPROVAL:
messages.error(
self.request,
'관리자 승인을 기다리고 있습니다. 잠시만 기다려주세요.'
)
return self.form_invalid(form)
elif user.status == UserStatus.SUSPENDED:
messages.error(
self.request,
f'계정이 정지되었습니다. 정지 사유: {user.suspension_reason}'
)
return self.form_invalid(form)
elif user.status == UserStatus.DEACTIVATED:
messages.error(
self.request,
'비활성화된 계정입니다. 고객센터에 문의해주세요.'
)
return self.form_invalid(form)
elif user.status == UserStatus.ACTIVE:
# 정상 로그인
login(self.request, user)
# 로그인 IP 저장
self.save_login_ip(user)
messages.success(
self.request,
f'환영합니다, {user.get_short_name()}님!'
)
return super().form_valid(form)
return self.form_invalid(form)
def save_login_ip(self, user):
"""로그인 IP 저장"""
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
user.last_login_ip = ip
user.save(update_fields=['last_login_ip'])
📊 모니터링 및 통계
사용자 상태 대시보드
# accounts/dashboard.py
from django.db.models import Count, Q
from django.utils import timezone
from datetime import timedelta
from .models import CustomUser, UserStatus
class UserStatusDashboard:
"""사용자 상태 대시보드"""
@staticmethod
def get_user_statistics():
"""사용자 통계 조회"""
total_users = CustomUser.objects.count()
status_counts = CustomUser.objects.aggregate(
pending_email=Count('id', filter=Q(status=UserStatus.PENDING_EMAIL)),
pending_approval=Count('id', filter=Q(status=UserStatus.PENDING_APPROVAL)),
active=Count('id', filter=Q(status=UserStatus.ACTIVE)),
suspended=Count('id', filter=Q(status=UserStatus.SUSPENDED)),
deactivated=Count('id', filter=Q(status=UserStatus.DEACTIVATED)),
)
# 최근 가입자 (7일)
week_ago = timezone.now() - timedelta(days=7)
recent_registrations = CustomUser.objects.filter(
date_joined__gte=week_ago
).count()
return {
'total_users': total_users,
'recent_registrations': recent_registrations,
**status_counts
}
@staticmethod
def get_pending_approvals():
"""승인 대기 사용자 목록"""
return CustomUser.objects.filter(
status=UserStatus.PENDING_APPROVAL,
email_verified=True
).order_by('date_joined')
@staticmethod
def get_recent_suspensions(days=30):
"""최근 정지된 사용자 목록"""
since = timezone.now() - timedelta(days=days)
return CustomUser.objects.filter(
status=UserStatus.SUSPENDED,
suspended_at__gte=since
).select_related('suspended_by').order_by('-suspended_at')
⚡ 성능 최적화 팁
1. 데이터베이스 인덱스 최적화
# accounts/models.py (인덱스 추가)
class CustomUser(AbstractBaseUser, PermissionsMixin):
# ... 기존 필드들 ...
class Meta:
verbose_name = '사용자'
verbose_name_plural = '사용자들'
db_table = 'auth_user'
indexes = [
models.Index(fields=['status']),
models.Index(fields=['email_verified']),
models.Index(fields=['is_approved']),
models.Index(fields=['status', 'email_verified', 'is_approved']),
models.Index(fields=['date_joined']),
models.Index(fields=['suspended_at']),
]
2. 쿼리 최적화
# accounts/querysets.py
from django.db import models
from .models import UserStatus
class CustomUserQuerySet(models.QuerySet):
"""최적화된 쿼리셋"""
def active_users(self):
"""활성 사용자 조회"""
return self.filter(
status=UserStatus.ACTIVE,
is_active=True,
email_verified=True,
is_approved=True
)
def pending_approval(self):
"""승인 대기 사용자"""
return self.filter(
status=UserStatus.PENDING_APPROVAL,
email_verified=True
)
def with_approval_info(self):
"""승인 정보 포함"""
return self.select_related('approved_by', 'suspended_by')
def recent_registrations(self, days=7):
"""최근 가입자"""
from django.utils import timezone
from datetime import timedelta
since = timezone.now() - timedelta(days=days)
return self.filter(date_joined__gte=since)
# managers.py에서 사용
class CustomUserManager(BaseUserManager):
def get_queryset(self):
return CustomUserQuerySet(self.model, using=self._db)
def active_users(self):
return self.get_queryset().active_users()
def pending_approval(self):
return self.get_queryset().pending_approval()
🧪 테스트 코드
단위 테스트
# tests/test_user_model.py
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.utils import timezone
from accounts.models import CustomUser, UserStatus
class CustomUserModelTest(TestCase):
"""User 모델 테스트"""
def setUp(self):
self.admin_user = CustomUser.objects.create_superuser(
email='admin@example.com',
password='testpass123'
)
def test_create_user(self):
"""일반 사용자 생성 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123',
first_name='테스트',
last_name='사용자'
)
self.assertEqual(user.email, 'test@example.com')
self.assertEqual(user.status, UserStatus.PENDING_EMAIL)
self.assertFalse(user.email_verified)
self.assertFalse(user.is_approved)
self.assertTrue(user.is_active)
self.assertFalse(user.is_staff)
def test_email_verification(self):
"""이메일 인증 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 토큰 생성
token = user.generate_email_verification_token()
self.assertTrue(token)
self.assertTrue(user.email_verification_token)
# 이메일 인증
user.verify_email()
self.assertTrue(user.email_verified)
self.assertEqual(user.status, UserStatus.PENDING_APPROVAL)
self.assertEqual(user.email_verification_token, '')
def test_user_approval(self):
"""사용자 승인 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 이메일 인증 완료
user.verify_email()
# 관리자 승인
user.approve_user(self.admin_user)
self.assertTrue(user.is_approved)
self.assertEqual(user.approved_by, self.admin_user)
self.assertEqual(user.status, UserStatus.ACTIVE)
self.assertIsNotNone(user.approved_at)
def test_user_suspension(self):
"""사용자 정지 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 활성 상태로 만들기
user.verify_email()
user.approve_user(self.admin_user)
# 정지 처리
reason = '테스트 정지'
user.suspend_user(self.admin_user, reason)
self.assertEqual(user.status, UserStatus.SUSPENDED)
self.assertEqual(user.suspended_by, self.admin_user)
self.assertEqual(user.suspension_reason, reason)
self.assertIsNotNone(user.suspended_at)
def test_user_reactivation(self):
"""사용자 재활성화 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 정지 상태로 만들기
user.verify_email()
user.approve_user(self.admin_user)
user.suspend_user(self.admin_user, '테스트')
# 재활성화
user.reactivate_user()
if user.email_verified:
# 이메일 인증 완료된 사용자는 관리자 승인 대기 상태로 변경
self.assertEqual(user.status, UserStatus.PENDING_APPROVAL)
self.assertFalse(user.is_approved)
else:
# 이메일 인증 안된 사용자는 이메일 인증 대기 상태로 변경
self.assertEqual(user.status, UserStatus.PENDING_EMAIL)
self.assertFalse(user.is_approved)
self.assertTrue(user.email_verification_token) # 새 토큰 생성됨
self.assertIsNone(user.suspended_by)
self.assertIsNone(user.suspended_at)
self.assertEqual(user.suspension_reason, '')
def test_can_login(self):
"""로그인 가능 여부 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 초기 상태: 로그인 불가
self.assertFalse(user.can_login())
# 이메일 인증 후: 여전히 로그인 불가
user.verify_email()
self.assertFalse(user.can_login())
# 승인 후: 로그인 가능
user.approve_user(self.admin_user)
self.assertTrue(user.can_login())
# 정지 후: 로그인 불가
user.suspend_user(self.admin_user, '테스트')
self.assertFalse(user.can_login())
def test_user_reactivation_with_email_verified(self):
"""이메일 인증 완료된 사용자 재활성화 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 활성 상태로 만들고 정지
user.verify_email()
user.approve_user(self.admin_user)
user.suspend_user(self.admin_user, '테스트')
# 재활성화 (이메일 인증 완료된 상태)
user.reactivate_user()
self.assertEqual(user.status, UserStatus.PENDING_APPROVAL)
self.assertFalse(user.is_approved)
self.assertTrue(user.email_verified) # 이메일 인증은 유지
def test_user_reactivation_without_email_verified(self):
"""이메일 인증 안된 사용자 재활성화 테스트"""
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
# 이메일 인증 없이 강제로 활성화 후 정지
user.status = UserStatus.ACTIVE
user.is_approved = True
user.save()
user.suspend_user(self.admin_user, '테스트')
# 재활성화 (이메일 인증 안된 상태)
user.reactivate_user()
self.assertEqual(user.status, UserStatus.PENDING_EMAIL)
self.assertFalse(user.is_approved)
self.assertFalse(user.email_verified)
self.assertTrue(user.email_verification_token) # 새 토큰 생성됨
API 테스트
# tests/test_api.py
from django.test import TestCase
from django.test.client import Client
from django.urls import reverse
import json
from accounts.models import CustomUser, UserStatus
class UserAPITest(TestCase):
def setUp(self):
self.client = Client()
self.admin_user = CustomUser.objects.create_superuser(
email='admin@example.com',
password='testpass123'
)
def test_user_registration_api(self):
"""사용자 등록 API 테스트"""
data = {
"email": "test@example.com",
"password": "testpass123",
"first_name": "테스트",
"last_name": "사용자"
}
response = self.client.post(
'/api/auth/register',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 201)
self.assertTrue(CustomUser.objects.filter(email="test@example.com").exists())
def test_user_login_api(self):
"""사용자 로그인 API 테스트"""
# 활성 사용자 생성
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
user.verify_email()
user.approve_user(self.admin_user)
data = {
"email": "test@example.com",
"password": "testpass123"
}
response = self.client.post(
'/api/auth/login',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
response_data = response.json()
self.assertIn('access_token', response_data)
self.assertIn('user', response_data)
def test_user_approval_api(self):
"""사용자 승인 API 테스트"""
# 승인 대기 사용자 생성
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
user.verify_email()
# 관리자 로그인
self.client.login(email='admin@example.com', password='testpass123')
data = {
"user_id": user.id,
"approval_note": "승인 완료"
}
response = self.client.post(
'/api/admin/users/approve',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertEqual(user.status, UserStatus.ACTIVE)
self.assertTrue(user.is_approved)
def test_user_suspension_api(self):
"""사용자 정지 API 테스트"""
# 활성 사용자 생성
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
user.verify_email()
user.approve_user(self.admin_user)
# 관리자 로그인
self.client.login(email='admin@example.com', password='testpass123')
data = {
"user_id": user.id,
"reason": "테스트 정지"
}
response = self.client.post(
'/api/admin/users/suspend',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertEqual(user.status, UserStatus.SUSPENDED)
self.assertEqual(user.suspension_reason, "테스트 정지")
def test_user_reactivation_api(self):
"""사용자 재활성화 API 테스트"""
# 정지된 사용자 생성
user = CustomUser.objects.create_user(
email='test@example.com',
password='testpass123'
)
user.verify_email()
user.approve_user(self.admin_user)
user.suspend_user(self.admin_user, '테스트')
# 관리자 로그인
self.client.login(email='admin@example.com', password='testpass123')
data = {"user_id": user.id}
response = self.client.post(
'/api/admin/users/reactivate',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
# 이메일 인증된 사용자는 승인 대기 상태로
self.assertEqual(user.status, UserStatus.PENDING_APPROVAL)
self.assertFalse(user.is_approved)
🔧 설정 및 배포
requirements.txt
Django==4.2.7
django-ninja==1.0.1
pydantic[email]==2.5.0
마이그레이션 및 초기 설정
# 패키지 설치
pip install -r requirements.txt
# 마이그레이션 생성 및 적용
python manage.py makemigrations accounts
python manage.py migrate
# 슈퍼유저 생성
python manage.py createsuperuser
# 개발 서버 실행
python manage.py runserver
프로덕션 환경 설정
# settings/production.py
import os
from .base import *
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com']
# 보안 설정
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# 데이터베이스 (PostgreSQL 권장)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
# Redis 캐시 (선택사항)
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.getenv('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# 이메일 설정
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', '587'))
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL')
# 로깅
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/django/user_management.log',
},
},
'loggers': {
'accounts': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
📋 마이그레이션 및 초기 데이터
마이그레이션 생성 및 적용
# 마이그레이션 생성
python manage.py makemigrations accounts
# 마이그레이션 적용
python manage.py migrate
# 슈퍼유저 생성
python manage.py createsuperuser
초기 데이터 fixture
# accounts/fixtures/initial_data.json
[
{
"model": "accounts.customuser",
"pk": 1,
"fields": {
"email": "admin@example.com",
"first_name": "관리자",
"last_name": "시스템",
"is_staff": true,
"is_superuser": true,
"is_active": true,
"status": "active",
"email_verified": true,
"is_approved": true,
"date_joined": "2025-08-01T00:00:00Z"
}
}
]
🚀 결론
이 글에서 구현한 Django Ninja 기반 고급 User 모델 API는 다음과 같은 특징을 가집니다:
✅ 주요 기능
- 완전한 REST API: Django Ninja 기반의 현대적인 API
- 이메일 인증 시스템: 토큰 기반 이메일 인증
- 관리자 승인 워크플로우: 체계적인 사용자 승인 프로세스
- 지능적 재활성화: 이메일 인증 상태에 따른 차별적 처리
- 상태 기반 로그인 제어: 5단계 사용자 상태 관리
- 자동 API 문서: Swagger UI 지원
🎯 핵심 개선사항
- 재활성화 로직 개선:
- 이메일 인증 완료 사용자 → 관리자 승인 대기 상태
- 이메일 미인증 사용자 → 이메일 인증 대기 상태 + 새 토큰 발급
- API 우선 설계:
- RESTful API 엔드포인트
- Pydantic 스키마 기반 데이터 검증
- 자동 API 문서 생성
- 확장 가능한 인증:
- 토큰 기반 인증 (세션/JWT 선택 가능)
- 역할별 권한 관리
- 관리자 전용 엔드포인트
🔧 실무 적용 포인트
- 프론트엔드 친화적: React, Vue.js 등과 쉬운 연동
- 모바일 앱 지원: REST API로 모바일 앱 개발 가능
- 마이크로서비스 아키텍처: 독립적인 사용자 관리 서비스
- 테스트 용이성: API 테스트 자동화 가능
- 문서화 자동화: Swagger UI로 개발자 경험 향상
이 구현을 통해 현대적인 웹/모바일 애플리케이션에 적합한 사용자 관리 시스템을 구축할 수 있으며, 특히 정지된 사용자의 재활성화 시 이메일 인증 상태를 고려한 차별적 처리로 보안성과 사용자 경험을 모두 향상시킬 수 있습니다.
다음 글에서는 JWT 인증과 Redis를 활용한 고급 세션 관리 기법에 대해 다뤄보겠습니다!