개발기록장

[Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (RESTful API) 본문

TIL/Django

[Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (RESTful API)

yangahh 2021. 2. 9. 16:02

 

[이전 글]

[Django] 인스타그램 클론 코딩(3) - Authorization Decorator 만들고 활용하기

 

[Django] 인스타그램 클론 코딩(3) - Authorization Decorator 만들고 활용

[이전 글] devvvyang.tistory.com/41 [Django] 인스타그램 클론 코딩(2) - 로그인 기능 구현 인스타그램은 전화번호, 사용자 이름, 이메일 중 하나와 패스워드로 로그인을 할 수 있다. 이를 views.py에 LoginView..

devvvyang.tistory.com

[다음 글]

[Django] 인스타그램 클론 코딩(5) - 좋아요(Like) 기능 구현

 

[Django] 인스타그램 클론 코딩(5) - 좋아요(Like) 기능 구현

[이전 글] [Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (+ RESTful API에서 Update에는 무슨 메소드를 써야하는가...) [Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (+ RESTful API..

devvvyang.tistory.com

 

 

1. 구현해야 할 기능

- 게시물(post)과 관련된 기능을 처리할 app생성 (post라는 이름으로 생성함)

- 게시물 및 댓글 기능구현을 위한 model 생성

- RESTful하게 게시글 및 댓글 CRUD 구현

 

 

2. post/models.py

from django.db   import models

from user.models import User


# 게시물 관련 테이블
class Post(models.Model):
    user        = models.ForeignKey('user.User', on_delete=models.CASCADE) 
    image_url   = models.URLField(max_length=500)
    content     = models.TextField(null=True)
    created_at  = models.DateTimeField(auto_now_add=True)
    updated_at  = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'posts'


# 댓글 관련 테이블
class Comment(models.Model):
    user       = models.ForeignKey('user.User', on_delete=models.CASCADE)
    post       = models.ForeignKey('Post', on_delete=models.CASCADE)
    content    = models.CharField(max_length=500)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'comments'

 

  • Post 클래스는 게시물에 대한 모델로 작성자를 의미하는 user, 이미지의 url 주소가 들어갈 image_url, 게시물의 내용이 들어갈 content로 구성하였다.
  • Comment 클래스는 댓글에 관련 모델로, Post와 마찬가지로 댓글 작성자를 의미하는 user, 어느 게시물에 달린 댓글인지를 나타내는 post, 그리고 댓글 내용이 들어갈 content로 구성하였다.
  • 하나의 게시물에는 여러개의 댓글이 달릴 수 있으므로 1:N 관계가 성립한다. Comment의 post필드에 ForeignKey 옵션으로 1:N 관계를 구현하였다.
  • 한 명의 유저가 여러 게시물 또는 여러 댓글을 작성할 수 있으므로 User와 Post, User와 Comment 모두 1:N 관계를 성립하며, 이를 모두 ForeignKey 옵션으로 구현하였다.

 

3-1. post/views.py (게시물 관련 CRUD)

import json
from json.decoder import JSONDecodeError

from django.views import View
from django.http  import JsonResponse

from .models      import Post, Comment
from user.models  import User
from user.utils   import login_decorator

class PostView(View):
    @login_decorator
    def post(self, request):
        data      = json.loads(request.body)
        user      = request.user
        image_url = data.get('image_url', None)
        content   = data.get('content', None)
        
        # KEY_ERROR check
        if not image_url:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)
            
        post = Post.objects.create(
            user      = user,
            image_url = image_url,
            content   = content
        )
        return JsonResponse({'message': 'SUCCESS'}, status=200)

    def get(self, request):
        post_list = [{
                'user'      : post.user.name,
                'image_url' : post.image_url,
                'content'   : post.content,
                'created_at': post.created_at,
                'updated_at': post.updated_at,
            } for post in Post.objects.all()
        ]
        return JsonResponse({'data': post_list}, status=200)


class PostDetailView(View):
    def get(self, request, post_id):
        # valid post check
        if not Post.objects.filter(id=post_id).exists():
            return JsonResponse({'message': 'INVALID_POST'}, status=404)
        
        context = {}
        
        # post 정보
        post = Post.objects.get(id=post_id)
        context['user']       = post.user.name
        context['image_url']  = post.image_url
        context['content']    = post.content
        context['created_at'] = post.created_at

        # comment 정보
        comments = Comment.objects.filter(post=post)
        if comments:
            comment_list = [{
            	'user'      : comment.user.name,
                'content'   : comment.content,
                'created_at': comment.created_at
            	} for comment in comments
            ]
            context['comment_list'] = comment_list

        return JsonResponse({'data': context}, status=200)
    
    # update 
    @login_decorator
    def post(self, request, post_id):
        try: 
            data = json.loads(request.body)

            # valid post check
            if not Post.objects.filter(id=post_id).exists():
                return JsonResponse({'message': 'INVALID_POST'}, status=404)
            
            post = Post.objects.get(id=post_id)

            # valid user check
            if post.user != request.user:
                return JsonResponse({'message': 'INVALID_USER'}, status=400)

            post.image_url = data.get('image_url', post.image_url)
            post.content   = data.get('content', post.content)
            post.save()
            return JsonResponse({'message': 'SUCCESS'}, status=200)
        
        except JSONDecodeError:
            return JsonResponse({'message': 'REQUEST_BOBY_DOES_NOT_EXISTS'}, status=400)
    
    @login_decorator
    def delete(self, request, post_id):
        # valid post check
        if not Post.objects.filter(id=post_id).exists():
            return JsonResponse({'message': 'INVALID_POST'}, status=400)
        
        post = Post.objects.get(id=post_id)

        # valid user check
        if post.user != request.user:
            return JsonResponse({'message': 'INVALID_USER'}, status=401)

        post.delete()
        return JsonResponse({'message': 'SUCCESS'}, status=200)

  • PostView
    • post 메소드 : 게시물 작성(Create) 기능 담당. 로그인이 필수인 기능이기 때문에 login_decorator로 유저의 권한을 검증
    • get 메소드 : 인스타그램에 들어가면 로그인하지 않아도 볼 수 있는 전체 게시물 데이터를 가져오는 기능을 담당
  • PostDetailView - post_id를 url에서 받아서 처리할 수 있는 기능을 모아 둔 view.
    • get 메소드 : url에서 받은 post_id와 일치하는 하나의 게시물에 대한 세부 데이터를 보여주는(Read) 기능을 담당. 댓글(comment)이 있다면 comment까지 보여준다.
    • post 메소드 : url에서 받은 post_id와 일치하는 하나의 게시물을 수정(Update)하는 기능을 담당. 로그인이 필수일 뿐만 아니라 게시물을 작성한 유저만 수정이 가능해야 하기 때문에 post.user와 request.user가 일치하는지 체크하는 로직이 필요하다.
    • delete 메소드 : url에서 받은 post_id와 일치하는 하나의 게시물을 삭제(Delete)하는 기능을 담당. Update와 마찬가지로 게시물을 작성한 유저만 삭제가 가능해야 하기 때문에 이를 체크하는 로직이 필요하다.

 

 

3-2. post/views.py (댓글 관련 CRUD)

class CommentView(View):
    @login_decorator
    def post(self, request):
        data      = json.loads(request.body)
        user      = request.user
        post_id   = data.get('post', None)
        content   = data.get('content', None)
        
        # KEY_ERROR check
        if not (post_id and content):
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)
        
        # valid post check
        if not Post.objects.filter(id=post_id).exists():
            return JsonResponse({'message': 'INVALID_POST'}, status=400)

        Comment.objects.create(
            user      = user,
            post      = Post.objects.get(id=post_id),
            content   = content
        )
        return JsonResponse({'message': 'SUCCESS'}, status=200)
        

class CommentDetailView(View):
    # update
    @login_decorator
    def post(self, request, comment_id):
        try:
            data    = json.loads(request.body)
            content = data.get('content', None)

            # KEY_ERROR check
            if not content:
                return JsonResponse({'message': 'KEY_ERROR'}, status=400)
            
            comment = Comment.objects.get(id=comment_id)
            
            # valid user check
            if comment.user != request.user:
                return JsonResponse({'message': 'INVALID_USER'}, status=401)

            comment.content = content 
            comment.save()
            return JsonResponse({'message': 'SUCCESS'}, status=200)

        except JSONDecodeError:
            return JsonResponse({'message': 'REQUEST_BOBY_DOES_NOT_EXISTS'}, status=400)

    @login_decorator
    def delete(self, request, comment_id):
        # vaild comment check
        if not Comment.objects.filter(id=comment_id).exists():
            return JsonResponse({'message': 'INVALID_COMMENT'}, status=400)
        
        comment = Comment.objects.get(id=comment_id)
        
        # valid user check
        if comment.user != request.user:
            return JsonResponse({'message': 'INVALID_USER'}, status=401)

        comment.delete()
        return JsonResponse({'message': 'SUCCESS'}, status=200)

  • CommentView 
    • post 메소드 : 댓글을 작성(Create)하는 기능을 담당. 게시물과 마찬가지로 로그인이 필수인 서비스이기 때문에 login_decorator로 유저의 권한을 검증해야 한다.
  • CommentDetailView
    • post 메소드 : url에서 받은 comment_id와 일치하는 id의 댓글을 수정(Update)하는 기능. 게시물과 마찬가지로 댓글을 작성한 사람만 수정이 가능해야 하므로 comment.user와 request.user가 일치하는지 확인해야 한다.
    • delete 메소드 : url에서 받은 comment_id와 일치하는 id의 댓글을 삭제(Delete)하는 기능. Update와 마찬가지로 댓글을 작성한 사람만 삭제가 가능해야 하므로 이를 체크하는 로직을 구현하였다.

 

 

4. post/urls.py

각 View에 엔드포인트 추가

from django.urls import path

from .views import (
    PostView, 
    PostDetailView, 
    CommentView, 
    CommentDetailView,
)

urlpatterns = [
    path('', PostView.as_view()),
    path('/<int:post_id>', PostDetailView.as_view()),
    path('/comment', CommentView.as_view()),
    path('/comment/<int:comment_id>', CommentDetailView.as_view()),
]

 

 

- 참고

RESTful API에 Update에는 PUT / PATCH / POST  무슨 메소드를 써야 할까?

1) put : 전체를 업데이트할 때 주로 사용

2) patch : 부분을 업데이트할 때 주로 사용

3) post : RESTful API 이전에는 POST로 모든 update를 처리하다. RESTful API로 넘어오면도 아직 POST로 update를 처리하는 경우가 많다.

 

그런데 3번의 경우처럼 POST로 create와 update를 모두 구현하면 구분을 위해 억지로 다른 class에 나눠야 하는지에 대한 의문이 있었는데,

일단 create와 update는 url이 다르기 때문에(update의 경우 url에서 특정 id를 받아야 하므로) class를 나누어 써야 한다.

(한 class에 쓴다고 하더라도 에러가 나는 것은 아니지만 권장되지는 않는다)