Merge branch 'dev'
Deploy / deploy (push) Failing after 12m36s
CI / build (push) Failing after 14m4s

This commit is contained in:
ByungCheol
2026-06-22 23:57:19 +09:00
+64 -32
View File
@@ -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;