From 5b46ba93f336d9d6357bd948f7b9877d8236c878 Mon Sep 17 00:00:00 2001 From: sb Date: Mon, 6 Jul 2026 02:01:33 +0900 Subject: [PATCH] =?UTF-8?q?fix(stats):=20=EC=98=88=EC=83=81=EC=A7=80?= =?UTF-8?q?=EC=B6=9C=20=E2=80=94=20=EB=B9=84=EC=A0=95=EA=B8=B0=20=ED=81=B0?= =?UTF-8?q?=20=EC=A7=80=EC=B6=9C(=EB=8C=80=EC=B6=9C=EC=83=81=ED=99=98=20?= =?UTF-8?q?=EB=93=B1)=EC=9D=84=20=EB=AA=A8=EB=91=90=20=EC=9D=BC=ED=8F=89?= =?UTF-8?q?=EA=B7=A0=EC=97=90=EC=84=9C=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 가장 큰 하루 1건만 제외하던 것을, 중앙값 3배(또는 10만원) 초과 '모든' 날 제외로 변경. 대출상환·다건 일회성이 일평균에 남아 남은 날에 곱해져 예상지출이 과다 추정되던 문제 해결. Co-Authored-By: Claude Opus 4.8 --- src/views/account/AccountDashboardView.vue | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) 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