docs(realestate): naver-fetch 워커 설계 스펙(가정 IP 네이버 fetch→NAS ingest, trade-monitor 동형)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
This commit is contained in:
2026-07-09 23:59:51 +09:00
parent cdc309150e
commit e09e11be7b

View File

@@ -0,0 +1,144 @@
# 네이버 매물 fetch 워커 (naver-fetch) — 설계
> 상태: **설계 승인됨(2026-07-09) → 구현 계획(writing-plans) 대기**
> 관련: [[2026-07-09-매물알림-안전마진-design]](매물 파이프라인 본체), `infra_distributed_workers` 메모리(워커↔NAS 계약·관측)
## 1. 배경 / 목표
매물 알림 파이프라인(2026-07-09 배포)에서 **국토부 실거래(합법 baseline)는 정상**이나, **네이버 호가 매물 수집은 NAS(datacenter IP)에서 `ReadTimeout/429`로 차단**되어 자동 매물 유입이 막혀 있다. → 네이버 fetch를 **Windows AI 머신(192.168.45.59, 가정 IP)**의 신규 워커에서 수행해 NAS로 push한다. 기존 render/trade-monitor 워커와 **동형 패턴**.
**성공 기준**: 워커가 대상 동의 네이버 호가 매물을 주기적으로 fetch → NAS가 upsert→매칭→(안전마진 판정 포함)→신규 매물 텔레그램 알림. 워커는 `/infra`에서 관측 가능. 네이버가 일시 차단돼도 실거래·안전마진·청약 무중단.
## 2. 비목표 (Out of Scope)
- 네이버 인증 토큰 획득 전략(SPA Bearer JWT 등) — **web-ai 워커의 구현·병렬 spike**로 확인(BE 계약과 독립).
- 단지(complex)별 심층 크롤·과거 backfill — MVP는 동(법정동)별 article list만.
- 안전마진/적정성 판정 로직 — 이미 존재·불변(`listing_matcher`).
- `market_deals` 수집 성능 개선(행별 개별 `_conn` upsert가 대량시 느림) — 별개 후속(관측됨, §9 참조).
## 3. 아키텍처 (3-repo 분담)
```
[web-ai: naver-fetch 워커] (Windows 192.168.45.59, WSL2 docker, 가정 IP)
자체 스케줄 루프(2~3h, 낮 시간):
1. GET NAS /api/internal/realestate/targets (X-Internal-Key)
→ {dongs:[{dong, cortar_no}], deal_types, page_limit}
2. 네이버 new.land.naver.com/api/articles?cortarNo=<10자리>&... 동별 fetch
(인증 토큰 확보는 워커 책임 — 병렬 spike)
3. POST NAS /api/internal/realestate/listings-ingest (X-Internal-Key)
{fetched_at, batches:[{dong, articles:[<raw 네이버 article>...]}]}
4. worker:naver-fetch:heartbeat (EX45, kind=fetcher, state=idle/fetching)
[web-backend: NAS realestate-lab] (BE 담당)
- GET /api/internal/realestate/targets → criteria.dongs + cortar_no 매핑 + criteria.deal_types
- POST /api/internal/realestate/listings-ingest
→ 각 article _parse_naver_article(기존·검증) + dong 태깅 → upsert_listing
→ run_listing_matching() + notify_new_listings() (기존 재사용)
→ {received, new, matched}
- node_monitor.WORKER_REGISTRY += naver-fetch(kind=fetcher)
- nginx: /api/internal/realestate/ → realestate-lab:8000 (IP 화이트리스트)
- collect_listings의 _fetch_naver_all deprecated (MOLIT 실거래 cron은 유지)
[web-ui: /infra] (FE 담당, 소규모)
- fetcher kind 노드 시각(statusVisual 색·라벨)
```
**설계 원칙**: 워커는 **무상태 얇은 프록시**("네이버 IP차단 우회 fetch"만). 파싱·dedup·매칭·알림·상태는 전부 NAS. 파싱은 NAS의 검증된 `_parse_naver_article` 한 곳에서만 유지(네이버 포맷 변경 시 BE만 수정).
## 4. 계약 (BE 소유 — cross-repo lock)
### 4.1 `GET /api/internal/realestate/targets` (X-Internal-Key)
워커가 "무엇을 긁을지" pull.
```json
{
"dongs": [
{"dong": "신대방동", "cortar_no": "1159010100"},
{"dong": "봉천동", "cortar_no": "1162010100"}
],
"deal_types": ["전세", "반전세", "매매"],
"page_limit": 2
}
```
- `dongs`: `listing_criteria.dongs` 각각에 대해 `NAVER_CORTAR[dong]`(10자리 법정동코드) 조인. 매핑 없는 동은 제외(+경고 로그).
- `deal_types`: `listing_criteria.deal_types`.
- `page_limit`: 동당 fetch 페이지 수 상한(기본 2). 상수/env.
- 인증 실패(키 불일치/누락) → 401.
### 4.2 `POST /api/internal/realestate/listings-ingest` (X-Internal-Key)
워커가 네이버 raw 매물 push.
```json
{
"fetched_at": "2026-07-09T05:44:23Z",
"batches": [
{"dong": "신대방동", "articles": [ { /* articleList item raw */ }, ... ]},
{"dong": "봉천동", "articles": [ ... ]}
]
}
```
- 각 batch의 각 article → `_parse_naver_article(article)``d["dong"] = batch.dong``upsert_listing(d)`. article별 try/except(불량 1건 skip).
- 전체 upsert 후 `run_listing_matching()` + `notify_new_listings()` 호출(신규 매물만 알림, 멱등).
- 응답: `{"received": N, "new": M, "matched": K}` (received=처리 article 수, new=신규 upsert 수, matched=passed match 수).
- 인증 실패 → 401. `batches` 비면 no-op `{received:0,new:0,matched:0}`.
- ⚠️ MVP는 인라인 처리(매물 수 수백 규모). 대량 시 upsert만 동기+매칭/알림 BackgroundTask로 분리 여지(§9).
### 4.3 cortarNo 매핑 (BE)
`app/lawd_codes.py``NAVER_CORTAR: dict[str,str]`(동명→**10자리 법정동코드**) 추가. 현재 `LAWD_CODES`(5자리 시군구)는 MOLIT 전용으로 유지 — 네이버는 10자리 필요(기존 `_fetch_naver_all`이 5자리를 넘기던 버그를 이 매핑으로 해소). 초기 대상 6동:
| 동 | 구 | 10자리(구현 시 행정표준코드로 검증) |
|---|---|---|
| 신대방동 | 동작구 | 1159010100 |
| 대방동 | 동작구 | 1159010200 |
| 상도동 | 동작구 | 1159010600 |
| 봉천동 | 관악구 | 1162010100 |
| 대림동 | 영등포구 | 1156013600 |
| 신길동 | 영등포구 | 1156013200 |
> 위 코드는 구현 시 행정표준코드관리시스템으로 최종 검증(법정동 통폐합 반영).
### 4.4 heartbeat (워커→Redis, 관측 계약)
`worker:naver-fetch:heartbeat` EX45, 값 JSON:
```json
{"name":"naver-fetch","kind":"fetcher","state":"idle|fetching","ts":<epoch>,
"last_fetch_at":<epoch|null>,"listings_pushed":<int>,"errors":<int>}
```
- `kind="fetcher"`는 신규(기존 render/watcher/trader에 추가). state 의미: idle=대기, fetching=수집중.
## 5. 관측 (팀 규칙 3단계 — `infra_distributed_workers`)
1. **워커 heartbeat** (§4.4) — web-ai.
2. **BE 등재**: `agent-office/app/node_monitor.py``WORKER_REGISTRY += {"name":"naver-fetch","kind":"fetcher","queue":None}`. `collect_status()``fetcher` kind를 처리(heartbeat 유무→alive/dead, link type=`http-pull`(ai_trade/trade-monitor와 동일, redis-queue 아님)). 다운/복구 텔레그램 경보는 기존 1분 cron 재사용(`_notified` 패턴).
3. **FE**: `/infra` `statusVisual``fetcher` 라벨·색 + http-pull 링크. (web-ui, 소규모)
## 6. 인증 / 보안
- **nginx**: 신규 location `/api/internal/realestate/``realestate-lab:8000`, **IP 화이트리스트**(기존 render 워커 화이트리스트에 쓰는 Windows 워커 IP 재사용). (nginx-conf 변경 → 배포)
- **앱 레벨**: realestate-lab이 `X-Internal-Key` 검증(env `REALESTATE_INTERNAL_KEY`). 다른 서비스 `internal_router` 패턴 준용. GET targets·POST ingest 둘 다 검증.
- 워커는 `.env`로 키 주입(커밋 금지).
## 7. 에러처리 / 멱등성
- 워커: 네이버 차단/토큰만료 → 사이클 skip, 다음 폴링 재시도. heartbeat는 계속 발신(생사 가시성). errors 카운트 증가.
- NAS ingest: article별 try/except(1건 실패 격리). 멱등 — `listings.article_no` UNIQUE upsert + `listing_matches.notified_at` → 같은 매물 재push해도 재알림 없음.
- NAS `collect_listings`: `_fetch_naver_all` 미호출로 변경(MOLIT만). `naver_ok`/naver diag는 collect_log에서 의미 상실 → MOLIT 전용 로그로 정리(컬럼은 nullable 유지, 하위호환).
## 8. 테스트 (BE)
- `test_internal_targets`: criteria seed 후 GET → dongs에 cortar_no 조인, deal_types 반환. 매핑 없는 동 제외.
- `test_internal_ingest`: raw 네이버 article batch POST → upsert_listing 호출 + `run_listing_matching`/`notify_new_listings` 호출(mock) + `{received,new,matched}`. 멱등(재POST시 new=0).
- `test_internal_auth`: X-Internal-Key 없거나 틀리면 401.
- `test_naver_cortar_mapping`: `NAVER_CORTAR` 10자리·대상 6동.
- 워커 테스트는 web-ai(별도).
## 9. 후속 / 알려진 개선점 (비차단)
- `market_deals`/`listings` upsert가 행별 개별 `_conn()` 커밋 → 대량 시 Celeron에서 느림(관측: 매매 승인 후 collect 수 분). 배치 트랜잭션(단일 `_conn` executemany)로 개선 여지.
- 네이버 인증 토큰 만료·갱신 자동화(워커, web-ai).
- page_limit·수집 주기 튜닝(네이버 rate-limit 관측 후).
## 10. 의존성 / 분담 요약
| repo | 작업 | 세션 |
|---|---|---|
| web-backend | targets/ingest 엔드포인트, X-Internal-Key, NAVER_CORTAR 매핑, node_monitor fetcher 등재, nginx `/api/internal/realestate/`, `_fetch_naver_all` deprecate | **BE(이 세션)** |
| web-ai | naver-fetch 워커(폴링+네이버 fetch+토큰+push+heartbeat) + Dockerfile `_shared` 포함 확인 | AI (병렬 spike 포함) |
| web-ui | `/infra` fetcher 노드 시각 | FE (소규모) |
계약(§4)은 co-gahusb로 잠그고 3세션 병렬. 계약 변경 시 web-ai+web-backend(+web-ui) 동시 수정.