From 7888a06d8fde8129c9df8842ace792c99c5244db Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 13 Aug 2026 18:28:37 +0900 Subject: [PATCH] Refine transaction form validation UI --- assets/css/main.css | 20 ++++++ components/BottomSheetDatePicker.vue | 4 +- components/BottomSheetSelect.vue | 14 +++- docs/changelog.md | 5 +- docs/project-guidelines.md | 3 + pages/add.vue | 97 +++++++++++++++++++++------- pages/transactions/[id].vue | 97 +++++++++++++++++++++------- todo.md | 1 + 8 files changed, 193 insertions(+), 48 deletions(-) diff --git a/assets/css/main.css b/assets/css/main.css index 8c63b40..eff57b1 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -49,4 +49,24 @@ button { .segmented-button-active { @apply bg-ink text-white; } + + .transaction-form-row { + @apply grid grid-cols-[4.25rem_1fr] items-center gap-3 border-b border-line py-3 transition-colors duration-150; + } + + .transaction-form-row:focus-within { + @apply border-brand-600; + } + + .transaction-form-row-error { + @apply border-red-500; + } + + .transaction-form-control { + @apply w-full border-0 bg-transparent px-0 py-2 text-sm font-semibold outline-none shadow-none focus:ring-0; + } + + .transaction-form-select { + @apply flex min-h-10 w-full items-center justify-between border-0 bg-transparent px-0 py-2 text-left text-sm font-semibold outline-none shadow-none focus:ring-0; + } } diff --git a/components/BottomSheetDatePicker.vue b/components/BottomSheetDatePicker.vue index ea6a24b..ca3db6a 100644 --- a/components/BottomSheetDatePicker.vue +++ b/components/BottomSheetDatePicker.vue @@ -2,7 +2,7 @@

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

- + -
-
+
+
날짜 - +
-
+
자산
-
+
분류
-
+
- +
-
+
- +
-
+
- +
-
+
위치
보기 @@ -79,10 +96,21 @@
- +
+ + +
+ + +
+ {{ toastMessage }} +
+
@@ -93,6 +121,13 @@ const route = useRoute() const router = useRouter() const id = computed(() => String(route.params.id)) const pending = ref(false) +const invalidField = ref('') +const toastMessage = ref('') +let toastTimer: ReturnType | null = null +const assetSelectRef = ref<{ focus: () => void } | null>(null) +const categorySelectRef = ref<{ focus: () => void } | null>(null) +const amountInputRef = ref(null) +const titleInputRef = ref(null) const { data: transaction } = await useFetch(() => `/api/transactions/${id.value}`) const { data: assetsData } = await useFetch('/api/assets') const { data: categoriesData } = await useFetch('/api/categories') @@ -148,12 +183,9 @@ watch(selectableCategories, (items: any[]) => { if (form.categoryId && !items.some(item => item.id === form.categoryId)) form.categoryId = '' }) -const canSubmit = computed(() => { - return Boolean(form.assetId && form.categoryId && form.amount && form.amount > 0 && form.title.trim()) -}) - function setType(type: TransactionType) { form.type = type + invalidField.value = '' } function clearLocation() { @@ -176,6 +208,7 @@ function payload() { } async function save() { + if (!validateForm()) return pending.value = true await $fetch(`/api/transactions/${id.value}`, { method: 'PATCH', @@ -204,4 +237,24 @@ async function copy() { function goBack() { router.back() } + +function validateForm() { + if (!form.assetId) return showValidation('assetId', '자산을 선택해 주세요.', () => assetSelectRef.value?.focus()) + if (!form.categoryId) return showValidation('categoryId', '분류를 선택해 주세요.', () => categorySelectRef.value?.focus()) + if (!form.amount || form.amount < 1) return showValidation('amount', '금액을 입력해 주세요.', () => amountInputRef.value?.focus()) + if (!form.title.trim()) return showValidation('title', '내용을 입력해 주세요.', () => titleInputRef.value?.focus()) + invalidField.value = '' + return true +} + +function showValidation(field: string, message: string, focus: () => void) { + invalidField.value = field + toastMessage.value = message + if (toastTimer) clearTimeout(toastTimer) + toastTimer = setTimeout(() => { + toastMessage.value = '' + }, 2200) + nextTick(() => focus()) + return false +} diff --git a/todo.md b/todo.md index 99b5341..094f4ee 100644 --- a/todo.md +++ b/todo.md @@ -21,6 +21,7 @@ - 날짜 선택 바텀 시트의 오늘/이번 달 빠른 이동 기능을 추가한다. - 자산 선택 후 카테고리 선택으로 자연스럽게 이어지는 입력 흐름을 만든다. - 금액 입력 키패드 또는 숫자 입력 UX를 개선한다. +- 입력 화면의 모바일 키보드 노출 상태에서 저장/취소 버튼 위치와 스크롤 동작을 점검한다. - 복사, 삭제, 자주사용 버튼의 실제 동작을 완성한다. - 사진 첨부 UI의 저장 구조를 설계하고 구현한다.