Merge branch 'dev'
This commit is contained in:
@@ -229,28 +229,43 @@ async function loadCategories() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 현재 구분(수입/지출)에 맞는 분류 + 편집 중 현재 값 보존
|
// 현재 구분(수입/지출)에 맞는 분류 + 편집 중 현재 값 보존
|
||||||
const categoryOptions = computed(() => {
|
// ===== 분류: 대분류 → 소분류 2단 선택 (긴 드롭다운 스크롤 방지) =====
|
||||||
const names = categories.value.filter((c) => c.type === form.type).map((c) => c.name)
|
const categoryMajor = ref('') // 선택된 대분류 이름
|
||||||
if (form.category && !names.includes(form.category)) names.unshift(form.category)
|
// 현재 구분의 대분류(부모 없음) 이름 + 편집 중 현재 대분류 보존
|
||||||
|
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
|
return names
|
||||||
})
|
})
|
||||||
// 대분류로 그룹핑(소분류 = optgroup 옵션 / 자식없는 대분류 = 단독 옵션). 값은 분류명(소분류) 그대로.
|
// 선택된 대분류의 소분류 이름 목록 (없으면 대분류 자체가 분류)
|
||||||
const categoryGroups = computed(() => {
|
const subOptions = computed(() => {
|
||||||
const list = categories.value.filter((c) => c.type === form.type)
|
const major = categories.value.find(
|
||||||
const groups = []
|
(c) => c.type === form.type && c.parentId == null && c.name === categoryMajor.value,
|
||||||
if (form.category && !list.some((c) => c.name === form.category)) {
|
)
|
||||||
groups.push({ key: '_cur', label: null, options: [form.category] })
|
if (!major) return []
|
||||||
}
|
return categories.value.filter((c) => c.parentId === major.id).map((c) => c.name)
|
||||||
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
|
|
||||||
})
|
})
|
||||||
|
// 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))])
|
const allCategoryNames = computed(() => [...new Set(categories.value.map((c) => c.name))])
|
||||||
|
|
||||||
@@ -270,15 +285,22 @@ async function confirmAddCategory() {
|
|||||||
const name = newCategoryName.value.trim()
|
const name = newCategoryName.value.trim()
|
||||||
if (!name) return
|
if (!name) return
|
||||||
catSubmitting.value = true
|
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 {
|
try {
|
||||||
await accountApi.createCategory({ type: form.type, name })
|
await accountApi.createCategory({ type: form.type, name, parentId })
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
form.category = name
|
form.category = name
|
||||||
|
if (!parentId) categoryMajor.value = name // 새 대분류면 대분류로 선택
|
||||||
cancelAddCategory()
|
cancelAddCategory()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response?.status === 409) {
|
if (e.response?.status === 409) {
|
||||||
// 이미 있는 분류면 그대로 선택
|
// 이미 있는 분류면 그대로 선택
|
||||||
form.category = name
|
form.category = name
|
||||||
|
syncCategoryMajor()
|
||||||
cancelAddCategory()
|
cancelAddCategory()
|
||||||
} else {
|
} else {
|
||||||
alert(e.response?.data?.message || '분류 추가에 실패했습니다.')
|
alert(e.response?.data?.message || '분류 추가에 실패했습니다.')
|
||||||
@@ -287,6 +309,11 @@ async function confirmAddCategory() {
|
|||||||
catSubmitting.value = false
|
catSubmitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 구분(수입/지출/이체) 변경 시 분류 초기화
|
||||||
|
function onTypeChange() {
|
||||||
|
form.category = ''
|
||||||
|
categoryMajor.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
// 태그 (태그 관리에 등록된 태그 선택)
|
// 태그 (태그 관리에 등록된 태그 선택)
|
||||||
const tagOptions = ref([]) // [{ id, name }]
|
const tagOptions = ref([]) // [{ id, name }]
|
||||||
@@ -412,6 +439,7 @@ function openCreate() {
|
|||||||
editingPending.value = false
|
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: '' })
|
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 = []
|
selectedTagIds.value = []
|
||||||
|
syncCategoryMajor()
|
||||||
cancelAddCategory()
|
cancelAddCategory()
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formOpen.value = true
|
formOpen.value = true
|
||||||
@@ -441,6 +469,7 @@ function openEdit(e) {
|
|||||||
const nameToId = {}
|
const nameToId = {}
|
||||||
tagOptions.value.forEach((t) => (nameToId[t.name] = t.id))
|
tagOptions.value.forEach((t) => (nameToId[t.name] = t.id))
|
||||||
selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean)
|
selectedTagIds.value = (e.tags || []).map((n) => nameToId[n]).filter(Boolean)
|
||||||
|
syncCategoryMajor()
|
||||||
cancelAddCategory()
|
cancelAddCategory()
|
||||||
formError.value = null
|
formError.value = null
|
||||||
formOpen.value = true
|
formOpen.value = true
|
||||||
@@ -756,7 +785,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<label>거래일<input v-model="form.entryDate" type="date" :disabled="submitting" /></label>
|
<label>거래일<input v-model="form.entryDate" type="date" :disabled="submitting" /></label>
|
||||||
<label>구분
|
<label>구분
|
||||||
<select v-model="form.type" :disabled="submitting">
|
<select v-model="form.type" :disabled="submitting" @change="onTypeChange">
|
||||||
<option value="EXPENSE">지출</option>
|
<option value="EXPENSE">지출</option>
|
||||||
<option value="INCOME">수입</option>
|
<option value="INCOME">수입</option>
|
||||||
<option value="TRANSFER">이체</option>
|
<option value="TRANSFER">이체</option>
|
||||||
@@ -825,17 +854,14 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<label v-if="form.type === 'INCOME' || form.type === 'EXPENSE'">분류
|
<label v-if="form.type === 'INCOME' || form.type === 'EXPENSE'">분류
|
||||||
<div v-if="!addingCategory" class="cat-input">
|
<div v-if="!addingCategory" class="cat-input cascade">
|
||||||
<select v-model="form.category" :disabled="submitting">
|
<select v-model="categoryMajor" :disabled="submitting" @change="onMajorChange" title="대분류">
|
||||||
<option value="">(선택)</option>
|
<option value="">대분류 선택</option>
|
||||||
<template v-for="g in categoryGroups" :key="g.key">
|
<option v-for="m in majorOptions" :key="m" :value="m">{{ m }}</option>
|
||||||
<optgroup v-if="g.label" :label="g.label">
|
</select>
|
||||||
<option v-for="n in g.options" :key="n" :value="n">{{ n }}</option>
|
<select v-if="subOptions.length" v-model="form.category" :disabled="submitting" title="소분류">
|
||||||
</optgroup>
|
<option value="">소분류 선택</option>
|
||||||
<template v-else>
|
<option v-for="s in subOptions" :key="s" :value="s">{{ s }}</option>
|
||||||
<option v-for="n in g.options" :key="n" :value="n">{{ n }}</option>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="cat-add-btn" :disabled="submitting" @click="startAddCategory">+ 추가</button>
|
<button type="button" class="cat-add-btn" :disabled="submitting" @click="startAddCategory">+ 추가</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1412,6 +1438,12 @@ button.primary {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
.cat-input.cascade {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.cat-input.cascade select {
|
||||||
|
flex: 1 1 40%;
|
||||||
|
}
|
||||||
.cat-input select,
|
.cat-input select,
|
||||||
.cat-input input {
|
.cat-input input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user