<학습 목표>
- restful한 api 설계를 할 수 있다
- 미디어 파일과 스태틱 파일에 대해 이해한다
- 게시글 모델과 조회/업로드를 위한 serializer를 만들 수 있다
- 이미지를 포함한 게시글 기능을 개발할 수 있다
- 포스트맨으로 백엔드 개발을 하면서 테스팅을 할 수 있다
- drf에서 댓글 기능을 개발할 수 있다
- drf에서 좋아요 기능을 개발할 수 있다
- drf에서 follow 기능을 개발할 수 있다
- many-to-many 관계를 설정하는 경우와 방법, 그리고 related_name의 사용 방법을 이해한다
<실습>
- 3주차에서 생성한 drf_project 폴더에 이어서 작성한다
- restful하게 api 설계해보기
- 필요한 api를 생각해보면 게시글(조회/작성), 게시글 상세보기(조회/수정/삭제), 댓글(조회/작성), 댓글 상세(수정/삭제), 좋아요(on/off)가 있다
- articles 앱을 생성해서 urls.py와 views.py를 연결해보자
python manage.py startapp articles
- articles 앱을 생성한 후 앱 안에 urls.py 파일을 생성한다
- drf_project/urls.py에 articles 경로를 설정하고, settings.py에도 articles 앱을 추가해준다
# drf_project/settings.py
...
INSTALLED_APPS = [
...
'articles',
]
# drf_project/urls.py
...
urlpatterns = [
...
path('articles/', include('articles.urls')),
]
- articles/urls.py에 url 정해서 위에서 생각했던 api들의 경로를 쭉 작성한다
# articles/urls.py
...
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'),
]
- 순서대로 게시글, 게시글 상세, 댓글, 댓글 상세, 좋아요 기능이 수행될 url과 그 기능을 수행할 함수들을 연결했다
# articles/views.py
...
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
- 함수들을 먼저 작성한 후에 함수 내용을 작성할 예정이다
- 게시글의 모델 설계
- article 모델을 설계할 차례다
- articles/models.py에서 작성한다
# articles/models.py
from django.db import models
from users.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()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.title)
- user는 작성자로, User 모델과 게시글은 1대N 관계이기 때문에 외래키를 사용한다
- on_delete=models.CASCADE는 유저가 삭제되면 해당 유저가 쓴 게시글도 같이 삭제한다는 의미다
- 인스타그램 클론인데 title이 왜 필요한가..? 싶은데 일단 강의에서 하는대로 쓴다
- content필드는 텍스트 필드로 정의했는데 그럼 max_length를 설정할 필요가 없다
- 이미지필드를 사용할 때는 뭔가 추가적인 게 필요하다고 하는데 아래 내용에서 다룰 예정
- 생성시간과 업데이트시간도 필드로 정의해줬다
- __str__() 함수는 admin 페이지에서 게시글 목록에 타이틀만 보이게 설정해줬다
- 미디어 파일과 스태틱 파일에 대해
- 공식 문서: https://docs.djangoproject.com/en/4.2/howto/static-files/
- 위 문서를 참고해서 하면 된다
- 장고 프로젝트에서 이미지나 영상 등 미디어 파일을 쓰고 싶으면 setting에 루트경로를 설정해줘야 한다
- 그리고 이미지필드를 사용하기 위해서는 Pillow 라이브러리를 설치해야 한다
pip install Pillow
- 일단 라이브러리를 설치하고, requirements.txt에 freeze 하는 거 잊지 말고
- drf_project/settings.py에 경로를 추가한다
# drf_project/settings.py
...
STATIC_ROOT = BASE_DIR / "static"
STATIC_URL = "/static/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
- drf_project 최상위 폴더에 있는 media, static 폴더를 루트폴더로 삼는다는 내용이다
- 다음엔 urls.py에 가서 urlpatterns에 추가를 해줘야 한다
# drf_urls.py
from django.conf import settings
from django.conf.urls.static import static
...
urlpatterns = [
...
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- 이제 테스트를 하기 위해 articles/admin.py에 Article모델을 추가해준다
# articles/admin.py
from django.contrib import admin
from articles.models import Article
admin.site.register(Article)
- 서버 실행 후 http://127.0.0.1:8000/admin/ 여기로 가서 게시글을 작성하고 사진도 넣어본다
- 다시 vscode로 돌아오면 최상위경로에 media 폴더가 생성되어 있고 그 안에 내가 방금 넣은 이미지가 들어가 있는 것을 확인할 수 있다
- 근데 이렇게 하면 media 폴더에 사진들이 뒤죽박죽으로 들어가서 나중에 관리가 어려울 수 있다
- articles/models.py에 가서 image 필드를 수정 해준다
# articles/models.py
...
class Article(models.Model):
...
image = models.ImageField(blank=True, upload_to='%Y/%m/')
...
- 이미지를 업로드 하지 않아도 글을 작성할 수 있게 blank=True 로 변경했다
- 그리고 사진은 media 폴더 안에 해당 연도 폴더 안에 해당 월 폴더에 저장되도록 경로를 설정했다
- 중간에 이미지가 잘 저장됐을 때 주소창에 기본주소/media/이미지파일이름 을 치면 이미지가 나오는 걸 튜터님이 보여주셨다
- 근데 나는 아무리 해도 page not found 오류가 떴다...
- 에러를 그냥 복사해서 구글에 검색해보기도 하고 에러 페이지를 정독하기도 했는데 어느 순간 머리에 느낌표가 생겼다
- 나는 models.py를 수정해서 이미지를 업로드 하면 media 폴더 안에 2023 폴더 안에 04월 폴더에 사진이 저장되게끔 했는데 튜터님이 하신 그대로 기본주소/media/이미지파일이름 으로 접속하고 있던 거였다 실제 이미지 파일은 미디어 폴더 아래에 바로 있는 게 아니라 2023/04/를 추가해줬어야 했다 다시 기본주소/media/2023/04/이미지파일이름 을 하니까 이미지가 제대로 나왔다
- 그리고 models.py를 수정한 후에 makemigrations와 migrate를 잊지 말 것!!
- 게시글 리스트와 작성 serializers.py, views.py 그리고 포스트맨으로 테스트하기
게시글 리스트
- 먼저 2주차 때 했던 것처럼 articles 앱에 serializers.py 파일을 생성한다
- ArticleSerializer를 만든다
# articles/serializers.py
from rest_framework import serializers
from articles.models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = "__all__"
- 2주차에 했던 거 복붙해왔다 ㅎ
- 다음은 views.py를 작성한다
# articles/views.py
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from articles.models import Article
from articles.serializers import ArticleSerializer
class ArticleView(APIView):
def get(self, request):
"""전체게시글 조회"""
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
- 이것도 2주차 때 했던 걸 참고하면 된다
- articles 변수에 Article 모델의 오브젝트들을 모두 담고
- 이걸 그냥 Response에 담으면 뭐 안 읽힌다 그랬나 쿼리셋 어쩌고 했던 것 같은데...여튼 그래가지고 시리얼라이저가 필요하다
- serializer 변수에 위에서 만든 ArticleSerializer를 여러 개 가져올 수 있게 옵션을 설정해서 담는다
- 반환 값으로 Response를 보내면 끝
- 그냥 serializer를 반환하면 또 뭐 JSON 어쩌고 하면서 못 읽는다 시리얼라이저의 data를 반환해야 한다
- 포스트맨으로 가서 새 콜렉션을 만들고 articles list 라는 이름으로 리퀘스트 생성 후 저장한다
- 알맞은 URL을 입력하고 send를 누르면 게시글 목록이 뜨는 것을 확인할 수 있다
- 그런데 게시글 목록에 너무 많은 정보가 나오는 것 같다
- content와 created_at은 게시글 목록에서 볼 땐 필요가 없을 것 같다
- 먼저 시리얼라이저를 새로 만들어준다
# articles/serializers.py
...
class ArticleListSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("pk", "title", "image", "updated_at", "user")
- ArticleListSerializer라는 이름의 시리얼라이저를 새로 만들어줬다
- content 필드와 create_at 필드만 빼고 나오게 했다
- views.py에서 ArticleSerializer를 ArticleListSerializer로 수정해준다
# articles/views.py
...
from articles.serializers import ArticleSerializer, ArticleListSerializer
class ArticleView(APIView):
def get(self, request):
...
serializer = ArticleListSerializer(articles, many=True)
...
- 다시 포스트맨으로 가서 send를 눌러보면 content와 created_at 필드가 안 뜨는 것을 확인할 수 있다
- 이렇게 보니 user가 아이디번호가 아닌 회원가입 시 입력했던 이메일형식으로 나오면 좋을 것 같단 생각이 든다
- 시리얼라이저를 변경해보자
# articles/serializers.py
...
class ArticleListSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.email
class Meta:
model = Article
fields = ("pk", "title", "image", "updated_at", "user")
- user 변수에 시리얼메소드필드를 추가했는데, 아래 있는 get_user 함수를 실행시킨다
- get_user함수의 obj은 해당 게시글을 의미하고 그 게시글의 유저의 이메일을 반환한다
- 반환한 값이 user에 들어간다 다시 포스트맨을 실행하면
- user에 아이디번호가 아닌 이메일형식으로 나오게 된다
게시글 작성
- views.py에서 아티클뷰 클래스에 post 방식 함수를 작성한다
# articles/views.py
...
class ArticleView(APIView):
...
def post(self, request):
"""게시글 작성"""
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
- 2주차 때 했던 대로 이렇게 작성하고 포스트맨에서 실행시켜 보면
- 타이틀, 컨텐트, 유저가 필수 항목이라고 나온다
- 2주차 때 아티클모델은 유저가 없고 컨텐트는 blank=True, null=True로 해서 타이틀만 필수 항목이었지만 이번에 만든 모델은 저 3가지가 필수 항목이다
- 특히 유저는 로그인한 사용자가 자동으로 입력되게끔 해줘야 하기 때문에 일단 로그인을 하고 로그인 정보를 게시글 헤더에 실어 보낼 것이다
- 일단 포스트맨에서 전에 만들어준 로그인 리퀘스트로 로그인을 실행한다
- access 토큰을 복사해서 일단 Environment에 가서 token이라는 변수를 만들고 값에 붙여넣는다
- 다시 게시글만들기 리퀘스트로 가서 Header에 Key는 Authorization, Value에는 Bearer {{token}}을 넣어준다
- 이렇게 하고 타이틀과 컨텐트를 채워서 다시 send를 하면
- 여전히 유저를 넣어달라고 말한다
- 게시글 작성 함수에 가서 print(request.user)를 하고 다시 send를 누르면
- 일단 유저가 누구인지 인식은 하고 있다
- 다시 게시글 작성 함수로 가서 serializer 변수를 저장할 때 user=request.user를 넣어주면 user에 로그인한 사용자가 저장된다
serializer.save(user=request.user)
- 그런데 이렇게 해도 send를 보내면 여전히 user를 입력하라고 나온다
- is_valid()에는 여전히 user가 빈 값이 들어가 있는데 ArticleSerializer에는 모든 필드를 입력하게끔 되어 있기 때문에 그렇다(생성시간, 업데이트 시간은 자동으로 되는 거라 입력해야 하는 값으로 인식하진 않는 것 같다)
- 어쨌든 ArticleCreateSerializer를 새로 생성해준다
- 필드에는 사용자가 입력할(=is_valid를 거쳐갈) 필드들만 넣어준다
# articles/serializers.py
...
class ArticleCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("title", "image", "content")
- 이제 views.py에 가서 ArticleCreateSerializer를 import 해주고 게시글 작성 함수를 수정한다
# articles/views.py
...
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)
- 이제 다시 포스트맨으로 가서 포스트를 send해보면
- 게시글이 작성되는 것을 확인할 수 있다
- 포스트맨 게시글 리스트에서도 방금 작성한 글을 확인할 수 있다
- 물론 데이터베이스에서도 방금 작성한 글을 확인할 수 있다!
- 이미지를 넣고 싶을 때는 raw 방식이 아니라 form-data 방식으로 첨부할 수 있다
- 위 이미지에 보이는대로 raw가 아닌 form-data에 체크한 다음 key값에 title, content, image를 넣고 value에는 내용을 적으면 되는데 image의 경우 File이라고 써있는 부분에 마우스를 올려놓으면 text로 할지 file로 할지 선택할 수 있다 file로 선택하면 value에 select file 버튼이 생긴다 그 버튼을 눌러 원하는 이미지를 선택한 다음 send를 하면 이미지가 업로드 되는 것을 확인할 수 있다
- vscode 작업환경에 있는 media 폴더 안에 방금 업로드한 사진이 올라간 것을 확인할 수 있다