diff --git a/src/views/account/AccountDashboardView.vue b/src/views/account/AccountDashboardView.vue index 377b021..00bf10c 100644 --- a/src/views/account/AccountDashboardView.vue +++ b/src/views/account/AccountDashboardView.vue @@ -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