Files
ai-trade/services/naver-fetch/DESIGN.md

9.2 KiB
Raw Blame History

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.

책임

  1. 2~3h 주간 주기로 NAS targets 조회(감시 동 목록).
  2. Playwright로 네이버 Bearer 토큰 획득 → httpx로 동별 매물 fetch(페이지네이션).
  3. raw article을 그대로 NAS listings-ingest로 전달(파싱·매칭·알림은 NAS).
  4. Redis heartbeat(worker:naver-fetch:heartbeat EX45, kind=fetcher).
  5. 토큰/네트워크 오류를 사이클·동 단위로 격리.

경계 밖(안 함): article 파싱, 매물 매칭, 텔레그램 발송(전부 NAS), dedup.

Spike 검증 완료 (2026-07-10)

  • Playwright 헤드리스 → new.land.naver.com 로드 → /api/articles XHR의 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}
for d in tg.dongs:                      # 동 단위 try/except 격리 (fetch/ingest 각각)
    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
    # ⚠️ 동별로 즉시 ingest — 전체 batch를 한 POST로 보내면 NAS 동기 처리(매물 수 비례)가
    #    >180s 타임아웃. 동 단위(각 수십건)로 쪼개 각 POST가 timeout 내 완료 + 부분 실패 격리.
    r = await nas.post_ingest(fetched_at=iso_utc_now, batches=[{"dong": d.dong, "articles": articles}])
    state.listings_pushed += len(articles)   # {received,new,matched}
state.last_fetch_at = epoch_now
state.state = "idle"

⚠️ ingest 분할: 스펙은 batches(복수)를 한 POST로 예시하지만, 첫 실행 대량(6동×~40건)이 NAS에서 >180s 걸려 타임아웃 → 동별 1 POST로 분할(batches 리스트 계약 호환, 각 POST는 batches 1개). nas_client의 ingest는 timeout 180s·재시도 없음(재전송 시 NAS 중복 알림 방지). BE에 분할 통지 필요.

⚠️ 시간 포맷 2종: ingest fetched_atISO UTC 문자열(스펙 §4 ingest 스키마, 예 2026-07-09T05:44:23Z), heartbeat ts/last_fetch_atepoch 정수(스펙 §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 GET https://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 (compose: NAVER_NAS_BASE_URL) http://192.168.45.54:18800 realestate-lab 직결 targets/ingest (stock :18500 아님)
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. 미해결 플래그 / 후속

  1. INTERNAL_API_KEY vs REALESTATE_INTERNAL_KEY — 스펙 §config는 후자, BE 메시지는 전자(render 공유키). BE 메시지 우선(INTERNAL_API_KEY), 배포 전 co-gahusb로 1줄 확인.
  2. 네이버 헤드리스 탐지 — 현재 spike는 통과. 차단 시 stealth 헤더/headed 필요할 수 있음(첫 운영 모니터링).
  3. isMoreData/페이지 상한 — page_limit로 제한, 네이버 rate limit 재유입 시 동 간 지연 고려(첫 운영).
  4. deal_types 미사용 — 워커는 전체 유형 fetch, NAS가 필터. 필요 시 tradeType 매핑으로 최적화.
  5. 토큰 재사용 실패(만료) 대비 — 사이클 중 fetch가 401 반환 시 acquire_token 1회 재시도(구현 시 반영 검토).