o2o-site-AEO/admin/backend/app.py
Mina Choi 374a3a4a6e 구조: admin 을 backend/frontend 로 가른다
admin/ 이 프론트 파일만 널려 있는 폴더였다. :9801 을 띄우는 코드도 solution/backend 안에
얹혀 있어서, 폴더만 봐서는 admin 에 백엔드가 있다는 걸 몰랐다.

  admin/backend/   main.py · app.py   (:9801 진입점. 도메인 코드는 PYTHONPATH 로 solution/backend)
  admin/frontend/  운영 화면

패키지 이름도 폴더에 맞췄다: @o2o/front → @o2o/frontend.
이미지 빌드 컨텍스트를 레포 루트로 올렸다 — 진입점(admin/)과 도메인 코드(solution/)가
한 이미지에 들어와야 한다. 루트 .dockerignore 로 프론트·문서·시크릿을 잘라냈다.

★ 실측으로 잡은 것: 패키지명을 바꾸면서 compose 의 `-w @o2o/front` 를 안 고쳐
  web 컨테이너가 `No workspaces found` 로 재기동 루프에 빠져 있었다.

주석은 짧게 줄였다.

검증: lint·build 6개 전부 0. 컨테이너 5개 엔드포인트(9800·9801·3000·3002·80) 전부 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
2026-08-31 16:22:29 +09:00

87 lines
2.8 KiB
Python

"""내부 운영 API. 사장님 API(:9800)와 프로세스·포트가 갈린다.
도메인 코드는 solution/backend 것을 PYTHONPATH 로 쓴다 — admin 전용 라우터가 0개라
(전부 place·fact) 새로 쓰면 같은 테이블을 두 벌 구현하는 것뿐이다.
경로 접두어가 아니라 포트를 가른 이유: 접두어는 같은 프로세스라 사장님이 닿는 서버에
내부 엔드포인트가 존재한다. 포트를 가르면 아예 없다.
"""
import time
from fastapi import Depends, 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 router.v1.validator.dependencies import RequireDeveloper
import router.v1.auth.account
import router.v1.fact.fact
import router.v1.job.job
import router.v1.local.local
import router.v1.place.place
import router.v1.site.site
from contextlib import asynccontextmanager
API_SERVER_START_TIME = GTime.UTCStr()
@asynccontextmanager
async def lifespan(app: FastAPI):
# 크론은 :9800 담당. 여기서도 돌리면 같은 시각에 중복 실행된다.
yield
await DB_SESSION_MNG.dispose_all()
app = FastAPI(title="Web4Ai Admin API", lifespan=lifespan)
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)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
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)
LOG.d(f"[admin] {response.status_code} {request.method} {request.url.path} - {time.time() - start_time:.4f}s")
return response
@app.get(path="/healthz")
async def healthz():
return API_SERVER_START_TIME
# auth 만 게이트 밖 — 로그인까지 막으면 아무도 못 들어온다.
app.include_router(router.v1.auth.account.router)
# 나머지 전부 role >= DEVELOPER.
_gate = [Depends(RequireDeveloper)]
app.include_router(router.v1.place.place.router, dependencies=_gate)
app.include_router(router.v1.fact.fact.router, dependencies=_gate)
app.include_router(router.v1.job.job.router, dependencies=_gate)
app.include_router(router.v1.site.site.router, dependencies=_gate)
app.include_router(router.v1.local.local.router, dependencies=_gate)