
구현 웹사이트 : 쿡킷
프론트엔드 : 김동우, 이지선, 황문실 (3명)
백엔드 : 김훈태, 장이주 (2명)
링크 : 유튜브, 깃허브
백엔드 구현 기능 목록
사용기술 : Python, Django, Mysql, AWS EC2, RDS, POSTMAN
- 회원가입 / 로그인 API 및 로그인 데코레이터
- 상품목록 필터링,정렬 API
- 상품상세정보 제공 API
- 장바구니 CRUD API
- 상품검색 API
2주차에 구매, 리뷰 기능을 구현하기로 한 기존 계획을 대폭 수정했다. 플랜미팅 후 1차 프로젝트가 협업을 경험해보기 위한 프로젝트인만큼 프론트엔드와 백엔드의 진도를 맞춰서 진행하기로 결정
했고, 백엔드에서 독자적으로 더 많은 기능을 구현하는 것보다 프론트엔드와 맞춰볼 수 있는 기능을 더 심화시켜 진행
하기로 합의했다.
모델링

- 다대다 관계인 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()
|