네이버 쪽에는 창이 없었다. 서치어드바이저 소유확인이 안 붙어 사이트맵 제출·수집 요청·
진단을 쓸 수 없었고, 유일한 자동 통로인 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
48 lines
1.9 KiB
Python
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)
|