네이버 쪽에는 창이 없었다. 서치어드바이저 소유확인이 안 붙어 사이트맵 제출·수집 요청·
진단을 쓸 수 없었고, 유일한 자동 통로인 IndexNow 는 조용히 0건이었다 —
indexnow.py 가 읽는 <out>/s/<slug>/sitemap.xml 을 프리렌더가 더는 굽지 않는데
(사이트 한 장 → 루트 사이트맵 통합) 발행 잡은 경고 한 줄만 남기고 성공한다.
조사 결과 AI 브리핑 출처는 네이버 생태계 편향이라, 네이버에서의 목표를 "인용" 이 아니라
"플레이스↔홈페이지 결합 + 웹문서 검색 노출" 로 다시 잡았다(docs/NAVER_EO.md).
- geo/: solution·admin 을 고치지 않고 import 만 하는 최상단 모듈. 밖에서 HTTP 로만 본다
- naver/checks.py: 소유확인(상태코드가 아니라 내용 — SPA 폴백이 200 을 준다) · Yeti 랜딩 ·
통보 URL 재현 · 웹문서 색인(근사) · 스마트플레이스 역방향 링크
- naver/robots.py: 네이버 관점 판정 — Yeti·Daumoa · 사이트맵 지시 · JS/CSS 자산 차단
(RFC 9309 그룹 경계: 규칙 뒤의 User-agent 는 새 그룹)
- naver/notify.py: 루트 사이트맵에서 주소를 골라 IndexNow 통보. 백엔드가 고쳐지는 날
GEO_NOTIFY_ENABLED=0 으로 끈다(담당 중복 = 429)
- scripts/preflight.py(발행 전·오리진) · postflight.py(발행 후·200 확인 뒤에만 통보) ·
watch.py(사이트맵 lastmod 변화만). 상태는 성공분만 geo/state/ 에 기록
- naver/web_search.py: 웹문서검색 호출기 — 백엔드를 못 고쳐 여기 있다. 쿼터 카운터가 둘로 갈린다
- nginx/site.conf.example: 소유확인 location = 블록(주석). 메타태그는 solution/frontend 수정이라 제외
- .env.example: NAVER_SITE_VERIFICATION · GEO_NOTIFY_ENABLED · GEO_STATE_DIR
- docs: NAVER_EO.md(조사·설계) · AGENTS·README·ARCHITECTURE 4절·DEPLOY 2-2·DEVLOG
가짜 사이트맵·IndexNow 서버로 통보 7시나리오(slug 경계·dry-run·중복 없음·lastmod 변경분·
비200 미통보) · preflight 정상/고장 · robots 판정 · 소유확인 4분기 통과.
실도메인·pytest 는 미실행(.venv·.env 없음). solution/·admin/ 무변경.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8SMKqBu9N723AxVBJhACW
107 lines
6.3 KiB
Plaintext
107 lines
6.3 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=
|
|
# 네이버 서치어드바이저 소유확인 토큰 = 준 파일명에서 `.html` 을 뺀 값(예: naver1234abcd).
|
|
# ★ 네이버는 DNS TXT 를 안 받는다 — 구글처럼 DNS 로 끝낼 수 없다.
|
|
# ★ **실제로 내주는 곳은 `nginx/site.conf`** 다(주석 처리된 블록을 풀어 쓴다).
|
|
# 여기 값은 "우리가 등록한 것", nginx 값은 "실제로 나가는 것" — **두 곳이다.**
|
|
# nginx 가 env 를 못 읽고 site.conf 는 .example 만 커밋되기 때문이고, 어긋나면 아래 점검이
|
|
# 잡는다. 그게 두 곳을 감수하는 근거다(geo/README.md '제약').
|
|
# ★ 프론트 재빌드는 필요 없다 — 번들에 안 들어간다. nginx reload 로 끝난다.
|
|
# ★ 등록 안 하면 사이트맵 제출·수집 요청·색인 진단을 아예 쓸 수 없다(docs/DEPLOY.md 2-2단계).
|
|
# 확인(레포 루트에서): solution/backend/.venv/bin/python geo/scripts/check_naver_eo.py
|
|
NAVER_SITE_VERIFICATION=
|
|
|
|
# ★ 알리기(IndexNow 통보)를 geo 가 맡는다 — 백엔드 쪽이 고장 나 0건이기 때문이다.
|
|
# ⚠️ **담당이 두 곳이 되면 안 된다.** 백엔드 indexnow 를 고치는 날 여기를 0 으로 끈다.
|
|
# (그대로 두면 같은 URL 이 두 번 나가고 429 대상이 된다)
|
|
# 실행: geo/scripts/postflight.py <slug> · 자동: geo/scripts/watch.py
|
|
GEO_NOTIFY_ENABLED=1
|
|
# geo 가 "무엇을 이미 알렸나" 를 두는 자리. 비우면 geo/state/ 다(git 에 안 올라간다).
|
|
GEO_STATE_DIR=
|
|
# 미발급. 없으면 네이버 지역검색을 쓴다
|
|
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=
|