크리넥스는 커피원두 판매 사이트인 테라로사를 기반으로 기존 사이트를 개선한 프로젝트이다.
테라로사 사이트에 없는 검색과 필터기능을 추가해 개선해 보려 했고 검색기능과 필터링 기능을 추가했다.
필터 기능은 백엔드에만 구현이 되어있는 상태다.
프로젝트 Git주소
https://github.com/wecode-bootcamp-korea/35-1st-kleenex-backend.git
2022.07.18 ~ 2022.07.28 (12일)
이부분은에서 막히는 부분은 없었던 것 같다. 다만 img의 id와 url을 딕셔너리 형태로 바꾸는 수정을 진행했고 이후 모든 코드도 아래와 같은 방식으로 사용했다.
class MainProductView(View):
def get(self, request):
premiums = Product.objects.all().order_by('-price')[:3]
fresh_products = Product.objects.all().order_by('-roasting_date')[:4]
result_premium = [{
'id' : premium.id,
'name' : premium.name,
'eng_name' : premium.eng_name,
'img' : [{
'img_id' : image.id,
'img_url' : image.url
} for image in premium.productimage_set.all()],
'roasting_date' : premium.roasting_date,
'taste' : [{
'taste_id' : flavor.taste.id,
'taste_name' : flavor.taste.name
} for flavor in premium.tastebyproduct_set.all()],
'price' : premium.price
} for premium in premiums]
result_fresh_product = [{
'id' : fresh_product.id,
'name' : fresh_product.name,
'eng_name' : fresh_product.eng_name,
'img' : [{
'img_id' : image.id,
'img_url' : image.url
} for image in fresh_product.productimage_set.all()],
'roasting_date' : fresh_product.roasting_date,
'taste' : [{
'taste_id' : flavor.taste.id,
'taste_name' : flavor.taste.name
} for flavor in fresh_product.tastebyproduct_set.all()],
'price' : fresh_product.price
} for fresh_product in fresh_products]
return JsonResponse(
{'premium' : result_premium,'fresh_product' : result_fresh_product}, status = 200)
상품 리스트에선 다중 선택 필터링을 구현하고 싶었다. 리뷰를 받기 전엔 Q객체를 사용하지 않고 필터링을 구현했었는데 리뷰를 받고 Q객체로 필터링을 하는 방식으로 수정했다. Q객체로 코드도 깔끔해지고 코드 가독성도 더 좋아진것 같다. 아래 주석처리한 부분이 Q객체를 사용하기 전 작성한 코드이다.
처음엔 Pagination 부분도 django에서 지원해주는 paginator 클래스로 작성했지만 코드리뷰 에서 django에만 있는 클래스를 사용하지 않고 offset&limit 을 이용해 API를 구현해 보라는 리뷰를 반영해 offset&limit을 이용해 구현했다.
class CoffeeProductView(View):
def get(self, request):
category = request.GET.get('category')
tastes = request.GET.getlist('taste')
sorting = request.GET.get('sorting')
offset = int(request.GET.get('offset', 0))
limit = int(request.GET.get('limit', 12))
'''
if category:
products = Product.objects.filter(subcategory_id=category).order_by('id')
if coffee_category_id:
products = Product.objects.filter(subcategory_id = coffee_category_id)
if tastes:
products = products.filter(taste__name__in=tastes).order_by('id').distinct()
'''
q = Q()
if category:
q &= Q(subcategory_id = category)
if tastes:
q &= Q(taste__name__in = tastes)
sort_dict = {
'Highprice' : '-price',
'Lowprice' : 'price',
'roast' : '-roasting_date',
None : 'id'
}
total = Product.objects.all().count()
products = Product.objects.filter(q).order_by(sort_dict.get(sorting)).distinct()[offset:offset+limit]
result_products = [{
'id' : product.id,
'name' : product.name,
'eng_name' : product.eng_name,
'img' : [{
'img_id' : image.id,
'img_url' : image.url
} for image in product.productimage_set.all()],
'taste' : [{
'taste_id' : flavor.taste.id,
'taste_name' : flavor.taste.name
} for flavor in product.tastebyproduct_set.all()],
'roasting_date' : product.roasting_date,
'price' : product.price
}for product in products]
return JsonResponse(
{
'total' : total,
'shop_product_list' : result_products
},
status = 200
)
상품 상세 페이지는 그라인드 타입과 중량을 선택해 가격을 반영하는 구조로 해당 품목에대한 그라인드 타입과 중량에 따른 가격 전달을 하는 API를 구현했다. 마찬가지로 딕셔너리 구조로 각 속성에 대한 id, name등을 전달하도록 구현했다.
class ProductDetailView(View):
def get(self, request, product_id):
try:
product = Product.objects.get(id=product_id)
product_detail = (
{
'id' : product.id,
'name' : product.name,
'price' : product.price,
'img' : [{
'img_id' : image.id,
'img_url' : image.url
} for image in product.productimage_set.all()],
'taste' : [{
'taste_id' : flavor.taste.id,
'taste_name' : flavor.taste.name
} for flavor in product.tastebyproduct_set.all()],
'graind' : [{
'grind_id' : grind.id,
'grind_type' : grind.type
} for graind in product.grainding_set.all()],
'size' : [{
'size_id' : size.id,
'size_name' : size.name,
'size_price' : size.price
} for size in product.size_set.all()],
}
)
return JsonResponse({'product_detail' : product_detail}, status = 200)
except Product.DoesNotExist:
return JsonResponse(
{'MESSAGE' : 'Product matching query does not exist.'}, status = 404)
한글 검색기능을 사용하기 위해 클라이언트에게 받은 byte 타입의 데이터를 unquote를 이용해 다시 한글로 되돌리는 로직을 구현했다.
class MainSearchView(View):
def get(self, request):
search = request.GET.get('keywords')
products = Product.objects.filter(name__icontains=unquote(search))
result = [{
'id' : product.id,
'name' : product.name,
'eng_name' : product.eng_name,
'img' : [{
'img_id' : image.id,
'img_url' : image.url
} for image in product.productimage_set.all()],
'taste' : [{
'taste_id' : flavor.taste.id,
'taste_name' : flavor.taste.name
} for flavor in product.tastebyproduct_set.all()],
'roasting_date' : product.roasting_date,
'price' : product.price
}for product in products]
if not products.exists():
return JsonResponse({'MESSAGE' : 'NO RESULT'}, status=404)
return JsonResponse({'result' : result}, status =200)
노션을 이용해 API명세서를 작성했다.
시작부터 어떻게 할지 걱정만 앞서던 프로젝트가 막상 끝나니 좀 아쉬운점이 많다.
처음 프로젝트 주제를 알게 되었을땐 어려운것이 없어 보여 세세하게 페이지를 보지 않았다.
모델링 할 때부터 자세히 보게 되었는데 생각보다 사이트에 다양한 요소가 많아 데이터 모델링에 시간을 많이 쓰게되었다. 모델링을 잘하면 프로젝트 진행이 수월할 것이라는 생각에 같이하는 백엔드 팀원과 모델링에 시간을 많이 투자 했다.
처음엔 실제 사이트의 구조를 최대한 반영해보자는 생각이 있어서 데이터 구조가 복잡해졌다. 하지만 멘토님과의 미팅때 중요한것은 API를 실제로 구현해보는 것이지 복잡한 데이터를 넣는 시간을 많이 사용하는게 아니라는 말을 듣고 다시 모델링을 수정하고 반영했다. 데이터보다는 기능에 집중해 구현하려고 노력했다.
기본적인 기능들은 다 구현했지만 처음부터 방향을 잘 잡았더라면 더 좋은 기능들을 추가할 수 있지 않았을까 하는 아쉬움이 남는다. 다음 프로젝트는 계획을 잘 세워 조금더 많은 기능을 구현 해 보고싶다.
협업으로 진행되는 프로젝트 였기 때문에 팀원과의 호흡이 중요했다.
어떤 정보를 보내는지, 어떻게 요청을 보내는지에 대한 소통을 활발히 했다.
아무래도 처음 진행하는 프로젝트이다 보니 서로의 진행 방식에대해 잘 몰랐다.
그래서 많이 물어보고 맞춰보면서 일을 진행 했던거 같다.
하지만 아침마다 스탠드업 미팅때 서로의 진행 사항을 자세히 물어보거나 알리지 않았다.
진행이 어떻게 되고있는지 어떤 부분에서 막혀있는지 등에 대한 얘기를 하지않아 하고 있나보다 잘 되고 있나보다 정도였던거 같다.
프로젝트 막바지에 팀원이 구현사항에 대해 잘못 인지하고 있다는걸 알게되어 프로젝트 처음에 기획했던 필터 기능을 API만 구현하게 되었다. 미팅때마다 필터기능에 대해 얘기하긴 했지만 자세한 진행상황 등이나 세세한 부분에대해 놓쳤기 때문에 발생한 일이라 생각한다.
스탠드업 미팅은 짧고 간결하게 해야한다는 생각에 너무 '짧게 간결하게' 에만 치중했던게 아닌가 하는 생각이 든다. 짧고 대충이 아닌 진행 상황을 간결하게 요점위주로의 회의를 했어야 했다.