본문 바로가기

FastAPI

FastAPI + Jinja Templates 기반 APP개발 시작

# Project_folder/main.py

from app.core.inits import initialize_app

app = initialize_app()

 

# Project_folder/app/core/inits.py

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi_csrf_jinja.middleware import FastAPICSRFJinjaMiddleware

from app.core.settings import STATIC_DIR, MEDIA_DIR, CONFIG
from app.views import index


def including_middleware(app):
    app.add_middleware(CORSMiddleware,
                       allow_origins=["*"],  # 실제 프론트 주소
                       allow_methods=["*"],
                       allow_headers=["*"],
                       allow_credentials=True,
                       max_age=-1)
    app.add_middleware(FastAPICSRFJinjaMiddleware, secret=CONFIG.SECRET_KEY,
                       cookie_name="csrf_token", header_name="X-CSRF-Token")


def including_router(app):
    app.include_router(index.router, prefix="", tags=["Index"])


def initialize_app():
    app = FastAPI()
    app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
    app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
    including_middleware(app)
    including_router(app)
    return app

 

# Project_folder/app/core/settings.py

import os
from functools import lru_cache
from pathlib import Path
from typing import Optional

from fastapi.templating import Jinja2Templates
from fastapi_csrf_jinja.jinja_processor import csrf_token_processor
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

PRESENT_DIR = os.path.dirname(os.path.abspath(__file__))
APP_DIR = Path(__file__).resolve().parent.parent
ROOT_DIR = Path(__file__).resolve().parent.parent.parent  ## root폴더
TEMPLATE_DIR = os.path.join(APP_DIR, 'templates')
STATIC_DIR = os.path.join(APP_DIR, 'static')
MEDIA_DIR = os.path.join(APP_DIR, 'media')

templates = Jinja2Templates(
    directory=TEMPLATE_DIR,
    context_processors=[csrf_token_processor("csrf_token", "X-CSRF-Token")]
)

class Settings(BaseSettings):
    """여기에서 값이 비어있는 것은 .env 파일에서 채워진다.
    그 값은 override 되지 않는다. 흐름상 그럴것 같기는 한데...???
    최종적으로 .env에 설정된 값으로 채워져 버리는 것인것 같다."""
    APP_ENV: str = "development"
    APP_NAME: str = "Svelte_FastAPI"
    APP_VERSION: str = "0.0.2"
    APP_DESCRIPTION: str = "FastAPI_Svelte을 이용한 프로젝트 개발"

    DEBUG: bool = False
    SECRET_KEY: Optional[str] = None

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
        case_sensitive=False,
    )


class DevSettings(Settings):
    DEBUG: bool = Field(..., validation_alias="DEBUG_TRUE")

    ORIGINS: str = Field(..., validation_alias="DEV_ORIGINS")

    DB_NAME: str = Field(..., validation_alias="DEV_DB_NAME")
    DB_HOST: str = Field(..., validation_alias="DEV_DB_HOST")
    DB_PORT: str = Field(..., validation_alias="DEV_DB_PORT")
    DB_USER: str = Field(..., validation_alias="DEV_DB_USER")
    DB_PASSWORD: str = Field(..., validation_alias="DEV_DB_PASSWORD")


class ProdSettings(Settings):
    APP_NAME: str = Field(..., validation_alias="PROD_APP_NAME")
    APP_VERSION: str = Field(..., validation_alias="PROD_APP_VERSION")
    APP_DESCRIPTION: str = Field(..., validation_alias="PROD_APP_DESCRIPTION")

    ORIGINS: str = Field(..., validation_alias="PROD_ORIGINS")

    DB_NAME: str = Field(..., validation_alias="PROD_DB_NAME")
    DB_HOST: str = Field(..., validation_alias="PROD_DB_HOST")
    DB_PORT: str = Field(..., validation_alias="PROD_DB_PORT")
    DB_USER: str = Field(..., validation_alias="PROD_DB_USER")
    DB_PASSWORD: str = Field(..., validation_alias="PROD_DB_PASSWORD")

    REDIS_HOST: str = Field(..., validation_alias="REDIS_HOST")
    REDIS_PORT: str = Field(..., validation_alias="REDIS_PORT")
    REDIS_DB: str = Field(..., validation_alias="REDIS_DB")
    REDIS_PASSWORD: str = Field(..., validation_alias="REDIS_PASSWORD")

    # 운영에서는 SECRET_KEY 필수
    @model_validator(mode="after")
    def ensure_secret_key(self):
        if not self.SECRET_KEY or len(self.SECRET_KEY) < 16:
            raise ValueError("In production, SECRET_KEY must be set and sufficiently long.")
        return self


@lru_cache(maxsize=1)
def get_settings() -> Settings:
    print("1. setting start... Only One")
    # model_config의 env_file 설정만으로도 충분하지만, 아래 호출이 있어도 문제는 없습니다.
    try:
        from dotenv import load_dotenv
        load_dotenv(".env", override=False, encoding="utf-8")
    except Exception as e:
        print(f"load_dotenv error: {e}")
        # python-dotenv 미설치 등인 경우에도 설정 로딩은 pydantic-settings가 처리하므로 무시 가능
        pass

    app_env = os.getenv("APP_ENV", "development").strip().lower()
    if app_env == "production":
        return ProdSettings()
    print("2. settings...APP_ENV: ", app_env)
    return DevSettings()

CONFIG = get_settings()

 

# Project_folder/templates/common/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2><stong>{{message}}</stong></h2>
</body>
</html>

 

# Project_folder/.env

# 배포시에 주석해제
#APP_ENV=production

DEBUG_TRUE=true
DEBUG_FALSE=false
# production시 DEBUG_FALSE = false

SECRET_KEY=7b6b057992ae6b79d3fecc6d3ebe40626ef4e2ed8221303202e60fdff204d2a7d334b0
ALGORITHM = HS256

DB_TYPE=mysql
DB_DRIVER=aiomysql

DEV_DB_NAME=advanced_db
DEV_DB_HOST=localhost
DEV_DB_PORT=3306
DEV_DB_USER=root
DEV_DB_PASSWORD=<비밀번호>


PROD_APP_NAME=Deployed APP
PROD_APP_VERSION=Deployed V_0
PROD_APP_DESCRIPTION=FastAPI_Jinja를 이용해 개발한 프로젝트에 대한 배포판

PROD_DB_NAME=advanced_db
PROD_DB_HOST=<배포서버 IP주소>
PROD_DB_PORT=<포트번호>
PROD_DB_USER=root
PROD_DB_PASSWORD=<비밀번호>


# redis_config.py 연결 설정: REDIS_PASSWORD None은 에러 유발, 배포 과정에 비번 지정
REDIS_HOST=<배포서버 IP주소>
REDIS_PORT=<포트번호>
REDIS_DB=0
REDIS_PASSWORD=<비밀번호>


# 이메일 보내기 sender 정보
SMTP_USERNAME=abcdefg@gmail.com
SMTP_PASSWORD=iccxyzmnkyfvszux
SMTP_FROM=abcdefg@gmail.com
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587

ACCESS_COOKIE_NAME = access_token
REFRESH_COOKIE_NAME = refresh_token
NEW_ACCESS_COOKIE_NAME = new_access_token
NEW_REFRESH_COOKIE_NAME = new_refresh_token

ACCESS_TOKEN_EXPIRE = 30
REFRESH_TOKEN_EXPIRE = 7


PROFILE_IMAGE_URL = user_images/accounts/profiles



# 프론트엔드 개발에 사용되는 포트: 아래 두개(127.0.0.1, localhost)는 별개로 인식한다. 둘다 필요하다.
# 리스트는 한 줄로..., 파싱 가능한 형태로 바꾸기
#DEV_ORIGINS="http://localhost:5173,http://localhost:5100,http://127.0.0.1:5173,http://127.0.0.1:5100"
DEV_ORIGINS=["http://localhost:5173","http://localhost:5174","http://localhost:7100","http://localhost:7101","http://127.0.0.1:5173","http://127.0.0.1:5174","http://127.0.0.1:7100","http://127.0.0.1:7101"]
PROD_ORIGINS=[]