Django 기반 영화 매장의 인증, 리뷰 및 주문 프로세스 구축

사용자 인증 시스템 구성

Django 웹 애플리케이션에서 사용자 관리 기능은 보안과 신뢰성을 확보하는 데 필수적입니다. 외부 라이브러리에 의존하지 않고 Django 내장 인증 시스템을 활용하여 등록, 로그인, 로그아웃 기능을 구현해 보겠습니다. 이를 위해 별도의 accounts 앱을 생성하고 기존 프로젝트 설정에 통합합니다.

앱 생성 및 설정 적용

프로젝트 루트 디렉토리에서 터미널을 실행하여 새 앱 초기화 명령어를 입력합니다.

python manage.py startapp membership

생성된 앱은 settings.py 파일의 INSTALLED_APPS 목록에 추가되어야 활성화됩니다. 또한 프로젝트 최상위 urls.py 에 해당 경로를 포함시켜 라우팅을 가능하게 합니다.

INSTALLED_APPS = [
    # 기존 앱들...
    'membership',
]

# urls.py 수정 예시
path('auth/', include('membership.urls')),

회원가입 뷰 구현

기본 제공되는 폼 클래스를 상속받아 커스텀 폼을 작성하면 UI 일관성과 유효성 검사를 쉽게 관리할 수 있습니다. forms.py 에 다음 내용을 정의합니다.

from django.contrib.auth.forms import UserCreationForm

class RegistrationForm(UserCreationForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field_name in ['username', 'password1', 'password2']:
            self.fields[field_name].help_text = None
            self.fields[field_name].widget.attrs.update({'class': 'form-control'})

이후 views.py 에서 폼 제출을 처리하는 함수를 작성하며, 성공 시 홈 페이지로, 실패 시 에러가 포함된 폼을 다시 렌더링하도록 설계합니다.

from django.shortcuts import render, redirect
from .forms import RegistrationForm

def user_register(request):
    context = {'page_title': '회원가입'}
    if request.method == 'POST':
        form_instance = RegistrationForm(request.POST)
        if form_instance.is_valid():
            form_instance.save()
            return redirect('home.main')
    else:
        form_instance = RegistrationForm()
    context['registration_form'] = form_instance
    return render(request, 'membership/register.html', context)

로그인 및 세션 관리

사용자 식별은 HTTP 의 무상태적 특성을 보완하기 위해 세션을 사용합니다. 로그인은 직접적인 HTML 폼을 만들어 처리함으로써 템플릿 제어력을 높일 수 있습니다.

from django.contrib.auth import authenticate, login
# views.py 내부

def sign_in(request):
    error_msg = ''
    if request.method == 'POST':
        user_obj = authenticate(
            username=request.POST.get('username'),
            password=request.POST.get('pass_word')
        )
        if user_obj:
            login(request, user_obj)
            return redirect('home.main')
        else:
            error_msg = '정보 불일치'
    return render(request, 'membership/login.html', {'error_message': error_msg})
로그인 인터페이스 예시

콘텐츠 상호작용: 리뷰 시스템

인증이 완료되면 사용자가 영화에 대해 의견을 남길 수 있어야 합니다. 이는 기본적인 CRUD(생성, 조회, 업데이트, 삭제) 패턴을 따르며, 데이터 무결성을 위해 외키 관계를 정의합니다.

리뷰 모델 설계

movies 앱 내에 새로운 모델을 추가합니다. 각 리뷰는 특정 영화와 작성자와의 연결 고리를 가져야 하므로 ForeignKey 를 활용합니다.

from django.db import models
from django.contrib.auth.models import User

class FilmCritique(models.Model):
    critique_text = models.CharField(max_length=500)
    created_at = models.DateTimeField(auto_now_add=True)
    target_movie = models.ForeignKey('Movie', on_delete=models.CASCADE)
    written_by = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.target_movie.name} - {self.written_by.username}"

모델 변경 사항을 반영하기 위해 데이터베이스 마이그레이션을 수행해야 합니다.

python manage.py makemigrations
python manage.py migrate

리뷰 생성 로직

영화 상세 페이지에서 인증된 사용자만 리뷰 제출 폼을 보이도록 템플릿 조건문을 적용하고, 뷰 함수에서는 권한 확인을 엄격히 합니다.

@login_required
def submit_critique(request, movie_id):
    film = get_object_or_404(Movie, pk=movie_id)
    if request.method == 'POST':
        new_critique = FilmCritique.objects.create(
            critique_text=request.POST.get('critique'),
            target_movie=film,
            written_by=request.user
        )
        return redirect('films.details', pk=movie_id)
    return redirect('films.details', pk=movie_id)
영화 상세 및 리뷰 폼

기존 리뷰 수정 및 삭제

작성자 본인에게만 수정 및 삭제 권한을 부여하기 위해 뷰 내부에서 소유권 체크를 수행합니다. get_object_or_404 를 사용하여 잘못된 ID 요청 시 안전함을 보장합니다.

@login_required
def edit_critique(request, movie_id, critique_id):
    item = get_object_or_404(FilmCritique, id=critique_id, written_by=request.user)
    if request.method == 'POST':
        item.critique_text = request.POST.get('text')
        item.save()
    return redirect('films.details', pk=movie_id)

쇼핑 카트 및 세션 활용

장바구니 기능은 사용자의 상태를 지속 유지해야 하는 대표적인 사례입니다. 이때 Django 의 세션 메커니즘이 유용하게 쓰이며, 쿠키를 통해 클라이언트와 서버 간 데이터를 동기화합니다.

카트 앱 구조

쇼핑 관련 로직은 전용 앱으로 분리하여 관리성을 높입니다. 세션 딕셔너리를 사용하여 담겨있는 상품 정보를 임시 저장합니다.

# cart/views.py
from django.shortcuts import redirect
from movies.models import Film

def add_to_bag(request, film_id):
    film_obj = get_object_or_404(Film, id=film_id)
    session_bag = request.session.get('basket_items', {})
    
    quantity = int(request.POST.get('qty', 1))
    session_bag[str(film_id)] = quantity
    
    request.session['basket_items'] = session_bag
    return redirect('shop.basket_view')

총액 계산 유틸리티

복잡한 계산을 뷰에서 분리하여 재사용성을 높이려면 유틸리티 파일을 사용하는 것이 좋습니다. 각 아이템의 가격을 양과 곱하여 합산합니다.

# cart/utils.py
def compute_bag_amount(items_dict, films_list):
    grand_total = 0
    for film in films_list:
        count = items_dict.get(str(film.id), 0)
        grand_total += film.price * int(count)
    return grand_total

세션을 통한 상태 추적

웹 브라우저의 개발자 도구를 통해 sessionid 쿠키가 어떻게 교환되는지 확인할 수 있습니다. 이 코드는 백엔드에서 사용자 액션 기록을 추적하고, 결제 전까지 데이터를 보유하는 역할을 합니다.

Django 세션 작동 원리

주문 처리 및 영수증 모델

결제 버튼 클릭 시 메모리에 있는 카트 데이터를 영구적인 주문 레코드로 변환해야 합니다. 이를 위해 거래 내역 (Transaction) 과 항목 (TransactionItem) 모델을 정의합니다.

데이터 모델 정제

주문 발생 시점의 정보 (구매자, 총금액, 날짜) 와 개별 구매 항목 (상품, 단가, 수량) 을 구분하여 저장합니다.

from django.db import models

class SalesRecord(models.Model):
    customer = models.ForeignKey(User, on_delete=models.CASCADE)
    total_price = models.DecimalField(max_digits=10, decimal_places=2)
    purchase_date = models.DateTimeField(auto_now_add=True)

class SaleDetail(models.Model):
    record = models.ForeignKey(SalesRecord, on_delete=models.CASCADE, related_name='details')
    product = models.ForeignKey(Film, on_delete=models.SET_NULL, null=True)
    amount = models.IntegerField(default=1)
    unit_cost = models.DecimalField(max_digits=10, decimal_places=2)

결제 프로세스 실행

주문 뷰는 로그인 여부와 빈 카트를 먼저 검증한 뒤, 실제로는 데이터베이스 트랜잭션을 일으키는 작업을 수행합니다. 구매가 확정되면 세션 카트는 즉시 비워집니다.

@login_required
def complete_purchase(request):
    basket = request.session.get('basket_items', {})
    if not basket:
        return redirect('shop.basket_view')
        
    films = list(Film.objects.filter(id__in=list(basket.keys())))
    total_amt = compute_bag_amount(basket, films)
    
    order = SalesRecord.objects.create(customer=request.user, total_price=total_amt)
    
    for film in films:
        SaleDetail.objects.create(
            record=order,
            product=film,
            amount=basket.get(str(film.id)),
            unit_cost=film.price
        )
    
    request.session['basket_items'] = {}
    return render(request, 'cart/confirmation.html', {'record_id': order.id})
주문 인보이스 구조

주문 내역 조회

사용자는 본인의 과거 구매 내역을 확인할 수 있어야 합니다. ForeignKey 관계의 역방향 접근 (user.salesrecord_set) 을 이용하여 특정 고객에게 해당하는 모든 주문을 필터링합니다.

@login_required
def my_records(request):
    history = request.user.salesrecord_set.all().order_by('-purchase_date')
    return render(request, 'membership/orders.html', {'transactions': history})

태그: Django python web-development authentication session-management

6월 21일 00:38에 게시됨