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:
@@ -449,6 +449,81 @@ def upsert_model(data: Dict[str, Any]):
|
|||||||
""", data)
|
""", 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 ────────────────────────────────────────────────────────
|
# ── user_profile CRUD ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _profile_row_to_dict(r) -> Dict[str, Any]:
|
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]]:
|
def get_profile() -> Optional[Dict[str, Any]]:
|
||||||
with _conn() as conn:
|
with _conn() as conn:
|
||||||
r = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
|
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 = {
|
PROFILE_COLUMNS = {
|
||||||
@@ -512,7 +591,9 @@ def upsert_profile(data: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
vals,
|
vals,
|
||||||
)
|
)
|
||||||
row = conn.execute("SELECT * FROM user_profile WHERE id = 1").fetchone()
|
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 ───────────────────────────────────────────────────────
|
# ── 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]:
|
def get_matches(page: int = 1, size: int = 20) -> Dict[str, Any]:
|
||||||
offset = (page - 1) * size
|
offset = (page - 1) * size
|
||||||
with _conn() as conn:
|
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]
|
total = conn.execute("SELECT COUNT(*) FROM match_results").fetchone()[0]
|
||||||
rows = conn.execute("""
|
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.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
|
FROM match_results m
|
||||||
JOIN announcements a ON a.id = m.announcement_id
|
JOIN announcements a ON a.id = m.announcement_id
|
||||||
ORDER BY m.is_new DESC, m.match_score DESC
|
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["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["eligible_types"] = json.loads(d["eligible_types"]) if d["eligible_types"] else []
|
||||||
items.append(d)
|
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:
|
def mark_match_read(match_id: int) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user