- 단일 긴 드롭다운(대분류 많으면 스크롤 과다) → 대분류 선택 후 소분류 선택 - 자식 없는 대분류는 그 자체가 분류(소분류 select 숨김) - 편집 진입 시 현재 분류로부터 대분류 자동 추론, 구분 변경 시 초기화 - 인라인 분류 추가는 선택된 대분류의 소분류로 생성 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
<label>거래일<input v-model="form.entryDate" type="date" :disabled="submitting" /></label>
|
||||
<label>구분
|
||||
<select v-model="form.type" :disabled="submitting">
|
||||
<select v-model="form.type" :disabled="submitting" @change="onTypeChange">
|
||||
<option value="EXPENSE">지출</option>
|
||||
<option value="INCOME">수입</option>
|
||||
<option value="TRANSFER">이체</option>
|
||||
@@ -825,17 +854,14 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<label v-if="form.type === 'INCOME' || form.type === 'EXPENSE'">분류
|
||||
<div v-if="!addingCategory" class="cat-input">
|
||||
<select v-model="form.category" :disabled="submitting">
|
||||
<option value="">(선택)</option>
|
||||
<template v-for="g in categoryGroups" :key="g.key">
|
||||
<optgroup v-if="g.label" :label="g.label">
|
||||
<option v-for="n in g.options" :key="n" :value="n">{{ n }}</option>
|
||||
</optgroup>
|
||||
<template v-else>
|
||||
<option v-for="n in g.options" :key="n" :value="n">{{ n }}</option>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="!addingCategory" class="cat-input cascade">
|
||||
<select v-model="categoryMajor" :disabled="submitting" @change="onMajorChange" title="대분류">
|
||||
<option value="">대분류 선택</option>
|
||||
<option v-for="m in majorOptions" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
<select v-if="subOptions.length" v-model="form.category" :disabled="submitting" title="소분류">
|
||||
<option value="">소분류 선택</option>
|
||||
<option v-for="s in subOptions" :key="s" :value="s">{{ s }}</option>
|
||||
</select>
|
||||
<button type="button" class="cat-add-btn" :disabled="submitting" @click="startAddCategory">+ 추가</button>
|
||||
</div>
|
||||
@@ -1412,6 +1438,12 @@ button.primary {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.cat-input.cascade {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cat-input.cascade select {
|
||||
flex: 1 1 40%;
|
||||
}
|
||||
.cat-input select,
|
||||
.cat-input input {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user