kr4phy's picture
Sync from GitHub
cff6ac7
Raw
History Blame Contribute Delete
6.01 kB
"""
FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์ .
์‹คํ–‰ ๋ฐฉ๋ฒ•:
uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --reload
"""
from __future__ import annotations
import logging
import os as _os
import time
import uuid
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Callable
import structlog
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from src.api.routes import demo, diagnostics, predictions, regions, schools
from src.config import get_settings
# โ”€โ”€ ๋กœ๊น… ์„ค์ • โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _configure_logging(log_level: str) -> None:
"""structlog ์„ JSON ํฌ๋งท์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค."""
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(
logging.getLevelName(log_level)
),
logger_factory=structlog.PrintLoggerFactory(),
)
# ํ‘œ์ค€ logging ๋„ ๋™์ผ ๋ ˆ๋ฒจ๋กœ ์„ค์ •
logging.basicConfig(level=log_level)
# โ”€โ”€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒ๋ช…์ฃผ๊ธฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
cfg = get_settings()
_configure_logging(cfg.log_level)
logger = structlog.get_logger()
logger.info(
"์„œ๋ฒ„ ์‹œ์ž‘",
env=cfg.app_env,
port=cfg.api_port,
granite_model=cfg.granite_model_id,
)
yield
logger.info("์„œ๋ฒ„ ์ข…๋ฃŒ")
# โ”€โ”€ FastAPI ์•ฑ ์ƒ์„ฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
cfg = get_settings()
app = FastAPI(
title="AI ๊ธฐ๋ฐ˜ ํ•™๊ต ์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ง„๋‹จ ์‹œ์Šคํ…œ",
description=(
"๊ณต๊ณต๋ฐ์ดํ„ฐ์™€ IBM Granite Time Series ๋ชจ๋ธ์„ ๊ฒฐํ•ฉํ•˜์—ฌ "
"ํ•™๊ต์˜ ์ง€์† ๊ฐ€๋Šฅ์„ฑ์„ ์ง„๋‹จํ•˜๊ณ  ์˜ˆ์ธกํ•˜๋Š” API ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค.\n\n"
"์ฃผ์š” ๊ธฐ๋Šฅ:\n"
"- ํ•™๊ต๊ธฐ๋ณธ์ •๋ณด ์กฐํšŒ (NEIS Open API ์—ฐ๋™)\n"
"- ํ•™์ƒ ์ˆ˜ ์‹œ๊ณ„์—ด ์˜ˆ์ธก (Granite TTM / AutoARIMA fallback)\n"
"- ์ข…ํ•ฉ ์ง„๋‹จ (SHAP XAI + ์ง€์† ๊ฐ€๋Šฅ์„ฑ ์ ์ˆ˜ + ์ •์ฑ… ์‹œ๋ฎฌ๋ ˆ์ด์…˜)\n"
"- ์ง€์—ญ ์œ„ํ—˜์ง€๋„ ๋ฐ์ดํ„ฐ ์ œ๊ณต\n"
),
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
)
# โ”€โ”€ CORS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if cfg.app_env == "development" else [],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# โ”€โ”€ ์š”์ฒญ ID / ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๋ฏธ๋“ค์›จ์–ด โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.middleware("http")
async def request_id_middleware(request: Request, call_next: Callable) -> Response:
request_id = str(uuid.uuid4())
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(request_id=request_id)
start = time.perf_counter()
response: Response = await call_next(request)
elapsed_ms = round((time.perf_counter() - start) * 1000, 1)
response.headers["X-Request-ID"] = request_id
response.headers["X-Response-Time-Ms"] = str(elapsed_ms)
return response
# โ”€โ”€ ์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
logger = structlog.get_logger()
logger.error("์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ", exc_info=exc, path=str(request.url))
return JSONResponse(
status_code=500,
content={
"code": "INTERNAL_SERVER_ERROR",
"message": "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.",
"detail": str(exc),
},
)
# โ”€โ”€ ๋ผ์šฐํ„ฐ ๋“ฑ๋ก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.include_router(schools.router)
app.include_router(predictions.router)
app.include_router(diagnostics.router)
app.include_router(regions.router)
app.include_router(demo.router)
# โ”€โ”€ ํ—ฌ์Šค ์ฒดํฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.get("/health", tags=["health"], summary="์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ")
async def health_check() -> dict:
"""์„œ๋ฒ„ ์ƒํƒœ ๋ฐ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
return {
"status": "ok",
"env": cfg.app_env,
"granite_model": cfg.granite_model_id,
}
# โ”€โ”€ ํ”„๋ก ํŠธ์—”๋“œ ์ •์  ํŒŒ์ผ ์„œ๋น™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_frontend_dir = _os.path.join(_os.path.dirname(__file__), "..", "..", "frontend")
if _os.path.isdir(_frontend_dir):
app.mount("/", StaticFiles(directory=_frontend_dir, html=True), name="frontend")