Workfolio - Filter

eslerkang·2021년 12월 21일
0

Workfolio

목록 보기
2/4
post-thumbnail

Filtering & Ordering

이번 프로젝트에서는 정보를 가져오는(GET) 메소드 위주로 구현을 맡아 진행했기에 Filtering과 Ordering을 할 일이 있었다. 그 중 가장 복잡하다고 생각이 드는 BuildingListView에 대해 정리하고자 한다.

코드

class BuildingListView(View):
    def get(self, request):
        try:
            data  = request.GET

            orders = {
                'price-low'      : 'min_price',
                'price-high'     : '-min_price',
                'popularity'     : '-popularity',
                'headcount-low'  : 'min_capacity',
                'headcount-high' : '-max_capacity'
            }

            filters = {
                'special' : 'specials__name__in',
                'country' : 'country__in',
                'city'    : 'city__in',
                'district': 'district__in'
            }

            limit      = int(data.get('limit', 10) or 10)
            offset     = int(data.get('offset', 0) or 0)
            max_price  = int(data.get('max-price', 50000) or 50000)
            min_price  = int(data.get('min-price', 0) or 0)
            capacity   = int(data.get('headcount', 10) or 10)
            check_in   = data.get('check-in', None)
            check_out  = data.get('check-out', None)
            search     = data.get('search', None)
            order_by   = data.get('order-by', None)
            order_by   = orders[order_by] if order_by in orders else 'id'

            filter_set = {
                filters.get(key): value
                for (key, value) in data.lists()\
                if filters.get(key) and value != ['']
            }

            if search:
                filter_set['name__contains'] = search

            offices = Office.objects.exclude(
                Q(
                    Q(reservation__check_out_date__gt = check_in) &
                    Q(reservation__check_in_date__lte = check_in)
                ) |
                Q(reservation__check_in_date__range = [
                    check_in,
                    datetime.date.fromisoformat(check_out) - datetime.timedelta(days=1)
                ])
            ).values_list('id')\
            if check_in and check_out else Office.objects.all().values_list('id')

            office_ids = [office[0] for office in offices]

            buildings = Building.objects.filter(
                office__id__in    = office_ids
            ).distinct().annotate(
                max_price         = Max(F('office__price')),
                min_price         = Min(F('office__price')),
                max_capacity      = Max(F('office__capacity_max')),
                min_capacity      = Min(F('office__capacity')),
                popularity        = Count(F('office__reservation')),
            ).prefetch_related('buildingimage_set').filter(
                max_price__gte    = min_price,
                min_price__lte    = max_price,
                max_capacity__gte = capacity,
                min_capacity__lte = capacity,
                **filter_set,
            ).order_by(order_by)

            result = {
                'count'     : buildings.count(),
                'buildings' : [
                    {
                        'id'           : building.id,
                        'name'         : building.name,
                        'city'         : building.city,
                        'district'     : building.district,
                        'image'        : building.buildingimage_set.all()[0].url,
                        'min_capacity' : building.min_capacity,
                        'max_capacity' : building.max_capacity,
                        'min_price'    : building.min_price,
                        'max_price'    : building.max_price,
                        'latitude'     : building.latitude,
                        'longitude'    : building.longitude,
                    }
                    for building in buildings[offset:offset+limit]
                ]
            }

            return JsonResponse({'MESSAGE': 'SUCCESS', 'RESULT': result}, status=200)

        except ValueError:
            return JsonResponse({'MESSAGE': 'INVALID_FILTER'}, status=400)

전체 코드는 위와 같다. 이 전체 코드를 부분부분 나누어 살펴보도록 하겠다.

            data  = request.GET

            orders = {
                'price-low'      : 'min_price',
                'price-high'     : '-min_price',
                'popularity'     : '-popularity',
                'headcount-low'  : 'min_capacity',
                'headcount-high' : '-max_capacity'
            }

            filters = {
                'special' : 'specials__name__in',
                'country' : 'country__in',
                'city'    : 'city__in',
                'district': 'district__in'
            }

            limit      = int(data.get('limit', 10) or 10)
            offset     = int(data.get('offset', 0) or 0)
            max_price  = int(data.get('max-price', 50000) or 50000)
            min_price  = int(data.get('min-price', 0) or 0)
            capacity   = int(data.get('headcount', 10) or 10)
            check_in   = data.get('check-in', None)
            check_out  = data.get('check-out', None)
            search     = data.get('search', None)
            order_by   = data.get('order-by', None)
            order_by   = orders[order_by] if order_by in orders else 'id'

            filter_set = {
                filters.get(key): value
                for (key, value) in data.lists()\
                if filters.get(key) and value != ['']
            }

            if search:
                filter_set['name__contains'] = search

이 부분을 살펴보자면 쿼리 파라미터를 통해 order-by 값과 Filtering을 위한 쿼리들을 가져오는 부분이다. orders, filters를 통해 입력되는 값에 따라 ORM에서 order, filter를 걸기 위한 양식으로 바꿀 수 있도록 key:value 쌍으로 선언해주었다. 그 아래에는 하나의 값 만을 기대하는(받을) 키들에 대해 값을 가져오는 작업을 수행해주었다. 또한 그 아래 줄에서는 위에서 선언한 filters를 활용하여 .filter에 들어갈 쿼리를 만들어주는 작업을 진행하였다.

같은 Filtering인데 객체를 이용한 것과 그렇지 않은 것의 차이점은 한 키에 여러 값(배열)이 들어올 수 있냐 없냐의 차이로 발생했다. special, country, city, district의 경우 OR 조건을 통해 Filtering 할 생각이었고 그 외의 값들은 __gte, __lte 등을 통해 Filter 할 예정이었기에 하나의 값 만을 받아야했다.

기본적으로 Qeury Params은 QueryDict라는 장고만의 자료형으로 들어오는데, 이는 일반적인 Dictionary와는 약간의 차이가 있기에 하나의 값이 들어오더라도 ['6']과 같이 배열로 입력된다는 특징이 있고, .get, .items을 통해 값을 가져오면 배열이 아닌 배열 안의 마지막 값을 가져온다는 특징이 있다.

그렇기에 request.GET.items()를 통해 filter_set을 생성하면 값이 하나인 키들에 대해서는 올바른 객체가 생기겠지만 한 키에 여러 값이 배열로 담겨있는 것들에 대해서는 배열의 마지막 값만 나오게 되어 의도한 로직을 구현할 수 없게 된다. 그렇기에 하나의 값만을 요구하는 키들(limit, offset, check_in 등)은 .get 메소드를 통해 받아오고, 배열로 입력되어야하는(?special=a&sepcial=b) 값들은 request.GET.lists()를 통해 QueryDict에서 값들을 리스트 형태 그대로 가져올 수 있도록 하여 처리하였다.
또한 filters를 통해 'specials__name__in': ['a', 'b']등과 같이 .filter안에 들어갈 키, 값을 filter_set으로 정의해두어 아래에서 실제 필터링을 할 때 **filter_set과 같이 간단하게 표현할 수 있도록 하였다.
하나의 값들을 필요로 하는 키들에 대해서도 filter_set처럼 객체 형태로 생성할 수도 있었겠지만 정수형으로 타입을 변경하고 값을 가져오는 과정에서 default 값을 각각 정해주고 형변환을 진행하는 등의 과정을 거치고 있기에 객체로 정의하는 과정이 변수에 값을 할당해 후에 필터링 하는 것이 더 효율적일 것이라고 생각해 현재의 방식으로 구현했다.

그리고 Order의 경우에도 순서를 결정하는 요인을 하나로 통제하였기에 값이 몇개가 들어오든 가장 마지막에 입력되는 order-by 값을 기준으로 정렬할 수 있도록 .get을 통해 값을 가져오고, 정렬 가능한 값을 기준으로만 정렬 할 수 있도록 하기 위해 orders 객체를 만들어 올바른 입력값에 대해서만 정렬을 가능하도록 처리하였다.

정리

QueryDict의 경우에는 전 프로젝트에서도 query param을 사용했기에 사용해본 적은 있지만 그 당시에는 QueryDict의 특징, 메소드에 따른 값을 가져오는 방식에 대해서는 고려하지 않고 구현했었다. 이번 프로젝트에서는 키의 존재 여부에 따라, 키에 대해 필요한 값의 개수에 대해 고려해보며 전에는 모른채로 지나갔던 장고의 특성에 대해 알아볼 수 있어 좋았다. 그러나 아직 하나하나 변수로, default 값을 주어 가져오는 이 과정이 완벽하다고 생각하지는 않기에 더 좋은(효율적인) 방법에 대해 고민해봐야 할 것 같다.

profile
Surfer surfing on the dynamic flow of the world

0개의 댓글