구조: 내부 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
This commit is contained in:
parent
c6b45fdda4
commit
cabcdeaacc
31
.dockerignore
Normal file
31
.dockerignore
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# ★ 빌드 컨텍스트가 레포 루트다(solution/backend/Dockerfile 주석 참조).
|
||||||
|
# 컨텍스트가 넓어진 만큼 여기서 확실히 잘라내야 이미지가 붓지 않는다.
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
**/.pytest_cache/
|
||||||
|
.git/
|
||||||
|
.venv/
|
||||||
|
**/.venv/
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
**/dist/
|
||||||
|
**/.vite/
|
||||||
|
|
||||||
|
# 프론트·문서는 백엔드 이미지에 들어갈 이유가 없다.
|
||||||
|
solution/frontend/
|
||||||
|
solution/site/
|
||||||
|
solution/shared/
|
||||||
|
admin/src/
|
||||||
|
docs/
|
||||||
|
nginx/
|
||||||
|
postgres-init/
|
||||||
|
**/*.md
|
||||||
|
solution/backend/tests/
|
||||||
|
solution/backend/loadtest/
|
||||||
|
|
||||||
|
# 시크릿 — 이미지에 굽지 않는다. Dockerfile 이 example 을 복사해 넣고 실값은 compose env 로 준다.
|
||||||
|
**/config.local.toml
|
||||||
|
**/config.test.toml
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
solution/ 사장님 — backend · frontend(빌더) · site(발행물) · shared(계약)
|
solution/ 사장님 — backend · frontend(빌더) · site(발행물) · shared(계약)
|
||||||
admin/ 우리 — 전체 사이트 운영. 프론트만. API 는 아래 :9801 을 본다
|
admin/ 우리 — backend(진입점만, :9801) · frontend(운영 화면)
|
||||||
```
|
```
|
||||||
|
|
||||||
최상단은 **프로젝트 단위**다(`o2o-negosium` 과 같은 규약). `frontend/` `backend/` 를 최상단
|
최상단은 **프로젝트 단위**다(`o2o-negosium` 과 같은 규약). `frontend/` `backend/` 를 최상단
|
||||||
@ -75,8 +75,8 @@ admin/ 우리 — 전체 사이트 운영. 프론트만. API 는 아래 :9
|
|||||||
|
|
||||||
| | 포트 | 진입점 | 권한 |
|
| | 포트 | 진입점 | 권한 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 사장님 API | 9800 | `web_main.py` → `router/router.py` | 엔드포인트별 |
|
| 사장님 API | 9800 | `solution/backend/web_main.py` | 엔드포인트별 |
|
||||||
| 내부 API | 9801 | `admin_main.py` → `router/admin_router.py` | **앱 전체 role >= DEVELOPER** |
|
| 내부 API | 9801 | `admin/backend/main.py` → `app.py` | **앱 전체 role >= DEVELOPER** |
|
||||||
|
|
||||||
`services`·`crud`·`models` 은 그대로 공유한다. admin 전용 라우터가 0개라(세어봤다:
|
`services`·`crud`·`models` 은 그대로 공유한다. admin 전용 라우터가 0개라(세어봤다:
|
||||||
admin 화면이 부르는 건 전부 place·fact 다) 도메인을 복제하지 않고 **같은 router 객체를
|
admin 화면이 부르는 건 전부 place·fact 다) 도메인을 복제하지 않고 **같은 router 객체를
|
||||||
|
|||||||
@ -35,7 +35,9 @@ solution/ 사장님 — 사이트 만들기·관리
|
|||||||
frontend/ 빌더(위저드 + 에디터 + 발행 게이트)
|
frontend/ 빌더(위저드 + 에디터 + 발행 게이트)
|
||||||
site/ 발행 사이트. SSR 엔트리 + 프리렌더 + 정적 서버
|
site/ 발행 사이트. SSR 엔트리 + 프리렌더 + 정적 서버
|
||||||
shared/ front·site·백엔드 계약이 만나는 타입·규칙 (SitePayload, slug)
|
shared/ front·site·백엔드 계약이 만나는 타입·규칙 (SitePayload, slug)
|
||||||
admin/ 우리 — 전체 사이트 운영 (프론트). API 는 solution/backend 의 :9801 진입점
|
admin/ 우리 — 전체 사이트 운영
|
||||||
|
backend/ 내부 API 진입점(:9801). 도메인 코드는 solution/backend 를 PYTHONPATH 로 쓴다
|
||||||
|
frontend/ 내부 운영 화면
|
||||||
docs/ 아래 표
|
docs/ 아래 표
|
||||||
nginx/ 발행 사이트 정적 서빙 (site.conf 는 .example 만 커밋한다)
|
nginx/ 발행 사이트 정적 서빙 (site.conf 는 .example 만 커밋한다)
|
||||||
postgres-init/ 스키마 DDL (init.sql 한 벌)
|
postgres-init/ 스키마 DDL (init.sql 한 벌)
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
"""내부 운영 API (admin 앱 전용). 사장님 API(:9800)와 **프로세스와 포트가 갈린다.**
|
"""내부 운영 API. 사장님 API(:9800)와 **프로세스와 포트가 갈린다.**
|
||||||
|
|
||||||
|
★ 이 파일은 `admin/` 안에 있지만 도메인 코드는 `solution/backend` 것을 그대로 쓴다.
|
||||||
|
PYTHONPATH 에 `solution/backend` 가 들어 있어서 `common.*` `router.*` `services.*` 가
|
||||||
|
그대로 import 된다(compose 의 api-admin 서비스 · Dockerfile 참조).
|
||||||
|
왜 복제하지 않는지는 아래 첫 번째 ★ 를 볼 것.
|
||||||
|
|
||||||
★ 왜 라우터를 새로 쓰지 않고 같은 것을 다시 마운트하나
|
★ 왜 라우터를 새로 쓰지 않고 같은 것을 다시 마운트하나
|
||||||
admin 화면이 부르는 API 는 전부 place·fact 라우터에 이미 있다(세어봤다: admin 전용
|
admin 화면이 부르는 API 는 전부 place·fact 라우터에 이미 있다(세어봤다: admin 전용
|
||||||
@ -1,10 +1,11 @@
|
|||||||
# 내부 운영 API 서버. 사장님 API(web_main.py, :9800)와 **다른 프로세스·다른 포트**다.
|
# 내부 운영 API 서버. 사장님 API(solution/backend/web_main.py, :9800)와
|
||||||
|
# **다른 프로세스·다른 포트**다.
|
||||||
#
|
#
|
||||||
# python admin_main.py
|
# PYTHONPATH=../../solution/backend python main.py
|
||||||
#
|
#
|
||||||
# ★ 코드는 나누지 않는다. services/crud/models 를 web_main 과 그대로 공유하고
|
# ★ 코드는 나누지 않는다. services/crud/models 를 web_main 과 그대로 공유하고
|
||||||
# 진입점만 둘이다 — 도메인을 두 번 구현하지 않으면서 프로세스·네트워크만 가른다.
|
# 진입점만 둘이다 — 도메인을 두 번 구현하지 않으면서 프로세스·네트워크만 가른다.
|
||||||
# 왜 그래야 하는지는 router/admin_router.py 주석에 있다.
|
# 왜 그래야 하는지는 app.py 주석에 있다.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ from config.server_configs import web_server_config
|
|||||||
|
|
||||||
LOG.SetPrefix(f"{web_server_config.server_name}-admin")
|
LOG.SetPrefix(f"{web_server_config.server_name}-admin")
|
||||||
|
|
||||||
import router.admin_router
|
import app # noqa: F401 (import 시점에 FastAPI app · DB 세션 매니저가 선다)
|
||||||
|
|
||||||
ADMIN_PORT = int(os.environ.get("ADMIN_API_PORT", "9801"))
|
ADMIN_PORT = int(os.environ.get("ADMIN_API_PORT", "9801"))
|
||||||
|
|
||||||
@ -26,4 +27,4 @@ if __name__ == "__main__":
|
|||||||
run_kwargs["reload"] = True
|
run_kwargs["reload"] = True
|
||||||
else:
|
else:
|
||||||
run_kwargs["workers"] = 1
|
run_kwargs["workers"] = 1
|
||||||
uvicorn.run("router.admin_router:app", **run_kwargs)
|
uvicorn.run("app:app", **run_kwargs)
|
||||||
@ -31,8 +31,8 @@ x-common-env: &common-env
|
|||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
context: ./solution/backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: solution/backend/Dockerfile
|
||||||
image: o2o-web4ai-backend
|
image: o2o-web4ai-backend
|
||||||
container_name: o2o-web4ai-api
|
container_name: o2o-web4ai-api
|
||||||
command: ["python", "web_main.py"]
|
command: ["python", "web_main.py"]
|
||||||
@ -55,8 +55,8 @@ services:
|
|||||||
|
|
||||||
worker:
|
worker:
|
||||||
build:
|
build:
|
||||||
context: ./solution/backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: solution/backend/Dockerfile
|
||||||
image: o2o-web4ai-backend
|
image: o2o-web4ai-backend
|
||||||
command: ["python", "worker_main.py"]
|
command: ["python", "worker_main.py"]
|
||||||
env_file:
|
env_file:
|
||||||
@ -83,13 +83,14 @@ services:
|
|||||||
driver: json-file
|
driver: json-file
|
||||||
options: { max-size: "10m", max-file: "5" }
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
# 내부 운영 API(:9801). 코드는 api 와 같은 이미지·같은 소스이고 진입점만 다르다
|
# 내부 운영 API(:9801). 진입점은 admin/backend/{main,app}.py 이고 도메인 코드는
|
||||||
# (admin_main.py → router/admin_router.py). place·fact 를 두 번 구현하지 않으면서
|
# solution/backend 것을 PYTHONPATH 로 그대로 쓴다 — place·fact 를 두 번 구현하지 않으면서
|
||||||
# 프로세스와 포트만 가른다. 여기 붙는 모든 엔드포인트는 role >= DEVELOPER 다.
|
# 프로세스와 포트만 가른다. 여기 붙는 모든 엔드포인트는 role >= DEVELOPER 다.
|
||||||
api-admin:
|
api-admin:
|
||||||
image: o2o-web4ai-backend
|
image: o2o-web4ai-backend
|
||||||
container_name: o2o-web4ai-api-admin
|
container_name: o2o-web4ai-api-admin
|
||||||
command: ["python", "admin_main.py"]
|
working_dir: /app/admin/backend
|
||||||
|
command: ["python", "main.py"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@ -99,7 +99,9 @@ o2o-web4ai/
|
|||||||
│ ├─ site/ 발행 정적 사이트 (SSR 엔트리 + 프리렌더)
|
│ ├─ site/ 발행 정적 사이트 (SSR 엔트리 + 프리렌더)
|
||||||
│ └─ shared/ front·site·백엔드 계약 (SitePayload · slug · 토큰)
|
│ └─ shared/ front·site·백엔드 계약 (SitePayload · slug · 토큰)
|
||||||
│
|
│
|
||||||
├─ admin/ 우리 — 전체 사이트 운영 (프론트). API 는 :9801
|
├─ admin/ 우리 — 전체 사이트 운영
|
||||||
|
│ ├─ backend/ 내부 API 진입점(:9801). 도메인 코드는 solution/backend
|
||||||
|
│ └─ frontend/ 내부 운영 화면
|
||||||
│
|
│
|
||||||
├─ docs/ nginx/ postgres-init/ docker-compose.yml package.json(워크스페이스 루트)
|
├─ docs/ nginx/ postgres-init/ docker-compose.yml package.json(워크스페이스 루트)
|
||||||
```
|
```
|
||||||
@ -126,10 +128,11 @@ negosium 대응: `negodata/{backend, front}` 가 프로젝트 안에서 f/b 를
|
|||||||
|
|
||||||
| | 포트 | 진입점 | 권한 |
|
| | 포트 | 진입점 | 권한 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 사장님 API | 9800 | `web_main.py` → `router/router.py` | 엔드포인트별 |
|
| 사장님 API | 9800 | `solution/backend/web_main.py` | 엔드포인트별 |
|
||||||
| 내부 API | **9801** | `admin_main.py` → `router/admin_router.py` | **앱 전체 `role >= DEVELOPER`** |
|
| 내부 API | **9801** | `admin/backend/main.py` → `app.py` | **앱 전체 `role >= DEVELOPER`** |
|
||||||
|
|
||||||
`services`·`crud`·`models` 은 공유한다. **admin 전용 라우터가 0개**이기 때문이다 — 세어봤다:
|
`services`·`crud`·`models` 은 공유한다 — `admin/backend` 는 진입점 두 파일뿐이고,
|
||||||
|
도메인 코드는 `PYTHONPATH=/app/solution/backend` 로 그대로 import 한다. **admin 전용 라우터가 0개**이기 때문이다 — 세어봤다:
|
||||||
|
|
||||||
```
|
```
|
||||||
useGetPlace · useListPlaces · useListLinks · useConfirmLink → router/v1/place
|
useGetPlace · useListPlaces · useListLinks · useConfirmLink → router/v1/place
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
# `__pycache__/` 는 최상위만 매칭한다 — 하위 디렉터리(config/, common/ …)까지 빼려면 ** 가 필요하다.
|
|
||||||
# (원본 보일러플레이트엔 이 줄이 없어 config/__pycache__ 가 이미지에 들어가 있었다.)
|
|
||||||
**/__pycache__/
|
|
||||||
*.pyc
|
|
||||||
.pytest_cache/
|
|
||||||
.git/
|
|
||||||
tests/
|
|
||||||
loadtest/
|
|
||||||
*.md
|
|
||||||
|
|
||||||
# 가상환경 — 호스트 전용(수백 MB). 컨테이너는 requirements.txt 로 새로 설치한다.
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
|
|
||||||
# 시크릿 — 이미지에 굽지 않는다. Dockerfile 이 example 을 복사해 넣고,
|
|
||||||
# 실값은 compose env 로 주입한다(server_configs 의 env override).
|
|
||||||
config/config.local.toml
|
|
||||||
# 테스트 설정 — 컨테이너에서 pytest 를 돌리지 않는다(테스트는 호스트 venv 에서).
|
|
||||||
config/config.test.toml
|
|
||||||
# 레포 최상위 .env 는 빌드 컨텍스트(./backend) 밖이라 애초에 안 들어오지만, 방어적으로 막아둔다.
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
@ -1,35 +1,40 @@
|
|||||||
# o2o-web4ai 백엔드 이미지 — API 와 워커가 **같은 이미지**를 공유하고 command 로만 갈린다.
|
# o2o-web4ai 백엔드 이미지 — 세 프로세스가 **같은 이미지**를 공유하고 command 로만 갈린다.
|
||||||
# API : python web_main.py (요청 접수/조회, :9800)
|
# 사장님 API : python web_main.py (:9800)
|
||||||
# 워커 : python worker_main.py (수집·비전분석·빌드 잡 처리, 포트 없음)
|
# 내부 API : python main.py (:9801, working_dir=/app/admin/backend)
|
||||||
|
# 워커 : python worker_main.py (수집·비전분석·빌드 잡, 포트 없음)
|
||||||
#
|
#
|
||||||
# Phase 1 은 collector 가 MockAdapter 만 등록하므로 브라우저가 필요 없다 → 이 lean 이미지 하나면 된다.
|
# ★ 빌드 컨텍스트가 **레포 루트**다(./solution/backend 가 아니다).
|
||||||
# 크롤링 법무 검토가 끝나 HeadlessAdapter 를 붙이면, 그때 Chrome+Xvfb 를 담은 별도 워커 이미지
|
# 내부 API 진입점이 admin/backend 에 있고 도메인 코드는 solution/backend 에 있어서,
|
||||||
# (Dockerfile.worker)를 만들어 갈라낸다 — 선례: o2o-negosium/lps/Dockerfile.worker
|
# 한 이미지에 둘 다 들어와야 한다. 나누면 requirements 를 두 번 설치하게 된다.
|
||||||
#
|
#
|
||||||
# 시크릿은 이미지에 굽지 않는다 — config 는 example(플레이스홀더)로 대체되고,
|
# 시크릿은 이미지에 굽지 않는다 — config 는 example(플레이스홀더)로 대체되고,
|
||||||
# 실제 값은 compose 의 env(DB_USER/DB_PASSWORD, *_API_KEY 등)로 주입된다(server_configs override).
|
# 실제 값은 compose 의 env 로 주입된다(server_configs override).
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 의존성 먼저 설치 (레이어 캐시 활용)
|
# 의존성 먼저 설치 (레이어 캐시 활용)
|
||||||
COPY requirements.txt .
|
COPY solution/backend/requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY . .
|
COPY solution/backend ./solution/backend
|
||||||
|
COPY admin/backend ./admin/backend
|
||||||
|
|
||||||
# 시크릿 든 config.local.toml 은 .dockerignore 로 제외됨 → example(플레이스홀더)로 대체.
|
# 시크릿 든 config.local.toml 은 .dockerignore 로 제외됨 → example(플레이스홀더)로 대체.
|
||||||
# 실값은 env 주입: DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME,
|
RUN cp solution/backend/config/config.local.toml.example solution/backend/config/config.local.toml
|
||||||
# PERPLEXITY_API_KEY/KAKAO_REST_API_KEY/GEMINI_API_KEY/TOUR_API_KEY
|
|
||||||
RUN cp config/config.local.toml.example config/config.local.toml
|
|
||||||
|
|
||||||
# 항상 APP_ENV=local 로 실행 → config.local.toml(=example 사본) + env override.
|
|
||||||
ENV APP_ENV=local
|
ENV APP_ENV=local
|
||||||
|
# ★ 내부 API(admin/backend)가 common·router·services 를 그대로 import 하는 경로.
|
||||||
|
# 도메인 코드를 복제하지 않는 대신 이 한 줄이 두 폴더를 잇는다.
|
||||||
|
ENV PYTHONPATH=/app/solution/backend
|
||||||
|
|
||||||
|
# 기본은 사장님 API. 내부 API·워커는 compose 가 working_dir·command 를 갈아끼운다.
|
||||||
|
WORKDIR /app/solution/backend
|
||||||
|
|
||||||
EXPOSE 9800
|
EXPOSE 9800
|
||||||
|
|
||||||
# API 컨테이너용. 워커는 command 를 갈아끼우고 이 헬스체크를 쓰지 않는다
|
# 사장님 API 컨테이너용. 워커는 포트가 없어 이 체크를 끄고, 내부 API 는 :9801 로 다시 건다
|
||||||
# (워커는 포트가 없다 — 하트비트 파일 기반 체크는 필요해질 때 추가한다).
|
# (둘 다 compose 에서 override).
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:9800/healthz', timeout=4).status==200 else 1)"
|
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:9800/healthz', timeout=4).status==200 else 1)"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user