날씨별 잘 팔린 메뉴
- 기획 의도
- 사용자의 주소의 현재 날씨에 맞는 메뉴 보여주기
- 날씨에 따라 먹고 싶은 음식이 바뀐다.
- 맨날 똑같은 것만 배달시키지 말고 지금 날씨에 딱 맞는 메뉴를 먹게 한다.
- 화면
- 코드
사용자 주소에서 oo동을 가지고 온다.
grid 테이블에서 해당 동에 맞는 x, y좌표를 가지고 날씨 API 호출을 한다.
응답 값에서 날씨 정보를 받아온다.
날씨와 관계없이 모든 날씨(4개)에 잘 팔린 메뉴를 찾는다.
주문 내역에서 사용자 동, 동의 현재 날씨와 일치하는 레코드들을 가져온다.
날씨와 관계없이 잘 팔린 메뉴는 배제한다.
메뉴명으로 그룹화하고 수량을 센다.
이번, 지난, 지지난 달에 팔린 수량에 계산해서 가중치를 구한다.
가중치 기준으로 정렬한다.
class CategoryNum(enum.IntEnum):
WEATHER_ID = 14
class WeatherNum(enum.IntEnum):
SUNNY = 1
PARTLY_CLOUDY = 2
CLOUDY = 3
BLUR = 4
class Month(enum.IntEnum):
one_month = 1
two_month = 2
three_month = 3
class Weight(enum.auto):
one_month_ago_weight = 0.8
two_month_ago_weight = 0.6
class MenuListAPIView(View):
def get(self, request, *args, **kwargs):
category_id = kwargs['category_id']
weather_list = [WeatherNum.SUNNY, WeatherNum.PARTLY_CLOUDY,
WeatherNum.CLOUDY, WeatherNum.BLUR, ]
count_duplicated_weather_order = {}
menu_quantity_to_exclude_or_not = {}
excluded_menu_count = {}
excluded_menu_quantity = {}
this_month = datetime.now().month
if category_id != CategoryNum.WEATHER_ID:
return get_restaurants(**kwargs)
elif category_id == CategoryNum.WEATHER_ID:
category_id = kwargs['category_id']
if request.user.is_anonymous:
return JsonResponse(
{
"message": "로그인되어 있지 않습니다.",
},
status=HTTPStatus.UNAUTHORIZED,
)
if not request.user.address:
return JsonResponse(
{
"message": "사용자 주소가 존재하지 않습니다.",
},
status=HTTPStatus.BAD_REQUEST,
)
user_dong = get_dong(request.user.address)
if user_dong is None:
return JsonResponse(
{'message': 'mypage에서 주소의 동을 입력하세요.'},
status=HTTPStatus.BAD_REQUEST
)
x_y_grid = get_x_y_grid(user_dong)
try:
x = x_y_grid.x
y = x_y_grid.y
except:
return x_y_grid
user_dong_weather = get_user_dong_weather(x, y)
if type(user_dong_weather) != int:
return user_dong_weather
for weather in weather_list:
menu_list = (Cart.objects
.prefetch_related('order')
.filter(order__address__contains=user_dong, order__weather=weather.value, order__created_time__month__gt=this_month - Month.three_month)
.exclude(cartitem__menu__isnull=True)
.values('cartitem__menu__name')
.annotate(menu_quantity=Sum('cartitem__quantity'), menu=F('cartitem__menu__name'))
.order_by('-menu_quantity')[:5]
.values('menu', quantity=F('menu_quantity'),
price=F('cartitem__menu__price'), img=F('cartitem__menu__img'),
id=F('cartitem__menu__id'), detail=F('cartitem__menu__detail'),
restaurant=F(
'cartitem__menu__restaurant__id'),
)
)
for menu in menu_list:
if count_duplicated_weather_order.get(menu['menu']):
count_duplicated_weather_order[menu['menu']] += 1
menu_quantity_to_exclude_or_not[menu['menu']] += menu['quantity']
else:
count_duplicated_weather_order[menu['menu']] = 1
menu_quantity_to_exclude_or_not[menu['menu']] = menu['quantity']
try:
for menu_name, menu_cnt in count_duplicated_weather_order.items():
if menu_cnt >= 4:
excluded_menu_count[menu_name] = count_duplicated_weather_order[menu_name]
excluded_menu_quantity[menu_name] = menu_quantity_to_exclude_or_not[menu_name]
except KeyError:
return HttpResponse(
status=HTTPStatus.BAD_REQUEST,
content={
'message': '사전 key(메뉴명) 에러',
}
)
menu_list = (Cart.objects
.prefetch_related('order')
.filter(order__address__contains=user_dong, order__weather=user_dong_weather, order__created_time__month__gt=this_month - Month.three_month)
.values('cartitem__menu__name')
.annotate(menu=F('cartitem__menu__name'), quantity=Sum('cartitem__quantity'))
.exclude(menu__in=excluded_menu_count)
.order_by('-quantity')
.values('menu', 'quantity',
price=F('cartitem__menu__price'), img=F('cartitem__menu__img'),
id=F('cartitem__menu__id'), detail=F('cartitem__menu__detail'),
restaurant=F(
'cartitem__menu__restaurant__id'),
)
)[:10]
if not menu_list:
return JsonResponse(
{'message': '날씨에 따라 잘 팔린 메뉴가 없습니다.'},
status=HTTPStatus.BAD_REQUEST
)
weighted_quantity = {}
for menu in menu_list:
for month in range(this_month, this_month - Month.three_month, -1):
queryset = (Order.objects
.filter(cart__cartitem__menu__name=menu['menu'], created_time__month=month,
weather=user_dong_weather)
.exclude(cart__isnull=True)
.select_related('cart', 'cart__cartitem__menu')
.prefetch_related('cart__cartitem')
.annotate(name=F('cart__cartitem__menu__name'),
quantity=F('cart__cartitem__quantity'))
.values('name', 'quantity', 'created_time__month')
)
for q in queryset:
if month == this_month:
if q['name'] not in weighted_quantity:
weighted_quantity[q['name']] = q['quantity']
else:
weighted_quantity[q['name']] += q['quantity']
elif month == this_month - Month.one_month:
if q['name'] not in weighted_quantity:
weighted_quantity[q['name']] = round(
q['quantity'] * Weight.one_month_ago_weight, 2)
else:
weighted_quantity[q['name']] += round(
q['quantity'] * Weight.one_month_ago_weight, 2)
elif month == this_month - Month.two_month:
if q['name'] not in weighted_quantity:
weighted_quantity[q['name']] = round(
q['quantity'] * Weight.two_month_ago_weight, 2)
else:
weighted_quantity[q['name']] += round(
q['quantity'] * Weight.two_month_ago_weight, 2)
if menu['menu'] in weighted_quantity.keys():
menu['weighted_quantity'] = weighted_quantity[menu['menu']]
menu_list = sorted(list(menu_list), key=operator.itemgetter('weighted_quantity'), reverse=True)
menu_list = list(menu_list, )
data = {
"menu_list": menu_list,
"category_id": category_id,
"sky_info": user_dong_weather,
"exclude_menu": excluded_menu_count,
"exclude_menu_quantity": excluded_menu_quantity,
"message": "날씨별 메뉴 Top5 성공"
}
return JsonResponse(
data,
)
- 테스트 코드
from http import HTTPStatus
from django.test import TestCase, RequestFactory
from django.urls import reverse
from django.contrib.auth import get_user_model
from cart.models import Cart, CartItem
from category.models import Category
from order.models import Order
from restaurant.models import Restaurant
from menu.models import Menu
from grid.models import Grid
User = get_user_model()
class MenuTestClass(TestCase):
def setUp(self):
self.request_factory = RequestFactory()
self.user1 = User.objects.create_user(username='mike', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user2 = User.objects.create_user(username='jake', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user3 = User.objects.create_user(username='fake', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user4 = User.objects.create_user(username='lake', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user5 = User.objects.create_user(username='cake', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user6 = User.objects.create_user(username='cake2', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user7 = User.objects.create_user(username='cake3', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user8 = User.objects.create_user(username='cake4', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user9 = User.objects.create_user(username='cake5', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user10 = User.objects.create_user(username='cake6', email='', password='2',
address='서울시 서초구 서초2동 사랑의 교회 1300호')
self.user_without_addr = User.objects.create_user(username='james', email='', password='1')
self.user_without_dong = User.objects.create_user(username='ajax', email='', password='1',
address='서울시 서초구 100호')
self.user_without_x_y_dong = User.objects.create_user(username='차이니즈', email='', password='1',
address='서울시 서초구 베이징3동')
self.user_with_wrong_x_y = User.objects.create_user(username='xy오류', email='', password='1',
address='서울시 서초구 서초9동')
self.user_with_wrong_weather = User.objects.create_user(username='abc', email='', password='1',
address='서울시 서초구 서초2동')
self.restaurant = Restaurant.objects.create(
name='굽내치킨', title='굽내치킨-서초점',
min_order_price=10000, delivery_charge=1000, estimated_delivery_time='20:00',
operation_start_hour='11:00', operation_end_hour='20:00', )
self.menu = Menu.objects.create(
restaurant=self.restaurant,
name='볼케이노',
detail='매콤한 맛입니다.',
price=20000,
type='양념류',
img='test.jpg',
)
self.menu2 = Menu.objects.create(
restaurant=self.restaurant,
name='갈비천왕',
detail='갈비 맛입니다.',
price=20000,
type='양념류',
img='test.jpg',
)
self.menu3 = Menu.objects.create(
restaurant=self.restaurant,
name='허니멜로',
detail='꿀 맛입니다.',
price=20000,
type='양념류',
img='test.jpg',
)
self.cart1 = Cart.objects.create(user=self.user1)
self.cart2 = Cart.objects.create(user=self.user2)
self.cart3 = Cart.objects.create(user=self.user3)
self.cart4 = Cart.objects.create(user=self.user4)
self.cart5 = Cart.objects.create(user=self.user5)
self.cart6 = Cart.objects.create(user=self.user6)
self.cart7 = Cart.objects.create(user=self.user7)
self.cart8 = Cart.objects.create(user=self.user8)
self.cart9 = Cart.objects.create(user=self.user9)
self.cart10 = Cart.objects.create(user=self.user10)
self.cart_without_user_addr = Cart.objects.create(user=self.user_without_addr)
self.cart_item1 = CartItem.objects.create(menu=self.menu, cart=self.cart1, quantity=1)
self.cart_item2 = CartItem.objects.create(menu=self.menu, cart=self.cart2, quantity=2)
self.cart_item3 = CartItem.objects.create(menu=self.menu, cart=self.cart3, quantity=3)
self.cart_item4 = CartItem.objects.create(menu=self.menu2, cart=self.cart4, quantity=4)
self.cart_item5 = CartItem.objects.create(menu=self.menu2, cart=self.cart5, quantity=5)
self.cart_item6 = CartItem.objects.create(menu=self.menu2, cart=self.cart6, quantity=6)
self.cart_item7 = CartItem.objects.create(menu=self.menu3, cart=self.cart7, quantity=7)
self.cart_item8 = CartItem.objects.create(menu=self.menu3, cart=self.cart8, quantity=8)
self.cart_item9 = CartItem.objects.create(menu=self.menu3, cart=self.cart9, quantity=9)
self.cart_item10 = CartItem.objects.create(menu=self.menu3, cart=self.cart10, quantity=10)
self.cart_item_without_user_addr = CartItem.objects.create(menu=self.menu, cart=self.cart_without_user_addr, quantity=1)
self.category = Category.objects.create(name='치킨', img='restaurant/chicken.jpg')
self.category_weather = Category.objects.create(id=15, name='날씨별 추천')
self.grid1 = Grid.objects.create(
name='서초2동',
x=61,
y=125,
)
self.grid2 = Grid.objects.create(
name='서초9동',
x=32767,
y=32767,
)
def test_menu_list_api_for_weather_should_return_datas_on_valid_request(self):
'''
사용자 로그인 성공, 주소 존재, 동 정보 존재, 동에 대한 x, y 좌표 존재, 기상청 API 응답 성공
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.client.login(username='mike', password='2')
self.order_weather_1 = Order.objects.create(
user=self.user1,
cart=self.cart1,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=1,
)
self.order_weather_2 = Order.objects.create(
user=self.user2,
cart=self.cart2,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=2,
)
self.order_weather_3 = Order.objects.create(
user=self.user3,
cart=self.cart3,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=3,
)
self.order_weather_4 = Order.objects.create(
user=self.user4,
cart=self.cart4,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=4,
)
# When
response = self.client.get(reverse('menu_api:menu_list_api', kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.OK)
def test_menu_list_api_for_weather_should_return_UNAUTHORIZED_on_userless_request(self):
'''
AnonymousUser인 경우 401 반환
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
# When
response = self.client.get(reverse("menu_api:menu_list_api", kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
def test_menu_list_api_for_weather_should_return_BAD_REQUEST_on_addressless_request(self):
'''
사용자 계정은 있지만 주소가 없는 경우 400 반환
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.client.login(username='james', password='1')
# When
response = self.client.get(reverse('menu_api:menu_list_api', kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
def test_menu_list_api_for_weather_should_return_BAD_REQUEST_on_dongless_request(self):
'''
사용자 주소의 동이 없는 경우 400 반환
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.client.login(username='ajax', password='1')
# When
response = self.client.get(reverse('menu_api:menu_list_api', kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
def test_menu_list_api_for_weather_should_return_BAD_REQUEST_on_xygridless_request(self):
'''
Grid 테이블에서 사용자의 동에 대한 x, y 좌표가 없는 경우 400 반환
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.client.login(username='차이니즈', password='1')
# When
response = self.client.get(reverse('menu_api:menu_list_api', kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
def test_menu_list_api_for_weather_should_return_INTERNAL_SERVER_ERROR_when_meteorological_administration_return_err(
self):
'''
기상청 API 응답에 문제가 있는 경우 500 반환, 기상청에 직접적으로 문제가 있는지 판단하기 어려우므로 x,y 좌표를 큰 값으로 줘서 에러 응답을 만듦.
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.order_weather_1 = Order.objects.create(
user=self.user1,
cart=self.cart1,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=1,
)
self.order_weather_2 = Order.objects.create(
user=self.user1,
cart=self.cart2,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=2,
)
self.order_weather_3 = Order.objects.create(
user=self.user1,
cart=self.cart3,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=3,
)
self.order_weather_4 = Order.objects.create(
user=self.user1,
cart=self.cart4,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=0,
weather=4,
)
self.client.login(username='xy오류', password='1')
# When
response = self.client.get(reverse('menu_api:menu_list_api', kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
def test_return_OK_when_no_order_related_with_weather(self):
'''
날씨와 관계 있는 잘 팔린 메뉴가 없는 경우 200 반환
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.order1 = Order.objects.create(
user=self.user2,
cart=self.cart2,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=1,
)
self.order2 = Order.objects.create(
user=self.user2,
cart=self.cart2,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=2,
)
self.client.login(username='mike', password='2')
# When
response = self.client.get(reverse("menu_api:menu_list_api", kwargs=kwargs))
# Then
message = response.json()['message']
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(message, '날씨에 따라 잘 팔린 메뉴가 없습니다.')
def test_return_OK_when_request(self):
'''
정상 동작 시 OK
'''
# Given
kwargs = {
'category_id': self.category_weather.id,
}
self.client.login(username='mike', password='2')
self.order1 = Order.objects.create(
user=self.user1,
cart=self.cart1,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=1,
)
self.order2 = Order.objects.create(
user=self.user2,
cart=self.cart2,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=1,
)
self.order3 = Order.objects.create(
user=self.user3,
cart=self.cart3,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=2,
)
self.order4 = Order.objects.create(
user=self.user4,
cart=self.cart4,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=2,
)
self.order5 = Order.objects.create(
user=self.user5,
cart=self.cart5,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=3,
)
self.order6 = Order.objects.create(
user=self.user6,
cart=self.cart6,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=3,
)
self.order7 = Order.objects.create(
user=self.user7,
cart=self.cart7,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=3,
)
self.order8 = Order.objects.create(
user=self.user8,
cart=self.cart8,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=3,
)
self.order9 = Order.objects.create(
user=self.user9,
cart=self.cart9,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=4,
)
self.order10 = Order.objects.create(
user=self.user10,
cart=self.cart10,
restaurant=self.restaurant,
address='서울시 서초구 서초2동',
status=0,
delivery_status=1,
weather=4,
)
# When
response = self.client.get(reverse('menu_api:menu_list_api', kwargs=kwargs))
# Then
self.assertEqual(response.status_code, HTTPStatus.OK)
- 어려웠던 점
- ORM
- 가중치 계산
- 특정 키의 값을 기준으로 dict 정렬하는 것
- 해결방법
- Order 테이블에서 부터 join을 하려고 했으나 안먹혔다.. 그래서 Cart 테이블부터 시작해서 join했다.
- 돌머리 싸매면서 계산했다.
- sorted로 dict 정렬을 했다. sorted 함수에 key 속성에다가 operator.itemgetter(KEY)를 넣으면 KEY의 value를 기준으로 정렬한다.
- 느낀점
- ORM을 자유자재로 다루는 것이 매우 중요하다. 여러 테이블을 join시키고 집계함수를 사용해서 값을 계산하는 게 쉽진 않다. ORM이 어떻게 동작하는지 제대로된 원리를 알고 싶다. 이런건 어디서 알지
'인턴 프로젝트' 카테고리의 다른 글
미니 요기요 프로젝트(6) - 요식이 이벤트 리스트 기타 등등 (0) | 2019.07.24 |
---|---|
미니 요기요 프로젝트(5) - 레스토랑 구독하기, 구독 중인 레스토랑 (0) | 2019.07.24 |
미니 요기요 프로젝트(3) - 메뉴, 메뉴 디테일 (0) | 2019.07.23 |
미니 요기요 프로젝트(2) - 레스토랑, 레스토랑 디테일 (0) | 2019.07.23 |
미니 요기요 프로젝트(1) - 카테고리 (0) | 2019.07.23 |