fix(stats): 예상지출 — 비정기 큰 지출(대출상환 등)을 모두 일평균에서 제외
Deploy / deploy (push) Failing after 14m58s

가장 큰 하루 1건만 제외하던 것을, 중앙값 3배(또는 10만원) 초과 '모든' 날 제외로 변경.
대출상환·다건 일회성이 일평균에 남아 남은 날에 곱해져 예상지출이 과다 추정되던 문제 해결.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-06 02:01:33 +09:00
parent a52ac6532c
commit 5b46ba93f3
+21 -10
View File
@@ -72,16 +72,27 @@ const expenseDelta = computed(() => {
일별 지출을 받아 '평소 하루 지출'을 구하되, 일회성 큰 지출(노트북 등)이 있는 날은
일평균을 왜곡하므로 제외한다. 추정 = 실제 지출(일회성 포함) + 남은 날 × 평소 하루 지출. */
function robustDailyRate(days) {
if (!days.length) return { rate: 0, dropped: 0 }
if (!days.length) return { rate: 0, dropped: 0, droppedCount: 0 }
const sorted = [...days].sort((a, b) => a - b)
const mid = Math.floor(sorted.length / 2)
const median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
const max = sorted[sorted.length - 1]
// 최대 지출일이 중앙값의 3배(그리고 10만원)를 넘으면 일회성으로 보고 하루평균 산정에서 제외
const dropOutlier = sorted.length >= 4 && max > Math.max(median * 3, 100000)
const base = dropOutlier ? sorted.slice(0, -1) : sorted
const rate = base.reduce((a, b) => a + b, 0) / base.length
return { rate, dropped: dropOutlier ? max : 0 }
// 비정기 큰 지출(대출상환·가전·여행 등)은 남은 날에 반복되지 않으므로,
// 중앙값의 3배(또는 10만원)를 넘는 '모든' 날을 하루평균 산정에서 제외(가장 큰 1건만이 아님).
const threshold = Math.max(median * 3, 100000)
const canTrim = sorted.length >= 4
const kept = []
let dropped = 0
let droppedCount = 0
for (const d of sorted) {
if (canTrim && d > threshold) {
dropped += d
droppedCount++
} else {
kept.push(d)
}
}
const rate = kept.length ? kept.reduce((a, b) => a + b, 0) / kept.length : 0
return { rate, dropped, droppedCount }
}
async function loadForecast() {
forecast.value = null
@@ -98,12 +109,12 @@ async function loadForecast() {
;(daily || []).forEach((d) => (map[String(d.bucket)] = d.expense || 0))
const days = []
for (let d = 1; d <= elapsed; d++) days.push(map[String(d)] || 0)
const { rate, dropped } = robustDailyRate(days)
const { rate, dropped, droppedCount } = robustDailyRate(days)
const remaining = totalDays - elapsed
forecast.value = Math.round(monthExpense.value + rate * remaining)
forecastNote.value =
dropped > 0
? `가장 큰 하루 지출(${won(dropped)})은 일회성으로 보고 남은 ${remaining}일 추정에서 제외`
droppedCount > 0
? `대출상환 등 비정기 큰 지출 ${droppedCount}건(합계 ${won(dropped)})은 남은 ${remaining}일 추정에서 제외`
: `최근 하루 평균 지출로 남은 ${remaining}일을 더한 추정`
} catch {
forecast.value = null