"""Vignette FastAPI 앱 엔트리포인트. Dockerfile CMD: uvicorn app.main:app --host 0.0.0.0 --port 8000 소유: 상태머신·가드레일·엔진 어댑터 호출부·SSE 스트리머·RBAC (마스터플랜 §5). 엔진 게이트웨이(engine_gateway/)는 람다 직접 소유 — 여기선 engine_client 로 호출만. """ from __future__ import annotations from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from . import __version__ from .auth_sessions import ensure_runtime_tables from .config import settings from .db import close_pool, healthcheck, init_pool from .engine_client import engine_client from .persona_repository import materialize_seed_personas from .session_persistence import ensure_review_tables from .routes import auth as auth_routes from .routes import admin as admin_routes from .routes import eval as eval_routes from .routes import kb as kb_routes from .routes import personas as persona_routes from .routes import sessions as session_routes from .routes import share as share_routes from .routes import teacher as teacher_routes from .routes import users as user_routes from .routes import voice as voice_routes from .services.notifications import ensure_notification_tables from .services.voice import voice_service @asynccontextmanager async def lifespan(app: FastAPI): """startup: DB 풀 + 엔진 클라이언트 / shutdown: 정리. DB 미가용(Docker off / NAS 연결불가)이면 store 인메모리 폴백으로 degraded 기동한다. """ try: await init_pool() await ensure_runtime_tables() await ensure_review_tables() await ensure_notification_tables() if settings.auto_seed_personas: await materialize_seed_personas() await admin_routes.apply_engine_config_from_store() except Exception as exc: # DB 없어도 store 폴백으로 1턴 동작 (dev/로컬) if settings.environment != "dev": raise import logging logging.getLogger("uvicorn.error").warning( "DB 풀 초기화 실패 — store 인메모리 폴백으로 degraded 기동: %s", exc ) await engine_client.startup() await voice_service.startup() session_routes.schedule_missing_session_evaluation_recovery() try: yield finally: session_routes.cancel_missing_session_evaluation_recovery() await voice_service.shutdown() await engine_client.shutdown() try: await close_pool() except Exception: pass app = FastAPI( title="Vignette API", version=__version__, description="상담 시뮬레이션 훈련 플랫폼 백엔드 (FastAPI · 상태머신 · 가드레일 · RBAC)", lifespan=lifespan, ) # CORS — 정적 프론트(chanpaca.net) + SSE 분리경로(stream.chanpaca.net) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, # __Host- HttpOnly 쿠키 전송 allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["*"], ) # TODO: Presidio PII 마스킹 미들웨어 (외부 LLM 경로 진입 전 하드 게이트, R7/F-03) _upload_root = Path(settings.user_upload_dir) if not _upload_root.is_absolute(): _upload_root = Path.cwd() / _upload_root _upload_root.mkdir(parents=True, exist_ok=True) app.mount("/uploads", StaticFiles(directory=str(_upload_root)), name="uploads") app.include_router(auth_routes.router) app.include_router(admin_routes.router) app.include_router(persona_routes.router) app.include_router(session_routes.router) app.include_router(share_routes.router) app.include_router(teacher_routes.router) app.include_router(user_routes.router) # Features 트랙 스텁 라우터(evaluator/voice/rag 가 채움). 등록만 — import 가능 보장. app.include_router(eval_routes.router) app.include_router(voice_routes.router) app.include_router(kb_routes.router) @app.get("/health", tags=["meta"]) async def health() -> dict[str, object]: """liveness + DB + 엔진 게이트웨이 readiness.""" db_ok = await healthcheck() engine = await engine_client.health_detail() engine_ok = bool(engine.get("ok")) return { "status": "ok" if db_ok and engine_ok else "degraded", "version": __version__, "environment": settings.environment, "db": db_ok, "engine": engine_ok, "engine_detail": engine.get("detail"), "engine_mode": settings.engine_mode, }