상품 상세 페이지에서 사용자가 구매 수량을 선택하고 '장바구니 담기' 버튼을 클릭하면, 전체 페이지를 새로 고치지 않고 화면 오른쪽 상단의 장바구니 아이콘에 표시되는 품목 수량만 실시간으로 갱신되도록 구현하는 것이 목표입니다. 이를 위해 전통적인 폼 제출 대신 비동기식 Ajax 요청을 사용합니다. 이 방식은 사용자 경험을 향상시키며, 불필요한 전체 렌더링 없이 부분 UI만 업데이트할 수 있습니다.
Ajax 기반 장바구니 추가 로직 개요
- 프론트엔드에서 jQuery를 이용해 사용자 입력 데이터(상품 ID, 수량)와 함께 POST 요청을 전송
- 서버 측 뷰에서 데이터 검증 후 Redis 기반 장바구니에 저장
- 처리 결과를 JSON 형태로 반환
- 클라이언트에서 응답을 받아 성공 시 시각적 피드백(예: 점프 애니메이션) 및 카운트 갱신
프론트엔드 JavaScript 로직 구현
아래는 jQuery 기반의 동작 스크립트입니다. 수량 조정, 유효성 검사, 그리고 Ajax 요청 처리를 포함합니다.
{% block endfiles %}
<div class="add_jump"></div>
<script type="text/javascript" src="{% static 'js/jquery-1.12.4.min.js' %}"></script>
<script type="text/javascript">
// 가격 계산 함수
function updateTotalPrice() {
const unitPrice = parseFloat($('.show_price em').text());
const quantity = parseInt($('.num_show').val());
const totalPrice = (unitPrice * quantity).toFixed(2);
$('.total em').text(totalPrice + '원');
}
// 수량 증가/감소 핸들러
$('.add').click(() => adjustQuantity(1));
$('.minus').click(() => adjustQuantity(-1));
function adjustQuantity(delta) {
let currentQty = parseInt($('.num_show').val());
currentQty += delta;
if (currentQty < 1) currentQty = 1;
$('.num_show').val(currentQty);
updateTotalPrice();
}
// 수동 입력 시 유효성 검사
$('.num_show').blur(function() {
let inputVal = $(this).val().trim();
if (!inputVal || isNaN(inputVal) || parseInt(inputVal) < 1) {
$(this).val(1);
}
updateTotalPrice();
});
// 애니메이션 좌표 계산
const fromTop = $('#add_cart').offset().top;
const fromLeft = $('#add_cart').offset().left;
const toTop = $('#show_count').offset().top;
const toLeft = $('#show_count').offset().left;
// 장바구니 추가 이벤트 바인딩
$('#add_cart').click(function() {
const productId = $(this).data('goods-id');
const quantity = $('.num_show').val();
const csrfToken = $('input[name="csrfmiddlewaretoken"]').val();
$.post(
'/cart/add/',
{
goods_id: productId,
count: quantity,
csrfmiddlewaretoken: csrfToken
},
function(response) {
if (response.status === 'S') {
// 애니메이션 효과 트리거
$('.add_jump')
.css({ left: fromLeft + 80, top: fromTop + 10, display: 'block' })
.stop()
.animate(
{ left: toLeft + 7, top: toTop + 7 },
'fast',
function() {
$(this).fadeOut('fast', () => {
$('#show_count').text(response.cart_count);
});
}
);
} else {
alert('오류: ' + response.errmsg);
}
}
);
});
</script>
{% endblock endfiles %}
URL 라우팅 설정
cart 앱 내 urls.py에 Ajax 엔드포인트를 등록합니다.
from django.urls import path
from .views import CartAddView, CartListView
urlpatterns = [
path('add/', CartAddView.as_view(), name='cart_add'),
path('', CartListView.as_view(), name='cart_list'),
]
뷰 클래스 작성
사용자 인증 상태 확인, 입력값 검증, Redis 저장소와의 상호작용을 처리하는 뷰 클래스입니다. LoginRequiredMixin 미사용 이유는 Ajax 요청 시 리디렉션 응답이 클라이언트에서 예상대로 작동하지 않기 때문입니다.
from django.views import View
from django.http import JsonResponse
from django.contrib.auth.models import User
from goods.models import Goods
from django_redis import get_redis_connection
class CartAddView(View):
def post(self, request):
# 인증 여부 확인
if not request.user.is_authenticated:
return JsonResponse({
'status': 'E',
'errmsg': '로그인이 필요합니다.'
})
user = request.user
data = request.POST
product_id = data.get('goods_id')
quantity_str = data.get('count')
# 필수 파라미터 검증
if not all([product_id, quantity_str]):
return JsonResponse({
'status': 'E',
'errmsg': '필수 정보 누락.'
})
# 상품 존재 여부 확인
try:
product_id = int(product_id)
product = Goods.objects.get(id=product_id)
except (ValueError, Goods.DoesNotExist):
return JsonResponse({
'status': 'E',
'errmsg': '유효하지 않은 상품입니다.'
})
# 수량 형식 검증
try:
quantity = int(quantity_str)
if quantity < 1:
raise ValueError
except ValueError:
return JsonResponse({
'status': 'E',
'errmsg': '수량은 1 이상이어야 합니다.'
})
# 재고 초과 여부 확인
if quantity > product.onhand:
return JsonResponse({
'status': 'E',
'errmsg': '재고 수량을 초과했습니다.'
})
# Redis에 장바구니 정보 저장
redis_conn = get_redis_connection('default')
cart_key = f'cart_{user.id}'
# 기존 수량 가져와서 합산
existing_qty = redis_conn.hget(cart_key, product_id)
new_quantity = quantity + (int(existing_qty) if existing_qty else 0)
redis_conn.hset(cart_key, product_id, new_quantity)
# 현재 장바구니 전체 품목 수 계산
total_items = redis_conn.hlen(cart_key)
return JsonResponse({
'status': 'S',
'cart_count': total_items
})