될때까지

((TURTLE HOME)) 11일차 : ProductListView filtering refactoring 본문

프로젝트/wecode1차 : TURTLE-HOME

((TURTLE HOME)) 11일차 : ProductListView filtering refactoring

랖니 2022. 7. 30. 12:00
728x90

드디어 완성한 상품 리스트뷰.. 필터와 정렬이 이렇게 어려운 지 몰랐다. API하나 만드는데 시간이 이렇게 오래 걸릴 일인가 현타도 많이왔다. 나는 앞으로 나아가질 못하고 있는데 이미 다른 팀원들은 추가기능까지 도전하고 있고.. ㅜ_ㅜ 누구나 꽃피는 시기는 달라 어제의 나와 비교하자 파이팅!!! 멘탈을 다잡고 이제 완성된 코드를 리팩토링 해보자.

class ProductListView(View):
    def get(self, request):
        
        sort_by   = request.GET.get('sort_by')
        size      = request.GET.get('size')
        min_price = request.GET.get('min_price', 0)
        max_price = request.GET.get('max_price')
        limit     = int(request.GET.get('limit', 20))
        offset    = int(request.GET.get('offset', 0))
        
        sort_conditions = {
            'high_price'  : '-min_price',
            'low_price'   : 'min_price',
            'newest'      : '-created_at'
        }
        
        sort_field = sort_conditions.get(sort_by, 'created_at')
        
        q = Q()
        
        if size: 
            q &= Q(productoption__size__name = size)
        
        q.add(Q(min_price__gte = min_price), q.AND)
                
        if max_price :
            q.add(Q(min_price__lt = max_price), q.AND)   
            
        products = Product.objects\
            .annotate(min_price = Min('productoption__price'))\
            .annotate(max_price = Max('productoption__price'))\
            .filter(q).order_by(sort_field)[offset:offset+limit]
        
        result = [{ 
            'id'       : product.id,
            'name'     : product.name,
            'image_url': product.image_url,
            'min_price': product.min_price,
            'max_price': product.max_price 
        } for product in products]
        
        return JsonResponse({'result':result}, status=200)

원래 정렬에 대한 분기처리(낮은 가격? 높은가격? 최신순?)를 if문을 반복해서 처리했었다. 그러다보니 계속 반복되는 if문 구조

if sort_by == '정렬조건':
	정렬할 방법

맨 위의 코드처럼 딕셔너리로 만들어서 처리했더니, 반복되던 코드가 사라지고 훨씬 간결해졌다.

그렇다면 size, min_price, max_price도 딕셔너리로 처리할 수 있지않나!? 라는 생각이 들었고, 멘토님도 그렇게도 가능하다고 하셨다. 그렇다면 참을 수 없지 도전!!

 

딕셔너리 컴프리헨션 사용

class ProductListView(View):
    def get(self, request):
        
        sort_by   = request.GET.get('sort_by')
        limit     = int(request.GET.get('limit', 20))
        offset    = int(request.GET.get('offset', 0))
        
        sort_conditions = {
            'high_price'  : '-min_price',
            'low_price'   : 'min_price',
            'newest'      : '-created_at'
        }
        
        sort_field = sort_conditions.get(sort_by, 'created_at')
        
        filter_conditions = {
            'size'     : 'productoption__size__name',
            'min_price': 'min_price__gte',
            'max_price': 'min_price__lt'
        }

먼저 filter_conditions라는 빈 딕셔너리를 생성한다. 여기서 filter_conditions의 키값은 적용시킬 필터 조건이 된다. 

프론트로부터 size, min_price, max_price의 키 이름으로 각각 키에 해당하는 벨류를 전달받을 예정이다.

url에 filter_conditions에 있는 키값이 있나 확인하고 있다면 각 키에 맞는 value값으로 가져오고 filter에 적용을 시켜야한다.

 

1. request.GET.items()를 사용하면 클라이언트로부터 요청을 받을 때 키와 벨류의 쌍을 얻을 수 있고 (key, value)형식으로 담긴다. 다만 뒤에 if조건식을 통해서 클라이언트러부터 전달받은 필터 이름이 filter_conditions에 있는 키값인 경우에만(size, min_price, max_price가 들어온 경우) 키, 벨류형식으로 담긴다.

/products?size=S?min_price=150000&max_price=300000 같은 경우
('size', 's'), ('min_price',150000), ('max_price', 30000) 처럼 담긴다
        filter_field = {
            filter_conditions.get(key):value 
                for (key, value) in request.GET.items() 
                    if filter_conditions.get(key) 
        }

2. 반복문을 돌면서 클라이언트로부터 입력받은 조건들이 하나씩 (키,벨류) 형식으로 담겨있다. filter_conditions.get(key) : value를 실행하면서 이제는 filter_conditions의 벨류값이 왼쪽에 담기고(딕셔너리.get(key)는 key에 해당하는 벨류값을 반환) 클라이언트로부터 입력받은 value가 오른쪽에 담긴다.

/products?size=S?min_price=150000&max_price=300000 같은 경우
('size', 's'), ('min_price',150000), ('max_price', 30000) 처럼 담긴다.

첫번째 ('size', 's')의 경우 filter_conditions.get(size)의 결과값은 'productoption__size__name'이 된다. 
value는 's'가 된다.
=> 'productoption__size__name' : 's' 

두번째 ('min_price', 150000)의 경우 filter_conditions.get(min_price)의 결과값은 'min_price__gte'가 된다.
value는 150000이 된다.
=> 'min_price__get' : 150000

print(filter_field)
# {'productoption__size__name': 'double', 'min_price__gte': '150000', 'min_price__lt': '300000'}

3. 딕셔너리 앞에 별표를 2개 붙이면 ** 딕셔너리 언패킹이 일어난다. 그래서 키워드 인수로 사용할 수 있다.

        products = Product.objects\
            .annotate(min_price = Min('productoption__price'))\
            .annotate(max_price = Max('productoption__price'))\
            .filter(**filter_field).order_by(sort_field)[offset:offset+limit]

4. Product.objects.filter(productoption__size__name='double', min_price__gte='150000', min_price__lt='300000')가 되고, 상품 객체들 중 필터안의 조건에 해당하는 객체들을 찾아서 쿼리셋에 담아서 반환한다. (filter는 쿼리셋, get는 객체) 그럼 이제 리스트 컴프리헨션을 사용해서 각 객체에 맞는 값들을 넣어주면 코드 완성이다!

class ProductListView(View):
    def get(self, request):
        
        sort_by   = request.GET.get('sort_by')
        limit     = int(request.GET.get('limit', 20))
        offset    = int(request.GET.get('offset', 0))
        
        sort_conditions = {
            'high_price'  : '-min_price',
            'low_price'   : 'min_price',
            'newest'      : '-created_at'
        }
        
        sort_field = sort_conditions.get(sort_by, 'created_at')
        
        filter_conditions = {
            'size'     : 'productoption__size__name',
            'min_price': 'min_price__gte',
            'max_price': 'min_price__lt'
        }
        
        filter_field = {
            filter_conditions.get(key):value 
                for (key, value) in request.GET.items() 
                    if filter_conditions.get(key) 
        }
            
        products = Product.objects\
            .annotate(min_price = Min('productoption__price'))\
            .annotate(max_price = Max('productoption__price'))\
            .filter(**filter_field).order_by(sort_field)[offset:offset+limit]
        
        result = [{ 
            'id'       : product.id,
            'name'     : product.name,
            'image_url': product.image_url,
            'min_price': product.min_price,
            'max_price': product.max_price 
        } for product in products]
        
        return JsonResponse({'result':result}, status=200)

 

딕셔너리 컴프리헨션, 리스트 컴프리헨션은 마스터 한 것 같아서 뿌듯하다 😎

 

* 참고한 자료

https://velog.io/@adsf25/Django-%EB%8B%A4%EC%A4%91-%ED%95%84%ED%84%B0-%EC%A0%81%EC%9A%A9

 

[Django] 다중 필터 적용

https://velog.io/@suasue/Django-%ED%95%84%ED%84%B0%EB%A7%81-%EB%BD%80%EA%B0%9C%EA%B8%B0-if%EB%AC%B8-%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC-Q-%EA%B0

velog.io

https://velog.io/@suasue/Django-%ED%95%84%ED%84%B0%EB%A7%81-%EB%BD%80%EA%B0%9C%EA%B8%B0-if%EB%AC%B8-%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC-Q-%EA%B0%9D%EC%B2%B4-%ED%99%9C%EC%9A%A9

 

Django | 필터링 뽀개기! if문, 딕셔너리, Q 객체 활용

장고에서 필터링 기능을 구현할 수 있는 방법에는 크게 3가지 방법이 있다. if문을 이용하는 방법, 딕셔너리를 이용하는 방법, Q 객체를 이용하는 방법이다. 오늘의 집을 예시로 해서 3가지 방법

velog.io

 

728x90