Improve transaction entry flow

This commit is contained in:
2026-08-14 09:52:16 +09:00
parent b62797ae83
commit d954dd2f50
4 changed files with 86 additions and 16 deletions

View File

@@ -66,3 +66,5 @@
- 내역, 통계, 자산 화면의 월 이동 UI를 공통 `MonthNavigator` 컴포넌트로 통일했다.
- 통계와 자산 화면의 상단 헤더와 월 이동 영역을 분리해 페이지 간 날짜 조작 위치를 맞췄다.
- 거래 입력/수정 날짜 선택 바텀시트에 `오늘`, `이번 달`, `선택일` 빠른 이동 버튼을 추가했다.
- 거래 입력/수정에서 자산 선택 후 분류 선택 바텀시트로 자연스럽게 이어지도록 했다.
- 거래 입력/수정의 수입/지출 타입 변경 시 카테고리 선택을 초기화하고 저장/삭제/복사 실패 토스트를 추가했다.

View File

@@ -36,6 +36,7 @@
placeholder=""
button-class="transaction-form-select"
:options="assetOptions"
@update:model-value="handleAssetSelected"
/>
</div>
@@ -48,6 +49,7 @@
placeholder=""
button-class="transaction-form-select"
:options="categoryOptions"
@update:model-value="invalidField = ''"
/>
</div>
@@ -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)
}
</script>

View File

@@ -36,6 +36,7 @@
placeholder=""
button-class="transaction-form-select"
:options="assetOptions"
@update:model-value="handleAssetSelected"
/>
</div>
@@ -48,6 +49,7 @@
placeholder=""
button-class="transaction-form-select"
:options="categoryOptions"
@update:model-value="invalidField = ''"
/>
</div>
@@ -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<any>('/api/transactions', {
method: 'POST',
body: { ...payload(), occurredAt: dateOnlyToNoon(formatDateOnly()) }
})
pending.value = false
await navigateTo(`/transactions/${created.id}`)
try {
const created = await $fetch<any>('/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)
}
</script>

View File

@@ -16,8 +16,6 @@
## 3. 거래 입력/수정
- 수입과 지출 탭의 동작과 저장 로직을 더 안정화한다.
- 자산 선택 후 카테고리 선택으로 자연스럽게 이어지는 입력 흐름을 만든다.
- 금액 입력 키패드 또는 숫자 입력 UX를 개선한다.
- 입력 화면의 모바일 키보드 노출 상태에서 저장/취소 버튼 위치와 스크롤 동작을 점검한다.
- 복사, 삭제, 자주사용 버튼의 실제 동작을 완성한다.