feat: 통계 분류별 집계를 대분류 기준으로 (소분류 펼침 제거로 스크롤 단축)

- catByMajor: 소분류를 부모 대분류로 합산해 파이/범례 표기
- 중복되던 '대분류 합계' 섹션 제거

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-27 21:38:07 +09:00
parent a92063dd64
commit ef1d718fb8
+23 -32
View File
@@ -114,12 +114,32 @@ function hideTip() {
const PIE_R = 46 // viewBox 120 기준, 선두께 20(hover 24) 포함해도 잘리지 않도록 (46+12=58 ≤ 60) const PIE_R = 46 // viewBox 120 기준, 선두께 20(hover 24) 포함해도 잘리지 않도록 (46+12=58 ≤ 60)
const PIE_C = 2 * Math.PI * PIE_R const PIE_C = 2 * Math.PI * PIE_R
const PIE_COLORS = ['#3b82a6', '#e67e22', '#2e7d32', '#9b59b6', '#c0392b', '#16a085', '#f39c12', '#8e44ad', '#27ae60', '#d35400', '#2980b9', '#7f8c8d'] const PIE_COLORS = ['#3b82a6', '#e67e22', '#2e7d32', '#9b59b6', '#c0392b', '#16a085', '#f39c12', '#8e44ad', '#27ae60', '#d35400', '#2980b9', '#7f8c8d']
const catTotal = computed(() => catData.value.reduce((s, d) => s + d.total, 0)) // 분류별 집계를 대분류 기준으로 묶는다 (소분류까지 펼치면 항목이 너무 많아 스크롤이 길어짐).
// 소분류는 부모(대분류)명으로 합산, 매핑이 없으면 자기 자신을 대분류로 본다.
const catByMajor = 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
}
return Object.entries(map)
.map(([category, total]) => ({ category, total }))
.sort((a, b) => b.total - a.total)
})
const catTotal = computed(() => catByMajor.value.reduce((s, d) => s + d.total, 0))
const pieSegments = computed(() => { const pieSegments = computed(() => {
const total = catTotal.value const total = catTotal.value
if (total <= 0) return [] if (total <= 0) return []
let acc = 0 let acc = 0
return catData.value.map((d, i) => { return catByMajor.value.map((d, i) => {
const len = (d.total / total) * PIE_C const len = (d.total / total) * PIE_C
const seg = { const seg = {
category: d.category, category: d.category,
@@ -196,26 +216,6 @@ async function loadCategoryList() {
categoryList.value = [] 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) { function setCatType(t) {
catType.value = t catType.value = t
loadCat() loadCat()
@@ -422,16 +422,7 @@ onMounted(async () => {
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="majorRollup.length" class="major-rollup"> <p v-if="!pieSegments.length" class="empty">{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.</p>
<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> </div>
<!-- 기간별 예산 대비 지출 막대 차트 --> <!-- 기간별 예산 대비 지출 막대 차트 -->