django #8 인스타그램 클론 코딩(4) - 게시물/댓글 작성 , 좋아요, 팔로우

Junyoung Kim·2022년 2월 2일
0

django

목록 보기
8/10

로그인 데코레이터를 적용해 로그인 후 가능한 게시물/댓글/좋아요/팔로우 기능을 짜보았다.

게시물

python manage.py startapp postings

게시물/댓글/좋아요 기능은 회원과 다른 영역인 게시물의 영역에 있기 때문에 앱을 분리한다.
분리 후 회원가입에서 그랬던 것 처럼 모델링부터 시작한다.

models.py

# postings/models.py

from django.db import models

from users.models import User

class Posting(models.Model):
    content    = models.CharField(max_length=2200, null=True)
    user       = models.ForeignKey("users.User",on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "postings"

class Image(models.Model):
    image_url = models.URLField(max_length=2000)
    posting   = models.ForeignKey('Posting',on_delete=models.CASCADE, related_name='images')
    
    class Meta:
        db_table = 'images'
  • 유저와 게시물은 일대다 관계이므로, 미리 만들어뒀던 user 앱에서 users.User의 형식으로 import해준다. 다른 앱에서 클래스를 가져올때는 (앱 이름).(클래스명)으로 가져올 수 있다.
  • 한 게시물에는 여러장의 이미지가 들어갈 수 있으므로 역시 일대다 관계로 필드를 선언해준다.
  • 게시물에는 사진만 있고 글은 없는 경우도 있을 수 있으므로 null=True를 사용한다.
  • 생성과 수정이 빈번한 데이터는 항상 created_atupdated_at auto_now_addauto_now를 사용하여 설정해준다.



views.py

# postings.views.py
import json

from django.views import View
from django.http  import JsonResponse
from json.decoder import JSONDecodeError

from users.models    import User
from users.utils     import login_decorator
from postings.models import Posting, Image, Comment, Like

class PostingView(View):
    @login_decorator
    def post(self,request):
        try:
            data           = json.loads(request.body)
            user           = request.user
            
            content        = data.get('content')
            image_url_list = data.get('image_url').split(',')

            post = Posting.objects.create(
                content = content,
                user = user,
            )
            for image_url in image_url_list:
                Image.objects.create(
                    image_url = image_url,
                    posting   = post
                )

            return JsonResponse({'message':'SUCCESS'}, status=200)
        
        except AttributeError:
            return JsonResponse({'message':'NO_IMAGE_URL'}, status=400)
            
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)

    @login_decorator
    def get(self, request):
        try:
            posts = Posting.objects.all()
            result= [{
                'Name'      : User.objects.get(id=post.user.id).name,
                'Content'   : post.content,
                'Images'    : [image.image_url for image in post.images.all()],
                'Created_at': post.created_at
            } for post in posts
                ]
            
            return JsonResponse({'message': result}, status=200)
        
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
    

post

  • request.user에 저장한 JWTuser_id를 변수 user에 다시 저장한다.

  • 여러 장의 이미지를 따옴표 안의 쉼표로 구분해서 요청받은 뒤에, 그것을 split() 함수를 이용해 배열로 저장한다.

  • posting table에 게시글과 유저는 그대로 저장하지만, 이미지는 여러장이므로 for문을 이용하여 저장했던 배열을 불러와 여러 장을 저장한다.

  • 이전에 회원정보를 받을 때 사용했던 것 처럼 data['image_url']이 아니라data.get('image_url')을 사용하였다.
    get 메서드를 사용하였으므로, request에 이미지 URL이 존재하지 않을 때에는 파이썬 딕셔너리 key값 오류로 인해서 발생하는 KeyError가 아닌 None Type을 반환해준다.
    여기에서 이미지 여러개를 받을 때를 대비해서 split()메서드를 사용했는데, None Type은 문자열이 아니므로 해당 속성(attribute)을 가지고 있지 않다. 따라서 AttributeError로 예외처리 해준다.



게시글이 성공적으로 작성되었다.


POST 요청으로 작성했던 게시물도 GET 요청으로 성공적으로 조회했다.






댓글 작성

댓글은 게시물과 똑같이 postings 앱에서 작성해준다.

models.py

# postings/models.py

class Comment(models.Model):
    content    = models.CharField(max_length=2200)
    user       = models.ForeignKey("users.User",on_delete=models.CASCADE, related_name='comments')
    posting    = models.ForeignKey("Posting",on_delete=models.CASCADE, related_name='comments')
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        db_table = 'comments'

한 게시물에는 여러 댓글이 들어갈 수 있으므로 일대다 관계로 만들어준다.
마찬가지로 여러 유저가 댓글을 작성할 수 있으므로 일대다 관계로 만들어준다.

views.py

    
class CommentView(View):
    @login_decorator
    def post(self, request):
        try:
            data           = json.loads(request.body)
            user           = request.user
            content        = data.get('content')
            posting_id     = data.get('posting')
            
            if not (content and posting_id) :
                return JsonResponse({'message' : 'KEY_ERROR'}, status=400)
            
            if not Posting.objects.filter(id=posting_id).exists():
                return JsonResponse({'message':"POSTING_DOES_NOT_EXIST"}, status=404)
            
            posting = Posting.objects.get(id = posting_id)
            
            Comment.objects.create(
                content = content,
                user    = user,
                posting = posting
            )
            return JsonResponse({'message':'SUCCESS'}, status=200)
    
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
    
    def get(self, request):
        try:
            comments = Comment.objects.filter(posting_id=1)
            result   = [{
                'Name'       : User.objects.get(id=comment.user.id).name,
                'Content'    : comment.content,
                'Created_at' : comment.created_at
            } for comment in comments
                ]
            
            return JsonResponse({'message': result}, status=200)
        
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
  • 댓글과 작성자의 id정보가 없을때는 KeyError 반환

  • 댓글을 달려는 게시물이 없을때는 exist() 메서드를 사용하여 DOESNOTEXIST 에러 반환

  • BODY에 아무 내용이 없을 경우에는 JSON_DECODE_ERROR 반환


POST GET 전부 성공적으로 수행






좋아요

models.py

# postings/models.py

from django.db import models

from users.models import User

class Posting(models.Model):
    content    = models.CharField(max_length=2200, null=True)
    user       = models.ForeignKey("users.User",on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    like       = models.ManyToManyField("users.User", related_name= 'likes', through="Like")

    class Meta:
        db_table = "postings"

/...

class Like(models.Model):
    posting = models.ForeignKey('Posting', on_delete=models.CASCADE, related_name='post_likes')
    user    = models.ForeignKey('users.User',on_delete=models.CASCADE,related_name='user_likes')

    class Meta:
        db_table = 'likes'
  • 한 유저는 여러 게시물에 좋아요 가능

  • 한 게시물은 여러명의 유저에게 좋아요를 받을 수 있음

  • 좋아요는 유저와 게시물의 중간 테이블 역할을 수행, ManyToManyField, through 옵션 사용

  • 역참조에 쓰이는 related_name은 복수형으로 설정(one 이 many 테이블을 참조하므로)

views.py

# postings/views.py

class LikeView(View):
    @login_decorator
    def post(self, request):
        try:
            data         = json.loads(request.body)
            user         = request.user
            posting_id   = data.get('posting')
            posting      = Posting.objects.get(id = posting_id)
            
            obj, created = Like.objects.get_or_create(user = user, posting = posting)
            
            if not created:
                obj.delete()
                return JsonResponse({'message' : 'CANCEL_LIKE'},status =200)
            else:
                return JsonResponse({'message':'SUCCESS'}, status=200)

        except Posting.DoesNotExist:
            return JsonResponse({'message':"POSTING_DOES_NOT_EXIST"}, status=404)    
    
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
  • 좋아요가 이미 되어있는 상황일 경우에는, 좋아요를 취소하기 위해서 get_or_create 함수를 사용

  • get_or_create
    조건을 만족하는 객체를 찾아 존재하지 않을 경우에는 객체를 생성하고, 객체와 객체의 존재 여부를 튜플 형태로 반환
    객체가 존재하지 않을 때는 True, 존재할때는 False를 반환한다.
    여기서는 객체를 obj로, 존재 여부를 created로 변수 선언했다.

  • createdFalse일 경우 (존재할 때), 객체인 objdelete를 사용해 삭제하고, 그렇지 않을 때는 성공 메시지를 반환한다.


좋아요를 이미 되어있는 게시물에 했을 때 정상적으로 취소 처리되는 것을 확인할 수 있다.






팔로우

팔로우는 유저(사용자)간의 관계를 나타내는 데이터이므로 다시 user app으로 돌아와서 작성한다.

models.py

# users/models.py

from re import A
from django.db import models

class User(models.Model):
    name         = models.CharField(max_length=50)
    email        = models.EmailField(max_length=100, unique=True)
    password     = models.CharField(max_length=200)
    phone_number = models.CharField(max_length=100)
    created_at   = models.DateTimeField(auto_now_add=True) 
    updated_at   = models.DateTimeField(auto_now=True)
    follow       = models.ManyToManyField('self', through='Follow', symmetrical=False)
        
    class Meta:
        db_table = 'users'

class Follow(models.Model):
    follower  = models.ForeignKey('User',on_delete=models.CASCADE, related_name='following')
    following = models.ForeignKey('User',on_delete=models.CASCADE, related_name='follower')
    
    class Meta:
        db_table = 'follows'
  • 한 유저는 여러 유저를 팔로우 할 수 있다. 다른 유저도 여러 유저를 팔로우 할 수 있다.
    즉 자기 자신을 참조하는 다대다 관계이다.

  • 동일한 필드를 참조하므로 self를 적어주고, Follow 클래스를 선언한 뒤 through 옵션을 이용해 중간테이블로 지정해준다.
    어떤 유저가 다른 유저를 팔로우 할 때, 다른 유저도 자동으로 해당 유저를 팔로우 하는 것이 아니므로 대칭(symmetrical)을 False로 설정해준다.

  • 자기 자신을 참조할때는 related_name의 설정이 중요하다.
    한 유저의 follower는 이 유저를 following 하고 있는 것이고,
    한 유저가 following하는 유저는 그 유저의 follower가 된다.
    즉 역참조할때는 서로의 위치가 바뀌므로 related_name을 역으로 설정해 준다.

views.py

# users/views.py

class FollowView(View):
    @login_decorator
    def post(self, request):
        try:
            data         = json.loads(request.body)
            user         = request.user
            following_id = data.get('user_id')
            following = User.objects.get(id=following_id)
            
            if not User.objects.filter(id = following_id).exists():
                return JsonResponse({'message':"USER_DOES_NOT_EXIST"}, status=404)
            
            if  user.id == following.id:
                return JsonResponse({'message' : 'CANNOT_FOLLOW_MYSELF'})
            else:
                obj, created = Follow.objects.get_or_create(follower = user, following = following)
            
            if not created:
                obj.delete()
                return JsonResponse({'message' : 'CANCEL_FOLLOW'},status =200)
            
            return JsonResponse({'message':'SUCCESS'}, status=200)
    
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
        
    @login_decorator
    def get(self, request):
        try:
            data         = json.loads(request.body)
            user_id      = data.get('user_id')
            target_user  = User.objects.get(id=user_id)

            result = {
                'user'           : [{
                    'name' : target_user.name, 
                    'id'   : target_user.id,
                }],
                'follower_list'  : [{
                    'id'   : follower.id,
                    'name' : follower.follower.name
                } for follower in target_user.follower.all()],
                'following_list' : [{
                    'id'   : following.id,
                    'name' : following.following.name
                } for following in target_user.following.all()]
            }
            return JsonResponse({'message':'SUCCESS' , 'Follow_list' : result}, status=200)
    
        except JSONDecodeError:
            return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
  • 팔로우와 마찬가지로 get_or_create를 활용해 이미 되어있는 팔로우를 다시 요청하면 취소하는 로직을 작성

  • 자기 자신을 팔로우 할수는 없으므로 if문을 활용해 로직 작성

  • 유저의 팔로우 리스트를 반환받는 GET 메서드는, 리스트 컴프리헨션과 역참조를 적절하게 활용, 팔로워와 팔로우 하고 있는 사람의 이름과 아이디를 중첩 딕셔너리 형태로 출력


팔로우 POST 요청

자기 자신 Follow 요청시 에러 출력

팔로우 리스트 GET 요청으로 출력

0개의 댓글

관련 채용 정보