fix(repayment): 원리금균등 월 상환액을 현재 잔액 기준 상각으로 계산
Deploy / deploy (push) Failing after 12m40s

- 원금 음수·잔액 증가 버그 수정: 최초원금 기준 상환액을 쓰면 잔액>원금 등
  데이터 불일치 시 상환액<이자가 되어 원금이 음수가 되고 잔액이 늘어남
- 남은 개월수(n)는 기존대로 잔액 역산, 상환액은 현재 잔액을 n개월 상각 →
  원금 항상 양수, 잔액 0 수렴. 역산 불가 시 이번 달 이자만(안내)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-07 22:53:49 +09:00
parent a42715834b
commit b039069d6c
+7 -7
View File
@@ -70,20 +70,18 @@ export function loanSchedule(w, now = currentYm(), cap = 600) {
return { rows, partial: !nRem } return { rows, partial: !nRem }
} }
// EQUAL_PAYMENT 원리금균등: 매월 상환액 고정 // EQUAL_PAYMENT 원리금균등
// 남은 개월수는 '현재 잔액 + 최초 상환액'으로 역산loanStart 없어도 정확(기존 대출 대응) // (1) 남은 개월수 n 산출: 최초 상환액으로 역산(loanStart 없어도 정확) → 안되면 loanStart 경과
let n = null let n = null
let payment = null
if (w.loanAmount && w.loanMonths && r > 0) { if (w.loanAmount && w.loanMonths && r > 0) {
const origPay = (Number(w.loanAmount) * r) / (1 - Math.pow(1 + r, -Number(w.loanMonths))) const origPay = (Number(w.loanAmount) * r) / (1 - Math.pow(1 + r, -Number(w.loanMonths)))
payment = Math.round(origPay)
// bal = origPay·(1-(1+r)^-n)/r → n = -ln(1 - bal·r/origPay) / ln(1+r) // bal = origPay·(1-(1+r)^-n)/r → n = -ln(1 - bal·r/origPay) / ln(1+r)
const ratio = 1 - (bal * r) / origPay const ratio = 1 - (bal * r) / origPay
if (ratio > 0 && ratio < 1) { if (ratio > 0 && ratio < 1) {
n = Math.min(Number(w.loanMonths), Math.max(1, Math.ceil(-Math.log(ratio) / Math.log(1 + r) - 1e-9))) n = Math.min(Number(w.loanMonths), Math.max(1, Math.ceil(-Math.log(ratio) / Math.log(1 + r) - 1e-9)))
} }
} }
if (n == null) n = nRem // 잔액 역산 불가 시 loanStart 경과 기반 if (n == null) n = nRem
if (n == null) { if (n == null) {
// 개월·원금 정보 부족 → 이번 달 이자만 안내(원금 분리 불가) // 개월·원금 정보 부족 → 이번 달 이자만 안내(원금 분리 불가)
const interest = Math.round(bal * r) const interest = Math.round(bal * r)
@@ -92,10 +90,12 @@ export function loanSchedule(w, now = currentYm(), cap = 600) {
partial: true, partial: true,
} }
} }
if (payment == null) payment = r > 0 ? Math.round((bal * r) / (1 - Math.pow(1 + r, -n))) : Math.round(bal / n) // (2) 스케줄 상환액은 '현재 잔액'을 n개월에 상각 → 원금 항상 양수, 잔액이 0으로 수렴
// (최초원금 기준을 쓰면 잔액>원금 등 데이터 불일치 시 원금이 음수가 되어 잔액이 증가함)
const payment = r > 0 ? Math.round((bal * r) / (1 - Math.pow(1 + r, -n))) : Math.round(bal / n)
for (let i = 0; i < n && bal > 0 && rows.length < cap; i++) { for (let i = 0; i < n && bal > 0 && rows.length < cap; i++) {
const interest = Math.round(bal * r) const interest = Math.round(bal * r)
let principal = payment - interest let principal = Math.max(0, payment - interest)
if (i === n - 1 || principal > bal) principal = bal // 마지막 회차 잔액 정산 if (i === n - 1 || principal > bal) principal = bal // 마지막 회차 잔액 정산
bal = Math.max(0, bal - principal) bal = Math.max(0, bal - principal)
rows.push({ label: label(y, m), principal, interest, total: principal + interest, balance: bal }) rows.push({ label: label(y, m), principal, interest, total: principal + interest, balance: bal })