[ PROJECT ] 카카오프렌즈샵 클론코딩 - #05 게시물 목록조회 / 게시물 상세조회 기능 구현!

Hailee·2020년 12월 22일
0

[ PROJECT ]

목록 보기
9/16
post-thumbnail

Logging 코드 추가

우선, 어떻게 돌아가고있는지 확인하기 위해서
쿼리를 콘솔에 출력하도록 settings.py 내 로깅 관련 코드 추가해주기!
(서버 실행시 코드 실행때마다 쿼리가 찍히는 설정)

LOGGING = {
    'disable_existing_loggers': False,
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'DEBUG',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': False,
        },
    },
}

게시물 목록조회

http -v localhost:8000/board/index 		--> 접속!

애증의 목록조회.... 후.....

앞서 작성했던 엄청나게 길고, 못생겼던 내 코드를 기억하는 사람..?
list comprehension을 사용하니까 코드의 양이 1/3로 줄어들었다..!!!(엄청난 충격)
도대체가 어떻게 응용해서 써야하는지 감이 오질 않아서 우선 무식하게 for문으로 돌리고.. 새로운 배열에 append하는 식으로 했었던 어제.
나도 간결한 코드 짜고싶은데ㅜㅜ 어떻게 해야할지 도저히 감이 안왔다.

리뷰받은대로 list comprehension을 사용하니까 사용되는 쿼리 수도 반이상 줄고, 코드도 반 이상 줄어들었다.
확실히 속도 측면에서도, 기능이 작동하면서 변수를 생성하고 하는 부분에서도 훨씬 효율적인 코드가 되었다!!

class BoardListView(View):
    def check_comment(self, board_id):
        if Comment.objects.filter(board_id = board_id).exists() : 
            data = Comment.objects.filter(board_id = board_id).order_by('-created_at').first()
            comment_data = {
                "writer"    : data.writer.nickname,
                "content"   : data.content
            }
        else:
            comment_data = ''
        return comment_data

    def get(self, request):
        try: 
            board_list = [{
                "id"            : board.id,
                "uploader"      : board.uploader,
                "theme"         : board.theme,
                "title"         : board.title,
                "content"       : board.content,
                "created_at"    : board.created_at,
                "board_images"  : list(BoardImage.objects.filter(board_id = board.id).values('image_url')),
                "board_likes"   : BoardLike.objects.filter(board_id = board.id).count(),
                "comment"       : BoardListView.check_comment(self,board.id)
            } for board in Board.objects.all()]  
            return JsonResponse({'message' : 'SUCCESS', 'board_list' : board_list}, status = 200)
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)
        except ValueError:
            return JsonResponse({'message' : 'DECODE_ERROR'}, status = 400)
        except Board.DoesNotExist:
            return JsonResponse({'message' : 'NO_EXIST_DATA'}, status = 500)
        except Board.MultipleObjectsReturned:
            return JsonResponse({'message' : 'TOO_MANY_DATA'}, status = 500)
        

날 너무 힘들게 했던.. 대표 댓글 하나
이건 멘토님이랑 상담한 결과, 좋아요 수를 기준으로 처리하는 것이 더 위험하다는 결론이 났다.

1. 우선 카카오프렌즈샵이 가장 최신 댓글을 기준으로 표출하고있다는 점, (몰랐다는게 더 충격)
2. 좋아요 수가 똑같은, 가장 좋아요 수가 많은 댓글이 2개 이상이 있다면? 대표 댓글은 누가?

이러한 이유로 그냥 가장 최신의 댓글을 보여주기로 했다. (밀리초까지 같은 댓글이 존재하기란 하늘의 별따기일것 같아서.. 하핳)

하지만 이미 list comprehension으로 작성해둔 상태이기 때문에, comment를 하나로 추출하는 과정이 너무 힘들었다.

board_list = [{
                "id"            : board.id,
                "uploader"      : board.uploader,
                "theme"         : board.theme,
                "title"         : board.title,
                "content"       : board.content,
                "created_at"    : board.created_at,
                "board_images"  : list(BoardImage.objects.filter(board_id = board.id).values('image_url')),
                "board_likes"   : BoardLike.objects.filter(board_id = board.id).count(),
                "comment"  :  [{
                                    "id"      : comment.id,
                                    "content" : comment.content,
                                } for comment in Comment.objects.filter(board_id=board.id).order_by('-created_at').first() if comment != None]
            } for board in Board.objects.all()]  

👆🏻 위 코드의 문제는?
order_by(), earliest(), latest()를 사용하면, QuerySet이 아닌, class 로 타입이 변환된다는 것!!!!!!!
dJango QuerySet API reference 👈🏻 꼭 읽어봐라

알다시피, for loop이 동작하기 위해서는 iterable한 객체여야만 한다.
하지만 class타입은 그렇지 않기 때문에, for 문을 사용할 수 없다는 딜레마에 빠지고 만다.
그렇다고 comment 추출 관련 코드만 바깥쪽으로 빼두자니, board_id를 받아갈 수 없게 된다.
(list comprehension내부에서만 board_id를 받을 수 있기 때문!!!!)

comment 관련 dictionary내부에 orm을 통해 객체를 직접 호출하자니 자꾸 None 타입이라고 하는것이 아닌가.. ㅠㅠ


생각할 수 있는 모든 방법을 생각했는데도, 막상 마무리 할 때가 되어가면 노답인 것을 발견하며 멘탈이 갈리고 서러워지기 시작했을 즈음에...
고민에 고민에 고민을 한 결과, 멘토님께 질문드렸고(사실 지금 생각하면 이미 그 시점에 여러 멘토분들께 질문드려서 상당히 민망한 상황이었지만, 멘탈이 갈려서 아무 생각이 었었다) method화 하는 것으로 했다.

self, 매개변수를 잘 입력해주었는데도 자꾸 missing 1 required positional argument: 'board_id'라고 하는 것이 아닌가.. 사람 서럽게

알고보니 파이썬에서의 메서드는 선언, 호출부 모두 self를 꼭 적어주어야하는데
바보같은 내가 호출할 때 board_id만 매개변수로 입력했기 때문에, self가 없다고 인식되어 positional argument가 없다고 했던 것..!!! (바보멍청이!!!!)

아 그리고 공용 메서드로 사용할 것은 아니지만, 그래도 get 메서드 내부보다는 바깥에 위치하는 것이 더 좋다고 하셔서
같은 클래스 하위, get메서드의 바깥 부분으로 옮겨서 위치시켰다.


내가 만들었지만 너무 고생하고 예쁜 코드였어.. 🥺 이제 RESTful 측면에서 해야하는, 상세조회 기능구현! 한번 코딩해볼까?!?!

번외) board_image들 좀 더 예쁘게 다듬어보기!


지금은 이렇게.. 너무 보기 흉하게 출력된다.
난 프론트에서 board_images[0] 이런식으로 바로 꺼내서 쓸 수 있게 해주고 싶은데... 딕셔너리로 된 리스트가 아니라 값만 담겨있는 리스트로 보내주고 싶은데!!!

board_list = [{
                "id"            : board.id,
                "uploader"      : board.uploader,
                "theme"         : board.theme,
                "title"         : board.title,
                "content"       : board.content,
                "created_at"    : board.created_at,
                "board_images"  : list(BoardImage.objects.filter(board_id = board.id).values('image_url')),     <---
                "board_likes"   : BoardLike.objects.filter(board_id = board.id).count(),
                "comment"       : BoardListView.chec

지금은 values 메서드를 사용하기 때문에, 딕셔너리 형태로 출력이 된다.

board_list = [{
                "id"            : board.id,
                "uploader"      : board.uploader,
                "theme"         : board.theme,
                "title"         : board.title,
                "content"       : board.content,
                "created_at"    : board.created_at,
     --->       "board_images"  : list(BoardImage.objects.filter(board_id = board.id).values_list('image_url', flat=True)),
                "board_likes"   : BoardLike.objects.filter(board_id = board.id).count(),
                "comment"       : BoardListView.check_comment(self, board.id)
            } for board in Board.objects.all()]  

dJango 공식문서로 배우는 values_list() 메서드!
👆🏻 이 문서를 확인해보니 QuerySet타입의 딕셔너리로 받는게 아닌, 원하는대로 값만 추출할 수도 있는 것!!!!
magical values_list()!!!!

이제 프론트에서 좀더 쓰기쉽게 보내줄 수 있따 드디어!!! ><


게시물 상세조회

http -v localhost:8000/board/feed/3                				--> board 접속!
http -v localhost:8000/board/comment board_id==3 sort==-created_at		--> comment 접속 !


멘토님께 게시물 목록조회 시 어느 기준을 가진 어떤 댓글 하나를 표출해줄 것인지에 대해 질문하던 도중,
상세조회는 저렇게... 데이터의 정렬 상태에 따라 3가지 방식으로 나뉘어진다는 것을 깨달았다. (하하하하 구현하면서 깨달아요-)

데이터 중심으로 진행되는 웹 개발! RESTful 한 API 구현하기 ~~~


자, 데이터 중심으로 web이 기능한다는 것은 알겠어. 근데 이게 path parameter, query string 중 무엇인지?!?

순간 과거에 post 방식으로 통신했던 시절이 스쳐지나가면서.. 아.. 이게 그런건가보다..
사용자는 URI를 통해 아무 것도 모르지만.. 프-백은 통신하고있는.. 그런건가보다.. 라고 생각했는데

어쨌든 조건에 맞게 filtering 하는 상황이기 때문에, 현재 상황은 query string을 사용하는 것!

확실히 하고 싶다면, 개발자도구 창에서 Network 탭을 확인하면 된다.
내가 댓글 정렬 조건을 선택할 때마다 orderby의 값이 바뀌는 것을 확인할 수 있다.

이제 이걸 코드로 구현하기만 하면... (프론트랑 합의해서 paging은 하지 않기로 해따.. 2차를 기다린다 👩‍💻 👩‍💻)

# 2. 게시물 상세조회
class GetBoardView(View):
    def get_comments(self, request, board_id):
        sort = request.GET.get('sort', None)
        if Comment.objects.filter(board_id = board_id).exists(): 
            data = Comment.objects.filter(board_id=board_id).order_by(sort).values()
            comment_data = [{
                "id"        : comment['id'],
                "writer"    : Member.objects.get(id=comment['writer_id']).nickname,
                "content"   : comment['content'],
                "is_liked"  : CommentLike.objects.filter(comment_id = comment['id'], is_like = 1).count(), 
                "created_at": comment['created_at']
            } for comment in data]
        else:
            comment_data = ''
        #print(comment_data)
        return comment_data

    def get(self, request):
        try:
            board_id = request.GET.get('board_id', None)
            orderby = request.GET.get('orderby', None)

            data = Board.objects.filter(id = board_id)[0]

            # 리턴보낼 board_data 준비
            board_data = {
                "id"            : data.id,
                "uploader"      : data.uploader,
                "theme"         : data.theme,
                "title"         : data.title,
                "content"       : data.content,
                "created_at"    : data.created_at,
                "board_images"  : list(BoardImage.objects.filter(board_id = board_id).values_list('image_url', flat=True)),
                "board_likes"   : BoardLike.objects.filter(board_id = board_id).count(),
                "comment"       : GetBoardView.get_comments(self, request, board_id)
            }
            return JsonResponse({'message' : 'SUCCESS', 'board_list' : board_data}, status = 200)
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)
        except ValueError:
            return JsonResponse({'message' : 'DECODE_ERROR'}, status = 400)
        except Board.DoesNotExist:
            return JsonResponse({'message' : 'NO_EXIST_DATA'}, status = 500)
        except Board.MultipleObjectsReturned:
            return JsonResponse({'message' : 'TOO_MANY_DATA'}, status = 500)

원래는 이렇게, 앞서 전체 목록을 조회한 것처럼 댓글 관련 부분을 메서드화 해서 진행하려고 했는데,
지금 상황이라면 댓글만 최신, 과거 순으로 정렬되는 것이 아닌, 매번 board까지 다시 호출해오게 되는 상황이 된다.

댓글만 재정렬하고 싶은데 매번 게시물까지 다시 호출해오게 되면 불필요한 작업이 계속해서 반복되는 것!

원래는 여기까지만 작성하고 commit한 뒤 잠들었었는데, 꿈속에서 계속 댓글을 재정렬할 때, 댓글을 입력할 때마다 페이지가 다시 호출되는 그런 꿈을 꿨다.


안그래도 멘토님께 질문드려보니 게시물, 댓글 호출은 그냥 별개의 url로 호출하는 것이 맞다고 하셨다.
상세조회 첫 로딩시에는 2번의 url요청이 오지만, 그 이후로는 댓글에 대한 url호출만 오면 되기 때문!

그래서 결국 이렇게 두개의 class로 분리하기로 했다. 한 클래스 내에는 어짜피 HTTP 메서드를 종류별로 하나밖에 못쓰기 때문!

class GetBoardView(View):
    def get(self, request, board_id):
        try:
            data = Board.objects.filter(id = board_id)[0]

            # 리턴보낼 board_data 준비
            board_data = {
                "id"            : data.id,
                "uploader"      : data.uploader,
                "theme"         : data.theme,
                "title"         : data.title,
                "content"       : data.content,
                "created_at"    : data.created_at,
                "board_images"  : list(BoardImage.objects.filter(board_id = board_id).values_list('image_url', flat=True)),
                "board_likes"   : BoardLike.objects.filter(board_id = board_id).count()
            }
            return JsonResponse({'message' : 'SUCCESS', 'board_list' : board_data}, status = 200)
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)
        except ValueError:
            return JsonResponse({'message' : 'DECODE_ERROR'}, status = 400)
        except Board.DoesNotExist:
            return JsonResponse({'message' : 'NO_EXIST_DATA'}, status = 500)
        except Board.MultipleObjectsReturned:
            return JsonResponse({'message' : 'TOO_MANY_DATA'}, status = 500)        

# 2-2. 게시물 상세조회 - 댓글 목록 호출
class GetCommentView(View):
    def get(self, request):
        try:
            board_id    = request.GET.get('board_id', None)
            sort        = request.GET.get('sort', None)

            if Comment.objects.filter(board_id = board_id).exists(): 
                data = Comment.objects.filter(board_id=board_id).order_by(sort).values()
                comment_data = [{
                    "id"        : comment['id'],
                    "writer"    : Member.objects.get(id=comment['writer_id']).nickname,
                    "content"   : comment['content'],
                    "is_liked"  : CommentLike.objects.filter(comment_id = comment['id'], is_like = 1).count(), 
                    "created_at": comment['created_at']
                } for comment in data]
            else:
                comment_data = ''
            return JsonResponse({'message' : 'SUCCESS', 'comment_data' : comment_data}, status = 200)
        except KeyError:
            return JsonResponse({'message' : 'KEY_ERROR'}, status = 400)
        except ValueError:
            return JsonResponse({'message' : 'DECODE_ERROR'}, status = 400)
        except Board.DoesNotExist:
            return JsonResponse({'message' : 'NO_EXIST_DATA'}, status = 500)
        except Board.MultipleObjectsReturned:
            return JsonResponse({'message' : 'TOO_MANY_DATA'}, status = 500)   

즐거운.... 코딩...
결국 좋아요가 많은 순으로 댓글 정렬을 하는 건 성공하지 못했다.

annotation을 사용하면 된다는데, 이전에 stackoverflow에 질문했을 때에도 다들 너무나도 쉽게 대답해준건데! 나는 왜 이렇게 힘든걸까
내가 너무 바보처럼 느껴지는 한 주인것 같다.

profile
웹 개발 🐷😎👊🏻🔥

0개의 댓글