diff --git a/src/api/accountApi.js b/src/api/accountApi.js index b25b5b4..45f1ebc 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -76,8 +76,12 @@ const realApi = { }, // 예산 (사용자별) - budgets() { - return http.get('/account/budgets') + budgets({ year, month } = {}) { + 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 }) { return http.get('/account/budgets/status', { params: { year, month } }) @@ -91,8 +95,8 @@ const realApi = { setExpectedIncome({ year, month, expectedIncome }) { return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } }) }, - createBudget(payload) { - return http.post('/account/budgets', payload) + createBudget(payload, { year, month } = {}) { + return http.post('/account/budgets', payload, { params: { year, month } }) }, updateBudget(id, payload) { return http.put(`/account/budgets/${id}`, payload) diff --git a/src/utils/backup.js b/src/utils/backup.js index e70cad1..4a1777f 100644 --- a/src/utils/backup.js +++ b/src/utils/backup.js @@ -44,7 +44,8 @@ export async function exportBackup() { safe(accountApi.wallets()), safe(accountApi.categories()), safe(accountApi.recurrings()), - safe(accountApi.budgets()), + // 예산은 월별 — 백업엔 현재 월 예산을 담는다 + safe(accountApi.budgets({ year: new Date().getFullYear(), month: new Date().getMonth() + 1 })), safe(accountApi.tags()), safe(accountApi.quickEntries()), ]) diff --git a/src/views/account/BudgetView.vue b/src/views/account/BudgetView.vue index 007b321..90d9074 100644 --- a/src/views/account/BudgetView.vue +++ b/src/views/account/BudgetView.vue @@ -127,7 +127,7 @@ async function load() { error.value = null try { 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 budgetsRaw.value = raw } catch (e) { @@ -154,6 +154,46 @@ function nextMonth() { 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() { editId.value = null Object.assign(form, { @@ -213,7 +253,7 @@ async function submit() { submitting.value = true try { 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 await load() } catch (e) { @@ -252,6 +292,12 @@ onMounted(() => { + +
+ + +
+
@@ -431,6 +477,26 @@ button.primary { text-align: center; } /* 예상 수입 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 { border: 1px solid var(--color-border); border-radius: 8px;