본문 바로가기

PYTHON-BACK

#파이썬 32일차_온라인 상점 구축 프로젝트2

728x90

쇼핑 카트 및 세션 사용하기

1. 세션(Session)

  • 정의: 웹 브라우저를 통한 사용자의 요청을 하나의 상태로 보고 유지시키는 기술.
  • 특징: 서버에 정보를 저장하며, 브라우저를 닫거나 서버에서 삭제될 때만 세션이 종료됨.
  • 세션 vs. 쿠키: 세션은 서버 자원을 사용, 쿠키는 클라이언트 자원을 사용.

2. 장고 세션 프레임워크

  • 기능: 익명 및 사용자 세션을 지원하며, 각 방문자의 데이터를 저장 가능.
  • 사용 방법:
    • request.session을 사용해 세션에 접근.
    • 세션 데이터는 딕셔너리처럼 취급 가능.

3. 세션 설정

  • SESSION_ENGINE: 세션 데이터의 저장 위치를 설정.
  • 세션 엔진 옵션:
    • 데이터베이스 세션 (기본)
    • 파일 기반 세션, 캐시 기반 세션 등.
  • 세션 설정 옵션:
    • SESSION_COOKIE_AGE: 세션 쿠키의 유효기간.
    • SESSION_EXPIRE_AT_BROWSER_CLOSE: 브라우저 종료 시 세션 만료 여부.
    • SESSION_SAVE_EVERY_REQUEST: 요청마다 세션을 저장할지 여부.

4. 세션 만료

  • 브라우저 종료 시 세션 만료: SESSION_EXPIRE_AT_BROWSER_CLOSE 설정으로 제어.
  • 세션 지속 시간: SESSION_COOKIE_AGE로 제어, request.session.set_expiry()로 덮어쓰기 가능.

5. 세션에 쇼핑 카트 저장

  • 카트 구성: 제품 ID, 수량, 단가 등의 정보를 포함.
  • 쇼핑 카트 생성:
    • 세션 키를 통해 카트를 생성 및 관리.
    • 동일한 세션 키로 카트 아이템들을 조회 가능.
  • myshop/settings.py
CART_SESSION_ID = 'cart'
  • Terminal
    • cart 앱 생성
python manage.py startapp cart
  • myshop/settings.py
INSTALLED_APPS = [
    ...
    'shop.apps.ShopConfig',
    'cart.apps.CartConfig',
]
  • cart/cart.py
    • 카트 초기화를 위한 __ init__() 정의
    • 카트에 아이템을 추가하기 위한 add(), save() 정의
      • add()의 매개변수
        • product: 카트에 추가하거나 업데이트할 제품의 인스턴스
        • quantity: 제품의 수량. 기본 값은 1
        • override_quantity: 지정된 수량으로 수량을 재정의해야 하는지(True) 또는 기존 수량에 새로운 수량을 추가해야 하는지(False)를 나타내는 Boolean 값
    • 카트의 아이템을 제거하기 위한 remove() 정의
from decimal import Decimal
from django.conf import settings
from shop.models import Product

class Cart:
    def __init__(self, request):
        self.session = request.session
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            cart = self.session[settings.CART_SESSION_ID] = {}
        self.cart = cart

    def add(self, product, quantity=1, override_quantity=False):
        product_id = str(product.id)
        if product_id not in self.cart:
            self.cart[product_id] = {'quantity': 0, 'price': str(product.price)}
        if override_quantity:
            self.cart[product_id]['quantity'] = quantity
        else:
            self.cart[product_id]['quantity'] += quantity
        self.save()

    def save(self):
        self.session.modified = True

    def remove(self, product):
        product_id = str(product.id)
        if product_id in self.cart:
            del self.cart[product_id]
            self.save()
  • cart/cart.py
    • 카트에 포함된 아이템들을 반복해서 관련 Product 인스턴스에 접근해야 함. 이를 위해서 __ iter__()정의
    • 카트에 포함된 아이템들의 총 수를 반환하기 위하여 __ len__() 정의
    def __iter__(self):
        product_ids = self.cart.keys()
        # get the product objects and add them to the cart
        products = Product.objects.filter(id__in=product_ids)
        cart = self.cart.copy()
        for product in products:
            cart[str(product.id)]['product'] = product
        for item in cart.values():
            item['price'] = Decimal(item['price'])
            item['total_price'] = item['price'] * item['quantity']
            yield item

    def __len__(self):
        return sum(item['quantity'] for item in self.cart.values())
  • cart/cart.py
    • 모든 카트 아이템들의 수량의 합을 반환하기 위하여 get_total_price() 정의
    • 카트 세션을 지우기 위한 clear() 정의
    def get_total_price(self):
        return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())

    def clear(self):
        # remove cart from session
        del self.session[settings.CART_SESSION_ID]
        self.save()

2.2.6 카트 관련 뷰 만들기

  • cart/forms.py
    • 카트에 아이템 추가하기
from django import forms

PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]

class CartAddProductForm(forms.Form):
    quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int)
    override = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
  • cart/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from shop.models import Product
from .cart import Cart
from .forms import CartAddProductForm


@require_POST
def cart_add(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product_id)
    form = CartAddProductForm(request.POST)
    if form.is_valid():
        cd = form.cleaned_data
        cart.add(product=product,
                 quantity=cd['quantity'],
                 override_quantity=cd['override'])
    return redirect('cart:cart_detail')


@require_POST
def cart_remove(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product_id)
    cart.remove(product)
    return redirect('cart:cart_detail')


def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(initial={
                            'quantity': item['quantity'],
                            'override': True})
    return render(request, 'cart/detail.html', {'cart': cart})
  • cart/urls.py
from django.urls import path
from . import views

app_name = 'cart'

urlpatterns = [
    path('', views.cart_detail, name='cart_detail'),
    path('add/<int:product_id>/', views.cart_add, name='cart_add'),
    path('remove/<int:product_id>/', views.cart_remove, name='cart_remove'),
]
  • cart/templates/cart/detail.html
    • 카트를 표시하기 위한 템플릿 작성
{% extends "shop/base.html" %}
{% load static %}

{% block title %}
  Your shopping cart
{% endblock %}

{% block content %}
<h1>Your shopping cart</h1>
<table class="cart">
    <thead>
        <tr>
            <th>Image</th>
            <th>Product</th>
            <th>Quantity</th>
            <th>Remove</th>
            <th>Unit price</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
        {% for item in cart %}
            {% with product=item.product %}
                <tr>
                    <td>
                        <a href="{{ product.get_absolute_url }}">
                            <img src="{% if product.image %}{{ product.image.url }}
                                        {% else %}{% static "img/no_image.png" %}{% endif %}">
                        </a>
                    </td>
                    <td>{{ product.name }}</td>
                    <td>{{ item.quantity}}</td>
                    <td>
                        <form action="{% url "cart:cart_remove" product.id %}" method="post">
                            <input type="submit" value="Remove">
                            {% csrf_token %}
                        </form>
                    </td>
                    <td class="num">${{ item.price }}</td>
                    <td class="num">${{ item.total_price }}</td>
                </tr>
            {% endwith %}
        {% endfor %}
        <tr class="total">
            <td>Total</td>
            <td colspan="4"></td>
            <td class="num">${{ cart.get_total_price }}</td>
        </tr>
    </tbody>
</table>
<p class="text-right">
    <a href="{% url "shop:product_list" %}" class="button light">Continue shopping</a>
    <a href="{% url "orders:order_create" %}" class="button">Checkout</a>
</p>
{% endblock %}
  • shop/views.py
    • 카트에 제품 추가하기
from cart.forms import CartAddProductForm

...
def product_detail(request, id, slug):
    product = get_object_or_404(Product, id=id, slug=slug, available=True)
    cart_product_form = CartAddProductForm()
    return render(request, 'shop/product/detail.html',
                  {'product': product, 'cart_product_form': cart_product_form})
  • shop/templates/shop/product/detail.html
...
<p class="price">${{ product.price }}</p>
<form action="{% url "cart:cart_add" product.id %}" method="post">
    {{ cart_product_form }}
    {% csrf_token %}
    <input type="submit" value="Add to cart">
</form>
{{ product.description|linebreaks }}
...
  • myshop/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('cart/', include('cart.urls', namespace="cart")),
    path('', include('shop.urls', namespace='shop')),
]
  • cart/views.py
    • 카트에 제품 수량 업데이트하기
...
def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(initial={
                            'quantity': item['quantity'],
                            'override': True})
    return render(request, 'cart/detail.html', {'cart': cart})
  • cart/templates/cart/detail.html
# <td>{{ item.quantity}}</td>를 찾아서 수정
...
<td>
    <form action="{% url "cart:cart_add" product.id %}" method="post">
        {{ item.update_quantity_form.quantity }}
        {{ item.update_quantity_form.override }}
        <input type="submit" value="Update">
        {% csrf_token %}
    </form>
</td>
...
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from shop.models import Product
from .cart import Cart
from .forms import CartAddProductForm


@require_POST
def cart_add(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product_id)
    form = CartAddProductForm(request.POST)
    if form.is_valid():
        cd = form.cleaned_data
        cart.add(product=product, quantity=cd['quantity'], override_quantity=cd['override'])
    return redirect('cart:cart_detail')


@require_POST
def cart_remove(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product_id)
    cart.remove(product)
    return redirect('cart:cart_detail')


def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(initial={
                            'quantity': item['quantity'], 'override': True})
    return render(request, 'cart/detail.html', {'cart': cart})

2.2.7 현재 카트에 대한 콘텍스트 프로세스 생성하기

  • 콘텍스트 프로세서
    • 요청 객체를 인수로 받아서 요청 콘텍스트에 추가된 딕셔너리를 반환하는 파이썬 함수
    • 모든 템플릿에서 전역적으로 사용할 수 있는 함수를 만들어야 할 때 유용함
    • startproject를 사용하여 새로운 프로젝트를 생성할 때 TEMPLATES 설정 내의 context_processors 옵션에 다음과 같은 템플릿 콘텍스트 프로세스가 포함됨
      • django.template.context_processors.debug
        • 요청에서 실행된 SQL 쿼리 목록을 출력하는 콘텍스트의 debug와 sql_queries 변수를 설정함
      • django.template.context_processors.request
        • 콘텍스트의 request 변수를 설정함
      • django.template.context_processors.auth
        • 요청의 user 변수를 설정함
      • django.template.context_processors.messages
        • 콘텍스트에서 메시지 프레임워크를 사용해서 생성된 모든 메시지를 담는 message 변수를 설정함
  • 또한 장고는 django.template.context_processors.csrf를 활성화하여 사이트 간 요청 위조(CSRF, Cross Site Request Forgery) 공격을 방지함. 이 프로세서는 설정에는 포함되어 있지 않지만 항상 활성화되어 있으며, 보안상의 이유로 인해 해제할 수 없음

요청 콘텍스트에 카트 설정하기

  • 현재 카트를 요청 콘텍스트에 설정하는 콘텍스트 프로세서 만들기
  • 이 프로세서를 사용하면 모든 템플릿에서 카트에 엑세스할 수 있음
  • 순서
    1. cart 애플리케이션 디렉토리 내에 새로운 파일(context_processors.py)을 생성
      • 콘텍스트 프로세서는 어디에 저장해도 무방함(코드 작성, 구성에 용이한 장소에 저장할 것을 권장)
    2. context_processors.py 파일에 다음 코드를 추가
from.cart import Cart

def cart(request):
    return { 'cart' : Cart(request)}
    • 콘텍스트 프로세서에서 요청 객체를 사용하여 Cart를 인스턴스화하고 템플릿에서 cart라는 이름의 변수를 사용할 수 있도록 함

3. 프로젝트의 settings.py 파일을 편집하여 TEMPLATES 설정 내의 context_processors 옵션에 cart.context_processors.cart 추가

 TEMPLATES = [
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
         'DIRS': [],
         'APP_DIRS': True,
         'OPTIONS': {
             'context_processors': [
                 'django.template.context_processors.debug',
                 'django.template.context_processors.request',
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
                 'cart.context_processors.cart',
             ],
         },
     },
 ]

cart 콘텍스트 프로세서는 템플릿이 렌더링될 때마다 장고의 RequestContext를 사용해서 실행됨

cart 변수는 템플릿의 콘텍스트에 설정됨

  • 4. shop 애플리케이션의 shop/base.html 템플릿 편집

다음의 코드를 찾음

  <div class="cart">
      Your cart is empty.
  </div>

 

찾은 코드를 다음의 코드로 수정함

  <div class="cart">
      {% with total_items=cart|length %}
      {% if total_items > 0 %}
          Your cart:
          <a href="{% url "cart:cart_detail" %}">
          {{ total_items }} item{{ total_items|pluralize }},
          ${{ cart.get_total_price }}
          </a>
      {% elif not order %}
          Your cart is empty.
      {% endif %}
      {% endwith %}
  </div>

 

5. 서버 재시작

python manege.py runserver

 

728x90