API 개발/API 개발 Project

API Project : 레시피 생성 API 개발

신강희 2024. 5. 29. 15:20
728x90

< 레시피 생성 API 개발 >

 

# 레시피를 생성하는 API 개발을 위해 우선 필요한 데이터를 MySQL Workbench를 이용하여 DB 및 테이블을 생성하고 recipe_db 전용 커넥터 생성

- 개발 코드중 쿼리문 작성은 모두 MySQL에서 테스트를 먼저 진행하고 사용

 

 

# CMD로 serverless 실행하여 배포 폴더 생성하고 Github 연동

 

# Postman을 실행하여 API 설계 및 리퀘스트 생성

 

# Serverless로 생성한 폴더를 Visual Studio Code로 실행하여 설계한 API맞게 개발 시작

- API 개발은 resources 폴더를 생성하여 그안에 파일로 작성

- 보안관리와 비밀번호용 config.py와 utils.py 파일 생성

- API 연결코드 작성은 app.py DB 커넥터는 mysql_connection.py 파일 생성하여 작성

 

- app.py 파일

 
# API를 처리하는 코드는
# Resource 클래스를 상속받아서 작성한다.
# 이 클래스에는 get, post, put, delete 함수를 상속받는다.
# 따라서 이 함수들을, 우리의 서비스에 맞게 수정해서 사용하면된다.

from flask import Flask
from flask_restful import Api
# 생성한 API class 사용을위해서 생성시마다 import 필요!
from resources.recipe import RecipeListResource, RecipePublishResource, RecipeResource
from resources.user import UserLoginResource, UserLogoutResource, UserRegisterResource

from flask_jwt_extended import JWTManager
from config import Config

from resources.user import jwt_blacklist

app = Flask(__name__)

# 환경변수 셋팅 (jwt 사용을 위한)
app.config.from_object(Config)

# JWT 매니저 초기화
jwt = JWTManager(app)

# 로그아웃된 토큰으로 요청하는 경우, 처리하는 함수 작성
@jwt.token_in_blocklist_loader
def check_if_token_is_revoked(jwt_header, jwt_payload) :
    jti = jwt_payload['jti']
    return jti in jwt_blacklist

api = Api(app)

# 경로(path)와 리소스(API 코드)를 연결한다.
# <int> flask 문법임 외워야됨
api.add_resource( RecipeListResource , '/recipes')
api.add_resource( RecipeResource , '/recipes/<int:recipe_id>')
api.add_resource( RecipePublishResource , '/recipes/<int:recipe_id>/publish')
api.add_resource( UserRegisterResource , '/users/register')
api.add_resource( UserLoginResource, '/users/login')
api.add_resource( UserLogoutResource, '/users/logout')

if __name__ == '__main__' :
    app.run()
 

 

- config.py 파일


class Config :
    # 대문자로 적는 이유는 바뀌지 않는 상수를 뜻하기 위해
    # 앞으로는 접속 ID / PW 는 config로 관리 이 파일은 GIT hub에 올리면 안됨
    HOST = 'yh-db.cpfgfgmsk4u1po.ap-northeast-2.rds.amazonaws.com'
    DATABASE = 'recipe_db'
    DB_USER = 'recipe_db_user'
    DB_PASSWORD = '1234'

    # 실무에서 이 키값은 절대 노출되면 안됨
    SALT = 'sad44;klfjsdlkf12@dkfj'

    # JWT 관련 변수 셋팅
    # 실무에서 이 키값은 절대 노출되면 안됨
    JWT_SECRET_KEY = 'yhscdfdool,2405422'
    # False로 설정하면 유효기간이 없고, True로 설정하면 유효기간이 생긴다.
    JWT_ACCESS_TOKEN_EXPIRES = False
    PROPAGATE_EXCEPTIONS = True
 

 

- mysql.connection.py 파일

 
from config import Config
import mysql.connector

# mysql db 에 접속하는 함수
def get_connection() :

    connection = mysql.connector.connect(
        host = Config.HOST ,
        database = Config.DATABASE,
        user = Config.DB_USER,
        password = Config.DB_PASSWORD
    )
    return connection
 

 

- resources > recipe.py 파일

 
from flask import request
from flask_jwt_extended import get_jwt_identity, jwt_required
from flask_restful import Resource

from mysql_connection import get_connection
from mysql.connector import Error

class RecipeListResource(Resource) :
    # 레시피를 생성하는 API

    # JWT 토큰이 헤더에 있어야지만 이 API를 실행할수 있다는 뜻
    # JWT 토큰이 필수로 필요하도록 지정하는 함수!! == 로그인한 유저만 이 API 실행이 가능하다.
    # JWT 토큰 생성후 1-1. 과정 추가함
    @jwt_required()

    def post(self) :

        # 1. 클라이언트가 보내준 데이터가 있으면
        #    그 데이터를 받아준다.

        data = request.get_json()

        # 1-1. 헤더에 JWT 토큰이 있으면, 토큰 정보도 받는다.
        user_id = get_jwt_identity()

        # 2. 이 정보를 DB에 저장한다. (MySQL 커넥터를 사용하는 순서)
        try :
            ### 1. DB에 연결
            connection = get_connection()

            ### 2. 쿼리문 만들기
            query = '''insert into recipe
                    (user_id, name, description, num_of_servings, cook_time, directions)
                    values
                    ( %s, %s, %s, %s, %s, %s );'''

            ### 3. 쿼리에 매칭되는 변수 처리 => 튜플로!!
            record = (user_id, data['name'], data['description'], data['num_of_servings'], data['cook_time'], data['directions'])

            ### 4. 커서를 가져온다.
            cursor = connection.cursor()

            ### 5. 쿼리문을 커서로 실행한다.
            cursor.execute(query, record)

            ### 6. DB에 완전히 반영하기 위해서는 commit 한다.
            connection.commit()

            ### 7. 자원 해제
            cursor.close()
            connection.close()

        except Error as e :
            if cursor is not None :
                cursor.close()
            if connection is not None :
                connection.close()
            return {'result' : 'fail' , 'error' : str(e)}, 500

        return {'result' : 'success'}, 200
   
    # 전체 레시피를 가져오는 API
    @jwt_required()
    def get(self) :

        # 1. 클라이언트가 보낸 데이터가 있으면
        # 받아준다.

        offset = request.args['offset']
        limit = request.args['limit']

        print(offset, limit)

        # 1-2. JWT 토큰에서 유저아이디 가져온다.
        user_id = get_jwt_identity()

        # 2. DB로부터 데이터를 가져온다.
        try :
           
            connection = get_connection()

            query = '''select *
                        from recipe
                        where user_id = %s
                        limit '''+offset+''', '''+limit+''';'''
            record = (user_id , )
           
            cursor = connection.cursor(dictionary=True)

            cursor.execute( query , record )

            result_list = cursor.fetchall()

            print(result_list)

            cursor.close()
            connection.close()


        except Error as e :
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail', 'errer' : str(e)}, 500

        # 3. 클라이언트에 json 만들어서 응답한다. => 문자와 숫자로만 구성하여 보내야 한다.
        i = 0
        for row in result_list :
            result_list[i]['created_at'] = row['created_at'].isoformat()
            result_list[i]['updated_at'] = row['updated_at'].isoformat()
            i = i + 1

            print()
            print(result_list)
       
        return {'items' : result_list,
                'count' : len(result_list),
                'result' : 'success'}
   

class RecipeResource(Resource) :
    # 특정 레시피 한개를 가져오는 API
    @jwt_required()
    def get(self, recipe_id) :
       
        # 1. 클라이언트로부터 데이터를 받는다.
        print(recipe_id)

        # 1-2. JWT 토큰에서 유저아이디 가져온다.
        user_id = get_jwt_identity()

        # 2. DB로 부터 데이터를 가져온다.
        # 위의 recipe_id 에 해당하는 데이터를 가져온다.
        try :
            connection = get_connection()

            # 디버깅 (수정) 과정은 print 문으로 중간 중간 확인 하는것
            # 프린트 문이 출력되는 부분까지 문제가 없는거고, print가 실행되지 않는 그 부분이 문제가있는문구인것
            print('커넥션 실행')

            query = '''select *
                    from recipe
                    where id = %s;'''
            # 튜플을 단독값으로 넣을땐 , 꼭 추가해줘야 한다! 이런거 기억할것!!!
            record = (recipe_id,)
            print(record)
            # 실행 확인되면 dictionary=True 추가
            cursor = connection.cursor(dictionary=True)

            print('커서 가져오기 성공')

            cursor.execute(query, record)

            print('쿼리문 실행')

            result_list = cursor.fetchall()
            print(result_list)

            cursor.close()
            connection.close()


        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail',
                    'error' : str(e)}, 500
       
        # 3. 응답할 데이터를 JSON으로 만든다.
        i = 0
        for row in result_list :
            result_list[i]['created_at'] = row['created_at'].isoformat()
            result_list[i]['updated_at'] = row['updated_at'].isoformat()
            i = i + 1

        if len(result_list) == 1 :

            if result_list[0]['user_id'] == user_id :

                return {"item" : result_list[0],
                    "result" : "success"}
            else :
                return {"result" : "fail"}, 401
        else :
            return {"result": "fail",
                    "error" : "해당 아이디는 존재하지 않습니다."}, 400

    # 레시피 수정하는 API
    @jwt_required()
    def put(self, recipe_id) :

        # 1. 클라이언트로 부터 데이터를 받아온다.
        print(recipe_id)

        data = request.get_json()

        # 토큰 ID 받기
        user_id = get_jwt_identity()

        # 2. DB 에 수정한다.
        try :
            connection = get_connection()

            # 컬럼과 매칭되는 데이터는 %s로 하면된다.
            query = '''update recipe
                        set name = %s,
                            description = %s,
                            num_of_servings = %s,
                            cook_time = %s,
                            directions = %s
                        where id = %s and user_id = %s;'''
            record = (data['name'],
                      data['description'],
                      data['num_of_servings'],
                      data['cook_time'],
                      data['directions'],
                      recipe_id,
                      user_id)

            cursor = connection.cursor()
            cursor.execute(query, record)

            connection.commit()

            cursor.close()
            connection.close()

        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail',
                    'error' : str(e)}, 500

        # 3. 클라이언트에 JSON 리턴한다.

        return {'result' : 'success'}
   
    # 레시피 삭제하는 API
    @jwt_required()
    def delete(self, recipe_id) :
       
        # 토큰 사용위해 유저 ID 받아오기
        user_id = get_jwt_identity()

        # DB 에 수정한다.
        try :
            connection = get_connection()

            # 컬럼과 매칭되는 데이터는 %s로 하면된다.
            query = '''delete from recipe
                        where id = %s and user_id = %s;'''
            record = (recipe_id ,
                      user_id )

            cursor = connection.cursor()

            cursor.execute(query, record)

            connection.commit()

            cursor.close()
            connection.close()

        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail',
                    'error' : str(e)}, 500

        # 클라이언트에 JSON 리턴한다.

        return {'result' : 'success'}
   
class RecipePublishResource(Resource) :
    # 레시피를 발행하는 API
    @jwt_required()
    def put(self, recipe_id) :

        user_id = get_jwt_identity()
        # 메뉴얼대로 실행 순서
        try :
            connection = get_connection()
            query = '''update recipe
                        set is_publish = 1
                        where id = %s and user_id = %s;'''
            record = (recipe_id ,
                      user_id)
            cursor = connection.cursor()
            cursor.execute(query, record)
            connection.commit()
            cursor.close()
            connection.close()

        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail',
                    'error' : str(e)}, 500
           
        return {"result" : "success"}

    # 레시피를 임시 저장하는 API
    @jwt_required()
    def delete(self, recipe_id) :

        user_id = get_jwt_identity()

        try :
            connection = get_connection()
            query = '''update recipe
                        set is_publish = 0
                        where id = %s and user_id = %s;'''
            record = (recipe_id ,
                      user_id)
            cursor = connection.cursor()
            cursor.execute(query, record)
            connection.commit()
            cursor.close()
            connection.close()

        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail',
                    'error' : str(e)}, 500
           
        return {"result" : "success"}
 

 

- resources > user.py 파일

 
import datetime
from email_validator import EmailNotValidError, validate_email

from flask import request
from flask_jwt_extended import create_access_token, jwt_required, get_jwt
from mysql.connector import Error
from flask_restful import Resource

from mysql_connection import get_connection
from utils import check_password, hash_password

# 회원가입 API
class UserRegisterResource(Resource) :
    def post(self) :
        # 1. 클라이언트가 보낸 데이터를 받아준다.
        # 포스트맨으로 작성한 body 데이터를 json으로 받아온다.
        data = request.get_json()
        print(data)
        # 2. 데이터가 모두 있는지 확인 (두가지 방법)
        # 1) if 문으로 나누어서 작성 (이게 좀더 직관적으로 구분되므로 해당 방법 추천)
        #if 'email' not in data or 'username' not in data or 'password' not in data :
        #    return {"result" : "fail"}, 400
       
        #if data['email'].strip() == '' or data['username'].strip() == '' or data['password'].strip() == '' :
        #    return {"result" : "fail"}, 400
        # 2) if 문 길게 한줄로 작성
        if data.get('email') is None or data.get('email').strip() == '' or \
            data.get('username') is None or data.get('username').strip() == '' or \
            data.get('password') is None or data.get('password').strip() == '':
            return {'result' : 'fail'}, 400

        # 3. 이메일주소 형식이 올바른지 확인
        # 지정된 형식인 EmailNotValidError 사용 (정의된 라이브러리이므로 해당 형식 그대로 사용해야함)
        try :
            validate_email(data['email'])
        except EmailNotValidError as e :
            return {'result' : 'fail', 'error' : str(e)}, 400

        # 4. 비밀번호 길이가 유효한지 체크한다.
        #    예) 비번은 4자리 이상 12 자리 이하
        if len(data["password"]) < 4 or len(data["password"]) > 12 :
            return {'result' : 'fail'}, 400

        # 5. 비밀번호를 암호화 한다.
        password = hash_password( data['password'])
        print(password)

        # 6. DB에 저장한다.
        try :
            connection = get_connection()
            # 이번 쿼리문 작성 주의할점! record에 password는 암호화된 패스워드를 가져와야 하기 때문에 상단에서 암호화한 변수로 지정해줘야함!
            query = '''insert into user
                (username, email, password)
                values
                (%s, %s, %s);'''
            record = (data['username'], data['email'], password)
            cursor = connection.cursor()
            cursor.execute(query, record)
            connection.commit()

            ### 중요하다!!!
            ### DB에 회원가입하여, user 테이블에 insert 된 후,
            ### 이 user 테이블의 id 값을 가져와야 한다.
            ### 생성한 변수를 마지막 리턴에 작성하여 클라이언트에게 보내준다.
            user_id = cursor.lastrowid

            cursor.close()
            connection.close()

        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result' : 'fail'}, 500
       
        # 6-2. user_id를 바로 클라이언트에게 보내면 안되고,
        ##     JWT 로 암호화 해서, 인증토큰을 보내야 한다.
        ### 토큰 만료시킬때 사용
        # access_token = create_access_token(user_id,
        #                                    expires_delta= datetime.timedelta(minutes=3))
        access_token = create_access_token(user_id, )
           
        # 7. 응답할 데이터를 JSON으로 만들어서 리턴!

        return {"result" : "success", 'access_token' : access_token}
   
        # 이렇게 생성한 user_id는 결국 생성한 레시피를 DB에 저장할때 구분하기 위해서 만든것이므로 생성한 user_id 컬럼을 레시피 파일에 메뉴 생성 API 함수에다가 추가해줘야한다.
        # 이제 수정이든, 삭제든 user_id가 필요해졌는데 이게 보안되지 않으면 제3의 인물이 user_id를 동일하게 해서 생성하거나 삭제할수도 있다. 이게 해킹
        # 그러므로 인증토큰(jwt)이 필요하다! jwt 라이브러리 설치! pip install flask-jwt-extended
        # 하고 결과에 access_token으로 생성한 변수를 추가해 준다.

# 로그인 API
class UserLoginResource(Resource) :
    def post(self) :

        # 1. 클라이언트로부터 데이터를 받는다.
        data = request.get_json()

        if 'email' not in data or 'password' not in data:
            return {'result' : 'fail'}, 400
        if data['email'].strip() == '' or data['password'].strip() == '':
            return {'result' : 'fail'}, 400

        # 2. DB로부터 이메일에 해당하는 유저 정보를 가져온다.
        try :
            connection = get_connection()
            query = '''select *
                from user
                where email = %s;'''
            record = (data['email'] , )
            cursor = connection.cursor(dictionary=True)
            cursor.execute(query, record)

            result_list = cursor.fetchall()

            print(result_list)

            cursor.close()
            connection.close()

        except Error as e:
            if cursor is not None:
                cursor.close()
            if connection is not None:
                connection.close()
            return {'result':'fail', 'error':str(e)},500

        # 3. 회원인지 확인한다.
        if result_list == [] :
            return {'result' : 'fail'} , 401

        # 4. 비밀번호를 체크한다.
        # 유저가 입력한 비번 data['password']
        # DB에 암호화된 비번 result_list[0]['password']
        isCorrect = check_password(data['password'] , result_list[0]['password'])
        if isCorrect == False :
            return {'result' : 'fail'} , 401

        # 5. 유저아이디를 가져온다.
        user_id = result_list[0]['id']

        # 6. JWT 토큰을 만든다.
        # access_token = create_access_token(user_id,
        #                                    expires_delta= datetime.timedelta(minutes=3))
        access_token = create_access_token(user_id,)

        # 7. 클라이언트에 응답한다.

        return {'result' : 'success', 'access_token':access_token}

# 로그아웃 API => 다른 user API들에비해 복잡도가 있다!
# 로그아웃된 토큰을 저장할, set을 만든다.
jwt_blacklist = set()

class UserLogoutResource(Resource) :

    @jwt_required()
    def delete(self) :

        # 메뉴얼대로 작성하는것!
        jti = get_jwt()['jti']
        jwt_blacklist.add(jti)
        return
 

 

# 이제 개발된 코드를 로컬 환경에서 테스트!

- cmd를 실행시켜 flask run

- Postman을 이용해서 테스트

- 중간 중간 쿼리문 테스틑 MySQL에서 실제 DB에 잘적용되는지도 테스트

 

# 테스트에 문제가 없었다면 이제 서버에 배포 준비!

- serverless.yml과 requirements.txt 수정!

- serverless.yml 파일 (이전에 서버리스 버전이 최신버전이라 이렇게 실행 이후부터는 수정)

 
# "org" ensures this Service is used with the correct Serverless Framework Access Key.
org: kangseokgyu
# "app" enables Serverless Framework Dashboard features and sharing them with other Services.
app: recipe-server2
# "service" is the name of this project. This will also be added to your AWS resource names.
service: aws-recipe-server

custom:
  wsgi:
    app: app.app

provider:
  name: aws
  runtime: python3.10
  region : ap-northeast-2
functions:
  api:
    handler: wsgi_handler.handler
    events:
      - http:
          path: /
          method: ANY
      - http:
          path: /{proxy+}
          method: ANY

plugins:
  - serverless-wsgi
  - serverless-python-requirements

package:
  individually: true
  exclude:
    - node_modules/**  # node_modules 폴더와 그 하위 모든 파일 및 디렉토리 제외
    - .git/**  # .git 폴더와 그 하위 모든 파일 및 디렉토리 제외
    - tests/**  # tests 폴더와 그 하위 모든 파일 및 디렉토리 제외
  include:
    - src/**  # src 폴더와 그 하위 모든 파일 및 디렉토리 포함
    - handler.py  # 루트 디렉토리의 handler.py 파일 포함
    - config/**  # config 폴더와 그 하위 모든 파일 및 디렉토리 포함
 

 

- requirements.txt 파일

 
Flask==1.1.4
Werkzeug==1.0.1
markupsafe==2.0.1

flask-restful
mysql-connector-python
psycopg-binary
passlib
flask-jwt-extended
email-validator
 

 

# 저장후에 cmd창 실행시켜서 sls deploy 서버 배포 확인!

 

다음 게시글로 계속~!

 

반응형