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

72 lines
3.8 KiB
Markdown

# Search Console 클라이언트
`solution/backend/services/search_console_client.py` — Google Search Console 에
사이트맵을 제출하고 URL 색인 상태를 조회하는 REST 클라이언트만 다룬다.
DB 저장·스케줄링·발행 감지·환경 설정은 [SEARCH_CONSOLE.md](SEARCH_CONSOLE.md)를 따른다.
공식 문서: [Sitemaps.submit](https://developers.google.com/webmaster-tools/v1/sitemaps/submit) ·
[urlInspection.index.inspect](https://developers.google.com/webmaster-tools/v1/urlInspection.index/inspect)
## 1. 계약
```python
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. 테스트
```bash
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` 로 막았다 — 네트워크 호출도, 과금도 없다.