From 48476264247abe8ed7089540186d6bc4a5927e79 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 9 Jul 2026 13:39:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(realestate):=20notify=5Fnew=5Flistings=20?= =?UTF-8?q?=E2=80=94=20=EB=A7=A4=EB=AC=BC=20=EC=95=8C=EB=A6=BC=20agent-off?= =?UTF-8?q?ice=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realestate-lab/app/notifier.py | 27 +++++++++++++++++- realestate-lab/tests/test_listing_notifier.py | 28 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 realestate-lab/tests/test_listing_notifier.py diff --git a/realestate-lab/app/notifier.py b/realestate-lab/app/notifier.py index 301ee34..40f6976 100644 --- a/realestate-lab/app/notifier.py +++ b/realestate-lab/app/notifier.py @@ -3,7 +3,8 @@ import os import logging import requests -from .db import get_profile, get_unnotified_matches, mark_matches_notified +from .db import (get_profile, get_unnotified_matches, mark_matches_notified, + get_listing_criteria, get_unnotified_listing_matches, mark_listings_notified) logger = logging.getLogger("realestate-lab") @@ -44,3 +45,27 @@ def notify_new_matches() -> dict: mark_matches_notified(sent_ids) logger.info("알림 송신: %d건", len(sent_ids)) return body + + +def notify_new_listings() -> dict: + """임계 통과 미알림 매물을 agent-office로 push (notify_new_matches 대칭). + passed 매물은 전 안전등급 알림(위험도 회피 목적 유용). criteria.min_safety_tier 설정 시 그 이상만.""" + crit = get_listing_criteria() + if not crit.get("notify_enabled"): + return {"sent": 0, "skipped": "notify_disabled"} + listings = get_unnotified_listing_matches(min_tier=crit.get("min_safety_tier")) + if not listings: + return {"sent": 0} + url = f"{AGENT_OFFICE_URL}/api/agent-office/realestate/notify-listing" + try: + resp = requests.post(url, json={"listings": listings}, timeout=NOTIFY_TIMEOUT_SECONDS) + resp.raise_for_status() + body = resp.json() + except requests.RequestException as e: + logger.error("agent-office 매물 push 실패: %s", e) + return {"sent": 0, "error": str(e)} + sent_ids = body.get("sent_ids") or [] + if sent_ids: + mark_listings_notified(sent_ids) + logger.info("매물 알림 송신: %d건", len(sent_ids)) + return body diff --git a/realestate-lab/tests/test_listing_notifier.py b/realestate-lab/tests/test_listing_notifier.py new file mode 100644 index 0000000..906362f --- /dev/null +++ b/realestate-lab/tests/test_listing_notifier.py @@ -0,0 +1,28 @@ +from unittest.mock import patch, MagicMock + + +def _seed_passed_match(): + from app import db + row, _ = db.upsert_listing({"article_no": "N1", "source": "naver", "deal_type": "전세", + "deposit": 29000, "area_exclusive": 42.0, "dong": "신대방동"}) + db.upsert_listing_match({"listing_id": row["id"], "category": "임차", "passed": 1, + "match_score": 100, "safety_tier": "안전", "sample_size": 7, + "reasons": ["ok"], "is_new": 1}) + return db.get_unnotified_listing_matches()[0]["id"] + +def test_notify_listings_pushes_and_marks(): + from app import notifier, db + mid = _seed_passed_match() + fake = MagicMock(); fake.json.return_value = {"sent": 1, "sent_ids": [mid]}; fake.raise_for_status.return_value = None + with patch.object(notifier.requests, "post", return_value=fake) as post: + res = notifier.notify_new_listings() + assert post.call_count == 1 + assert db.get_unnotified_listing_matches() == [] # 마킹됨 + +def test_notify_listings_no_mark_on_failure(): + from app import notifier, db + import requests as rq + mid = _seed_passed_match() + with patch.object(notifier.requests, "post", side_effect=rq.RequestException("down")): + notifier.notify_new_listings() + assert len(db.get_unnotified_listing_matches()) == 1 # 미마킹→재시도