From caacb072a2fb54c883e1d26dea7dec1ded2c47cf Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 1 May 2026 08:56:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(realestate):=205=EC=B6=95=20=EC=A0=90?= =?UTF-8?q?=EC=88=98=20breakdown=20+=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20pass=5Fcount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - matcher: _compute_score()에 score_breakdown {region/type/area/price/eligibility} 반환 - matcher: run_matching() DB INSERT에 score_breakdown JSON 저장 - db: match_results에 score_breakdown 컬럼 마이그레이션 - db: _enrich_items / get_matches에서 score_breakdown 파싱 포함 - db: get_matches에 a.district 컬럼 추가 - db: get_dashboard()에 pass_count (min_match_score 임계값 통과 건수) 추가 Co-Authored-By: Claude Sonnet 4.6 --- realestate-lab/app/db.py | 20 ++++++++++++++++++-- realestate-lab/app/matcher.py | 21 ++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/realestate-lab/app/db.py b/realestate-lab/app/db.py index e949065..f3c41fc 100644 --- a/realestate-lab/app/db.py +++ b/realestate-lab/app/db.py @@ -159,6 +159,12 @@ def init_db(): except Exception: conn.execute("ALTER TABLE match_results ADD COLUMN notified_at TEXT") + # ── 마이그레이션: score_breakdown 컬럼 추가 ── + try: + conn.execute("SELECT score_breakdown FROM match_results LIMIT 1") + except Exception: + conn.execute("ALTER TABLE match_results ADD COLUMN score_breakdown TEXT") + # ── collect_log ────────────────────────────────────────────────── conn.execute(""" CREATE TABLE IF NOT EXISTS collect_log ( @@ -281,13 +287,14 @@ def _enrich_items(conn, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # 매칭 점수 if ann_id: match_row = conn.execute( - "SELECT match_score, match_reasons, eligible_types FROM match_results WHERE announcement_id = ?", + "SELECT match_score, match_reasons, eligible_types, score_breakdown FROM match_results WHERE announcement_id = ?", (ann_id,), ).fetchone() if match_row: item["match_score"] = match_row["match_score"] item["match_reasons"] = json.loads(match_row["match_reasons"]) if match_row["match_reasons"] else [] item["eligible_types"] = json.loads(match_row["eligible_types"]) if match_row["eligible_types"] else [] + item["score_breakdown"] = json.loads(match_row["score_breakdown"]) if match_row["score_breakdown"] else None return items @@ -681,7 +688,7 @@ def get_matches(page: int = 1, size: int = 20) -> Dict[str, Any]: total = conn.execute("SELECT COUNT(*) FROM match_results").fetchone()[0] rows = conn.execute(""" - SELECT m.*, a.house_nm, a.region_name, a.address, a.status as ann_status, + SELECT m.*, a.house_nm, a.region_name, a.address, a.district, a.status as ann_status, a.receipt_start, a.receipt_end, a.winner_date, a.pblanc_url, a.house_secd, a.is_speculative_area FROM match_results m @@ -695,6 +702,7 @@ def get_matches(page: int = 1, size: int = 20) -> Dict[str, Any]: d = {c: r[c] for c in r.keys()} d["match_reasons"] = json.loads(d["match_reasons"]) if d["match_reasons"] else [] d["eligible_types"] = json.loads(d["eligible_types"]) if d["eligible_types"] else [] + d["score_breakdown"] = json.loads(d["score_breakdown"]) if d.get("score_breakdown") else None items.append(d) return { "items": items, @@ -776,6 +784,13 @@ def get_dashboard() -> Dict[str, Any]: bookmarked_count = conn.execute( "SELECT COUNT(*) FROM announcements WHERE is_bookmarked = 1" ).fetchone()[0] + profile_row = conn.execute( + "SELECT min_match_score FROM user_profile WHERE id = 1" + ).fetchone() + min_score = profile_row["min_match_score"] if profile_row else 70 + pass_count = conn.execute( + "SELECT COUNT(*) FROM match_results WHERE match_score >= ?", (min_score,) + ).fetchone()[0] # 다가오는 일정을 개별 이벤트로 분해 upcoming_rows = conn.execute(""" @@ -820,6 +835,7 @@ def get_dashboard() -> Dict[str, Any]: return { "active_count": active, "new_match_count": new_matches, + "pass_count": pass_count, "bookmarked_count": bookmarked_count, "upcoming_schedules": schedules, "bookmarked": bookmarked_items, diff --git a/realestate-lab/app/matcher.py b/realestate-lab/app/matcher.py index 60039b8..3ccd5be 100644 --- a/realestate-lab/app/matcher.py +++ b/realestate-lab/app/matcher.py @@ -113,27 +113,33 @@ def _compute_score( preferred_types = profile.get("preferred_types") or [] house_secd = ann.get("house_secd") or "" type_name = _HOUSE_TYPE_MAP.get(house_secd, house_secd) + type_score = 0 if type_name and type_name in preferred_types: + type_score = 10 score += 10 reasons.append(f"선호 유형 일치: {type_name}") # 3. 면적 (15점) — binary, 범위 안 모델 1개라도 있으면 통과 min_area = profile.get("min_area") max_area = profile.get("max_area") + area_score = 0 if min_area is not None and max_area is not None and models: for m in models: supply_area = m.get("supply_area") if supply_area is not None and min_area <= supply_area <= max_area: + area_score = 15 score += 15 reasons.append(f"희망 면적 범위 내 모델 존재 ({supply_area}㎡)") break # 4. 가격 (15점) — binary, 예산 이하 모델 1개라도 있으면 통과 max_price = profile.get("max_price") + price_score = 0 if max_price is not None and models: for m in models: top_amount = m.get("top_amount") if top_amount is not None and top_amount <= max_price: + price_score = 15 score += 15 reasons.append(f"예산 범위 내 모델 존재 (최고가 {top_amount:,}만원)") break @@ -149,6 +155,13 @@ def _compute_score( "match_score": score, "match_reasons": reasons, "eligible_types": eligible_types, + "score_breakdown": { + "region": region_score, + "type": type_score, + "area": area_score, + "price": price_score, + "eligibility": elig_score, + }, } @@ -178,18 +191,20 @@ def run_matching(): result = _compute_score(profile, ann, model_list) if result["match_score"] > 0: conn.execute(""" - INSERT INTO match_results (announcement_id, model_id, match_score, match_reasons, eligible_types, is_new) - VALUES (?, ?, ?, ?, ?, 1) + INSERT INTO match_results (announcement_id, model_id, match_score, match_reasons, eligible_types, score_breakdown, is_new) + VALUES (?, ?, ?, ?, ?, ?, 1) ON CONFLICT(announcement_id, model_id) DO UPDATE SET match_score=excluded.match_score, match_reasons=excluded.match_reasons, - eligible_types=excluded.eligible_types + eligible_types=excluded.eligible_types, + score_breakdown=excluded.score_breakdown """, ( ann["id"], None, result["match_score"], json.dumps(result["match_reasons"]), json.dumps(result["eligible_types"]), + json.dumps(result["score_breakdown"]), )) # Clean up stale match results for completed announcements