feat(realestate): 5축 점수 breakdown + 대시보드 pass_count
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user