From f8e6c6b0eedb5455269ddc41a60d7c4dd025e832 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Thu, 2 Jul 2026 00:02:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(account):=20=EB=8C=80=EC=B6=9C=20=EA=B3=84?= =?UTF-8?q?=EC=A2=8C=20=EA=B8=88=EB=A6=AC/=EC=83=81=ED=99=98=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=A0=80=EC=9E=A5=20+=20=EC=83=81=ED=99=98=20?= =?UTF-8?q?=EC=8B=9C=20=EB=82=A9=EC=9E=85=EA=B8=88=EC=95=A1=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wallet 테이블에 loan_rate/loan_method/loan_months/loan_start 컬럼 추가(ALTER TABLE) - 대출 계좌 폼에 연이자율(%), 상환방식(원리금균등/원금균등/만기일시), 기간, 시작일 입력 필드 추가 - 계좌 목록에 금리·상환방식 표시 - 상환 폼: 금리 설정된 대출 계좌 선택 시 납입금액 입력 → 이자/원금 자동계산 - 원리금균등/원금균등: 잔액 × 연이율/12 = 이자, 납입액-이자 = 원금 - 만기일시상환: 납입액 전체 이자로 처리 - 자동계산 후 수동 조정 가능 Co-Authored-By: Claude Sonnet 4.6 --- src/views/account/AccountView.vue | 58 ++++++++++++++++++++++++- src/views/account/AccountWalletView.vue | 47 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index 1fd61f5..b1f4e5e 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -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 () => { - - + + + @@ -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; diff --git a/src/views/account/AccountWalletView.vue b/src/views/account/AccountWalletView.vue index 87ae50b..bbb113c 100644 --- a/src/views/account/AccountWalletView.vue +++ b/src/views/account/AccountWalletView.vue @@ -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) ? '🙈' : '👁' }} +
@@ -458,6 +482,29 @@ onBeforeUnmount(() => sortable?.destroy()) + + +

{{ formError }}