루나의 TIL 기술 블로그

장고 게시판 CRUD 복습

|

게시판 CRUD하는 것을 다시 해보았다. 모든 코드는 깃 레포에서 확인할 수 있다. 게시판CRUD

장고 프로젝트 시작

  • Miniconda 가상환경 생성 및 가상환경 activate
conda create -n 가상환경이름 python=3.8
conda activate 가상환경이름
conda info --envs #설정한 가상환경 리스트 확인

데이터베이스 생성

mysql -u root -p #마이sql시작
show databases; --mysql의 모든 db들 보여주기
use [데이터베이스이름]; --사용할 db골라주기
create database [데이터베이스이름] character set utf8mb4 collate utf8mb4_general_ci; --데이터베이스 생성 및 utf8설정(한중일)
show tables; --db안의 모든 테이블들 보여주기

mysql 나가기 키는 \q 컴퓨터에 mysql응용프로그램이 켜져있으면 터미널로 열리지 않을 수 있다.

프로젝트 시작을 위한 python package 설치

pip install 패키지이름 #파이썬 패키지 설치

pip install Django #장고 설치
pip install mysqlclient # mysqlclient설치 (mysql 먼저 설치 필요!!!)
pip install django-cors-headers #CORS 해결을 위한 패키지

pip freeze #가상환경 패키지 리스트 확인

장고 프로젝트 생성

django-admin startproject 프로젝트이름 . #장고 프로젝트 생성
cd 프로젝트이름 #프로젝트 디렉토리로 들어감
python manage.py startapp 앱이름 #앱 생성(manage.py가 존재하는 디렉토리에서)

이때 프로젝트 다음에 점 기호(.)가 있음에 주의하자. 점 기호는 현재 디렉터리를 의미한다. 위 명령의 의미는 현재 디렉터리를 기준으로 프로젝트를 생성하겠다는 의미이다.

.을 안 쓰면 현재 디렉터리 밑에 같은 이름의 앱 디렉터리가 생성되어 mysite/mysite와 같은 구조가 되어 버린다.

settings.py 설정

from pathlib        import Path #기존에 settings.py 에 있는 코드
from my_settings   import DATABASES, SECRET_KEY #my_settings.py에서 가져와야한다

#시크릿 키와 데이터베이스 변수는 my_settings파일을 만들어서 갈음한다.
SECRET_KEY = SECRET_KEY # 기존의 시크릿키 변수 삭제 후 대체
DATABASES = DATABASES # 기존의 데이터베이스 변수 삭제 필수!

ALLOWED_HOSTS = ['*'] #수정 : 모두 접속 가능

APPEND_SLASH = False #추가 : 슬래시 자동으로 삽입하지 않음

INSTALLED_APPS = [
    # 'django.contrib.admin', #admin도 
    # 'django.contrib.auth', #login auth도 직접 만들어쓸 예정이다
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders', # corsheaders 추가
    'products', #앱을 새로 만들면 여기에 추가해야한다
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware', csrf 주석처리
    # 'django.contrib.auth.middleware.AuthenticationMiddleware', auth 주석처리
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware', #corsheaders middleware 추가
]

# 데이터베이스 부분 삭제
# 뒷부분 생략

##CORS 부분 추가 
CORS_ORIGIN_ALLOW_ALL=True
CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_METHODS = (
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
)

CORS_ALLOW_HEADERS = (
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
	#만약 허용해야할 추가적인 헤더키가 있다면?(사용자정의 키) 여기에 추가하면 됩니다.
)

위와 같이 settings.py를 추가 및 수정한다.

my_setting.py 파일 추가

시크릿 키와 디비정보가 깃에 공개적으로 노출되지 않도록 빼기 위해서
manage.py있는 디렉토리에 my_setting.py라는 새 파일을 만들어서 추가한다.

#클라우드에 올리지 않는, 키를 보관하는 파일
DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'westarbucks_db',
        'USER': 'root',
        'PASSWORD': '', #원하는 db 비밀번호
        'HOST': '127.0.0.1', #데이터베이스의 IP주소, 이건 각자의 컴퓨터
        'PORT': '3306',
    }
}
SECRET_KEY ='_9^=58g2r*r(6@q7ugn!fxg-!fo48$7af9i4yn9-1$+...'
#setting.py에 있던 시크릿 키를 붙여넣음

urls.py수정

from django.urls import path

urlpatterns = [
]

gitignore추가 (manage.py가 존재하는 디렉토리에)

소스를 공유하기 위해 깃을 사용하지만 올리고 싶은것 올리고 싶지 않은것, 올려서는 안되는 것들이 존재하고 이를 구분하기 위해 깃이 설치된 디렉토리에 .gitignore파일을 생성해서 관리해야 한다.

gitignore.io

위의 사이트에서 사용하는 환경에 해당하는 키워드를 선택하면, 자동으로 .gitignore 파일에 정의할 요소들을 생성해준다.

python,pycharm,visualstudiocode,vim,macos,linux,zsh

이 키워드들을 추가해서 파일을 만들고 마지막에 my_settings.py도 추가해준다.

보안관련파일과 크롤링파일

my_settings.py (보안 관련 파일은 github에 업로드되면 안된다.)

git ignore 펼쳐서 보기/접기

#보안관련파일과 크롤링파일을 위해서 추가하는 부분
my_settings.py
*.csv 
#아래부터 끝까지는 자동생성된 부분

# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,vim,macos,linux,zsh
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,visualstudiocode,vim,macos,linux,zsh

### Linux

\*~

# temporary files which can be created if a process still has a handle open of a deleted file

.fuse_hidden\*

# KDE directory preferences

.directory

# Linux trash folder which might appear on any partition or disk

.Trash-\*

# .nfs files are created when an open file is removed but is still being accessed

.nfs\*

### macOS

# General

.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r

Icon

# Thumbnails

.\_\*

# Files that might appear in the root of a volume

.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share

.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### PyCharm

# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider

# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff

.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/\*\*/shelf

# AWS User-specific

.idea/\*\*/aws.xml

# Generated files

.idea/\*\*/contentModel.xml

# Sensitive or high-churn files

.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/\*\*/dbnavigator.xml

# Gradle

.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import

# When using Gradle or Maven with auto-import, you should exclude module files,

# since they will be recreated, and may cause churn. Uncomment if using

# auto-import.

# .idea/artifacts

# .idea/compiler.xml

# .idea/jarRepositories.xml

# .idea/modules.xml

# .idea/\*.iml

# .idea/modules

# \*.iml

# \*.ipr

# CMake

cmake-build-\*/

# Mongo Explorer plugin

.idea/\*\*/mongoSettings.xml

# File-based project format

\*.iws

# IntelliJ

out/

# mpeltonen/sbt-idea plugin

.idea_modules/

# JIRA plugin

atlassian-ide-plugin.xml

# Cursive Clojure plugin

.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)

com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client

.idea/httpRequests

# Android studio 3.1+ serialized cache file

.idea/caches/build_file_checksums.ser

### PyCharm Patch

# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# \*.iml

# modules.xml

# .idea/misc.xml

# \*.ipr

# Sonarlint plugin

# https://plugins.jetbrains.com/plugin/7973-sonarlint

.idea/\*\*/sonarlint/

# SonarQube Plugin

# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin

.idea/\*\*/sonarIssues.xml

# Markdown Navigator plugin

# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced

.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/\*\*/markdown-navigator/

# Cache file creation bug

# See https://youtrack.jetbrains.com/issue/JBR-2257

.idea/$CACHE_FILE$

# CodeStream plugin

# https://plugins.jetbrains.com/plugin/12206-codestream

.idea/codestream.xml

### Python

# Byte-compiled / optimized / DLL files

**pycache**/
_.py[cod]
_$py.class

# C extensions

\*.so

# Distribution / packaging

.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
_.egg-info/
.installed.cfg
_.egg
MANIFEST

# PyInstaller

# Usually these files are written by a python script from a template

# before PyInstaller builds the exe, so as to inject date/other infos into it.

_.manifest
_.spec

# Installer logs

pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports

htmlcov/
.tox/
.nox/
.coverage
.coverage._
.cache
nosetests.xml
coverage.xml
_.cover
\*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations

_.mo
_.pot

# Django stuff:

\*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:

instance/
.webassets-cache

# Scrapy stuff:

.scrapy

# Sphinx documentation

docs/\_build/

# PyBuilder

.pybuilder/
target/

# Jupyter Notebook

.ipynb_checkpoints

# IPython

profile_default/
ipython_config.py

# pyenv

# For a library or package, you might want to ignore these files since the code is

# intended to run in multiple environments; otherwise, check them in:

# .python-version

# pipenv

# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

# However, in case of collaboration, if having platform-specific dependencies or dependencies

# having no cross-platform support, pipenv may install dependencies that don't work, or not

# install all needed dependencies.

#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow

**pypackages**/

# Celery stuff

celerybeat-schedule
celerybeat.pid

# SageMath parsed files

\*.sage.py

# Environments

.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings

.spyderproject
.spyproject

# Rope project settings

.ropeproject

# mkdocs documentation

/site

# mypy

.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker

.pyre/

# pytype static type analyzer

.pytype/

# Cython debug symbols

cython_debug/

### Vim

# Swap

[._]_.s[a-v][a-z]
!_.svg # comment out if you don't need vector files
[._]\*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]

# Session

Session.vim
Sessionx.vim

# Temporary

.netrwhist

# Auto-generated tag files

tags

# Persistent undo

[._]\*.un~

### VisualStudioCode

.vscode/_
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
_.code-workspace

# Local History for Visual Studio Code

.history/

### VisualStudioCode Patch

# Ignore all local history of files

.history
.ionide

### Zsh

# Zsh compiled script + zrecompile backup

_.zwc
_.zwc.old

# Zsh completion-optimization dumpfile

_zcompdump_

# Zsh zcalc history

.zcalc_history

# A popular plugin manager's files

.\_zinit
.zinit_lstupd

# zdharma/zshelldoc tool's files

zsdoc/data

# robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files

# (when set-up to store the history in the local directory)

.directory_history

# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files

# (for Zsh plugins using Python)

# Zunit tests' output

/tests/\_output/\*
!/tests/\_output/.gitkeep

# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,vim,macos,linux,zsh

실행

pythong manage.py runserver

requirements.txt 추가

pip freeze > requirements.txt

이렇게 manage.py가 있는 디렉토리에 설치된 라이브러리의 버전을 명시해준다.

Django==3.2.6
django-cors-headers==3.7.0
mysql-client==0.0.1
PyMySQL==1.0.2 #맥 M1의 경우 설치한 파일

장고에서 자동으로 설치되는 것을 제외하고 직접 설치한 것들만 남겨주면 좋다.

디렉토리 구조

(참고) 프로젝트 디렉토리 구조 구조
└── wantedxwecode(mysite)
    ├── manage.py
    ├── my_settings.py
    ├── READEME.md
    ├── requirements.txt
    └── wantedxwecode(mysite)
        ├── \__init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py
    └── wanted(myapp)
        ├── \__init__.py
        ├── admin.py
        ├── apps.py
        ├── models.py
        ├── urls.py(추가)
        ├── decorator.py(추가)
        ├── tests.py
        └── views.py

Model.py

필요한 게시판의 데이터 관계도를 만들고 새로 생성된 앱의 models.py에 아래 migration을 넣어서 데이터베이스 토대를 만들어준다.

#project > wanted > models.py
from django.db import models

class User(models.Model):
    name         = models.CharField(max_length=40, null=True)
    email        = models.EmailField(max_length=200, unique=True)
    password     = models.CharField(max_length=200)

    class Meta:
        db_table = 'users'

class Post(models.Model): 
    user        = models.ForeignKey(User, on_delete=models.CASCADE, db_column='user_id')
    text        = models.CharField(max_length=300)
    created_at  = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        db_table = 'posts'

migration 만들고 migrate 실행하기

python manage.py makemigrations 
#모델의 변경사항을 파악, 설계도 작성(하고나서 만들어진 파일을 잘 살펴본다)
python manage.py showmigrations 
#현재 migrations가 어떤 상태인지 살펴보기
python manage.py sqlmigrate [앱이름] [마이그레이션번호]
#실제 데이터베이스에 전달되는 SQL 쿼리문을 확인
python manage.py migrate 
#자동으로 migration을 실행

migration을 만드는 것과 migrate는 각각 클래스에 맞게 설계도를 만들고 설계한대로 데이터베이스를 건설하겠다는 뜻이라고 할 수 있다.

SQL

select * from django_migrations; --장고 마이그레이션 보여주기
desc posts; --포스트 테이블이 잘 만들어졌는지 보여준다

View.py 회원가입, 로그인 구현

#mysite > users > views.py
import json, re, bcrypt, jwt

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

from users.models         import User  

from my_settings          import SECRET_KEY, const_algorithm

class SignUp(View):
    def post(self, request):
        try:
            data            = json.loads(request.body)
            email           = data['email']
            password        = data['password']
            hashed_password = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())

            if User.objects.filter(email=email).exists():
                return JsonResponse({"MESSAGE": "EMAIL_ALREADY_EXIST"}, status=400)

            if not re.match(r"^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email):
                return JsonResponse({"MESSAGE": "INVALID_FORMAT"}, status=400)

            if not re.match(r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$", password):
                return JsonResponse({"MESSAGE": "INVALID_FORMAT"}, status=400)

            User.objects.create(
                name     =   data.get('name'), #선택적으로 입력받을 때
                email    =   email,
                password =   hashed_password.decode('UTF-8'),
            )
            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)      
            email    = data['email']
            password = data['password']        

            if not User.objects.filter(email = email).exists():
                return JsonResponse({'MESSAGE':'INVALID_VALUE'}, status = 401)

            if bcrypt.checkpw(password.encode('utf-8'),User.objects.get(email=email).password.encode('utf-8')):
                token = jwt.encode({'id':User.objects.get(email=email).id}, SECRET_KEY)
            
                return JsonResponse({'TOKEN': token}, status = 200)

            return JsonResponse({'MESSAGE':'INVALID_USER'}, status=401)

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

View.py 게시글 기능 구현

import json, re, bcrypt, jwt

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

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

from .models               import User
from .models               import Post as PostModel

from my_settings           import SECRET_KEY

from wanted.decorator      import login_decorator

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

            PostModel.objects.create( #디비에 값을 추가
                user_id  = user.id, #요청을 수행하는 유저의 아이디 
                text     = data["text"]#입력받은 값
            )
            return JsonResponse({"message": "SUCCESS"}, status=201)

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

    def get(self, request):
            post_list = PostModel.objects.all().order_by('id')
            paginator = Paginator(post_list, 3)
            # 한 페이지당 오브젝트 3개씩 나오게 설정
            page      = int(request.GET.get('page',1))
            # page라는 값으로 받을거고, 없으면 첫번째 페이지로

            try:
                posts = paginator.page(page)
            except PageNotAnInteger:
                posts = paginator.page(1)
            except EmptyPage:
                posts = paginator.page(paginator.num_pages)

            results = []

            results.append([{
                        "post_id"    : post.id,
                        "user_id"    : post.user_id,#글 객체의 유저아이디
                        "text"       : post.text,
                        "created_at" : post.created_at,
                    } for post in posts ])
            return JsonResponse({"page" : page, "results": results}, status=200)

class PostModify(View):
    @login_decorator
    def patch(self,request, post_id):
        try:
            data = json.loads(request.body)
            post = PostModel.objects.get(id=post_id)
            
            if post.user_id == request.user.id : #요청하는 유저가 글 쓴 사람이라면
                PostModel.objects.filter(id=post_id).update( 
                    text     = data["text"]
                )
                return JsonResponse({"message": "SUCCESS"}, status=201)
            else:
                return JsonResponse({"message": "NOT_AUTHORIZED"}, status=403)
        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)
    
    @login_decorator
    def delete(self,request, post_id):
        try:
            post = PostModel.objects.get(id=post_id)
            if post.user_id == request.user.id:
                post.delete()
                return JsonResponse({"message": "SUCCESS"}, status=201)
            else:
                return JsonResponse({"message": "NOT_AUTHORIZED"}, status=403)
        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)

로그인 데코레이터

import jwt

from django.http            import JsonResponse
from django.core.exceptions import ObjectDoesNotExist

from my_settings            import SECRET_KEY
from .models                import User

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        #들어올 수 있는 인자를 모두 받도록 한다
        try:
            token         = request.headers.get("Authorization", None)
            #헤더에서 Authorization(헤더에 있는 속성)을 가져와서 토큰에 저장한다.
            user          = jwt.decode(token, SECRET_KEY, algorithms='HS256')
            #토큰을 시크릿키를 이용해 디코드해서 유저 아이디를 알아내서 유저에 저장한다.
            request.user  = User.objects.get(id = user['id'])
            #유저의 아이디에 해당하는 유저객체를 리퀘스트.유저에 저장한다.

            return func(self, request, *args, **kwargs)
            #받은 인자들을 모두 전달해준다(예를 들어 이미지, 텍스트 등등)

        except jwt.exceptions.DecodeError:
            return JsonResponse({"message" : "INVALID_TOKEN"}, status=400)

        except ObjectDoesNotExist:
            return JsonResponse({"message" : "INVALID_USER"}, status=400)

    return wrapper

4. Urls.py 작성

클라이언트의 요청을 받아서 게시판 뷰를 호출할 수 있도록 urls.py 를 작성해야합니다.

#상위 프로젝트 폴더의 urls.py
from django.urls import path,include

urlpatterns = [
    path("",include("wanted.urls")),
]

#하위 post앱 폴더의 urls.py
from django.urls import path
from .views      import Post, PostModify, SignUp, SignIn

urlpatterns = [
    path("post", Post.as_view()),
    path("post/<int:post_id>", PostModify.as_view()),
    path("signup", SignUp.as_view()),
    path("signin", SignIn.as_view()),
]

POSTMAN에서 회원가입으로 아이디를 하나 만들고 (비밀번호 유효성 검사 8자이상, 알파벳, 숫자 포함 필요) 글을 작성해보면 맨 위 그림과 같이 잘 작동되는 것을 볼 수 있다.