장고 - 쇼핑몰 장바구니 기능 학습

  • 이번 포스트에서는 온라인 쇼핑몰 사이트를 예로 들어, 장바구니 기능 구현 방법에 대해 알아볼 것이다.

1. 장바구니 기능 작성

  • 경로 : cart(앱) > cart.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    from django.conf import settings
    from shop.models import Product

    class Cart(object):
    def __init__(self, request):
    self.session = request.session
    cart = self.session.get(settings.CART_ID)
    if not cart:
    # 세션에 없던 키 값을 생성하면 자동 저장
    cart = self.session[settings.CART_ID] = {}
    # 세션에 이미 있는 키 값에 대한 값을 수정하면 수동으로 저장
    self.cart = cart

    def __len__(self):
    # 요소가 몇개인지 갯수를 반환해주는 함수
    """
    id : 실제제품
    """
    return sum(item['quantity'] for item in self.cart.values())

    def __iter__(self):
    # for문 같은 문법을 사용할 때 안에 있는 요소를 어떤 형태로 반환할 것인지 결정하는 함수
    product_ids = self.cart.keys()
    products = Product.objects.filter(id__in=product_ids)

    for product in products:
    self.cart[str(product.id)]['product'] = product

    for item in self.cart.values():
    item['total_price'] = item['price'] * item['quantity']
    yield item

    def add(self, product, quantity=1, is_update=False):
    product_id = str(product.id)
    if product_id not in self.cart:
    # 만약 제품 정보가 Decimal 이라면 세션에 저장할 때는 str로 형변환 해서 저장하고
    # 꺼내올 때는 Decimal로 형변환해서 사용해야 한다.
    self.cart[product_id] = {'quantity':0, 'price':product.price}
    if is_update:
    self.cart[product_id]['quantity'] = quantity
    else:
    self.cart[product_id]['quantity'] += quantity
    self.save()

    def remove(self, product):
    product_id = str(product.id)
    if product_id in self.cart:
    del(self.cart[product_id])
    self.save()

    def save(self):
    self.session[settings.CART_ID] = self.cart
    self.session.modified = True

    def clear(self):
    self.cart = {}
    self.save()

    # 전체 제품 가격
    def get_total_price(self):
    return sum(item['quantity']*item['price'] for item in self.cart.values())

2. 여러 페이지에 cart를 context data로 전달하기 위해 context processors 작성

  • 경로 : cart > context_processors.py
    1
    2
    3
    4
    5
    from .cart import Cart

    def cart(request):
    cart = Cart(request)
    return {'cart':cart}

3. 장고 프로젝트 설정에 context_processors 추가

  • 경로 : config(프로젝트) > settings.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    TEMPLATES = [
    {
    ...
    'OPTIONS': {
    'context_processors': [
    ...
    'cart.context_processors.cart',
    ],
    },
    },
    ]

4. 상품을 장바구니에 추가하기 위해 고객이 입력해야 할 폼 설정

  • 경로 : cart > forms.py
    1
    2
    3
    4
    5
    6
    7
    from django import forms

    class AddToCartForm(forms.Form):
    # 상품 수량 설정 field
    quantity = forms.IntegerField(initial=1)
    # 수정 여부 확인 field
    is_update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)

5. 상품 상세 페이지에 특정 상품을 장바구니에 저장해주는 버튼 생성

  • 경로 : shop > views.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ProductDetail(DetailView):
    model = Product
    template_name = 'shop/product_detail.html'

    # context data로 form 전달
    def get_context_data(self, **kwargs):
    form = AddToCartForm()
    kwargs.update({'form':form})
    return super().get_context_data(**kwargs)
  • 경로 : shop > templates > shop > product_detail.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    {% extends 'base.html' %}

    {% block content %}
    <!-- 금액을 10^3 단위로 comma 표시하기 위해 humanize 로드 (settings.py에서 INSTALLED_APPS = [ 'django.contrib.humanize',] 추가) -->
    {% load humanize %}
    <div class="row no-gutters bg-light position-relative mt-3">
    <div class="col-md-6 position-static p-4 pl-md-0">
    <h5 class="mt-0">{{object.name}}</h5>
    <p>
    <form action="{% url 'add_product' object.id %}" method="post">
    <table class="table table-striped">
    <tr>
    <td>Price</td>
    <!-- 해당 금액 10^3 단위로 comma 표시 -->
    <td>{{object.price|intcomma}}</td>
    </tr>
    <tr>
    <td>Stock</td>
    <td>{{object.stock|intcomma}}</td>
    </tr>
    <tr>
    <td>Order Available</td>
    <td>{{object.available_order}}</td>
    </tr>
    <tr>
    <td>
    {{form.quantity.label}}
    </td>
    <td>
    {{form.quantity}}
    </td>
    </tr>
    </table>
    {% csrf_token %}
    <!-- 장바구니 이동 버튼 -->
    <input type="submit" class="btn btn-sm btn-outline-success float-right" value="Add to Cart">
    </form>
    </p>
    </div>
    </div>
    {% endblock %}

6. 장바구니 관련 페이지 경로 설정

  • 경로 : cart > urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from django.urls import path
    from .views import *

    urlpatterns = [
    # 장바구니에 상품 추가하는 페이지로 이동하는 경로
    path('add/<int:product_id>/', add_product, name='add_product'),
    # 장바구니에서 상품 삭제하는 view로 이동하는 경로
    path('remove/<int:product_id>/', remove_product, name='remove_product'),
    # 장바구니 상세정보가 조회되는 페이지로 이동하는 경로
    path('detail/', cart_detail, name='cart_detail'),
    ]

7. 장바구니 페이지 구현

  • 경로 : cart > views.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    from django.shortcuts import render
    from django.views.decorators.http import require_POST
    from django.shortcuts import redirect

    from shop.models import Product
    from .cart import Cart
    from .forms import AddToCartForm

    @require_POST
    def add_product(request, product_id):
    product = Product.objects.filter(pk=product_id)
    if product.exists():
    cart = Cart(request)
    form = AddToCartForm(request.POST)
    if form.is_valid():
    cd = form.cleaned_data
    # filter로 객체 가져와서, 그 객체를 사용할 때는 객체 뒤에 '[0]'을 붙여야 한다.
    cart.add(product=product[0], quantity=cd['quantity'], is_update=cd['is_update'])
    print(cart.cart.values())
    return redirect('cart_detail')

    def remove_product(request, product_id):
    product = Product.objects.filter(pk=product_id)
    if product.exists():
    cart = Cart(request)
    cart.remove(product[0])
    return redirect('cart_detail')

    def cart_detail(request):
    # 장바구니에 담겨 있는 제품 목록 띄우기, 제품 수량 수정, 지우기, 장바구니 비우기 버튼 구현
    cart = Cart(request)
    for item in cart:

    item['quantity_form'] = AddToCartForm(initial={'quantity':item['quantity'], 'is_update':True})

    continue_url = '/'
    # 현재 페이지 주소 얻기
    # 1) request.build_absolute_uri('?') : 쿼리스트링 없이 현재 페이지 주소 얻기
    # 2) request.build_absolute_uri() : 쿼리스트링까지 얻어오기
    current_url = request.build_absolute_uri('?')
    if 'HTTP_REFERER' in request.META and current_url != request.META['HTTP_REFERER']:
    # 이전 페이지로 이동하는 continue_url 설정
    continue_url = request.META['HTTP_REFERER']

    return render(request,'cart/cart_detail.html', {'cart':cart, 'continue_url':continue_url})
  • 경로 : cart > templates > cart > cart_detail.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    {% extends 'base.html' %}

    {% block content %}
    {% load humanize %}
    <table class="table table-striped mt-3">
    <thead>
    <tr>
    <th>#</th>
    <th>Image</th>
    <th>Name</th>
    <th>Quantity</th>
    <th>Unit Price</th>
    <th>Price</th>
    <th>Remove</th>
    </tr>
    </thead>
    <tbody>
    {% for item in cart %}
    {% with product=item.product %}
    <tr>
    <td>{{forloop.counter}}</td>
    <td><a href="{{product.get_absolute_url}}" target="_blank"><img src="{{product.image.url}}" width="100%" class="img-thumbnail"></a></td>
    <td><a href="{{product.get_absolute_url}}" target="_blank">{{product.name}}</a></td>
    <td>
    <form action="{% url 'add_product' product.id %}" method="post">
    {% csrf_token %}
    {{item.quantity_form.quantity}}
    {{item.quantity_form.is_update}}
    <input type="submit" class="btn btn-success btn-sm" value="Update">
    </form>
    </td>
    <td>{{item.price|intcomma}}</td>
    <td>{{item.total_price|intcomma}}</td>
    <td><a href="{% url 'remove_product' product.id %}" class="btn btn-sm btn-outline-warning">remove</a></td>
    </tr>
    {% endwith %}
    {% endfor %}
    <tr>
    <td>
    Total
    </td>
    <td colspan="5"></td>
    <td class="num">{{cart.get_total_price|intcomma}}</td>
    </tr>

    <tr>
    <td colspan="7">
    <!-- 이전 페이지로 이동 -->
    <a href="{{continue_url}}" class="float-left btn btn-lg btn-primary">Continue Shopping</a>
    <!-- 결제창으로 이동(결제 기능 포스트 참고) -->
    <a href="{% url 'order_create' %}" class="float-right btn btn-lg btn-info">Checkout</a>
    </td>

    </tr>
    </tbody>
    </table>
    {% endblock %}