Django ⎮ 게시판 CRUD

Chris-Yang·2021년 10월 30일
1

Django

목록 보기
5/7
post-thumbnail
post-custom-banner

> End Point

- 회원가입 : POST/users/signup
- 로그인 : POST/users/signin
- 게시물 목록 : GET/bulletin-board/post
- 게시물 작성 : POST/bulletin-board/post
- 게시물 수정 : PATCH/bulletin-board/post/int:post_id
- 게시물 삭세 : DELETE/bulletin-board/post/int:post_id
- 게시물 상세 : GET/bulletin-board/post-detail/int:post_id

🥕 github:
https://github.com/chrisYang256/wnated-wecode-FreeOnboarding




> users App

▶︎ 특이사항

회원가입 및 로그인한 유저만 게시글 작성, 수정, 삭제가 가능하도록 설계하여
회원가입 및 로그인 데코레이터를 통한 인증, 인가 기능을 추가하였습니다.


▶︎ code

- models.py

from django.db               import models

class User(models.Model):
    name       = models.CharField(max_length = 20)
    password   = models.CharField(max_length = 200)
    email      = models.EmailField(max_length = 50, unique = True)
    created_at = models.DateTimeField(auto_now_add = True)
    updated_at = models.DateTimeField(auto_now = True)
        
    class Meta:
        db_table = 'users'

- views.py

import json
import re
import bcrypt
import jwt

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

from my_settings import SECRET_KEY, ALGORITHMS
from users.models import User

class SignUp(View):
    def post(self, request):
        data          = json.loads(request.body)
        REGX_EMAIL    = '^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
        REGX_PASSWORD = '^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$'

        try:
            if User.objects.filter(email=data['email']).exists():
                return JsonResponse({'message': 'EXIST_EMAIL'}, status=400)

            if not re.match(REGX_EMAIL, data['email']):
                return JsonResponse({'message': 'INVALID_EMAIL_FORM'}, status=400)

            if not re.match(REGX_PASSWORD, data['password']):
                return JsonResponse({'message': 'INVALID_PASSWORD_FORM'}, status=400)

            password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
            
            User.objects.create(
                name     = data['name'],
                email    = data['email'],
                password = password,
            )

            return JsonResponse({'message': 'SUCCESS'}, status=201)
        
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

class SignIn(View):
    def post(self, request):
        try:
            data = json.loads(request.body)
            user = User.objects.get(email = data['email'])

            if not (user and bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8'))):
                return JsonResponse({"message" : "INVALID_USER"}, status=401)

        except KeyError:
            return JsonResponse({"message" : "KEY_ERROR"}, status=400)

        access_token = jwt.encode({'user_id' : user.id}, SECRET_KEY, ALGORITHMS)

        return JsonResponse({'access_token' : access_token}, status=201)

- utils.py

import jwt

from django.http            import JsonResponse

from my_settings  import SECRET_KEY, ALGORITHMS
from users.models import User

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        if 'Authorization' not in request.headers: 
            return JsonResponse ({'message' : 'UNAUTHORIZED'}, status=401)

        access_token = request.headers.get('Authorization')
        
        try:
            payload      = jwt.decode(access_token, SECRET_KEY, algorithms=ALGORITHMS)
            user         = User.objects.get(id=payload['user_id'])
            request.user = user

        except jwt.exceptions.DecodeError:
            return JsonResponse({'MESSAGE': 'INVALID_TOKEN'}, status=401)

        except User.DoesNotExist:
            return JsonResponse({'MESSAGE': 'INVALID_USER'}, status=401)

        return func(self, request,  *args, **kwargs)

    return wrapper

- urls.py

from django.urls import path

from users.views import SignIn, SignUp

urlpatterns = [
    path('/signup', SignUp.as_view()),
    path('/signin', SignIn.as_view()),
]

▶︎ Divide & Conquer

- SignUp API

python의 re.match()를 이용하여 정규표현식과 입력값을 대조하여
예외처리를 하였습니다.

- utils.py

login_decorator함수가 데코레이터로 붙은 API들은 토큰 검사 후 토큰을 decode하여
user_id를 request.user에 담아 넘겨줌으로써 유효성 검사 및 HTTP stateless를
해결하였습니다.




> bulletin_board App

▶︎ 특이사항

RESTful API를 준수하였습니다.
게시물 보기 외에는 모두 인가 관련 예외처리를 하였습니다.


▶︎ code

- models.py

from django.db               import models

class BulletinBoard(models.Model):
    author      = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='bulletin_board')
    title       = models.CharField(max_length=100)
    description = models.CharField(max_length=5000)
    created_at  = models.DateTimeField(auto_now_add=True)
    updated_at  = models.DateTimeField(auto_now=True)
        
    class Meta:
        db_table = 'bulletin_boards'

- views.py

import json
import time
import datetime

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

from users.utils  import login_decorator
from .models      import BulletinBoard
from users.models import User
    
class Post(View):
    def get(self, request):
        limit  = int(request.GET.get('limit', 5))
        offset = int(request.GET.get('offset', 0))

        limit  = limit + offset

        post_list = BulletinBoard.objects.all()[offset:limit]

        results = [
            {
            'author'     : post.author.name if post.author else "사라진 회원입니다.",
            'title'      : post.title,
            'created_at' : post.created_at
        } for post in post_list]

        return JsonResponse({'results' : results}, status=200)

    @login_decorator
    def post(self, request):
        try:
            data = json.loads(request.body)

            if not User.objects.filter(id=request.user.id):
                return JsonResponse({'message' : 'INVALID_USER'}, status=404)

            BulletinBoard.objects.create(
                    author_id   = request.user.id,
                    title       = data['title'],
                    description = data['description'],
                    created_at  = data['created_at']
            )

            time.sleep(1)

            return JsonResponse({'message' : 'SUCCESS'}, status=201)

        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

    @login_decorator
    def patch(self, request, post_id):
        try:
            data = json.loads(request.body)
            post = BulletinBoard.objects.get(id=post_id)

            if not post.author_id == request.user.id:
                return JsonResponse({'message' : 'INVALID_USER'}, status=404)

            post.title       = data['title']
            post.description = data['description']
            post.updated_at  = datetime.datetime.now()
            post.save()

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

        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400) 

        except BulletinBoard.DoesNotExist:
                return JsonResponse({'message' : 'INVALID_POST'}, status=404)  

    @login_decorator
    def delete(self, request, post_id):
        try:
            post = BulletinBoard.objects.get(id = post_id)

            if not post.author_id == request.user.id:
                return JsonResponse({'message' : 'INVALID_USER'}, status=404)

            BulletinBoard.objects.get(id=post_id).delete()

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

        except BulletinBoard.DoesNotExist:
                return JsonResponse({'message' : 'INVALID_POST'}, status=404)
            
class PostDetail(View):
    def get(self, request, post_id):
        try:
            post = BulletinBoard.objects.get(id=post_id)

            results = {
                'author'      : post.author.name if post.author else "사라진 회원입니다.",
                'title'       : post.title,
                'description' : post.description,
                'created_at'  : post.created_at,
                'updated_at'  : post.updated_at
            }

            return JsonResponse({'results' : results}, status=200)

        except BulletinBoard.DoesNotExist:
                return JsonResponse({'message' : 'INVALID_POST'}, status=404)

- urls.py

from django.urls import path

from .views import Post, PostDetail

urlpatterns = [
    path('/post', Post.as_view()),
    path('/post/<int:post_id>', Post.as_view()),
    path('/post-detail/<int:post_id>', PostDetail.as_view()),
]


▶︎ Divide & Conquer

- models.py의 BulletinBoar 테이블

author colum의 옵션으로 on_delete=models.SET_NULL을 설정하여 해당 글을
작성한 회원이 삭제되어도 글이 삭제되지 않도록 설계하였습니다.


- POST API의 get

BD에서 삭제된 회원의 경우 3항연산자를 이용하여 게시물 정보를 return할 때
"사라진 회원입니다."라는 텍스트를 제공하도록 하였습니다.


- POST API의 post

python의 time.sleep()을 사용하여 게시물 등록 시간을 임의로 지연시켜
DDos공격을 차단하는 개념을 아주 간단하게 표현해 보았습니다.


- POST API의 patch

게시물의 일부 데이터를 수정하기 때문에 PUT이 아닌 PATCH 메소드를 사용했습니다.




🌈 작은 회고 🤔

오랜만에 써보는 작은 회고인 것 같습니다.

얼마 전부터 글을 반말에서 존대로 바꿨습니다.

글자 수를 줄이는게 효율적이고 보기에 더 좋을 것 같아 반말컨셉을 잡은건데
건방져보이는 감이 있어서 살짝 불편했었기에 바꿔봤는데 마음이 평화로워집니다.

파이썬과 장고를 배우며 처음으로 하나의 프로젝트에 온전한 CRUD를 전부
구현해 본 것 같습니다.

아니 정확히는 코딩을 배우면서 처음인 것 같습니다.

그러면서 RESTful API도 처음으로 제대로 적용해 본 것 같습니다.

URI를 POST/post-write와 같이 각각 설정하고 API를 모두 분리해 놓았었는데
제대로 REST를 파괴한 흑역사임과 동시에 잊혀지지 않을 배움이었습니다.

전체적으로 미숙한데 code convetion도 지키려고 몇번을 다시 검토하느라
시간이 좀 걸린것도 기억에 남는데 깔끔한 코드를 보니 행복해져 옵니다.

작지만 큰 한발짝을 내딛은 것 같은 이 뿌듯함을 또 경험할 수 있기를..! ❤️‍🔥

profile
sharing all the world
post-custom-banner

0개의 댓글