post-custom-banner

1. restul하게 api 설계해보기

게시글&댓글 관련 기초 api 설계를 해보자!

- urls.py

from django.urls import path
from articles import views


urlpatterns = [
    path('', views.ArticleView.as_view(), name='article_view'),
    path('<int:article_id>/', views.ArticleDetailView.as_view(), name='article_detail_view'),
    path('comment/', views.CommentView.as_view(), name='comment_view'),
    path('comment/<int:comment_id>/', views.CommentDetailView.as_view(), name='comment_detail_view'),
    path('like/', views.LikeView.as_view(), name='like_view'),
]

- views.py

from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions

class ArticleView(APIView):
    def get(self, request):
        pass
    def post(self, request):
        pass
    
class ArticleDetailView(APIView):
    def get(self, request, article_id):
        pass
    def put(self, request, article_id):
        pass
    def delete(self, request, article_id):
        pass
    
class CommentView(APIView):
    def get(self, request):
        pass
    def post(self, request):
        pass
    
class CommentDetailView(APIView):
    def put(self, request, comment_id):
        pass
    def delete(self, request, comment_id):
        pass
    
class LikeView(APIView):
    def post(self, request):
        pass

게시글 리스트 보여주기, 게시글 작성하기, 게시글 상세페이지 보여주기, 게시글 상세페이지에서 수정하기, 게시글 상세페이지에서 삭제하기, 댓글 전체 보여주기, 댓글 작성하기, 댓글 수정하기, 댓글 삭제하기, 좋아요에 대한 api들을 작성!!

2. 게시글의 모델 설계

from django.db import models
from user.models import User

class Article(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length = 50)
    content = models.TextField()
    image = models.ImageField(blank=True, upload_to='%Y/%m/')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now = True)
    
    def __str__(self):
        return str(self.title)

여기서 upload_to='%Y/%m/' 는 사용자가 사진을 업로드 하였을 때 미디어 폴더 안에 년/월 폴더를 자동으로 생성하여 그 안에 사진을 보관한다.

3. 미디어 파일과 스태틱 파일에 대해

장고 스테틱 관련 자료 : https://docs.djangoproject.com/en/4.1/howto/static-files/

- 스테틱 파일(static files)이란?

웹사이트에는 일반적으로 이미지, 자바스크립트, css같은 추가적인 파일들이 필요한데 이것들을 static files 이라고 한다.
사용자의 요청에 따라 내용이 바뀌는 것이 아니라 요청한것을 그대로 내어주면 되는 파일들이다!

- 스테틱 파일과 미디어 파일의 차이?

스테틱 파일-> 관리자가 올린 파일
미디어 파일-> 사용자들이 올린 파일

-settings.py

STATIC_ROOT = BASE_DIR / "static"
STATIC_URL = "static/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

이 코드를 setting.py에 추가

-urls.py

from django.conf import settings
from django.conf.urls.static import static

urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

이 코드를 프로젝트의 urls.py에 추가

4. 게시글 리스트와 작성 serializers.py, views.py 그리고 포스트맨으로 테스트하기

- 게시글 리스트

- views.py

from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from articles.models import Article
from articles.serializers import ArticleListSerializer

class ArticleView(APIView):
    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleListSerializer(articles, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

- serializers.py

from rest_framework import serializers
from articles.models import Article

class ArticleListSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField() # user의 벨류값이 pk값으로 나오는것을 email로 불러내기 위해 메소드시리얼라이저 사용
    
    def get_user(self, obj):
        return obj.user.email
    
    class Meta:
        model = Article
        fields = ("pk", "title", "image", "updated_at", "user") # 원하는 필드들을 고를 수 있음

원하는 필드들만 게시글 리스트에 보여주기 위해 fields에서 직접 선정해 주었다.
또한 user의 값이 pk형태로 나와서 email형태로 나오게 해주기 위해 메소드시리얼라이저를 사용하였다.
아티클의 오브젝트의 유저의 이메일을 리턴값으로 돌려주고 이 값이 user로 들어가서 화면에 email이 뜨게 되는 형식이다.

- 게시글 작성

- views.py

from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from articles import serializers
from articles.models import Article
from articles.serializers import ArticleListSerializer, ArticleSerializer, ArticleCreateSerializer

class ArticleView(APIView):
    def post(self, request):
        serializer = ArticleCreateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(user=request.user)
            return Response(serializer.data)
        else:
            return Response(serializer.errors)

이 코드를 views.py에 추가

- serializers.py

from rest_framework import serializers
from articles.models import Article

class ArticleCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ("title", "image", "content")

이 코드를 serializers.py에 추가

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

- 게시글 상세페이지

- 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)

- sertializers.py

class ArticleSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    
    def get_user(self, obj):
        return obj.user.email
    
    class Meta:
        model = Article
        fields = "__all__"

이 또한 user를 email형태로 부르기 위해

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

코드를 추가해주었다.

- 게시글 수정

- views.py

from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from articles import serializers
from articles.models import Article
from articles.serializers import ArticleListSerializer, ArticleSerializer, ArticleCreateSerializer
from rest_framework.generics import get_object_or_404

class ArticleDetailView(APIView):
    def put(self, request, article_id):
        article = get_object_or_404(Article, id=article_id)
        if request.user == article.user:
            serializer = ArticleCreateSerializer(article, 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)

게시글 수정 기능과 본인이 아닐경우 삭제를 할 수 없도록 하는 기능을 추가하였다!!

- 게시글 삭제

- views.py

class ArticleDetailView(APIView):
    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)

게시글 삭제 기능, 본인이 아니면 삭제할 수 없게하는 기능을 추가

6. 댓글의 models-serializer-views

- models.py

class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comment_set") # 역으로 참조할 때에는 related_name 사용, related_name="comment_set"은 디폴트 값이어서 작성 안해줘도 있는것으로 인식된다.
    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)

- ManyToMany 필드 역참조

_set 또는 related_name 사용

- _set

아무것도 작성을 안했을 때 디폴트 값은 comment_set형태이다.
ex) 나를 바라보고 있는 것_set(comment_set)

_set를 대신하여 사용할 수 있는것으로 이름을 직관적으로 작성할 수 있다.
related_name이 꼭 필요한 경우가 있는데 이는 한 클래스에서 서로 다른 두 column이 같은 table을 참조하는 경우이다.
예) comment와 user 모두 참조하는 경우
참고 자료 : https://velog.io/@hj8853/Django-ManyToMany-relatedname

- views.py

class CommentView(APIView):
    def get(self, request, article_id):
        article = Article.objects.get(id = article_id)
        comments = article.comment_set.all()
        serializer = CommentSerializer(comments, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
    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)
        else:
            return Response(serializer.errors)

댓글 보이기, 댓글 작성하기 코드 작성

class CommentDetailView(APIView):
    def put(self, request, article_id, comment_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)
    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)

댓글 수정하기, 댓글 삭제하기 코드 추가

- serializers.py

class CommentSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    
    def get_user(self, obj):
        return obj.user.email
    
    class Meta:
        model = Comment
        fields= "__all__"
        
class CommentCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ("content",)  # 들어있는데 핃드가 1개라도 콤마 필수

fields안에 1개의 변수만 들어갈 때도 끝에 꼭 ,를 붙여줘야 한다.
fields(복수형)이라는 조건이 있기 때문에!!(안쓰면 string으로 인식한다!)

7. 좋아요 models-serializer-views

- models.py

class Article(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length = 50)
    content = models.TextField()
    image = models.ImageField(blank=True, upload_to='%Y/%m/')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now = True)
    likes = models.ManyToManyField(User, related_name="like_articles") #manytomany 필드는 related_name을 설정안하면 이름이 중복되기 때문에 꼭 설정을 해주어야 한다!!
    
    def __str__(self):
        return str(self.title)

위에서 설명한 것 처럼 foreignkey에서 이미 역참조를 하고 있는 상황에서 아무런 설정 없이 참조를 또 하게되면 오류가 발생한다.
때문에 likes에서 역참조 할 때에는 related_name을 사용하여 like_articles라고 이름을 정해주었다.

- views.py

class LikeView(APIView):
    def post(self, request, article_id):
        article = get_object_or_404(Article, id=article_id)
        if request.user in article.likes.all():
            article.likes.remove(request.user)
            return Response("좋아요 취소", status=status.HTTP_200_OK)
        else:
            article.likes.add(request.user)
            return Response("좋아요", status=status.HTTP_200_OK)

request.user가 게시글에 이미 좋아요를 눌렀으면 request.user를 좋아요 목록에서 빼고, 아니면 좋아요 목록에 추가해라 라는 코드를 추가하였다.

8. follow models-serializer-views

- models.py

followings = models.ManyToManyField('self', symmetrical=False, related_name='followers') # symmetical은 대칭인지 아닌지!

User모델에 이 코드를 추가하였다.
이 또한 ManyToMany필드 역참조라 related_name 추가해주고, symmetrical이라는 대칭인지 아닌지를 구분해주는 것을 추가해주었다.
인스타그램의 경우 일방적으로 팔로우가 가능하므로 대칭형태가 아니기 때문에 False로 값을 주었다.

- views.py

class FollowView(APIView):
    def post(self, request, user_id):
        you = get_object_or_404(User, id=user_id)
        me = request.user
        if me in you.followers.all():
            you.followers.remove(me)
            return Response("언팔로우", status=status.HTTP_200_OK)
        else:
            you.followers.add(me)
            return Response("팔로우", status=status.HTTP_200_OK)

좋아요에서 쓴 구문을 동일하게 작성해주었다.

9. 게시글 리스트 / 게시글 상세페이지 serializer 수정

- 게시글 리스트

- 좋아요 갯수 추가하기, 댓글 갯수 추가하기

class ArticleListSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    likes_count = serializers.SerializerMethodField()
    comment_set_count = serializers.SerializerMethodField()
    
    def get_user(self, obj):
        return obj.user.email
    
    def get_likes_count(self, obj):
        return obj.likes.count()
    
    def get_comment_set_count(self, obj):
        return obj.comment_set.count()
    
    class Meta:
        model = Article
        fields = ("pk", "title", "image", "updated_at", "user", "likes_count", "comment_set_count")
  • SerializerMethodField를 사용하여 좋아요 수와 댓글 수를 보이게 하였다.
    댓글 갯수에 대한 코드를 짤 때 comment_set_count라고 작성한 이유는 related_name이 comment_set이기 때문이다. 다른것으로 변경하면 오류가 난다!

- 게시글 상세페이지

- article필드 없애기, 게시글 디테일 페이지에 모든 댓글 보이게 하기, 좋아요 누른 사람을 email값으로 보이게 하기

class CommentSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    
    def get_user(self, obj):
        return obj.user.email
    
    class Meta:
        model = Comment
        exclude = ("article",)

user가 이메일 형태로 나오게 설정되어 있고, article필드를 제외하기 위해서 exclude를 사용하였다.
이 또한 필드가 1개뿐이더라도 끝에 꼭 ,를 붙여야 한다!

class ArticleSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()
    comment_set = CommentSerializer(many=True)
    likes = serializers.StringRelatedField(many=True)
    
    def get_user(self, obj):
        return obj.user.email
    
    class Meta:
        model = Article
        fields = "__all__"
  • comment_set = CommentSerializer(many=True) 를 추가하여 게시글 디테일에 게시글에 달린 모든 댓글이 보이도록 하였다. 이 때 comment_set는 모델에 작성한 related_name으로 꼭 동일하게 작성해주어야 한다!!
  • 좋아요를 누른 사람을 pk값이 아니라 email값으로 보여주기 위해 likes = serializers.StringRelatedField(many=True)를 추가하였다.
    StringRelatedField는 User모델의 str(스트링필드)에서 지정한 값으로 값이 나오게 한다!

10. Profile 페이지 api 만들어보기

- urls.py

path('<int:user_id>/', views.ProfileView.as_view(), name='profile_view'),

- views.py

class ProfileView(APIView):
    def get(self, request, user_id):
        user = get_object_or_404(User, id=user_id)
        serializer = UserProfileSerializer(user)
        
        return Response(serializer.data)

- serializers.py

class UserProfileSerializer(serializers.ModelSerializer):
    # followers = serializers.PrimaryKeyRelatedField(many=True, read_only=True) followers와 followings가 pk(id)값으로 들어가게 하는 코드
    followers = serializers.StringRelatedField(many=True) # 이메일로 보이게
    followings = serializers.StringRelatedField(many=True) # 이베일로 보이게
    article_set = ArticleListSerializer(many=True) # 본인 게시글 모두 불러오기
    like_articles = ArticleListSerializer(many=True) # 좋아요한 게시글 모두 불러오기
    
    class Meta:
        model = User
        fields=("id", "email", "followings", "followers", "article_set", "like_articles")

- admin.py

class UserAdmin(BaseUserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm

    list_display = ('id', 'email', 'is_admin')
    list_filter = ('is_admin',)
    fieldsets = (
        (None, {'fields': ('email', 'password', 'followings')}),
        ('Permissions', {'fields': ('is_admin',)}),
    )
  • admin의 User 페이지에서 id도 보일 수 있게 list_display에 추가
  • admin의 User의 디테일 페이지에서 followings도 할 수 있게 필드셋을 추가

11. Feed 페이지 api 만들어보기

Q-object 관련자료 : https://docs.djangoproject.com/en/4.1/topics/db/queries/#complex-lookups-with-q

- 팔로우하고있는 사람의 게시글 불러오기

- urls.py

path('feed/', views.FeedView.as_view(), name='feed_view'),

- views.py

from django.db.models.query_utils import Q

class FeedView(APIView):
    permission_classes = [permissions.IsAuthenticated] # 로그인이 되어있는지
    def get(self, request):
        q = Q()
        for user in request.user.followings.all(): # 로그인한 유저가 팔로잉하고있는 모든 유저들을 for문으로 돌리겠다.
            q.add(Q(user=user), q.OR) # user가 내가 팔로잉하고 있는 유저일 때 그 유저의 값들을 모두 q에 넣는다.(or이기 때문에)
        feeds = Article.objects.filter(q) # feeds는 내가 팔로잉하고 있는 유저들의 피드들이다.
        serializer = ArticleListSerializer(feeds, many=True) # 그 feeds를 모두 불러오겠다!
        return Response(serializer.data)

쿼리에 or나 and를 적용시킬 수 있는 Q를 사용하였다.
user가 내가 팔로잉하고 있는 유저일 때의 값을 모두 q에 넣고 그 q값을 feeds의 코드에 넣으면 팔로잉하고 있는 유저의 모든 게시물들이 불려온다!!

깃허브 주소 : https://github.com/ksykma/drf_project

profile
개발과 지식의 성장을 즐기는 개발자
post-custom-banner

0개의 댓글