o2o-site-AEO/geo/state.py
민헌 76d51207c6 [feat] geo,docs,nginx: 네이버 탐색 모듈 geo 추가 — 발행 전/후 점검 · IndexNow 알리기 대행
네이버 쪽에는 창이 없었다. 서치어드바이저 소유확인이 안 붙어 사이트맵 제출·수집 요청·
진단을 쓸 수 없었고, 유일한 자동 통로인 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
2026-09-14 10:17:28 +09:00

48 lines
1.9 KiB
Python

"""무엇을 이미 알렸는지 기억한다 — **DB 가 아니라 파일이다.**
★ 표를 만들지 않은 이유: 이 모듈은 `solution/backend` 를 고치지 않는다는 제약 아래 있고
(geo/README.md '제약'), 스키마는 `init.sql` 과 마이그레이션 **양쪽**을 고쳐야 한다.
그리고 여기 담기는 건 "마지막으로 통보한 시각" 하나라, 표가 필요해지는 종류가 아니다.
추이·리포트가 요구사항이 되면 그때 표를 정한다.
★ 잃어도 치명적이지 않게 설계한다. 파일이 사라지면 **전부 다시 통보**할 뿐이다 —
같은 URL 을 다시 알리는 건 규격상 정상이고(변경 통보), 손해는 호출 몇 번이다.
반대로 "안 알린 것을 알렸다고 기억" 하는 쪽이 위험해서, **통보 성공 뒤에만 기록한다.**
★ 자리는 `GEO_STATE_DIR`. 기본은 `geo/state/` 이고 git 에 올리지 않는다.
"""
import json
import os
from pathlib import Path
STATE_DIR_ENV = "GEO_STATE_DIR"
def state_dir() -> Path:
raw = os.environ.get(STATE_DIR_ENV, "").strip()
return Path(raw) if raw else Path(__file__).resolve().parent / "state"
def _path(name: str) -> Path:
return state_dir() / f"{name}.json"
def load(name: str) -> dict:
path = _path(name)
if not path.is_file():
return {}
try:
return json.loads(path.read_text("utf-8"))
except (OSError, ValueError):
# 깨진 파일 때문에 통보가 멈추면 안 된다 — 빈 상태로 보고 다시 알린다.
return {}
def save(name: str, data: dict) -> None:
path = _path(name)
path.parent.mkdir(parents=True, exist_ok=True)
# 같은 디렉토리에 쓰고 바꿔치기한다 — 쓰는 중에 죽어도 반쪽 파일이 남지 않는다.
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), "utf-8")
tmp.replace(path)