@@ -127,6 +127,33 @@ const repayTargetIsCard = computed(() => {
|
|||||||
const w = wallets.value.find((x) => x.id === form.toWalletId)
|
const w = wallets.value.find((x) => x.id === form.toWalletId)
|
||||||
return !!w && w.type === 'CARD'
|
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개월, 일시불은 빈값)
|
// 카드 지출일 때만 할부 입력 노출 (2~24개월, 일시불은 빈값)
|
||||||
const showInstallment = computed(() => form.type === 'EXPENSE' && form.walletKind === 'CARD')
|
const showInstallment = computed(() => form.type === 'EXPENSE' && form.walletKind === 'CARD')
|
||||||
const installmentMonthly = computed(() => {
|
const installmentMonthly = computed(() => {
|
||||||
@@ -1135,8 +1162,23 @@ onMounted(async () => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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.principal" type="number" min="0" placeholder="원 (이체로 기록)" :disabled="submitting" /></label>
|
||||||
<label>이자<input v-model.number="form.interest" 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>
|
<label v-if="repayTargetIsCard">연회비<input v-model.number="form.annualFee" type="number" min="0" placeholder="원 (지출·분류 연회비)" :disabled="submitting" /></label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1973,6 +2015,18 @@ button.primary {
|
|||||||
.acc-chip.add-chip.sub-add {
|
.acc-chip.add-chip.sub-add {
|
||||||
flex: 0 0 auto;
|
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 {
|
.tag-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ const form = reactive({
|
|||||||
openingBalance: 0,
|
openingBalance: 0,
|
||||||
openingDate: '',
|
openingDate: '',
|
||||||
currentValue: null,
|
currentValue: null,
|
||||||
|
// 대출(LOAN) 전용
|
||||||
|
loanRate: null,
|
||||||
|
loanMethod: '',
|
||||||
|
loanMonths: null,
|
||||||
|
loanStart: '',
|
||||||
})
|
})
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const formError = ref(null)
|
const formError = ref(null)
|
||||||
@@ -229,6 +234,10 @@ function openCreate() {
|
|||||||
openingBalance: 0,
|
openingBalance: 0,
|
||||||
openingDate: '',
|
openingDate: '',
|
||||||
currentValue: null,
|
currentValue: null,
|
||||||
|
loanRate: null,
|
||||||
|
loanMethod: '',
|
||||||
|
loanMonths: null,
|
||||||
|
loanStart: '',
|
||||||
})
|
})
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formOpen.value = true
|
formOpen.value = true
|
||||||
@@ -245,6 +254,10 @@ function openEdit(w) {
|
|||||||
openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0,
|
openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0,
|
||||||
openingDate: w.openingDate || '',
|
openingDate: w.openingDate || '',
|
||||||
currentValue: w.currentValue ?? null,
|
currentValue: w.currentValue ?? null,
|
||||||
|
loanRate: w.loanRate ?? null,
|
||||||
|
loanMethod: w.loanMethod || '',
|
||||||
|
loanMonths: w.loanMonths ?? null,
|
||||||
|
loanStart: w.loanStart || '',
|
||||||
})
|
})
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formOpen.value = true
|
formOpen.value = true
|
||||||
@@ -258,6 +271,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
const magnitude = Number(form.openingBalance) || 0
|
const magnitude = Number(form.openingBalance) || 0
|
||||||
|
const isLoan = form.type === 'LOAN'
|
||||||
const payload = {
|
const payload = {
|
||||||
type: form.type,
|
type: form.type,
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
@@ -271,6 +285,10 @@ async function submit() {
|
|||||||
form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null
|
form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null
|
||||||
? Number(form.currentValue)
|
? Number(form.currentValue)
|
||||||
: null,
|
: 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 {
|
try {
|
||||||
if (editId.value) await accountApi.updateWallet(editId.value, payload)
|
if (editId.value) await accountApi.updateWallet(editId.value, payload)
|
||||||
@@ -297,6 +315,9 @@ async function remove(w) {
|
|||||||
function cardTypeLabel(v) {
|
function cardTypeLabel(v) {
|
||||||
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
|
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
|
||||||
}
|
}
|
||||||
|
function loanMethodLabel(v) {
|
||||||
|
return v === 'EQUAL_PAYMENT' ? '원리금균등' : v === 'EQUAL_PRINCIPAL' ? '원금균등' : v === 'BULLET' ? '만기일시' : ''
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await load()
|
await load()
|
||||||
@@ -362,6 +383,9 @@ onBeforeUnmount(() => sortable?.destroy())
|
|||||||
>{{ revealedAccts.has(w.id) ? '🙈' : '👁' }}</button>
|
>{{ revealedAccts.has(w.id) ? '🙈' : '👁' }}</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="w.type === 'INVEST'"> · 투자금 {{ won(w.investedAmount) }}</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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="balance-wrap">
|
<div class="balance-wrap">
|
||||||
@@ -458,6 +482,29 @@ onBeforeUnmount(() => sortable?.destroy())
|
|||||||
</template>
|
</template>
|
||||||
<label>기준일<input v-model="form.openingDate" type="date" :disabled="submitting" /></label>
|
<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>
|
<p v-if="formError" class="msg error">{{ formError }}</p>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
|||||||
Reference in New Issue
Block a user