feat(home): 대시보드에 이번 달 상환 예정 금액 카드
Deploy / deploy (push) Successful in 34s

- 대출 월 상환금 + 카드 할부(이번 달 회차) 합계를 홈 대시보드에 표기
- 현재일 기준, 가계부에 상환 기록(REPAYMENT) 있는 계좌는 제외
- 모두 기록 시 '이번 달 상환 완료' 표시, 부채 없으면 카드 숨김
- 계산은 utils/repayment.js 재사용

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-07 23:37:03 +09:00
parent e4ab683ac8
commit 2b58e976fc
+92 -1
View File
@@ -4,6 +4,7 @@ import { RouterLink, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui'
import { accountApi } from '@/api/accountApi'
import { loanSchedule, cardInstallmentSchedule, currentYm } from '@/utils/repayment'
import { reminders } from '@/native/reminders'
import { ID_LOGIN_ENABLED } from '@/config/features'
import { enterDemo } from '@/demo'
@@ -27,6 +28,8 @@ const networth = ref({ totalAssets: 0, totalLiabilities: 0, netWorth: 0 })
const budgetTotal = ref(0)
const spentTotal = ref(0)
const entries = ref([])
// 이번 달 상환 예정(대출·카드할부) — 현재일 기준, 가계부에 상환 기록 있으면 제외
const repayDue = ref({ total: 0, count: 0, hadAny: false })
const loading = ref(false)
const error = ref(null)
@@ -143,17 +146,19 @@ async function load() {
error.value = null
try {
// 예산은 유료 전용 — 무료 회원은 호출하지 않음(403/업그레이드 리다이렉트 방지)
const [sum, nw, status, list] = await Promise.all([
const [sum, nw, status, list, ws] = await Promise.all([
accountApi.summary({ year, month }),
accountApi.netWorth(),
auth.isPremium ? accountApi.budgetStatus({ year, month }) : Promise.resolve([]),
accountApi.list({ year, month }),
accountApi.wallets(),
])
summary.value = sum
networth.value = nw
budgetTotal.value = (status || []).reduce((s, x) => s + (x.monthlyBudget || 0), 0)
spentTotal.value = (status || []).reduce((s, x) => s + (x.spent || 0), 0)
entries.value = list || []
await computeRepayDue(ws || [], list || [])
// 오늘 기록 여부로 가계부 리마인더 보정(오늘 기록 있으면 오늘자 알림 제외)
if (reminders.isNative()) {
const t = new Date()
@@ -167,6 +172,54 @@ async function load() {
}
}
// 이번 달 상환 예정 계산: 대출 월 상환금 + 카드 할부(이번 달 회차).
// '가계부에 상환 기록(REPAYMENT) 있으면 제외' — 해당 계좌는 이번 달 이미 납입한 것으로 봄.
async function computeRepayDue(ws, monthEntries) {
const nw = currentYm()
const nowLabel = `${nw.y}.${String(nw.m).padStart(2, '0')}`
const paid = new Set(
(monthEntries || [])
.filter((e) => e.type === 'REPAYMENT' && e.toWalletId)
.map((e) => e.toWalletId),
)
let total = 0
let count = 0
let hadAny = false
// 대출: 이번 달 상환액(원금+이자)
for (const w of ws.filter((x) => x.type === 'LOAN' && x.loanRate)) {
const { rows } = loanSchedule(w, nw)
const row = rows.find((r) => r.label === nowLabel)
if (!row || !row.total) continue
hadAny = true
if (paid.has(w.id)) continue
total += row.total
count += 1
}
// 카드 할부: 이번 달 청구 회차 합(무이자). 카드에 이번 달 상환 기록 있으면 제외.
const cards = ws.filter((x) => x.type === 'CARD')
if (cards.length) {
const lists = await Promise.all(cards.map((c) => accountApi.walletEntries(c.id).catch(() => [])))
cards.forEach((c, i) => {
let cardSum = 0
for (const e of (lists[i] || []).filter((e) => e.type === 'EXPENSE' && Number(e.installmentMonths) >= 2)) {
const sch = cardInstallmentSchedule(e, nw)
if (!sch) continue
const row = sch.rows.find((r) => r.label === nowLabel)
if (row) cardSum += row.total
}
if (cardSum <= 0) return
hadAny = true
if (paid.has(c.id)) return
total += cardSum
count += 1
})
}
repayDue.value = { total, count, hadAny }
}
// 로그인/로그아웃 전환 시 갱신
watch(() => auth.isAuthenticated, (v) => {
if (v) load()
@@ -176,6 +229,7 @@ watch(() => auth.isAuthenticated, (v) => {
budgetTotal.value = 0
spentTotal.value = 0
entries.value = []
repayDue.value = { total: 0, count: 0, hadAny: false }
activeDay.value = 0
}
})
@@ -274,6 +328,22 @@ onMounted(load)
</div>
</div>
<!-- 이번 상환 예정 (대출·카드할부, 현재일 기준 · 상환 기록 있으면 제외) -->
<div v-if="repayDue.hadAny" class="card repay-card">
<div class="card-head">
<span class="card-title">이번 상환 예정</span>
<RouterLink to="/account/repayments" class="more">상환 </RouterLink>
</div>
<div v-if="repayDue.count > 0" class="repay-body">
<span class="repay-amt expense">{{ won(repayDue.total) }}</span>
<span class="repay-sub">아직 기록되지 않은 대출·할부 {{ repayDue.count }}</span>
</div>
<div v-else class="repay-body done">
<span class="repay-amt">이번 상환 완료 </span>
<span class="repay-sub">예정된 대출·할부를 모두 기록했어요</span>
</div>
</div>
<!-- 예산 대비 지출 (유료 전용 무료 회원은 업그레이드 안내) -->
<div class="card budget-card">
<div class="card-head">
@@ -563,6 +633,27 @@ onMounted(load)
}
/* 예산 대비 지출 카드 */
.repay-card {
margin-bottom: 1.2rem;
}
.repay-body {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.repay-amt {
font-size: 1.5rem;
font-weight: 800;
}
.repay-body.done .repay-amt {
font-size: 1.05rem;
font-weight: 700;
color: hsla(160, 100%, 37%, 1);
}
.repay-sub {
font-size: 0.82rem;
opacity: 0.65;
}
.budget-card {
margin-bottom: 1.2rem;
}