네이버 article에 cortarNo가 없는 경우 listing.dong_code가 None으로 저장되어 get_market_deals_for가 시세를 못 찾아 모든 매물이 '보류'(판정 불가) 상태였다. dong명은 항상 정확히 태깅되므로 lawd_code(dong)으로 5자리 시군구코드를 유도해 market_deals와 매칭시킨다. - internal_router.listings_ingest: dong_code 없으면 batch.dong에서 lawd_code로 보정 - db.upsert_listing: ON CONFLICT UPDATE SET에 dong_code 추가(재유입 시 기존 None행 갱신) - listing_matcher.run_listing_matching: get_market_deals_for 호출 전 dong_code 없으면 listing.dong에서 즉시 유도(기존 저장된 None행도 재실행만으로 판정 복구) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
"""naver-fetch 워커 내부 계약: targets 조회 + listings ingest."""
|
|
import os
|
|
import time
|
|
import logging
|
|
from typing import List, Dict, Any
|
|
from fastapi import APIRouter, Depends, BackgroundTasks
|
|
from pydantic import BaseModel
|
|
|
|
from .auth import verify_internal_key
|
|
from .db import get_listing_criteria, upsert_listing
|
|
from .lawd_codes import naver_cortar, lawd_code
|
|
from .listing_collector import _parse_naver_article
|
|
from .listing_matcher import run_listing_matching
|
|
from .notifier import notify_new_listings
|
|
from .pipeline_lock import match_notify_lock
|
|
|
|
logger = logging.getLogger("realestate-lab")
|
|
router = APIRouter()
|
|
|
|
NAVER_PAGE_LIMIT = int(os.getenv("NAVER_PAGE_LIMIT", "2"))
|
|
|
|
|
|
@router.get("/api/internal/realestate/targets", dependencies=[Depends(verify_internal_key)])
|
|
def listing_targets():
|
|
crit = get_listing_criteria()
|
|
dongs = []
|
|
for d in crit.get("dongs", []):
|
|
code = naver_cortar(d)
|
|
if not code:
|
|
logger.warning("naver cortarNo 매핑 없음 — 대상 제외: %s", d)
|
|
continue
|
|
dongs.append({"dong": d, "cortar_no": code})
|
|
return {"dongs": dongs, "deal_types": crit.get("deal_types", []),
|
|
"page_limit": NAVER_PAGE_LIMIT}
|
|
|
|
|
|
class _Batch(BaseModel):
|
|
dong: str
|
|
articles: List[Dict[str, Any]] = []
|
|
|
|
|
|
class ListingsIngest(BaseModel):
|
|
fetched_at: str | None = None
|
|
batches: List[_Batch] = []
|
|
|
|
|
|
@router.post("/api/internal/realestate/listings-ingest",
|
|
dependencies=[Depends(verify_internal_key)])
|
|
def listings_ingest(body: ListingsIngest, background_tasks: BackgroundTasks):
|
|
received = new = 0
|
|
for batch in body.batches:
|
|
for raw in batch.articles:
|
|
try:
|
|
d = _parse_naver_article(raw)
|
|
if not d.get("article_no"):
|
|
continue
|
|
d["dong"] = batch.dong
|
|
# 네이버 article에 cortarNo가 없는 경우가 실사용에서 확인됨 → dong명에서
|
|
# 5자리 시군구코드(lawd_code)를 유도(market_deals는 이 5자리를 쓴다).
|
|
d["dong_code"] = d.get("dong_code") or lawd_code(batch.dong)
|
|
_, is_new = upsert_listing(d)
|
|
received += 1
|
|
if is_new:
|
|
new += 1
|
|
except Exception as e:
|
|
logger.warning("네이버 ingest 파싱 실패: %s", e)
|
|
if received:
|
|
background_tasks.add_task(_match_and_notify)
|
|
return {"received": received, "new": new, "queued": bool(received)}
|
|
|
|
|
|
def _match_and_notify():
|
|
"""ingest 후처리(백그라운드): 매칭+알림을 공유 락으로 직렬화. 타이밍 로그로 병목 관측."""
|
|
with match_notify_lock:
|
|
t0 = time.time()
|
|
run_listing_matching()
|
|
t1 = time.time()
|
|
noti = notify_new_listings()
|
|
t2 = time.time()
|
|
sent = (noti or {}).get("sent") if isinstance(noti, dict) else noti
|
|
logger.info("ingest 후처리: match=%.1fs notify=%.1fs sent=%s", t1 - t0, t2 - t1, sent)
|