feat: 분류 드래그앤드랍 정렬 + 예산화면 예상수입/예산 비교
CI / build (push) Failing after 13m46s

- 분류 관리: SortableJS 드래그 정렬(터치 지원), 순서는 내역 추가 화면에도 반영
- 예산: 월 예상 수입 입력 + 수입 대비 총 예산 비교(여유/초과)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-01 22:37:56 +09:00
parent 63a8a71e3b
commit 252f00e527
5 changed files with 194 additions and 6 deletions
+111
View File
@@ -81,6 +81,32 @@ function barClass(s) {
return r >= 1 ? 'over' : r >= 0.8 ? 'warn' : 'ok'
}
// ===== 월 예상 수입 vs 총 예산 비교 =====
const expectedIncome = ref(null)
const incomeDraft = ref('')
async function loadIncome() {
try {
const r = await accountApi.expectedIncome()
expectedIncome.value = r.expectedIncome
incomeDraft.value = r.expectedIncome ?? ''
} catch {
expectedIncome.value = null
}
}
async function saveIncome() {
try {
const v = incomeDraft.value === '' || incomeDraft.value == null ? 0 : Number(incomeDraft.value)
const r = await accountApi.setExpectedIncome(v)
expectedIncome.value = r.expectedIncome
} catch (e) {
alert(e.response?.data?.message || '저장에 실패했습니다.')
}
}
const totalBudget = computed(() => statuses.value.reduce((s, x) => s + (x.monthlyBudget || 0), 0))
const incomeVal = computed(() => Number(expectedIncome.value) || 0)
const leftover = computed(() => incomeVal.value - totalBudget.value) // 여유(저축 가능액)
const budgetOfIncome = computed(() => (incomeVal.value > 0 ? Math.min(totalBudget.value / incomeVal.value, 1) : 0))
async function load() {
loading.value = true
error.value = null
@@ -193,6 +219,7 @@ async function remove(s) {
onMounted(() => {
load()
loadCategories()
loadIncome()
})
</script>
@@ -212,6 +239,28 @@ onMounted(() => {
<IconBtn icon="chevronRight" title="다음 달" size="sm" @click="nextMonth" />
</div>
<!-- 예상 수입 vs 예산 -->
<div class="income-panel">
<div class="inc-input">
<label> 예상 수입</label>
<input v-model.number="incomeDraft" type="number" min="0" placeholder="예상 수입(원)" @keyup.enter="saveIncome" />
<IconBtn icon="check" title="저장" variant="primary" size="sm" @click="saveIncome" />
</div>
<div v-if="incomeVal > 0" class="inc-compare">
<div class="inc-bar">
<div class="inc-fill" :class="leftover < 0 ? 'over' : ''" :style="{ width: budgetOfIncome * 100 + '%' }"></div>
</div>
<div class="inc-nums">
<span>수입 <b class="income">{{ won(incomeVal) }}</b></span>
<span>예산 <b>{{ won(totalBudget) }}</b></span>
<span :class="leftover < 0 ? 'expense' : 'income'">
{{ leftover >= 0 ? '여유' : '초과' }} <b>{{ won(Math.abs(leftover)) }}</b>
</span>
</div>
</div>
<p v-else class="inc-hint">예상 수입을 입력하면 설정한 예산과 비교해 여유 금액을 보여줍니다.</p>
</div>
<p v-if="error" class="msg error">{{ error }}</p>
<p v-if="loading" class="msg">불러오는 중...</p>
@@ -352,6 +401,68 @@ button.primary {
min-width: 8rem;
text-align: center;
}
/* 예상 수입 vs 예산 */
.income-panel {
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.8rem 1rem;
margin-bottom: 1.25rem;
}
.inc-input {
display: flex;
align-items: center;
gap: 0.5rem;
}
.inc-input label {
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.inc-input input {
flex: 1;
min-width: 0;
padding: 0.45rem 0.6rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
}
.inc-compare {
margin-top: 0.7rem;
}
.inc-bar {
height: 8px;
background: var(--color-background-mute);
border-radius: 999px;
overflow: hidden;
}
.inc-fill {
height: 100%;
border-radius: 999px;
background: #2e7d32;
transition: width 0.3s;
}
.inc-fill.over {
background: #c0392b;
}
.inc-nums {
display: flex;
justify-content: space-between;
gap: 0.5rem;
margin-top: 0.4rem;
font-size: 0.85rem;
}
.inc-nums .income {
color: #2e7d32;
}
.inc-nums .expense {
color: #c0392b;
}
.inc-hint {
margin-top: 0.5rem;
font-size: 0.8rem;
opacity: 0.65;
}
.status-list {
list-style: none;
}