루나의 TIL 기술 블로그

위스타그램 4 업로드 (로그인 데코레이터)

|

[Mission 7] 게시물 등록 기능 구현

1. Postings app 생성

  • Django에서는 주로 다루는 데이터의 종류가 달라지는 시점에서 앱을 분리합니다. 인스타그램의 게시물은 이용자 데이터와는 그 성질이 달라 데이터베이스에서 테이블을 따로 관리합니다. 따라서, 주로 다루는 테이블이 달라지므로 앱을 분리하는 것이 좋습니다.

2. Posting Model 생성

  • 인스타그램에 게시물을 등록하기 위해서는 사용자, 생성 시간, 이미지 url이 필요합니다.
  • 해당 게시물의 유저는 Foreign Key를 이용하여 이미 서비스에 가입된 사람으로 연결시켜 주세요.
from django.db import models
from users.models import User

class Upload(models.Model): #나는 포스팅대신 업로드라는 이름을 사용했다
    user_id = models.ForeignKey(User, on_delete=models.CASCADE, db_column='user_id')
    #User테이블의 id를 참조하는 'user_id'이름의 컬럼을 만든다.
    created_at = models.DateTimeField(auto_now_add=True)
    text = models.CharField(max_length=300)
    img = models.CharField(max_length=400)
    
    class Meta:
        db_table = 'uploads'

데이터베이스 상의 필드 이름(db_column)은 테이블에 정의될 이름을 의미한다. db_column 매개변수를 사용하지 않는다면, 데이터베이스 필드에 작성될 필드명은 user_id_id가 되는데 이는 의도한 필드명이 아니므로, db_column 매개변수의 인수에 user_id를 사용한다.

3. Postings View 작성

:: 게시물 등록

  • 게시물 등록 클래스를 생성해주세요.
  • 게시물을 등록할 때에는 post메소드를 사용합니다.
  • 게시물 생성 시간은 등록하는 현재 시간이어야 합니다.

:: 게시물 표출

  • 등록된 모든 게시물을 나열하는 게시물 표출 클래스를 생성해주세요.
  • 게시물을 나타낼 때에는 get메소드를 사용합니다.
  • 게시물을 나타낼 때에는 등록한 사람, 게시물, 게시된 내용, 게시된 시각이 포함되어야 합니다.
import json

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

from .models        import Upload as UploadModel 
#AttributeError: type object 'Upload' has no attribute 'objects'
#이 에러가 자꾸나서 스택오버플로우에서 찾은대로 as UploadModel로 갈음해주었다.
from users.models   import User
from users.utils    import login_decorator
#데코레이터는 유저스앱안에 따로 유틸스파일에 들어있다.

class Upload(View):
    @login_decorator #유저가 올바른 토큰을 가져왔다면
    def post(self, request):
        try:
            data = json.loads(request.body)

            if (data["img"] == ""): #이미지가 없으면 에러
                return JsonResponse({"message": "EMPTY_IMAGE"}, status=400)

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

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

    def get(self, request):
            uploads = UploadModel.objects.all()
            #업로드모델에 따라 객체를 다 가져와서 저장
            results = []

            for upload in uploads: #저장된 객체들을 한 개씩 돌면서
                results.append({
                    "user_id"      : upload.user_id.id,
                    #객체의 유저아이디번호에 해당하는 id
                    "img"        : upload.img,
                    "text"     : upload.text,
                    "created_at" : upload.created_at
                })
            return JsonResponse({"results": results}, status=200)

Decorater의 구조

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 를 작성해야합니다.

  • ✅ 적절한 url 작성
#상위프로젝트폴더urls.py
from django.urls import path,include

urlpatterns = [
    path("users",include("users.urls")),
    path("upload",include("upload.urls"))
]
#하위앱폴더urls.py
from django.urls import path
from .views      import Upload

urlpatterns = [
    path("", Upload.as_view())
]

POSTMAN을 이용해서 Headers에 Authorization값을 넣고 Body에 JSON형식으로 값을 넣어서 디비에 이미지링크과 텍스트가 잘 입력된 모습을 볼 수 있다. 업로드

위스타그램 3 암호화, 토큰

|

[Mission 5] 회원가입 비밀번호 암호화 적용

  • 유저가 가입할 때 비밀번호를 암호화에서 DB에 저장

Salting(실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해서 해시값을 계산하는 방법)과 Key Stretching(단방향 해쉬값을 계산 한 후 그 해쉬값을 또 해쉬 하고, 또 이를 반복하는 것을 말한다)을 이용하여 비밀번호를 단방향 암호화하기 위해 만들어진 해쉬함수 bcrypt를 사용한다.

bcrypt 라이브러리 사용하여 비밀번호 암호화

pip install bcrypt 
import bcrypt
password = '1234'
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
print(hashed_password)
b'$2b$12$YFs9rh.1LgJwZuf9ibyjpuLvBoCaGX0MzedFWF2Jo0zU3lMZurZ4a'
#yooyoung > users > views.py

import json, re, bcrypt, jwt #brcypt(암호화)와 jwt(토큰) 임포트
#...
from westagram.settings  import SECRET_KEY #세팅파일에서 시크릿 키 불러오기

class SignUp(View):
    def post(self, request):
        try:
            data = json.loads(request.body)
            hashed_password = bcrypt.hashpw(data['password'].encode('UTF-8'), bcrypt.gensalt())
            #유저에게 받은 새 문자열 비밀번호를 인코딩해서 바이트형태로 이진화하고, 비크립트로 암호화한다.

            #...

            User.objects.create(
                password     =   hashed_password.decode('UTF-8'),
                #DB에 문자열로 저장하기 위해서 암호화된 바이트형태의 비밀번호를 디코딩해준다.
            )

[Mission 6] 로그인 JWT 적용

  • 유저가 입력한 비밀번호 암호화 한후 암호화되서 DB에 저정된 유저 비밀번호와 비교
  • 로그인 성공하면 token을 클라이언트에게 전송 (유저는 로그인 성공후 다음부터는 access token을 첨부해서 request를 서버에 전송함으로서 매번 로그인 해도 되지 않도록 한다)

단방향 해쉬 함수(one-way hash function)는 원본 메시지를 알면 암호화된 메시지를 구하기는 쉽지만 암호화된 메시지로는 원본 메시지를 구할 수 없다. 그래서 비밀번호 확인은 bcrypt.checkpw(입력받은 패스워드, 저장된 암호화된 패스워드)매쏘드를 통해서 한다.

password = '1234'
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
new_password = '1234'
bcrypt.checkpw(new_password.encode('utf-8'),hashed_password)
True
class SignIn(View):
    def post(self, request):
        try:
            #...
            user = User.objects.get(email=data['email'])
            #유저가 반복해서 나오니까 변수로 저장

            if not bcrypt.checkpw(data['password'].encode('UTF-8'), user.password.encode('UTF-8')):
                return JsonResponse({"massage": "INVALID_USER"}, status=401)
                #유저에게 로그인시 입력받은 문자열 비밀번호를 인코딩(이진화)해서 디비에 저장된 비밀번호를 인코딩한 것과 비교해서
                #같으면 트루를 반환한다. 같지 않으면 에러를 반환한다.  

            token = jwt.encode({'user_id': user.id}, SECRET_KEY, algorithm='HS256')
            #입력받은 유저의 아이디로 토큰을 생성

            return JsonResponse({'token': token}, status = 200)
            #이메일과 비밀번호가 일치하는 경우 유저에게 토큰을 전달한다.

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

JSON Web Token JWT

pip install pyjwt
import jwt
SECRET = 'secret' #'랜덤한 조합의 키' 예제이므로 단순하게 'secret'이라고 하겠습니다.
token = jwt.encode({'id' : 1}, SECRET, algorithm = 'HS256') #토큰을 만드는 문법
print(token)
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.-xXA0iKB4mVNvWLYFtt2xNiYkFpObF54J9lj2RwduAI'

인증(로그인)을 통과한 사용자에게 토큰을 발급한다.
인증을 통과한 사용자만 접근하려면 이 토큰을 받아서 다시 우리가 발행한 토큰이 맞는지 확인한다.

header = jwt.decode(token, SECRET, algorithm = 'HS256')
print(header)
{'id': 1}

인증하는 코드는 보통 user app에 utils.py를 만들어서 데코레이터로 구현한다.

깃헙 전체 코드 링크

암호화를 하면 이렇게 비밀번호가 암호화된 테이블을 볼 수 있다.

비밀번호가 암호화되어 저장된 테이블

프로그래머스 level 2 문자열압축

|

문제 설명

데이터 처리 전문가가 되고 싶은 “어피치”는 문자열을 압축하는 방법에 대해 공부를 하고 있습니다. 최근에 대량의 데이터 처리를 위한 간단한 비손실 압축 방법에 대해 공부를 하고 있는데, 문자열에서 같은 값이 연속해서 나타나는 것을 그 문자의 개수와 반복되는 값으로 표현하여 더 짧은 문자열로 줄여서 표현하는 알고리즘을 공부하고 있습니다. 간단한 예로 “aabbaccc”의 경우 “2a2ba3c”(문자가 반복되지 않아 한번만 나타난 경우 1은 생략함)와 같이 표현할 수 있는데, 이러한 방식은 반복되는 문자가 적은 경우 압축률이 낮다는 단점이 있습니다. 예를 들면, “abcabcdede”와 같은 문자열은 전혀 압축되지 않습니다. “어피치”는 이러한 단점을 해결하기 위해 문자열을 1개 이상의 단위로 잘라서 압축하여 더 짧은 문자열로 표현할 수 있는지 방법을 찾아보려고 합니다.

예를 들어, “ababcdcdababcdcd”의 경우 문자를 1개 단위로 자르면 전혀 압축되지 않지만, 2개 단위로 잘라서 압축한다면 “2ab2cd2ab2cd”로 표현할 수 있습니다. 다른 방법으로 8개 단위로 잘라서 압축한다면 “2ababcdcd”로 표현할 수 있으며, 이때가 가장 짧게 압축하여 표현할 수 있는 방법입니다.

다른 예로, “abcabcdede”와 같은 경우, 문자를 2개 단위로 잘라서 압축하면 “abcabc2de”가 되지만, 3개 단위로 자른다면 “2abcdede”가 되어 3개 단위가 가장 짧은 압축 방법이 됩니다. 이때 3개 단위로 자르고 마지막에 남는 문자열은 그대로 붙여주면 됩니다.

압축할 문자열 s가 매개변수로 주어질 때, 위에 설명한 방법으로 1개 이상 단위로 문자열을 잘라 압축하여 표현한 문자열 중 가장 짧은 것의 길이를 return 하도록 solution 함수를 완성해주세요.

입출력 예

s result
"aabbaccc" 7
"ababcdcdababcdcd" 9
"abcabcdede" 8
"abcabcabcabcdededededede" 14
"xababcdcdababcdcd" 17

입출력 예 #1

문자열을 1개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #2

문자열을 8개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #3

문자열을 3개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #4

문자열을 2개 단위로 자르면 “abcabcabcabc6de” 가 됩니다. 문자열을 3개 단위로 자르면 “4abcdededededede” 가 됩니다. 문자열을 4개 단위로 자르면 “abcabcabcabc3dede” 가 됩니다. 문자열을 6개 단위로 자를 경우 “2abcabc2dedede”가 되며, 이때의 길이가 14로 가장 짧습니다.

입출력 예 #5

문자열은 제일 앞부터 정해진 길이만큼 잘라야 합니다. 따라서 주어진 문자열을 x / ababcdcd / ababcdcd 로 자르는 것은 불가능 합니다. 이 경우 어떻게 문자열을 잘라도 압축되지 않으므로 가장 짧은 길이는 17이 됩니다.

사고 과정

반복되는 문자열 중에서 반복된 구간을 포함해서 가장 긴 것을 찾는다 - 반복되는 문자열들을 찾아서 리스트에 저장한다 - 그 문자열이 리스트 안에 몇 개나 차지하고 있는지 확인한다 - 크기를 비교해서 제일 긴 것을 남긴다 - 제일 긴 문자열을 숫자로 치환한다

라고 생각했지만 반복되는 문자열을 어떻게 찾아야하는지 모르겠어서 검색을 해봤다.

  1. 문자열을 정해진 길이로 판별하며 반복해야 하고, 문자열의 반을 넘어가면 같은 문자열이 나올 수 없으므로, 문자열의 절반 까지만 반복

  2. 정해진 길이의 문자열 S가 다음 문자열과 같으면, 숫자를 올리고 다음 문자열과 다시 비교, 다르면 이전의 문자열과 숫자를 합해 한곳에 저장하고, 문자열 S를 달라진 문자열로 변경하고 비교를 반복

  3. 끝까지 비교하면 그 문자열의 길이를 배열 Length에 저장

  4. 배열 Length의 최소값을 리턴

출처 디듀오 IT 개발 연습 노트 블로그

모범 답안

def cut_str(s):
    length =[]
    result = ""
    
    if len(s)==1:  #문자열 길이가 1이면 결과는 무조건 1 
         return 1
    for cut in range(1, len(s)//2+1): #1 부터 문자열의 절반까지 자르며 비교해야하기떄문에
        temp = s[:cut]     #초기값 설정
        num = 1
        for i in range(cut, len(s), cut): #정해진 길이를 문자열 s의 길이까지 정해진 길이 스텝으로 반복
            if s[i:i+cut] == temp:    #만약 다음으로 자른값과 같을경우
                num+=1      #숫자 올려줌 (문자열 앞에 붙을 숫자)
            else:       #다를 경우
                if num == 1:  #숫자가 1 이면 문자열에 붙지 않으므로 없애줌
                    num=""
                result += str(num)+temp   #결과값에 숫자와 temp에 저장된 값을 저장
                temp = s[i:i+cut]   #temp값을 변경
                num=1       #숫자 다시 초기화
        if num == 1:  #else로 끝날을때만 저장이 되므로 반복
            num=""
        result +=str(num)+temp
        length.append(len(temp))
        temp=""
    return min(length) #최소값을 반환

위스타그램 2 로그인기능 구현

|

[Mission 4] 로그인 기능 구현

  • 로그인을 위한 View를 작성해야합니다. **로그인 정보(이메일, 비밀번호)
  • 로그인을 할 때는 사용자 계정과 비밀번호가 필수입니다.
  • 계정이나 패스워드 키가 전달되지 않았을 경우, {“message”: “KEY_ERROR”}, status code 400 을 반환합니다.
  • 계정을 잘 못 입력한 경우 {“message”: “INVALID_USER”}, status code 401을 반환합니다.
  • 비밀번호를 잘 못 입력한 경우 {“message”: “INVALID_USER”}, status code 401을 반환합니다.
  • 로그인이 성공하면 {“message”: “SUCCESS”}, status code 200을 반환합니다.
#yooyoung > users > urls.py
from django.urls import path
from .views      import SignUp, SignIn

urlpatterns = [
    path("/signup", SignUp.as_view()),
    path("/signin", SignIn.as_view())
]
#yooyoung > users > views.py
class SignIn(View):
    def post(self, request):
        try: #예외처리
            data = json.loads(request.body)
            #프론트에서 json으로 데이터 받기

            if (data["email"] == "") or (data["password"] == ""):
                return JsonResponse({'message': 'KEY_ERROR'}, status=400)
                #이메일이나 비밀번호가 입력되지 않았으면 에러 반환
            
            if not User.objects.filter(email=data['email']).exists():
                return JsonResponse({'message': 'INVALID_USER'}, status=401)
                #유저가 입력한 이메일이 디비에 존재하지 않으면 에러 반환

            if not User.objects.filter(password=data['password']):
                return JsonResponse({'massage': 'INVALID_USER'}, status=401)
                #유저가 입력한 이메일이 존재해서 넘어왔으나 비밀번호가 디비와 일치하지 않으면 에러 반환

            if User.objects.filter(email=data['email']) and User.objects.filter(password=data['password']):
                return JsonResponse({'message': 'SUCCESS'}, status=200)
                #유저가 입력한 이메일과 비밀번호가 디비와 일치하면 성공

        except KeyError:
            return JsonResponse({'massage': 'KEY_ERROR'}, status=400)
            #예외처리 : 원본 코드에서 없는 key를 접근하려고 했을때 KeyError 가 발생

http 상태 코드

1xx (정보): 요청을 받았으며 프로세스를 계속한다
2xx (성공): 요청을 성공적으로 받았으며 인식했고 수용하였다
3xx (리다이렉션): 요청 완료를 위해 추가 작업 조치가 필요하다
4xx (클라이언트 오류): 요청의 문법이 잘못되었거나 요청을 처리할 수 없다
5xx (서버 오류): 서버가 명백히 유효한 요청에 대해 충족을 실패했다

200(성공): 서버가 요청을 제대로 처리했다는 뜻이다. 이는 주로 서버가 요청한 페이지를 제공했다는 의미로 쓰인다.
400(잘못된 요청): 서버가 요청의 구문을 인식하지 못했다.
401(권한 없음): 이 요청은 인증이 필요하다. 서버는 로그인이 필요한 페이지에 대해 이 요청을 제공할 수 있다. 상태 코드 이름이 권한 없음(Unauthorized)으로 되어 있지만 실제 뜻은 인증 안됨(Unauthenticated)에 더 가깝다.
403(Forbidden, 금지됨): 서버가 요청을 거부하고 있다. 예를 들자면, 사용자가 리소스에 대한 필요 권한을 갖고 있지 않다. (401은 인증 실패, 403은 인가 실패라고 볼 수 있음)

401은 누구세요 403은 너는 권한이 없어요

코드카타 1주차

|

DAY1

문제

two_sum함수에 숫자 리스트와 ‘특정 수’를 인자로 넘기면, 더해서 ‘특정 수’가 나오는 index를 배열에 담아 return해 주세요.

nums: 숫자 배열
target: 두 수를 더해서 나올 수 있는 합계
return: 두 수의 index를 가진 숫자 배열

예를 들어,

nums은 [4, 9, 11, 14]
target은 13 

nums[0] + nums[1] = 4 + 9 = 13 이죠?

그러면 [0, 1]이 return 되어야 합니다.

가정

target으로 보내는 합계의 조합은 배열 전체 중에 2개 밖에 없다고 가정하겠습니다.

제출코드

def two_sum(nums, target):
  for i in range(len(nums)):
    for j in range(len(nums)):
      if nums[i]+nums[j] == target:
        #주어진 수 둘의 조합이 합쳐서 target이 되는 경우 해당 수 출력
        return [i,j]

DAY2

문제

reverse 함수에 정수인 숫자를 인자로 받습니다.

그 숫자를 뒤집어서 return해주세요.

x: 숫자

return: 뒤집어진 숫자를 반환!

예들 들어,

x: 1234
return: 4321
x: -1234
return: -4321
x: 1230
return: 321

제출 코드

def reverse(number):
  if number > 0:
      return int(str(number)[::-1])
      #양수일때는 그냥 뒤집기
  if number < 0:
      return int('-'+str(number*-1)[::-1])
      #음수일때는 -뺐다가 붙임
  else:
      return 0 #0일때는 0

DAY3

문제

String 형인 str 인자에서 중복되지 않은 알파벳으로 이루어진 제일 긴 단어의 길이를 반환해주세요.

str: 텍스트
return: 중복되지 않은 알파벳 길이 (숫자 반환)

예를 들어,

str = "abcabcabc"
return 은 3
=> 'abc' 가 제일 길기 때문
str = "aaaaa"
return 은 1
=> 'a' 가 제일 길기 때문
str = "sttrg"
return 은 3
=> 'trg' 가 제일 길기 때문

제출코드

def get_len_of_str(s):
    temp_list = []
    max_len = 0
    str_list = list(s)
    for i in str_list:
        if i in temp_list:
            temp_list=[]
            #2.해당 알파벳이 이미 리스트에 있다면 리스트를 비운다 
        temp_list.append(i)
        #1.처음 나오는 알파벳들을 리스트에 넣는다
        if len(temp_list) > max_len:
            max_len = len(temp_list)
            #3. 리스트의 길이가 현재까지 갱신된 최고 길이보다 길면 최고 길이를 갈음한다
    return max_len

DAY4

문제

숫자인 num을 인자로 넘겨주면, 뒤집은 모양이 num과 똑같은지 여부를 반환해주세요.

num: 숫자
return: true or false (뒤집은 모양이 num와 똑같은지 여부)

예를 들어,

num = 123
return false 
=> 뒤집은 모양이 321 이기 때문
num = 1221
return true 
=> 뒤집은 모양이 1221 이기 때문
num = -121
return false 
=> 뒤집은 모양이 121- 이기 때문
num = 10
return false 
=> 뒤집은 모양이 01 이기 때문

제출코드

def same_reverse(num):
  return str(num) == str(num)[::-1]
  #뒤집어서 같으면 True 다르면 False 반환

DAY5

문제

strs은 단어가 담긴 배열입니다.

공통된 시작 단어(prefix)를 반환해주세요.

예를 들어

strs = ['start', 'stair', 'step']
return은 'st'
strs = ['start', 'wework', 'today']
return은 ''

제출코드

def get_prefix(strs):
    answer=[]
    strs = sorted(strs)
    #sort하면 a, ab, abc, ac 이렇게 정렬이 된다
    if strs == []:
      return ''
    #빈문자열 예외처리
    for i in range(len(strs[-1])):
        if strs[0][i] == strs[-1][i]:
          #첫문자와 마지막문자의 알파벳 비교
            answer.append(strs[0][i])
            #같으면 답에 넣는다 
        else:
          break
    return ''.join(answer)

위스타그램 1 회원가입기능구현

|

[Mission 1] Django초기 세팅

지난 게시물을 참고하여 만들 수 있다. makemigrations할 때 프로젝트 안에 앱이 2개 이상일때는 뒤에 앱이름을 붙여주어야한다.

python manage.py makemigrations users
python manage.py migrate users
  1. 모델 작성
  2. 프로젝트 settings.py의 installed_app에 앱 이름 추가
  3. makemigrations 실행
  4. 상위 프로젝트 폴더의 urls.py에 경로 추가

[Mission 2] 모델링

#yooyoung > users > models.py
from django.db import models


class User(models.Model):
    name         = models.CharField(max_length=40, null=True)
    #null=True를 넣으면 값이 들어오지 않아도 에러가 나지 않는다.
    email        = models.EmailField(max_length=200, unique=True)
    password     = models.CharField(max_length=200)
    age          = models.PositiveIntegerField(null=True)
    phone_number = models.CharField(max_length=60, null=True)

    class Meta:
        db_table = 'users' #디비 이름 네이밍 컨벤션 : 소문자 복수형

[Mission 3] 회원가입 기능 구현

  • 회원가입을 위한 View 를 작성해야합니다. 사용자 정보는 이름, 이메일, 비밀번호, 연락처(휴대폰), 그 외 개인정보를 포함한다.
  • 이메일이나 패스워드가 전달되지 않을 경우, {"message": "KEY_ERROR"}, status code 400 을 반환합니다.
  • 이메일에는 @와 .이 필수로 포함되어야 합니다. 해당 조건이 만족되지 않은 경우 적절한 에러를 반환해주세요. 이 과정을 Email Validation이라고 합니다. 정규표현식을 활용해주세요.
  • 비밀번호는 8자리 이상, 문자, 숫자, 특수문자의 복합이어야 합니다. 해당 조건이 만족되지 않은 경우, 적절한 에러를 반환해주세요. 이 과정을 Password Validation이라고 합니다. 정규표현식을 활용해주세요.
  • Email validation, Password Validation 과정에서 정규식을 사용해보세요.
  • 회원가입시 서로 다른 사람이 같은 이메일을 사용하지 않으므로 기존에 존재하는 자료와 중복되면 안됩니다. 적절한 에러를 반환해주세요.
  • 회원가입이 성공하면 {“message”: “SUCCESS”}, status code 201을 반환합니다.
#yooyoung > urls.py
from django.urls import path,include

urlpatterns = [
    path("users",include("users.urls"))
]
#yooyoung > users > urls.py
from django.urls import path
from .views      import SignUp

urlpatterns = [
    path("", SignUp.as_view())
]
#yooyoung > users > views.py
from django.views import View
from django.http import JsonResponse
from .models import User
import json
import re


class SignUp(View):
    def post(self, request):
        try:
            data = json.loads(request.body)

            if User.objects.filter(email=data['email']).exists():
                return JsonResponse({"message": "ERROR_EMAIL_ALREADY_EXIST"}, status=400)
                #이메일이 디비에 이미 있는 경우 중복에러
            if (data["email"] == "") or (data["password"] == ""):
                return JsonResponse({"message": "ERROR_EMPTY_EMAIL_OR_PASSWORD"}, status=400)
                #이메일이나 비밀번호가 누락된 경우 에러
            if re.match(r"^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", data["email"]) == None:
                return JsonResponse({"message": "ERROR_EMAIL_NEED_@AND."}, status=400)
                #이메일 형식이 안 맞는 경우 에러
            if re.match(r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$", data["password"]) == None:
                #(알파벳이 있고)(숫자가 있고)(특수문자가 있으면)8자 이상의 해당 문자열을 입력받는다.
                # 그래서 특수문자에 넣고싶은 문자가 더 있으면 여기에 추가해주면 된다. 양쪽에 다 추가해야함! 
                return JsonResponse({"message": "ERROR_REQUIRE_8_LETTER,NUMBER,SPECIAL_SYMBOLS)"}, status=400)
                #비밀번호 8자리 이상, 문자, 숫자, 특수문자의 복합아니면 에러

            User.objects.create(
                name         =   data['name'],
                email        =   data['email'],
                password     =   data['password'],
                phone_number =   data['phone_number'],
                age          =   data['age']
            )
            #그렇지 않은 경우 데이터 저장

            return JsonResponse({'Message': 'SUCCESS'}, status=201)
        
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)
            #try: except를 써야 예외처리할 수 있다. 키에러일 경우 키에러를 리턴한다.

쉘로 데이터 입력하기

>>>from users.models import User
>>>User.objects.create(name='경훈',email='123@gmail.com',password='!234567eight', age=18, phone_number='01012345678')
<User: User object (1)>
...

httpie POST로 데이터 입력하기

http -v POST 127.0.0.1:8000/app이름/urls등록한이름 컬럼='' 컬럼='' 
공백으로 컬럼을 구분한다.

예를 들자면

http -v POST 127.0.0.1:8000/users name='민' email='min@abc.com' password='*fsgfdlkjhlkjh1' phone='010-1234-5678' age=32

결과값(비밀번호를 대강 쉘로 넣어서 정규표현식이 적용되어있지 않다) mission3 db

장고 CRUD2 영화와 배우

|

장고 CRUD2 추가 과제 영화와 배우

영화와 배우 ERD

GET

  1. 등록된 배우 목록을 리턴해주는 GET 메소드를 구현해주세요.
    • 배우의 이름, 성, 그리고 출연한 영화 제목 목록
  2. 등록된 영화 목록을 리턴해주는 GET 메소드를 구현해주세요.
    • 영화의 제목, 상영시간, 출연한 배우 목록 (이름만)

migration을 위한 models.py작성

from django.db import models

class Actor(models.Model):
    first_name = models.CharField(max_length=45)
    last_name = models.CharField(max_length=45)
    birth_date = models.DateField()
    movie = models.ManyToManyField('Movie')
    class Meta:
        db_table='actors'
    def __str__(self):
        return self.first_name + self.last_name

class Movie(models.Model):
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()
    class Meta:
        db_table='movies'
    def __str__(self):
        return self.title

migration 만들고 migrate 실행하기

sqlmigrate했을 때 나오는 구문은 아래와 같다.

-- Create model Movie
CREATE TABLE `movies` (
    `id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, 
    `title` varchar(45) NOT NULL, 
    `release_date` date NOT NULL
    );

-- Create model Actor
CREATE TABLE `actors` (
    `id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, 
    `first_name` varchar(45) NOT NULL, 
    `last_name` varchar(45) NOT NULL, 
    `birth_date` date NOT NULL
    );

CREATE TABLE `actors_movie` (
    `id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, 
    `actor_id` bigint NOT NULL, `movie_id` bigint NOT NULL
    );

ALTER TABLE `actors_movie` ADD CONSTRAINT `actors_movie_actor_id_movie_id_01924769_uniq` UNIQUE (`actor_id`, `movie_id`);
ALTER TABLE `actors_movie` ADD CONSTRAINT `actors_movie_actor_id_1cdc8fdb_fk_actors_id` FOREIGN KEY (`actor_id`) REFERENCES `actors` (`id`);
ALTER TABLE `actors_movie` ADD CONSTRAINT `actors_movie_movie_id_a197a03f_fk_movies_id` FOREIGN KEY (`movie_id`) REFERENCES `movies` (`id`);

장고에서 manytomanyfield를 썼더니 포린키 actor_id, movie_id로 구성된 중간테이블이 자동으로 만들어졌다.

IntegerField뒤에 ()를 빼먹어서 아래와 같은 에러가 자꾸 났다.
SyntaxError: EOL while scanning string literal

쿼리셋을 이용한 shell에서의 CREATE

>>>from movie.models import Movie, Actor

>>> Movie.objects.create(title='good will hunting', running_time=127, release_date='1993-05-21')
<Movie: good will hunting>

>>> Movie.objects.create(title='martian', running_time=151, release_date='2015-10-08')
<Movie: martian>

>>> Actor.objects.create(first_name='Jessica', last_name='Chastain', birth_date='1977-03-24')
<Actor: JessicaChastain>  
#데이터가 알아서 성과 이름을 붙여서 저장해주는게 신기하다

>>> Movie.objects.create(title='miss sloane', running_time=132, release_date='2017-02-02')
<Movie: miss sloane>

>>> Actor.objects.create(first_name='Matt', last_name='Damon', birth_date='1970-10-08')
<Actor: MattDamon>
#ManytoManyfield를 사용해서 연결시켜주면 직접 Actor데이터에 movie_id를 넣을 수 
#없고 movie.set을 사용해서 넣어줘야한다고 한다.

>>> m1 = Movie.objects.get(id=1)
>>> a2 = Actor.objects.get(id=2)
>>> m1.actor.add(a2)
#AttributeError: 'Movie' object has no attribute 'actor'
#나는 Actor클래스에 ManyToManyField를 넣어줬기 때문에 
# movie에 actor를 넣어줄 수 없고 actor에 movie를 넣어줘야한다.
>>> a2.movie.add(m1)
>>> Actor.objects.get(id=1).movie.add(Movie.objects.get(id=3))

영화와 배우 테이블에 데이터 삽입하고 연결한 결과 영화와 배우 테이블에 데이터 삽입하고 연결한 결과

영화와 배우 중간테이블에 데이터 삽입된 결과 영화와 배우 중간테이블에 데이터 삽입된 결과

View 작성하기

  1. 등록된 배우 목록을 리턴해주는 GET 메소드를 구현해주세요.
    • 배우의 이름, 성, 그리고 출연한 영화 제목 목록
  2. 등록된 영화 목록을 리턴해주는 GET 메소드를 구현해주세요.
    • 영화의 제목, 상영시간, 출연한 배우 목록 (이름만)
# movie > views.py
import json

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

from movie.models import Movie, Actor

class ActorView(View):
    def get(self, request):
        actors = Actor.objects.all()
        results=[]
        for actor in actors:
            #movies = [{movie.title for movie in actor.movie.all()}]
            # for movie in actor.movie.all():
            results.append(
            {
                "name" : actor.first_name + ' ' + actor.last_name,
                "Filmography" : [{"movie" : movie.title} for movie in actor.movie.all()]
            }
        )
        return JsonResponse({'result':results}, status=200)

class MovieView(View):
    def get(self, request):
        movies = Movie.objects.all()
        results=[]
        for movie in movies:
            results.append(
            {
                "title" : movie.title,
                "running time" : movie.running_time
            }
        )
        return JsonResponse({'result':results}, status=200)

GET으로 데이터베이스 JSON파일 형식으로 출력

http -v GET 127.0.0.1:8000/movie/actor

결과1

urls.py

client로 부터 데이터 요청을 받으면 우선 제일 부모 폴더에 있는 urls.py로 받는다.

movie_project(상위 프로젝트 폴더) > urls.py

from django.urls import path

urlpatterns = [
    path('movie', include('movie.urls'))
]
# 부모 urls.py, server로 movie 앱을 사용하겠다는 요청이 오면 movie로 보내주는 역할을 한다.

movie_project > movie(내부 앱 폴더) > urls.py

from django.urls   import path
from movie.views     import MovieView, ActorView

urlpatterns = [
    path('/actor', ActorView.as_view()),
    path('/movie', MovieView.as_view()),
    #as_view는 해당 경로까지 온 요청이 어떤 method인지 판단해주는 함수이다.
]

SQL 테이블 출력

전체 디비 출력

주요 포인트 및 생각해볼 점

테이블끼리 넘나들면서 데이터를 가져오는 뷰를 작성하는 부분이 너무 헷갈려서
쉘에서 쿼리셋을 치는 부분부터 다시 연습해봐야겠다.

장고 쿼리셋 API 문서

장고 ManyToMany 관계 문서

추가 쿼리셋 연습문제

제시카 차스테인이 출연한 영화들을 리스트로 출력

>>> a0 = Actor.objects.get(id = 1)
>>> <Actor: JessicaChastain>

>>> a0.movie.all() #여러개의 쿼리셋으로 받기
>>> <QuerySet [<Movie: martian>, <Movie: miss sloane>, <Movie: interstella>]>

>>> list(a0.movie.all()) #리스트 형태로 받기
>>> [<Movie: martian>, <Movie: miss sloane>, <Movie: interstella>]

>>> {movie.title for movie in a0.movie.all()}
>>> {'interstella', 'martian', 'miss sloane'}

>>> [movie.title for movie in list(a0.movie.all())]
>>> ['martian', 'miss sloane', 'interstella']

>>> Movie.objects.filter(actor__id=1)
#역참조할 때는 밑줄을 두 개 
>>> <QuerySet [<Movie: martian>, <Movie: miss sloane>, <Movie: interstella>]>

인터스텔라에 출연한 배우들 리스트 출력

>>> Movie.objects.filter(title='interstella')
<QuerySet [<Movie: interstella>]>

>>> m1 = Movie.objects.get(title='interstella')
#위의 filter는 쿼리셋이기 때문에 변수로 쓰려면 get으로 한 개만 받아서 넣어줘야한다.

>>> m1.actor_set.all()
#역참조할 때는 _set 추가
>>> <QuerySet [<Actor: JessicaChastain>, <Actor: TimotheeChalamet>]>

>>> {actor.first_name +' '+ actor.last_name for actor in m1.actor_set.all()}
>>> {'Jessica Chastain', 'Timothee Chalamet'}

이렇게 쿼리셋을 연습해서 배우가 출연한 영화들을 리스트로 출력해봤다. 영화 리스트 출력

class ActorView(View):
    def get(self, request):
        actors = Actor.objects.all()
        results=[]
        for actor in actors:
            results.append(
                {
                    "name" : actor.first_name + ' ' + actor.last_name,
                    "Filmography" : [movie.title for movie in list(actor.movie.all())]
                }
            )
        return JsonResponse({'result':results}, status=200)

장고 CRUD2 주인과 강아지

|

위코드에서 CRUD 두번째 세션을 마치고 과제인 주인과 강아지를 하면서 진행 과정을 써보았다.

장고 CRUD2 과제 주인과 강아지

주인과 강아지 ERD

POST

각 기능을 서로 다른 클래스로 구현해주세요.

  1. 신규 주인 등록
  2. 강아지 등록 (주인정보 필요)

위 기능을 구현 후 직접 httpie 를 활용하여 주인 2명의 정보와 각 주인 당 2~3마리의 강아지 정보를 데이터베이스에 저장해주세요.

GET

각 기능을 서로 다른 클래스로 구현해주세요.

  1. 주인 리스트
    • 이름, 이메일, 나이 포함
  2. 강아지 리스트
    • 이름, 나이, 주인 이름 포함
  3. 주인 리스트 (1번 코드에 추가)
    • 이름, 나이 포함, 키우는 강아지 리스트 (이름, 나이 포함)

진행 과정

초기 세팅 진행

  • Miniconda 가상환경 생성 및 가상환경 activate
    conda create -n dog python=3.8
    conda activate dog
    
  • Database 생성
    mysql -u root -p #마이sql시작
    
    mysql> create database dog character set utf8mb4 collate utf8mb4_general_ci;
    
  • 프로젝트 시작을 위한 python package 설치
    pip install Django #장고 설치
    pip install mysqlclient # mysqlclient설치 (mysql 먼저 설치 필요!!!)
    pip install django-cors-headers #CORS 해결을 위한 패키지
    
  • 장고 프로젝트 생성
    django-admin startproject dog_owner #프로젝트 생성
    cd dog #프로젝트 디렉토리로 들어감
    python manage.py startapp dog #앱 생성(manage.py가 존재하는 디렉토리에서)
    

프로젝트 폴더(dog_owner)의 settings.py 설정

setting.py파일 펼치기/접기
from pathlib        import Path #기존에 settings.py 에 있는 코드
from my_settings   import DATABASES, SECRET_KEY

SECRET_KEY = SECRET_KEY
DATABASES = DATABASES
ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    # 'django.contrib.admin', #admin도 
    # 'django.contrib.auth', #login auth도 직접 만들어쓸 예정이다
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders', # 추가
    'products', #새로 만든 앱 이름 추가
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    # 'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware', #추가함
]
##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를 추가 및 수정한다. 미들웨어에서 csrf와 auth를 주석으로 만들었어야되는데 잘못해서 그 전 줄을 주석화했다가
RuntimeError: Model class django.contrib.auth.models.Permission doesn’t declare an explicit app_label and isn’t in an application in INSTALLED_APPS.
에러가 계속 났었다. 수업을 들으면서 어떤 코드를 쓰거나 지울 때 그걸 왜 쓰는지 알고 써야될 것 같다.

my_setting.py 파일 추가

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

#클라우드에 올리지 않는, 키를 보관하는 파일
DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'dog',
        'USER': 'root',
        'PASSWORD': '',
        '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가 존재하는 디렉토리에)

git ignore 펼쳐서 보기/접기

# 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

#보안관련파일과 크롤링파일
my_setting.py
*.csv 

실행

pythong manage.py runserver

migration을 위한 models.py작성

from django.db import models

class Owner(models.Model):
    name = models.CharField(max_length=45)
    email = models.EmailField(max_length=254)
    age = models.BigIntegerField
    class Meta: 
        db_table = 'owners' 
    def __str__(self):
        return self.name

class Dog(models.Model):
    name = models.CharField(max_length=45)
    owner = models.ForeignKey('Owner', on_delete=models.CASCADE, default='')
    age = models.BigIntegerField
    class Meta:
        db_table='dogs'  
    def __str__(self):
        return self.name

migration 만들고 migrate 실행하기

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

sqlmigrate했을 때 나오는 구문은 아래와 같다.

BEGIN;
--
-- Create model Owner
--
CREATE TABLE "owners" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "name" varchar(45) NOT NULL, 
    "email" varchar(254) NOT NULL
    );
--
-- Create model Dog
--
CREATE TABLE "dogs" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "name" varchar(45) NOT NULL, 
    "owner_id" bigint NOT NULL REFERENCES "owners" ("id") DEFERRABLE INITIALLY DEFERRED
    );
CREATE INDEX "dogs_owner_id_b381d414" ON "dogs" ("owner_id");
COMMIT;

DEFERRABLE INITIALLY DEFERRED는 제약조건을 커밋할 때까지 연기하는거라고 하는데 아직 몰라도 될 것 같다.

에러

장고에는 원래 sqllite3가 있는데 위코드 커리큘럼에서 이걸 제거하고 mysql에 연결해주는 과정이 있었다. 그걸 하면서 settings.py에서 아래 코드를 지우고 DATABASES = DATABASES로 바꿨었는데 dog 장고앱을 만들면서 아래부분을 안 지웠더니 마이그레이션 이후에도 계속 mysql에 테이블이 생기지 않는 에러가 나서 2시간정도 고생하다가 다행히 학우분께서 이걸 찾아주셨다.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
#settings.py에서 이걸 지워야된다

쿼리셋을 이용한 shell에서의 CREATE

python manage.py shell 
>>>from dog.models import Dog, Owner
>>> Owner.objects.create(name='명주', email='abc@gmail.com')
<Owner: 명주>
>>> Owner.objects.create(name='경훈', email='123@gmail.com')
<Owner: 경훈>
>>> Owner.objects.create(name='은영', email='567@gmail.com')
<Owner: 은영>
>>> Dog.objects.create(name = '삼월', owner_id=1)
<Dog: 삼월>
>>> Dog.objects.create(name = '허순이', owner_id=2)
<Dog: 허순이>
>>> Dog.objects.create(name = '맹구', owner_id=2)
<Dog: 맹구>

dog 테이블

쿼리셋을 이용한 shell에서의 READ

>>> Dog.objects.all()
<QuerySet [<Dog: 삼월>, <Dog: 허순이>, <Dog: 맹구>]>
>>> Owner.objects.values()
<QuerySet [{'id': 1, 'name': '명주', 'email': 'abc@gmail.com'}, {'id': 2, 'name': '경훈', 'email': '123@gmail.com'}, {'id': 3, 'name': '은영', 'email': '567@gmail.com'}]>
>>> Dog.objects.values_list()
<QuerySet [(1, '삼월', 1), (2, '허순이', 2), (3, '맹구', 2)]>

CRUD2

brew install httpie

httpie는 파이썬으로 개발된 http 클라이언트 유틸리티로 개발 및 디버깅 용도로 사용 가능하다.
curl에 비해서 사용이 쉽고 가독성이 좋다.

View 작성하기

READ를 위한 get method

# dog>views.py
import json

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

from dog.models import Dog, Owner
class DogView(View):
    def get(self, request):
        dogs = Dog.objects.all()
        results=[]
        for dog in dogs:
            results.append(
            {
                "name" : dog.name,
                "owner" : dog.owner.name
            }
        )
        return JsonResponse({'result':results}, status=200)

class OwnerView(View):
    def get(self, request):
        owners = Owner.objects.all()
        results=[]
        for owner in owners:
            results.append(
            {
                "name" : owner.name,
                "email" : owner.email,
                "dogs" : owner.dog.name
            }
        )
        return JsonResponse({'result':results}, status=200)

client로 부터 데이터 요청을 받으면 우선 제일 부모 폴더에 있는 urls.py로 받는다.

#dog_owner(상위 프로젝트 폴더) > urls.py
#상위 프로젝트 폴더 dog_owner가 dog앱 안의 urls을 바라보게 만들어줌
from django.urls import path, include

urlpatterns = [
    path('dog', include('dog.urls'))
]

여기가 부모 urls.py 이다. 여기는 server로 오는 요청을 경로를 처리하는 파일이라고 생각하면 된다.

localhost:8000/dog

위의 경로로 dog app을 사용하겠다는 요청이 오면 다시 상세 app으로 보내주는 역할을 한다.
그 역할을 하는 것이 바로 include 이다. 그러면 그 해당 app 의 urls.py로 보내준다.
근데 처음 startapp 으로 app 을 생성했을 때 urls.py는 존재하지 않는다. 그렇기 때문에 dog app 안에 새로 생성해줘여한다.

#dog(하위 앱 폴더, urls.py새로 만들어주어야함) > urls.py
#하위 앱 폴더 dog에서 DogView를 연결
from django.urls   import path
from dog.views     import OwnerView, DogView
#OwnerView, DogView 는 view.py로부터 get, post 기능을 수행하는 클래스를 import해온다.

urlpatterns = [
    path('/dog', DogView.as_view()),
    path('/owner', OwnerView.as_view()),
    #as_view는 해당 경로까지 온 요청이 어떤 method인지 판단해주는 함수이다.
]

둘을 바꿔 넣어서 에러가 났었는데 동료분께서 찾아주셨다..!!

#url의 위치
.
├── manage.py
├── products
│   ├── models.py
│   ├── urls.py (없음, 새로 생성해야함)
│   └── views.py
└── westarbucks
    └── urls.py : main urls.py (부모, 요청 url 분석을 가장 먼저 하는 위치)

CREATE를 위한 post method

POST방식으로 개와 주인 데이터를 추가해보자.

# dog>views.py
import json

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

from .models import Dog, Owner

class OwnerView(View):
    def post(self, request):
        data  = json.loads(request.body)
        # view는 받아온 json 데이터를 template의 html 파일로 전달하고 
        # 템플릿으로 전달된 json 데이터는 자바스크립트에 의해 활용할 수 있는 
        # 형태로 다시 받아온다. 자바스크립트로 json데이터를 자유롭게 사용할 수 있다.
        name = data['name']
        # 받아온 json데이터 중 변수 name의 키값을 name변수에 저장한다 
        email= data['email']
        Owner.objects.create(name = name, email=email)        
        # Owner모델을 이용해서 받아온 name과 email의 값을 가진 데이터를 추가한다.
        return JsonResponse({'Message':'Created'}, status=201)

    def get(self, request):
        owners = Owner.objects.all()
        results=[]
        for owner in owners:
            dogs = 
            [
                {"이름":dog.name} for dog in Dog.objects.filter(owner_id=owner.id)
            ]
            # for문부터 읽어서, model에서 owner의 포린키 아이디(owner_id)가 
            # 오너의 아이디(owner.id)와 일치하는 개들을 filter로 불러와서 
            # "이름": 값 형태로 dogs라는 변수에 저장한다.
            results.append(
            {
                "name" : owner.name,
                "email" : owner.email,
                "dog_list" : dogs
            }
        )
        return JsonResponse({'result':results}, status=200)

class DogView(View):
    def post(self, request):
        data = json.loads(request.body)
        # owner = Owner.objects.get(name = data['owner']) 이렇게 해서 owner = owner라고 써줘도 됨
        # 프론트에서 준 데이터인 오너의 이름을 가진 데이터들을 불러와서 owner변수에 저장
        dog = Dog.objects.create(
            name = data['name'], 
            owner_id = data['owner_id']
        )
        # Dog모델을 이용해서 받아온 name과 owner의 값을 가진 데이터를 추가한다.

            if not Owner.objects.filter(id=data["owner_id"].exists():
                return JsonResponse({"message": "Owner_does_not_exist", status=404})
            
        return JsonResponse({'Message':'Created'}, status=201)

    def get(self, request):
        dogs = Dog.objects.all()
        results=[]
        for dog in dogs:
            results.append(
            {
                "name" : dog.name,
                "owner" : dog.owner.name
            }
        )
        return JsonResponse({'result':results}, status=200)

/dog/dog 해야되는걸 모르고 한시간정도 헤메였는데 한성봉님의 블로그를 보고 알게되었다. 한성봉님의 블로그

POST 로 주인과 강아지 추가

http -v POST 127.0.0.1:8000/dog/dog name="땅콩" owner="경훈"

owner=”경훈” 이렇게 이름으로 찾아도 되지만 owner_id=3 해서 pk으로 주고받는것이 정석이다. 주인과 강아지 추가

GET으로 데이터베이스 JSON파일 형식으로 출력

http -v GET 127.0.0.1:8000/dog/owner 

전체 디비 출력

주요 포인트 및 생각해볼 점

이 부분은 나는 다른 분 블로그를 보고 안 썼으면 정말 몰랐을 것 같다.

dogs = [{"이름":dog.name} for dog in Dog.objects.filter(owner_id=owner.id)]

장고 설정, CRUD1 (migration 초기화하기)

|

개인적으로 복습할 용도로 명령어 위주로 적었어서 혹시 전체적인 흐름에 대해서 더 잘 알고싶다면 아래 두 분의 블로그를 추천한다.
홍데브님의 블로그
cmin님의 블로그

장고 설정

장고 프로젝트 시작

  • Miniconda 가상환경 생성 및 가상환경 activate
conda create -n 가상환경이름 python=3.8
conda activate 가상환경이름
conda info --envs #설정한 가상환경 리스트 확인
  • Database 생성
mysql.server start
mysql -u root -p #마이sql시작
mysql> create database "NAME" character set utf8mb4 collate utf8mb4_general_ci;
  • 프로젝트 시작을 위한 python package 설치 컴퓨터에 mysql응용프로그램이 켜져있으면 터미널로 열리지 않을 수 있다.
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가 존재하는 디렉토리에서)

settings.py 설정

from pathlib        import Path #기존에 settings.py 에 있는 코드
from my_settings   import DATABASES, SECRET_KEY

#시크릿 키와 데이터베이스 변수는 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 추가
]

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

#REMOVE_APPEND_SLASH_WARNING 추가
APPEND_SLASH = False

##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': '',
        '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 = [
]

데이터베이스 생성

  • mysql 데이터베이스 생성
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

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

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

gitignore.io

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

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

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

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

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

*.csv (crwaling한 파일 역시 업로드하지 않는다.)

git ignore 펼쳐서 보기/접기

# 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

#보안관련파일과 크롤링파일
my_settings.py
*.csv 

실행

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의 경우 설치한 파일

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

디렉토리 구조

#(참고) 프로젝트 디렉토리 구조 구조
.
└── suntae
    ├── manage.py
    ├── my_settings.py
    └── westagram
        ├── \__init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

CRUD 1

ERD테이블

스타벅스 페이지의 ERD 스타벅스 페이지를 보고 데이터 관계도를 만든다. 새로 생성된 앱의 models.py에 아래 migration을 넣어서 데이터베이스 토대를 만들어준다.

migration

models.py 펼쳐서 보기/접기
from django.db import models

class Menu(models.Model):
    name = models.CharField(max_length=45)
    #id는 장고가 자동으로 만들어주니까 클래스 객체에 안 넣어도 됨
    
    class Meta: #테이블 이름 지정
        db_table = 'menus' #안 쓰면 products_menus라고 컬럼이름 자동 생성
    
    def __str__(self):
        return self.name

class Category(models.Model):
    name = models.CharField(max_length=45)
    menu = models.ForeignKey('Menu', on_delete=models.CASCADE, default='')
    #default에러가 떠서 디폴트를 추가했다 
    #외래키 쓸 때 장고가 알아서 _id를 붙여서 데이터베이스에 넘겨서 _id를 안 써도 된다.
    
    class Meta:
        db_table='categories'
    
    def __str__(self):
        return self.name

class Product(models.Model):
    korean_name = models.CharField(max_length=45, default='')
    english_name = models.CharField(max_length=45, default='')
    description = models.TextField(max_length=300, default='')
    isnew = models.BooleanField(default=False)
    category = models.ForeignKey('Category', on_delete=models.CASCADE) #cascade : 부모개체가 사라지면 자식도 사라지도록
    
    class Meta:
        db_table = 'products'

    # def __str__(self):
    #     return self.name
    # 객체에 name이 없는데 위의 매쏘드를 넣었다가 쿼리셋 작성시 Attribution 에러가 났었다.

class Image(models.Model):
    image_url = models.URLField(max_length=2000)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)
    
    class Meta:
        db_table = 'images'

class Allergy(models.Model):
    name = models.CharField(max_length=45)
    products = models.ManyToManyField('Product', through='AllergyProduct')
    
    class Meta:
        db_table = 'allergies'

class AllergyProduct(models.Model):
    allergy = models.ForeignKey('Allergy',on_delete=models.CASCADE)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)
    
    class Meta:
        db_table = 'allergies_products'

class Nutrition(models.Model):
    kcal = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    sodium = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    s_fat = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    sugar = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    protein = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    caffeine = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    product = models.OneToOneField('Product', on_delete=models.CASCADE)

    class Meta:
        db_table = 'nutritions'

처음에 뭔가 잘못 만들어서 마이그레이션 파일을 지우고 초기화했다. models.py를 처음 만들때 정성스럽게 만들어야할 것 같다.

migration 초기화하기

데이터베이스를 다 날려도 괜찮은 경우에 아래의 방법으로 마이그레이션을 재설정할 수 있다.

  1. 마이그레이션 폴더에서 __init__.py을 제외한 모든 파일을 삭제하거나 Unix OS의 경우 아래 명령어를 실행한다.
    find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
    find . -path "*/migrations/*.pyc"  -delete
    
  2. sql에서 모든 테이블을 드랍한다.
    DROP DATABASE westarbucks_db; --db 전체 삭제
    create database westarbucks_db character set utf8mb4 collate utf8mb4_general_ci; --데이터베이스 생성 및 utf8설정
    
  3. 마이그레이션을 다시 실행한다.
    python manage.py makemigrations
    python manage.py migrate
    

    출처: How to Reset Migrations

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 products; --프로덕츠 테이블이 잘 만들어졌는지 보여준다

마지막 명령어를 치면 해당 테이블 컬럼들의 조건식들이 잘 만들어졌는지 볼 수 있다. products 테이블

QuerySet으로 CRUD하기

Django의 QuerySet API는 데이터 작업을 위한 포괄적인 메서드를 제공한다.
※ QuerySet: Django에 내장 된 일반적인 데이터 관리 기능
일반적으로 자주 사용되는 Model method를, 정확하게는 QuerySet method들은 아래와 같다.
Model method의 실행 결과는 QuerySet을 반환하거나 그렇지 않은 경우가 있다.

  • Model method 종류 all() , filter() , exclude() , values() , values_list() , get() , create() , count() , exists() , update() , delete() , first() , last() ..
  • QuerySet을 반환하는 경우 <QuerySet [<Category: Category object (1)>, <Category: Category object (2)>]> (장고에서 만든 클래스의 인스턴스, 객체들이 모여 있는 리스트)
  • 그렇지 않은 경우 <Category: Category object (1)> , 9 , True ..

QuerySet을 반환하지않는 Model method

create()

Table에 데이터를 추가(INSERT) 해주는 method로, 생성된 인스턴스를 반환해준다.

In  : Category.objects.create(name='콜드브루')
Out : <Category: Category object (1)>

#category 변수에 반환된 값을 저장하고, 생성된 data를 사용할 수 있다.
#인스턴스로 반환되므로 category.name으로 class 안에 변수에 접근할 수 있다.
In  : category = Category.objects.create(name='콜드브루')
In  : category.name
Out : '콜드브루'

참고) save method : INSERT 또는 UPDATE
Category(name='콜드브루').save()

get()

지정된 조회 매개 변수와 일치하는 인스턴스를 반환합니다.이 매개 변수는 필드 조회에 설명 된 형식이어야합니다.

In  : Category.objects.get(id=1)
Out : <Category: Category object (1)>

update()

지정된 필드에 대해 업데이트 쿼리를 수행하고 일치하는 행 수를 반환한다. (일부 행에 이미 새 값이있는 경우 업데이트 된 행 수와 같지 않을 수 있음).

In  : Category.objects.filter(name='탄산').update(name='콜드브루')
Out : 2 #총 업데이트된 row 개수

delete()

QuerySet의 모든 행에 대해 SQL 삭제 쿼리를 수행하고 삭제 된 개체 수와 개체 유형별 삭제 횟수가 있는 dictionary를 반환합니다.

In  : Category.objects.filter(name='qp').delete()
Out : (1, {'products.Category': 1})

save()

INSERT 또는 UPDATE 를 수행하는 method로, 단일 객체에 대해서 업데이트를 수행할 때 많이 사용된다.

In  : category = Category.objects.get(id=2)
Out : <Category: Category object (2)>

In  : category.name
Out : '브루드커피'

In  : category.name = 'new name'
In  : category.save()

In  : category.name
Out : 'new name'

QuerySet을 반환하는 Model method

all()

한 테이블의 모든 레코드를 가져오려면 아래와 같이 all() method를 사용합니다. 그 결과로 QuerySet 을 반환합니다. 이때, QuerySet 안에는 각각 인스턴스가 포함되어 있습니다.

In  : Category.objects.all()
Out : <QuerySet [<Category: Category object (2)>, <Category: Category object (3)>, <Category: Category object (4)>, <Category: Category object (5)>, <Category: Category object (6)>, <Category: Category object (7)>]>

In  : for category in Category.objects.all()
		print(category.name)

#아래와 같이 인스턴스들이 담겨 있는 QuerySet이 반환되기 때문에, 모든 속성에 접근해서 데이터를 관리할 수 있습니다.
Out : 
      브루드커리
      브루드커피
      콜드브루
      콜드브루

filter() & exclude()

한 테이블의 특정 레코드를 가져오려면 필터를 사용할 수 있다. filter() method는 가장 자주 사용하는 필터 기능이다. filter(**kwargs): 키워드 인자로 주어진 lookup 조건에 일치하는 레코드들의 QuerySet을 반환한다.

case1
In  : Category.objects.filter(name='브루드커피')
Out : [<Category: Category object (3)>, <Category: Category object (4)>]

case2
In  : Category.objects.filter(name='브루드커피').filter(id=3)
Out : [<Category: Category object (3)>]

case3
In  : Category.objects.filter(name='브루드커피').exclude(id=3)
Out : [<Category: Category object (4)>]

values()

iterable로 사용될 때 모델 인스턴스가 아닌 dictionary을 포함하는 QuerySet을 반환합니다.

In  : Category.objects.filter(name='브루드커피')
Out : [<Category: Category object (3)>, <Category: Category object (4)>]

In  : Category.objects.filter(name='브루드커피').values()
Out : <QuerySet [{'id': 3, 'name': '브루드커피', 'created_at': datetime.datetime(2020, 9, 8, 5, 43, 30, 4068, tzinfo=<UTC>), 'updated_at': datetime.datetime(2020, 9, 8, 5, 43, 30, 21801, tzinfo=<UTC>)}, {'id': 4, 'name': '브루드커피', 'created_at': datetime.datetime(2020, 9, 8, 5, 43, 30, 4068, tzinfo=<UTC>), 'updated_at': datetime.datetime(2020, 9, 8, 5, 43, 30, 21801, tzinfo=<UTC>)}]>

values_list()

values_list() method는 dictionary를 반환하는 대신 반복 될 때 튜플을 반환한다는 점을 제외하면 values ()와 유사합니다. 각 튜플에는 values_list () 호출에 전달 된 각 필드 또는 표현식의 값이 포함되어 있으므로 첫 번째 항목이 첫 번째 필드입니다.

In  : Category.objects.filter(name='브루드커피').values_list()
Out : <QuerySet [(3, '브루드커피', datetime.datetime(2020, 9, 8, 5, 43, 30, 4068, tzinfo=<UTC>), datetime.datetime(2020, 9, 8, 5, 43, 30, 21801, tzinfo=<UTC>)), (4, '브루드커피', datetime.datetime(2020, 9, 8, 5, 43, 30, 4068, tzinfo=<UTC>), datetime.datetime(2020, 9, 8, 5, 43, 30, 21801, tzinfo=<UTC>))]>

CREATE

장고에서 미리 있는 models.Model을 이용해서 Person.objects.create 매쏘드를 이용해서 데이터를 만들 수 있다.

python manage.py shell #파이썬에서 쉘 열기
>>>from products.models import Menu
#쉘에서 장고를 연결해주려면 import해야함, 시작 전 작성한 모델 클래스 import
>>> Menu.objects.create(name = '음료')  #class명.objects.method명(~~)

exit() 하면 쉘을 종료할 수 있다. 주의 : shell을 켤때마다 from products.models import Menu 클래스를 임포트해주어야한다.

위와같이 입력하면 Menu클래스를 이용하여 ‘음료’가 name컬럼에 삽입된 것을 볼 수 있다.
왠지 모르겠지만 쌍따옴표를 쓰니까 에러가 나서 작은따옴표를 썼다.

쿼리셋 create 결과1

이번에는 영양성분표를 넣어보자

from products.models import Nutrition
Nutrition.objects.create(product_id=1, one_serving_kcal=75, sodium_mg=20, saturated_fat_g=2, sugars_g=10, caffeine_mg=245)

product_id를 1로 하고 싶은데 내가 저번에 쓰다지웠다해서 그런지 product_id=1자리가 비워져있지 않다. 테이블을 드랍한 뒤에 다시 시도해본다.

mysql> drop table nutritions;

드랍했더니 테이블 자체가 사라져버렸다 ㅋㅋㅋㅋㅋ 원래 컬럼값이 마음에 안 들었으니 테이블을 sql구문으로 다시 만들어보자. 데이터만 지우려면 Truncate를 사용해야한다.

class Nutrition(models.Model):
    kcal = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    sodium = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    s_fat = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    sugar = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    protein = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    caffeine = models.DecimalField(max_digits=5, decimal_places=1, null=True)
    product = models.OneToOneField('Product', on_delete=models.CASCADE)
    
    class Meta:
        db_table = 'nutritions'

이걸 sql로 다시 쓰면 아래와 같다.

CREATE TABLE nutritions
(
    id INTEGER NOT NULL,
    product_id INTEGER,
    kcal DECIMAL(5,1),
    sodium DECIMAL(5,1),
    s_fat DECIMAL(5,1),
    sugar DECIMAL(5,1),
    protein DECIMAL(5,1),
    caffeine DECIMAL(5,1),
    PRIMARY KEY (id),
    FOREIGN KEY (product_id) REFERENCES products (id)
);

외래키가 중복된다고 에러가 난다!

ERROR 3780 (HY000): Referencing column ‘product_id’ and referenced column ‘id’ in foreign key constraint ‘nutritions_ibfk_1’ are incompatible.

sql문에서 외래키부분(FOREIGN KEY (product_id) REFERENCES products (id))을 제외하고 실행해서 테이블을 다시 만들었다.

ALTER TABLE nutritions
ADD FOREIGN KEY (product_id) REFERENCES products(id);

위 명령어를 이용해 sql로 외래키를 추가하려고 했는데 3780에러가 계속 났다.
인터넷에서 검색하다보니 mysql에서 보이지는 않지만 외래키 constraint 설정되어있는 것과 관련이 있는 것 같다. 모든 constraint를 보는 명령어를 쳐봤다.

select * from information_schema.table_constraints where constraint_schema = 'westarbucks_db';

sql - 모든 제약조건 출력

에러에서 나왔던 외래키 constraint ‘nutritions_ibfk_1’가 표에는 나오지 않는다.
그래도 지울 수 있을까 싶어서 아래 명령어를 쳐봤다.

ALTER TABLE nutritions DROP CONTRAINT nutritions_ibfk_1;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘nutritions_ibfk_1’ at line 1

문법을 틀린 것 같은데 너무 멀리 온 것 같으니까 싹 밀고 마이그레이션을 다시 해야겠다.

READ

get은 쿼리셋을 1개, all(전부)과 filter는 쿼리셋을 많이 불러올 수 있고 쿼리셋에 담겨온 데이터들은 for문을 사용할 수 있다. filter는 쿼리셋을 0개 불러올 수도 있다.

>>> from products.models import Menu
>>> Menu.objects.all()
#<QuerySet [<Menu: 음료>, <Menu: 푸드>]
Menu.objects.get(name="음료")
#<Menu: 음료>
Menu.objects.filter(name="음료")
#<QuerySet [<Menu: 음료>]>
Menu.objects.filter(name="음료").values()
#<QuerySet [{'id': 1, 'name': '음료'}]>

values를 쓰면 제일 편하지만 처음에는 관계를 생각하면서 read하는걸 연습하기 위해서 다른 방법을 사용한다. - 위코드 멘토님

07.20 쿼리셋을 충분히 연습하지 않았어서 CRUD2를 할 때 복잡한 쿼리셋을 써야하면서 애를 먹었다. 뒤로 가니까 이런 식으로 사용하게 되는데 연습을 충분히 하지 않아서 그런지 잘 모르겠다. 다른 학우분들은 value+for문을 사용하고 있는데 보면서 물어보고 더 공부해봐야겠다.

Owner.objects.get(name = data['owner'])

[{"이름":dog.name, "나이":dog.age} for dog in Dog.objects.filter(owner_id=owner.id)] 

UPDATE

쿼리셋을 먼저 불러와야 update를 사용할 수 있다.
어떤 모델매쏘드가 어떤 데이터타입을 어떻게 반환하는지 알아야한다.

프로그래머스 level 2 소수찾기

|

걸린 시간 : 3시간

문제 설명

한자리 숫자가 적힌 종이 조각이 흩어져있습니다. 흩어진 종이 조각을 붙여 소수를 몇 개 만들 수 있는지 알아내려 합니다.

각 종이 조각에 적힌 숫자가 적힌 문자열 numbers가 주어졌을 때, 종이 조각으로 만들 수 있는 소수가 몇 개인지 return 하도록 solution 함수를 완성해주세요.

  • numbers는 길이 1 이상 7 이하인 문자열입니다.
  • numbers는 0~9까지 숫자만으로 이루어져 있습니다.
  • “013”은 0, 1, 3 숫자가 적힌 종이 조각이 흩어져있다는 의미입니다.

입출력 예

numbers return
"17" 3
"011" 2

예제 #1
[1, 7]으로는 소수 [7, 17, 71]를 만들 수 있습니다.

예제 #2
[0, 1, 1]으로는 소수 [11, 101]를 만들 수 있습니다.

사고 과정

주어진 숫자들의 순열로 만든 숫자를 리스트에 저장한다. 그 중 소수를 찾아서 소수의 갯수를 출력한다.

import itertools
from itertools import permutations
#순열(반복 가능한 객체에 대해서 중복을 허용하지 않고 r개를 뽑아서 나열한다)
import math

def solution(numbers):
    nums=[]
    nums = list(str(numbers))
    arr2=[]
    for i in range(1,len(nums)+1):
        arr=[]
        arr = list(map(''.join, itertools.permutations(nums, i)))
        for i in arr:
            arr2.append(i)

    arr3=[]
    arr3 = list(map(int, arr2))
    #01인 경우 1로 바꿔줌
    arr3 = list(map(str,arr3))
    #set을 하기 위해서 문자로 바꿔줌
    arr3 = set(arr3)
    #iterate하기 위해서 다시 숫자로 바꿔줌
    arr3 = list(map(int, arr3))

    def isPrime(n): 
        if n == 1 or n==0:
            return False
        for div in range(2,int(math.sqrt(n))+1):
            if n%div == 0:
                return False
        return True

    answers = []
    
    for i in arr3:
        if isPrime(i):
            answers.append(i)

    return len(answers)

처음에 1,4,10,12 번이 실패해서 앞에 01 이렇게 0이 들어가는 수에서 앞의 0을 지워주는 구문을 넣었다.

모범 답안

from itertools import permutations
def solution(n):
    a = set()
    for i in range(len(n)):
        a |= set(map(int, map("".join, permutations(list(n), i + 1))))
    a -= set(range(0, 2))
    for i in range(2, int(max(a) ** 0.5) + 1):
        a -= set(range(i * 2, max(a) + 1, i))
    return len(a)

가능한 수를 a에 저장하고 에라토스테네스 체를 사용해서 a에서 제거했다. a |= 이건뭘까?? ㅋㅋㅋㅋ

주요 포인트 및 생각해볼 점

내 코드는 흐질구질하지만 일단 통과했으니 다음에 다시 와서 살펴보기로 하겠다.