Compare commits

...

2 Commits

Author SHA1 Message Date
ByungCheol 31cb4b7198 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m36s
2026-07-02 00:02:13 +09:00
ByungCheol f8e6c6b0ee feat(account): 대출 계좌 금리/상환방식 저장 + 상환 시 납입금액 자동분리
CI / build (push) Failing after 14m10s
- wallet 테이블에 loan_rate/loan_method/loan_months/loan_start 컬럼 추가(ALTER TABLE)
- 대출 계좌 폼에 연이자율(%), 상환방식(원리금균등/원금균등/만기일시), 기간, 시작일 입력 필드 추가
- 계좌 목록에 금리·상환방식 표시
- 상환 폼: 금리 설정된 대출 계좌 선택 시 납입금액 입력 → 이자/원금 자동계산
  - 원리금균등/원금균등: 잔액 × 연이율/12 = 이자, 납입액-이자 = 원금
  - 만기일시상환: 납입액 전체 이자로 처리
  - 자동계산 후 수동 조정 가능

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 00:02:10 +09:00
2 changed files with 103 additions and 2 deletions
+54
View File
@@ -127,6 +127,33 @@ const repayTargetIsCard = computed(() => {
const w = wallets.value.find((x) => x.id === form.toWalletId)
return !!w && w.type === 'CARD'
})
// 금리가 설정된 대출 계좌를 선택한 경우 자동계산 활성
const repayTargetLoanWallet = computed(() => {
const w = wallets.value.find((x) => x.id === form.toWalletId)
return w?.type === 'LOAN' && w.loanRate ? w : null
})
// 납입총액 입력 → 원금/이자 자동분리
const loanPaymentAmount = ref(null)
function calcLoanRepayment(payment) {
const loan = repayTargetLoanWallet.value
if (!loan || !payment || payment <= 0) return
const monthlyRate = Number(loan.loanRate) / 100 / 12
const remainingDebt = Math.abs(loan.balance || 0)
const monthlyInterest = Math.round(remainingDebt * monthlyRate)
if (loan.loanMethod === 'BULLET') {
form.interest = payment
form.principal = 0
} else {
form.interest = Math.min(monthlyInterest, payment)
form.principal = Math.max(0, payment - form.interest)
}
}
watch(loanPaymentAmount, (val) => { if (val) calcLoanRepayment(Number(val)) })
watch(() => form.toWalletId, () => {
loanPaymentAmount.value = null
form.principal = null
form.interest = null
})
// 카드 지출일 때만 할부 입력 노출 (2~24개월, 일시불은 빈값)
const showInstallment = computed(() => form.type === 'EXPENSE' && form.walletKind === 'CARD')
const installmentMonthly = computed(() => {
@@ -1135,8 +1162,23 @@ onMounted(async () => {
</option>
</select>
</label>
<!-- 금리 설정된 대출 계좌: 납입금액 입력 원금/이자 자동계산 -->
<template v-if="repayTargetLoanWallet">
<label>납입금액
<input v-model.number="loanPaymentAmount" type="number" min="0"
placeholder="이번 달 납입할 총금액" :disabled="submitting" @input="calcLoanRepayment(loanPaymentAmount)" />
</label>
<div v-if="loanPaymentAmount > 0" class="loan-breakdown">
<span>이자 <b>{{ (form.interest || 0).toLocaleString('ko-KR') }}</b></span>
<span>원금상환 <b>{{ (form.principal || 0).toLocaleString('ko-KR') }}</b></span>
</div>
<label>이자(조정)<input v-model.number="form.interest" type="number" min="0" :disabled="submitting" /></label>
<label>원금(조정)<input v-model.number="form.principal" type="number" min="0" :disabled="submitting" /></label>
</template>
<template v-else>
<label>원금<input v-model.number="form.principal" type="number" min="0" placeholder="원 (이체로 기록)" :disabled="submitting" /></label>
<label>이자<input v-model.number="form.interest" type="number" min="0" placeholder="원 (지출·분류 이자)" :disabled="submitting" /></label>
</template>
<label v-if="repayTargetIsCard">연회비<input v-model.number="form.annualFee" type="number" min="0" placeholder="원 (지출·분류 연회비)" :disabled="submitting" /></label>
</template>
@@ -1973,6 +2015,18 @@ button.primary {
.acc-chip.add-chip.sub-add {
flex: 0 0 auto;
}
.loan-breakdown {
display: flex;
gap: 1rem;
padding: 0.45rem 0.6rem;
border-radius: 6px;
background: var(--color-background-soft);
font-size: 0.85rem;
color: var(--color-text);
}
.loan-breakdown b {
color: hsla(160, 100%, 37%, 1);
}
.tag-field {
display: flex;
flex-direction: column;
+47
View File
@@ -33,6 +33,11 @@ const form = reactive({
openingBalance: 0,
openingDate: '',
currentValue: null,
// 대출(LOAN) 전용
loanRate: null,
loanMethod: '',
loanMonths: null,
loanStart: '',
})
const submitting = ref(false)
const formError = ref(null)
@@ -229,6 +234,10 @@ function openCreate() {
openingBalance: 0,
openingDate: '',
currentValue: null,
loanRate: null,
loanMethod: '',
loanMonths: null,
loanStart: '',
})
formError.value = null
formOpen.value = true
@@ -245,6 +254,10 @@ function openEdit(w) {
openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0,
openingDate: w.openingDate || '',
currentValue: w.currentValue ?? null,
loanRate: w.loanRate ?? null,
loanMethod: w.loanMethod || '',
loanMonths: w.loanMonths ?? null,
loanStart: w.loanStart || '',
})
formError.value = null
formOpen.value = true
@@ -258,6 +271,7 @@ async function submit() {
}
submitting.value = true
const magnitude = Number(form.openingBalance) || 0
const isLoan = form.type === 'LOAN'
const payload = {
type: form.type,
name: form.name.trim(),
@@ -271,6 +285,10 @@ async function submit() {
form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null
? Number(form.currentValue)
: null,
loanRate: isLoan && form.loanRate !== null && form.loanRate !== '' ? Number(form.loanRate) : null,
loanMethod: isLoan && form.loanMethod ? form.loanMethod : null,
loanMonths: isLoan && form.loanMonths ? Number(form.loanMonths) : null,
loanStart: isLoan && form.loanStart ? form.loanStart : null,
}
try {
if (editId.value) await accountApi.updateWallet(editId.value, payload)
@@ -297,6 +315,9 @@ async function remove(w) {
function cardTypeLabel(v) {
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
}
function loanMethodLabel(v) {
return v === 'EQUAL_PAYMENT' ? '원리금균등' : v === 'EQUAL_PRINCIPAL' ? '원금균등' : v === 'BULLET' ? '만기일시' : ''
}
onMounted(async () => {
await load()
@@ -362,6 +383,9 @@ onBeforeUnmount(() => sortable?.destroy())
>{{ revealedAccts.has(w.id) ? '🙈' : '👁' }}</button>
</template>
<template v-if="w.type === 'INVEST'"> · 투자금 {{ won(w.investedAmount) }}</template>
<template v-if="w.type === 'LOAN' && w.loanRate">
· {{ w.loanRate }}% <span v-if="w.loanMethod">/ {{ loanMethodLabel(w.loanMethod) }}</span>
</template>
</span>
</div>
<div class="balance-wrap">
@@ -458,6 +482,29 @@ onBeforeUnmount(() => sortable?.destroy())
</template>
<label>기준일<input v-model="form.openingDate" type="date" :disabled="submitting" /></label>
<!-- 대출 전용 -->
<template v-if="form.type === 'LOAN'">
<label>연이자율(%)
<input v-model.number="form.loanRate" type="number" min="0" max="100" step="0.01"
placeholder="예: 5.25" :disabled="submitting" />
</label>
<label>상환방식
<select v-model="form.loanMethod" :disabled="submitting">
<option value="">(선택 )</option>
<option value="EQUAL_PAYMENT">원리금균등상환</option>
<option value="EQUAL_PRINCIPAL">원금균등상환</option>
<option value="BULLET">만기일시상환</option>
</select>
</label>
<label>대출기간(개월)
<input v-model.number="form.loanMonths" type="number" min="1"
placeholder="예: 120 (10년)" :disabled="submitting" />
</label>
<label>대출시작일
<input v-model="form.loanStart" type="date" :disabled="submitting" />
</label>
</template>
<p v-if="formError" class="msg error">{{ formError }}</p>
<div class="buttons">