Unify month navigation UI

This commit is contained in:
2026-08-13 19:02:13 +09:00
parent c531ac14a1
commit f11a175674
6 changed files with 67 additions and 66 deletions

View File

@@ -0,0 +1,53 @@
<template>
<div class="flex items-center gap-2">
<button
type="button"
class="flex h-9 w-9 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="rounded-md px-1 py-2 text-left font-extrabold active:bg-paper"
:class="labelMode === 'month' ? 'text-2xl' : 'text-xl'"
@click="monthPickerOpen = true"
>
{{ displayMonth }}
</button>
<button
type="button"
class="flex h-9 w-9 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_right.svg" alt="">
</button>
</div>
<BottomSheetMonthPicker v-model="model" v-model:open="monthPickerOpen" />
</template>
<script setup lang="ts">
import { addMonths, format, parse } from 'date-fns'
const props = withDefaults(defineProps<{
labelMode?: 'month' | 'yearMonth'
}>(), {
labelMode: 'yearMonth'
})
const model = defineModel<string>({ required: true })
const monthPickerOpen = ref(false)
const displayMonth = computed(() => {
const date = parse(model.value, 'yyyy-MM', new Date())
return props.labelMode === 'month' ? format(date, 'M월') : format(date, 'yyyy년 M월')
})
function moveMonth(amount: number) {
const date = parse(model.value, 'yyyy-MM', new Date())
model.value = format(addMonths(date, amount), 'yyyy-MM')
}
</script>

View File

@@ -63,3 +63,5 @@
- 개발 환경에서만 노출되는 테스트 로그인 버튼과 개발용 로그인 API를 추가했다.
- 카테고리 상세 화면에 삭제 동작을 추가하고, 하위 카테고리나 거래가 연결된 카테고리는 삭제하지 못하도록 안내를 추가했다.
- 하단 네비게이션과 PC 사이드바 SVG 아이콘을 마스크 방식 대신 인라인 SVG 렌더링으로 변경해 네모 박스로 보이는 문제를 수정했다.
- 내역, 통계, 자산 화면의 월 이동 UI를 공통 `MonthNavigator` 컴포넌트로 통일했다.
- 통계와 자산 화면의 상단 헤더와 월 이동 영역을 분리해 페이지 간 날짜 조작 위치를 맞췄다.

View File

@@ -14,17 +14,11 @@
</button>
<h1 class="text-lg font-bold">{{ selectedAsset?.name || '자산' }}</h1>
</div>
<div class="flex items-center gap-1">
<button class="flex h-9 w-8 items-center justify-center rounded-md active:bg-paper" type="button" @click="moveMonth(-1)" aria-label="이전 달">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_left.svg" alt="">
</button>
<button class="rounded-md px-2 py-2 text-base font-semibold active:bg-paper" type="button" @click="monthPickerOpen = true">
{{ displayMonth }}
</button>
<button class="flex h-9 w-8 items-center justify-center rounded-md active:bg-paper" type="button" @click="moveMonth(1)" aria-label="다음 달">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_right.svg" alt="">
</button>
</div>
<div class="h-9 w-9" aria-hidden="true" />
</div>
<div class="border-t border-line px-4 py-3">
<MonthNavigator v-model="selectedMonth" />
</div>
<div v-if="!selectedAsset" class="grid grid-cols-3 border-t border-line px-4 py-3 text-center">
@@ -107,17 +101,15 @@
</div>
</div>
<BottomSheetMonthPicker v-model="selectedMonth" v-model:open="monthPickerOpen" />
</section>
</template>
<script setup lang="ts">
import { addMonths, format, parse } from 'date-fns'
import { format, parse } from 'date-fns'
import type { Transaction } from '~/composables/useMoney'
const selectedMonth = ref(monthKey())
const selectedAssetId = ref('')
const monthPickerOpen = ref(false)
const { data: assetData } = await useFetch('/api/assets', { query: { month: selectedMonth } })
const { data: transactionData } = await useFetch('/api/transactions', { query: { month: selectedMonth } })
@@ -161,8 +153,4 @@ const groupedTransactions = computed(() => {
return Array.from(groups.values()).sort((a, b) => b.date.localeCompare(a.date))
})
function moveMonth(amount: number) {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
selectedMonth.value = format(addMonths(date, amount), 'yyyy-MM')
}
</script>

View File

@@ -25,17 +25,7 @@
</div>
<div class="px-4 pb-4 pt-3 md:px-5">
<div class="flex items-center gap-2">
<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="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)">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_right.svg" alt="">
</button>
</div>
<MonthNavigator v-model="selectedMonthModel" label-mode="month" />
<div class="mt-4 flex items-end justify-between gap-4">
<div>
@@ -180,7 +170,6 @@
</Transition>
</Teleport>
<BottomSheetMonthPicker v-model="selectedMonthModel" v-model:open="monthPickerOpen" />
</section>
</template>
@@ -193,7 +182,6 @@ const weekSwipeStartX = ref(0)
const weekDirection = ref<'next' | 'prev'>('next')
const searchOpen = ref(false)
const filterSheetOpen = ref(false)
const monthPickerOpen = ref(false)
const searchKeyword = ref('')
const filters = reactive({
type: '',
@@ -249,11 +237,6 @@ const groupedTransactions = computed(() => {
return Array.from(groups.values()).sort((a, b) => b.date.localeCompare(a.date))
})
const displayMonth = computed(() => {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
return format(date, 'M월')
})
const weekDays = computed(() => {
const base = parse(selectedDate.value, 'yyyy-MM-dd', new Date())
const start = startOfWeek(base, { weekStartsOn: 0 })
@@ -298,12 +281,6 @@ watch(() => filters.type, () => {
if (selected && selected.type !== filters.type) filters.categoryId = ''
})
function moveMonth(amount: number) {
weekDirection.value = amount > 0 ? 'next' : 'prev'
const date = parse(selectedDate.value, 'yyyy-MM-dd', new Date())
selectedDate.value = format(addMonths(date, amount), 'yyyy-MM-dd')
}
function moveWeek(amount: number) {
weekDirection.value = amount > 0 ? 'next' : 'prev'
const date = parse(selectedDate.value, 'yyyy-MM-dd', new Date())

View File

@@ -2,22 +2,16 @@
<section class="min-h-dvh bg-paper">
<header class="sticky top-0 z-10 border-b border-line bg-white">
<div class="flex h-14 items-center justify-between px-4">
<div class="flex items-center gap-2">
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" @click="moveMonth(-1)" aria-label="이전 달">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_left.svg" alt="">
</button>
<button class="rounded-md px-1 py-2 text-left text-lg font-semibold active:bg-paper" type="button" @click="monthPickerOpen = true">
{{ displayMonth }}
</button>
<button class="flex h-9 w-9 items-center justify-center rounded-md active:bg-paper" type="button" @click="moveMonth(1)" aria-label="다음 달">
<img class="h-6 w-6 opacity-70" src="~/assets/icons/keyboard_arrow_right.svg" alt="">
</button>
</div>
<h1 class="text-lg font-bold">통계</h1>
<button class="btn-secondary min-h-10 px-3 text-base" type="button" @click="periodSheetOpen = true">
월간 ⌄
</button>
</div>
<div class="border-t border-line px-4 py-3">
<MonthNavigator v-model="selectedMonth" />
</div>
<div class="grid grid-cols-2 border-t border-line">
<button
type="button"
@@ -107,26 +101,18 @@
</Transition>
</Teleport>
<BottomSheetMonthPicker v-model="selectedMonth" v-model:open="monthPickerOpen" />
</section>
</template>
<script setup lang="ts">
import { addMonths, format, parse } from 'date-fns'
type StatType = 'income' | 'expense'
const selectedMonth = ref(monthKey())
const selectedType = ref<StatType>('expense')
const monthPickerOpen = ref(false)
const periodSheetOpen = ref(false)
const { data } = await useFetch('/api/transactions', { query: { month: selectedMonth } })
const summary = computed(() => data.value?.summary || { income: 0, expense: 0, diff: 0 })
const displayMonth = computed(() => {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
return format(date, 'yyyy년 M월')
})
const selectedTotal = computed(() => selectedType.value === 'income' ? summary.value.income : summary.value.expense)
const categoryTotals = computed(() => {
const map = new Map<string, any>()
@@ -152,8 +138,4 @@ const categoryTotals = computed(() => {
.sort((a, b) => b.total - a.total)
})
function moveMonth(amount: number) {
const date = parse(selectedMonth.value, 'yyyy-MM', new Date())
selectedMonth.value = format(addMonths(date, amount), 'yyyy-MM')
}
</script>

View File

@@ -5,7 +5,6 @@
## 1. 공통 화면 구조 정리
- 모든 주요 페이지의 상단 헤더 구조를 통일한다.
- 내역, 통계, 자산 페이지의 월 표시 위치와 좌우 이동 버튼 위치를 맞춘다.
- PC 화면에서 각 페이지가 너무 모바일 화면처럼 보이지 않도록 표/패널 밀도를 조정한다.
- 모바일과 PC에서 같은 기능이 서로 다른 위치에 흩어지지 않도록 액션 버튼 배치를 정리한다.
- 화면 전환, 날짜 이동, 바텀 시트 표시처럼 사용자가 조작 결과를 바로 느껴야 하는 부분에 모션 피드백을 추가한다.