8.5 KiB
naver-fetch 워커 — 구현 설계
2026-07-10 · web-ai 소유. realestate 매물 알림 파이프라인의 가정-IP fetch 워커. 권위 계약(원본 스펙): web-backend repo
realestate-lab/docs/superpowers/specs/2026-07-09-naver-listing-worker-design.md§4. 이 문서는 그 계약을 Windows Docker 워커로 구현하기 위한 구현 설계.
1. 역할 · 경계
네이버가 NAS(데이터센터) IP를 429/ReadTimeout으로 막기 때문에, 가정 IP(LAN 192.168.45.x)에서 네이버 매물을 fetch해 NAS로 전달하는 얇은 프록시 워커. trade-monitor·render 워커와 동일한 FastAPI + asyncio 관례. WSL2 Docker.
책임
- 2~3h 주간 주기로 NAS
targets조회(감시 동 목록). - Playwright로 네이버 Bearer 토큰 획득 → httpx로 동별 매물 fetch(페이지네이션).
- raw article을 그대로 NAS
listings-ingest로 전달(파싱·매칭·알림은 NAS). - Redis heartbeat(
worker:naver-fetch:heartbeatEX45, kind=fetcher). - 토큰/네트워크 오류를 사이클·동 단위로 격리.
경계 밖(안 함): article 파싱, 매물 매칭, 텔레그램 발송(전부 NAS), dedup.
Spike 검증 완료 (2026-07-10)
- Playwright 헤드리스 →
new.land.naver.com로드 →/api/articlesXHR의 Bearer JWT 토큰 인터셉트 성공. - 인터셉트 토큰+쿠키를 httpx로 재사용 →
GET /api/articles?cortarNo=1159010100&...HTTP 200, articleList 20건(isMoreData 페이지네이션). 토큰 있으면 이 LAN에서 429 회피됨. - raw article 키:
articleNo, articleName, tradeTypeName, dealOrWarrantPrc, areaName, buildingName, floorInfo등 → NAS로 그대로 전달.
2. 모듈 분해
| 파일 | 책임 | 인터페이스 |
|---|---|---|
main.py |
FastAPI + lifespan. fetch_loop + heartbeat_loop 스폰, /health |
부작용 |
fetch_worker.py |
FetchState, run_cycle, fetch_loop, heartbeat_loop(전용) |
부작용 |
nas_client.py |
get_targets() / post_ingest(fetched_at, batches) (X-Internal-Key + retry) |
부작용(HTTP) |
naver_client.py |
NaverClient: acquire_token()(Playwright) + fetch_articles(cortar_no, page)(httpx) |
부작용(브라우저/HTTP) |
config.py |
Settings — env 로드 |
순수 |
순수 로직이 적은 워커(얇은 프록시)라 파싱은 없음. 테스트는 HTTP mock(respx) + fetch_worker 조립 중심.
3. 데이터 흐름 (run_cycle)
state.state = "fetching"
await naver.acquire_token() # Playwright 1회 (사이클당)
tg = await nas.get_targets() # {dongs:[{dong,cortar_no}], deal_types, page_limit}
batches = []
for d in tg.dongs: # 동 단위 try/except 격리
articles = []
for page in 1..tg.page_limit:
resp = await naver.fetch_articles(d.cortar_no, page) # httpx GET
articles += resp.get("articleList", [])
if not resp.get("isMoreData"): break
batches.append({"dong": d.dong, "articles": articles})
r = await nas.post_ingest(fetched_at=iso_utc_now, batches=batches) # {received,new,matched}
state.listings_pushed += sum(len(b["articles"]) for b in batches)
state.last_fetch_at = epoch_now
state.state = "idle"
⚠️ 시간 포맷 2종: ingest
fetched_at은 ISO UTC 문자열(스펙 §4 ingest 스키마, 예2026-07-09T05:44:23Z), heartbeatts/last_fetch_at은 epoch 정수(스펙 §4.4). 혼동 주의.
heartbeat_loop(별도 15초)이 FetchState를 읽어 §4.4 페이로드 발신.
4. naver_client (Playwright 토큰 + httpx fetch)
async acquire_token():async_playwright로 chromium 헤드리스 기동 →new.land.naver.com/houses?ms=...(설정NAVER_TOKEN_URL) 로드 →page.on("request")로 첫Authorization: Bearer ...헤더 캡처 →self._token,self._cookies(context.cookies) 저장 → 브라우저 종료. 실패 시 예외(사이클 skip).- 토큰 수명 정책: 사이클마다 fresh 획득(2~3h당 1회). JWT 만료 무관, 캐시/갱신 로직 없음(단순).
async fetch_articles(cortar_no, page) -> dict: httpx GEThttps://new.land.naver.com/api/articles?cortarNo=<cortar_no>&order=rank&realEstateType=<NAVER_REAL_ESTATE_TYPE>&tradeType=&...&page=<page>with headers{authorization: self._token, user-agent, referer: https://new.land.naver.com/, accept, cookie: self._cookies}. 200 아니면 예외. raw JSON dict 반환.async close(): httpx client 정리.
얇은 프록시: tradeType 비움(전체 유형) + realEstateType 설정값(기본 APT:ABYG:JGC:PRE). deal_types 필터는 NAS 담당(targets의 deal_types는 워커가 그대로 두고 NAS가 매칭 시 사용). 추후 네이버 쿼리 최적화 여지.
5. heartbeat (§4.4 — 전용 스키마)
worker:naver-fetch:heartbeat EX45. trade-monitor의 _shared.build_payload(ISO ts + jobs_done/jobs_failed)와 스키마가 달라 전용 emit 작성:
{"name":"naver-fetch","kind":"fetcher","state":"idle|fetching",
"ts":<epoch int>,"last_fetch_at":<epoch int|null>,"listings_pushed":<int>,"errors":<int>}
- 15초 독립 태스크(주기 fetch가 2~3h라 TTL45 유지 위해 필수).
FetchState를 읽어 발신. - BE가 node_monitor.WORKER_REGISTRY에
naver-fetch(kind=fetcher)로 등재 완료 → 발신 시/nodes·/infra에서 alive.
6. 스케줄 (fetch_loop)
- env
FETCH_INTERVAL_HOURS(기본 2.5),DAYTIME_START/DAYTIME_END(기본 08:00/20:00 KST). - 루프: ~5분마다 판정 — 주간창 안 && (now - last_run) ≥ interval 이면
run_cycle실행 후last_run=now. 창 밖이면 idle(state=idle, fetch 안 함). - 최상위 try/except로 루프 불사(어떤 예외도 죽이지 않음).
7. 설정 (env)
| env | 기본값 | 용도 |
|---|---|---|
NAS_BASE_URL |
http://192.168.45.54:18500 |
targets/ingest |
INTERNAL_API_KEY |
(필수) | X-Internal-Key (render 워커 공유키) |
REDIS_URL |
redis://192.168.45.54:6379 |
heartbeat |
FETCH_INTERVAL_HOURS |
2.5 |
fetch 주기 |
DAYTIME_START / DAYTIME_END |
08:00 / 20:00 |
주간창(KST) |
NAVER_REAL_ESTATE_TYPE |
APT:ABYG:JGC:PRE |
네이버 realEstateType |
NAVER_TOKEN_URL |
(기본 상수) | Playwright 토큰 획득용 네이버 URL |
NAVER_HEADLESS |
1 |
Playwright headless |
8. 에러 처리
- 토큰 획득 실패: 사이클 skip,
errors++, state=idle, 다음 주기 재시도. - targets 실패: 사이클 skip,
errors++. - 동별 fetch 실패: 해당 동 skip(warning), 나머지 동 계속.
- ingest 실패: 로그 error,
errors++(다음 주기 재fetch). - 루프 최상위 try/except.
9. 테스트 (pytest, 시스템 Python)
| 파일 | 검증 |
|---|---|
test_config.py |
env 기본/override |
test_nas_client.py |
respx — targets 파싱, ingest 페이로드/응답, X-Internal-Key, retry |
test_naver_client.py |
respx — fetch_articles URL·헤더(authorization/cookie)·articleList 파싱·비200 예외. (Playwright acquire_token은 실브라우저 통합이라 단위 mock: 토큰 주입 후 fetch 경로만) |
test_fetch_worker.py |
mock: 주간창 게이트(창 밖 skip), run_cycle 동별 조립·batches, 동 fetch 실패 격리, listings_pushed/last_fetch_at 갱신, heartbeat 필드(epoch ts/kind=fetcher) |
10. 배포
Dockerfile: Playwright 베이스(mcr.microsoft.com/playwright/python또는 insta-render 관례) + chromium,COPY _shared /app/_shared(빌드 컨텍스트.),COPY naver-fetch/. /app/,PYTHONPATH=/app, uvicorn:8000.services/docker-compose.yml:naver-fetch서비스, 포트 18716,TZ=Asia/Seoul, INTERNAL_API_KEY/REDIS/NAS env, healthcheck/health.- 엔드포인트는 nginx
/api/internal/realestate/(IP 화이트리스트, render 워커 IP 재사용).
11. 미해결 플래그 / 후속
INTERNAL_API_KEYvsREALESTATE_INTERNAL_KEY— 스펙 §config는 후자, BE 메시지는 전자(render 공유키). BE 메시지 우선(INTERNAL_API_KEY), 배포 전 co-gahusb로 1줄 확인.- 네이버 헤드리스 탐지 — 현재 spike는 통과. 차단 시 stealth 헤더/headed 필요할 수 있음(첫 운영 모니터링).
isMoreData/페이지 상한 — page_limit로 제한, 네이버 rate limit 재유입 시 동 간 지연 고려(첫 운영).- deal_types 미사용 — 워커는 전체 유형 fetch, NAS가 필터. 필요 시 tradeType 매핑으로 최적화.
- 토큰 재사용 실패(만료) 대비 — 사이클 중 fetch가 401 반환 시 acquire_token 1회 재시도(구현 시 반영 검토).