- 미확인(pending) 내역에 '확인필요' 배지 + 행 강조 + '확인' 버튼(즉시 확정) - 수정 저장 시 미확인 건은 확정 처리 - 가계부 헤더에 '확인 필요 N건' 카운트 배지 - accountApi: pendingCount/confirmEntry Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,14 @@ export const accountApi = {
|
|||||||
return http.delete(`/account/tags/${id}`)
|
return http.delete(`/account/tags/${id}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 카드 결제 알림 자동인식 (미확인 내역)
|
||||||
|
pendingCount() {
|
||||||
|
return http.get('/account/entries/pending/count')
|
||||||
|
},
|
||||||
|
confirmEntry(id, payload) {
|
||||||
|
return http.post(`/account/entries/${id}/confirm`, payload || {})
|
||||||
|
},
|
||||||
|
|
||||||
// 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환)
|
// 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환)
|
||||||
ocrReceipt(blob) {
|
ocrReceipt(blob) {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
|
|||||||
@@ -14,6 +14,26 @@ const entries = ref([])
|
|||||||
const summary = ref({ totalIncome: 0, totalExpense: 0, balance: 0 })
|
const summary = ref({ totalIncome: 0, totalExpense: 0, balance: 0 })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
const pendingCount = ref(0) // 확인 필요(카드 알림 자동인식) 건수
|
||||||
|
const editingPending = ref(false) // 수정 중인 항목이 미확인 건인지
|
||||||
|
|
||||||
|
async function loadPendingCount() {
|
||||||
|
try {
|
||||||
|
pendingCount.value = (await accountApi.pendingCount()).count || 0
|
||||||
|
} catch {
|
||||||
|
pendingCount.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 미확인 내역 즉시 확정(분류 미지정으로 수락)
|
||||||
|
async function confirmRow(e) {
|
||||||
|
try {
|
||||||
|
await accountApi.confirmEntry(e.id, {})
|
||||||
|
await load()
|
||||||
|
await loadPendingCount()
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.response?.data?.message || '확인 처리에 실패했습니다.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 검색·필터
|
// 검색·필터
|
||||||
const filterOpen = ref(false)
|
const filterOpen = ref(false)
|
||||||
@@ -331,6 +351,7 @@ function todayStr() {
|
|||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
editId.value = null
|
editId.value = null
|
||||||
|
editingPending.value = false
|
||||||
Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, installmentMonths: '' })
|
Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: '', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, installmentMonths: '' })
|
||||||
selectedTagIds.value = []
|
selectedTagIds.value = []
|
||||||
cancelAddCategory()
|
cancelAddCategory()
|
||||||
@@ -339,6 +360,7 @@ function openCreate() {
|
|||||||
}
|
}
|
||||||
function openEdit(e) {
|
function openEdit(e) {
|
||||||
editId.value = e.id
|
editId.value = e.id
|
||||||
|
editingPending.value = !!e.pending
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
entryDate: e.entryDate,
|
entryDate: e.entryDate,
|
||||||
type: e.type,
|
type: e.type,
|
||||||
@@ -428,10 +450,18 @@ async function submit() {
|
|||||||
tagIds: selectedTagIds.value,
|
tagIds: selectedTagIds.value,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (editId.value) await accountApi.update(editId.value, payload)
|
if (editId.value) {
|
||||||
else await accountApi.create(payload)
|
await accountApi.update(editId.value, payload)
|
||||||
|
// 미확인(카드 알림) 건이면 수정 저장 후 확정 처리
|
||||||
|
if (editingPending.value) {
|
||||||
|
await accountApi.confirmEntry(editId.value, {})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await accountApi.create(payload)
|
||||||
|
}
|
||||||
formOpen.value = false
|
formOpen.value = false
|
||||||
await load()
|
await load()
|
||||||
|
await loadPendingCount()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
formError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
formError.value = e.response?.data?.message || '저장에 실패했습니다.'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -460,13 +490,14 @@ onMounted(async () => {
|
|||||||
loadTagOptions()
|
loadTagOptions()
|
||||||
loadWallets()
|
loadWallets()
|
||||||
loadCategories()
|
loadCategories()
|
||||||
|
loadPendingCount()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="account">
|
<section class="account">
|
||||||
<header class="account-head">
|
<header class="account-head">
|
||||||
<h1>가계부</h1>
|
<h1>가계부<span v-if="pendingCount" class="pending-count">확인 필요 {{ pendingCount }}건</span></h1>
|
||||||
<IconBtn icon="plus" title="내역 추가" variant="primary" @click="openCreate" />
|
<IconBtn icon="plus" title="내역 추가" variant="primary" @click="openCreate" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -535,17 +566,19 @@ onMounted(async () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="day-items">
|
<ul class="day-items">
|
||||||
<li v-for="e in g.items" :key="e.id" class="entry-item">
|
<li v-for="e in g.items" :key="e.id" class="entry-item" :class="{ 'is-pending': e.pending }">
|
||||||
<div class="ei-line1">
|
<div class="ei-line1">
|
||||||
<span class="ei-cat">
|
<span class="ei-cat">
|
||||||
<span class="type-dot" :class="dotClass(e.type)"></span>
|
<span class="type-dot" :class="dotClass(e.type)"></span>
|
||||||
<template v-if="e.type === 'TRANSFER'">이체</template>
|
<template v-if="e.type === 'TRANSFER'">이체</template>
|
||||||
<template v-else>{{ e.category || (e.type === 'INCOME' ? '수입' : '지출') }}</template>
|
<template v-else>{{ e.category || (e.type === 'INCOME' ? '수입' : '지출') }}</template>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="e.pending" class="ei-pending">확인필요</span>
|
||||||
<span v-if="e.type === 'TRANSFER'" class="ei-wallet">{{ e.walletName }} → {{ e.toWalletName }}</span>
|
<span v-if="e.type === 'TRANSFER'" class="ei-wallet">{{ e.walletName }} → {{ e.toWalletName }}</span>
|
||||||
<span v-else-if="e.walletName" class="ei-wallet">{{ e.walletName }}</span>
|
<span v-else-if="e.walletName" class="ei-wallet">{{ e.walletName }}</span>
|
||||||
<span class="ei-amount" :class="amountClass(e.type)">{{ amountSign(e.type) }}{{ won(e.amount) }}</span>
|
<span class="ei-amount" :class="amountClass(e.type)">{{ amountSign(e.type) }}{{ won(e.amount) }}</span>
|
||||||
<span class="ei-act">
|
<span class="ei-act">
|
||||||
|
<button v-if="e.pending" type="button" class="confirm-btn" title="확인(확정)" @click="confirmRow(e)">확인</button>
|
||||||
<IconBtn icon="edit" title="수정" size="sm" @click="openEdit(e)" />
|
<IconBtn icon="edit" title="수정" size="sm" @click="openEdit(e)" />
|
||||||
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="remove(e)" />
|
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="remove(e)" />
|
||||||
</span>
|
</span>
|
||||||
@@ -876,6 +909,39 @@ button.primary {
|
|||||||
padding: 0.5rem 0.1rem;
|
padding: 0.5rem 0.1rem;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
.entry-item.is-pending {
|
||||||
|
background: rgba(230, 126, 34, 0.07);
|
||||||
|
}
|
||||||
|
.ei-pending {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e67e22;
|
||||||
|
border: 1px solid #e67e22;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.02rem 0.3rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.confirm-btn {
|
||||||
|
padding: 0.18rem 0.5rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid hsla(160, 100%, 37%, 1);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: hsla(160, 100%, 37%, 1);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pending-count {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: #e67e22;
|
||||||
|
border: 1px solid #e67e22;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
}
|
||||||
.ei-line1 {
|
.ei-line1 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user