[PetMo] 댓글, 대댓글 기능 구현하기

silverKi·2023년 7월 23일
0

커뮤니티, 게시판, 블로그,SNS 등 에서는 client 간 또는 client 와 관리자(admin) 간에 온라인 상에서 원활한 의사소통을 위해 댓글과 대댓글 기능이 사용된다.

일단 먼저,일의 우선순위에 따라서 구현해야 할 댓글 기능부터 하고 대댓글은 나중에 생각하기로 했다.

댓글기능을 다 완성하고, 대댓글을 구현하고 싶다는 욕심이 생겼다. 그래서 나는 대댓글에 도전해보기로 결정하였고, 대댓글 구현에 성공했다.


처음에는 댓글 기능만 가진 Comment model을 구현하였다.

댓글기능을 구현한 상태에서 대댓글 기능을 가진 또 다른 모델을 만드는 것으로 생각했는데,

댓글과 대댓글은 기본적으로 댓글의 기능을 가지고 있으며, 차이점은 댓글의 부모가 게시글이라면 대댓글의 경우 부모가 댓글이라는 서로 종속된 관계다. 이 둘은 대부분의 필드가 동일하다는 점에서 유사하다고 생각했다.

그래서 나는 대댓글의 기능을 가지는 또 다른 모델을 만들기 보다는, 이미 있는 댓글 모델에 댓글과 대댓글을 구분하는 toggle field(=parent_comment)를 추가하였다.


class Comment(CommonModel):
    user=models.ForeignKey(
        "users.User",
        on_delete=models.CASCADE,
        help_text="댓글 작성자의 pk"
    )    
    post=models.ForeignKey(
        "posts.Post",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name="post_comments",
        help_text="댓글이 달린 게시글의 pk"
    )
    content=models.CharField(#댓글 작성
        max_length=150,
        blank=True,
        null=True,
        help_text="댓글 내용"
    )
    parent_comment=models.ForeignKey(#parent_comment에 값이 있으면 대댓글, 값이 없으면 댓글 
        "self",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name="replies",
        help_text="댓글인지 대댓글인지 구분 토글, if parent_comment==Null: 댓글, else: 대댓글"
    )
   
    def __str__(self):
        return f"{self.user} - {self.content}"

위 처럼 Comment Model을 구성하고, 데이터를 특정 형식으로 변환또는 직렬화 및 역 직렬화 하기 위해 Serializer를 구성하였고 마지막으로 APIView를 구성하였다.

class CommentSerializers(ModelSerializer):
    user=SimpleUserSerializer(read_only=True)
    class Meta:
        model=Comment
        fields=( 
            "pk",
            "parent_comment",
            "post",  
            "user",
            "content",
            "createdDate",
            "updatedDate"
        ) 
class ReplySerializers(ModelSerializer):
    children=serializers.SerializerMethodField()
    user=SimpleUserSerializer(read_only=True)
    
    class Meta:
        model=Comment
        fields=(
            "id",
            "parent_comment",
            "post",  
            "user",
            "content",
            "createdDate",
            "updatedDate",
            "children"
        )

    def get_children(self, obj):
        children=Comment.objects.filter(parent_comment=obj.id).order_by('createdDate')
        if not children.exists():
            return None
        serializer=ReplySerializers(children, many=True,)
        return serializer.data
    
    def get_coments_count(self, obj):
        return obj.coments_count

ReplySerializers 클래스는 Comment 모델을 기반으로 한 댓글의 대댓글을 직렬화하는 클래스이다. children field는 get_children()를 통해 대댓글들을 직렬화한 결과를 반환하기 위해 작성했다.

get_children()는 해당 댓글의 대댓글들을 가져와서 직렬화한 후 반환한다. 이를 통해서 댓글에 대한 대댓글들을 client에게 제공한다.

class Comments(APIView):#등록되어진 모든 댓글
    def get(self,request):
        all_comments=Comment.objects.filter(parent_comment=None).order_by('-createdDate')
        serializer=ReplySerializers(all_comments, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def post(self, request):
        content=request.data.get("content")
        post_id=request.data.get("post")
        parent_comment_id = request.data.get("parent_comment", None)#부모댓글 정보 #부모댓글 정보가 전달 되지 않을 경우, None할당(=댓글)
        
        try:
            post=Post.objects.get(id=post_id)
        except Post.DoesNotExist:
            return Response({"error":"해당 게시글이 존재하지 않습니다."}, status=status.HTTP_404_NOT_FOUND)

        if parent_comment_id is not None:#대댓글
            try:
                parent_comment = Comment.objects.get(id=parent_comment_id)
                print(parent_comment)
            except Comment.DoesNotExist:
                return Response({"error":"해당 댓글이 존재하지 않습니다."}, status=status.HTTP_404_NOT_FOUND)
        
            comment=Comment.objects.create(
                content=content,
                user=request.user,
                post=parent_comment.post,
                parent_comment=parent_comment
            )
            serializer = ReplySerializers(comment)
            return Response(serializer.data, status=status.HTTP_201_CREATED)           
        else: #댓글
            print("댓글")
            serializer=CommentSerializers(data=request.data)
            if serializer.is_valid():
                comment=serializer.save(
                    post=post,
                    user=request.user,
                )
                serializer=CommentSerializers(comment)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

  class CommentDetail(APIView):# 댓글:  조회 생성, 수정, 삭제(ok)
    
    def get_object(self, pk):
        try:
            return Comment.objects.get(pk=pk)
        except Comment.DoesNotExist:
            raise NotFound

    def get(self, request, pk):#댓글의 pk로 접속시 해당 댓글이 갖고 있는 대댓글도 같이 조회함
        comment=self.get_object(pk=pk)
        serializer=ReplySerializers(
            comment,
            context={"request":request},                                    
        )
        return Response(serializer.data, status=status.HTTP_200_OK)
  
    def put(self, request,pk): 
        #예외: 댓글 수정시에 해당 댓글이 있는지 우선 확인해야
        
        comment=self.get_object(pk=pk)
        
        if comment.parent_comment:
            if comment.parent_comment.post.id!=comment.post.id:
                return Response({"error":"해당 댓글이 게시글에 존재하지 않습니다."}, status=status.HTTP_400_BAD_REQUEST)
        serializer=CommentSerializers(#before : commentDetailSerializers
            comment, 
            data=request.data,
            partial=True
        )
        if serializer.is_valid():
            updated_comment=serializer.save()
            return Response(CommentSerializers(updated_comment).data, status=status.HTTP_202_ACCEPTED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request,pk):
        #댓글 삭제시 대댓글도 삭제 
        comment=self.get_object(pk)
        
        if comment.user!=request.user:
            raise PermissionDenied
        comment.delete()
        return Response(status=status.HTTP_200_OK)

반환되는 JSON-Data에서 parent_comment,children field를 주목하길 바란다.

parent_comment==null인 경우 댓글, 만약 null이 아니고 숫자인 경우 해당 숫자는 댓글의 pk를 의미.
(댓글의 부모는 게시글(null), 대댓글의 부모는 댓글(댓글 pk))

댓글은 대댓글을 가질 수도 있고 없을수도 있다.

만약, 댓글에 대댓글이 없다면 children field는 null을 가지고, 대댓글이 존재한다면 대댓글 반환할 것.
//대댓글을 갖고 있지 않은 댓글 조회 
{
    "id": 60,
    "parent_comment": null,#댓글인지 대댓글인지
    "post": 63,
    "user": {
        "username": "momo2",
        "profile": "https://www.lifewithcats.tv/wp-content/uploads/2011/04/Jumping-Cat.jpg",
        "regionDepth2": "연수구",
        "regionDepth3": "송도동"
    },
    "content": "댓글2",
    "createdDate": "2023-06-07T15:21:28.389334+09:00",
    "updatedDate": "2023-06-07T15:21:28.389397+09:00",
    "children": null #대댓글이 없는 댓글
}


//대댓글을 갖고 있는 댓글 조회
{
    "id": 59,
    "parent_comment": null,
    "post": 63,
    "user": {
        "username": "momo2",
        "profile": "https://www.lifewithcats.tv/wp-content/uploads/2011/04/Jumping-Cat.jpg",
        "regionDepth2": "연수구",
        "regionDepth3": "송도동"
    },
    "content": "댓글1",
    "createdDate": "2023-06-07T15:21:16.189655+09:00",
    "updatedDate": "2023-06-07T15:21:16.189714+09:00",
    "children": [
        {
            "id": 61,
            "parent_comment": 59,
            "post": 63,
            "user": {
                "username": "momo2",
                "profile": "https://www.lifewithcats.tv/wp-content/uploads/2011/04/Jumping-Cat.jpg",
                "regionDepth2": "연수구",
                "regionDepth3": "송도동"
            },
            "content": "대댓글1",
            "createdDate": "2023-06-07T15:21:38.306141+09:00",
            "updatedDate": "2023-06-07T15:21:38.306199+09:00",
            "children": null
        }
    ]
}

느낀점 >

댓글까지 구현하는건 별로 어렵지 않았다.. 하지만 댓글에 대댓글을 계층적으로 구현하는 부분이 까다로웠다.

구현하면서 내가 까다롭다고 생각한 부분은 댓글에 대댓글 1개 생성하고 해당 댓글을 조회하면 댓글에 종속된 대댓글1개와 만든 대댓글1개 총 2개가 조회되는 경우 였다. 아마 내가 재귀로 구현해서 그런게 아닐까 생각했고 get()으로 모든 객체를 반환하는게 아니라 filter()를 이용하여 조건을 걸고 댓글인 경우만 반환하였다. (대댓글은 댓글에 종속되어 있기 때문)

그리고 댓글이 삭제되는 경우 어떻게 처리할 것인지도 생각이 필요한 부분이엿다.

나는 댓글이 삭제되는 경우 대댓글도 삭제되게 구현하였는데, 댓글이 삭제되는 경우 해당 댓글만 삭제되고 삭제된 댓글에 달린 대댓글들을 유지하게 할 수도 있을 것이다. 이 경우에는 재귀함수로 구현하기 보다는 아마 대댓글 모델을 만들어서 구현하면 되지 않을까? 라고 생각이 든다.

나중에 시간이 된다면, 지금 내가 한 방식을 변경해서 Comment Model과 ReComment Model을 만들어서 도전해 보고 싶다.

마무리하자면, 계층 구조로 구현이 된 결과를 보니 너무 너무 뿌듯했고,

장고 serilazer의 단점과 장점을 다 겪어본 경험이였다.!

profile
아악! 뜨거워!!

0개의 댓글