o2o-site-AEO/.env.example
hbyang 55ee968f9d [fix] deploy: 앱이 부르는 API 주소를 한 오리진으로 — :9800 기본값이 로그인을 막았다
nginx/site.conf 는 /v1 을 같은 오리진으로 프록시하고 주석에도 "앱과 같은 오리진이라
프리플라이트가 아예 발생하지 않는다" 고 적혀 있는데, compose 빌드 인자 기본값이
VITE_API_BASE_URL=http://localhost:9800 이었다. :80 으로 앱을 열면 번들이 :9800 을 부르므로
스스로 크로스 오리진이 되고, CLIENT_URL 기본값(3000~3005)에 http://localhost 가 없어
로그인만 실패한다.

증상이 사람을 속인다 — 서버는 200 에 토큰까지 내려보내고 브라우저가 allow-origin 이 없어
그 응답을 버리므로, 화면에는 "로그인에 실패했습니다" 만 뜬다. 비밀번호를 의심하게 된다.

- docker-compose.yml · .env.example: 기본값을 앱과 같은 오리진(http://localhost)으로.
  CORS 를 허용해 뚫는 게 아니라 크로스 오리진을 만들지 않는다
- .env.example: PUBLIC_API_BASE_URL 을 주석이 아니라 값으로 내놨다 — 주석으로 두면
  compose 기본값이 이기고, 그 기본값이 문제였다

검증: down -v 후 up -d --build → 번들의 localhost:9800 참조 0건 ·
POST http://localhost/v1/auth/login 200(프리플라이트 없음) · 프리렌더 재굽기 2건 ·
/ · /s/ · /s/<slug> 전부 200.
2026-09-07 16:19:34 +09:00

89 lines
4.8 KiB
Plaintext

# cp .env.example .env 후 값을 채운다. .env 는 커밋되지 않는다.
# 우선순위: 실제 환경변수(compose) > .env > 코드 기본값(config_models.py)
#
# ★ 값 뒤에 주석을 붙이지 않는다. compose 의 `env_file` 은 줄 끝 주석을 **값으로 읽는다** —
# `AZURE_STORAGE_CONNECTION_STRING= # 비우면...` 은 "빈 값"이 아니라 "# 비우면..." 이라는 값이다.
# 실측(2026-09-07): 그래서 Azure 를 끈 로컬에서 발행 잡이 업로드를 시도하고
# "Connection string is either blank or malformed" 로 죽었다. 게이트는 통과했는데 발행만 실패한다.
# 주석은 반드시 **윗줄**에 둔다.
# ── 공통 solution/backend · admin/backend (server_configs 가 읽는다)
APP_ENV=local
# ★ compose 로 띄우면 `host.docker.internal` 이다 — 컨테이너 안의 127.0.0.1 은 그 컨테이너다.
# 127.0.0.1 은 백엔드를 **네이티브로**(.venv/bin/python) 돌릴 때만 맞다.
# 이 값을 그대로 두고 `docker compose up` 하면 API 는 healthz 200 으로 멀쩡해 보이는데
# 워커만 조용히 재시작을 반복한다(ConnectionRefusedError 5432) — 발행 잡이 영원히 안 돈다.
DB_HOST=host.docker.internal
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=
DB_NAME=web4ai_db
# 비우면 토큰 서명이 안 된다
# 생성: python -c "import secrets; print(secrets.token_urlsafe(48))"
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=
# 키가 비면 그 어댑터만 꺼진다. 서버는 뜬다.
PERPLEXITY_API_KEY=
# Perplexity 채널 발견. 0=끔(기본)
COLLECT_USE_PERPLEXITY=0
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
# 미발급. 없으면 네이버 지역검색을 쓴다
KAKAO_REST_API_KEY=
GEMINI_API_KEY=
# 디코딩된 키(인코딩 키는 이중 인코딩된다)
TOUR_API_KEY=
# 구글 로그인. 비우면 구글 로그인만 꺼진다(서버는 뜨고, 화면에 버튼도 안 뜬다).
# Google Cloud Console > API 및 서비스 > 사용자 인증 정보 > OAuth 2.0 클라이언트 ID(웹 애플리케이션)
# "승인된 JavaScript 원본" 에 화면 주소를 등록해야 브라우저에서 토큰이 나온다(리디렉션 URI 는 필요 없다).
# ★ 백엔드(aud 대조)와 프론트(버튼)가 **같은 값**을 써야 한다 — compose 가 이 하나를
# VITE_GOOGLE_CLIENT_ID 로 흘려보낸다. 두 곳에 따로 적지 않는다.
# ★ 바꾸면 프론트를 다시 구워야 한다: ./deploy.sh solution-site
GOOGLE_CLIENT_ID=
# CORS 허용 오리진. 쉼표로 여럿.
# 서버에 올리면 반드시 적는다. 안 적으면 화면은 뜨고 API 만 막힌다.
# CLIENT_URL=http://172.30.1.36:30031,http://localhost:3002
# LANDING_URL=
# ── solution/frontend 브라우저가 부르는 주소 (compose 가 VITE_* 로 주입)
# ★ 브라우저가 부르는 주소다. 서버에 올리면 localhost 는 즉시 틀린다.
# ★ **앱과 같은 오리진을 적는다.** nginx(:80)가 /v1 을 같은 오리진으로 프록시하므로
# (nginx/site.conf) 앱이 부를 주소는 `:9800` 이 아니라 앱 주소 그 자체다. `:9800` 을 적으면
# 스스로 크로스 오리진을 만들어 CORS 가 붙고, 화면은 뜨는데 **로그인만 계속 실패한다** —
# 서버는 200 에 토큰까지 내려보내고 브라우저가 allow-origin 이 없어 그 응답을 버린다.
# 실측(2026-09-07): 이 기본값 그대로 띄우면 :80 으로 연 앱에서 로그인이 안 된다.
# ★ 값을 바꾸면 번들을 다시 구워야 한다: ./deploy.sh solution-site
PUBLIC_API_BASE_URL=http://localhost
PUBLIC_WEB_BASE_URL=http://localhost
# ── solution/site 발행물 — solution/backend 도 같이 본다
# canonical·og:url·sitemap·IndexNow 가 전부 SITE_PUBLIC_HOST 를 쓴다.
# 로컬은 비워 둔다(기본값 localhost). 서버에 올릴 때만 실제 도메인을 적는다.
# SITE_PUBLIC_HOST=web4ai.o2osolution.ai
# 비우면 색인 통보를 건너뛴다(발행은 정상)
INDEXNOW_KEY=
# 비우면 로컬 발행만 한다
AZURE_STORAGE_CONNECTION_STRING=
AZURE_STORAGE_CONTAINER=
AZURE_STORAGE_PREFIX=
# ── compose 포트 매핑
# 비우면 로컬 기본값. 서버 값은 docs/SERVERS.md
# SITE_HTTP_PORT=80 # 발행 사이트
# WEB_PORT=3000 # 사장님 앱
# API_PORT=9800 # 사장님 API
# ADMIN_PORT=3002 # 내부 화면 (bind 127.0.0.1)
# ADMIN_API_PORT_PUBLIC=9801 # 내부 API (bind 127.0.0.1)
# 자동 로그인 — 위저드 앞에 로그인 화면을 세우지 않으려고 세션을 미리 잡는다.
# ⚠️ 이 값은 **프론트 번들에 구워진다.** 페이지를 연 사람은 누구나 JS 에서 읽는다 —
# 내부 테스트 호스트에서만 채우고, 사장님에게 여는 순간 비운다(lib/autoSession.ts).
# ★ 바꾸면 재빌드해야 한다: ./deploy.sh solution-site
AUTO_LOGIN_ID=
AUTO_LOGIN_PW=