- 분류 관리: SortableJS 드래그 정렬(터치 지원), 순서는 내역 추가 화면에도 반영 - 예산: 월 예상 수입 입력 + 수입 대비 총 예산 비교(여유/초과) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user