DRF(Django-rest-framework)를 통해 게시글을 생성하는 것 자체는 어렵지 않습니다. 하지만 게시글을 조회하는 수를 무한정 증가시키지 않으려면 쿠키를 통해 중복을 방지해야 합니다. 물론, 쿠키를 계속 삭제하면 조회수 증가가 되겠죠.(그건 고려하지 않겠습니다 ㅠ)
# views.py
from django.shortcuts import render, get_object_or_404
from .serializers import *
from .models import *
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
# 권한이 있는(jwt 토큰을 가진 사용자) 사람은 생성, 수정, 삭제 가능 <-> 일반 사용자(혹은 로그인 X)는 보기만 가능
from rest_framework.permissions import IsAuthenticatedOrReadOnly
# 쿠키 설정에 사용될 시간 관련 라이브러리
import datetime
from django.utils import timezone
# 트랜잭션
from django.db import transaction
# 페이지네이션
from rest_framework.pagination import PageNumberPagination
class BasicPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
views.py 코드가 조금 길어져 라이브러리를 임포트하는 부분은 따로 적었습니다. DRF에서 제공하는 Viewsets를 사용했습니다. 관련 내용은 공식 홈페이지를 참고해주세요. Viewsets를 사용한 이유를 간단히 말하자면, 함수가 여러 개인 하나의 클래스만으로 urls.py 부분을 여러 개 설정할 수 있는데요. 예시 화면은 아래와 같습니다.
이중에서도 저는 ModelViewSet을 사용했습니다. GenericAPIView를 상속하고, 믹스인 클래스를 통해 여러 메서드를 사용할 수 있기 때문입니다.
다음은 페이지네이션 설정을 살펴보겠습니다. 페이지네이션은 자동으로 페이지를 설정해주는 기능인데, 한 페이지에 100개의 게시글을 보여줄 순 없겠죠. 100개의 게시글이 있고, page_size=10이면 한 페이지에 10개의 게시글을 보여줄 것입니다.
각종 다양한 파라미터는 공식 홈페이지에서 참고를 하시면 되겠습니다. 제가 추가한 page_size_query_paㅇram 같은 경우, 클라이언트 측에서 request로 page_size를 정할 수 쿼리문의 파라미터입니다. 10개씩 보기, 20개씩 보기 등에 해당하는 것 같네요.
다음은 트랜잭션에 관한 설정입니다. 은행 송금이나 포인트 지급 등 DB 액션 중 중간에 끊어져서는 안되는 경우가 있죠. 문제가 생길 것 같으면 아예 액션을 수행하지 않던지, 문제가 없다면 처음부터 끝까지 액션을 완성하던지 해야 합니다. 원자(Atom)는 쪼갤 수 없는 마지막 입자(논란이 있을 수 있지만 여기선 무시합니다)로 해당 DB 관련 액션을 원자성을 지닌다고 표현하며 좀 더 구체적으로는 트랜잭션(transaction)이라고 칭합니다. 트랜잭션의 모든 연산은 한 번에 완료해야 하며, "A는 DB에 저장되었는데, B는 저장이 안되었어" 라는 상황 자체가 존재할 수 없습니다.
아래 코드는 한 외국 사이트에서 찾은 DRF에서 트랜잭션을 사용하는 예시입니다.
# decorator 사용하기
from django.db import transaction
@transaction.atomic
def create_category(name, products):
category = Category.objects.create(name=name)
product_api.add_products_to_category(category, products)
activate_category(category)
# context manager 사용하기
from django.db import transaction
def create_category(name, products):
with transaction.atomic():
category = Category.objects.create(name=name)
product_api.add_products_to_category(category, products)
activate_category(category)
아래 코드에서 보겠지만 저는 두 번째 방법인 context manager를 사용했습니다.
본격적으로 ModelViewSet 오버라이딩 코드를 살펴보겠습니다. 일단 전체 게시글 목록을 볼 수 있는 list() 함수와 게시글 1개를 자세히 볼 수 있는 retrieve() 함수를 만들었습니다.
# views.py
class BoardAPIView(viewsets.ModelViewSet):
queryset = Board_post.objects.all()
serializer_class = BoardSerializer
permission_classes = (IsAuthenticatedOrReadOnly, )
pagination_class = BasicPagination
def list(self, request):
qs = self.get_queryset()
page = self.paginate_queryset(qs)
if page is not None:
serializer = self.get_paginated_response(self.get_serializer(page, many=True).data)
else:
serializer = self.get_serializer(page, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, pk=None):
instance = get_object_or_404(self.get_queryset(), pk=pk)
# 당일날 밤 12시에 쿠키 초기화
tomorrow = datetime.datetime.replace(timezone.datetime.now(), hour=23, minute=59, second=0)
expires = datetime.datetime.strftime(tomorrow, "%a, %d-%b-%Y %H:%M:%S GMT")
# response를 미리 받고 쿠키를 만들어야 한다
serializer = self.get_serializer(instance)
response = Response(serializer.data, status=status.HTTP_200_OK)
# 쿠키 읽기 & 생성
if request.COOKIES.get('hit') is not None: # 쿠키에 hit 값이 이미 있을 경우
cookies = request.COOKIES.get('hit')
cookies_list = cookies.split('|') # '|'는 다르게 설정 가능 ex) '.'
if str(pk) not in cookies_list:
response.set_cookie('hit', cookies+f'|{pk}', expires=expires) # 쿠키 생성
with transaction.atomic(): # 모델 필드인 views에 1 추가
instance.views += 1
instance.save()
else: # 쿠키에 hit 값이 없을 경우(즉 현재 보는 게시글이 첫 게시글임)
response.set_cookie('hit', pk, expires=expires)
instance.views += 1
instance.save()
# views가 추가되면 해당 instance를 serializer에 표시
serializer = self.get_serializer(instance)
response = Response(serializer.data, status=status.HTTP_200_OK)
return response
퍼미션(permission)을 적용할 때 주의할 점이 있는데요, 아래와 같이 반드시 "," 를 찍어줘야 합니다. 이유는 튜플이나 리스트를 사용하게 되어있기 때문이죠.
permission_classes = (IsAuthenticatedOrReadOnly, )
# permission_classes = [IsAuthenticatedOrReadOnly]
이제 조회수 중복 방지를 위한 쿠키 설정을 설명해보겠습니다.
1. 쿠키 만들기
set_cookie(name, value, max_age)
name: 쿠키 이름
value: 저장하는 값
max_age: 쿠키 유지 시간
2. 쿠키 데이터 읽기
request.COOKIES.get(cookie name)
아래는 print(request.COOKIES) 의 출력 결과입니다.
{'sessionid': '****************************', 'csrftoken': '***************************************************************', 'hit': '2'}
개발자 도구(F12)에서 아래 쿠키 정보를 확인해볼 수 있습니다. hit 인자 value 값에 '.'이 찍힌 이유는 위에서 주석으로 언급한 것과 연관되어 있습니다.(이것저것 시도해봤습니다)
cookies_list를 출력하면 아래와 같이 리스트로 나옵니다.
cookies_list = ['2.3', '3', '2', '4']
마지막으로 models.py 와 urls.py 코드를 공유하겠습니다. 자세한 설명은 생략하겠습니다.
# models.py
from django.db import models
from django.conf import settings
class Category(models.Model):
name = models.CharField(max_length=30)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return '%s' % (self.name)
class Board_post(models.Model):
writer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True)
category_id = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
title = models.CharField(max_length=40, null=False, blank=False)
body = models.TextField()
views = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return '%s' % (self.id)
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('board', views.BoardAPIView.as_view({'get': 'list'}), name='all_board'),
path('board/<int:pk>', views.BoardAPIView.as_view({'get': 'retrieve'}), name='detail_board')
]
쿠키 참고 원문: Django-mysite만들기9 게시판 조회수 중복 증가 쿠키 처리 · 콩정의 개발 정리 블로그
'문돌이 존버 > Django 스터디' 카테고리의 다른 글
장고(Django) admin 페이지 커스터마이징 (0) | 2021.08.05 |
---|---|
장고 크론탭(django crontab) 사용해보기 (0) | 2021.02.20 |
DRF user 회원가입 UI 아이디, 이메일 중복 검사 (0) | 2021.02.13 |
DRF 유저 모델(Profile), 아이디 및 이메일 중복확인 (0) | 2021.02.12 |
DRF rest-auth, allauth 이메일 인증(email verification) 처리하기! (1) | 2021.02.12 |