feat(account): 통계 화면에 AI 재무 코멘트 카드
Deploy / deploy (push) Failing after 14m16s

집계만 /account/ai-comment 로 보내 코멘트 수신, 상단에 카드 표시(로딩·빈값 처리).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-05 15:06:56 +09:00
parent 5660451f04
commit 7affe5c318
2 changed files with 70 additions and 1 deletions
+5 -1
View File
@@ -15,6 +15,10 @@ const realApi = {
categoryStats({ type, year, month } = {}) {
return http.get('/account/category-stats', { params: { type, year, month } })
},
// 통계 화면 집계 → AI 재무 코멘트(유료). 실패해도 화면을 막지 않도록 전역 배너 억제
aiComment(payload) {
return http.post('/account/ai-comment', payload, { timeout: 30000, suppressNetError: true })
},
netWorthTrend({ months, weeks, unit } = {}) {
return http.get('/account/networth/trend', { params: { months, weeks, unit } })
},
@@ -262,7 +266,7 @@ const DEMO_WRITES = new Set([
'createRecurring', 'updateRecurring', 'removeRecurring', 'runRecurrings',
'createWallet', 'updateWallet', 'removeWallet', 'reorderWallets',
'createCategory', 'updateCategory', 'removeCategory', 'reorderCategories', 'importCategories',
'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'parseReceiptAi', 'restore',
'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'parseReceiptAi', 'aiComment', 'restore',
'createBudget', 'updateBudget', 'removeBudget', 'setExpectedIncome',
'createHolding', 'updateHolding', 'removeHolding', 'addTrade', 'updateTrade', 'removeTrade',
'refreshPrices', 'refreshAllPrices',
@@ -24,6 +24,8 @@ const catType = ref('EXPENSE') // 분류별 파이차트 구분
const catData = ref([]) // [{category, total}]
const categoryList = ref([]) // 분류 목록(대/소분류 매핑용)
const trendData = ref([]) // [{month, netWorth}]
const aiBullets = ref([]) // AI 재무 코멘트 문장들
const aiLoading = ref(false)
const periodLabel = computed(() => `${year.value}${String(month.value).padStart(2, '0')}`)
@@ -264,6 +266,7 @@ async function load() {
monthIncome.value = summary.totalIncome
monthExpense.value = summary.totalExpense
prevMonthExpense.value = prevSummary.totalExpense
loadAiComment() // 집계가 준비된 뒤 AI 코멘트 생성(비동기, 화면 로딩 막지 않음)
} catch (e) {
error.value = e.response?.data?.message || '불러오지 못했습니다.'
} finally {
@@ -271,6 +274,29 @@ async function load() {
}
}
// 이번 달 집계만 백엔드로 보내 AI 재무 코멘트를 받는다(거래 원문 미전송, 실패해도 무중단)
async function loadAiComment() {
aiLoading.value = true
aiBullets.value = []
try {
const res = await accountApi.aiComment({
year: year.value,
month: month.value,
income: monthIncome.value,
expense: monthExpense.value,
prevExpense: prevMonthExpense.value,
budget: budgetTotal.value,
spent: spentTotal.value,
categories: (catData.value || []).slice(0, 8).map((c) => ({ category: c.category, total: c.total })),
})
aiBullets.value = res?.bullets || []
} catch {
aiBullets.value = []
} finally {
aiLoading.value = false
}
}
function setUnit(u) {
unit.value = u
loadBar()
@@ -335,6 +361,15 @@ onMounted(async () => {
<p v-if="loading" class="msg">불러오는 중...</p>
<template v-else>
<!-- AI 재무 코멘트: 이번 집계 해석 -->
<div v-if="aiLoading || aiBullets.length" class="ai-comment">
<div class="ai-comment-head"> 이번 AI 코멘트</div>
<p v-if="aiLoading" class="ai-comment-loading">집계를 분석하고 있어요</p>
<ul v-else class="ai-comment-list">
<li v-for="(b, i) in aiBullets" :key="i">{{ b }}</li>
</ul>
</div>
<!-- 상단: 예산 대비 지출(도넛) + 수입 대비 지출 -->
<div class="top-grid">
<div class="panel donut-panel">
@@ -551,6 +586,36 @@ h2 {
padding: 1rem 1.1rem;
margin-bottom: 1rem;
}
/* AI 재무 코멘트 카드 */
.ai-comment {
border: 1px solid hsla(160, 100%, 37%, 0.45);
border-radius: 12px;
background: hsla(160, 100%, 37%, 0.07);
padding: 0.9rem 1.1rem;
margin-bottom: 1rem;
}
.ai-comment-head {
font-weight: 700;
font-size: 0.95rem;
color: hsla(160, 100%, 32%, 1);
margin-bottom: 0.5rem;
}
.ai-comment-loading {
font-size: 0.85rem;
opacity: 0.6;
margin: 0;
}
.ai-comment-list {
margin: 0;
padding-left: 1.1rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.ai-comment-list li {
font-size: 0.9rem;
line-height: 1.45;
}
.panel-head {
display: flex;
align-items: center;