feat: 분류 대/소분류 UI — 관리·입력·예산·고정지출·통계
CI / build (push) Failing after 12m0s

- 분류 관리: 대분류 선택·소분류 들여쓰기(드래그 정렬 유지)
- 입력/예산/고정지출 분류 드롭다운 대분류 optgroup 그룹핑(값은 소분류명)
- 대시보드: 파이는 소분류 기준 + 대분류 합계 롤업 표시
- 릴리스 노트 33

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-22 23:16:46 +09:00
parent b1f85cee52
commit 6ab8d331e3
6 changed files with 182 additions and 9 deletions
@@ -22,6 +22,7 @@ const barData = ref([]) // [{bucket, budget, expense}]
const monthlyStats = ref([]) // MONTH unit of year (income/expense)
const catType = ref('EXPENSE') // 분류별 파이차트 구분
const catData = ref([]) // [{category, total}]
const categoryList = ref([]) // 분류 목록(대/소분류 매핑용)
const trendData = ref([]) // [{month, netWorth}]
const periodLabel = computed(() => `${year.value}${String(month.value).padStart(2, '0')}`)
@@ -188,6 +189,33 @@ async function loadCat() {
catData.value = []
}
}
async function loadCategoryList() {
try {
categoryList.value = await accountApi.categories()
} catch {
categoryList.value = []
}
}
// 소분류명 → 대분류명 매핑(현재 구분). 소분류가 아니면 자기 자신.
const majorRollup = computed(() => {
const list = categoryList.value.filter((c) => c.type === catType.value)
const byId = {}
list.forEach((c) => (byId[c.id] = c))
const toMajor = (name) => {
const c = list.find((x) => x.name === name)
if (c && c.parentId != null && byId[c.parentId]) return byId[c.parentId].name
return name
}
const map = {}
for (const d of catData.value) {
const major = toMajor(d.category)
map[major] = (map[major] || 0) + d.total
}
const rows = Object.entries(map).map(([category, total]) => ({ category, total })).sort((a, b) => b.total - a.total)
// 실제로 묶인 게 있을 때만(소분류 존재) 노출
const hasSub = list.some((c) => c.parentId != null) && rows.length < catData.value.length
return hasSub ? rows : []
})
function setCatType(t) {
catType.value = t
loadCat()
@@ -290,6 +318,7 @@ onMounted(async () => {
load()
loadMonthly()
loadTrend()
loadCategoryList()
})
</script>
@@ -393,6 +422,15 @@ onMounted(async () => {
</li>
</ul>
</div>
<div v-if="majorRollup.length" class="major-rollup">
<div class="mr-head">대분류 합계</div>
<ul class="mr-list">
<li v-for="m in majorRollup" :key="m.category">
<span class="mr-cat">{{ m.category }}</span>
<span class="mr-amt">{{ won(m.total) }}</span>
</li>
</ul>
</div>
<p v-else class="empty">{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.</p>
</div>
@@ -760,6 +798,33 @@ h2 {
fill: var(--color-text);
opacity: 0.6;
}
.major-rollup {
margin-top: 0.8rem;
padding-top: 0.6rem;
border-top: 1px dashed var(--color-border);
}
.mr-head {
font-size: 0.78rem;
opacity: 0.6;
margin-bottom: 0.3rem;
}
.mr-list {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.1rem 1rem;
margin: 0;
padding: 0;
}
.mr-list li {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
padding: 0.1rem 0;
}
.mr-cat {
font-weight: 600;
}
.pie-legend {
list-style: none;
width: 100%;