# 네이버 매물 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:[...]}]} 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":, "last_fetch_at":,"listings_pushed":,"errors":} ``` - `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) 동시 수정.