feat(realestate-lab): 청약 가점제 계산 (84점 만점)

- calculate_subscription_points(): 무주택기간(32) + 부양가족(35) + 통장기간(17)
- 프로필 GET/PUT 응답에 subscription_points 포함
- 매칭 결과 API에 my_points 포함 (가점 비교용)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:27:53 +09:00
parent 074dd4041f
commit 262c088c8a

View File

@@ -449,6 +449,81 @@ def upsert_model(data: Dict[str, Any]):
""", data)
# ── 청약 가점 계산 ───────────────────────────────────────────────────────────
def calculate_subscription_points(profile: Dict[str, Any]) -> Dict[str, Any]:
"""청약 가점제 점수 계산 (총 84점 만점).
1. 무주택기간 (0~32점): 만 30세부터 기산, 연 2점
2. 부양가족 수 (0~35점): 인당 5점, 6명+ 만점
3. 청약통장 가입기간 (0~17점): 6개월 미만 1점 ~ 15년+ 17점
"""
result = {
"homeless_duration": {"score": 0, "max": 32, "detail": ""},
"dependents": {"score": 0, "max": 35, "detail": ""},
"subscription_period": {"score": 0, "max": 17, "detail": ""},
"total": 0,
"max_total": 84,
}
if not profile:
return result
# 1. 무주택기간 (만 30세부터 기산, 연 2점, 최대 32점)
age = profile.get("age") or 0
is_homeless = profile.get("is_homeless", False)
if is_homeless and age >= 30:
homeless_years = age - 30
score = min(homeless_years * 2, 32)
# 1년 미만도 2점
if homeless_years == 0:
score = 2
result["homeless_duration"]["score"] = score
result["homeless_duration"]["detail"] = f"{age}세, 무주택 약 {homeless_years}"
elif is_homeless and age < 30:
result["homeless_duration"]["score"] = 0
result["homeless_duration"]["detail"] = f"{age}세 (30세 미만, 기간 미산정)"
else:
result["homeless_duration"]["detail"] = "유주택자"
# 2. 부양가족 수 (인당 5점, 최대 35점)
family_members = profile.get("family_members") or 0
dependents = max(family_members - 1, 0) # 본인 제외
dep_score = min(dependents * 5, 35)
result["dependents"]["score"] = dep_score
result["dependents"]["detail"] = f"{dependents}" if dependents > 0 else "0명 (본인만)"
# 3. 청약통장 가입기간 (6개월 미만 1점, 이후 1년마다 +1점, 최대 17점)
months = profile.get("subscription_months") or 0
if months <= 0:
sub_score = 0
sub_detail = "미가입"
elif months < 6:
sub_score = 1
sub_detail = f"{months}개월 (6개월 미만)"
else:
years = months / 12
# 6개월~1년 = 2점, 1~2년 = 3점, ..., 14~15년 = 16점, 15년+ = 17점
sub_score = min(int(years) + 2, 17)
if years < 1:
sub_score = 2
if years >= 1:
y = int(years)
sub_detail = f"{y}{months - y*12}개월"
else:
sub_detail = f"{months}개월"
result["subscription_period"]["score"] = sub_score
result["subscription_period"]["detail"] = sub_detail
result["total"] = (
result["homeless_duration"]["score"]
+ result["dependents"]["score"]
+ result["subscription_period"]["score"]
)
return result
# ── user_profile CRUD ────────────────────────────────────────────────────────
def _profile_row_to_dict(r) -> Dict[str, Any]:
@@ -468,7 +543,11 @@ def _profile_row_to_dict(r) -> Dict[str, Any]:
def get_profile() -> Optional[Dict[str, Any]]:
with _conn() as conn:
r = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
return _profile_row_to_dict(r) if r else None
if not r:
return None
profile = _profile_row_to_dict(r)
profile["subscription_points"] = calculate_subscription_points(profile)
return profile
PROFILE_COLUMNS = {
@@ -512,7 +591,9 @@ def upsert_profile(data: Dict[str, Any]) -> Dict[str, Any]:
vals,
)
row = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
return _profile_row_to_dict(row)
profile = _profile_row_to_dict(row)
profile["subscription_points"] = calculate_subscription_points(profile)
return profile
# ── match_results CRUD ───────────────────────────────────────────────────────
@@ -536,10 +617,18 @@ def save_match_result(data: Dict[str, Any]):
def get_matches(page: int = 1, size: int = 20) -> Dict[str, Any]:
offset = (page - 1) * size
with _conn() as conn:
# 프로필 가점 계산
profile_row = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
points = None
if profile_row:
profile = _profile_row_to_dict(profile_row)
points = calculate_subscription_points(profile)
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,
a.receipt_start, a.receipt_end, a.winner_date, a.pblanc_url
a.receipt_start, a.receipt_end, a.winner_date, a.pblanc_url,
a.house_secd, a.is_speculative_area
FROM match_results m
JOIN announcements a ON a.id = m.announcement_id
ORDER BY m.is_new DESC, m.match_score DESC
@@ -552,7 +641,13 @@ def get_matches(page: int = 1, size: int = 20) -> Dict[str, Any]:
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 []
items.append(d)
return {"items": items, "total": total, "page": page, "size": size}
return {
"items": items,
"total": total,
"page": page,
"size": size,
"my_points": points,
}
def mark_match_read(match_id: int) -> bool: