diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be60bda --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 2641397..a45308a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ ``` solution/ 사장님 — backend · frontend(빌더) · site(발행물) · shared(계약) -admin/ 우리 — 전체 사이트 운영. 프론트만. API 는 아래 :9801 을 본다 +admin/ 우리 — backend(진입점만, :9801) · frontend(운영 화면) ``` 최상단은 **프로젝트 단위**다(`o2o-negosium` 과 같은 규약). `frontend/` `backend/` 를 최상단 @@ -75,8 +75,8 @@ admin/ 우리 — 전체 사이트 운영. 프론트만. API 는 아래 :9 | | 포트 | 진입점 | 권한 | |---|---|---|---| -| 사장님 API | 9800 | `web_main.py` → `router/router.py` | 엔드포인트별 | -| 내부 API | 9801 | `admin_main.py` → `router/admin_router.py` | **앱 전체 role >= DEVELOPER** | +| 사장님 API | 9800 | `solution/backend/web_main.py` | 엔드포인트별 | +| 내부 API | 9801 | `admin/backend/main.py` → `app.py` | **앱 전체 role >= DEVELOPER** | `services`·`crud`·`models` 은 그대로 공유한다. admin 전용 라우터가 0개라(세어봤다: admin 화면이 부르는 건 전부 place·fact 다) 도메인을 복제하지 않고 **같은 router 객체를 diff --git a/README.md b/README.md index ee67b6a..27de4d8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ solution/ 사장님 — 사이트 만들기·관리 frontend/ 빌더(위저드 + 에디터 + 발행 게이트) site/ 발행 사이트. SSR 엔트리 + 프리렌더 + 정적 서버 shared/ front·site·백엔드 계약이 만나는 타입·규칙 (SitePayload, slug) -admin/ 우리 — 전체 사이트 운영 (프론트). API 는 solution/backend 의 :9801 진입점 +admin/ 우리 — 전체 사이트 운영 + backend/ 내부 API 진입점(:9801). 도메인 코드는 solution/backend 를 PYTHONPATH 로 쓴다 + frontend/ 내부 운영 화면 docs/ 아래 표 nginx/ 발행 사이트 정적 서빙 (site.conf 는 .example 만 커밋한다) postgres-init/ 스키마 DDL (init.sql 한 벌) diff --git a/solution/backend/router/admin_router.py b/admin/backend/app.py similarity index 89% rename from solution/backend/router/admin_router.py rename to admin/backend/app.py index 0c5e2e2..0de81ee 100644 --- a/solution/backend/router/admin_router.py +++ b/admin/backend/app.py @@ -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 전용 diff --git a/solution/backend/admin_main.py b/admin/backend/main.py similarity index 66% rename from solution/backend/admin_main.py rename to admin/backend/main.py index c1299a4..47591f7 100644 --- a/solution/backend/admin_main.py +++ b/admin/backend/main.py @@ -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 과 그대로 공유하고 # 진입점만 둘이다 — 도메인을 두 번 구현하지 않으면서 프로세스·네트워크만 가른다. -# 왜 그래야 하는지는 router/admin_router.py 주석에 있다. +# 왜 그래야 하는지는 app.py 주석에 있다. import os @@ -15,7 +16,7 @@ from config.server_configs import web_server_config 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")) @@ -26,4 +27,4 @@ if __name__ == "__main__": run_kwargs["reload"] = True else: run_kwargs["workers"] = 1 - uvicorn.run("router.admin_router:app", **run_kwargs) + uvicorn.run("app:app", **run_kwargs) diff --git a/docker-compose.yml b/docker-compose.yml index 67378b2..afa0818 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,8 +31,8 @@ x-common-env: &common-env services: api: build: - context: ./solution/backend - dockerfile: Dockerfile + context: . + dockerfile: solution/backend/Dockerfile image: o2o-web4ai-backend container_name: o2o-web4ai-api command: ["python", "web_main.py"] @@ -55,8 +55,8 @@ services: worker: build: - context: ./solution/backend - dockerfile: Dockerfile + context: . + dockerfile: solution/backend/Dockerfile image: o2o-web4ai-backend command: ["python", "worker_main.py"] env_file: @@ -83,13 +83,14 @@ services: driver: json-file options: { max-size: "10m", max-file: "5" } - # 내부 운영 API(:9801). 코드는 api 와 같은 이미지·같은 소스이고 진입점만 다르다 - # (admin_main.py → router/admin_router.py). place·fact 를 두 번 구현하지 않으면서 + # 내부 운영 API(:9801). 진입점은 admin/backend/{main,app}.py 이고 도메인 코드는 + # solution/backend 것을 PYTHONPATH 로 그대로 쓴다 — place·fact 를 두 번 구현하지 않으면서 # 프로세스와 포트만 가른다. 여기 붙는 모든 엔드포인트는 role >= DEVELOPER 다. api-admin: image: o2o-web4ai-backend container_name: o2o-web4ai-api-admin - command: ["python", "admin_main.py"] + working_dir: /app/admin/backend + command: ["python", "main.py"] env_file: - .env environment: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a7bbd0..f9ea487 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -99,7 +99,9 @@ o2o-web4ai/ │ ├─ site/ 발행 정적 사이트 (SSR 엔트리 + 프리렌더) │ └─ shared/ front·site·백엔드 계약 (SitePayload · slug · 토큰) │ -├─ admin/ 우리 — 전체 사이트 운영 (프론트). API 는 :9801 +├─ admin/ 우리 — 전체 사이트 운영 +│ ├─ backend/ 내부 API 진입점(:9801). 도메인 코드는 solution/backend +│ └─ frontend/ 내부 운영 화면 │ ├─ 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 | **9801** | `admin_main.py` → `router/admin_router.py` | **앱 전체 `role >= DEVELOPER`** | +| 사장님 API | 9800 | `solution/backend/web_main.py` | 엔드포인트별 | +| 내부 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 diff --git a/solution/backend/.dockerignore b/solution/backend/.dockerignore deleted file mode 100644 index 0c8c6a0..0000000 --- a/solution/backend/.dockerignore +++ /dev/null @@ -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.* diff --git a/solution/backend/Dockerfile b/solution/backend/Dockerfile index ce1e824..c08dbf3 100644 --- a/solution/backend/Dockerfile +++ b/solution/backend/Dockerfile @@ -1,35 +1,40 @@ -# o2o-web4ai 백엔드 이미지 — API 와 워커가 **같은 이미지**를 공유하고 command 로만 갈린다. -# API : python web_main.py (요청 접수/조회, :9800) -# 워커 : python worker_main.py (수집·비전분석·빌드 잡 처리, 포트 없음) +# o2o-web4ai 백엔드 이미지 — 세 프로세스가 **같은 이미지**를 공유하고 command 로만 갈린다. +# 사장님 API : python web_main.py (:9800) +# 내부 API : python main.py (:9801, working_dir=/app/admin/backend) +# 워커 : python worker_main.py (수집·비전분석·빌드 잡, 포트 없음) # -# Phase 1 은 collector 가 MockAdapter 만 등록하므로 브라우저가 필요 없다 → 이 lean 이미지 하나면 된다. -# 크롤링 법무 검토가 끝나 HeadlessAdapter 를 붙이면, 그때 Chrome+Xvfb 를 담은 별도 워커 이미지 -# (Dockerfile.worker)를 만들어 갈라낸다 — 선례: o2o-negosium/lps/Dockerfile.worker +# ★ 빌드 컨텍스트가 **레포 루트**다(./solution/backend 가 아니다). +# 내부 API 진입점이 admin/backend 에 있고 도메인 코드는 solution/backend 에 있어서, +# 한 이미지에 둘 다 들어와야 한다. 나누면 requirements 를 두 번 설치하게 된다. # # 시크릿은 이미지에 굽지 않는다 — config 는 example(플레이스홀더)로 대체되고, -# 실제 값은 compose 의 env(DB_USER/DB_PASSWORD, *_API_KEY 등)로 주입된다(server_configs override). +# 실제 값은 compose 의 env 로 주입된다(server_configs override). FROM python:3.12-slim WORKDIR /app # 의존성 먼저 설치 (레이어 캐시 활용) -COPY requirements.txt . +COPY solution/backend/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(플레이스홀더)로 대체. -# 실값은 env 주입: DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME, -# PERPLEXITY_API_KEY/KAKAO_REST_API_KEY/GEMINI_API_KEY/TOUR_API_KEY -RUN cp config/config.local.toml.example config/config.local.toml +RUN cp solution/backend/config/config.local.toml.example solution/backend/config/config.local.toml -# 항상 APP_ENV=local 로 실행 → config.local.toml(=example 사본) + env override. 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 -# API 컨테이너용. 워커는 command 를 갈아끼우고 이 헬스체크를 쓰지 않는다 -# (워커는 포트가 없다 — 하트비트 파일 기반 체크는 필요해질 때 추가한다). +# 사장님 API 컨테이너용. 워커는 포트가 없어 이 체크를 끄고, 내부 API 는 :9801 로 다시 건다 +# (둘 다 compose 에서 override). 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)"