From 87cd0be64fa733ccb8699fc040198a6cfd87155d Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 13 Aug 2026 17:37:07 +0900 Subject: [PATCH] Add ledger search and filters --- docs/changelog.md | 2 + pages/index.vue | 135 ++++++++++++++++++++++++++- server/api/transactions/index.get.ts | 19 +++- todo.md | 3 - 4 files changed, 152 insertions(+), 7 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 77c5a3b..364fd52 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -30,3 +30,5 @@ - 월 지출 하단 설명을 수입/차액 대신 지난달 대비 지출 비교 문구로 단순화했다. - 하단 네비게이션과 PC 사이드바에 SVG 메뉴 아이콘을 적용하고 활성 탭 색상을 명확하게 표시했다. - SVG 메뉴 아이콘이 사각형으로 보이는 문제를 수정하고 활성 아이콘 배경을 추가했다. +- 내역 검색과 유형/자산/카테고리 필터 바텀 시트를 구현했다. +- 거래 목록 API에 검색어, 유형, 자산, 카테고리 필터 query를 추가했다. diff --git a/pages/index.vue b/pages/index.vue index 3a86072..fec8284 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -7,11 +7,23 @@

최근 이용 내역

- - + +
+
+ +
+
+
+
+ +
+
+

유형

+
+ +
+
+ +
+

자산

+
+ +
+
+ +
+

카테고리

+
+ +
+
+
+ +
+ + +
+ + + @@ -97,19 +181,40 @@ import type { Transaction } from '~/composables/useMoney' const selectedDate = ref(formatDateOnly()) const weekSwipeStartX = ref(0) +const searchOpen = ref(false) +const filterSheetOpen = ref(false) +const searchKeyword = ref('') +const filters = reactive({ + type: '', + assetId: '', + categoryId: '' +}) const selectedMonth = computed(() => monthKey(parse(selectedDate.value, 'yyyy-MM-dd', new Date()))) const previousMonth = computed(() => { const date = parse(selectedMonth.value, 'yyyy-MM', new Date()) return format(addMonths(date, -1), 'yyyy-MM') }) const { data } = await useFetch('/api/transactions', { - query: { month: selectedMonth } + query: { + month: selectedMonth, + q: searchKeyword, + type: computed(() => filters.type), + assetId: computed(() => filters.assetId), + categoryId: computed(() => filters.categoryId) + } }) const { data: previousData } = await useFetch('/api/transactions', { key: computed(() => `transactions-previous-${previousMonth.value}`), query: { month: previousMonth } }) +const { data: assetData } = await useFetch('/api/assets') +const { data: categoryData } = await useFetch('/api/categories') const weekdays = ['일', '월', '화', '수', '목', '금', '토'] +const typeOptions = [ + { value: '', label: '전체' }, + { value: 'income', label: '수입' }, + { value: 'expense', label: '지출' } +] const groupedTransactions = computed(() => { const groups = new Map() @@ -154,6 +259,23 @@ const monthlyComparisonLabel = computed(() => { if (diff < 0) return `지난달보다 ${compactWon(Math.abs(diff))} 더 쓰는 중` return '지난달과 같은 속도로 쓰는 중' }) +const activeFilterCount = computed(() => [filters.type, filters.assetId, filters.categoryId, searchKeyword.value.trim()].filter(Boolean).length) +const assetFilterOptions = computed(() => [ + { value: '', label: '전체 자산' }, + ...(assetData.value || []).map((asset: any) => ({ value: asset.id, label: asset.name })) +]) +const categoryFilterOptions = computed(() => [ + { value: '', label: '전체 카테고리' }, + ...(categoryData.value || []) + .filter((category: any) => !filters.type || category.type === filters.type) + .map((category: any) => ({ value: category.id, label: category.name })) +]) + +watch(() => filters.type, () => { + if (!filters.categoryId) return + const selected = (categoryData.value || []).find((category: any) => category.id === filters.categoryId) + if (selected && selected.type !== filters.type) filters.categoryId = '' +}) function moveMonth(amount: number) { const date = parse(selectedDate.value, 'yyyy-MM-dd', new Date()) @@ -210,6 +332,13 @@ function endWeekSwipe(event: TouchEvent) { moveWeek(diff < 0 ? 1 : -1) } +function resetFilters() { + filters.type = '' + filters.assetId = '' + filters.categoryId = '' + searchKeyword.value = '' +} + function goBack() { if (history.length > 1) history.back() } diff --git a/server/api/transactions/index.get.ts b/server/api/transactions/index.get.ts index 8bc2f16..2109890 100644 --- a/server/api/transactions/index.get.ts +++ b/server/api/transactions/index.get.ts @@ -7,13 +7,30 @@ 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 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 transactions = await prisma.transaction.findMany({ where: { userId: user.id, - occurredAt: { gte: monthStart, lte: monthEnd } + occurredAt: { gte: monthStart, lte: monthEnd }, + ...(type ? { type } : {}), + ...(assetId ? { assetId } : {}), + ...(categoryId ? { categoryId } : {}), + ...(q + ? { + OR: [ + { title: { contains: q, mode: 'insensitive' } }, + { memo: { contains: q, mode: 'insensitive' } }, + { asset: { name: { contains: q, mode: 'insensitive' } } }, + { category: { name: { contains: q, mode: 'insensitive' } } } + ] + } + : {}) }, include: { asset: true, diff --git a/todo.md b/todo.md index c919813..938ac2a 100644 --- a/todo.md +++ b/todo.md @@ -11,10 +11,7 @@ ## 2. 내역 화면 -- 검색 버튼 동작을 구현한다. -- 필터 버튼을 바텀 시트로 연결하고 유형, 자산, 카테고리 필터를 제공한다. - 월 제목을 누르면 월 선택 바텀 시트가 열리도록 구현한다. -- 주간 날짜 스트립의 스와이프/이동 동작을 더 자연스럽게 다듬는다. - 날짜별 수입/지출 표시가 좁은 화면에서도 겹치지 않도록 최종 조정한다. - 거래 행 왼쪽 아이콘을 카테고리별 아이콘으로 표시한다. - 거래 취소/환불 같은 상태 표시 규칙을 정한다.