Apply user date preferences

This commit is contained in:
2026-08-14 13:19:17 +09:00
parent 0f6bf385eb
commit 59ea372ac7
6 changed files with 47 additions and 21 deletions

View File

@@ -79,3 +79,5 @@
- 설정 홈을 계정, 가계부, 데이터 섹션으로 나누어 각 항목의 목적이 먼저 보이도록 정리했다.
- 플로팅 추가 버튼과 설정 화면 추가 버튼을 텍스트 `+` 대신 `assets/icons/add.svg` 아이콘으로 교체했다.
- 사용자 설정 테이블과 API를 추가하고, 설정 화면에서 주 화폐, 월 시작일, 주간 시작일을 저장할 수 있도록 했다.
- 월별 거래/자산 조회 범위가 사용자 설정의 월 시작일을 따르도록 연결했다.
- 내역 화면의 주간 날짜 스트립 요일 순서와 주 시작 계산이 사용자 설정의 주간 시작일을 따르도록 연결했다.

View File

@@ -175,6 +175,7 @@
<script setup lang="ts">
import { addDays, addMonths, format, parse, startOfWeek } from 'date-fns'
import type { WeekStartsOn } from 'date-fns'
import type { Transaction } from '~/composables/useMoney'
const selectedDate = ref(formatDateOnly())
@@ -188,8 +189,10 @@ const filters = reactive({
assetId: '',
categoryId: ''
})
const { data: preferenceData } = await useFetch('/api/user-settings')
const weekStartDay = computed(() => Math.min(Math.max(preferenceData.value?.weekStartDay ?? 0, 0), 6) as WeekStartsOn)
const selectedMonth = computed(() => monthKey(parse(selectedDate.value, 'yyyy-MM-dd', new Date())))
const weekKey = computed(() => format(startOfWeek(parse(selectedDate.value, 'yyyy-MM-dd', new Date()), { weekStartsOn: 0 }), 'yyyy-MM-dd'))
const weekKey = computed(() => format(startOfWeek(parse(selectedDate.value, 'yyyy-MM-dd', new Date()), { weekStartsOn: weekStartDay.value }), 'yyyy-MM-dd'))
const weekTransitionName = computed(() => weekDirection.value === 'next' ? 'week-next' : 'week-prev')
const selectedMonthModel = computed({
get: () => selectedMonth.value,
@@ -218,7 +221,8 @@ const { data: previousData } = await useFetch('/api/transactions', {
})
const { data: assetData } = await useFetch('/api/assets')
const { data: categoryData } = await useFetch('/api/categories')
const weekdays = ['일', '월', '화', '수', '목', '금', '토']
const weekdayLabels = ['일', '월', '화', '수', '목', '금', '토']
const weekdays = computed(() => Array.from({ length: 7 }, (_, index) => weekdayLabels[(weekStartDay.value + index) % 7]))
const typeOptions = [
{ value: '', label: '전체' },
{ value: 'income', label: '수입' },
@@ -239,7 +243,7 @@ const groupedTransactions = computed(() => {
const weekDays = computed(() => {
const base = parse(selectedDate.value, 'yyyy-MM-dd', new Date())
const start = startOfWeek(base, { weekStartsOn: 0 })
const start = startOfWeek(base, { weekStartsOn: weekStartDay.value })
return Array.from({ length: 7 }, (_, index) => {
const date = addDays(start, index)
const key = format(date, 'yyyy-MM-dd')

View File

@@ -1,8 +1,8 @@
import { getQuery } from 'h3'
import { startOfMonth, endOfMonth } from 'date-fns'
import { prisma } from '../../utils/db'
import { requireUser } from '../../utils/auth'
import { ensureDefaultMoneyData } from '../../utils/defaults'
import { getUserSetting, monthRange } from '../../utils/userSettings'
export default defineEventHandler(async (event) => {
const user = await requireUser(event)
@@ -10,8 +10,8 @@ export default defineEventHandler(async (event) => {
const query = getQuery(event)
const includeInactive = query.includeInactive === 'true'
const month = typeof query.month === 'string' ? query.month : new Date().toISOString().slice(0, 7)
const monthStart = startOfMonth(new Date(`${month}-01T00:00:00`))
const monthEnd = endOfMonth(monthStart)
const setting = await getUserSetting(prisma, user.id)
const range = monthRange(month, setting.monthStartDay)
const assets = await prisma.asset.findMany({
where: { userId: user.id, ...(includeInactive ? {} : { active: true }) },
@@ -23,7 +23,7 @@ export default defineEventHandler(async (event) => {
userId: user.id,
assetId: asset.id,
...(asset.balanceType === 'monthly_reset'
? { occurredAt: { gte: monthStart, lte: monthEnd } }
? { occurredAt: { gte: range.start, lte: range.end } }
: {})
}
const [income, expense] = await Promise.all([

View File

@@ -1,7 +1,7 @@
import { getQuery } from 'h3'
import { startOfMonth, endOfMonth, startOfYear, endOfYear } from 'date-fns'
import { prisma } from '../../utils/db'
import { requireUser } from '../../utils/auth'
import { getUserSetting, monthRange, yearRange } from '../../utils/userSettings'
export default defineEventHandler(async (event) => {
const user = await requireUser(event)
@@ -12,15 +12,13 @@ export default defineEventHandler(async (event) => {
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 setting = await getUserSetting(prisma, user.id)
const range = year ? yearRange(year) : monthRange(month, setting.monthStartDay)
const transactions = await prisma.transaction.findMany({
where: {
userId: user.id,
occurredAt: { gte: rangeStart, lte: rangeEnd },
occurredAt: { gte: range.start, lte: range.end },
...(type ? { type } : {}),
...(assetId ? { assetId } : {}),
...(categoryId ? { categoryId } : {}),

View File

@@ -0,0 +1,26 @@
import type { Prisma, PrismaClient } from '@prisma/client'
import { addMonths, endOfDay, startOfDay, startOfYear, endOfYear, subDays } from 'date-fns'
type SettingsClient = PrismaClient | Prisma.TransactionClient
export async function getUserSetting(prisma: SettingsClient, userId: string) {
return prisma.userSetting.upsert({
where: { userId },
update: {},
create: { userId }
})
}
export function monthRange(month: string, monthStartDay: number) {
const [year, monthNumber] = month.split('-').map(Number)
const normalizedDay = Math.min(Math.max(monthStartDay || 1, 1), 28)
const start = startOfDay(new Date(year, monthNumber - 1, normalizedDay))
const end = endOfDay(subDays(addMonths(start, 1), 1))
return { start, end }
}
export function yearRange(year: string) {
const start = startOfYear(new Date(`${year}-01-01T00:00:00`))
const end = endOfYear(start)
return { start, end }
}

12
todo.md
View File

@@ -28,27 +28,23 @@
- 기간 선택 바텀 시트를 월간, 연간, 사용자 지정 범위로 확장한다.
- 그래프가 필요해질 경우 최소한의 막대/도넛 차트를 추가한다.
## 6. 설정
- 설정 변경이 실제 화면 계산과 표시 방식에 반영되도록 연결한다.
## 7. 데이터와 API
## 6. 데이터와 API
- 거래 목록 조회 성능을 고려해 필요한 index를 점검한다.
- CSV 내보내기 컬럼을 실제 사용 흐름에 맞게 정리한다.
## 8. 인증과 사용자 경험
## 7. 인증과 사용자 경험
- 회원 탈퇴 또는 데이터 삭제 정책을 정한다.
- 로그인 실패, 세션 만료, 네트워크 오류 상태를 화면에서 더 명확히 보여준다.
- 빈 데이터 상태 화면을 페이지별로 다듬는다.
## 9. 공유
## 8. 공유
- 공유 링크 재생성 기능을 구현한다.
- 공유 접근 화면의 자산/통계 표시를 더 풍부하게 만든다.
## 10. 품질
## 9. 품질
- 핵심 유틸 함수 테스트를 추가한다.
- 거래 생성, 수정, 삭제 API 테스트를 추가한다.