o2o-site-AEO/docs/SEARCH_CONSOLE_CLIENT.md
Mina Choi 3f47d5ecd2 [feat] solution/backend: 서치콘솔 자동 제출·색인 상태 추적 추가
사이트 발행 성공과 Google 색인 관측은 별도 상태다. 외부 API 장애로 발행이 실패하거나 재시작 때 추적 정보가 사라지지 않도록 분리.

- Google 클라이언트·배치·DB·Teams 알림 모듈 분리
- 기존 스케줄러 연결, 재시도·중복 실행 방지와 선택 설정 추가
- ORM·초기 DDL·마이그레이션·운영 설정 문서 동시 갱신

검증: 관련 59건 통과, compose 설정·diff 검사 통과. 추가 회귀 23건 통과, 기존 발행 검수 실패 1건은 변경 전 코드에서도 재현. 운영 배포·Google/Teams 실호출 미실행.
2026-09-15 14:50:30 +09:00

3.8 KiB

Search Console 클라이언트

solution/backend/services/search_console_client.py — Google Search Console 에 사이트맵을 제출하고 URL 색인 상태를 조회하는 REST 클라이언트만 다룬다. DB 저장·스케줄링·발행 감지·환경 설정은 SEARCH_CONSOLE.md를 따른다.

공식 문서: Sitemaps.submit · urlInspection.index.inspect

1. 계약

class SearchConsoleClient:
    def __init__(self, credentials_file: str, *, transport: httpx.AsyncBaseTransport | None = None): ...
    async def submit_sitemap(self, property_url: str, sitemap_url: str) -> None: ...
    async def inspect_url(self, property_url: str, page_url: str) -> dict: ...  # indexStatusResult 만
    async def aclose(self) -> None: ...
    # async with SearchConsoleClient(...) as client: ...
  • credentials_file: 서비스 계정 JSON 키 파일 경로.
  • transport: 테스트에서 httpx.MockTransport 를 꽂는 자리 — 실제 Google 호출 없이 검증한다.
  • inspect_url 은 응답의 inspectionResult.indexStatusResult 만 돌려준다. 그 경로가 없거나(검사 실패) 빈 dict 면(실제 검사가 안 된 응답) SearchConsoleError 를 올린다 — "미색인"으로 넘겨짚지 않는다.

2. 인증

서비스 계정 JSON 키 파일 + scope https://www.googleapis.com/auth/webmasters. google.oauth2.service_account · google.auth.transport.requests.Request · requests 는 전부 함수 안에서 import 한다. 두 패키지는 백엔드 requirements.txt에 포함되어 있다.

  • 토큰은 클라이언트 인스턴스에 캐시된다(credentials.valid 인 동안 재사용, 매 호출 갱신하지 않는다). 동시 호출은 asyncio.Lock 으로 갱신을 한 번만 태운다.
  • 갱신은 asyncio.to_thread 로 별도 스레드에서 돈다. 내부 requests.Session 요청에는 타임아웃을 강제로 20초로 덮어씌운다(Request.__call__ 기본값 120초를 무시) — 만료된 키·막힌 네트워크에서 무한정 걸리는 것을 막는다.

3. 오류 — SearchConsoleError(code)

code 문자열 하나만 들고 다닌다. Google 응답 본문·액세스 토큰·키 파일 내용·원본 예외 메시지는 절대 담지 않는다 — 로그·잡 상태·관리 화면 어디로 흘러도 안전하다.

code 뜻
invalid_credentials_file 키 파일을 못 읽거나 형식이 잘못됨
auth_failed 토큰 갱신 실패, 또는 갱신 후에도 토큰이 비어 있음
unauthorized HTTP 401
forbidden HTTP 403
rate_limited HTTP 429
server_error HTTP 5xx
http_<code> 그 외 실패 상태코드
timeout 요청 타임아웃
transport_error 그 외 전송 실패(연결 끊김 등)
invalid_json 200 인데 본문이 JSON 이 아님
missing_inspection_result 응답에 inspectionResult 가 없음
missing_index_status_result inspectionResult 는 있는데 indexStatusResult 가 없거나 빈 dict

4. 테스트

cd solution/backend
APP_ENV=test .venv/bin/python -m pytest tests/test_search_console_client.py --confcutdir=tests

--confcutdir=tests 가 필요한 이유: 저장소 루트 conftest.py 의 세션 스코프 autouse 픽스처가 실 Postgres 연결을 요구한다(solution/backend/conftest.py). 이 클라이언트 테스트는 DB 를 전혀 쓰지 않으므로 그 픽스처를 건너뛴다 — --confcutdir=tests 로 상위 conftest.py 탐색을 끊는다. (통합 후 전체 스위트를 돌릴 때는 이 플래그 없이 실행한다.)

Google 실 API 는 전부 httpx.MockTransport 로 막았다 — 네트워크 호출도, 과금도 없다.