From d954dd2f503c9dfcf47ed3aa08324429e27cada0 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 14 Aug 2026 09:52:16 +0900 Subject: [PATCH] Improve transaction entry flow --- docs/changelog.md | 2 ++ pages/add.vue | 27 ++++++++++++++ pages/transactions/[id].vue | 71 +++++++++++++++++++++++++++++-------- todo.md | 2 -- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 5067bda..d5b6005 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -66,3 +66,5 @@ - 내역, 통계, 자산 화면의 월 이동 UI를 공통 `MonthNavigator` 컴포넌트로 통일했다. - 통계와 자산 화면의 상단 헤더와 월 이동 영역을 분리해 페이지 간 날짜 조작 위치를 맞췄다. - 거래 입력/수정 날짜 선택 바텀시트에 `오늘`, `이번 달`, `선택일` 빠른 이동 버튼을 추가했다. +- 거래 입력/수정에서 자산 선택 후 분류 선택 바텀시트로 자연스럽게 이어지도록 했다. +- 거래 입력/수정의 수입/지출 타입 변경 시 카테고리 선택을 초기화하고 저장/삭제/복사 실패 토스트를 추가했다. diff --git a/pages/add.vue b/pages/add.vue index 221a029..18fe14e 100644 --- a/pages/add.vue +++ b/pages/add.vue @@ -36,6 +36,7 @@ placeholder="" button-class="transaction-form-select" :options="assetOptions" + @update:model-value="handleAssetSelected" /> @@ -48,6 +49,7 @@ placeholder="" button-class="transaction-form-select" :options="categoryOptions" + @update:model-value="invalidField = ''" /> @@ -161,8 +163,23 @@ watch(selectableCategories, (items: any[]) => { }, { immediate: true }) function setType(type: TransactionType) { + if (form.type === type) return form.type = type + form.categoryId = '' invalidField.value = '' + if (form.assetId) openCategorySelect() +} + +function handleAssetSelected() { + invalidField.value = '' + if (!form.categoryId) openCategorySelect() +} + +function openCategorySelect() { + if (!categoryOptions.value.length) return + window.setTimeout(() => { + categorySelectRef.value?.focus() + }, 220) } function captureLocation() { @@ -200,6 +217,8 @@ async function submit() { longitude: null }) await navigateTo('/') + } catch { + showToast('거래를 저장하지 못했습니다.') } finally { pending.value = false } @@ -228,4 +247,12 @@ function showValidation(field: string, message: string, focus: () => void) { nextTick(() => focus()) return false } + +function showToast(message: string) { + toastMessage.value = message + if (toastTimer) clearTimeout(toastTimer) + toastTimer = setTimeout(() => { + toastMessage.value = '' + }, 2200) +} diff --git a/pages/transactions/[id].vue b/pages/transactions/[id].vue index 67782f3..f760d77 100644 --- a/pages/transactions/[id].vue +++ b/pages/transactions/[id].vue @@ -36,6 +36,7 @@ placeholder="" button-class="transaction-form-select" :options="assetOptions" + @update:model-value="handleAssetSelected" /> @@ -48,6 +49,7 @@ placeholder="" button-class="transaction-form-select" :options="categoryOptions" + @update:model-value="invalidField = ''" /> @@ -184,8 +186,23 @@ watch(selectableCategories, (items: any[]) => { }) function setType(type: TransactionType) { + if (form.type === type) return form.type = type + form.categoryId = '' invalidField.value = '' + if (form.assetId) openCategorySelect() +} + +function handleAssetSelected() { + invalidField.value = '' + if (!form.categoryId) openCategorySelect() +} + +function openCategorySelect() { + if (!categoryOptions.value.length) return + window.setTimeout(() => { + categorySelectRef.value?.focus() + }, 220) } function clearLocation() { @@ -210,28 +227,46 @@ function payload() { async function save() { if (!validateForm()) return pending.value = true - await $fetch(`/api/transactions/${id.value}`, { - method: 'PATCH', - body: payload() - }) - pending.value = false - await router.back() + try { + await $fetch(`/api/transactions/${id.value}`, { + method: 'PATCH', + body: payload() + }) + await router.back() + } catch { + showToast('거래를 저장하지 못했습니다.') + } finally { + pending.value = false + } } async function remove() { if (!confirm('이 거래를 삭제할까요?')) return - await $fetch(`/api/transactions/${id.value}`, { method: 'DELETE' }) - await navigateTo('/') + pending.value = true + try { + await $fetch(`/api/transactions/${id.value}`, { method: 'DELETE' }) + await navigateTo('/') + } catch { + showToast('거래를 삭제하지 못했습니다.') + } finally { + pending.value = false + } } async function copy() { + if (!validateForm()) return pending.value = true - const created = await $fetch('/api/transactions', { - method: 'POST', - body: { ...payload(), occurredAt: dateOnlyToNoon(formatDateOnly()) } - }) - pending.value = false - await navigateTo(`/transactions/${created.id}`) + try { + const created = await $fetch('/api/transactions', { + method: 'POST', + body: { ...payload(), occurredAt: dateOnlyToNoon(formatDateOnly()) } + }) + await navigateTo(`/transactions/${created.id}`) + } catch { + showToast('거래를 복사하지 못했습니다.') + } finally { + pending.value = false + } } function goBack() { @@ -257,4 +292,12 @@ function showValidation(field: string, message: string, focus: () => void) { nextTick(() => focus()) return false } + +function showToast(message: string) { + toastMessage.value = message + if (toastTimer) clearTimeout(toastTimer) + toastTimer = setTimeout(() => { + toastMessage.value = '' + }, 2200) +} diff --git a/todo.md b/todo.md index 7dc3d79..53007ef 100644 --- a/todo.md +++ b/todo.md @@ -16,8 +16,6 @@ ## 3. 거래 입력/수정 -- 수입과 지출 탭의 동작과 저장 로직을 더 안정화한다. -- 자산 선택 후 카테고리 선택으로 자연스럽게 이어지는 입력 흐름을 만든다. - 금액 입력 키패드 또는 숫자 입력 UX를 개선한다. - 입력 화면의 모바일 키보드 노출 상태에서 저장/취소 버튼 위치와 스크롤 동작을 점검한다. - 복사, 삭제, 자주사용 버튼의 실제 동작을 완성한다.