Share month picker sheet

This commit is contained in:
2026-08-13 17:39:08 +09:00
parent 87cd0be64f
commit abc857e60e
6 changed files with 102 additions and 107 deletions

View File

@@ -0,0 +1,87 @@
<template>
<Teleport to="body">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="open" class="fixed inset-0 z-40 bg-ink/35" @click="close" />
</Transition>
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-full"
enter-to-class="translate-y-0"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0"
leave-to-class="translate-y-full"
>
<section v-if="open" class="fixed inset-x-0 bottom-0 z-50 mx-auto max-w-lg rounded-t-2xl bg-white shadow-2xl">
<div class="border-b border-line px-4 pb-3 pt-2">
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-line" />
<div class="flex items-center justify-between">
<h2 class="text-base font-bold">월 선택</h2>
<button type="button" class="text-sm font-semibold text-muted" @click="close">닫기</button>
</div>
</div>
<div class="flex items-center justify-between px-4 py-4">
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" aria-label="이전 년도" @click="pickerYear--">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_left.svg" alt="">
</button>
<strong class="text-xl">{{ pickerYear }}</strong>
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" aria-label="다음 년도" @click="pickerYear++">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_right.svg" alt="">
</button>
</div>
<div class="grid grid-cols-4 gap-y-2 px-4 pb-6">
<button
v-for="month in 12"
:key="month"
type="button"
class="rounded-md py-3 text-base font-bold"
:class="isSelected(month) ? 'bg-red-50 text-red-500' : 'active:bg-paper'"
@click="select(month)"
>
{{ month }}월
</button>
</div>
</section>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: string
open: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:open': [value: boolean]
}>()
const pickerYear = ref(Number(props.modelValue.slice(0, 4)))
watch(() => props.open, (open) => {
if (open) pickerYear.value = Number(props.modelValue.slice(0, 4))
})
function isSelected(month: number) {
return props.modelValue === `${pickerYear.value}-${String(month).padStart(2, '0')}`
}
function select(month: number) {
emit('update:modelValue', `${pickerYear.value}-${String(month).padStart(2, '0')}`)
close()
}
function close() {
emit('update:open', false)
}
</script>

View File

@@ -32,3 +32,4 @@
- SVG 메뉴 아이콘이 사각형으로 보이는 문제를 수정하고 활성 아이콘 배경을 추가했다.
- 내역 검색과 유형/자산/카테고리 필터 바텀 시트를 구현했다.
- 거래 목록 API에 검색어, 유형, 자산, 카테고리 필터 query를 추가했다.
- 월 선택 바텀 시트를 공통 컴포넌트로 분리하고 내역, 통계, 자산 화면에 적용했다.

View File

@@ -107,43 +107,7 @@
</div>
</div>
<Teleport to="body">
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0" enter-to-class="opacity-100" leave-active-class="transition duration-150 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0">
<div v-if="monthPickerOpen" class="fixed inset-0 z-40 bg-ink/35" @click="monthPickerOpen = false" />
</Transition>
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="translate-y-full" enter-to-class="translate-y-0" leave-active-class="transition duration-150 ease-in" leave-from-class="translate-y-0" leave-to-class="translate-y-full">
<section v-if="monthPickerOpen" class="fixed inset-x-0 bottom-0 z-50 mx-auto max-w-lg rounded-t-2xl bg-white shadow-2xl">
<div class="border-b border-line px-4 pb-3 pt-2">
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-line" />
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold">월 선택</h2>
<button type="button" class="text-sm font-semibold text-muted" @click="monthPickerOpen = false">닫기</button>
</div>
</div>
<div class="flex items-center justify-between px-4 py-4">
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" @click="pickerYear--" aria-label="이전 년도">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_left.svg" alt="">
</button>
<strong class="text-xl">{{ pickerYear }}</strong>
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" @click="pickerYear++" aria-label="다음 년도">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_right.svg" alt="">
</button>
</div>
<div class="grid grid-cols-4 gap-y-2 px-4 pb-6">
<button
v-for="month in 12"
:key="month"
type="button"
class="rounded-md py-3 text-base font-bold"
:class="isSelectedPickerMonth(month) ? 'bg-red-50 text-red-500' : 'active:bg-paper'"
@click="selectPickerMonth(month)"
>
{{ month }}월
</button>
</div>
</section>
</Transition>
</Teleport>
<BottomSheetMonthPicker v-model="selectedMonth" v-model:open="monthPickerOpen" />
</section>
</template>
@@ -154,7 +118,6 @@ import type { Transaction } from '~/composables/useMoney'
const selectedMonth = ref(monthKey())
const selectedAssetId = ref('')
const monthPickerOpen = ref(false)
const pickerYear = ref(Number(selectedMonth.value.slice(0, 4)))
const { data: assetData } = await useFetch('/api/assets', { query: { month: selectedMonth } })
const { data: transactionData } = await useFetch('/api/transactions', { query: { month: selectedMonth } })
@@ -198,22 +161,8 @@ const groupedTransactions = computed(() => {
return Array.from(groups.values()).sort((a, b) => b.date.localeCompare(a.date))
})
watch(monthPickerOpen, (open) => {
if (open) pickerYear.value = Number(selectedMonth.value.slice(0, 4))
})
function moveMonth(amount: number) {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
selectedMonth.value = format(addMonths(date, amount), 'yyyy-MM')
}
function isSelectedPickerMonth(month: number) {
return selectedMonth.value === `${pickerYear.value}-${String(month).padStart(2, '0')}`
}
function selectPickerMonth(month: number) {
selectedMonth.value = `${pickerYear.value}-${String(month).padStart(2, '0')}`
monthPickerOpen.value = false
}
</script>

View File

@@ -29,7 +29,7 @@
<button type="button" class="flex h-8 w-8 items-center justify-center rounded-md active:bg-paper" aria-label="이전 달" @click="moveMonth(-1)">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_left.svg" alt="">
</button>
<button type="button" class="text-2xl font-extrabold active:opacity-70" @click="selectedDate = formatDateOnly()">
<button type="button" class="text-2xl font-extrabold active:opacity-70" @click="monthPickerOpen = true">
{{ displayMonth }}
</button>
<button type="button" class="flex h-8 w-8 items-center justify-center rounded-md active:bg-paper" aria-label="다음 달" @click="moveMonth(1)">
@@ -172,6 +172,8 @@
</section>
</Transition>
</Teleport>
<BottomSheetMonthPicker v-model="selectedMonthModel" v-model:open="monthPickerOpen" />
</section>
</template>
@@ -183,6 +185,7 @@ const selectedDate = ref(formatDateOnly())
const weekSwipeStartX = ref(0)
const searchOpen = ref(false)
const filterSheetOpen = ref(false)
const monthPickerOpen = ref(false)
const searchKeyword = ref('')
const filters = reactive({
type: '',
@@ -190,6 +193,12 @@ const filters = reactive({
categoryId: ''
})
const selectedMonth = computed(() => monthKey(parse(selectedDate.value, 'yyyy-MM-dd', new Date())))
const selectedMonthModel = computed({
get: () => selectedMonth.value,
set: (value: string) => {
selectedDate.value = `${value}-01`
}
})
const previousMonth = computed(() => {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
return format(addMonths(date, -1), 'yyyy-MM')

View File

@@ -84,40 +84,7 @@
<Teleport to="body">
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0" enter-to-class="opacity-100" leave-active-class="transition duration-150 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0">
<div v-if="monthPickerOpen || periodSheetOpen" class="fixed inset-0 z-40 bg-ink/35" @click="closeSheets" />
</Transition>
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="translate-y-full" enter-to-class="translate-y-0" leave-active-class="transition duration-150 ease-in" leave-from-class="translate-y-0" leave-to-class="translate-y-full">
<section v-if="monthPickerOpen" class="fixed inset-x-0 bottom-0 z-50 mx-auto max-w-lg rounded-t-2xl bg-white shadow-2xl">
<div class="border-b border-line px-4 pb-3 pt-2">
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-line" />
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold">월 선택</h2>
<button type="button" class="text-sm font-semibold text-muted" @click="monthPickerOpen = false">닫기</button>
</div>
</div>
<div class="flex items-center justify-between px-4 py-4">
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" @click="pickerYear--" aria-label="이전 년도">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_left.svg" alt="">
</button>
<strong class="text-xl">{{ pickerYear }}</strong>
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" @click="pickerYear++" aria-label="다음 년도">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_right.svg" alt="">
</button>
</div>
<div class="grid grid-cols-4 gap-y-2 px-4 pb-6">
<button
v-for="month in 12"
:key="month"
type="button"
class="rounded-md py-3 text-base font-bold"
:class="isSelectedPickerMonth(month) ? 'bg-red-50 text-red-500' : 'active:bg-paper'"
@click="selectPickerMonth(month)"
>
{{ month }}월
</button>
</div>
</section>
<div v-if="periodSheetOpen" class="fixed inset-0 z-40 bg-ink/35" @click="periodSheetOpen = false" />
</Transition>
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="translate-y-full" enter-to-class="translate-y-0" leave-active-class="transition duration-150 ease-in" leave-from-class="translate-y-0" leave-to-class="translate-y-full">
@@ -139,6 +106,8 @@
</section>
</Transition>
</Teleport>
<BottomSheetMonthPicker v-model="selectedMonth" v-model:open="monthPickerOpen" />
</section>
</template>
@@ -151,7 +120,6 @@ const selectedMonth = ref(monthKey())
const selectedType = ref<StatType>('expense')
const monthPickerOpen = ref(false)
const periodSheetOpen = ref(false)
const pickerYear = ref(Number(selectedMonth.value.slice(0, 4)))
const { data } = await useFetch('/api/transactions', { query: { month: selectedMonth } })
const summary = computed(() => data.value?.summary || { income: 0, expense: 0, diff: 0 })
@@ -184,26 +152,8 @@ const categoryTotals = computed(() => {
.sort((a, b) => b.total - a.total)
})
watch(monthPickerOpen, (open) => {
if (open) pickerYear.value = Number(selectedMonth.value.slice(0, 4))
})
function moveMonth(amount: number) {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
selectedMonth.value = format(addMonths(date, amount), 'yyyy-MM')
}
function isSelectedPickerMonth(month: number) {
return selectedMonth.value === `${pickerYear.value}-${String(month).padStart(2, '0')}`
}
function selectPickerMonth(month: number) {
selectedMonth.value = `${pickerYear.value}-${String(month).padStart(2, '0')}`
monthPickerOpen.value = false
}
function closeSheets() {
monthPickerOpen.value = false
periodSheetOpen.value = false
}
</script>

View File

@@ -11,7 +11,6 @@
## 2. 내역 화면
- 월 제목을 누르면 월 선택 바텀 시트가 열리도록 구현한다.
- 날짜별 수입/지출 표시가 좁은 화면에서도 겹치지 않도록 최종 조정한다.
- 거래 행 왼쪽 아이콘을 카테고리별 아이콘으로 표시한다.
- 거래 취소/환불 같은 상태 표시 규칙을 정한다.