- 대출 월 상환금 + 카드 할부(이번 달 회차) 합계를 홈 대시보드에 표기 - 현재일 기준, 가계부에 상환 기록(REPAYMENT) 있는 계좌는 제외 - 모두 기록 시 '이번 달 상환 완료' 표시, 부채 없으면 카드 숨김 - 계산은 utils/repayment.js 재사용 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+92
-1
@@ -4,6 +4,7 @@ import { RouterLink, useRouter } from 'vue-router'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useUiStore } from '@/stores/ui'
|
import { useUiStore } from '@/stores/ui'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
import { loanSchedule, cardInstallmentSchedule, currentYm } from '@/utils/repayment'
|
||||||
import { reminders } from '@/native/reminders'
|
import { reminders } from '@/native/reminders'
|
||||||
import { ID_LOGIN_ENABLED } from '@/config/features'
|
import { ID_LOGIN_ENABLED } from '@/config/features'
|
||||||
import { enterDemo } from '@/demo'
|
import { enterDemo } from '@/demo'
|
||||||
@@ -27,6 +28,8 @@ const networth = ref({ totalAssets: 0, totalLiabilities: 0, netWorth: 0 })
|
|||||||
const budgetTotal = ref(0)
|
const budgetTotal = ref(0)
|
||||||
const spentTotal = ref(0)
|
const spentTotal = ref(0)
|
||||||
const entries = ref([])
|
const entries = ref([])
|
||||||
|
// 이번 달 상환 예정(대출·카드할부) — 현재일 기준, 가계부에 상환 기록 있으면 제외
|
||||||
|
const repayDue = ref({ total: 0, count: 0, hadAny: false })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
@@ -143,17 +146,19 @@ async function load() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
// 예산은 유료 전용 — 무료 회원은 호출하지 않음(403/업그레이드 리다이렉트 방지)
|
// 예산은 유료 전용 — 무료 회원은 호출하지 않음(403/업그레이드 리다이렉트 방지)
|
||||||
const [sum, nw, status, list] = await Promise.all([
|
const [sum, nw, status, list, ws] = await Promise.all([
|
||||||
accountApi.summary({ year, month }),
|
accountApi.summary({ year, month }),
|
||||||
accountApi.netWorth(),
|
accountApi.netWorth(),
|
||||||
auth.isPremium ? accountApi.budgetStatus({ year, month }) : Promise.resolve([]),
|
auth.isPremium ? accountApi.budgetStatus({ year, month }) : Promise.resolve([]),
|
||||||
accountApi.list({ year, month }),
|
accountApi.list({ year, month }),
|
||||||
|
accountApi.wallets(),
|
||||||
])
|
])
|
||||||
summary.value = sum
|
summary.value = sum
|
||||||
networth.value = nw
|
networth.value = nw
|
||||||
budgetTotal.value = (status || []).reduce((s, x) => s + (x.monthlyBudget || 0), 0)
|
budgetTotal.value = (status || []).reduce((s, x) => s + (x.monthlyBudget || 0), 0)
|
||||||
spentTotal.value = (status || []).reduce((s, x) => s + (x.spent || 0), 0)
|
spentTotal.value = (status || []).reduce((s, x) => s + (x.spent || 0), 0)
|
||||||
entries.value = list || []
|
entries.value = list || []
|
||||||
|
await computeRepayDue(ws || [], list || [])
|
||||||
// 오늘 기록 여부로 가계부 리마인더 보정(오늘 기록 있으면 오늘자 알림 제외)
|
// 오늘 기록 여부로 가계부 리마인더 보정(오늘 기록 있으면 오늘자 알림 제외)
|
||||||
if (reminders.isNative()) {
|
if (reminders.isNative()) {
|
||||||
const t = new Date()
|
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) => {
|
watch(() => auth.isAuthenticated, (v) => {
|
||||||
if (v) load()
|
if (v) load()
|
||||||
@@ -176,6 +229,7 @@ watch(() => auth.isAuthenticated, (v) => {
|
|||||||
budgetTotal.value = 0
|
budgetTotal.value = 0
|
||||||
spentTotal.value = 0
|
spentTotal.value = 0
|
||||||
entries.value = []
|
entries.value = []
|
||||||
|
repayDue.value = { total: 0, count: 0, hadAny: false }
|
||||||
activeDay.value = 0
|
activeDay.value = 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -274,6 +328,22 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</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 budget-card">
|
||||||
<div class="card-head">
|
<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 {
|
.budget-card {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user