File size: 6,014 Bytes
cff6ac7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""
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")