From 90e8b48c2bcaaf01f14a847a39268a3d1bce9a80 Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 13 Aug 2026 16:57:27 +0900 Subject: [PATCH] Refine transaction date input and header --- assets/icons/keyboard_arrow_down.svg | 1 + assets/icons/keyboard_arrow_left.svg | 1 + assets/icons/keyboard_arrow_right.svg | 1 + assets/icons/keyboard_arrow_up.svg | 1 + components/BottomSheetDatePicker.vue | 121 ++++++++++++++++++ components/TransactionListRow.vue | 5 +- docs/changelog.md | 5 + docs/project-guidelines.md | 5 + pages/add.vue | 18 +-- pages/assets.vue | 20 ++- pages/index.vue | 171 +++++++++++++++++++++++--- pages/settings.vue | 8 +- pages/stats.vue | 16 ++- pages/transactions/[id].vue | 20 +-- utils/format.ts | 9 ++ 15 files changed, 346 insertions(+), 56 deletions(-) create mode 100644 assets/icons/keyboard_arrow_down.svg create mode 100644 assets/icons/keyboard_arrow_left.svg create mode 100644 assets/icons/keyboard_arrow_right.svg create mode 100644 assets/icons/keyboard_arrow_up.svg create mode 100644 components/BottomSheetDatePicker.vue diff --git a/assets/icons/keyboard_arrow_down.svg b/assets/icons/keyboard_arrow_down.svg new file mode 100644 index 0000000..0d51a14 --- /dev/null +++ b/assets/icons/keyboard_arrow_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/keyboard_arrow_left.svg b/assets/icons/keyboard_arrow_left.svg new file mode 100644 index 0000000..d95d499 --- /dev/null +++ b/assets/icons/keyboard_arrow_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/keyboard_arrow_right.svg b/assets/icons/keyboard_arrow_right.svg new file mode 100644 index 0000000..bb79bbe --- /dev/null +++ b/assets/icons/keyboard_arrow_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/keyboard_arrow_up.svg b/assets/icons/keyboard_arrow_up.svg new file mode 100644 index 0000000..c1b208a --- /dev/null +++ b/assets/icons/keyboard_arrow_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/BottomSheetDatePicker.vue b/components/BottomSheetDatePicker.vue new file mode 100644 index 0000000..ea6a24b --- /dev/null +++ b/components/BottomSheetDatePicker.vue @@ -0,0 +1,121 @@ + + + diff --git a/components/TransactionListRow.vue b/components/TransactionListRow.vue index 69af120..b7b1ece 100644 --- a/components/TransactionListRow.vue +++ b/components/TransactionListRow.vue @@ -41,7 +41,7 @@ const props = defineProps<{ const categoryLabel = computed(() => { const category = props.transaction.category - return category.parent ? `${category.parent.name} > ${category.name}` : category.name + return category.name }) const categoryInitial = computed(() => { @@ -51,8 +51,7 @@ const categoryInitial = computed(() => { }) const metaLine = computed(() => { - const time = new Intl.DateTimeFormat('ko-KR', { hour: '2-digit', minute: '2-digit' }).format(new Date(props.transaction.occurredAt)) - return [time, props.transaction.asset.name, categoryLabel.value].filter(Boolean).join(' · ') + return [props.transaction.asset.name, categoryLabel.value].filter(Boolean).join(' · ') }) const memoPreview = computed(() => props.transaction.memo?.trim().slice(0, 60) || '') diff --git a/docs/changelog.md b/docs/changelog.md index fef01b5..9b7fcc1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,3 +21,8 @@ - 입력 중 스크롤이 튀는 현상을 줄이기 위해 바텀 시트 body overflow 변경과 추가 화면 sticky 저장 버튼을 제거했다. - 거래 목록 행을 날짜 그룹, 카테고리 아이콘 자리, 기본 2줄 정보, 메모가 있을 때만 보이는 3번째 줄 구조로 변경했다. - 메모 입력과 저장 길이를 최대 60자로 제한했다. +- 거래 입력/수정에서 시간 입력을 제거하고 날짜 바텀 시트 선택으로 변경했다. +- 금액 입력 기본값을 제거하고 placeholder로 안내하도록 변경했다. +- 거래 목록의 카테고리 표시는 하위 카테고리명 중심으로 단순화했다. +- 내역 화면 상단을 헤더, 월 요약, 주간 날짜 스트립 구조로 개편했다. +- 방향 이동 버튼을 `assets/icons`의 SVG 화살표로 통일했다. diff --git a/docs/project-guidelines.md b/docs/project-guidelines.md index c972b9c..f6dd73a 100644 --- a/docs/project-guidelines.md +++ b/docs/project-guidelines.md @@ -13,9 +13,14 @@ - 모바일: 하단 탭, 플로팅 추가 버튼, 바텀 시트 선택 UI를 우선한다. - PC: 사이드바 내비게이션과 넓은 콘텐츠 영역을 사용한다. - 입력 화면은 한 화면에서 빠르게 끝나는 흐름을 유지한다. +- 거래 입력에서는 시간 입력을 요구하지 않고 날짜만 선택한다. +- 날짜 선택은 모바일 친화적인 바텀 시트 캘린더를 사용한다. +- 금액 입력은 기본값 `0`을 채우지 않고 placeholder로만 안내한다. - 셀렉트 박스보다 모바일 친화적인 바텀 시트를 우선한다. - 활성/비활성 같은 상태는 텍스트 버튼보다 토글처럼 현재 상태가 보이는 UI를 쓴다. - 통계 화면은 그래프보다 숫자와 분류별 리스트를 우선한다. +- 방향 이동 UI는 `assets/icons`의 키보드 화살표 SVG를 사용한다. +- 거래 목록의 카테고리 표시는 하위 카테고리가 있으면 하위 이름만, 없으면 상위 이름만 보여준다. ## 데이터 기준 diff --git a/pages/add.vue b/pages/add.vue index deccf07..8d01504 100644 --- a/pages/add.vue +++ b/pages/add.vue @@ -10,8 +10,8 @@
- - + +
반복 @@ -41,7 +41,7 @@
- +
@@ -81,10 +81,10 @@ const { data: categoriesData } = await useFetch('/api/categories') const form = reactive({ type: 'expense', - occurredAt: formatDateInput(), + occurredAt: formatDateOnly(), assetId: '', categoryId: '', - amount: 0, + amount: null as number | null, title: '', memo: '', latitude: null as number | null, @@ -101,7 +101,7 @@ const assetOptions = computed(() => assets.value.map((asset: any) => ({ }))) const categoryOptions = computed(() => filteredCategories.value.map((category: any) => ({ value: category.id, - label: category.parent ? `${category.parent.name} > ${category.name}` : category.name, + label: category.name, description: category.type === 'income' ? '수입' : '지출' }))) @@ -130,15 +130,15 @@ async function submit() { pending.value = true await $fetch('/api/transactions', { method: 'POST', - body: form + body: { ...form, occurredAt: dateOnlyToNoon(form.occurredAt) } }) const keepAssetId = form.assetId Object.assign(form, { type: form.type, - occurredAt: formatDateInput(), + occurredAt: formatDateOnly(), assetId: keepAssetId, categoryId: form.categoryId, - amount: 0, + amount: null, title: '', memo: '', latitude: null, diff --git a/pages/assets.vue b/pages/assets.vue index 0863412..38b8291 100644 --- a/pages/assets.vue +++ b/pages/assets.vue @@ -6,20 +6,24 @@

{{ selectedAsset?.name || '자산' }}

- + - +
@@ -117,9 +121,13 @@
- + {{ pickerYear }} - +
- - +
+
+ +

최근 이용 내역

+
+ + +
-
-
-

수입

-

{{ formatWon(data?.summary.income || 0) }}

+ +
+
+ + +
-
-

지출

-

{{ formatWon(data?.summary.expense || 0) }}

+ +
+
+

+ {{ formatWon(data?.summary.expense || 0) }} +

+

+ 수입 {{ formatWon(data?.summary.income || 0) }} + · 차액 {{ formatWon(data?.summary.diff || 0) }} +

+
+ + 소비 분류 +
-
-

차액

-

{{ formatWon(data?.summary.diff || 0) }}

+ +
+
+ {{ day }} +
+
+ +
+ +
+
+

선택일 수입

+

{{ formatWon(selectedDaySummary.income) }}

+
+
+

선택일 지출

+

{{ formatWon(selectedDaySummary.expense) }}

+
+
+

합계

+

{{ formatWon(selectedDaySummary.diff) }}

+
+
-
+

{{ group.label }}

@@ -42,13 +107,16 @@ diff --git a/pages/settings.vue b/pages/settings.vue index 12018be..01372c1 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -5,11 +5,11 @@

{{ title }}

@@ -39,7 +39,7 @@ {{ item.label }} {{ item.description }} - › +
@@ -130,7 +130,7 @@ {{ category.name }} {{ categoryDescription(category) }} - › +

diff --git a/pages/stats.vue b/pages/stats.vue index c0403ba..83619f6 100644 --- a/pages/stats.vue +++ b/pages/stats.vue @@ -3,11 +3,15 @@

- + - +
- + {{ pickerYear }} - +

{{ form.type === 'income' ? '수입' : '지출' }}

@@ -25,8 +25,8 @@
- - + 날짜 +
@@ -51,7 +51,7 @@
- +
@@ -96,7 +96,7 @@ const { data: categoriesData } = await useFetch('/api/categories') const form = reactive({ type: 'expense' as TransactionType, - occurredAt: formatDateInput(), + occurredAt: formatDateOnly(), assetId: '', categoryId: '', amount: 0, @@ -116,7 +116,7 @@ const assetOptions = computed(() => assets.value.map((asset: any) => ({ }))) const categoryOptions = computed(() => filteredCategories.value.map((category: any) => ({ value: category.id, - label: category.parent ? `${category.parent.name} > ${category.name}` : category.name, + label: category.name, description: category.type === 'income' ? '수입' : '지출' }))) const locationLabel = computed(() => form.latitude && form.longitude) @@ -126,7 +126,7 @@ watch(transaction, (value) => { if (!value) return Object.assign(form, { type: value.type, - occurredAt: formatDateInput(new Date(value.occurredAt)), + occurredAt: formatDateOnly(new Date(value.occurredAt)), assetId: value.assetId, categoryId: value.categoryId, amount: value.amount, @@ -153,7 +153,7 @@ function clearLocation() { function payload() { return { type: form.type, - occurredAt: form.occurredAt, + occurredAt: dateOnlyToNoon(form.occurredAt), assetId: form.assetId, categoryId: form.categoryId, amount: form.amount, @@ -184,7 +184,7 @@ async function copy() { pending.value = true const created = await $fetch('/api/transactions', { method: 'POST', - body: { ...payload(), occurredAt: formatDateInput() } + body: { ...payload(), occurredAt: dateOnlyToNoon(formatDateOnly()) } }) pending.value = false await navigateTo(`/transactions/${created.id}`) diff --git a/utils/format.ts b/utils/format.ts index d84631f..842421f 100644 --- a/utils/format.ts +++ b/utils/format.ts @@ -7,6 +7,15 @@ export function formatDateInput(date = new Date()) { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}` } +export function formatDateOnly(date = new Date()) { + const pad = (n: number) => String(n).padStart(2, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +} + +export function dateOnlyToNoon(value: string) { + return `${value}T12:00` +} + export function monthKey(date = new Date()) { const pad = (n: number) => String(n).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`