개발일지/스파르타코딩클럽

파이썬 장고 실무 심화 4주차 : 인스타그램 기능 클론(2)

마이구미+ 2023. 4. 25. 16:01

<실습>

- 게시글 상세/수정/삭제 serializers.py, views.py 그리고 포스트맨으로 테스트하기

게시글 상세

  • 먼저 views.py를 작성한다
# articles/views.py

...

class ArticleDetailView(APIView):
    def get(self, request, article_id):
        """상세게시글 조회"""
        article = Article.objects.get(id=article_id)
        serializer = ArticleSerializer(article)
        return Response(serializer.data, status=status.HTTP_200_OK)
  • 전체 게시글 보기랑 비슷한데, articles 변수에 all로 받아서 담는 게 아니라 get을 이용해 해당 게시글 id에 해당하는 글 하나를 가져오는 게 다르다
  • 시리얼라이저는 모든 필드를 가져오는 기본 아티클시리얼라이저를 쓴다
  • 저장하고 포스트맨으로 가서 실행시켜본다

articles/urls.py

  • article detail이라는 get 방식의 리퀘스트를 하나 만들고 url에는 urls.py에서 정한대로 게시글 아이디 번호를 넣어서 send 한다
  • 다 잘 나오는데 user에 이메일 형식이 아닌 아이디번호 형태로 나오는 게 아쉽다
  • 시리얼라이저를 수정한다
# articles/serializers.py

...
class ArticleSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()

    def get_user(self, obj):
        return obj.user.email

    class Meta:
        ...
    
...
  • ArticleListSerializer에 써놨던 것처럼 시리얼라이저메소드를 이용한 부분을 그대로 긁어서 class Meta 위에 추가해준다
  • 다시 포스트맨으로 가서 실행시켜 보면

  • user에 아이디번호가 아닌 이메일 형식으로 출력되는 것을 확인할 수 있다

게시글 수정

  • 2주차에 했던 것과 동일하게 해주면 된다
  • 일단 views.py만 수정하면 되는데
# articles/views.py

from rest_framework.generics import get_object_or_404
...

class ArticleView(APIView):
    ...


class ArticleDetailView(APIView):
    def get(self, request, article_id):
        ...

    def put(self, request, article_id):
        """게시글 수정"""
        article = get_object_or_404(Article, id=article_id)
        serializer = ArticleCreateSerializer(article, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  •  이렇게 하면 된다
  • 데이터베이스에 저장된 데이터 아이디가 5까지밖에 없는데 누군가 주소를 직접 쳐서 5가 아닌 데이터베이스에 없는 숫자를 입력한다면 에러창이 뜬다
  • 그걸 방지하기 위해 get_object_or_404를 쓴다
  • 시리얼라이저에는 원래 게시글 정보를 넣어주고, data로 수정할 내용을 담아 넣고 검증을 통과하면 수정한대로 저장된다
  • 포스트맨에서 확인해보자 아이디 4인 게시글을 수정하려고 한다 일단 데이터베이스에는 이렇게 써있다

  • 포스트맨에서 수정 리퀘스트를 새로 만들고 바꾸고 싶은 내용을 입력하고 send를 누른다

  • 잘 수정된 것 같다 다시 데이터베이스로 가보면

  • 아이디가 4인 게시글이 수정된 것을 확인할 수 있다 업데이트 시간도 잘 반영되었다
  • 그런데 여기 심각한 문제가 있다
  • 지금 코드상으로는 로그인한 사용자가 누구든 게시글을 수정할 수 있게끔 되어있다
  • 이걸 해당 게시글을 작성한 사용자만이 수정할 수 있도록 조건을 걸어줘야 한다
# articles/views.py

...

class ArticleView(APIView):
    ...


class ArticleDetailView(APIView):
    def get(self, request, article_id):
        ...

    def put(self, request, article_id):
        """게시글 수정"""
        article = get_object_or_404(Article, id=article_id)
        if request.user == article.user:
            ...
        else:
            return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
  • article 변수에 해당 article 정보를 담은 이후에 수정을 누른 user와 게시글 작성자가 일치하는지를 판별한다
  • 맞으면 코드를 이어서 진행하고, 틀리다면 권한이 없다는 메시지와 함께 403 코드를 띄운다
  • 보통 403 Forbidden 코드는 admin 계정이 아니거나 글 작성자가 아닌 경우에 띄우고 로그인이 안 되어 있는 경우에는 401 Unuthorized 코드를 띄운다
  • 이제 포스트맨으로 가서 다른 아이디로 로그인 한 후 4번 게시글을 수정하려고 하면

  • 작성한 메시지가 잘 나온다!

게시글 삭제

  • 2주차에 해봤듯 삭제는 간단하다
  • 다만 게시글 작성자와 로그인한 사용자가 일치하는지 조건문을 잊지 말고 잘 넣어줄 것!
# articles/views.py

...

class ArticleView(APIView):
    ...


class ArticleDetailView(APIView):
    def get(self, request, article_id):
        ...

    def put(self, request, article_id):
        ...

    def delete(self, request, article_id):
        """게시글 삭제"""
        article = get_object_or_404(Article, id=article_id)
        if request.user == article.user:
            article.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
        else:
            return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
  • 204코드를 쓰면 "글이 삭제되었습니다."하는 문구도 필요없다고 한다
  • 이대로 포스트맨으로 실행해보면

  • 삭제가 되지 않는다
  • 이 글 작성자가 아닌 다른 사용자로 아까 로그인했기 때문
  • 다시 이 글을 작성한 사용자로 로그인하고 실행하면

  • 원하는 상태코드가 나오는 것으로 보아 잘 삭제된 것 같다
  • 데이터베이스에 가보면

  • 원래 있었던 5번 게시글이

  • 삭제된 것을 확인할 수 있다

- 댓글 models-serializer-views

댓글 모델

  • 먼저 Comment 모델을 작성해보자!
# articles/models.py

...

class Article(models.Model):
    ...


class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return str(self.content)
  • 일단 user는 Article 모델에서 쓴 것처럼 User 모델에서 외래키로 가져왔고, article도 Article 모델에서 외래키로 가져왔다
  • 댓글 내용이 들어갈 content와 생성시간, 수정시간 필드를 넣었고, admin 페이지에서 댓글 조회할 때 목록에 댓글 내용이 보이도록 __str__함수를 만들어서 content 값을 리턴하도록 했다
  • 다음은 admin 페이지에 댓글 모델을 추가해준다
# articles/admin.py

...
from articles.models import Comment

...
admin.site.register(Comment)
  • 이후 서버를 실행해서 http://127.0.0.1:8000/admin/ 여기로 가면 Comment 카테고리가 생성된 것을 확인할 수 있다

  • add 눌러서 댓글을 생성해보면

  • __str__함수로 인해 댓글 내용이 목록에 바로 보이는 걸 확인할 수 있다

  • 댓글을 눌러보면 어떤 유저가 어떤 게시글에 썼는지 선택할 수 있게 되어 있다

댓글 조회

  • 일단 댓글이나 좋아요나 한 게시글에 대한 것이니까 url에 게시글 아이디가 있어야 할 것 같다 urls.py를 수정하자
# articles/urls.py

...

urlpatterns = [
    ...
    path('<int:article_id>/comment/', views.CommentView.as_view(), name='comment_view'),
    path('<int:article_id>/comment/<int:comment_id>/', views.CommentDetailView.as_view(), name='comment_detail_view'),
    path('<int:article_id>/like/', views.LikeView.as_view(), name='like_view'),
]
  • 댓글, 댓글 상세, 좋아요 url에 모두 게시글 아이디를 추가했다
  • 이제 serializers.py에 가서 CommentSerializer를 먼저 작성한다
# articles/serializers.py

...
from articles.models import Comment

...

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = "__all__"
  • 일단 게시글 기본 시리얼라이저와 동일하게 만들었다
  • 이제 views.py로 이동해서 작성한다
# articles/views.py

...
from articles.models import Comment
from articles.serializers import CommentSerializer

...

class CommentView(APIView):
    def get(self, request, article_id):
        """댓글 조회"""
        article = get_object_or_404(Article, id=article_id)
        comments = article.comment_set.all()
        serializer = CommentSerializer(comments, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
  • 댓글이 달린 해당 게시글을 get_object_or_404로 가져온다(강의에는 Article.objects.get(id=article_id)로 되어 있는데 전에 설명했던 것처럼 주소를 직접 쳐서 들어가는데 게시글 아이디가 없는 번호라면 에러창이 뜰 것 같아서 내 맘대로 get_object_or_404를 썼다 나중에 잘못되면 고치지 뭐..!)
  • comments 변수에는 해당 게시글의 댓글들을 모두 가져오는 코드인데 comment_set은 댓글에서 게시글을 역참조할 때(?)(아직 참조 역참조 그 방향?을 잘 모르겠다 여튼 역참조하는 거라고 한다) related_name이 기본값으로 commet_set 이라고 정해져 있다고 한다
  • 위에서 작성한 커멘트시리얼라이저를 serializer 변수에 넣고 매개변수에 댓글들이 들어간 comments 변수와 여러 쿼리셋을 불러올 때 쓰는 many=True를 넣었다
  • 이제 포스트맨으로 가보자

  • comments 콜렉션을 새로 만들어서 comments list라는 리퀘스트를 생성했다
  • 주소창에 아까 admin 페이지에서 댓글을 달았던 게시글 아이디가 3이어서 3을 넣어서 send했더니 댓글이 알맞게 잘 나왔다
  • 왠지 댓글리스트시리얼라이저를 새로 만들어서 필요한 필드만 남기고 user는 이메일, article은 제목이 나오게끔 고칠 것 같은 예감이 드는군

댓글 작성

  • 게시글 작성과 비슷하게 하면 된다
  • 일단 댓글작성시리얼라이저를 만들어 준다
  • 게시글 작성 때와 마찬가지로 user는 request.user로 저장할 거고, article id도 url에 있는 article id로 저장할 거다
  • 그러니 content만 사용자가 작성하면 된다
# articles/serializers.py

...

class CommentCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ("content",)
  • 여기서 주의할 점은 필드가 한 개만 있더라도 () 안에 넣어주고 ,(콤마)도 꼭 찍어줘야 한다 그래야 필드로 인식한다
  • 괄호 안에 안 넣거나 콤마를 안 찍으면 그냥 스트링으로 인식한다고 한다
  • 이제 다음은 views.py를 작성한다
# articles/views.py

...
from articles.serializers import CommentCreateSerializer

...

class CommentView(APIView):
    def get(self, request, article_id):
        ...

    def post(self, request, article_id):
        """댓글 작성"""
        serializer = CommentCreateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(user=request.user, article_id=article_id)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  • 게시글 작성 함수에서 그대로 복붙해서 시리얼라이저에 CommentCreateSerializer로 수정하고, 시리얼라이저 저장할 때 매개변수로 article_id=article_id를 추가해줬다

댓글 수정

  • 댓글 수정도 게시글 수정이랑 비슷하다
  • views.py에 게시글 수정 함수의 내용을 그대로 복사해서 댓글 수정 함수에 붙여준다
# articles/views.py

...
from articles.serializers import CommentCreateSerializer

...

class CommentDetailView(APIView):
    def put(self, request, comment_id, article_id):
        """댓글 수정"""
        comment = get_object_or_404(Comment, id=comment_id)
        if request.user == comment.user:
            serializer = CommentCreateSerializer(comment, data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data, status=status.HTTP_200_OK)
            else:
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        else:
            return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
  • 게시글 수정이랑 비슷하다 여기선 article 대신 comment를 불러온다
  • 댓글 작성자와 로그인한 사용자가 일치하면 수정을 진행한다
  • 시리얼라이저도 CommentCreateSerializer로 바꿔주고, 원래 정보인 comment 매개변수에 넣고, 수정한 데이터가 들어가게끔 data=request.data로 쓴다
  • 저장할 때는 이미 유저아이디와 아티클아이디가 저장이 되어 있기 때문에 추가로 매개변수로 넣어주지 않아도 된다
  • 함수에 article_id가 들어간 곳이 없는데 매개변수로 있는 이유는 url에 article_id가 들어가기 때문이다
  • 댓글 수정하는 데에 아티클아이디가 꼭 필요한 건 아닌데 url의 통일성을 위해 넣어주었다
  • 이제 포스트맨에서 테스트를 해보면

  • 댓글이 잘 수정되는 것을 확인할 수 있다
  • 댓글 수정 리퀘스트를 만들 때 Header에 유저 정보 넣는 것을 잊지 말아야 한다

  • 댓글 작성자가 아닌 다른 유저 정보를 넣으면 권한이 없다고 뜰 것이다
  • 댓글 리스트를 다시 조회해보면

  • 댓글이 수정된 것을 확인할 수 있다

댓글 삭제

  • 게시글 삭제 함수에서 복붙하면 된다
# articles/views.py

...

class CommentDetailView(APIView):
    def put(self, request, comment_id, article_id):
        ...

    def delete(self, request, comment_id, article_id):
        """댓글 삭제"""
        comment = get_object_or_404(Comment, id=comment_id)
        if request.user == comment.user:
            comment.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
        else:
            return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
  • article만 comment로 바꿔주면 된다
  • 포스트맨에서 실행해보면

  • 댓글이 잘 삭제되는 걸 볼 수 있다

  • 댓글리스트에도 해당 댓글이 없어진 것을 확인할 수 있다