import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.sessions import SessionMiddleware from app.config import settings from app.database import SessionLocal from app.routers import auth, habits, logs, pages, push from app.routers.pages import templates from app.security import get_current_user_optional from app.services import scheduler_service logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): scheduler_service.start_scheduler() yield scheduler_service.shutdown_scheduler() app = FastAPI(title="습관 트래커", lifespan=lifespan) # 구글 OAuth 핸드셰이크 중 state/nonce를 임시로 저장하는 데만 쓰는 세션이다. # 로그인 유지용 쿠키(habit_session)와는 별개. app.add_middleware(SessionMiddleware, secret_key=settings.secret_key, session_cookie="oauth_session", max_age=600) app.mount("/static", StaticFiles(directory="app/static"), name="static") app.include_router(auth.router) app.include_router(habits.router) app.include_router(logs.router) app.include_router(push.router) app.include_router(pages.router) def _is_api_path(path: str) -> bool: return path.startswith("/api/") or path.startswith("/static/") def _current_user_context(request: Request) -> dict: db = SessionLocal() try: current_user = get_current_user_optional(request, db) finally: db.close() return {"logged_in": current_user is not None, "current_user": current_user} @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): if exc.status_code == 404 and not _is_api_path(request.url.path): return templates.TemplateResponse( request, "404.html", _current_user_context(request), status_code=404 ) return JSONResponse({"detail": exc.detail}, status_code=exc.status_code) @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception): logger.exception("처리되지 않은 오류") if _is_api_path(request.url.path): return JSONResponse({"detail": "서버 오류가 발생했습니다"}, status_code=500) return templates.TemplateResponse( request, "500.html", _current_user_context(request), status_code=500 ) @app.get("/service-worker.js") def service_worker(): # 서비스워커 scope가 앱 전체를 커버하려면 /static/ 하위가 아닌 루트 경로로 서빙해야 한다. return FileResponse("app/static/service-worker.js", media_type="application/javascript")