Files
dbx-orderlist/app/main.py
T
king 1eabd7183c fix(routing): FastAPI(root_path=) 제거 — Mount/Route 비대칭 해결
Starlette Mount 가 FastAPI(root_path=...) 의 값을 mount 경로에 흡수해버려서
@app.get 라우트(/health/db)는 그대로지만 mount(/static)는 /orderlist/static
으로 등록되는 비대칭이 발생했다. NPM 이 /orderlist prefix 를 잘라서 백엔드로
보내면 /health/db 는 200 이지만 /static/css/style.css 는 404 가 났다.

해결: FastAPI 생성자에서 root_path 인자 제거. 대신 Dockerfile 의 uvicorn 명령에
--root-path "${APP_ROOT_PATH:-}" 를 추가해 ASGI scope 레벨에서만 root_path 가
세팅되도록 한다. 이러면 모든 라우트/마운트가 prefix 없이 등록되고 URL 생성
시에만 prefix 가 붙는다.

APP_ROOT_PATH 환경변수와 main.py 의 HTML 경로 치환 로직은 그대로 유지.
2026-05-24 14:05:32 +09:00

86 lines
4.0 KiB
Python

from fastapi import FastAPI, Depends, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
from starlette.middleware.sessions import SessionMiddleware
import logging
import os
from app.database import engine, Base, check_db_connection, log_db_startup_status
from app.database_maintenance import ensure_search_optimizations
from app.routers import orders, settings
from app.auth import router as auth_router
logging.basicConfig(level=logging.INFO)
APP_ROOT_PATH = os.getenv("APP_ROOT_PATH", "").rstrip("/")
# Verify DB connectivity at startup (log only — do not crash so the
# container can stay up and report status via /health/db).
log_db_startup_status()
# Create all DB tables
Base.metadata.create_all(bind=engine)
ensure_search_optimizations(engine)
# root_path 는 FastAPI 생성자에 넘기지 않는다.
# Starlette 의 Mount 가 root_path 를 흡수해 mount 경로에 prefix 가 붙어버려서
# @app.get 라우트와 mount 가 비대칭으로 등록되는 문제 때문이다.
# (예: /health/db 는 그대로지만 /static/* 는 /orderlist/static/* 로 등록됨)
#
# 대신 uvicorn --root-path 로 ASGI scope.root_path 만 세팅한다.
# 그러면 모든 라우트/마운트가 prefix 없이 등록되고, URL 생성 시에만 prefix 가 붙는다.
# Dockerfile 의 CMD 가 ${APP_ROOT_PATH} 를 --root-path 로 넘긴다.
app = FastAPI(title="Order Management System")
@app.get("/health/db")
async def health_db():
ok, msg = check_db_connection()
status_code = 200 if ok else 503
return JSONResponse({"ok": ok, "detail": msg}, status_code=status_code)
# ---- Session middleware ----
# dbx-main 과 SSO 하려면 SESSION_SECRET_KEY 와 SESSION_COOKIE_NAME 을
# 두 앱이 동일하게 맞춰야 한다. 그래야 dbx-main 이 발급한 세션 쿠키를
# OMS 가 그대로 읽을 수 있다.
_session_secret = os.getenv("SESSION_SECRET_KEY", "").strip()
if not _session_secret:
raise RuntimeError(
"SESSION_SECRET_KEY 환경변수가 설정되지 않았습니다. "
"openssl rand -hex 32 로 새 값을 만들어 .env 에 넣고 컨테이너를 재기동하세요. "
"dbx-main 과 SSO 하려면 양쪽 .env 에 같은 값이어야 합니다."
)
app.add_middleware(
SessionMiddleware,
secret_key=_session_secret,
session_cookie=os.getenv("SESSION_COOKIE_NAME", "session"),
same_site=os.getenv("SESSION_SAME_SITE", "lax"),
https_only=os.getenv("SESSION_HTTPS_ONLY", "0") == "1",
max_age=int(os.getenv("SESSION_MAX_AGE", "28800")), # 8h — dbx-main 과 통일
)
# Include routers
app.include_router(auth_router)
app.include_router(orders.router, prefix="/api/orders", tags=["orders"])
app.include_router(settings.router, prefix="/api/settings", tags=["settings"])
# Mount static files for frontend
os.makedirs("static", exist_ok=True)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", response_class=HTMLResponse)
async def read_root():
# Return the main index.html file
if os.path.exists("static/index.html"):
with open("static/index.html", "r", encoding="utf-8") as f:
content = f.read()
content = content.replace("__APP_BASE_PATH__", APP_ROOT_PATH)
content = content.replace('src="/static/', f'src="{APP_ROOT_PATH}/static/' if APP_ROOT_PATH else 'src="/static/')
content = content.replace('href="/static/', f'href="{APP_ROOT_PATH}/static/' if APP_ROOT_PATH else 'href="/static/')
content = content.replace("window.location.href='/login", f"window.location.href='{APP_ROOT_PATH}/login")
content = content.replace('window.location.href="/login', f'window.location.href="{APP_ROOT_PATH}/login')
content = content.replace("window.location.href='/logout", f"window.location.href='{APP_ROOT_PATH}/logout")
content = content.replace('window.location.href="/logout', f'window.location.href="{APP_ROOT_PATH}/logout')
return HTMLResponse(content=content)
return HTMLResponse(content="<h1>Welcome. static/index.html is missing.</h1>")