장바구니 API는 민혁님이 작성해주셨고 오더 API는 내가 만들게 되었다.
코드부터 보고 가자.
class CartView(View):
@authorization
def post(self, request):
try:
data = json.loads(request.body)
user = request.user
product_id = data['product_id']
quantity = data['quantity']
if not Product.objects.filter(id=product_id).exists():
return JsonResponse({"message" : "PRODUCT_NOT_EXIST"}, status=400)
cart, created = Cart.objects.get_or_create(
user = user,
product_id = product_id
)
cart.quantity += quantity
cart.save()
if cart.quantity <= 0 :
return JsonResponse({"message" : "QUANTITY_ERROR"}, status=400)
return JsonResponse({"message" : "SUCCESS"}, status=201)
except Cart.DoesNotExist:
return JsonResponse({"message" : "INVALID_CART"}, status=400)
except KeyError:
return JsonResponse({"message" : "KEY_ERROR"}, status=400)
@authorization
def get(self, request):
user = request.user
carts = Cart.objects.select_related('product').filter(user=user)
if not Cart.objects.filter(user=user).exists():
return JsonResponse({"message" : "CART_NOT_EXIST"}, status=400)
result = [{
'cart_id' : cart.id,
'product_id' : cart.product.id,
'korean_name' : cart.product.korean_name,
'price' : int(cart.product.price),
'thumbnail_image_url' : cart.product.thumbnail_image_url,
'quantity' : cart.quantity
} for cart in carts ]
return JsonResponse({"cart_info" : result}, status=200)
@authorization
def patch(self, request):
try:
data = json.loads(request.body)
user = request.body
cart_id = data['cart_id']
quantity = data['quantity']
if not Cart.objects.filter(id=cart_id, user=user).exists():
return JsonResponse({"message" : "CART_NOT_EXIST"}, status=400)
cart = Cart.objects.get(id=cart_id, user=user)
cart.quantity = quantity
cart.save()
if cart.quantity <= 0:
return JsonResponse({"message" : "QUANTITY_ERROR"}, status=400)
return JsonResponse({"message" : "SUCCESS"}, status=201)
except KeyError:
return JsonResponse({"message" : "KEY_ERROR"}, status=400)
@authorization
def delete(self, request):
data = json.loads(request.body)
user = request.user
cart_id = data['cart']
cart = Cart.objects.get(id = cart_id, user = user)
if not Cart.objects.filter(id = cart_id, user = user).exists():
return JsonResponse({"message" : "NOT_EXIST"}, status=400)
cart.delete()
return JsonResponse({"message" : "DELETE_SUCCESS"}, status=204)
class OrderStatusEnum(Enum):
PAID = 1
PENDING = 2
PREPARING = 3
SHIPPEND = 4
DELIVERD = 5
ORDER_CANCELLED = 6
class OrderView(View):
@authorization
def post(self, request):
data = json.loads(request.body)
user = request.user
items = data
try:
with transaction.atomic():
order = Order.objects.create(
address = User.objects.get(id=user.id).address,
user = user,
order_status = OrderStatus.objects.get(id=OrderStatusEnum.PENDING.value)
)
order_items = [OrderItem(
order = order,
product_id = item['product_id'],
quantity = item['quantity']
) for item in items]
OrderItem.objects.bulk_create(order_items)
carts = Cart.objects.filter(user = user)
carts.delete()
return JsonResponse({'message':order.id}, status=201)
except transaction.TransactionManagementError:
return JsonResponse({'message':'TransactionManagementError'}, status=401)
@authorization
def get(self, request):
user = request.user
order = request.GET.get('id',)
order_items = Order.objects.get(id=order).orderitem_set.all()
total_price = int(Order.objects.filter(id=order)\
.annotate(total=Sum(F('orderitem__product__price')*F('orderitem__quantity')))[0].total)
order_list = [{
'order_id' : order,
'user' : User.objects.get(id=user.id).name,
'address' : Order.objects.get(id=order).address,
'order_items' : [{
'product_id' : order_item.product.id,
'quantity' : order_item.quantity,
'product_name' : order_item.product.korean_name,
'product_image' : order_item.product.thumbnail_image_url,
'price' : int((order_item.quantity) * (order_item.product.price))
} for order_item in order_items],
'total_price' : total_price
}]
return JsonResponse({'order_list':order_list}, status=200)
@authorization
def patch(self, request):
data = json.loads(request.body)
order_id = data['order_id']
try:
Order.objects.filter(id=order_id).update(order_status=OrderStatusEnum.ORDER_CANCELLED.value)
return JsonResponse({'message':'SUCCESS'}, status=200)
except KeyError:
return JsonResponse({'message':'KEY_ERROR'}, status=400)
초기에 작성된 장바구니 API를 프론트와 맞춰보던 중, 장바구니의 수량이 감소되지 않고 계속 에러가 뜨는 이슈가 발생했다.
백엔드의 로직은 프론트에서 수량을 보내줄 때마다 그 수량을 더해서 DB에 업데이트하는 로직이었고, 프론트에서는 수량이 감소할 시에 음수를 백엔드로 보내주는데, 프론트로부터 입력받는 quantity가 음수일 경우 에러가 발생하도록 설정해놓았던 것이 원인이었다.
이후 카트 데이터의 quantity가 음수일 경우 에러가 발생하도록 수정하였다.
참고 : https://brownbears.tistory.com/181
데이터베이스 트랜잭션은 데이터베이스 내에서 한꺼번에 수행되어야 할 일련의 연산들이다.
전부 성공하거나 전부 실패되거나 둘 중 하나의 작업을 수행한다.
트랜잭션의 모든 연산은 반드시 한꺼번에 완료가 되어야 하며 그렇지 않은 경우에는 한꺼번에 취소되어야하는 원자성을 가지고 있다.
한꺼번에 완료가 된 경우에는 Commit을 호출해 작업결과를 데이터베이스에 반영한다.
취소되거나 문제가 발생한 경우에는 Rollback을 호출하고 작업결과를 모두 취소하여 데이터베이스에 영향을 미치지 않게 된다.
은행 결제를 생각하면 이해가 쉽다.
1차 프로젝트에서는 주문 건이 생성되는 동시에 해당 주문 건의 아이템들이 장바구니에서 삭제가 되어야 하기 때문에 트랜잭션을 사용했다. 트랜잭션 안에서의 연산 도중에 문제가 생기면 롤백을 통해 이전에 수행되었던 주문 건 생성도 취소가 된다.
django에서는
1) 트랜잭션 데코레이터를 사용하는 방법
2) with 명령어를 이용한 트랜잭션
3) savepoint를 직접 지정해주는 트랜잭션
이 있는데 나는 이번에 with 명령어를 이용한 트랜잭션을 사용했다.
이 과정에서, 트랜잭션 안에서의 연산이 실패해서 롤백이 되어도 success메세지가 리턴되는 것을 발견하고 트랜잭션 에러를 exception으로 추가해주었다.
작성한 1차 OrderView에서, OrderStatus 부분은 Enum Class를 활용하는 것이 좋을 것 같다는 멘토님의 피드백이 있었다.
회원가입, 글 작성, 제품 등록 등과 같이 데이터 입력을 많이 해야하는 필드와 달리 OrderStatus같은 필드는 데이터의 변동이 크지 않다. 따라서 이 데이터들은 미리 만들어둔 객체형(사전형)처럼 사용하는 것이 효율적이다.
(Enum Class 설명은 동기 고민혁님의 블로그를 참고하였다.)
이후 이와 유사한 django의 IntegerChoices와 TextChoices도 공부를 해 볼 예정.
처음에는 orders
테이블과 order_items
모두 bulk_create()
로 진행했다. 그런데 오류가 생겼고, 구글링해보니 Foreign Key를 가지고 있는 데이터는 bulk_create()
로 업로드할 수 없다고 한다. 자세한 원인은 나중에 다시 찾아보는 걸로...
장고 공식문서에서의 F() 객체에 대한 정의:
F() 객체는 모델의 필드 혹은 어노테이트된 열의 값을 나타낸다. 실제로 데이터베이스에서 Python 메모리로 가져오지 않고, 모델 필드 값을 참조하고 이를 데이터베이스에서 사용하여 작업할 수 있다.
주문내역에서 total price 부분의 데이터를 프론트에 전달할 때, 처음에는 무지성으로 빈 리스트를 만들어서 그 리스트 안에 quantity와 각 제품의 price를 곱한 값들을 다 넣고 리스트의 모든 값을 더하는 형식으로 파이썬에서 처리를 했다.
이후 쿼리 최적화를 위해 Django ORM으로 DB에서 연산된 값을 가져오는 형태로 변경하였고, 이 과정에서 필요한 F 객체를 사용하였다.
오더가 생성되면 Success메세지로 그 order의 id값을 백엔드에서 프론트엔드로 전달해준다. 이후, 결제 진행 페이지에서는 프론트엔드에서 해당 order의 id값을 백엔드로 전달해주고, 백엔드에서는 그 id값에 해당하는 order를 불러와서 해당 데이터를 프론트엔드로 전달해준다.
프로덕트 리스트 페이지에서, 제품들의 데이터를 프론트엔드에 전달해주는 과정에서 문제가 생겨 제품들이 노출되지 않는 이슈가 생겼었다.
원인은 해당 제품의 총 주문량 데이터를 뽑아오는 과정에 있었다.
리스트 인덱스 에러였는데, 주문량이 0인 제품의 경우 빈 쿼리셋을 불러오기 때문에, 빈 쿼리셋에는 인덱스가 0인 객체가 없기 때문에 발생하는 에러였다.
그래서 삼항연산자를 사용하여 코드를 수정해주었다.
'order_quantity': product.orderitem_set.all()[0].quantity,
'order_quantity' : product.orderitem_set.all()[0].quantity if product.orderitem_set.all().exists() else 0,