From cad1aba0ca90e2aad72a1fce40c5cf888f2e4f89 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Mon, 22 Jun 2026 23:57:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=80=EA=B3=84=EB=B6=80=20=EB=B6=84?= =?UTF-8?q?=EB=A5=98=EB=A5=BC=20=EB=8C=80=EB=B6=84=EB=A5=98=E2=86=92?= =?UTF-8?q?=EC=86=8C=EB=B6=84=EB=A5=98=202=EB=8B=A8=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일 긴 드롭다운(대분류 많으면 스크롤 과다) → 대분류 선택 후 소분류 선택 - 자식 없는 대분류는 그 자체가 분류(소분류 select 숨김) - 편집 진입 시 현재 분류로부터 대분류 자동 추론, 구분 변경 시 초기화 - 인라인 분류 추가는 선택된 대분류의 소분류로 생성 Co-Authored-By: Claude Opus 4.8 --- src/views/account/AccountView.vue | 96 ++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index a71c13c..178bf41 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -229,28 +229,43 @@ async function loadCategories() { } } // 현재 구분(수입/지출)에 맞는 분류 + 편집 중 현재 값 보존 -const categoryOptions = computed(() => { - const names = categories.value.filter((c) => c.type === form.type).map((c) => c.name) - if (form.category && !names.includes(form.category)) names.unshift(form.category) +// ===== 분류: 대분류 → 소분류 2단 선택 (긴 드롭다운 스크롤 방지) ===== +const categoryMajor = ref('') // 선택된 대분류 이름 +// 현재 구분의 대분류(부모 없음) 이름 + 편집 중 현재 대분류 보존 +const majorOptions = computed(() => { + const names = categories.value + .filter((c) => c.type === form.type && c.parentId == null) + .map((c) => c.name) + if (categoryMajor.value && !names.includes(categoryMajor.value)) names.unshift(categoryMajor.value) return names }) -// 대분류로 그룹핑(소분류 = optgroup 옵션 / 자식없는 대분류 = 단독 옵션). 값은 분류명(소분류) 그대로. -const categoryGroups = computed(() => { - const list = categories.value.filter((c) => c.type === form.type) - const groups = [] - if (form.category && !list.some((c) => c.name === form.category)) { - groups.push({ key: '_cur', label: null, options: [form.category] }) - } - for (const m of list.filter((c) => c.parentId == null)) { - const children = list.filter((c) => c.parentId === m.id) - if (children.length) { - groups.push({ key: 'g' + m.id, label: m.name, options: children.map((c) => c.name) }) - } else { - groups.push({ key: 'i' + m.id, label: null, options: [m.name] }) - } - } - return groups +// 선택된 대분류의 소분류 이름 목록 (없으면 대분류 자체가 분류) +const subOptions = computed(() => { + const major = categories.value.find( + (c) => c.type === form.type && c.parentId == null && c.name === categoryMajor.value, + ) + if (!major) return [] + return categories.value.filter((c) => c.parentId === major.id).map((c) => c.name) }) +// form.category 로부터 대분류 추론 (편집 진입 시) +function syncCategoryMajor() { + const cur = form.category + if (!cur) { + categoryMajor.value = '' + return + } + const c = categories.value.find((x) => x.type === form.type && x.name === cur) + if (c && c.parentId != null) { + const parent = categories.value.find((x) => x.id === c.parentId) + categoryMajor.value = parent ? parent.name : cur + } else { + categoryMajor.value = cur + } +} +// 대분류 변경: 소분류 있으면 소분류 선택 대기 / 없으면 대분류가 곧 분류 +function onMajorChange() { + form.category = subOptions.value.length ? '' : categoryMajor.value +} // 필터용 전체 분류명 (중복 제거) const allCategoryNames = computed(() => [...new Set(categories.value.map((c) => c.name))]) @@ -270,15 +285,22 @@ async function confirmAddCategory() { const name = newCategoryName.value.trim() if (!name) return catSubmitting.value = true + // 대분류가 선택돼 있으면 그 아래 소분류로 추가, 아니면 대분류로 추가 + const parent = categories.value.find( + (c) => c.type === form.type && c.parentId == null && c.name === categoryMajor.value, + ) + const parentId = parent ? parent.id : null try { - await accountApi.createCategory({ type: form.type, name }) + await accountApi.createCategory({ type: form.type, name, parentId }) await loadCategories() form.category = name + if (!parentId) categoryMajor.value = name // 새 대분류면 대분류로 선택 cancelAddCategory() } catch (e) { if (e.response?.status === 409) { // 이미 있는 분류면 그대로 선택 form.category = name + syncCategoryMajor() cancelAddCategory() } else { alert(e.response?.data?.message || '분류 추가에 실패했습니다.') @@ -287,6 +309,11 @@ async function confirmAddCategory() { catSubmitting.value = false } } +// 구분(수입/지출/이체) 변경 시 분류 초기화 +function onTypeChange() { + form.category = '' + categoryMajor.value = '' +} // 태그 (태그 관리에 등록된 태그 선택) const tagOptions = ref([]) // [{ id, name }] @@ -412,6 +439,7 @@ function openCreate() { editingPending.value = false Object.assign(form, { entryDate: todayStr(), type: 'EXPENSE', category: '', amount: null, memo: '', walletKind: 'BANK', walletId: '', toWalletKind: '', toWalletId: '', principal: null, interest: null, annualFee: null, installmentMonths: '' }) selectedTagIds.value = [] + syncCategoryMajor() cancelAddCategory() formError.value = null formOpen.value = true @@ -441,6 +469,7 @@ function openEdit(e) { const nameToId = {} tagOptions.value.forEach((t) => (nameToId[t.name] = t.id)) selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean) + syncCategoryMajor() cancelAddCategory() formError.value = null formOpen.value = true @@ -756,7 +785,7 @@ onMounted(async () => {