| """ |
| 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.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("์๋ฒ ์ข
๋ฃ") |
|
|
|
|
| |
|
|
| 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, |
| ) |
|
|
| |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"] if cfg.app_env == "development" else [], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
|
|
| |
|
|
| @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") |
|
|