Wecode 1차 프로젝트

GGUGIT - cookit clone project

Posted by Juri on August 13, 2021

menu-filter

구현 웹사이트 : 쿡킷
프론트엔드 : 김동우, 이지선, 황문실 (3명)
백엔드 : 김훈태, 장이주 (2명)
링크 : 유튜브, 깃허브

백엔드 구현 기능 목록

사용기술 : Python, Django, Mysql, AWS EC2, RDS, POSTMAN

  1. 회원가입 / 로그인 API 및 로그인 데코레이터
  2. 상품목록 필터링,정렬 API
  3. 상품상세정보 제공 API
  4. 장바구니 CRUD API
  5. 상품검색 API

2주차에 구매, 리뷰 기능을 구현하기로 한 기존 계획을 대폭 수정했다. 플랜미팅 후 1차 프로젝트가 협업을 경험해보기 위한 프로젝트인만큼 프론트엔드와 백엔드의 진도를 맞춰서 진행하기로 결정했고, 백엔드에서 독자적으로 더 많은 기능을 구현하는 것보다 프론트엔드와 맞춰볼 수 있는 기능을 더 심화시켜 진행하기로 합의했다.

모델링

cook_20210808_44_44

  • 다대다 관계인 product와 taste 주의 (중간테이블 이용)
  • image와 description은 확장성을 고려해서 테이블을 별도로 생성
  • 복수의 테이블을 참조하는 cart와 review 주의

모델링은 데이터간의 관계를 결정하는 중요한 단계이다. 이후의 코드작성을 포함한 모든 작업에 영향을 주는만큼 초기에 더 신중하게 시간을 들여 모델링을 해야한다. 또한, 확장성을 고려해 모델링을 하는 것도 중요하다. 모델을 수정하면 수정된 모델과 관련한 모든 코드를 수정해야하며 더 나아가 데이터베이스 자체를 통째로 갈아엎어야 할 수도 있다.(ㅠㅠ) 뭐든지 기초공사가 제일 중요하다.

1-1. 회원가입 API

▶️bcrypt를 이용해 사용자 패스워드 암호화 진행

1
2
data = json.loads(request.body)
hashed_password = bcrypt.hashpw(data['password'].encode('utf-8'),bcrypt.gensalt()).decode('utf-8')

request 메세지의 body에 담겨진 패스워드를 bcrypt를 이용해 암호화하여 DB에 저장한다.

▶️Error Case

1)이메일 양식

1
2
if not re.search(r'^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+[.]?\w{2,3}$', data['email']):
    return JsonResponse({'message':'NOT_EMAIL_FORMAT'}, status = 400)

2)패스워드 양식

1
2
if not re.search(r'^(?=(.*[A-Za-z]))(?=(.*[0-9]))(?=(.*[@#$%^!&+=.\-_*]))([a-zA-Z0-9@#$%^!&+=*.\-_]){8,}$', data['password']):
    return JsonResponse({'message':'NOT_PASSWORD_FORMAT'}, status = 400)

3)핸드폰번호 양식

1
2
if not re.search(r'^\d{3}-\d{3,4}-\d{4}$',data['phone_number']):
    return JsonResponse({'message':'INVALID_PHONE_NUMBER'}, status = 400)

4)이메일 중복확인

1
2
if User.objects.filter(email=data['email']).exists():
    return JsonResponse({'message':'INVALID_EMAIL'}, status = 400)

5)KeyError

▶️사용자가 입력한 정보를 DB에 저장

1
2
3
4
5
6
7
User.objects.create(
    name         = data['name'],
    email        = data['email'],
    password     = hashed_password,
    phone_number = data['phone_number'],
    birthday     = data['birthday'],
)   

패스워드는 bcrypt를 이용해 암호화한 hashed_password로 저장한다.

1-2. 로그인 API

▶️Error Case

1)이메일 가입여부 확인

1
2
if not User.objects.filter(email = data['email']).exists():
    return JsonResponse({'message':'INVALID_EMAIL'}, status = 401)

2)비밀번호 일치여부 확인

1
2
if not bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
    return JsonResponse({'message':'INVALID_USER'}, status = 401)

3)KeyError

▶️JWT를 이용해 access token 발행

1
access_token = jwt.encode({'id' : user.id}, SECRET_KEY, algorithm= 'HS256')

1-3. 로그인 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def LoginDecorator(func):
    def wrapper(self,request, *args, **kwargs):
        try:
            access_token = request.headers.get('Authorization', None)
            payload      = jwt.decode(access_token ,SECRET_KEY, algorithms= 'HS256')
            user         = User.objects.get(id = payload['id'])
            request.user = user

        except jwt.exceptions.DecodeError:
            return JsonResponse({'message':'INVALID_TOKEN'}, status = 400)
        
        except ObjectDoesNotExist:
            return JsonResponse({'message':'INVALID_USER'}, status=400)

        return func(self,request, *args, **kwargs)
        
    return wrapper

Products 앱 라우팅

1
2
3
4
urlpatterns = [
    path('', ProductView.as_view()),
    path('/<int:product_id>', ProductDetailView.as_view()),
]

2-1. 상품목록 필터링 API

▶️request.GET 으로 쿼리 파라미터 읽어오기

1
2
theme  = request.GET.get('theme', None) #type or taste
number = request.GET.get('number',1)

▶️theme의 종류에 맞는 상품 가져오기

1
2
3
4
5
6
7
8
9
if theme == 'taste':
    filter_taste = {taste.id : taste.name for taste in Taste.objects.all()}
    taste        = Taste.objects.get(name = filter_taste[int(number)])
    products     = Product.objects.filter(producttaste__taste = taste)

if theme == 'country':
    filter_type = {type.id : type.name for type in Type.objects.all()}
    country     = Type.objects.get(name = filter_type[int(number)])
    products    = Product.objects.filter(type = country)

2-2. 상품목록 정렬 API

▶️request.GET 으로 쿼리 파라미터 가져오기

1
order  = request.GET.get('order',1)

▶️딕셔너리를 이용해 ‘번호:정렬종류’ 로 연결하기

1
2
3
4
5
6
7
8
9
order_number = {
    1 : '-created_at',
    2 : '-sales',
    3 : '-price',
    4 : 'price',
    5 : '-stock',
}

products = products.order_by(order_number[int(order)])

3. 상품상세정보 제공 API

▶️상품번호를 이용해 특정 상품의 정보 가져오기

1
product = Product.objects.get(id=product_id)

Orders 앱 라우팅

1
2
3
4
urlpatterns = [
    path('', CartView.as_view()),
    path('/<int:cart_id>',CartView.as_view()),
]

4-1. 장바구니 Create API

▶️Error Case

1
2
3
#1) 존재하지 않는 상품
if not Product.objects.filter(id=data['product_id']).exists():
    return JsonResponse({'message' : 'NOT_FOUND'}, status=400)
1
2
3
#2) 상품 재고 부족
if product.stock < quantity:
    return JsonResponse({'message':'NO_STOCK'}, status=400)

▶️get_or_create를 사용해 Cart 객체 만들기

1
2
3
4
5
6
cart, created = Cart.objects.get_or_create(product=product, user=user, defaults = {'quantity' : quantity})
if not created:
    if product.stock < quantity + cart.quantity:
        return JsonResponse({'message':'no_stock'}, status=400)
    cart.quantity += data['quantity']
    cart.save()

해당 사용자의 장바구니에 특정 상품이 있는지 확인한 후 있으면 수량을 수정하고 없으면 장바구니 객체를 새로 생성한다.

4-2. 장바구니 Read API

▶️Error Case

1
2
3
# 장바구니에 담긴 상품이 없을 때
if not carts.exists():
    return JsonResponse({'message' : 'NO_CART'}, status=400)

▶️장바구니에 필요한 정보 반환

1
2
3
4
5
6
7
8
results=[{
"cart_id"    : cart.id,
"product_id" : cart.product.id,
"name"       : cart.product.name,
"price"      : round(cart.product.price),
"discount"   : round(int(cart.product.price) * 0.9),
"image_url"  : cart.product.image_set.get().image_url,
"quantity"   : cart.quantity} for cart in carts]

4-3. 장바구니 Update API

▶️상품재고 확인 후 장바구니 수량 변경

1
2
3
4
if stocks < cart.quantity + data['quantity']:
    return JsonResponse({'message':'OUT_OF_STOCK'},status = 400)
cart.quantity += data['quantity']
cart.save()

4-4. 장바구니 Delete API

▶️해당 장바구니 삭제

1
Cart.objects.filter(id = cart_id).delete()