- get_market_deals_for에 optional conn 파라미터 추가(재사용 시 open/close 안 함, 기존 단건 호출부는 불변) - bulk_upsert_listing_matches 추가(listing_matches 다건 upsert, 단일 connection) - run_listing_matching이 listings 조회+매물별 market_deals 조회+최종 upsert를 단일 _conn()으로 처리 (기존: 269건 매물마다 개별 _conn() 3회 → 병목) - POST /api/realestate/listings/rematch: MOLIT 재수집 없이 기존 listings+market_deals로 즉시 재판정 (알림 미발송, 즉시 피드백용) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
167 lines
9.5 KiB
Python
167 lines
9.5 KiB
Python
def test_listing_criteria_seed_and_update():
|
|
from app import db
|
|
c = db.get_listing_criteria()
|
|
assert c["id"] == 1 and "신대방동" in c["dongs"] and c["max_deposit"] == 32000
|
|
db.update_listing_criteria({"max_deposit": 30000, "equity": 15000})
|
|
assert db.get_listing_criteria()["max_deposit"] == 30000
|
|
assert db.get_listing_criteria()["equity"] == 15000
|
|
|
|
def test_upsert_listing_idempotent_and_is_new():
|
|
from app import db
|
|
row = {"article_no": "A1", "source": "naver", "deal_type": "전세", "deposit": 29000,
|
|
"area_exclusive": 42.0, "complex_name": "OO", "dong": "신대방동", "dong_code": "11590"}
|
|
_, is_new = db.upsert_listing(row)
|
|
assert is_new is True
|
|
_, is_new2 = db.upsert_listing({**row, "deposit": 28000})
|
|
assert is_new2 is False
|
|
ls = db.get_listings()
|
|
assert len(ls) == 1 and ls[0]["deposit"] == 28000
|
|
|
|
def test_market_deals_dedup_and_query():
|
|
from app import db
|
|
base = {"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
|
"complex_name": "OO", "area": 42.0, "deal_type": "전세",
|
|
"deposit": 43000, "sale_amount": None, "deal_ym": "202605", "floor": "5", "source": "molit"}
|
|
db.upsert_market_deal(base)
|
|
db.upsert_market_deal(base) # 동일 → dedup
|
|
deals = db.get_market_deals_for("11590", "OO", 42.0, "전세", months=120)
|
|
assert len(deals) == 1
|
|
|
|
def test_unnotified_listing_match_and_mark():
|
|
from app import db
|
|
lid, _ = db.upsert_listing({"article_no": "A2", "source": "naver", "deal_type": "전세",
|
|
"deposit": 29000, "area_exclusive": 42.0, "dong": "신대방동"})
|
|
db.upsert_listing_match({"listing_id": lid[0] if isinstance(lid, tuple) else lid["id"],
|
|
"category": "임차", "passed": 1, "match_score": 90,
|
|
"safety_tier": "안전", "sample_size": 7, "reasons": ["ok"], "is_new": 1})
|
|
un = db.get_unnotified_listing_matches()
|
|
assert len(un) == 1
|
|
db.mark_listings_notified([un[0]["id"]])
|
|
assert db.get_unnotified_listing_matches() == []
|
|
|
|
|
|
# ── 최종 리뷰 fix (M1/M3/M4) ─────────────────────────────────────────────────
|
|
|
|
def test_market_deals_broad_fallback_when_complex_sample_thin():
|
|
"""complex_name 지정 시 표본이 MIN_SAMPLE(3) 미만이면 complex 제약을 풀고
|
|
같은 dong_code/deal_type/area±5/recency 조건으로 광역 재조회해야 한다."""
|
|
from app import db
|
|
base = {"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
|
"area": 42.0, "deal_type": "전세", "sale_amount": None, "floor": "5", "source": "molit"}
|
|
# complex "A" 단독 1건 (< MIN_SAMPLE=3)
|
|
db.upsert_market_deal({**base, "complex_name": "A", "deposit": 30000, "deal_ym": "202601"})
|
|
# complex "B" 3건 (dedup 인덱스 회피 위해 deal_ym 다르게)
|
|
db.upsert_market_deal({**base, "complex_name": "B", "deposit": 31000, "deal_ym": "202602"})
|
|
db.upsert_market_deal({**base, "complex_name": "B", "deposit": 32000, "deal_ym": "202603"})
|
|
db.upsert_market_deal({**base, "complex_name": "B", "deposit": 33000, "deal_ym": "202604"})
|
|
|
|
deals = db.get_market_deals_for("11590", "A", 42.0, "전세", months=120)
|
|
assert len(deals) == 4 # A 1건 + B 3건 광역 폴백
|
|
|
|
|
|
def test_upsert_market_deal_area_null_skipped():
|
|
"""area=None인 실거래는 median 계산에 무용 + dedup 인덱스도 못 걸려 무한중복
|
|
유발하므로 저장 자체를 skip해야 한다."""
|
|
from app import db
|
|
d = {"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
|
"complex_name": "OO", "area": None, "deal_type": "전세",
|
|
"deposit": 43000, "sale_amount": None, "deal_ym": "202605", "floor": "5", "source": "molit"}
|
|
db.upsert_market_deal(d)
|
|
db.upsert_market_deal(d) # 재호출해도 여전히 저장 안 됨(무한중복 방지 확인)
|
|
with db._conn() as conn:
|
|
cnt = conn.execute("SELECT COUNT(*) FROM market_deals").fetchone()[0]
|
|
assert cnt == 0
|
|
|
|
|
|
def test_bulk_upsert_market_deals(monkeypatch):
|
|
from app import db
|
|
rows = [
|
|
{"house_type":"아파트","dong_code":"11590","dong":"신대방동","complex_name":"OO","area":42.0,
|
|
"deal_type":"전세","deposit":43000,"sale_amount":None,"deal_ym":"202606","floor":"5"},
|
|
{"house_type":"아파트","dong_code":"11590","dong":"신대방동","complex_name":"OO","area":42.0,
|
|
"deal_type":"전세","deposit":43000,"sale_amount":None,"deal_ym":"202606","floor":"5"}, # dup
|
|
{"house_type":"아파트","dong_code":"11590","dong":None,"complex_name":None,"area":None,
|
|
"deal_type":"매매","sale_amount":90000,"deal_ym":"202606","floor":"3"}, # area None → skip
|
|
]
|
|
n = db.bulk_upsert_market_deals(rows)
|
|
assert n == 2 # area=None 1건 skip
|
|
deals = db.get_market_deals_for("11590","OO",42.0,"전세",months=120)
|
|
assert len(deals) == 1 # dup dedup → 1건
|
|
|
|
|
|
def test_get_listing_matches_json_parsed():
|
|
"""get_listing_matches()도 get_unnotified_listing_matches()와 동일하게
|
|
regulation_flags/reasons를 list로 파싱해 반환해야 한다."""
|
|
from app import db
|
|
lid, _ = db.upsert_listing({"article_no": "A3", "source": "naver", "deal_type": "전세",
|
|
"deposit": 29000, "area_exclusive": 42.0, "dong": "신대방동"})
|
|
db.upsert_listing_match({"listing_id": lid["id"], "category": "임차", "passed": 1,
|
|
"match_score": 90, "safety_tier": "안전", "sample_size": 7,
|
|
"regulation_flags": ["토지거래허가구역"], "reasons": ["ok"], "is_new": 1})
|
|
matches = db.get_listing_matches()
|
|
assert len(matches) == 1
|
|
assert isinstance(matches[0]["regulation_flags"], list)
|
|
assert matches[0]["regulation_flags"] == ["토지거래허가구역"]
|
|
assert isinstance(matches[0]["reasons"], list)
|
|
assert matches[0]["reasons"] == ["ok"]
|
|
|
|
|
|
# ── Task 2: run_listing_matching 단일 conn 성능개선 ──────────────────────────
|
|
|
|
def test_get_market_deals_for_reuses_passed_conn():
|
|
"""conn 인자를 넘기면 그 connection을 재사용하고, 넘기지 않았을 때와 동일한
|
|
결과를 반환해야 한다(1차 complex 스코프 → 표본 부족 시 광역 폴백 로직 불변)."""
|
|
from app import db
|
|
base = {"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
|
"complex_name": "OO", "area": 42.0, "deal_type": "전세",
|
|
"sale_amount": None, "floor": "5", "source": "molit"}
|
|
db.upsert_market_deal({**base, "deposit": 40000, "deal_ym": "202601"})
|
|
db.upsert_market_deal({**base, "deposit": 41000, "deal_ym": "202602"})
|
|
db.upsert_market_deal({**base, "deposit": 42000, "deal_ym": "202603"})
|
|
|
|
without_conn = db.get_market_deals_for("11590", "OO", 42.0, "전세", months=120)
|
|
with db._conn() as shared:
|
|
with_conn = db.get_market_deals_for("11590", "OO", 42.0, "전세", months=120, conn=shared)
|
|
# conn을 넘겼으니 함수가 스스로 닫지 않아야 함 — 같은 conn으로 후속 쿼리 가능해야 한다.
|
|
still_open = shared.execute("SELECT COUNT(*) FROM market_deals").fetchone()[0]
|
|
assert len(with_conn) == len(without_conn) == 3
|
|
assert still_open == 3
|
|
|
|
|
|
def test_bulk_upsert_listing_matches_multi_insert_and_update():
|
|
"""bulk_upsert_listing_matches가 여러 건을 한 번에 upsert하고, 재호출 시
|
|
기존 upsert_listing_match와 동일하게 갱신(notified_at 보존)해야 한다."""
|
|
from app import db
|
|
l1, _ = db.upsert_listing({"article_no": "B1", "source": "naver", "deal_type": "전세",
|
|
"deposit": 29000, "area_exclusive": 42.0, "dong": "신대방동"})
|
|
l2, _ = db.upsert_listing({"article_no": "B2", "source": "naver", "deal_type": "매매",
|
|
"sale_price": 85000, "area_exclusive": 59.0, "dong": "상도동"})
|
|
db.bulk_upsert_listing_matches([
|
|
{"listing_id": l1["id"], "category": "임차", "passed": 1, "match_score": 100,
|
|
"safety_tier": "안전", "sample_size": 5, "reasons": ["ok"], "is_new": 1},
|
|
{"listing_id": l2["id"], "category": "매매", "passed": 0, "match_score": 0,
|
|
"valuation_tier": "고가", "sample_size": 4, "reasons": ["매수가능 상한 초과"], "is_new": 1},
|
|
])
|
|
matches = {m["listing_id"]: m for m in db.get_listing_matches()}
|
|
assert len(matches) == 2
|
|
assert matches[l1["id"]]["safety_tier"] == "안전" and matches[l1["id"]]["passed"] == 1
|
|
assert matches[l2["id"]]["valuation_tier"] == "고가" and matches[l2["id"]]["passed"] == 0
|
|
|
|
# notified_at 마킹 후 재호출해도 보존돼야 함(재알림 방지)
|
|
match_id = next(m["id"] for m in db.get_listing_matches() if m["listing_id"] == l1["id"])
|
|
db.mark_listings_notified([match_id])
|
|
db.bulk_upsert_listing_matches([
|
|
{"listing_id": l1["id"], "category": "임차", "passed": 1, "match_score": 95,
|
|
"safety_tier": "주의", "sample_size": 6, "reasons": ["ok"], "is_new": 1},
|
|
])
|
|
updated = {m["listing_id"]: m for m in db.get_listing_matches()}
|
|
assert updated[l1["id"]]["safety_tier"] == "주의" # 갱신됨
|
|
assert updated[l1["id"]]["notified_at"] is not None # 보존됨
|
|
|
|
|
|
def test_bulk_upsert_listing_matches_empty_noop():
|
|
"""빈 리스트는 아무 것도 하지 않는다(가드)."""
|
|
from app import db
|
|
db.bulk_upsert_listing_matches([])
|
|
assert db.get_listing_matches() == []
|