From 053ca55a4267804bd378bc1a0bfd87d3e14e0d40 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 14 Aug 2026 12:11:40 +0900 Subject: [PATCH] Add asset detail period views --- docs/changelog.md | 2 + pages/assets.vue | 171 ++++++++++++++++++++++----- server/api/transactions/index.get.ts | 7 +- todo.md | 17 +-- 4 files changed, 154 insertions(+), 43 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 6664894..18d10a2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -73,3 +73,5 @@ - 자산 화면을 넓은 화면에서 카드 그리드와 상세 2열 구조로 보이도록 조정했다. - 거래 상세 화면의 자주사용 버튼을 실제 템플릿 저장 기능과 연결하고, 거래 추가 화면에서 자주사용 내역을 바텀시트로 불러올 수 있도록 했다. - 거래 삭제 확인을 브라우저 기본 confirm 대신 앱 스타일의 바텀시트로 변경했다. +- 거래 목록 API가 월간 조회뿐 아니라 연간 조회도 받을 수 있도록 확장했다. +- 자산 상세 화면의 월 이동 위치를 헤더 아래 공통 구조로 맞추고, 일별/월별/연별 보기 전환과 전환 모션을 추가했다. diff --git a/pages/assets.vue b/pages/assets.vue index 3e52f0c..6366836 100644 --- a/pages/assets.vue +++ b/pages/assets.vue @@ -17,7 +17,7 @@ -
+
@@ -35,6 +35,25 @@

{{ formatWon(totalBalance) }}

+ +
+
+ + {{ selectedAsset.balanceType === 'cumulative' ? '누적형' : '월별' }} +
+
+ +
+
-

현재 표시 잔액

+

{{ detailSummaryTitle }}

{{ formatWon(selectedAsset.balance || 0) }}

입금

-

{{ formatWon(assetSummary.income) }}

+

{{ formatWon(currentSummary.income) }}

출금

-

{{ formatWon(assetSummary.expense) }}

+

{{ formatWon(currentSummary.expense) }}

합계

-

{{ formatWon(assetSummary.diff) }}

+

{{ formatWon(currentSummary.diff) }}

-
-
-
-
- {{ group.day }} - {{ group.weekday }} - {{ group.monthLabel }} + +
+
+
+
+ {{ group.day }} + {{ group.weekday }} + {{ group.monthLabel }} +
+
+ {{ formatWon(group.income) }} + {{ formatWon(group.expense) }} +
-
- {{ formatWon(group.income) }} - {{ formatWon(group.expense) }} + + +
+ +

+ 이 자산의 {{ displayMonth }} 거래가 없습니다. +

+
+ +
+ +
+ +
+
+

{{ selectedYear }}년 합계

+

{{ formatWon(yearSummary.diff) }}

+
+
+
+

입금

+

{{ formatWon(yearSummary.income) }}

+
+
+

출금

+

{{ formatWon(yearSummary.expense) }}

+
+
+

거래

+

{{ yearlyAssetTransactions.length }}건

- - -
- -

- 이 자산의 {{ displayMonth }} 거래가 없습니다. -

-
+
+ @@ -113,10 +172,23 @@ import { format, parse } from 'date-fns' import type { Transaction } from '~/composables/useMoney' +type DetailView = 'daily' | 'monthly' | 'yearly' + const selectedMonth = ref(monthKey()) const selectedAssetId = ref('') +const detailView = ref('daily') const { data: assetData } = await useFetch('/api/assets', { query: { month: selectedMonth } }) const { data: transactionData } = await useFetch('/api/transactions', { query: { month: selectedMonth } }) +const selectedYear = computed(() => selectedMonth.value.slice(0, 4)) +const { data: yearTransactionData } = await useFetch('/api/transactions', { + key: computed(() => `asset-year-transactions-${selectedYear.value}`), + query: { year: selectedYear } +}) +const detailViewOptions = [ + { value: 'daily', label: '일별' }, + { value: 'monthly', label: '월별' }, + { value: 'yearly', label: '연별' } +] as const const assets = computed(() => assetData.value || []) const selectedAsset = computed(() => assets.value.find((asset: any) => asset.id === selectedAssetId.value)) @@ -130,13 +202,39 @@ const totalBalance = computed(() => assets.value.reduce((sum: number, asset: any const assetTransactions = computed(() => { return (transactionData.value?.transactions || []).filter((tx: Transaction) => tx.assetId === selectedAssetId.value) }) -const assetSummary = computed(() => { - return assetTransactions.value.reduce((acc: any, tx: Transaction) => { +const yearlyAssetTransactions = computed(() => { + return (yearTransactionData.value?.transactions || []).filter((tx: Transaction) => tx.assetId === selectedAssetId.value) +}) +const monthSummary = computed(() => summarize(assetTransactions.value)) +const yearSummary = computed(() => summarize(yearlyAssetTransactions.value)) +const currentSummary = computed(() => detailView.value === 'daily' ? monthSummary.value : yearSummary.value) +const detailSummaryTitle = computed(() => detailView.value === 'daily' ? `${displayMonth.value} 표시 잔액` : `${selectedYear.value}년 흐름`) +const monthlyRows = computed(() => { + const groups = new Map() + for (const tx of yearlyAssetTransactions.value) { + const key = format(new Date(tx.occurredAt), 'yyyy-MM') + const group = groups.get(key) || { key, label: format(new Date(tx.occurredAt), 'M월'), income: 0, expense: 0, diff: 0 } + group[tx.type] += tx.amount + group.diff = group.income - group.expense + groups.set(key, group) + } + return Array.from({ length: 12 }, (_, index) => { + const key = `${selectedYear.value}-${String(index + 1).padStart(2, '0')}` + return groups.get(key) || { key, label: `${index + 1}월`, income: 0, expense: 0, diff: 0 } + }) +}) + +watch(selectedAssetId, () => { + detailView.value = 'daily' +}) + +function summarize(items: Transaction[]) { + return items.reduce((acc: any, tx: Transaction) => { acc[tx.type] += tx.amount acc.diff = acc.income - acc.expense return acc }, { income: 0, expense: 0, diff: 0 }) -}) +} const groupedTransactions = computed(() => { const groups = new Map() for (const tx of assetTransactions.value) { @@ -159,3 +257,16 @@ const groupedTransactions = computed(() => { }) + + diff --git a/server/api/transactions/index.get.ts b/server/api/transactions/index.get.ts index 2109890..bee020f 100644 --- a/server/api/transactions/index.get.ts +++ b/server/api/transactions/index.get.ts @@ -1,5 +1,5 @@ import { getQuery } from 'h3' -import { startOfMonth, endOfMonth } from 'date-fns' +import { startOfMonth, endOfMonth, startOfYear, endOfYear } from 'date-fns' import { prisma } from '../../utils/db' import { requireUser } from '../../utils/auth' @@ -7,17 +7,20 @@ export default defineEventHandler(async (event) => { const user = await requireUser(event) const query = getQuery(event) const month = typeof query.month === 'string' ? query.month : new Date().toISOString().slice(0, 7) + const year = typeof query.year === 'string' ? query.year : '' const type = query.type === 'income' || query.type === 'expense' ? query.type : undefined const assetId = typeof query.assetId === 'string' && query.assetId ? query.assetId : undefined const categoryId = typeof query.categoryId === 'string' && query.categoryId ? query.categoryId : undefined const q = typeof query.q === 'string' ? query.q.trim() : '' const monthStart = startOfMonth(new Date(`${month}-01T00:00:00`)) const monthEnd = endOfMonth(monthStart) + const rangeStart = year ? startOfYear(new Date(`${year}-01-01T00:00:00`)) : monthStart + const rangeEnd = year ? endOfYear(rangeStart) : monthEnd const transactions = await prisma.transaction.findMany({ where: { userId: user.id, - occurredAt: { gte: monthStart, lte: monthEnd }, + occurredAt: { gte: rangeStart, lte: rangeEnd }, ...(type ? { type } : {}), ...(assetId ? { assetId } : {}), ...(categoryId ? { categoryId } : {}), diff --git a/todo.md b/todo.md index 9d93232..99b9d26 100644 --- a/todo.md +++ b/todo.md @@ -23,40 +23,35 @@ - 카테고리 정렬 순서 변경 기능을 추가한다. -## 5. 자산 - -- 자산 상세 화면의 월 이동 헤더를 공통 구조에 맞춘다. -- 자산 상세에서 일별/월별/연별 보기 전환을 구현한다. - -## 6. 통계 +## 5. 통계 - 기간 선택 바텀 시트를 월간, 연간, 사용자 지정 범위로 확장한다. - 그래프가 필요해질 경우 최소한의 막대/도넛 차트를 추가한다. -## 7. 설정 +## 6. 설정 - 설정 홈의 항목 구성을 다시 정리한다. - 주 화폐, 월 시작일, 주간 시작일 같은 기본 설정 항목을 구현한다. - 설정 변경이 실제 화면 계산과 표시 방식에 반영되도록 연결한다. -## 8. 데이터와 API +## 7. 데이터와 API - 사용자 설정 테이블을 추가한다. - 거래 목록 조회 성능을 고려해 필요한 index를 점검한다. - CSV 내보내기 컬럼을 실제 사용 흐름에 맞게 정리한다. -## 9. 인증과 사용자 경험 +## 8. 인증과 사용자 경험 - 회원 탈퇴 또는 데이터 삭제 정책을 정한다. - 로그인 실패, 세션 만료, 네트워크 오류 상태를 화면에서 더 명확히 보여준다. - 빈 데이터 상태 화면을 페이지별로 다듬는다. -## 10. 공유 +## 9. 공유 - 공유 링크 재생성 기능을 구현한다. - 공유 접근 화면의 자산/통계 표시를 더 풍부하게 만든다. -## 11. 품질 +## 10. 품질 - 핵심 유틸 함수 테스트를 추가한다. - 거래 생성, 수정, 삭제 API 테스트를 추가한다.