설정: 어느 폴더에 .env 와 toml 이 들어가는지 한 표로 못 박는다

세팅할 때마다 "이 값이 어디 가는 거지" 를 코드에서 되짚고 있었다. AGENTS.md 에
설정 파일 지도를 넣고, 흩어져 있던 cp 명령을 거기 한 곳으로 모았다.

두는 곳 — 템플릿이 전부 짝을 갖는다(7개 확인).
  루트                        .env                    ← 백엔드가 읽는 유일한 .env
  solution/backend/config/    config.local.toml       DB·JWT·포트·CORS
  solution/backend/config/    config.test.toml        없으면 pytest 가 import 단계에서 죽는다
  solution/frontend/          .env                    VITE_* (API :9800)
  admin/frontend/             .env                    VITE_* (API :9801)
  nginx/                      site.conf               없으면 Docker 가 디렉토리를 만든다

두지 않는 곳과 그 이유도 같이 적었다 — solution/backend(dotenv 가 루트를 본다),
admin/backend(PYTHONPATH 로 solution 것을 쓴다), solution/site, solution/shared.

solution/site/.env.example 을 지웠다.
"여기엔 아무것도 없다" 는 걸 알리려고 둔 파일인데, .env.example 은 이 레포에서
"복사해서 채워라" 라는 뜻이라 정반대 신호를 준다. 그 설명은 지도의 '두지 않는 곳' 으로 옮겼다.

그 밖에
- config.test.toml.example 의 server_name 이 O2oSiteServerTest 로 남아 있었다.
- config.local.toml.example 에 생략 가능한 [ExternalApiConfig] 필드 셋을 주석으로 적었다.
- .gitignore 의 *.toml 이 레포 전체를 덮는다는 것과, 그래서 pyproject.toml 같은 걸
  추가하면 조용히 무시된다는 것을 규칙 옆에 적었다.
This commit is contained in:
Mina Choi 2026-08-31 17:11:25 +09:00
parent b9d598e214
commit fc47946d3f
7 changed files with 69 additions and 32 deletions

3
.gitignore vendored
View File

@ -4,6 +4,9 @@
!.env.example !.env.example
# 설정: 시크릿 포함이라 커밋하지 않는다. *.example 만 커밋한다. # 설정: 시크릿 포함이라 커밋하지 않는다. *.example 만 커밋한다.
# ★ 레포 전체의 *.toml 이 대상이다. toml 은 DB 비밀번호·JWT 키가 들어가는 파일이라
# 안전한 쪽으로 넓게 잡았다 — 대신 pyproject.toml 같은 걸 새로 추가하면 조용히
# 무시되므로, 그때는 여기에 예외(!)를 명시적으로 적는다.
*.toml *.toml
!*.toml.example !*.toml.example

View File

@ -96,8 +96,7 @@ docker compose logs -f worker
- 사장님 앱 `:3000`(API :9800) · 내부 운영 `:3002`(API :9801, 로컬호스트에만 열림) - 사장님 앱 `:3000`(API :9800) · 내부 운영 `:3002`(API :9801, 로컬호스트에만 열림)
- 발행 사이트: `http://localhost:3000/s/<slug>` (빌더 Vite 가 :3001 정적서버로 프록시) - 발행 사이트: `http://localhost:3000/s/<slug>` (빌더 Vite 가 :3001 정적서버로 프록시)
- **클론 직후 1회**: `cp .env.example .env` · `cp nginx/site.conf.example nginx/site.conf` - **클론 직후 1회**: 설정 파일 복사 — 아래 "설정 파일은 어느 폴더에 두나"
(후자를 빼먹으면 Docker 가 그 자리에 디렉토리를 만들어 nginx 가 설정 없이 뜬다)
- DB 는 compose 밖이다 (호스트 PostgreSQL, `host.docker.internal`). - DB 는 compose 밖이다 (호스트 PostgreSQL, `host.docker.internal`).
스키마는 `postgres-init/init-data/init.sql` **한 벌**이다 — 누적 ALTER 파일은 없다 스키마는 `postgres-init/init-data/init.sql` **한 벌**이다 — 누적 ALTER 파일은 없다
- npm 워크스페이스 루트는 **레포 루트**다. `npm install` 은 루트에서 한 번. - npm 워크스페이스 루트는 **레포 루트**다. `npm install` 은 루트에서 한 번.
@ -106,19 +105,55 @@ docker compose logs -f worker
- 테스트: `solution/backend/` 에서 `.venv/bin/pytest`. **`APP_ENV=test` 면 `.env` 를 읽지 않는다** — - 테스트: `solution/backend/` 에서 `.venv/bin/pytest`. **`APP_ENV=test` 면 `.env` 를 읽지 않는다** —
실키가 테스트로 새어 외부 API 요금이 나가는 경로를 막아 뒀다 실키가 테스트로 새어 외부 API 요금이 나가는 경로를 막아 뒀다
## .env 는 어디에 두나 ## 설정 파일은 어느 폴더에 두나
**루트 `.env` 가 단일 출처다.** compose 가 이 파일을 읽고(`${...}` 치환 + `env_file`), **실제 값은 하나도 커밋되지 않는다.** 커밋되는 건 `*.example` 뿐이고, 클론 직후 그걸 복사해
**백엔드도 compose 밖에서 띄우면 이 파일을 직접 읽는다**(`config/server_configs.py` 의 값을 채우는 게 세팅의 전부다.
python-dotenv). Vite 앱만 구조상 **자기 디렉토리의 `.env`** 를 읽는다 — 그래서 나뉘어 있는
것이지 취향이 아니다.
| 파일 | 담는 것 | | 폴더 | 파일 | 담는 것 | 템플릿 |
|---|---|---|---|
| **레포 루트** | `.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/backend/` | `.env` 를 여기 두면 아무도 안 읽는다. dotenv 가 **레포 루트**를 본다 (`config/server_configs.py`) |
| `solution/frontend/.env` · `admin/frontend/.env` | 그 앱에만 있는 `VITE_*`. admin 은 API 가 :9801 이다 | | `admin/backend/` | 진입점 두 파일뿐이다. `config/` 는 `PYTHONPATH` 로 `solution/backend` 것을 그대로 쓴다 |
| `solution/site` | **없다.** 렌더러는 런타임 env 를 안 쓴다 — 사이트별 값은 전부 `SitePayload` 로 들어온다 | | `solution/site/` | 렌더러는 런타임 env 를 안 쓴다 — 사이트별 값은 전부 `SitePayload` 로 들어온다. 그래야 payload 하나로 같은 HTML 이 재현된다. 프리렌더 옵션은 CLI 인자(`--payload` `--out`)이고, `INDEXNOW_KEY`·`PORT` 는 compose 가 프로세스에 넣는다 |
| `solution/backend` | **없다.** 여기 `.env` 를 두면 아무도 읽지 않는다 (루트를 본다) | | `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` 를 ★ **두 곳에 같은 값을 적지 않는다.** 발행 호스트는 compose 가 루트의 `SITE_PUBLIC_HOST` 를
`VITE_PUBLISH_HOST` 로 흘려보낸다. 각자 적으면 canonical 과 화면 주소가 조용히 갈라진다. `VITE_PUBLISH_HOST` 로 흘려보낸다. 각자 적으면 canonical 과 화면 주소가 조용히 갈라진다.

View File

@ -13,8 +13,11 @@
## 실행 ## 실행
```bash ```bash
cp .env.example .env # DB_*, JWT_*, 외부 API 키 # 클론 직후 1회 — 설정 파일 복사 목록은 AGENTS.md "설정 파일은 어느 폴더에 두나"
cp nginx/site.conf.example nginx/site.conf # 빼먹으면 nginx 가 설정 없이 뜬다 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 up -d
docker compose logs -f worker docker compose logs -f worker
``` ```

View File

@ -85,7 +85,7 @@ UNVERIFIED ──┬─> PENDING_OWNER ──┬─> VERIFIED ──┬─> CORR
``` ```
o2o-web4ai/ o2o-web4ai/
├── .env.example # 외부 API 키·DB 접속 주입 템플릿 (cp .env.example .env) ├── .env.example # ★ 루트다. 백엔드가 읽는 유일한 .env (cp .env.example .env)
├── postgres-init/ ├── postgres-init/
│ └── init-data/init.sql # 스키마 DDL 한 벌 (재실행 안전. 누적 ALTER 파일은 없다) │ └── init-data/init.sql # 스키마 DDL 한 벌 (재실행 안전. 누적 ALTER 파일은 없다)
├── admin/backend/ # 내부 API 진입점(:9801). 도메인 코드는 아래를 그대로 import 한다 ├── 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) | | DB 접속 · JWT · 포트 · CORS | `solution/backend/config/config.local.toml` | ✗ (`*.toml` ignore) |
| 외부 API 키 | 레포 최상위 `.env` | ✗ | | 테스트 DB | `solution/backend/config/config.test.toml` | ✗ |
| 템플릿 | `config/config.local.toml.example` · `.env.example` | ✓ | | 외부 API 키 · 수집/발행 스위치 | **레포 루트 `.env`** | ✗ |
| 템플릿 | 위 셋의 `*.example` | ✓ |
★ `solution/backend/.env` 는 **아무도 읽지 않는다** — dotenv 가 레포 루트를 본다.
어느 폴더에 무엇을 두는지는 [../../AGENTS.md](../../AGENTS.md) 가 단일 출처다.
우선순위: **실제 환경변수(docker-compose) > `.env` > `config.{APP_ENV}.toml`** 우선순위: **실제 환경변수(docker-compose) > `.env` > `config.{APP_ENV}.toml`**

View File

@ -49,4 +49,8 @@ kakao_rest_api_key = "" # 동일 업소 검증(미발급)
naver_client_id = "" # 동일 업소 검증 — 네이버 지역검색 naver_client_id = "" # 동일 업소 검증 — 네이버 지역검색
naver_client_secret = "" naver_client_secret = ""
gemini_api_key = "" # 사진 분류 · 카피 작성 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 # 미만이면 사람 확인 큐에 남긴다

View File

@ -6,7 +6,7 @@
# ★ DB 이름을 dev 와 절대 같게 두지 말 것 — 픽스처가 TRUNCATE 를 돌린다. # ★ DB 이름을 dev 와 절대 같게 두지 말 것 — 픽스처가 TRUNCATE 를 돌린다.
# conftest 의 안전가드가 이름을 확인하지만, 여기서부터 갈라 두는 게 먼저다. # conftest 의 안전가드가 이름을 확인하지만, 여기서부터 갈라 두는 게 먼저다.
[WebServerConfig] [WebServerConfig]
server_name = "O2oSiteServerTest" server_name = "Web4aiServerTest"
port = 9800 port = 9800
process_count = 1 process_count = 1
is_ssl = false is_ssl = false

View File

@ -1,12 +0,0 @@
# 발행 사이트 렌더러는 런타임 환경변수를 쓰지 않는다 — 여기 채울 것이 없다.
#
# 사이트마다 다른 값(도메인·색·내용)은 전부 SitePayload 로 들어온다.
# 그래야 payload 하나로 같은 HTML 이 재현된다 — 렌더 결과가 환경에 따라 달라지면
# "구워진 HTML 기준" 2차 발행 게이트가 의미를 잃는다.
#
# 프리렌더 옵션은 CLI 인자로 준다:
# npm run prerender -- --payload=./payloads --out=/var/www
#
# 예외로 프로세스 환경에서 읽는 둘은 compose 가 넣는다(이 파일 아님):
# INDEXNOW_KEY 루트 <key>.txt 를 굽는다. 백엔드와 같은 값이어야 한다
# PORT serve-sites.mjs 개발용 정적 서버 포트(:3001)