o2o-site-AEO/admin/backend/app.py
Mina Choi cabcdeaacc 구조: 내부 API 진입점을 admin/backend 로 옮긴다
admin/ 이 프론트만 있는 폴더였다. :9801 을 띄우는 코드는 solution/backend 안에
admin_main.py 로 얹혀 있었는데, 그러면 폴더만 봐서는 admin 에 백엔드가 있다는 걸 모른다.

  solution/backend/admin_main.py         → admin/backend/main.py
  solution/backend/router/admin_router.py → admin/backend/app.py

도메인 코드는 여전히 복제하지 않는다 — `PYTHONPATH=/app/solution/backend` 한 줄이
두 폴더를 잇는다. admin/backend 에 있는 건 진입점 두 파일뿐이다.

## 이미지 빌드 컨텍스트를 레포 루트로 올렸다

진입점이 admin/ 에, 도메인 코드가 solution/ 에 있어서 한 이미지에 둘 다 들어와야 한다.
나누면 requirements 를 두 번 설치하게 된다. 컨텍스트가 넓어진 만큼 루트 .dockerignore 로
프론트·문서·테스트·시크릿을 잘라냈다(옛 solution/backend/.dockerignore 대체).

이미지 배치:
  /app/solution/backend   ← 도메인 코드. api·worker 의 working_dir
  /app/admin/backend      ← 내부 API 진입점. api-admin 의 working_dir

검증: api·api-admin 둘 다 healthy, 다섯 엔드포인트(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:14:19 +09:00

103 lines
4.0 KiB
Python

"""내부 운영 API. 사장님 API(:9800)와 **프로세스와 포트가 갈린다.**
★ 이 파일은 `admin/` 안에 있지만 도메인 코드는 `solution/backend` 것을 그대로 쓴다.
PYTHONPATH 에 `solution/backend` 가 들어 있어서 `common.*` `router.*` `services.*` 가
그대로 import 된다(compose 의 api-admin 서비스 · Dockerfile 참조).
왜 복제하지 않는지는 아래 첫 번째 ★ 를 볼 것.
★ 왜 라우터를 새로 쓰지 않고 같은 것을 다시 마운트하나
admin 화면이 부르는 API 는 전부 place·fact 라우터에 이미 있다(세어봤다: admin 전용
라우터는 0개다). 여기서 엔드포인트를 새로 쓰면 같은 DB 의 같은 테이블을 두 벌
구현하게 된다. 그래서 **같은 router 객체를 그대로 include 하고, 앱 단위로 권한만 덧건다.**
★ 왜 경로 접두어(/v1/admin/...)가 아니라 포트를 가르나
접두어는 같은 프로세스 안에 있다 — 사장님이 닿는 서버에 내부 엔드포인트가 **존재한다.**
포트를 가르면 내부 API 는 사장님이 닿는 네트워크에 아예 없다. 가드보다 강하다.
(compose 에서 이 포트는 127.0.0.1 에만 연다. 0.0.0.0 으로 열면 가른 의미가 없다.)
★ 여기 붙는 모든 엔드포인트는 role >= DEVELOPER 다.
OWNER 는 자기 회사 최상위일 뿐 남의 회사를 볼 권한이 아니라서 막힌다
(RequireDeveloper 주석 참조). 예외를 한 곳이라도 두면 그 예외가 기본값이 된다.
"""
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 만 게이트 밖이다 — 로그인 자체를 막으면 아무도 들어올 수 없다.
# (로그인은 되지만 role 이 낮으면 아래 라우터가 전부 403 이다.)
app.include_router(router.v1.auth.account.router)
# 그 밖의 전부: 앱 단위 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)