네이버 쪽에는 창이 없었다. 서치어드바이저 소유확인이 안 붙어 사이트맵 제출·수집 요청·
진단을 쓸 수 없었고, 유일한 자동 통로인 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
78 lines
3.8 KiB
Python
78 lines
3.8 KiB
Python
"""네이버 탐색 준비 상태를 **사람이 읽게** 찍는다.
|
|
|
|
레포 루트에서 (백엔드 venv 를 쓴다 — httpx·pydantic 이 거기 있다):
|
|
|
|
solution/backend/.venv/bin/python geo/scripts/check_naver_eo.py
|
|
solution/backend/.venv/bin/python geo/scripts/check_naver_eo.py --place "스테이,머뭄"
|
|
solution/backend/.venv/bin/python geo/scripts/check_naver_eo.py --slug butter
|
|
solution/backend/.venv/bin/python geo/scripts/check_naver_eo.py --json (도구에 물릴 때)
|
|
|
|
★ **컨테이너 안에서는 못 돈다.** `geo/` 는 백엔드 이미지에 들어가지 않는다 — 넣으려면
|
|
`solution/backend/Dockerfile` 을 고쳐야 하고, 백엔드 코드는 고치지 않기로 했다(2026-09-11).
|
|
서버에서도 레포 체크아웃 + 백엔드 venv 로 돌린다. 정기 실행이 필요해지면 `geo/Dockerfile`
|
|
로 자기 이미지를 갖는 것이 이 제약 아래서의 길이다(geo/README.md).
|
|
|
|
★ 판정은 여기서 하지 않는다 — `geo.naver.probe()` 가 한다. 이 파일은 그 결과를 줄로 바꾸는
|
|
일만 한다. 판정을 화면 코드에 두면 라우터가 붙을 때 기준이 둘로 갈린다.
|
|
|
|
★ 오리진 기본값은 `site_payload.publish_origin()` 이다. 발행 호스트를 새 env 로 또 두면
|
|
canonical 과 갈린다(AGENTS.md '발행 호스트는 두 곳에 있고 같아야 한다').
|
|
"""
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
# ★ 두 자리를 얹는다: 레포 루트(`geo` 패키지) + `solution/backend`(도메인 코드).
|
|
# admin/backend 가 PYTHONPATH 로 같은 일을 한다 — geo 도 백엔드 코드를 복제하지 않는다.
|
|
_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
for _path in (_ROOT, os.path.join(_ROOT, "solution", "backend")):
|
|
if _path not in sys.path:
|
|
sys.path.insert(0, _path)
|
|
# ★ APP_ENV=test 면 .env 를 읽지 않는다(실키가 테스트로 새는 경로를 막아 뒀다).
|
|
# 이 스크립트는 실제 네이버 API 를 부르므로 local 이어야 한다 — 다른 스크립트도 같다.
|
|
os.environ.setdefault("APP_ENV", "local")
|
|
|
|
from geo.naver import FAIL, OK, SKIP, WARN, probe # noqa: E402
|
|
from services.site_payload import publish_origin # noqa: E402
|
|
|
|
MARK = {OK: " ✓", WARN: " !", FAIL: " ✗", SKIP: " ·"}
|
|
|
|
|
|
async def main() -> int:
|
|
parser = argparse.ArgumentParser(description="네이버 탐색 준비 상태 점검")
|
|
parser.add_argument("origin", nargs="?", default=None, help=f"확인할 오리진(기본 {publish_origin()})")
|
|
parser.add_argument("--place", help="상호명 — 색인·역방향 링크 점검에 쓴다")
|
|
parser.add_argument("--slug", help="통보 경로를 확인할 사이트(기본: 사이트맵의 첫 사이트)")
|
|
parser.add_argument("--json", action="store_true", help="사람이 아니라 도구가 읽을 형태로")
|
|
args = parser.parse_args()
|
|
|
|
report = await probe(
|
|
args.origin or publish_origin(),
|
|
place_name=args.place,
|
|
slug=args.slug,
|
|
)
|
|
|
|
if args.json:
|
|
print(json.dumps(report.as_dict(), ensure_ascii=False, indent=2))
|
|
return 0 if report.ok else 1
|
|
|
|
print(f"[네이버 점검] {report.origin}\n")
|
|
for f in report.findings:
|
|
print(f"{MARK[f.status]} {f.label}" + (f" — {f.detail}" if f.detail else ""))
|
|
|
|
passed = [f for f in report.findings if f.status == OK]
|
|
print(f"\n[결과] 통과 {len(passed)} · 주의 {len(report.warned)} · 실패 {len(report.failed)}")
|
|
for title, rows in (("먼저 고칠 것", report.failed), ("확인이 필요한 것", report.warned)):
|
|
if not rows:
|
|
continue
|
|
print(f"\n{title}:")
|
|
for f in rows:
|
|
print(f" - {f.label}: {f.detail}" + (f"\n → {f.fix}" if f.fix else ""))
|
|
return 0 if report.ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(asyncio.run(main()))
|