팀 프로젝트

국민은행 소프트웨어 경진대회(2022.07.17~2022.09.18)

땅콩콩 2022. 9. 28. 00:53

이번 여름방학에 아이디어톤 끝나고 한달정도 있다가 국민은행 소프트웨어 경진대회 공모전을 발견하고 너무 출품해보고싶어서 동아리 친구들을 꼬셔서 플젝을 같이 했다! 

 

백엔드 파트로 drf를 처음 써보는 플젝이었는데 공부할만한 한국어 자료가 엄청 많지는 않아서 처음에 공부하는 단계에서 좀 어려웠는데, 그래도 여기저기 나와있는 강의 이것저것+폭풍검색+질문으로 어떻게 잘 마무리했다ㅋㅋㅋㅋㅋㅋ

 

나도 나지만 다른 팀원들도 거의 drf나 리액트 처음 써봐서 고생이 많았는데 그래도 다같이 마지막까지 안놓고 열심히 해줘서 너무 고마운 마음이 든다. 솔직히 개강하기 전에 끝나는게 목표였는데 개강을 넘겨버려서 플젝이 흐지부지될까봐 좀 걱정했었는데 다들 마지막까지 같이 열심히 작업해서 제출까지 무사히 할 수 있었던것같다.

20초 남기고 11:59에 아슬아슬 제출....ㅋㅋㅋㅋㅠㅠㅠ

 

우리는 공모전 선택주제인 환경/사회공헌 중 환경을 선택했고, 서비스는 패스트패션(fast-fashion)으로 인한 환경문제 개선을 위한 중고의류 교환/거래 플랫폼 Trashion을 만들었다.

 

아래는 패스트패션에 대한 기사이다.

https://www.bbc.com/korean/international-61864499

 

환경 우려 부르는 패스트 패션... 해결책은? - BBC News 코리아

패션 관련 환경 문제는 무엇이고, 재활용 의류는 환경 보호에 얼마나 도움이 될까.

www.bbc.com

 

나는 백엔드 파트였고, 우리팀이 구현하려고 한 기본기능은 다음과 같았다.

1. 상품관련: 교환하고자 하는 상품의 기본적인 CRUD, 사용자의 지역별, 키별, 몸무게별, 카테고리, 판매여부별 등등 다양한 필터링

2. 유저관련: 회원가입/로그인 등 기본적인 유저기능과 소셜로그인, 팔로우, 물건 찜하기기능, 물건 추천기능

3. 거래관련: 채팅과 알람, 거래 이후에 이루어지는 리뷰기능

 

솔직히 이중에 백 구현 다해놓고 프론트단 손이 부족해서 가슴 아프게도 구현을 최종적으로 못한 부분도 있다...ㅋㅋㅋㅋ

그래서 특히 이번 플젝에서 느낀점이 진짜 넘넘 많다는것!! ㅋㅋㅋㅋ 이런걸 느끼려고 플젝을 하는구나 하는 생각이 들었다.

 

이번 프로젝트에서 배운점

 

1. 개발 마감기한은 우리가 생각하는 마지노선보다 최소 20프로는 당겨서 잡아야겠다!

 

마감이 다가오면서 배포가 가까워지면 우리가 생각하는것보다 오류가 많이나고, 그걸 고치는 속도는 일정하지가 못하다..ㅠㅠ 마감일자를 제출마감일자로만 생각하고 좀 더 미리 팀 일정을 관리하지 못했던 부분이 스스로 조금 아쉬웠다. 저번 먹키네이터 프로젝트때는 개발일정을 정말 타이트하게 잡아서 예상보다 빨리 배포까지 완전 끝나버렸기 때문에, 이번 플젝에서 좀 방심한 부분이 있었다는 생각이 들어서 스스로 반성했다.

 

2. 초기 DB모델 설계단계에서 구현방식을 정말 세세한 부분까지 다같이 하나하나 이야기하며 맞춰봐야겠다!

 

판매하고자 하는 의류 아이템을 CREATE하는 부분에서 지역을 여러개 받아오는지/ 하나만 받아오는지를 결정하는 부분에서 서로 생각하는 부분이 달랐는데,

우리가 DB설계를 구글 다이어그램에 파놓고 백엔드 인원이 다같이 무작위 작성 > django 모델로 옮김 이런 작업을 거치다보니 누구는 Item 과 Location을 다대다로 생각하고, 또 누구는 일대다로 생각해서 마감 직전에 구현상 불필요한 다대다 관계를 발견한 일이 있었다.

다행히 기능상 문제가 되는 부분은 아니었지만, 초기 설계단계에서 정확한 소통 부족으로 불필요하게 복잡한 db구조가 추가되었다는 점에서 다음 프로젝트에서는 이런일이 없도록 해야겠다는 생각을 했다.

 

3. 프레임워크에 대한 정확한 탐구를 하면서 사용해야겠다!

 

장고에서 제공하는 filter기능을 제대로 쓰는법을 몰라서 한참 뻘짓+개고생하다가 매우 허무해진 경험이 있었다....

 맨 처음에 Viewset으로 아이템들을 필터링하는 부분을 구현하면서 너무 단순하게 생각한 나머지 각각의 아이템을 필터링 해주는 별개의 엔드포인트를 만들었었다. 대충 몇개만 공유하자면 이런식으로....

# 현재 판매중인 아이템 조회 (판매완료 x)
@action(detail=False, methods=['GET'])
def not_sold_item(self, request):
    items = Item.objects.filter(sold_out=False)
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 판매된 아이템 (판매완료)
@action(detail=False, methods=['GET'])
def sold_item(self, request):
    items = Item.objects.filter(sold_out=True)
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 내 아이템 조회
@action(detail=False, methods=['GET'])
def my_item(self, request):
    items = Item.objects.filter(user_id=request.GET.get('user_id'))
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 빅카테고리별 아이템 조회('bigCategory'를 받아서 그걸로 분류)
@action(detail=False, methods=['GET'])
def bigCategory_item(self, request):
    categorys = Category.objects.filter(big_category=request.GET.get('bigCategory'))
    category_ids = []
    for i in categorys:
        category_ids.append(i.id)
    items = Item.objects.filter(category_id__in=category_ids)
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 카테고리별 아이템 조회('category_id'를 받아서 그걸로 분류)
@action(detail=False, methods=['GET'])
def category_item(self, request):
    items = Item.objects.filter(category_id=request.GET.get('category_id'))
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 지역별 아이템 조회
@action(detail=False, methods=['GET'])
def location_item(self, request):
    city = request.GET.getlist('city', None)  # 시 여러개 선택가능
    gu = request.GET.getlist('gu', None)  # 구는 여러개 선택 가능
    dong = request.GET.getlist('dong', None)  # 동은 여러개 선택 가능
    locations = Location.objects.filter(
        Q(city__in=city) & Q(gu__in=gu) & Q(dong__in=dong)
    )
    location_ids = []
    for i in locations:
        location_ids.append(i.id)
    locationsets = LocationSet.objects.filter(location_id__in=location_ids)
    item_ids = []
    for i in locationsets:
        item_ids.append(i.item_id)
    serializer = self.get_serializer(item_ids, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 사이즈별 아이템 조회(입력한 키의 +-3cm, 몸무게의 +-3kg 범주 내의 상품을 조회)
@action(detail=False, methods=['GET'])
def size_item(self, request):
    height = int(request.GET.get('height', None))
    weight = int(request.GET.get('weight', None))
    items = Item.objects.filter(
        Q(height__range=[height - 3, height + 3]) & Q(weight__range=[weight - 3, weight + 3])
    )
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 일반 이미지만 모아보기 : 같은 아이템에 사진 여러장일 경우에는 한장만!
@action(detail=False, methods=['GET'])
def photo_item_only(self, request):
    item_ids = Photo.objects.distinct().values_list('item_id', flat=True)
    items = Item.objects.filter(id__in=item_ids)
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 착장 이미지만 모아보기 : 같은 아이템에 사진 여러장일 경우에는 한장만!
@action(detail=False, methods=['GET'])
def stylephoto_item_only(self, request):
    item_ids = StylePhoto.objects.distinct().values_list('item_id', flat=True)
    items = Item.objects.filter(id__in=item_ids)
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)


# 검색 기능 (description 기준)
@action(detail=False, methods=['GET'])
def search_item(self, request):
    q = request.GET.get('q', None)
    items = Item.objects.filter(Q(description__icontains=q))
    serializer = self.get_serializer(items, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)

근데 프론트랑 붙이다보니 필터링을 저렇게 각각의 엔드포인트에서 하는게 아니라 하나의 엔드포인트로 다양한 조건을 모두 중복 필터링할 수 있는 함수가 필요했다. 그래서 진짜 부끄럽지만 이렇게 무시무시한 코드덩어리를 작성했다.. 진짜 부끄러운데 이걸 올리는 이유는 다시는 프레임워크가 이미 제공하는 기능 쓰는법을 몰라서 이런 바보같은 노가다를 하는 바보짓을 하지 않기 위해서이다! ㅋㅋㅋㅋㅋㅋ

# 필터 종합
    @action(detail=False, methods=['GET'])
    def filter_all(self, request):
        city = request.GET.getlist('city', None)
        gu = request.GET.getlist('gu', None)
        dong = request.GET.getlist('dong', None)
        big_category = request.GET.getlist('big_category', None)
        small_category = request.GET.getlist('small_category', None)
        height = request.GET.get('height', None)
        weight = request.GET.get('weight', None)
        sold_out = request.GET.get('sold_out', None)
        locations = Location.objects.all()
        categorys = Category.objects.all()
        items = []
        print(big_category, small_category)
        # 지역별 조회
        if city is not None:
            if gu == []:  # city만 보여주기
                locations = locations.filter(Q(city__in=city))
            elif dong == []:  # city랑 gu만 보여주기
                locations = Location.objects.filter(Q(city__in=city) & Q(gu__in=gu))
            else: # city, gu, dong 다 보여주기
                locations = Location.objects.filter(Q(city__in=city) & Q(gu__in=gu) & Q(dong__in=dong))
            location_ids = []
            for i in locations:
                location_ids.append(i.id)
            locationsets = LocationSet.objects.filter(location_id__in=location_ids)
            for i in locationsets:
                items.append(i.item_id)
        # 카테고리 조회
        if big_category is not None:
            if small_category == []:
                categorys = categorys.filter(Q(big_category__in=big_category))
            else:
                categorys = categorys.filter(Q(big_category__in=big_category) & Q(small_category__in=small_category))
            category_ids = []
            for i in categorys:
                category_ids.append(i.id)
            cate_item = Item.objects.filter(category_id__in=category_ids)
            items.extend(cate_item)
        # 판매완료/판매중 조회...
        if sold_out is not None:
            s_items = Item.objects.filter(sold_out=sold_out)
            items.extend(s_items)
        # 키/몸무게 사이즈별 조회
        if height is not None:
            height = int(height)
            if weight is None:
                s_items = Item.objects.filter(Q(height__range=[height - 3, height + 3]))
            else:
                weight = int(weight)
                s_items = Item.objects.filter(
                    Q(height__range=[height - 3, height + 3]) & Q(weight__range=[weight - 3, weight + 3])
                )
            items.extend(s_items)
        serializer = self.get_serializer(items, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

이 코드를 짜고 테스트하면서 진짜 머리가 깨지는줄 알았다... 무슨 백준 푸는것같은 드러운 분기문..... 짜면서도 왠지 이건 아닌거같으면서도 또 그게 로직상 통과해서 일부 필터링에 성공하면 기뻐하고, 또 다른 요소에서 걸리면 슬퍼하고... 이런 바보짓의 반복이었다. 근데 동방에서 이걸로 골머리를 앓던 와중 옆에 있던 장고 개고수 오빠가 코드를 좀 봐줬는데 너무 기쁘면서도 간단한(?) 해결 방법을 알게되었다.... 바로 filter_backend였다.

class ItemViewSet(ModelViewSet):
    queryset = Item.objects.all()\
        .prefetch_related('location_item_sets') #역참조는 prefetch_related, 정참조는 select_related!
    serializer_class = ItemSerializer
    parser_classes = (MultiPartParser, FormParser)
    filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend, ]
    filterset_fields = ['location_item_sets__location_id__city','location_item_sets__location_id__gu',
                        'location_item_sets__location_id__dong',
                        'category_id__big_category', 'category_id__small_category']

난 저 filter_backend에 SearchFilter, OrderingFilter를 갖다 써놓고도 저게 그래서 정확히 어떻게 쓰이는지를 몰라서(뭐 엔드포인트 뒤에 파라미터로 넣는다든지....암튼) 방치하고 있었는데, 저기에 DjangoFilterBackend라는 필터링 엔진을 넣고, filter_fields에는 필터링하고자하는 필드만 넣어주면 이 모든 고난이 해결되는것이었다.....

물론 필드가 역참조되는 필드면 related_name을 앞에쓰고 'relatedname__필드이름'  머 이런식으로 접근해줘야한다.

 

글고 쿼리셋에 .prefetch_related(), .select_related() 넣어주는건 사실 없어도 동작은 되는데, 이 부분은 장고 django ORM에서 발생할 수 있는 N+1문제를 해결하기 위한거라고 배웠다. (불필요한 쿼리문이 수십 수백개 날라가는 문제를 해결하기 위한 것!)

 

이 경험을 통해서... 장고라는 프레임워크가 제공하는 기능이 있음에도 이걸 제대로 쓰지못해서 개고생을 했던걸 생각하니 앞으로는 프레임워크가 제공하는 기능과 그 사용법에 대해서 정확하게 파악하고 또 파고드는 자세를 가지고 개발에 임해야겠다는 생각을 했다. 정말 많은것을 배운 지점이었다!

 

시연영상

공모전 제출양식에 맞추기 위해 1분내외이다 ㅠ ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

 

프로젝트를 마무리하며 한마디

이번 프로젝트는 전 프로젝트처럼 10일동안 후다닥하고 끝나버리는 것이 아니라 두달간 진행되다보니 체력적, 멘탈적으로도 훨씬 소모가 컸던것같다. 그리고 그렇기때문에 배운점이 더 많았다고 생각한다. 이번 경험을 토대로 다음 프로젝트부터는 더 탄탄한 멘탈+계획을 기반으로 더 열심히 개발을 해봐야겠다. 화이팅!!