o2o-site-AEO/solution/backend/router/router.py
Mina Choi 41e49c693b Merge branch 'main' into 홈페이지-레이아웃-구상
# Conflicts:
#	docs/DEVLOG.md
#	solution/backend/crud/site_crud.py
#	solution/backend/router/router.py
#	solution/backend/router/v1/site/protocol.py
2026-09-03 09:44:51 +09:00

96 lines
3.5 KiB
Python

import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from common.database.db_session_manager import DB_SESSION_MNG
from common.logger import LOG
from common.utils.gtime import GTime
from config.server_configs import web_server_config
from scheduler import shutdown_scheduler, start_scheduler
import router.v1.auth.account
import router.v1.place.place
import router.v1.fact.fact
import router.v1.faq.faq
import router.v1.media.media
import router.v1.job.job
import router.v1.site.site
import router.v1.site.showcase
import router.v1.local.local
API_SERVER_START_TIME = GTime.UTCStr()
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup: 배치 스케줄러 기동(SCHEDULER_ENABLED=1 인 프로세스에서만)
start_scheduler()
yield
# shutdown: 스케줄러 정지 + DB 엔진 커넥션 풀 정리
shutdown_scheduler()
await DB_SESSION_MNG.dispose_all()
app = FastAPI(title="Web4Ai API", lifespan=lifespan)
# CORS — 관리자 프론트(client_url) + 랜딩(landing_url, 미설정이면 제외).
#
# ★ client_url 은 쉼표로 여러 오리진을 받는다. 로컬 개발에서 vite 는 3000 이 막혀 있으면
# 3001, 3002… 로 옮겨 뜨는데(--port 는 희망값이지 고정이 아니다), 그때마다 서버 설정을
# 고치게 하면 원인이 CORS 라는 걸 알아내는 데만 반나절이 든다. 개발 포트 몇 개를 한 줄에 적어 둔다.
# 운영은 실제 도메인 하나만 적으면 된다.
def _origins(*values: str) -> list[str]:
seen: list[str] = []
for value in values:
for origin in value.split(","):
origin = origin.strip().rstrip("/")
if origin and origin not in seen:
seen.append(origin)
return seen
ALLOWED_ORIGINS = _origins(web_server_config.client_url, web_server_config.landing_url)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Accept-Encoding: gzip 요청에 대해 1000 bytes 이상 응답을 압축.
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.middleware("http")
async def log_time(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
elapsed = time.time() - start_time
# status_code 를 함께 남긴다(403/4xx 등을 로그만으로 식별 가능하게).
LOG.d(f"{response.status_code} {request.method} {request.url.path} - {elapsed:.4f}s")
return response
@app.get(path="/healthz", responses={404: {"description": "Not found"}})
async def healthz():
return API_SERVER_START_TIME
# 각 도메인 라우터를 등록한다. 새 기능 추가 시 router.v1.<domain>.<file> 를 import 후 include.
app.include_router(router.v1.auth.account.router)
app.include_router(router.v1.place.place.router)
app.include_router(router.v1.fact.fact.router)
app.include_router(router.v1.faq.faq.router)
app.include_router(router.v1.media.media.router)
app.include_router(router.v1.job.job.router)
app.include_router(router.v1.site.site.router)
app.include_router(router.v1.site.site.my_router)
# ★ 인증 없는 공개 목록. 랜딩이 부른다 — 어드민 진입점(:9801)에는 붙이지 않는다.
app.include_router(router.v1.site.showcase.router)
app.include_router(router.v1.local.local.router)
app.include_router(router.v1.local.local.weather_router)