Unify month navigation UI
This commit is contained in:
53
components/MonthNavigator.vue
Normal file
53
components/MonthNavigator.vue
Normal 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>
|
||||
@@ -63,3 +63,5 @@
|
||||
- 개발 환경에서만 노출되는 테스트 로그인 버튼과 개발용 로그인 API를 추가했다.
|
||||
- 카테고리 상세 화면에 삭제 동작을 추가하고, 하위 카테고리나 거래가 연결된 카테고리는 삭제하지 못하도록 안내를 추가했다.
|
||||
- 하단 네비게이션과 PC 사이드바 SVG 아이콘을 마스크 방식 대신 인라인 SVG 렌더링으로 변경해 네모 박스로 보이는 문제를 수정했다.
|
||||
- 내역, 통계, 자산 화면의 월 이동 UI를 공통 `MonthNavigator` 컴포넌트로 통일했다.
|
||||
- 통계와 자산 화면의 상단 헤더와 월 이동 영역을 분리해 페이지 간 날짜 조작 위치를 맞췄다.
|
||||
|
||||
@@ -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 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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
todo.md
1
todo.md
@@ -5,7 +5,6 @@
|
||||
## 1. 공통 화면 구조 정리
|
||||
|
||||
- 모든 주요 페이지의 상단 헤더 구조를 통일한다.
|
||||
- 내역, 통계, 자산 페이지의 월 표시 위치와 좌우 이동 버튼 위치를 맞춘다.
|
||||
- PC 화면에서 각 페이지가 너무 모바일 화면처럼 보이지 않도록 표/패널 밀도를 조정한다.
|
||||
- 모바일과 PC에서 같은 기능이 서로 다른 위치에 흩어지지 않도록 액션 버튼 배치를 정리한다.
|
||||
- 화면 전환, 날짜 이동, 바텀 시트 표시처럼 사용자가 조작 결과를 바로 느껴야 하는 부분에 모션 피드백을 추가한다.
|
||||
|
||||
Reference in New Issue
Block a user