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
87 lines
2.8 KiB
Python
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)
|