From b1f85cee5240f36cbd7ca61e49d2474581c6bad1 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Mon, 22 Jun 2026 22:38:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=86=B5=EA=B3=84=C2=B7=EA=B0=80?= =?UTF-8?q?=EA=B3=84=EB=B6=80=20UX=20=EA=B0=9C=EC=84=A0=20+=20=EA=B3=84?= =?UTF-8?q?=EC=A2=8C=20=ED=83=AD=20=ED=95=9C=20=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 통계: 순자산 추이 주간(최근 3개월), 이번달 예상 지출(run-rate), 지난달 대비 지출 - 가계부: 내역→고정지출 바로 등록, 날짜별 접기/펴기(기본 접힘·오늘만 펼침·모두 토글) - 계좌 관리 탭 한 줄(넘치면 가로 스크롤) - 릴리스 노트 갱신(23~32) Co-Authored-By: Claude Opus 4.8 --- docs/release-2026-06-06.md | 36 ++++++ src/api/accountApi.js | 4 +- src/views/account/AccountDashboardView.vue | 90 ++++++++++++-- src/views/account/AccountView.vue | 136 ++++++++++++++++++++- src/views/account/AccountWalletView.vue | 9 +- 5 files changed, 256 insertions(+), 19 deletions(-) diff --git a/docs/release-2026-06-06.md b/docs/release-2026-06-06.md index 72de242..fd2e174 100644 --- a/docs/release-2026-06-06.md +++ b/docs/release-2026-06-06.md @@ -113,6 +113,42 @@ --- +# (이어진 작업 3) 현금 계좌 · 통계 · 가계부 UX (2026-06-22) + +## 23. 상환/납부 — 연회비(카드) +- 카드 상환 시 **연회비**도 분리 입력(대상이 카드일 때만 노출). 원금=이체 · 이자=지출(분류 '이자') · **연회비=지출(분류 '연회비')**. 대출엔 연회비 입력 불가. + +## 24. 현금 계좌 +- **계좌 관리에 '현금' 유형 추가**(이름·초기 잔액만). 순자산 계산에 **자산으로 합산**(BANK와 동일 취급). +- **가계부 입력**: 현금 선택 시 등록된 현금 계좌를 고를 수 있고, 없으면 종전처럼 '미지정'으로 기록(하위호환). +- **이체에도 현금 지원**: 출금/입금 종류에 현금 포함 → **ATM 출금(은행→현금)·현금 입금(현금→은행)** 등 현금 계좌 이체 가능. 중복되던 별도 '현금' 라디오 제거(일반 계좌 종류로 통합). + +## 25. 고정 지출 — '확인 필요(pending)'로 생성 +- 자동 생성되는 고정지출 내역을 **'확인 필요'**로 만들어, 실제 결제·이체를 확인한 뒤 **'확인'으로 확정**(카드 알림과 동일 흐름). 메모 없으면 제목으로 채워 식별 쉽게. → "실제 처리 여부"를 사용자가 판단 가능. + +## 26. 가계부 내역 → 고정지출 바로 등록 +- 내역 **수정 모달에 「🔁 이 내역을 고정지출로 등록」** 버튼. 현재 항목의 종류·금액·분류·메모·계좌로 **매월** 고정지출 생성(일자=내역 날짜, 시작=다음날로 이번 회차 중복 방지). 주기는 고정지출 화면에서 변경. + +## 27. 통계 — 순자산 추이 주간화 +- 순자산 추이를 **최근 3개월·주간(월요일 기준 13주)**으로 변경. 백엔드 `weeklyWalletFlow` + `netWorthTrendWeekly` 추가, `/networth/trend?unit=WEEK&weeks=N`. x축 라벨은 격주 표기(겹침 방지). + +## 28. 통계 — 이번달 예상 지출(월말 추정) +- 대시보드 '수입 대비 지출'에 **예상 지출** 표시: `현재까지 지출 ÷ 경과일 × 그 달 총일수`(run-rate). 이번 달에만 표시, 예산 대비 %도 함께. + +## 29. 통계 — 지난달 대비 이번달 지출 +- 같은 패널에 **「지난달 대비」** 행: `±차액 (±%)`(늘면 빨강·줄면 초록), 기준 '지난달 N원' 표기. 선택 월 기준으로 직전 달과 비교. + +## 30. 가계부 — 날짜별 접기/펴기 +- 내역이 많을 때를 위해 **날짜 그룹 접기/펴기**. 기본은 **접힘 + 오늘만 펼침**(날짜 요약 = 날짜·건수·일 합계), 헤더 탭으로 개별 토글, 상단 **「모두 펴기/접기」** 토글. 월 이동 시 초기화, 내역 편집 시엔 펼침 상태 유지. + +## 31. 앱(안드로이드) — 키보드가 입력폼 가리던 문제 +- 에지투에지 환경에서 **IME(키보드) 인셋을 콘텐츠 패딩에 반영** → 키보드가 뜨면 WebView가 키보드 위로 줄어들어 입력 모달이 가려지지 않음(`windowSoftInputMode=adjustResize` + 인셋 리스너). 모든 모달 입력 공통. + +## 32. 계좌 관리 — 탭 한 줄 +- 계좌 종류 탭(은행/현금/카드/대출/투자)이 두 줄로 줄바꿈되던 것 → **한 줄(넘치면 가로 스크롤)**. 버튼 글자 줄바꿈 방지. + +--- + ## 운영 반영 시 체크 - 서버 `/opt/sb-backend/.env` - `ACCOUNT_CRYPTO_KEY` — 계좌번호 암호화를 켤 때만(미설정 시 평문, 깨지지 않음). diff --git a/src/api/accountApi.js b/src/api/accountApi.js index 6d180d9..2611b27 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -14,8 +14,8 @@ export const accountApi = { categoryStats({ type, year, month } = {}) { return http.get('/account/category-stats', { params: { type, year, month } }) }, - netWorthTrend({ months } = {}) { - return http.get('/account/networth/trend', { params: { months } }) + netWorthTrend({ months, weeks, unit } = {}) { + return http.get('/account/networth/trend', { params: { months, weeks, unit } }) }, create(payload) { return http.post('/account/entries', payload) diff --git a/src/views/account/AccountDashboardView.vue b/src/views/account/AccountDashboardView.vue index dfaa079..952e050 100644 --- a/src/views/account/AccountDashboardView.vue +++ b/src/views/account/AccountDashboardView.vue @@ -17,6 +17,7 @@ const budgetTotal = ref(0) const spentTotal = ref(0) const monthIncome = ref(0) const monthExpense = ref(0) +const prevMonthExpense = ref(0) // 선택 월의 직전 달 지출 (대비용) const barData = ref([]) // [{bucket, budget, expense}] const monthlyStats = ref([]) // MONTH unit of year (income/expense) const catType = ref('EXPENSE') // 분류별 파이차트 구분 @@ -52,6 +53,25 @@ const donutColor = computed(() => { const ieTotal = computed(() => monthIncome.value + monthExpense.value) const incomePct = computed(() => (ieTotal.value > 0 ? (monthIncome.value / ieTotal.value) * 100 : 0)) +/* ===== 지난달 대비 이번달 지출 ===== */ +const expenseDelta = computed(() => { + const prev = prevMonthExpense.value + const diff = monthExpense.value - prev + const pct = prev > 0 ? Math.round((diff / prev) * 1000) / 10 : null + 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 } +}) + /* ===== 막대 차트 (기간별 예산 대비 지출) ===== */ const bars = computed(() => { const map = {} @@ -123,13 +143,27 @@ const trendView = computed(() => { const max = Math.max(...vals, 0) const span = max - min || 1 const n = data.length - const pts = data.map((d, i) => ({ - x: n === 1 ? 50 : (i / (n - 1)) * 100, - y: 100 - ((d.netWorth - min) / span) * 100, - month: d.month, - netWorth: d.netWorth, - label: d.month.slice(5), // MM - })) + const weekly = data[0].month.length > 7 // 주간이면 'yyyy-MM-dd' + const pts = data.map((d, i) => { + let label, tip + if (weekly) { + const [, mm, dd] = d.month.split('-') + label = `${+mm}/${+dd}` + tip = `${+mm}/${+dd} 주` + } else { + label = d.month.slice(5) // MM + tip = d.month + } + return { + x: n === 1 ? 50 : (i / (n - 1)) * 100, + y: 100 - ((d.netWorth - min) / span) * 100, + month: tip, + netWorth: d.netWorth, + label, + // 점이 많으면(주간) 축 라벨을 격주로만 표기해 겹침 방지 + showLabel: n <= 8 || i % 2 === 0 || i === n - 1, + } + }) const line = pts.map((p) => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') const area = `${pts[0].x.toFixed(2)},100 ` + line + ` ${pts[n - 1].x.toFixed(2)},100` const zeroY = min < 0 && max > 0 ? 100 - ((0 - min) / span) * 100 : null @@ -160,7 +194,7 @@ function setCatType(t) { } async function loadTrend() { try { - trendData.value = await accountApi.netWorthTrend({ months: 12 }) + trendData.value = await accountApi.netWorthTrend({ unit: 'WEEK', weeks: 13 }) } catch { trendData.value = [] } @@ -188,9 +222,12 @@ async function load() { loading.value = true error.value = null try { - const [status, summary] = await Promise.all([ + const pmY = month.value === 1 ? year.value - 1 : year.value + const pmM = month.value === 1 ? 12 : month.value - 1 + const [status, summary, prevSummary] = await Promise.all([ accountApi.budgetStatus({ year: year.value, month: month.value }), accountApi.summary({ year: year.value, month: month.value }), + accountApi.summary({ year: pmY, month: pmM }), loadBar(), loadCat(), ]) @@ -198,6 +235,7 @@ async function load() { spentTotal.value = status.reduce((s, x) => s + x.spent, 0) monthIncome.value = summary.totalIncome monthExpense.value = summary.totalExpense + prevMonthExpense.value = prevSummary.totalExpense } catch (e) { error.value = e.response?.data?.message || '불러오지 못했습니다.' } finally { @@ -301,10 +339,23 @@ onMounted(async () => {
수입{{ won(monthIncome) }}
지출{{ won(monthExpense) }}
+
+ 지난달 대비 지난달 {{ won(expenseDelta.prev) }} + + {{ expenseDelta.diff >= 0 ? '+' : '' }}{{ won(expenseDelta.diff) }} + +
+
+ 예상 지출 월말 추정 + {{ won(expenseForecast.projected) }} +
수지{{ won(monthIncome - monthExpense) }}
+

+ 최근 추세 기준({{ expenseForecast.elapsed }}/{{ expenseForecast.totalDays }}일 · 현재 {{ won(monthExpense) }}) +

@@ -377,9 +428,9 @@ onMounted(async () => {

데이터가 없습니다.

- +
-

순자산 추이 최근 12개월

+

순자산 추이 최근 3개월 · 주간

{ @mouseenter="showTrendTip(p)" >
- {{ p.label }} + {{ p.showLabel ? p.label : '' }}
{{ trendTip.month }}
@@ -523,6 +574,21 @@ h2 { padding: 0.2rem 0; font-size: 0.9rem; } +.ie-row.forecast { + opacity: 0.85; +} +.ie-row.forecast small, +.ie-row.compare small { + font-size: 0.72rem; + opacity: 0.6; + font-weight: 400; + margin-left: 0.25rem; +} +.forecast-note { + margin: 0.3rem 0 0; + font-size: 0.72rem; + opacity: 0.6; +} .ie-row.net { border-top: 1px solid var(--color-border); margin-top: 0.3rem; diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index c4b4eef..99c11f5 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -1,5 +1,5 @@