diff --git a/.gitignore b/.gitignore index 8e202b1..7a3af39 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ !.env.example # 설정: 시크릿 포함이라 커밋하지 않는다. *.example 만 커밋한다. +# ★ 레포 전체의 *.toml 이 대상이다. toml 은 DB 비밀번호·JWT 키가 들어가는 파일이라 +# 안전한 쪽으로 넓게 잡았다 — 대신 pyproject.toml 같은 걸 새로 추가하면 조용히 +# 무시되므로, 그때는 여기에 예외(!)를 명시적으로 적는다. *.toml !*.toml.example diff --git a/AGENTS.md b/AGENTS.md index e7f3548..693fc9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,8 +96,7 @@ docker compose logs -f worker - 사장님 앱 `:3000`(API :9800) · 내부 운영 `:3002`(API :9801, 로컬호스트에만 열림) - 발행 사이트: `http://localhost:3000/s/` (빌더 Vite 가 :3001 정적서버로 프록시) -- **클론 직후 1회**: `cp .env.example .env` · `cp nginx/site.conf.example nginx/site.conf` - (후자를 빼먹으면 Docker 가 그 자리에 디렉토리를 만들어 nginx 가 설정 없이 뜬다) +- **클론 직후 1회**: 설정 파일 복사 — 아래 "설정 파일은 어느 폴더에 두나" - DB 는 compose 밖이다 (호스트 PostgreSQL, `host.docker.internal`). 스키마는 `postgres-init/init-data/init.sql` **한 벌**이다 — 누적 ALTER 파일은 없다 - npm 워크스페이스 루트는 **레포 루트**다. `npm install` 은 루트에서 한 번. @@ -106,19 +105,55 @@ docker compose logs -f worker - 테스트: `solution/backend/` 에서 `.venv/bin/pytest`. **`APP_ENV=test` 면 `.env` 를 읽지 않는다** — 실키가 테스트로 새어 외부 API 요금이 나가는 경로를 막아 뒀다 -## .env 는 어디에 두나 +## 설정 파일은 어느 폴더에 두나 -**루트 `.env` 가 단일 출처다.** compose 가 이 파일을 읽고(`${...}` 치환 + `env_file`), -**백엔드도 compose 밖에서 띄우면 이 파일을 직접 읽는다**(`config/server_configs.py` 의 -python-dotenv). Vite 앱만 구조상 **자기 디렉토리의 `.env`** 를 읽는다 — 그래서 나뉘어 있는 -것이지 취향이 아니다. +**실제 값은 하나도 커밋되지 않는다.** 커밋되는 건 `*.example` 뿐이고, 클론 직후 그걸 복사해 +값을 채우는 게 세팅의 전부다. -| 파일 | 담는 것 | +| 폴더 | 파일 | 담는 것 | 템플릿 | +|---|---|---|---| +| **레포 루트** | `.env` | DB · JWT · 외부 API 키 · 수집 어댑터 · 발행/색인 · 포트 바인딩 | `.env.example` | +| `solution/backend/config/` | `config.local.toml` | DB 접속 · JWT · 포트 · CORS(`client_url`) | `config.local.toml.example` | +| `solution/backend/config/` | `config.test.toml` | 테스트 DB(`web4ai_test_db`) | `config.test.toml.example` | +| `solution/frontend/` | `.env` | 이 앱의 `VITE_*` (API :9800 · 발행 호스트 · 개발 자동로그인) | `.env.example` | +| `admin/frontend/` | `.env` | 이 앱의 `VITE_*` (API **:9801**) | `.env.example` | +| `nginx/` | `site.conf` | 발행 사이트 정적 서빙 규칙 | `site.conf.example` | + +**두지 않는 곳** — 두면 조용히 무시된다. + +| 폴더 | 왜 없나 | |---|---| -| `.env` | DB · JWT · 외부 API 키 · 수집 어댑터 · 발행/색인 · 포트 바인딩 — **백엔드가 쓰는 전부** | -| `solution/frontend/.env` · `admin/frontend/.env` | 그 앱에만 있는 `VITE_*`. admin 은 API 가 :9801 이다 | -| `solution/site` | **없다.** 렌더러는 런타임 env 를 안 쓴다 — 사이트별 값은 전부 `SitePayload` 로 들어온다 | -| `solution/backend` | **없다.** 여기 `.env` 를 두면 아무도 읽지 않는다 (루트를 본다) | +| `solution/backend/` | `.env` 를 여기 두면 아무도 안 읽는다. dotenv 가 **레포 루트**를 본다 (`config/server_configs.py`) | +| `admin/backend/` | 진입점 두 파일뿐이다. `config/` 는 `PYTHONPATH` 로 `solution/backend` 것을 그대로 쓴다 | +| `solution/site/` | 렌더러는 런타임 env 를 안 쓴다 — 사이트별 값은 전부 `SitePayload` 로 들어온다. 그래야 payload 하나로 같은 HTML 이 재현된다. 프리렌더 옵션은 CLI 인자(`--payload` `--out`)이고, `INDEXNOW_KEY`·`PORT` 는 compose 가 프로세스에 넣는다 | +| `solution/shared/` | 코드만 있다 | + +### 클론 직후 한 번 + +```bash +cp .env.example .env +cp nginx/site.conf.example nginx/site.conf +cp solution/backend/config/config.local.toml.example solution/backend/config/config.local.toml +cp solution/backend/config/config.test.toml.example solution/backend/config/config.test.toml +cp solution/frontend/.env.example solution/frontend/.env # compose 로만 쓰면 생략 가능 +cp admin/frontend/.env.example admin/frontend/.env # 〃 +``` + +`site.conf` 를 빼먹으면 Docker 가 그 자리에 **디렉토리**를 만들어 nginx 가 설정 없이 뜬다. +`config.test.toml` 이 없으면 `pytest` 가 conftest import 단계에서 죽는다. + +### 누가 무엇을 읽나 — 우선순위 + +**실제 환경변수(compose) > 루트 `.env` > `config.{APP_ENV}.toml`** + +- compose 는 루트 `.env` 를 `env_file` 로 통째로 넣고 `${...}` 치환에도 쓴다. +- 백엔드는 **compose 밖에서 띄울 때** 루트 `.env` 를 python-dotenv 로 직접 읽는다. +- Vite 앱만 구조상 **자기 디렉토리의 `.env`** 를 읽는다 — 그래서 나뉜 것이지 취향이 아니다. +- 도커 이미지는 시크릿을 굽지 않는다. `config.local.toml` 을 **example 사본(플레이스홀더)으로 + 덮어** 넣으므로(`Dockerfile:24`), 컨테이너로 띄울 땐 `JWT_*` 를 env 로 반드시 주입해야 한다. + 안 하면 공개된 플레이스홀더가 서명 키가 된다. +- **`APP_ENV=test` 면 `.env` 를 읽지 않는다.** 실키가 테스트로 새어 외부 API 요금이 나가는 + 경로를 막아 뒀다 — 그래서 `config.test.toml` 의 키는 전부 빈 값이어야 한다. ★ **두 곳에 같은 값을 적지 않는다.** 발행 호스트는 compose 가 루트의 `SITE_PUBLIC_HOST` 를 `VITE_PUBLISH_HOST` 로 흘려보낸다. 각자 적으면 canonical 과 화면 주소가 조용히 갈라진다. diff --git a/README.md b/README.md index 0539654..aa7a431 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,11 @@ ## 실행 ```bash -cp .env.example .env # DB_*, JWT_*, 외부 API 키 -cp nginx/site.conf.example nginx/site.conf # 빼먹으면 nginx 가 설정 없이 뜬다 +# 클론 직후 1회 — 설정 파일 복사 목록은 AGENTS.md "설정 파일은 어느 폴더에 두나" +cp .env.example .env +cp nginx/site.conf.example nginx/site.conf +cp solution/backend/config/config.local.toml.example solution/backend/config/config.local.toml + docker compose up -d docker compose logs -f worker ``` diff --git a/solution/backend/README.md b/solution/backend/README.md index 1edc26e..b57e74e 100644 --- a/solution/backend/README.md +++ b/solution/backend/README.md @@ -85,7 +85,7 @@ UNVERIFIED ──┬─> PENDING_OWNER ──┬─> VERIFIED ──┬─> CORR ``` o2o-web4ai/ -├── .env.example # 외부 API 키·DB 접속 주입 템플릿 (cp .env.example .env) +├── .env.example # ★ 루트다. 백엔드가 읽는 유일한 .env (cp .env.example .env) ├── postgres-init/ │ └── init-data/init.sql # 스키마 DDL 한 벌 (재실행 안전. 누적 ALTER 파일은 없다) ├── admin/backend/ # 내부 API 진입점(:9801). 도메인 코드는 아래를 그대로 import 한다 @@ -196,9 +196,13 @@ POST /v1/place/{id}/site/slug {slug} → sites.domain 에 저장(사 | 무엇 | 어디 | 커밋 | |---|---|---| -| DB 접속 · JWT · 포트 | `solution/backend/config/config.local.toml` | ✗ (`*.toml` ignore) | -| 외부 API 키 | 레포 최상위 `.env` | ✗ | -| 템플릿 | `config/config.local.toml.example` · `.env.example` | ✓ | +| DB 접속 · JWT · 포트 · CORS | `solution/backend/config/config.local.toml` | ✗ (`*.toml` ignore) | +| 테스트 DB | `solution/backend/config/config.test.toml` | ✗ | +| 외부 API 키 · 수집/발행 스위치 | **레포 루트 `.env`** | ✗ | +| 템플릿 | 위 셋의 `*.example` | ✓ | + +★ `solution/backend/.env` 는 **아무도 읽지 않는다** — dotenv 가 레포 루트를 본다. +어느 폴더에 무엇을 두는지는 [../../AGENTS.md](../../AGENTS.md) 가 단일 출처다. 우선순위: **실제 환경변수(docker-compose) > `.env` > `config.{APP_ENV}.toml`** diff --git a/solution/backend/config/config.local.toml.example b/solution/backend/config/config.local.toml.example index 22040ae..8921330 100644 --- a/solution/backend/config/config.local.toml.example +++ b/solution/backend/config/config.local.toml.example @@ -49,4 +49,8 @@ kakao_rest_api_key = "" # 동일 업소 검증(미발급) naver_client_id = "" # 동일 업소 검증 — 네이버 지역검색 naver_client_secret = "" gemini_api_key = "" # 사진 분류 · 카피 작성 -tour_api_key = "" # 공공데이터포털 전국문화축제표준데이터 일반 인증키 +tour_api_key = "" # 공공데이터포털 일반 인증키. fact 의 주력 공급원 +# 아래 셋은 생략하면 코드 기본값을 쓴다(config_models.ExternalApiConfig). +# gemini_vision_model = "gemini-3.7-flash" +# gemini_text_model = "gemini-3.7-flash" +# vision_confidence_threshold = 0.7 # 미만이면 사람 확인 큐에 남긴다 diff --git a/solution/backend/config/config.test.toml.example b/solution/backend/config/config.test.toml.example index 7c3d7d5..5571313 100644 --- a/solution/backend/config/config.test.toml.example +++ b/solution/backend/config/config.test.toml.example @@ -6,7 +6,7 @@ # ★ DB 이름을 dev 와 절대 같게 두지 말 것 — 픽스처가 TRUNCATE 를 돌린다. # conftest 의 안전가드가 이름을 확인하지만, 여기서부터 갈라 두는 게 먼저다. [WebServerConfig] -server_name = "O2oSiteServerTest" +server_name = "Web4aiServerTest" port = 9800 process_count = 1 is_ssl = false diff --git a/solution/site/.env.example b/solution/site/.env.example deleted file mode 100644 index 263821c..0000000 --- a/solution/site/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# 발행 사이트 렌더러는 런타임 환경변수를 쓰지 않는다 — 여기 채울 것이 없다. -# -# 사이트마다 다른 값(도메인·색·내용)은 전부 SitePayload 로 들어온다. -# 그래야 payload 하나로 같은 HTML 이 재현된다 — 렌더 결과가 환경에 따라 달라지면 -# "구워진 HTML 기준" 2차 발행 게이트가 의미를 잃는다. -# -# 프리렌더 옵션은 CLI 인자로 준다: -# npm run prerender -- --payload=./payloads --out=/var/www -# -# 예외로 프로세스 환경에서 읽는 둘은 compose 가 넣는다(이 파일 아님): -# INDEXNOW_KEY 루트 .txt 를 굽는다. 백엔드와 같은 값이어야 한다 -# PORT serve-sites.mjs 개발용 정적 서버 포트(:3001)