본문 바로가기

인턴 프로젝트

미니 요기요 프로젝트(6) - 요식이 이벤트 리스트 기타 등등

요식이 이벤트 리스트

  • 기획 의도
    • 요기요 식권의 약자인 요식이라는 이벤트를 하는 레스토랑을 보여주고 요식이 티켓 수, 요식이 세트 수를 보여준다.
    • 페이지네이션
  • 화면

요식이 이벤트 리스트

  • 코드
import enum
from datetime import datetime

from django.core.paginator import Paginator
from django.db.models import F, Count

from django.http import JsonResponse
from django.views.generic.base import View

from accounts.mixins import LoginRequiredMixin

from restaurant.api.views import CategoryNum
from yosigy.models import Yosigy


class YosigyListInfo(enum.IntEnum):
    POST_TO_SHOW_IN_ONE_PAGE = 4
    PAGES_TO_SHOW = 3


class YosigyListAPIView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwargs):
        category_id = kwargs['category_id']
        today = datetime.now().date()
        tab_value = request.GET.get('tab_value', '')
        json_data = {}
        if kwargs['page']:
            self.page = kwargs['page']

        if not category_id or category_id == CategoryNum.ALL_ID:
            yosigy = (
                Yosigy.objects
                    .select_related('restaurant')
                    .prefetch_related('yosigymenu_set')
                    .filter(
                        restaurant__is_yosigy=True,
                        deadline__gte=today,
                    )
                    .values(
                        'restaurant',
                    )
                    .annotate(
                        is_yosigy_count=Count('yosigymenu__menu'),
                    )
                    .values(
                        'pk',
                        'is_yosigy_count',
                        restaurant_title=F('restaurant__title'),
                        restaurant_img=F('restaurant__img'),
                        yosigy_deadline=F('deadline'),
                        yosigy_notice=F('notice'),
                    )
                    .order_by('-created_time')
                )
        else:
            yosigy = (
                Yosigy.objects
                    .select_related('restaurant')
                    .prefetch_related('yosigymenu_set')
                    .filter(
                        restaurant__is_yosigy=True,
                        deadline__gte=today,
                        restaurant__category__pk=category_id,
                    )
                    .values(
                        'restaurant',
                    )
                    .annotate(
                        is_yosigy_count=Count('yosigymenu__menu'),
                    )
                    .values(
                        'pk',
                        'is_yosigy_count',
                        restaurant_title=F('restaurant__title'),
                        restaurant_img=F('restaurant__img'),
                        yosigy_deadline=F('deadline'),
                        yosigy_notice=F('notice'),
                    )
                    .order_by('-created_time')
                )
        yosigy_set = (
            Yosigy.objects
                .select_related('restaurant')
                .prefetch_related('yosigymenu_set')
                .filter(yosigymenu__menu__is_set_menu=True,)
                .annotate(
                    is_set_menu_count=Count('yosigymenu__menu'),
                )
                .values(
                    'is_set_menu_count',
                    'pk',
                )
            )
        for i in yosigy:
            for j in yosigy_set:
                if i['pk'] == j['pk']:
                    i['is_set_menu_count'] = j['is_set_menu_count']
        yosigy=list(yosigy)

        if not yosigy:
            json_data = {
                'message': '아직 공동 구매할 수 있는 메뉴가 없습니다.',
            }
        elif tab_value == 'deadline':
            yosigy=sorted(yosigy, key=lambda menu:menu['yosigy_deadline'])
            json_data = self.yosigy_paginator(yosigy)
            json_data['deadline'] = True
        elif tab_value == 'all' or tab_value == '':
            json_data = self.yosigy_paginator(yosigy)
            json_data['all'] = True

        return JsonResponse(
            json_data
        )

    def yosigy_paginator(self, yosigy):
        paginator = Paginator(yosigy, YosigyListInfo.POST_TO_SHOW_IN_ONE_PAGE)
        current_page = paginator.get_page(self.page)

        start = (self.page-1) // YosigyListInfo.PAGES_TO_SHOW * YosigyListInfo.PAGES_TO_SHOW + 1
        end = start + YosigyListInfo.PAGES_TO_SHOW

        last_page = len(paginator.page_range)

        if last_page < end:
            end = last_page

        yosigy_list = current_page.object_list

        page_range = range(start, end + 1)

        yosigy_list_data = {
            'yosigy_list': yosigy_list,
            'current_page': {
                'has_previous': current_page.has_previous(),
                'has_next': current_page.has_next(),
            },
            'page_range': [page_range[0], page_range[-1]],
        }

        if current_page.has_previous():
            yosigy_list_data['current_page']['previous_page_number'] = current_page.previous_page_number()
        if current_page.has_next():
            yosigy_list_data['current_page']['next_page_number'] = current_page.next_page_number()

        return yosigy_list_data
  • 테스트 코드
    • 이 시점에 테스트 코드의 중요성보다 새기능 구현에 초점이 맞춰져서 안짬..
  • 어려웠던 점
    • 하나의 ORM에서 2가지 필드에 대한 집계함수를 각각 사용하는 것.
  • 해결방법
    • 불가능했다. 그래서 ORM을 두번 사용해서 각각 필드에 대한 집계를 하고 for문을 돌면서 쿼리셋에 필드를 넣는 방법을 사용했다.
  • 느낀점
    • ORM으로 다 되는 건 아니였다.

2. 요식이 사용을 위해 체크하는 부분, 사용가능, 사용됨 따로 출력

  • 기획의도
    • 사용가능 탭에는 아직 사용하지 않은 요식이 티켓이, 사용됨 탭에는 사용된 요식이 티켓이 있음
    • 사용가능한 요식이 티켓은 체크해서 사용할 수 있음
    • 만약 같은 레스토랑이 아닌 메뉴를 체크하고 사용하기를 누르면 에러 문구가 뜸
    • 아무것도 선택하지 않고 사용하기를 누르면 선택하라는 문구가 뜸
  • 화면

  • 코드
import ast
from datetime import timedelta
from http import HTTPStatus

from django.db.models import F
from django.http import JsonResponse
from django.utils import timezone
from django.views.generic.base import View

from accounts.mixins import LoginRequiredMixin
from accounts.models import User
from yosigy.models import YosigyTicket, YosigyTiketStatus


class YosigyOrderListAPIView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwargs):
        user = request.user.pk
        now_time = timezone.now() + timedelta(hours=9)
        try:
            yosigy_pks = request.GET.get('yosigyPks')
            yosigy_pks = ast.literal_eval(yosigy_pks)
            yosigy_pks_list = [int(i) for i in yosigy_pks]
        except ValueError:
            return JsonResponse(
                status=HTTPStatus.BAD_REQUEST,
                data={
                    'message': '체크된 요식이가 없습니다. 요식이를 체크 후 사용해주세요 !'
                }
            )
        user_info = (
            User.objects
                .values(
                    'id',
                    'username',
                    'phone',
                    'address',
                    'address_detail',
                )
                .get(pk=user)
        )

        yosigy_order_info = (
            YosigyTicket.objects
                .select_related('menu', 'yosigymenu', )
                .filter(
                    pk__in=yosigy_pks_list,
                    user=user,
                    status__in=[YosigyTiketStatus.PUBLISHED, ],
                    expire_time__gte=now_time,
                )
                .annotate(
                    yosigy_id=F('pk'),
                    expore_time=F('expire_time'),
                    menu_name=F('menu__name'),
                    menu_img=F('menu__img'),
                    menu_detail=F('menu__detail'),
                    price=F('yosigy_menu__discounted_price'),
                    restaurant_id=F('yosigy_menu__yosigy__restaurant__pk'),
                    restaurant_title=F('yosigy_menu__yosigy__restaurant__title')
                )
                .values(
                    'yosigy_id',
                    'expire_time',
                    'menu_name',
                    'menu_img',
                    'menu_detail',
                    'price',
                    'restaurant_id',
                    'restaurant_title',
                )
        )

        if not yosigy_order_info:
            return JsonResponse(
                status=HTTPStatus.BAD_REQUEST,
                data={
                    'message': '해당 요식이가 없습니다. 확인 후 다시 사용해주세요 !'
                }
            )

        json_data = {
            'yosigy_order_info': list(yosigy_order_info),
            'user_info': user_info
        }

        return JsonResponse(
            json_data
        )
  • 테스트 코드
from datetime import datetime
from http import HTTPStatus

from dateutil.relativedelta import relativedelta
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from accounts.models import User
from menu.models import Menu
from restaurant.models import Restaurant
from yosigy.models import Yosigy, YosigyMenu, YosigyTicket, YosigyTiketStatus


class YosigyOrderListTest(TestCase):
    def setUp(self):
        self.datetime_future=timezone.now() + relativedelta(years=10)
        self.datetime_past=timezone.now() - relativedelta(years=10)

        self.owner = User.objects.create_user(
            username='owner',
            email='',
            password='1',
            address='서울시 서초구 서초2동 마제스타 시티',
        )
        self.user = User.objects.create_user(
            username='yosigy',
            email='',
            password='1',
            address='서울시 서초구 서초2동 사랑의 교회',
        )
        self.user_without_yosigy = User.objects.create_user(
            username='yosigy2',
            email='',
            password='1',
            address='서울시 서초구 서초2동 사랑의 교회',
        )
        self.restaurant1 = Restaurant.objects.create(
            name='굽네치킨',
            title='굽네치킨-서초점',
            min_order_price=10000,
            delivery_charge=1000,
            store_owner=self.owner,
            estimated_delivery_time='20:00',
            operation_start_hour='10:00',
            operation_end_hour='20:00',
        )
        self.restaurant2 = Restaurant.objects.create(
            name='굽네치킨2',
            title='굽네치킨2-서초점',
            min_order_price=10000,
            delivery_charge=1000,
            store_owner=self.owner,
            estimated_delivery_time='20:00',
            operation_start_hour='10:00',
            operation_end_hour='20:00',
        )
        self.menu1 = Menu.objects.create(
            restaurant=self.restaurant1,
            name='볼케이노',
            detail='매콤',
            price=15000,
            type='양념류',
            img='test.jpg',
            is_yosigy=True,
        )
        self.menu2 = Menu.objects.create(
            restaurant=self.restaurant1,
            name='허니멜로',
            detail='달콤',
            price=16000,
            type='양념류',
            img='test2.jpg',
            is_yosigy=True,
        )
        self.yosigy = Yosigy.objects.create(
            restaurant=self.restaurant1,
            user=self.user,
            deadline=datetime(2100,11,11),
            notice='요식이 레스토랑입니다.',
            min_price=20000,
        )
        self.yosigy_menu1 = YosigyMenu.objects.create(
            discounted_price=12000,
            menu=self.menu1,
            yosigy=self.yosigy,
        )
        self.yosigy_menu2 = YosigyMenu.objects.create(
            discounted_price=11000,
            menu=self.menu2,
            yosigy=self.yosigy,
        )
        self.yosigy_ticket_published = YosigyTicket.objects.create(
            user=self.user,
            yosigy_menu=self.yosigy_menu1,
            menu=self.menu1,
            expire_time=self.datetime_future,
            status=YosigyTiketStatus.PUBLISHED,
        )
        self.yosigy_ticket_used = YosigyTicket.objects.create(
            user=self.user,
            yosigy_menu=self.yosigy_menu2,
            menu=self.menu1,
            expire_time=self.datetime_future,
            status=YosigyTiketStatus.USED,
        )
        self.yosigy_ticket_expired = YosigyTicket.objects.create(
            user=self.user,
            yosigy_menu=self.yosigy_menu2,
            menu=self.menu1,
            expire_time=self.datetime_past,
            status=YosigyTiketStatus.USED,
        )

    def test_yosigy_order_list(self):
        '''
        사용자가 선택한 요식이가 요식이 주문 페이지에 나오는 지 테스트
        '''
        # Given
        self.client.login(username='yosigy', password='1')
        yosigy_ticket_pk = [str(self.yosigy_ticket_published.pk)]
        # When
        response = self.client.get(reverse('yosigy_api:yosigy_order_list_api'), {'yosigyPks': str(yosigy_ticket_pk)})
        # Then
        self.assertEqual(response.status_code, HTTPStatus.OK)

    def test_return_BAD_REQUEST_when_yosigy_unchecked(self):
        '''
        선택된 요식이가 없는 경우 BAD REQUEST 반환
        '''
        # Given
        self.client.login(username='yosigy', password='1')
        # When
        response = self.client.get(reverse('yosigy_api:yosigy_order_list_api'))
        # Then
        message = response.json()['message']
        self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
        self.assertEqual(message, '체크된 요식이가 없습니다. 요식이를 체크 후 사용해주세요 !')

    def test_return_BAD_REQUEST_when_no_yosigy(self):
        '''
        요식이가 없는 경우 BAD REQUEST 반환
        '''
        # Given
        self.client.login(username='yosigy', password='1')
        not_exist_yosigy_ticket_pk = 999999
        yosigy_ticket_pk = [str(not_exist_yosigy_ticket_pk)]
        # When
        response = self.client.get(reverse('yosigy_api:yosigy_order_list_api'), {'yosigyPks': str(yosigy_ticket_pk)})
        # Then
        message = response.json()['message']
        self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
        self.assertEqual(message, '해당 요식이가 없습니다. 확인 후 다시 사용해주세요 !')
  • 어려웠던 점
    • 클라이언트에서 서버로 값을 넘길 때 url 파라미터를 사용해서 넘겼다. http://127.0.0.1:8000/yosigy/order/list/?yosigy-check=5 요렇게..
    • 그런데 yosigy_pks = request.GET.get('yosigyPks') 이런식으로 값을 받아보니 이렇게 됐다... 이게 뭐지.

5가 아니라 "['5']"라니!?

  • 해결방법
    • ast.literal_eval()을 썼다.
    • yosigy_pks = ast.literal_eval(yosigy_pks)
    • str 타입의 '['5']'가 list 타입의 ['5']로 변한다.
    • literal_eval은 eval 과는 다르게 built-in 함수는 아니며, AST(Abstract Syntax Trees) module 에서 제공하는 함수 중 하나이다. AST 모듈은 문법을 구조화 시켜주는 모듈 정도로 이해하고 넘어가자. 출처: https://bluese05.tistory.com/65 [ㅍㅍㅋㄷ]
  • 느낀점
    • url 파라미터로 값을 넘겨줄 때는 곧이 곧대로 값이 오지 않으므로 처리를 해줘야한다.

3. 요식이 사용하기

  • 기획의도
    • 요식이 사용 시 사용가능한지 점검
  • 화면

  • 코드
from datetime import timedelta
from http import HTTPStatus

from django.db.models import F
from django.http import JsonResponse
from django.utils import timezone
from django.views.generic.base import View

from accounts.mixins import LoginRequiredMixin
from yosigy.models import YosigyTicket, YosigyTiketStatus


class YosigyTicketListAPIView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwargs):
        user = request.user.pk
        now_time = timezone.now() + timedelta(hours=9)
        yosigy_ticket = (
            YosigyTicket.objects
                .select_related('yosigy_menu', 'menu', 'user')
                .filter(
                    user=user,
                    status__in=[YosigyTiketStatus.PUBLISHED, YosigyTiketStatus.USED, ],
                    expire_time__gte=now_time,
                )
                .annotate(
                    price=F('yosigy_menu__discounted_price'),
                    restaurant_title=F('yosigy_menu__yosigy__restaurant__title'),
                    restaurant_id=F('yosigy_menu__yosigy__restaurant__pk')
                )
                .values(
                    'pk',
                    'menu__name',
                    'menu__img',
                    'status',
                    'expire_time',
                    'price',
                    'restaurant_title',
                    'restaurant_id',
                )
                .order_by('-created_time')
        )

        if not yosigy_ticket:
            return JsonResponse(
                status=HTTPStatus.BAD_REQUEST,
                data={
                    'message': '소유하신 요식이가 없습니다. 요식이를 구매 후 이용하세요 !'
                }
            )

        yosigy_ticket_rest = (
            YosigyTicket.objects
                .select_related('yosigy_menu', 'user')
                .filter(user=user, status__in=[YosigyTiketStatus.PUBLISHED, ], expire_time__gte=now_time,)
        )

        json_data = {
            'yosigy_ticket_list': list(yosigy_ticket),
            'yosigy_ticket_rest': len(yosigy_ticket_rest),
        }

        return JsonResponse(
            json_data
        )
  • 테스트 코드
import json
from datetime import datetime
from http import HTTPStatus

from dateutil.relativedelta import relativedelta
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from accounts.models import User
from menu.models import Menu
from restaurant.models import Restaurant
from yosigy.models import Yosigy, YosigyMenu, YosigyTicket, YosigyTiketStatus


class YosigyTicketListTest(TestCase):
    def setUp(self):
        self.datetime_future=timezone.now() + relativedelta(years=10)
        self.datetime_past=timezone.now() - relativedelta(years=10)

        self.owner = User.objects.create_user(
            username='owner',
            email='',
            password='1',
            address='서울시 서초구 서초2동 마제스타 시티',
        )
        self.user = User.objects.create_user(
            username='yosigy',
            email='',
            password='1',
            address='서울시 서초구 서초2동 사랑의 교회',
        )
        self.user_without_yosigy = User.objects.create_user(
            username='yosigy2',
            email='',
            password='1',
            address='서울시 서초구 서초2동 사랑의 교회',
        )
        self.restaurant1 = Restaurant.objects.create(
            name='굽네치킨',
            title='굽네치킨-서초점',
            min_order_price=10000,
            delivery_charge=1000,
            store_owner=self.owner,
            estimated_delivery_time='20:00',
            operation_start_hour='10:00',
            operation_end_hour='20:00',
        )
        self.restaurant2 = Restaurant.objects.create(
            name='굽네치킨2',
            title='굽네치킨2-서초점',
            min_order_price=10000,
            delivery_charge=1000,
            store_owner=self.owner,
            estimated_delivery_time='20:00',
            operation_start_hour='10:00',
            operation_end_hour='20:00',
        )
        self.menu1 = Menu.objects.create(
            restaurant=self.restaurant1,
            name='볼케이노',
            detail='매콤',
            price=15000,
            type='양념류',
            img='test.jpg',
            is_yosigy=True,
        )
        self.menu2 = Menu.objects.create(
            restaurant=self.restaurant1,
            name='허니멜로',
            detail='달콤',
            price=16000,
            type='양념류',
            img='test2.jpg',
            is_yosigy=True,
        )
        self.yosigy = Yosigy.objects.create(
            restaurant=self.restaurant1,
            user=self.user,
            deadline=datetime(2100,11,11),
            notice='요식이 레스토랑입니다.',
            min_price=20000,
        )
        self.yosigy_menu1 = YosigyMenu.objects.create(
            discounted_price=12000,
            menu=self.menu1,
            yosigy=self.yosigy,
        )
        self.yosigy_menu2 = YosigyMenu.objects.create(
            discounted_price=11000,
            menu=self.menu2,
            yosigy=self.yosigy,
        )
        self.yosigy_ticket_published = YosigyTicket.objects.create(
            user=self.user,
            yosigy_menu=self.yosigy_menu1,
            menu=self.menu1,
            expire_time=self.datetime_future,
            status=YosigyTiketStatus.PUBLISHED,
        )
        self.yosigy_ticket_used = YosigyTicket.objects.create(
            user=self.user,
            yosigy_menu=self.yosigy_menu2,
            menu=self.menu1,
            expire_time=self.datetime_future,
            status=YosigyTiketStatus.USED,
        )
        self.yosigy_ticket_expired = YosigyTicket.objects.create(
            user=self.user,
            yosigy_menu=self.yosigy_menu2,
            menu=self.menu1,
            expire_time=self.datetime_past,
            status=YosigyTiketStatus.USED,
        )

    def test_yosigy_ticket_list(self):
        '''
        사용자가 소유한 발행된 요식이, 사용된 요식이 체크, 사용 가능한 요식이 개수 체크
        '''
        # Given
        self.client.login(username='yosigy', password='1')
        # When
        response = self.client.get(reverse('yosigy_api:yosigy_ticket_list_api'))
        # Then
        response_content = json.loads(response.content)
        self.assertEqual(response.status_code, HTTPStatus.OK)
        self.assertEqual(response_content['yosigy_ticket_rest'], 1)
        for yosigy in response_content['yosigy_ticket_list']:
            self.assertIn(yosigy['status'], [YosigyTiketStatus.PUBLISHED, YosigyTiketStatus.USED])

    def test_return_BAD_REQUEST_when_user_has_no_yosigy(self):
        '''
        요식이를 소유하지 않은 사용자인 경우 BAD REQUEST 반환
        '''
        # Given
        self.client.login(username='yosigy2', password='1')
        # When
        response = self.client.get(reverse('yosigy_api:yosigy_ticket_list_api'))
        # Then
        response_json = response.json()
        message = response.json()['message']
        self.assertEqual(message, '소유하신 요식이가 없습니다. 요식이를 구매 후 이용하세요 !')