★ 버그 — 백엔드가 루트 .env 를 안 읽고 있었다. server_configs.py 의 _DOTENV_PATH 가 dirname 세 번이라 solution/.env 를 보고 있었다. backend/ 가 solution/ 안으로 들어갈 때 계단을 안 늘린 것이다. 도커는 compose 가 env 를 직접 주입해서 안 드러나고, compose 밖(스크립트·로컬 실행)에서만 외부 API 키가 조용히 안 들어왔다 — 어댑터가 말없이 비활성되는 종류라 아무도 눈치채지 못한다. solution/backend/.env.example 을 지웠다. 그 자리의 .env 는 위 버그를 고친 뒤에도 아무도 읽지 않는다(루트를 본다). 살아 있던 값은 루트로 옮겼다 — COLLECT_ADAPTERS(기본값을 코드와 맞췄다), NAVER_COOKIES, SITE_PAYLOAD_DIR. COLLECT_USE_PERPLEXITY 는 버렸다: collect_service 가 "env 로 일괄 활성화하지 않는다" 며 요청 옵션으로 옮겨서, 코드가 더는 읽지 않는다. 루트 .env.example 을 코드·compose 와 맞췄다. - 코드가 읽는데 템플릿에 없던 것: COLLECT_ADAPTERS · NAVER_COOKIES · SITE_PAYLOAD_DIR · SITE_OUTPUT_DIR · RENDER_TIMEOUT_SEC · ADMIN_API_PORT · JOB_LEASE_SEC · JOB_DEADLINE_SEC · WORKER_CONCURRENCY · JWT_*_EXPIRE_* · GEMINI_*_MODEL · VISION_CONFIDENCE_THRESHOLD - compose 가 읽는데 템플릿에 없던 것: API_BIND · ADMIN_BIND · ADMIN_API_BIND · ADMIN_API_BASE_URL. 내부 두 개를 0.0.0.0 으로 열면 앱을 가른 의미가 없는데, 템플릿에 없으면 그 선택지가 있는 줄도 모른다. - AZURE_STORAGE_PREFIX 기본값을 비웠다. 코드 기본값 ai-for-web 을 그대로 두면 HTML 이 가리키는 /assets/… 와 블롭 경로가 어긋나 CSS 가 404 다(AGENTS.md 함정). - 카카오 주석 블록 안에 네이버 설명이 끼어 있고 KAKAO_REST_API_KEY 가 저 아래 떨어져 있던 것을 공급자별로 다시 묶었다. 앱 쪽 템플릿은 "여기 뭘 적으면 안 되는가"를 적었다. site 는 런타임 env 를 안 쓴다는 사실을, admin/frontend 는 :9801 을 봐야 한다는 것을 남겼다.
131 lines
7.9 KiB
Plaintext
131 lines
7.9 KiB
Plaintext
# o2o-web4ai 환경변수 — 루트 .env 가 단일 출처다.
|
|
# cp .env.example .env 후 값을 채운다. .env 는 커밋되지 않는다(.gitignore).
|
|
#
|
|
# 누가 이 파일을 읽나
|
|
# · docker compose — env_file 로 통째로, 그리고 ${...} 치환에
|
|
# · 백엔드/워커 — compose 밖에서 띄울 때 python-dotenv 가 직접 (config/server_configs.py)
|
|
# Vite 앱만 예외다. 자기 디렉토리의 .env 만 읽는 구조라 VITE_* 는 거기에 있다:
|
|
# solution/frontend/.env · admin/frontend/.env
|
|
#
|
|
# 우선순위: 실제 환경변수(compose 등) > 이 파일 > config/config.{APP_ENV}.toml
|
|
# ★ 두 곳에 같은 값을 적지 않는다. 발행 호스트는 compose 가 SITE_PUBLIC_HOST 를
|
|
# VITE_PUBLISH_HOST 로 흘려보낸다 — 각자 적으면 canonical 과 화면 주소가 조용히 갈라진다.
|
|
# ★ APP_ENV=test 면 이 파일을 읽지 않는다. 실키가 새면 테스트가 외부 API 를 때린다.
|
|
|
|
|
|
# ═══ 실행 ═══════════════════════════════════════════════
|
|
APP_ENV=local
|
|
# RELOAD=1 # uvicorn --reload (개발 컨테이너)
|
|
# SCHEDULER_ENABLED=1 # 배치 스케줄러 기동(기본 0). 다중 워커면 한 프로세스에서만 1
|
|
# WORKER_CONCURRENCY=1 # 워커가 동시에 물 잡 수
|
|
# ADMIN_API_PORT=9801 # 내부 API 포트 (admin/backend/main.py)
|
|
|
|
# ── 포트를 어디에 열 것인가 (compose 가 읽는다) ──
|
|
# ★ 내부 두 개를 0.0.0.0 으로 열면 앱을 가른 의미가 없다. 기본값이 127.0.0.1 인 이유다.
|
|
# API_BIND=0.0.0.0 # 사장님 API :9800 — 외부 공개
|
|
# ADMIN_API_BIND=127.0.0.1 # 내부 API :9801
|
|
# ADMIN_BIND=127.0.0.1 # 내부 운영 화면 :3002
|
|
# ADMIN_API_BASE_URL=http://localhost:9801 # 내부 화면이 부를 API 주소
|
|
|
|
|
|
# ═══ DB (비우면 config.local.toml 의 [MainDBConfig]) ═════
|
|
# compose 는 host.docker.internal 을 기본으로 넣는다 — DB 는 compose 밖이다.
|
|
# DB_HOST=127.0.0.1
|
|
# DB_PORT=5432
|
|
# DB_USER=postgres
|
|
# DB_PASSWORD=
|
|
# DB_NAME=web4ai_db
|
|
|
|
|
|
# ═══ JWT 서명 키 ════════════════════════════════════════
|
|
# ★ 도커 이미지는 시크릿을 굽지 않는다(config.local.toml 이 플레이스홀더 사본).
|
|
# 컨테이너로 띄울 땐 반드시 여기서 주입한다 — 비우면 공개된 플레이스홀더가 서명 키가 된다.
|
|
# 생성: python3 -c "import secrets; print(secrets.token_urlsafe(48))"
|
|
JWT_ACCESS_SECRET=
|
|
JWT_REFRESH_SECRET=
|
|
# JWT_ACCESS_EXPIRE_MIN=
|
|
# JWT_REFRESH_EXPIRE_DAY=
|
|
|
|
|
|
# ═══ 외부 API 키 ════════════════════════════════════════
|
|
# 비어 있으면 해당 어댑터만 비활성된다. 서버는 그대로 뜬다 — 부팅이 외부 계약에 묶이면 안 된다.
|
|
# 단가와 원가 상한($1/사이트)은 docs/API_USAGE.md.
|
|
|
|
# ── 한국관광공사 TourAPI (apis.data.go.kr) — fact 의 주력 공급원 ──
|
|
# https://www.data.go.kr 일반 인증키. 무료(개발계정 1,000건/일)
|
|
# ★ 디코딩된 키를 넣는다. 인코딩 키를 넣으면 %2B 등이 이중 인코딩된다.
|
|
# ★ TourAPI 는 자체 areaCode/sigunguCode 체계다 — 카카오 행정구역 코드와 다르다.
|
|
TOUR_API_KEY=
|
|
|
|
# ── 네이버 지역검색 (openapi.naver.com) — 동일 업소 검증 ──
|
|
# https://developers.naver.com/apps 검색 API
|
|
# ★ 제약: display 최대 5건 · telephone 이 빈 값 · 행정구역 코드 없음
|
|
# → 카카오보다 판정 근거가 약하다(AMBIGUOUS 가 늘어난다). 카카오 키가 나오면 그쪽이 우선.
|
|
NAVER_CLIENT_ID=
|
|
NAVER_CLIENT_SECRET=
|
|
|
|
# ── 카카오 로컬 (dapi.kakao.com) — 동일 업소 검증 · 주변 정보 ──
|
|
# https://developers.kakao.com > 내 애플리케이션 > 앱 키 > REST API 키 (2026-08-31 현재 미발급)
|
|
# ★ 무료 쿼터는 개발자 계정의 "첫 번째 활성 앱" 에만 붙는다.
|
|
# dev/stage/prod 앱을 따로 파면 하나만 무료다 — 앱을 나누기 전에 확인할 것.
|
|
# 초과 단가: 키워드/카테고리 검색 2원, 좌표 변환 0.5원 (키워드가 4배 비싸다)
|
|
KAKAO_REST_API_KEY=
|
|
|
|
# ── Gemini (Google AI Studio) — 사진 분류 · 소개문/FAQ · 붙여넣기 추출 ──
|
|
# https://aistudio.google.com/apikey
|
|
GEMINI_API_KEY=
|
|
# GEMINI_VISION_MODEL=gemini-3.7-flash
|
|
# GEMINI_TEXT_MODEL=gemini-3.7-flash
|
|
# VISION_CONFIDENCE_THRESHOLD=0.7 # 미만이면 자동 반영하지 않고 사람 확인 큐에 남긴다
|
|
|
|
# ── Perplexity Sonar — 채널 URL 발견 ──
|
|
# ★ 요청 단위 옵션으로만 켠다("추가 채널까지 AI로 찾기"). 서버 스위치는 없다 —
|
|
# env 로 일괄 활성화하면 재수집마다 사용자가 모르는 유료 검색이 반복된다.
|
|
# ★ 답변을 사실로 쓰지 않는다. URL 발견 전용 — 동명 업소가 섞이고 환각이 있다.
|
|
PERPLEXITY_API_KEY=
|
|
|
|
# Open-Meteo(날씨)는 키가 필요 없다. 좌표만 있으면 된다.
|
|
|
|
|
|
# ═══ 수집 ═══════════════════════════════════════════════
|
|
# 켤 어댑터 id(쉼표 구분). 배포 없이 채널 하나를 내릴 수 있게 코드가 아니라 env 다.
|
|
# 비우면 코드 기본값과 같다.
|
|
COLLECT_ADAPTERS=mock,naver_place,tour_api,static_html
|
|
|
|
# 네이버 플레이스 GraphQL 은 익명 요청을 429 로 막는다.
|
|
# 정상 보유한 세션 쿠키가 있으면 여기 둔다. ★ IP 회전·핑거프린트 위조는 하지 않는다.
|
|
NAVER_COOKIES=
|
|
|
|
|
|
# ═══ 발행 ═══════════════════════════════════════════════
|
|
# 커스텀 도메인이 없는 사이트의 공개 주소(https://<이 값>/s/<slug>).
|
|
# canonical·og:url·sitemap·IndexNow 가 전부 이 값을 쓴다.
|
|
# ★ origin 은 payload JSON 에 구워진다 — 바꾸면 프리렌더 재실행이 아니라 백엔드에서 재발행해야 한다.
|
|
SITE_PUBLIC_HOST=w4ai.o2o.kr
|
|
|
|
# 산출물 경로. 기본값이 컨테이너 경로라, compose 밖에서 돌릴 땐 여기를 덮어야 한다.
|
|
# SITE_PAYLOAD_DIR=/app/out/payloads # BUILD 잡이 payload JSON 을 떨어뜨릴 곳(프리렌더의 유일한 입력)
|
|
# SITE_OUTPUT_DIR=/app/out/sites # 프리렌더가 구운 정적 트리
|
|
# RENDER_TIMEOUT_SEC=180 # .status 보고서를 기다리는 시간
|
|
|
|
# ── 잡 큐 ──
|
|
# JOB_LEASE_SEC=120 # 리스 만료 = 죽은 워커의 잡을 회수하는 기준
|
|
# JOB_DEADLINE_SEC=900 # 이 시간을 넘기면 DEAD
|
|
|
|
# ── 색인 통보(IndexNow) — 네이버·Bing·Yandex ──
|
|
# 비우면 통보를 건너뛴다(발행은 정상). 구글은 IndexNow 미지원 → Search Console 사이트맵 제출.
|
|
# ★ 백엔드와 프리렌더가 같은 값이어야 한다. 프리렌더가 루트에 <key>.txt 를 굽고 검색엔진이
|
|
# 대조한다 — 어긋나면 403. 비밀이 아니다(공개되어야 작동한다).
|
|
# 생성: python3 -c "import uuid; print(uuid.uuid4().hex)"
|
|
INDEXNOW_KEY=
|
|
|
|
# ── Azure Blob 정적 발행 (지금은 끈다 — docs/ARCHITECTURE.md 3절) ──
|
|
# 연결 문자열이 비면 발행 잡이 업로드 단계를 통째로 건너뛴다. 서빙은 nginx 가 맡는다.
|
|
AZURE_STORAGE_CONNECTION_STRING=
|
|
# ★ 셸에서 export 할 땐 반드시 작은따옴표 — '$web'. 큰따옴표면 빈 문자열이 된다.
|
|
AZURE_STORAGE_CONTAINER=$web
|
|
# ★ 비워 둔다. HTML 이 /assets/… 를 루트 절대경로로 가리키는데 접두사를 쓰면 블롭은
|
|
# <접두사>/assets/… 에 놓여 CSS 가 404 다. 오리진 경로를 매핑하는 CDN 을 앞에 세운 뒤에만 채운다.
|
|
# (코드 기본값은 ai-for-web 이므로 빈 값을 명시적으로 적어 둔다)
|
|
AZURE_STORAGE_PREFIX=
|