feat(stats): 예상 지출을 AI 추정으로 교체 — 일회성 큰 지출 왜곡 해결
Deploy / deploy (push) Failing after 10m53s

선형 계산(expenseForecast) 제거, AI 코멘트의 forecast/근거 표시. 경과일·총일수 전달.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-05 15:29:16 +09:00
parent 9f9fc14c64
commit e9e397434f
+17 -16
View File
@@ -26,6 +26,8 @@ const categoryList = ref([]) // 분류 목록(대/소분류 매핑용)
const trendData = ref([]) // [{month, netWorth}]
const aiBullets = ref([]) // AI 재무 코멘트(현황 관찰)
const aiTips = ref([]) // AI 절약 가이드(실행 제안)
const aiForecast = ref(null) // AI 월말 예상 지출(일회성 큰 지출 제외 추정)
const aiForecastNote = ref('') // 예상 지출 근거 문장
const aiLoading = ref(false)
const periodLabel = computed(() => `${year.value}${String(month.value).padStart(2, '0')}`)
@@ -65,16 +67,8 @@ const expenseDelta = computed(() => {
return { prev, diff, pct }
})
/* ===== 이번달 예상 지출 (실제 지출 추세로 월말 추정, run-rate) =====
경과일까지 실제 지출을 일평균으로 환산해 그 달 총일수로 추정. 이번 달에만 표시. */
const expenseForecast = computed(() => {
const isCurrent = year.value === now.getFullYear() && month.value === now.getMonth() + 1
if (!isCurrent) return null // 지난/미래 달은 추정 불필요(이미 확정 또는 데이터 없음)
const totalDays = daysInMonth(year.value, month.value)
const elapsed = now.getDate()
const projected = elapsed > 0 ? Math.round((monthExpense.value / elapsed) * totalDays) : monthExpense.value
return { projected, elapsed, totalDays }
})
/* 월말 예상 지출은 단순 선형(run-rate) 대신 AI가 추정한다(aiForecast).
일회성 큰 지출(노트북 등)을 일평균에서 제외해 현실적인 값을 준다. loadAiComment 참고. */
/* ===== 막대 차트 (기간별 예산 대비 지출) ===== */
const bars = computed(() => {
@@ -280,7 +274,10 @@ async function loadAiComment() {
aiLoading.value = true
aiBullets.value = []
aiTips.value = []
aiForecast.value = null
aiForecastNote.value = ''
try {
const isCurrent = year.value === now.getFullYear() && month.value === now.getMonth() + 1
const res = await accountApi.aiComment({
year: year.value,
month: month.value,
@@ -289,13 +286,19 @@ async function loadAiComment() {
prevExpense: prevMonthExpense.value,
budget: budgetTotal.value,
spent: spentTotal.value,
elapsedDays: isCurrent ? now.getDate() : 0,
totalDays: daysInMonth(year.value, month.value),
categories: (catData.value || []).slice(0, 8).map((c) => ({ category: c.category, total: c.total })),
})
aiBullets.value = res?.bullets || []
aiTips.value = res?.tips || []
aiForecast.value = res?.forecast || null
aiForecastNote.value = res?.forecastNote || ''
} catch {
aiBullets.value = []
aiTips.value = []
aiForecast.value = null
aiForecastNote.value = ''
} finally {
aiLoading.value = false
}
@@ -417,17 +420,15 @@ onMounted(async () => {
{{ expenseDelta.diff >= 0 ? '+' : '' }}{{ won(expenseDelta.diff) }}<template v-if="expenseDelta.pct !== null"> ({{ expenseDelta.pct >= 0 ? '+' : '' }}{{ expenseDelta.pct }}%)</template>
</b>
</div>
<div v-if="expenseForecast" class="ie-row forecast">
<span>예상 지출 <small>월말 추정</small></span>
<b class="expense">{{ won(expenseForecast.projected) }}</b>
<div v-if="aiForecast" class="ie-row forecast">
<span>예상 지출 <small>월말 추정 · AI</small></span>
<b class="expense">{{ won(aiForecast) }}</b>
</div>
<div class="ie-bar">
<div class="ie-fill income" :style="{ width: incomePct + '%' }"></div>
</div>
<div class="ie-row net"><span>수지</span><b :class="monthIncome - monthExpense < 0 ? 'expense' : 'income'">{{ won(monthIncome - monthExpense) }}</b></div>
<p v-if="expenseForecast" class="forecast-note">
최근 추세 기준({{ expenseForecast.elapsed }}/{{ expenseForecast.totalDays }} · 현재 {{ won(monthExpense) }})<template v-if="budgetTotal > 0">, 예산 대비 {{ Math.round((expenseForecast.projected / budgetTotal) * 100) }}%</template>
</p>
<p v-if="aiForecastNote" class="forecast-note">{{ aiForecastNote }}</p>
</div>
</div>
</div>