루나의 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자이상, 알파벳, 숫자 포함 필요) 글을 작성해보면 맨 위 그림과 같이 잘 작동되는 것을 볼 수 있다.

프로그래머스 level 2 타겟넘버 BFS

|

문제 설명

n개의 음이 아닌 정수가 있습니다. 이 수를 적절히 더하거나 빼서 타겟 넘버를 만들려고 합니다. 예를 들어 [1, 1, 1, 1, 1]로 숫자 3을 만들려면 다음 다섯 방법을 쓸 수 있습니다.

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

사용할 수 있는 숫자가 담긴 배열 numbers, 타겟 넘버 target이 매개변수로 주어질 때 숫자를 적절히 더하고 빼서 타겟 넘버를 만드는 방법의 수를 return 하도록 solution 함수를 작성해주세요.

제한 조건

주어지는 숫자의 개수는 2개 이상 20개 이하입니다.
각 숫자는 1 이상 50 이하인 자연수입니다.
타겟 넘버는 1 이상 1000 이하인 자연수입니다.

입출력 예

numbers target return
[1,1,1,1,1] 3 5

사고 과정

타겟넘버

0으로 시작해서 다음 수를 더하거나 빼는 경우의 수를 가지 2개로 뻗어서 계속 가지를 뻗어나가는 결과를 리스트에 보관했다가 마지막에 count로 원하는 결과값의 갯수를 세어준다.

제출 코드

def solution(numbers, target):
    sup = [0]
    #전체 노드의 합을 더해준 리스트를 super라고 하고 초기화해준다
    for i in numbers:
        # i가 numbers의 원소 길이만큼 반복하면서 sub이라는 배열을 생성한다
        sub = []
        for j in sup:
            # j가 sup의 원소만큼 반복한다. 그러니까 +인 경우, -인 경우에 대해서 반복한다  
            sub.append(j+i)
            sub.append(j-i)
            #그 경우에 대한 값을 sub에 넣어둔다 
        sup = sub
        #하나의 노드에 대한 탐색이 끝나면 반복이 끝나서 sub에서 계산한 값을 sup으로 덮어둔다
        #이렇게 2중 반복을 마치게되면 sup에는 모든 경우에 대해서 +인 경우, -인 경우를 조합한 합을 가지게 된다
    return sup.count(target)

queue 답안

from collections import deque
#collections 모듈의 deque는 double-ended queue의 약자로 데이터를 양방향에서 추가하고 제거할 수 있는 자료 구조이다.
 
def solution(numbers, target):
    answer = 0
    queue = deque() #queue 생성
    
    length = len(numbers)
    queue.append([-numbers[0], 0])
    queue.append([+numbers[0], 0])
    
    while queue :
        num, i = queue.popleft()
        #popleft()는 리스트의 첫 번째 데이터를 제거
        if i+1 == length :
            if num == target : answer += 1
        else :
            queue.append([num - numbers[i + 1], i + 1])
            queue.append([num + numbers[i + 1], i + 1])
    
    return answer

참고

배우고 정리하기 - level 2 - 타겟 넘버 ( python ) by 아뜨으츄 아뜨으츄

그럼에도 불구하고 - 코딩테스트 고득점 Kit DFS/BFS1 타겟넘버

파이썬에서 큐(queue) 자료 구조 사용하기

BFS/DFS 설명하는 예전 내 블로그 글

프로그래머스 level 2 가장 큰 수, 정렬

|

문제 설명

0 또는 양의 정수가 주어졌을 때, 정수를 이어 붙여 만들 수 있는 가장 큰 수를 알아내 주세요.

예를 들어, 주어진 정수가 [6, 10, 2]라면 [6102, 6210, 1062, 1026, 2610, 2106]를 만들 수 있고, 이중 가장 큰 수는 6210입니다.

0 또는 양의 정수가 담긴 배열 numbers가 매개변수로 주어질 때, 순서를 재배치하여 만들 수 있는 가장 큰 수를 문자열로 바꾸어 return 하도록 solution 함수를 작성해주세요.

제한 조건

numbers의 길이는 1 이상 100,000 이하입니다.
numbers의 원소는 0 이상 1,000 이하입니다.
정답이 너무 클 수 있으니 문자열로 바꾸어 return 합니다.

입출력 예

numbers return
[6,10,2] [3,30,34,5,9]
"6210" "9534330"

사고 과정

앞자리를 비교해서 큰 순서대로 새 리스트에 넣어주고 앞자리가 같은 경우에는 전체 숫자를 비교해서 큰 순서대로 넣어준다.

def solution(numbers):
    arr={}
    answer = []
    #맨 앞 숫자를 따서 큰 순서대로 답에 넣어주고 후보리스트에서 제거
    for number in numbers:
        arr.append(number,(str(number)[0]))
        answer.append(max(arr))
        numbers.remove(int(max(arr)))
        #숫자가 같으면 나머지 전체 숫자를 크기대로 정렬해서 답에 넣어주기
        if numbers.count(str(number[0])) > 1:
            numbers = sorted(numbers, reverse=True)
            answer.append(max(arr))
            numbers.remove(int(max(arr)))
    answer.join('')
    return answer

solution([3,30,34,5,9])

이렇게 짜니까 어떤 수가 어떤 수의 앞자리인 지 알 수 있어야해서 {맨 앞자리수 : 원래 숫자} 이런 식으로 딕셔너리로 리스트에 넣어줘야할 것 같았다.

그렇게 하지 않고 숫자의 크기를 비교할 수 없을까 생각하다가 검색을 해봤는데 다들 숫자가 최대 3자리 수라는 점을 이용해서 원래 숫자를 세 번 반복한 뒤에 문자열끼리 비교하면 아스키코드값에서 첫번째 인덱스 값으로 비교한다는 점을 이용해서 숫자의 크기를 비교하는 방법을 사용한 것을 볼 수 있었다.

def solution(numbers):
    answer = []
    new_nums =[]
    for num in numbers:
        new_nums.append(str(num)*3)
    new_nums = sorted(new_nums, reverse=True)
    print(new_nums) # ['999', '555', '343434', '333', '303030']

    for number in new_nums:
        answer.append(str(number)/3)

    return answer

solution([3,30,34,5,9])

그렇게 혼자 코드를 짜보려고 했는데 3번 반복해서 썼던 수를 다시 원래대로 돌릴 수가 없었다..!

제출 답안

def solution(num): 
    num = list(map(str, num)) 
    num.sort(key = lambda x : x*3, reverse = True) 
    return str(int(''.join(num)))

그래서 다른 블로그에 쓰인대로 람다를 사용했다.
개발개발 울었다에 나와있는 설명을 참고하자면 아래와 같다.

  • int형의 list를 map을 사용하여 string으로 치환한 뒤, list로 변환한다.
  • 변환된 num을 sort()를 사용하여 key 조건에 맞게 정렬한다.
  • lambda x : x3은 num 인자 각각의 문자열을 3번 반복한다는 뜻이다. x3을 하는 이유? -> num의 인수값이 1000 이하이므로 3자리수로 맞춘 뒤, 비교하겠다는 뜻. 이 문제의 핵심이라고 할 수 있다.
  • 문자열 비교는 ASCII 값으로 치환되어 정렬된다. 따라서 666, 101010, 222의 첫번째 인덱스 값으로 비교한다. 6 = 86, 1 = 81, 2 = 82 이므로 6 > 2 > 1순으로 크다.
  • sort()의 기본 정렬 기준은 오름차순이다. reverse = True 전의 sort된 결과값은 10, 2, 6이다.
  • 이를 reverse = True를 통해 내림차순 해주면 6,2,10이 된다. 이것을 ‘‘.join(num)을 통해 문자열을 합쳐주면 된다.
  • int로 변환한 뒤, 또 str로 변환해주는 이유? 모든 값이 0일 때(즉, ‘000’을 처리하기 위해) int로 변환한 뒤, 다시 str로 변환한다. 출처 : 개발개발 울었다

Django에서 API문서 관리 Swagger 사용법

|

스웨거 예시

스웨거

스웨거 설치

django-rest-swagger 패키지는 더이상 관리안해서 drf-yasg를 쓰도록 추천한다고 한다.
출처: https://hyeonyeee.tistory.com/66 [hyeoneee’s blog]

pip install -U drf-yasg

파일 설정

INSTALLED_APPS = [
   ...
   'drf_yasg',
   ...
]

‘django.contrib.auth’ 부분 주석처리했으면 풀어주기

urls.py

#urls.py
from django.urls import path, re_path, include

from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg       import openapi, generators

class BothHttpAndHttpsSchemaGenerator(generators.OpenAPISchemaGenerator):
    def get_schema(self, request=None, public=False):
        schema = super().get_schema(request, public)
        schema.schemes = ["http", "https"]
        return schema

urlpatterns = [
    path('users', include('users.urls')),
    path('applications', include('applications.urls')),
    path('recruits', include('recruits.urls')),
    path('', include('helloworld.urls')),
]

schema_view = get_schema_view(
    openapi.Info(
        title            = "******** API", #타이틀
        default_version  = "v1", #버전
        description      = "******** API 문서", #설명
        license          = openapi.License(name=""),
    ),
    public             = True,
    permission_classes = (permissions.AllowAny,),
    generator_class    = BothHttpAndHttpsSchemaGenerator,
)

urlpatterns += [
    re_path(r'swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name="schema-json"),
    re_path(r'swagger', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    re_path(r'redoc', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

뷰에 데코레이터 작성하기

class SuperAdminView(APIView):
    parameter_token = openapi.Parameter (
                                        "Authorization", 
                                        openapi.IN_HEADER, 
                                        description = "access_token", 
                                        type        = openapi.
                                        TYPE_STRING
    )
    supaeradmin_get_response = openapi.Response("result", SuperadminGetSerializer)
    #이 부분에 대한 설명 : https://swagger.io/docs/specification/describing-parameters/

    @swagger_auto_schema (
            manual_parameters = [parameter_token],
            responses = {
                "200": supaeradmin_get_response,
                "400": "BAD_REQUEST",
                "401": "INVALID_TOKEN"
            },
            operation_id = "(슈퍼관리자 전용)어드민 정보 조회",
            operation_description = "header에 토큰이 필요합니다."
        )
    #http://localhost:8000/swagger-ui 상에서 보여질 부분

    @superadmin_only
    def get(self, request):    

        result = [{
            "id"        : admin.id,
            "email"     : admin.email,
            "name"      : admin.name if admin.name else admin.email.split('@')[0],
            "created_at": admin.created_at,
            "updated_at": admin.updated_at,
        } for admin in User.objects.filter(role='admin') ]

        return JsonResponse({"result": result}, status=200)
    #실제 함수

이렇게 하면 서버를 구동했을 때 스웨거 페이지(http://localhost:8000/swagger-ui) 상에 표시가 되고 페이지 상에서 데이터를 입출력!!!해볼 수 있다.

스웨거

배포가 되면 스웨거가 이렇게 오픈이 되서 서버구동 없이도 url을 통해서 아무나 접근할 수도 있다.
https://api-we.stockfolio.ai/swagger

로그인 - jwt, Redis, stateless vs stateful, cookie vs session

|

회원가입/로그인이 이루어질 때 jwt를 발급하고 이를 통해 유저를 알아본다는 것까지 학원에서 실습 및 구현을 했었다. 기업협업 중에 admin/superadmin 데코레이터를 만드는데 pay_load에 넣은 role이 반영되지 않아서 하루동안 고민했는데 사수분께서 pay_load에 뭔가 새로 추가했으면 엑세스토큰을 다시 발급받았어야된다면서 jwt의 구성요소에 대해서 설명해주고 아래 개념들에 대해서 공부해보라고 하셨다.

참고로 첨부하는 admin_only 데코레이터

def admin_only(func):
    def wrapper(self, request, *args, **kwargs):
        try:
            access_token = request.headers.get('Authorization')
            pay_load     = jwt.decode(access_token, SECRET_KEY, algorithms=[ALGORITHM])
            role         = pay_load['role']
            user         = User.objects.get(id=pay_load['user_id'])
            request.user = user
            
            if not role == 'admin':
                return JsonResponse({'message': 'UNAUTHORIZED'}, status=401)

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

        except jwt.InvalidTokenError:
            return JsonResponse({'message': 'INVALID_TOKEN'}, status=401)
        except jwt.exceptions.DecodeError:
            return JsonResponse({'message': 'DECODE_ERROR'}, status=400)
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message': 'EXPIRED_TOKEN'}, status=401)
        except User.DoesNotExist:
            return JsonResponse({'message': 'USER_DOES_NOT_EXISTS'}, status=401)
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

jwt

JWT에 대해서 알고자 한다면 Auth0에서 만든 JWT 사이트가 참고하기 제일 좋다. 이 사이트에서 JWT 토큰을 테스트해보거나 구조도 파악해 볼 수 있고 언어별로 추천 라이브러리와 지원상태를 한눈에 볼 수 있어서 믿고 쓸 수 있고 JWT에 대한 보안 취약점도 잘 보고되고 있다.

jwt구성요소
https://jwt.io/

마침표를 구분자로 세 부분으로 나뉘어져있고 header에는 타입과 jwt signing 알고리즘(이 방법으로 토큰을 생성하겠다), 페이로드에는 담아서 전달하고 싶은 데이터가 들어있고 마지막으로 세트가 변조되었는지 확인하는 부분인 Signature이 있다.

JOSE 헤더와 JWT Claim Set를 base64로 인코딩해서 만든 두 값을 마침표(.)로 이어 붙이고 지정한 알고리즘 HS256으로 인코딩한 것이 JWT 토큰의 세 번째 부분인 Signature이다. 출처 : 아웃사이더의 데브 스토리

헤더와 JWT Claim Set은 암호화를 한 것이 아니라 단순히 JSON문자열을 base64로 인코딩한 것뿐이다. 그래서 누구나 이 값을 다시 디코딩하면 JSON에 어떤 내용이 들어있는지 확인할 수 있다. base64를 디코딩해보고 싶을 때는 아래에서 해봐도 된다. https://www.base64decode.org/

stateless vs stateful

서버가 클라이언트(사용자)의 정보를 저장하느냐 아니냐에 따라 stateful(상태유지)와 stateless(무상태)로 나눌 수 있다. 이 블로그의 자전거를 구매하는 대화의 예시를 통해 무상태와 상태유지의 차이를 쉽게 알 수 있다!

예를 들어서 stateless(무상태)는 서버가 아무것도 기억하지 않고 stateful(상태유지)는 클라이언트가 전에 했던 요청을 서버가 다 기억하고 있는 것이다. 무상태는 클라이언트의 요청으로 더 많은 데이터를 소모하지만 서버의 확장성이 높다. 상대적으로 특별한 일이 없다면 무상태를 지향해야한다.

HTTP는 statelss, connectless 즉 클라이언트의 요청을 저장하지 않고 연결된 상태로 있지 않는다. 무상태를 디폴트로 로그인을 유지하는 등의 기능이 필요할 때 쿠키와 세션을 사용한다.

헨젤과 그레텔이 쿠키를 조금씩 떨어뜨려서 길을 찾았다는 이야기로부터 이름을 따온 쿠키는 클라이언트에 저장된 사용자의 정보를 의미한다. 크롬의 자물쇠 표시를 누르면 저장된 쿠키들을 볼 수 있다. + 크롬의 시크릿모드는 창을 끄는 순간 쿠키가 지워진다.

2000년대 초반까지도 쿠키정보가 암호화되지 않아서 해킹에 취약했다고 한다. 그래서 생겨난 것이 정보를 웹 서버에 저장하는 세션이다. + 브라우저가 종료되면 세션도 만료된다.

쿠키

쿠키

  • 이름, 값, 만료일(저장 기간 설정), 경로 정보로 구성되어 있다.
  • 클라이언트에 총 300개의 쿠키를 저장할 수 있다, 도메인 당 20개의 쿠키를 가질 수 있다
  • 하나의 쿠키는 4KB(=4096byte)까지 저장 가능하다.

쿠키 사용 예시로는 방문했던 사이트에 다시 방문 하였을 때 아이디와 비밀번호 자동 입력, “오늘 이 창을 다시 보지 않기” 체크가 있다.

브라우저가 Request를 보낼 때 함께 보내는 쿠키의 값들은 장고에서 request.COOKIE 객체를 통해 접근 가능하다.

쿠키와 세션의 작동 방식

쿠키와 세션

세션

  • 웹 서버에 웹 컨테이너의 상태를 유지하기 위한 정보를 디비나 메모리에 저장한다.
  • 브라우저를 닫거나, 서버에서 세션을 삭제했을때만 삭제가 되므로 쿠키보다 보안이 좋다. 서버의 리소스를 쓰므로 비용이 있다.
  • 각 클라이언트 고유 Session ID를 부여한다, Session ID로 클라이언트를 구분하여 각 클라이언트 요구에 맞는 서비스 제공한다.

화면이 이동해도 로그인이 풀리지 않고 로그아웃하기 전까지 유지하는 것이 세션의 예시이다.

해당 브라우저와의 연결 상태를 기억하고 있는 세션 저장소는 장고에서 request.session객체를 통해 접근 가능하다. 장고에 SessionMiddleware라는 기본 미들웨어가 있는데 이는 클라이언트가 보내는 요청마다 request.session 속성에 이미 생성되어있는 세션 객체를 연결해준다. 세션이 생성되어있지 않은 상태라면 빈 껍데기를 연결해주고 session_key필드값이 None이다. 장고의 세션 프레임워크 튜토리얼

장고의 세션엔진 대용으로 Redis를 사용할 수 있고 Redis를 사용하면 성능이 향상된다. 보통 장고에서는 memcached 등을 사용해서 캐시를 설정하는데 Redis를 사용할 경우 memcached를 사용했을 때와 비교하여 손색이 없을정도라고 한다.

Redis

한 마디로 메모리를 이용하여 고속으로 <key, value> 스타일의 데이터를 저장하고 불러올 수 있는 시스템이라고 할 수 있다. 2009년 살바토르 산필리포가 개발한 오픈 소스 기반 비관계형 데이터베이스 관리 시스템으로 데이터베이스를 쓸 때 입출력에 시간이 걸리기 때문에 메모리 기반의 저장소에 캐시처럼 정보를 저장해놓고 빠르게 입출력할 수 있도록 만든 것이다.

Redis를 이용하면 MySQL보다 10배 빠를 수 있지만 메모리는 휘발성이기때문에 시스템이 꺼지면 모든 데이터는 날아가므로 Redis는 임시 데이터를 저장하는데 사용한다. -> 09.07수정 Redis에서 비휘발성으로 설정할 수도 있다.redis를 장고 세션 저장으로 사용하기(영문)

Django REST framework 1 Serializer, ApiView

|

위코드 22기 분들이 한 달동안 작성한 채용 관리 사이트를 drf로 리팩토링하라는 업무를 받음으로서 Django REST framework, 장고 레스트 프레임워크를 처음 접하게 됐다. 코드를 읽어보고 공식 문서와 블로그를 둘러보면서 알아보려고 하다가 모르겠어서 위코드의 멘토님께 강의를 추천받아서 들었더니 첫 기본 개념이 좀 잡혔다.

동기의 말에 따르면 학원에서 만들었던 퓨어장고는 레고와 같고 DRF는 철근과 같다고 한다. 멘토님의 말에 따르면 DRF를 사용하면 더 쉽고 빠르게 기능을 구현할 수 있고 틀이 정해져있어서 대부분의 개발자가 같은 형식을 공유하게 되고 에러가 날 여지가 더 적다고 한다.

코드를 뜯어보면 상속과 클래스를 통해 장고에서 만들어놓은 기본 기능을 가져다씀으로써 라우팅과 같은 기본 기능보다 상태별 모델링이나 API의 상호작용에 더 집중할 수 있도록 하는 것 같다.

Django Rest Framework

웹에서 모바일로 사용환경이 변화함에 따라서 HTML뿐만 아니라 JSON으로 데이터를 처리하는 것이 필요해졌다.

REST(representational state transfer)는 HTTP의 URL과 HTTP method(GET, POST, PUT, DELETE)를 사용하여 API 사용 가독성을 높인 구조화된 시스템 아키텍쳐(프레임워크)이고

REST의 설계 원칙으로는 서버와 클라이언트의 존재, Stateless, Uniform Interface등 다양한 조건이 존재하지만 현대 HTTP통신에서는 JSON 형식으로 데이터를 주고받기 때문에 self-descriptive의 조건을 만족하지 못해서 REST의 의도를 벗어난다고 한다.

API(Application Program Interface)는 request, response로 오가는 구조화된 데이터를 의미하고 클라이언트와 서버 간의 메신저, 매개체 역할을 통해 서로간의 데이터를 특정 형식에 맞게 전달하는 역할을 한다.

RESTful API는 이러한 RESTful의 개념과 API를 합쳐서 REST 설계 원칙을 따르는 API를 의미하며, 우리는 RESTful API를 통해 HTTP로 CRUD 등의 기능을 수행하는 API를 개발할 수 있다.

Django REST framework(이하 DRF)는 장고안에서 Restful API 서버를 쉽게 구축할 수 있도록 도와주는 오픈소스 라이브러리이다.

추천해주신 강의를 보고 개념을 이해하고 여러가지 블로그를 보고 정리했다.

내가 여태까지 이해한 바로 DRF의 주요 기능에는 다음과 같은 것들이 있다.

  • Serializer : data를 json으로 직렬화해준다. (API 디버깅을 쉽게 만들어주며 코드를 정리해서 보안 이슈를 해결하기도 하며 validation도 검증해준다.)
  • ModelSerializer : 모델에 serializer를 적용할 때 사용한다.
  • Api View : View를 기반으로 Restful API를 더 쉽게 작성할 수 있게 해준다. 함수기반 Function based View, 클래스기반 Class based View가 있다.
  • GenericAPIView, Mixins : CRUD기능이 미리 만들어져있어서 가져다 쓸수 있다.
  • ViewSet : Class Based View를 더 간결하고 쉽게 사용하지 위한 추상클래스를 이용해 확장된 버전이다.

뷰셋은 유용한 추상화를 제공하며 일관된 API 접근, 코드 작성의 최소화, 일일이 URL conf를 작성하는 대신 상호작용과 표현에 더 집중할 수 있도록 도와주지만 마치 클래스 기반 뷰와 함수 기반 뷰를 선택할 때와 비슷한 트레이드 오프가 있으며, 직접 뷰를 작성할 때보다 덜 명료하다. 그러니 뷰셋이 항상 좋은 선택이 아니라 상황에 따라 판단을 해야한다. - drf 공식 문서

Serializer

data를 json으로 직렬화 해준다. 직렬화에 대한 자세한 설명 반대로 parser는 json을 data로 다시 만들어준다.

ModelSerializer

모델의 인스턴스들을 serialize하려면 아래와 같이 길게 작성해야하는데 modelserializer1 Model Serializer를 이용하면 이렇게 짧게 작성할 수 있다. modelserializer2

ApiView

django 에서는 view 를 통해서 HTTP 요청을 처리하는데 Api view는 RESTful한 API를 만들 때 사용하는 것으로

  1. 클래스를 기반으로 사용하면 뷰 클래스를 APIView를 사용하여 클래스를 만들고 예시: class PostListAPIView(APIView)
  2. 함수를 기반으로 사용하면 @api_view([‘GET’,’POST’])이런 식으로 함수 위에 데코레이터를 사용하는 방식으로 쓸 수 있다.

1번 클래스기반 뷰


from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer

# 포스팅 목록 및 새 포스팅 작성
class PostListAPIView(APIView):
    def get(self, request):
        serializer = PostSerializer(Post.objects.all(), many=True)
        return Response(serializer.data)
    def post(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
          	serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)  
      
from django.shortcuts import get_object_or_404

# 포스팅 내용, 수정, 삭제
class PostDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Post, pk=pk)
      
    def get(self, request, pk, format=None):
        post = self.get_object(pk)
        serializer = PostSerializer(post)
        return Response(serializer.data)
    
    def put(self, request, pk):
      	post = self.get_object(pk)
        serializer = PostSerializer(post, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
    def delete(self, request, pk):
        post = self.get_object(pk)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

2번 함수기반 뷰

from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
from rest_framework.decorators import api_view

@api_view(['GET','POST'])
def post_list(request):
    if request.method == 'GET':
        qs = Post.objects.all()
        serializer = PostSerializer(qs, many=True)
        return Response(serializer.data)
    else:
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

@api_view(['GET','PUT','DELETE'])
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == 'GET':
        serializer = PostSerializer(post)
        return Response(serializer.data)
    elif request.method == 'PUT':
        serializer = PostSerializer(post, data=reqeust.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    else:
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

코드 출처 : 수학과의 좌충우돌 프로그래밍

프로그래머스 level 1 문자열 내 마음대로 정렬하기

|

문제 설명

문자열로 구성된 리스트 strings와, 정수 n이 주어졌을 때, 각 문자열의 인덱스 n번째 글자를 기준으로 오름차순 정렬하려 합니다. 예를 들어 strings가 [“sun”, “bed”, “car”]이고 n이 1이면 각 단어의 인덱스 1의 문자 “u”, “e”, “a”로 strings를 정렬합니다.

제한 조건

strings는 길이 1 이상, 50이하인 배열입니다. strings의 원소는 소문자 알파벳으로 이루어져 있습니다. strings의 원소는 길이 1 이상, 100이하인 문자열입니다. 모든 strings의 원소의 길이는 n보다 큽니다. 인덱스 1의 문자가 같은 문자열이 여럿 일 경우, 사전순으로 앞선 문자열이 앞쪽에 위치합니다.

입출력 예

strings n return
["sun", "bed", "car"] 1 ["car", "bed", "sun"]
["abce", "abcd", "cdx"] 2 ["abcd", "abce", "cdx"]

입출력 예 설명

입출력 예 1
“sun”, “bed”, “car”의 1번째 인덱스 값은 각각 “u”, “e”, “a” 입니다. 이를 기준으로 strings를 정렬하면 [“car”, “bed”, “sun”] 입니다.

입출력 예 2
“abce”와 “abcd”, “cdx”의 2번째 인덱스 값은 “c”, “c”, “x”입니다. 따라서 정렬 후에는 “cdx”가 가장 뒤에 위치합니다. “abce”와 “abcd”는 사전순으로 정렬하면 “abcd”가 우선하므로, 답은 [“abcd”, “abce”, “cdx”] 입니다.

사고 과정

def solution(strings, n):
    dict = {}
    strings = sorted(strings)
    #딕셔너리로 [문자값 : 해당 문자[n]값] 만들고 밸류값에 따라 정렬
    dict = {x:y for x,y in zip(strings,strings[n])}
    #이렇게 하니까 c,c,x 일 때 abcd, abce의 사전순서대로의 정렬이 일어나지 않았다.
    return sorted(dict, key=lambda x: x[1])
def solution(strings, n):
    answer = []
    dict_a = {}
    n_list= []
    for string in strings:
        n_list.append(string[n])
    #딕셔너리로 [문자값 : 해당 문자[n]값] 만들고
    dict_a = {x:y for x,y in zip(strings,n_list)}
    #키값에 따라 한 번 정렬한 뒤
    dict_a = dict(sorted(dict_a.items()))
    #밸류값에 따라 한 번 더 정렬
    answer = sorted(dict_a, key=lambda x: x[1])
    print(answer)
    #정답 딕셔너리에서 키값만 출력
    return answer

이렇게 하면 3번만 맞고 다 틀린다

모범 답안

def solution(strings, n):
  answer = []
  for i in range(len(Strings)):
    strings[i] = strings[i][n] + strings[i]
    #n번째 수를 글자의 앞에다 붙이고
  strings.sort()
  #정렬
  print(strings)

  for j in range(len(strings)):
    answer.append(strings[j][1:])
    #맨 앞 n을 뺀 부분을 answer에 넣기 
  retur answer

n번째 수를 글자의 앞에다 붙인 다음, 바로 정렬하면 끝!!

프로그래머스 level 1 같은 숫자는 싫어

|

문제 설명

배열 arr가 주어집니다. 배열 arr의 각 원소는 숫자 0부터 9까지로 이루어져 있습니다. 이때, 배열 arr에서 연속적으로 나타나는 숫자는 하나만 남기고 전부 제거하려고 합니다. 단, 제거된 후 남은 수들을 반환할 때는 배열 arr의 원소들의 순서를 유지해야 합니다. 예를 들면,

arr = [1, 1, 3, 3, 0, 1, 1] 이면 [1, 3, 0, 1] 을 return 합니다. arr = [4, 4, 4, 3, 3] 이면 [4, 3] 을 return 합니다. 배열 arr에서 연속적으로 나타나는 숫자는 제거하고 남은 수들을 return 하는 solution 함수를 완성해 주세요.

제한사항

배열 arr의 크기 : 1,000,000 이하의 자연수 배열 arr의 원소의 크기 : 0보다 크거나 같고 9보다 작거나 같은 정수

입출력 예

arr answer
[1,1,3,3,0,1,1] [1,3,0,1]
[4,4,4,3,3] [4,3]

제출 답안

def solution(arr):
    answer = []
    for i in range(0,len(arr)-1):
        if arr[i] != arr[i+1]:
            answer.append(arr[i])
    answer.append(arr[-1])
    return answer

모범 답안

def no_continuous(s):
    a = []
    for i in s:
        if a[-1:] == [i]: continue
        a.append(i)
    return a

a[-1:]는 a배열에서 마지막 1개 값을 뺀 나머지 리스트를 의미한다.

위코드 두번째 프로젝트 험블벅 3 카카오 소셜로그인, 최종 구현 영상

|

험블벅 최종 구현 영상

카카오 소셜로그인

먼저 공식문서에 나와있는대로 카카오디벨로퍼스에 웹 플랫폼 및 도메인 정보를 등록한다.

소셜로그인

프론트에서 카카오 로그인을 통해 토큰을 받을 수 있는 인증코드를 받고, 이 인증코드를 통해서 API를 호출 할 수 있는 사용자 토큰(Access Token, Refresh Token)을 카카오로부터 받아서 header에 담아 백엔드에 보내준다.

Request

GET/POST /v2/user/me HTTP/1.1  
Host: kapi.kakao.com  
Authorization: Bearer {access_token}  
Content-type: application/x-www-form-urlencoded;charset=utf-8  

백엔드 코드

class KakaologinView(View):
    def get(self, request):
        try:
            access_token = request.headers.get("Authorization")
            #프론트에서 헤더스에 담아보낸 토큰 받기
            if not access_token:
                return JsonResponse({"message" : "INVALID_TOKEN"}, status = 400)
                
            response = requests.get(
                    "https://kapi.kakao.com/v2/user/me", 
                    headers = {"Authorization" : f"Bearer {access_token}"}
                    )
            #{Authorization: bearer+access_token}의 양식으로 kakao에 사용자 정보 reqeust를 보낸다
                    
            if not response:
                    return JsonResponse({"message" : "NOT_FOUND_IN_KAKAO"}, status = 404)
                    
            profile_json  = response.json()
                
            kakao_id      = profile_json.get("id")
            kakao_account = profile_json.get("kakao_account")
            name          = kakao_account["profile"]["nickname"]
            #카카오에서 kakao_id, kakao_account, kakao_account.profile.nickname 받기
            
            if not User.objects.filter(kakao = kakao_id).exists():
                User.objects.create(
                            kakao     = kakao_id,
                            nickname      = name,
                )
            #험블벅 db와 유저정보 비교 후 존재하지 않으면 새로운 유저정보 생성 
            user           = User.objects.get(kakao = kakao_id)
            #험블벅 db와 유저정보 비교 후 존재하면 유저를 가져와서
            user.name      = name
            #이름을 넣고
            user.save()
            #저장

            token = jwt.encode({"id" : user.id}, SECRET_KEY, algorithm = "HS256")
            #토큰 발행
			
		    return JsonResponse({"message" : "SUCCESS", "acess_token" : token}, status = 200)
		except KeyError:
		    return JsonResponse({"message" : "KEY_ERROR"}, status = 400)

카카오에서 받게 되는 profile_json데이터는 아래와 같이 생겼다.

{
    'id': 1855599935,
    'connected_at': '2021-08-18T08:54:05Z',
    'properties':
        {
        'nickname': '한효주'
        },
    'kakao_account':
        {'profile_nickname_needs_agreement': False,
         'profile':
            {'nickname': '한효주'},
         'has_email': True,
         'email_needs_agreement': False,
         'is_email_valid': True,
         'is_email_verified': True,
         'email': 'hyojoo@gmail.com'
        }
}

위코드 두번째 프로젝트 험블벅 2 AWS S3 코드, 리팩토링

|

텀블벅 클론코딩

2차 프로젝트로 험블벅을 클론코딩하면서 프로젝트 이미지를 AWS S3서버에 올리는 코드를 작성했다.

https://github.com/boto/boto3/tree/develop/docs/source/guide

여기에 들어가보면 AWS공식 라이브러리인 boto3를 쓰는 예시가 자세히 잘 나와있다. 예를 들어서 Access permission을 받는 코드는 다음과 같다.

permission

AWS S3 권한 설정

권한 관리를 위해서 버킷 정책을 아래와같이 작성했다. 프로젝트 이미지라서 비회원한테도 보여야되니까 퍼블릭 엑세스 차단을 전부 해제했다.

** 권한 설정 비디오 튜토리얼 **

버킷 정책은 아래와 같이 했는데 버킷정책에 대한 자세한 내용은 AWS 사용설명서에 나와있는걸 읽어보면 된다. https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/example-policies-s3.html

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "StatementSid1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::182260599663:user/sang*****"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::humble/*"
        },
        {
            "Sid": "StatementSid2",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::humble/*"
        }
    ]
}
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "http://www.example1.com"
        ],
        "ExposeHeaders": [
            "ETag"
        ],
        "MaxAgeSeconds": 3000
    }
]

AWS S3액세스, ProjectView post

프로젝트를 업로드하는 뷰인데 이미지 파일 업로드가 함께 들어가있다

class ProjectView(View):
    @login_decorator
    def post(self, request):
        try:
            user = request.user
            data = request.POST

            image = request.FILES.get('image')
            #폼데이터로 받은 파일을 이미지 변수에 넣는다
            input_date = data.get('end_date')

            if not image:
                return JsonResponse({'MESSAGE' : 'IMAGE_EMPTY'}, status=400)

            upload_key = str(uuid.uuid4()) + image.name
            #uuid와 이미지 이름으로 키를 생성한다
            s3_client = boto3.client(
                's3',
                aws_access_key_id     = ACCESS_KEY_ID,
                aws_secret_access_key = SECRET_ACCESS_KEY,
            )
            #s3서버에 접근한다

            Project.objects.create(
                name           = data.get('name'),
                user_id        = user.id,
                aim_amount     = data.get('aim_amount'),
                description    = data.get('description'),
                end_date       = input_date[6:10]+'-'+input_date[:2]+'-'+input_date[3:5],
                category_id    = data.get('category_id'),
                main_image_url = 'https://humblebug.s3.us-east-2.amazonaws.com/' + upload_key
                #aws버킷경로와 업로드 키를 포함한 URL을 저장한다. 이렇게 하고 퍼블릭 엑세스를 해제하면 url을 클릭했을 때 브라우저에서 바로 이미지가 나온다.  
            )

            s3_client.upload_fileobj(
                image,
                BUCKET_NAME,
                upload_key,
                ExtraArgs = {
                    'ContentType' : image.content_type #확장자였나..?
                }
            )
            #s3서버에 이미지를 업로드한다

            return JsonResponse({'MESSAGE':'SUCCESS'},status = 201)
    
        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404) 

이런 방식으로 쓰면 이미지 파일 수정 및 삭제할 때도 액세스 코드를 계속 써야되고 멘토님께서 나중에 키가 바뀌거나 아마존이 아닌 구글로 서비스업체를 바꾼다던지 했을 때 유지/보수 및 디버깅이 쉽게 하기 위해서 아래와 같이 리팩토링할 것을 조언해주셨다.

AWS S3 관련 코드 리팩토링

class CloudStorage:
    def __init__(self, ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET_NAME):
        self.ACCESS_KEY_ID = ACCESS_KEY_ID
        self.SECRET_ACCESS_KEY = SECRET_ACCESS_KEY
        self.BUCKET_NAME = BUCKET_NAME
        self.client = boto3.client(
                's3',
                aws_access_key_id     = ACCESS_KEY_ID,
                aws_secret_access_key = SECRET_ACCESS_KEY,
            )
        self.resource = boto3.resource(
                's3',
                aws_access_key_id     = ACCESS_KEY_ID,
                aws_secret_access_key = SECRET_ACCESS_KEY,
            )
    #AWS 관련 데이터를 변수로 넣는다
        
    def upload_file(self, file):
        file_url = "사용하는 회사의 aws url.ap-northeast-2.rds.amazonaws.com/" + str(uuid.uuid1()) + file.name
        self.client.upload_fileobj(
                        file,
                        self.BUCKET_NAME,
                        file_url,
                        ExtraArgs={
                            "ContentType": file.content_type
                        }
        )
        return file_url

    def delete_file(self, file_url, project_id):
        main_image_url = Project.objects.get(id=project_id).file_url
        bucket = self.resource.Bucket(name=BUCKET_NAME)
        bucket.Object(key = file_url).delete()
    #AWS관련 액션들을 매쏘드로 넣는다

class ProjectUpload(View):
    @login_decorator
    def post(self, request):
        cloud_storage = CloudStorage(ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET_NAME)
        try:
            data = request.POST

            if request.FILES:
                file_url = (cloud_storage.upload_file(file))
                #그러면 이렇게 한 줄로 업로드하고 리턴 받은 url을 가져와서 사용할 수 있다.

            if not request.FILES:
                return JsonResponse({'MESSAGE' : 'IMAGE_EMPTY'}, status=400)

            Project.objects.create(
                    name           = data.get('name'),
                    user_id        = request.user.id,
                    aim_amount     = data.get('aim_amount'),
                    description    = data.get('description'),
                    end_date       = data.get('end_date')[0:4]+'-'+data.get('end_date')[5:7]+'-'+data.get('end_date')[8:10],
                    category_id    = data.get('category_id'),
                    main_image_url = file_url
                    #삭제하고 싶을 때는 앞에 url을 빼고 키값만 넣어줘야한다.
                    )
            return JsonResponse({'MESSAGE':'SUCCESS'}, status = 200)

        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404)

그 외 이미지 수정/삭제 코드

class ProjectModify(View):
    @login_decorator
    def post(self, request, project_id):
        try:
            data = request.POST

            if not Project.objects.filter(id=project_id, user_id=request.user.id).exists():
                return JsonResponse({'MESSAGE':'NOT_EXISTS'}, status=400)

            if request.FILES:
                cloud_storage.delete_file(file, file_url)
                file_url = (cloud_storage.upload_file(file))
                
            if not file:
                main_image_url = Project.objects.get(id=project_id).main_image_url

            Project.objects.filter(id=project_id, user_id=request.user.id).update(
                    name           = data.get('name'),
                    user_id        = request.user.id,
                    aim_amount     = data.get('aim_amount'),
                    description    = data.get('description'),
                    end_date       = data.get('end_date')[0:4]+'-'+data.get('end_date')[5:7]+'-'+data.get('end_date')[8:10],
                    category_id    = data.get('category_id'),
                    file_url = file_url
                    )
            return JsonResponse({'MESSAGE':'SUCCESS'}, status = 200)

        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404)

    @login_decorator
    def delete(self, request, project_id):
        cloud_storage = CloudStorage(ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET_NAME)
        user = request.user

        if not Project.objects.filter(id=project_id, user_id=user.id).exists():
            return JsonResponse({'MESSAGE':'NOT_EXISTS'}, status=400)
        
        file_url = Project.objects.get(id=project_id).file_url        
        cloud_storage.delete_file(file_url, project_id)

        Project.objects.get(id=project_id, user_id=user.id).delete()

        return JsonResponse({"MESSAGE": 'SUCCESS'}, status=204)

쉘을 통해서 AWS s3서버에서 이미지 삭제하는 코드

import boto3
ACCESS_KEY_ID = '**********'
SECRET_ACCESS_KEY = '******************'
BUCKET_NAME = '[name of your bucket]'
s3_client = boto3.client(
                 's3',
                 aws_access_key_id     = ACCESS_KEY_ID,
                 aws_secret_access_key = SECRET_ACCESS_KEY,
             )
s3_resource = boto3.resource(
                 's3',
                 aws_access_key_id     = ACCESS_KEY_ID,
                 aws_secret_access_key = SECRET_ACCESS_KEY,
             )
bucket = s3_resource.Bucket(name='버켓 이름')
#삭제하는 명령어1
bucket.Object('키값, 예를 들어 d457d1b0-169d-487a-950f-ef0cca576ad01.png').delete()
#삭제하는 명령어2
s3_client.delete_object(Bucket='humble', Key ='d457d1b0-169d-487a-950f-ef0cca576ad01.png')