diff --git a/realestate-lab/app/finance_rules.py b/realestate-lab/app/finance_rules.py new file mode 100644 index 0000000..b421ed9 --- /dev/null +++ b/realestate-lab/app/finance_rules.py @@ -0,0 +1,67 @@ +"""전세/매수 예산·규제 판정 (순수 함수, 외부 호출 없음).""" +from . import finance_rules_config as cfg + + +def region_regulation(dong: str) -> dict: + d = (dong or "").strip() + is_toheo = d in cfg.TOHEO_DONGS + is_regulated = d in cfg.REGULATED_DONGS or is_toheo + notes = [] + if is_toheo: + notes.append("토지거래허가구역 — 매수 시 실거주 의무(2년)+허가 필요, 갭투자 금지") + if is_regulated: + notes.append("규제지역 — LTV 강화") + notes.append("토허/규제 여부는 서울시 최신 고시로 확인 필요") + return {"is_toheo": is_toheo, "is_regulated": is_regulated, "notes": " · ".join(notes)} + + +def _dsr_max_loan(annual_income: int) -> int: + """DSR 한도 내 대출 원금 근사(만원). 원리금균등, 가정 금리·만기.""" + if not annual_income or annual_income <= 0: + return 0 + annual_pay_cap = annual_income * cfg.DSR_LIMIT_PCT / 100.0 # 연 상환 가능액 + r = cfg.ASSUMED_RATE_PCT / 100.0 / 12 + n = cfg.ASSUMED_TERM_YEARS * 12 + monthly_cap = annual_pay_cap / 12.0 + if r == 0: + return int(monthly_cap * n) + # PV = PMT * (1-(1+r)^-n)/r + pv = monthly_cap * (1 - (1 + r) ** (-n)) / r + return int(pv) + + +def estimate_jeonse_budget(equity: int, annual_income: int, jeonse_loan_ratio: float = None) -> dict: + equity = equity or 0 + ratio = jeonse_loan_ratio if jeonse_loan_ratio is not None else cfg.JEONSE_LOAN_RATIO + # 전세대출 한도 = min(보증기관 절대상한, DSR 한도). 보증금 비례는 매물별로 matcher에서 재확인. + loan_limit = min(cfg.JEONSE_LOAN_CAP, _dsr_max_loan(annual_income) or cfg.JEONSE_LOAN_CAP) + max_deposit = equity + loan_limit + return { + "loan_limit": loan_limit, + "max_deposit": max_deposit, + "notes": f"전세대출 한도≈{loan_limit}만(보증기관·DSR 근사, 보증금 대비 {int(ratio*100)}% 이내). {cfg.DISCLAIMER}", + } + + +def estimate_purchase_budget(equity: int, annual_income: int, region_flags: dict, + is_first_home: bool, is_homeless: bool) -> dict: + equity = equity or 0 + reg = "regulated" if (region_flags or {}).get("is_regulated") else "normal" + first = "first" if is_first_home else "normal" + ltv_pct = cfg.LTV_TABLE[(reg, first)] + # 대출가능액 = min(수도권 한도, DSR 한도). LTV는 매매가에 따라 matcher에서 상한 재확인. + dsr_cap = _dsr_max_loan(annual_income) + loan_cap = min(cfg.SUDOGWON_MORTGAGE_CAP, dsr_cap) if dsr_cap else cfg.SUDOGWON_MORTGAGE_CAP + max_price = equity + loan_cap + flags = [] + if (region_flags or {}).get("is_toheo"): + flags.append("토허") + if (region_flags or {}).get("is_regulated"): + flags.append("규제지역") + return { + "ltv_pct": ltv_pct, + "loan_cap": loan_cap, + "max_price": max_price, + "dsr_note": f"DSR {cfg.DSR_LIMIT_PCT}% 한도 근사(금리{cfg.ASSUMED_RATE_PCT}%·{cfg.ASSUMED_TERM_YEARS}년)", + "regulation_flags": flags, + } diff --git a/realestate-lab/app/finance_rules_config.py b/realestate-lab/app/finance_rules_config.py new file mode 100644 index 0000000..252ef9b --- /dev/null +++ b/realestate-lab/app/finance_rules_config.py @@ -0,0 +1,35 @@ +"""정책 파라미터 (외부 호출 없음). ⚠️ 정부 정책 수시 변동 — 계약·대출 전 은행/서울시 고시로 최종 확인. +기준: 2025 6·27 대책(수도권 주담대 6억 한도) 등. 실제 집행 전 최신값으로 갱신.""" + +# 수도권 주택담보대출 총액 한도 (만원). 6·27 대책: 6억. +SUDOGWON_MORTGAGE_CAP = 60000 + +# LTV(%) — (규제지역 여부, 생애최초 여부) → 한도. 무주택 기준. +LTV_TABLE = { + ("regulated", "first"): 70, + ("regulated", "normal"): 50, + ("normal", "first"): 80, + ("normal", "normal"): 70, +} + +# 스트레스 DSR 한도(%) — 연소득 대비 연원리금. +DSR_LIMIT_PCT = 40 +# 대출 산정용 가정 금리(연%)·만기(년) — 원리금균등 근사. +ASSUMED_RATE_PCT = 4.5 +ASSUMED_TERM_YEARS = 30 + +# 전세대출: 보증기관 보증비율(보증금 대비) 상한 근사. +JEONSE_LOAN_RATIO = 0.80 +# 전세대출 절대 상한(만원) — 보증기관 한도 근사. +JEONSE_LOAN_CAP = 44000 + +# 토지거래허가구역(구·동 단위, 서울시 고시 기준, 수동 갱신). 소수만. +TOHEO_DONGS = { + "대치동", "삼성동", "청담동", "잠실동", # 강남·송파 국제교류복합지구권 + "압구정동", "여의도동", "목동", "성수동", # 재건축 토허 + "이촌동", "한남동", # 용산 +} +# 규제지역(투기과열/조정) — 매수 LTV 강화 대상. +REGULATED_DONGS = set(TOHEO_DONGS) | {"반포동", "서초동"} + +DISCLAIMER = "정책(LTV·DSR·수도권 한도·토허)은 수시 변동 — 계약·대출 전 은행/중개사/서울시 고시로 최종 확인." diff --git a/realestate-lab/tests/test_finance_rules.py b/realestate-lab/tests/test_finance_rules.py new file mode 100644 index 0000000..50d346e --- /dev/null +++ b/realestate-lab/tests/test_finance_rules.py @@ -0,0 +1,21 @@ +def test_region_regulation_toheo_and_normal(): + from app.finance_rules import region_regulation + assert region_regulation("대치동")["is_toheo"] is True # 강남 토허 + r = region_regulation("신대방동") + assert r["is_toheo"] is False and "확인" in r["notes"] + +def test_jeonse_budget_equity_plus_loan(): + from app.finance_rules import estimate_jeonse_budget + b = estimate_jeonse_budget(equity=15000, annual_income=6000) # 자기자금 1.5억 + assert b["max_deposit"] == b["loan_limit"] + 15000 + assert b["loan_limit"] > 0 + +def test_purchase_budget_seoul_cap_applies(): + from app.finance_rules import region_regulation, estimate_purchase_budget + flags = region_regulation("상도동") + b = estimate_purchase_budget(equity=30000, annual_income=8000, + region_flags=flags, is_first_home=False, is_homeless=True) + # 수도권 주담대 한도(6·27 대책 6억=60000만원) 캡이 대출가능액에 반영 + assert b["loan_cap"] <= 60000 + assert b["max_price"] == b["loan_cap"] + 30000 + assert 0 < b["ltv_pct"] <= 100