@@ -76,8 +76,12 @@ const realApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 예산 (사용자별)
|
// 예산 (사용자별)
|
||||||
budgets() {
|
budgets({ year, month } = {}) {
|
||||||
return http.get('/account/budgets')
|
return http.get('/account/budgets', { params: { year, month } })
|
||||||
|
},
|
||||||
|
// 다른 월 예산을 이 달로 복사(전월 복사 등)
|
||||||
|
copyBudget({ fromYear, fromMonth, toYear, toMonth }) {
|
||||||
|
return http.post('/account/budgets/copy', null, { params: { fromYear, fromMonth, toYear, toMonth } })
|
||||||
},
|
},
|
||||||
budgetStatus({ year, month }) {
|
budgetStatus({ year, month }) {
|
||||||
return http.get('/account/budgets/status', { params: { year, month } })
|
return http.get('/account/budgets/status', { params: { year, month } })
|
||||||
@@ -91,8 +95,8 @@ const realApi = {
|
|||||||
setExpectedIncome({ year, month, expectedIncome }) {
|
setExpectedIncome({ year, month, expectedIncome }) {
|
||||||
return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } })
|
return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } })
|
||||||
},
|
},
|
||||||
createBudget(payload) {
|
createBudget(payload, { year, month } = {}) {
|
||||||
return http.post('/account/budgets', payload)
|
return http.post('/account/budgets', payload, { params: { year, month } })
|
||||||
},
|
},
|
||||||
updateBudget(id, payload) {
|
updateBudget(id, payload) {
|
||||||
return http.put(`/account/budgets/${id}`, payload)
|
return http.put(`/account/budgets/${id}`, payload)
|
||||||
|
|||||||
+2
-1
@@ -44,7 +44,8 @@ export async function exportBackup() {
|
|||||||
safe(accountApi.wallets()),
|
safe(accountApi.wallets()),
|
||||||
safe(accountApi.categories()),
|
safe(accountApi.categories()),
|
||||||
safe(accountApi.recurrings()),
|
safe(accountApi.recurrings()),
|
||||||
safe(accountApi.budgets()),
|
// 예산은 월별 — 백업엔 현재 월 예산을 담는다
|
||||||
|
safe(accountApi.budgets({ year: new Date().getFullYear(), month: new Date().getMonth() + 1 })),
|
||||||
safe(accountApi.tags()),
|
safe(accountApi.tags()),
|
||||||
safe(accountApi.quickEntries()),
|
safe(accountApi.quickEntries()),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ async function load() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const params = { year: year.value, month: month.value }
|
const params = { year: year.value, month: month.value }
|
||||||
const [st, raw] = await Promise.all([accountApi.budgetStatus(params), accountApi.budgets()])
|
const [st, raw] = await Promise.all([accountApi.budgetStatus(params), accountApi.budgets(params)])
|
||||||
statuses.value = st
|
statuses.value = st
|
||||||
budgetsRaw.value = raw
|
budgetsRaw.value = raw
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -154,6 +154,46 @@ function nextMonth() {
|
|||||||
loadIncome()
|
loadIncome()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 월별 예산 복사 =====
|
||||||
|
const copying = ref(false)
|
||||||
|
const ym = (y, m) => `${y}.${String(m).padStart(2, '0')}`
|
||||||
|
async function copyToNextMonth() {
|
||||||
|
if (copying.value || !budgetsRaw.value.length) return
|
||||||
|
const to = month.value === 12 ? { y: year.value + 1, m: 1 } : { y: year.value, m: month.value + 1 }
|
||||||
|
const ok = await dialog.confirm(
|
||||||
|
`${ym(year.value, month.value)} 예산을 ${ym(to.y, to.m)}(다음 달)로 복사할까요?\n다음 달의 같은 분류 예산은 덮어씁니다.`,
|
||||||
|
{ title: '다음 달로 복사' },
|
||||||
|
)
|
||||||
|
if (!ok) return
|
||||||
|
copying.value = true
|
||||||
|
try {
|
||||||
|
await accountApi.copyBudget({ fromYear: year.value, fromMonth: month.value, toYear: to.y, toMonth: to.m })
|
||||||
|
await dialog.alert(`${ym(to.y, to.m)}로 예산을 복사했어요.`)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e.response?.data?.message || '복사에 실패했습니다.'
|
||||||
|
} finally {
|
||||||
|
copying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function copyFromPrevMonth() {
|
||||||
|
if (copying.value) return
|
||||||
|
const prev = month.value === 1 ? { y: year.value - 1, m: 12 } : { y: year.value, m: month.value - 1 }
|
||||||
|
const ok = await dialog.confirm(
|
||||||
|
`전월(${ym(prev.y, prev.m)}) 예산을 ${ym(year.value, month.value)}로 가져올까요?\n이 달의 같은 분류 예산은 덮어씁니다.`,
|
||||||
|
{ title: '전월 예산 복사' },
|
||||||
|
)
|
||||||
|
if (!ok) return
|
||||||
|
copying.value = true
|
||||||
|
try {
|
||||||
|
await accountApi.copyBudget({ fromYear: prev.y, fromMonth: prev.m, toYear: year.value, toMonth: month.value })
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e.response?.data?.message || '복사에 실패했습니다.'
|
||||||
|
} finally {
|
||||||
|
copying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
editId.value = null
|
editId.value = null
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
@@ -213,7 +253,7 @@ async function submit() {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
if (editId.value) await accountApi.updateBudget(editId.value, payload)
|
if (editId.value) await accountApi.updateBudget(editId.value, payload)
|
||||||
else await accountApi.createBudget(payload)
|
else await accountApi.createBudget(payload, { year: year.value, month: month.value })
|
||||||
formOpen.value = false
|
formOpen.value = false
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -252,6 +292,12 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 월별 예산 복사 -->
|
||||||
|
<div class="copy-actions">
|
||||||
|
<button type="button" class="copy-btn" :disabled="copying" @click="copyFromPrevMonth">↤ 전월 예산 가져오기</button>
|
||||||
|
<button type="button" class="copy-btn" :disabled="copying || !budgetsRaw.length" @click="copyToNextMonth">다음 달로 복사 ↦</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 월 예상 수입 vs 총 예산 -->
|
<!-- 월 예상 수입 vs 총 예산 -->
|
||||||
<div class="income-panel">
|
<div class="income-panel">
|
||||||
<div class="inc-input">
|
<div class="inc-input">
|
||||||
@@ -431,6 +477,26 @@ button.primary {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
/* 예상 수입 vs 예산 */
|
/* 예상 수입 vs 예산 */
|
||||||
|
.copy-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: -0.4rem 0 1rem;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.copy-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
.income-panel {
|
.income-panel {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user