- 예산: 예상 수입을 해당 월별로 조회/저장(월 이동 시 갱신) - 계좌 관리: SortableJS 드래그 정렬(터치 지원), 타입별 순서 저장 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -65,11 +65,11 @@ export const accountApi = {
|
|||||||
budgetPeriod({ unit, year, month }) {
|
budgetPeriod({ unit, year, month }) {
|
||||||
return http.get('/account/budgets/period', { params: { unit, year, month } })
|
return http.get('/account/budgets/period', { params: { unit, year, month } })
|
||||||
},
|
},
|
||||||
expectedIncome() {
|
expectedIncome({ year, month }) {
|
||||||
return http.get('/account/budgets/income')
|
return http.get('/account/budgets/income', { params: { year, month } })
|
||||||
},
|
},
|
||||||
setExpectedIncome(expectedIncome) {
|
setExpectedIncome({ year, month, expectedIncome }) {
|
||||||
return http.put('/account/budgets/income', { expectedIncome })
|
return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } })
|
||||||
},
|
},
|
||||||
createBudget(payload) {
|
createBudget(payload) {
|
||||||
return http.post('/account/budgets', payload)
|
return http.post('/account/budgets', payload)
|
||||||
@@ -89,6 +89,9 @@ export const accountApi = {
|
|||||||
removeWallet(id) {
|
removeWallet(id) {
|
||||||
return http.delete(`/account/wallets/${id}`)
|
return http.delete(`/account/wallets/${id}`)
|
||||||
},
|
},
|
||||||
|
reorderWallets(type, ids) {
|
||||||
|
return http.put('/account/wallets/reorder', { type, ids })
|
||||||
|
},
|
||||||
walletEntries(id) {
|
walletEntries(id) {
|
||||||
return http.get(`/account/wallets/${id}/entries`)
|
return http.get(`/account/wallets/${id}/entries`)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import Sortable from 'sortablejs'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
import InvestPortfolio from './InvestPortfolio.vue'
|
import InvestPortfolio from './InvestPortfolio.vue'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
@@ -41,9 +42,46 @@ const expandedId = ref(null)
|
|||||||
const entriesByWallet = reactive({})
|
const entriesByWallet = reactive({})
|
||||||
const loadingEntries = ref(false)
|
const loadingEntries = ref(false)
|
||||||
|
|
||||||
const filtered = computed(() => wallets.value.filter((w) => w.type === activeType.value))
|
|
||||||
const isLiability = (t) => t === 'CARD' || t === 'LOAN'
|
const isLiability = (t) => t === 'CARD' || t === 'LOAN'
|
||||||
|
|
||||||
|
// 현재 탭(activeType)의 계좌 — 드래그 정렬 대상 (백엔드가 sort_order 순 정렬)
|
||||||
|
const rows = ref([])
|
||||||
|
function syncRows() {
|
||||||
|
rows.value = wallets.value.filter((w) => w.type === activeType.value)
|
||||||
|
}
|
||||||
|
watch([wallets, activeType], syncRows, { immediate: true })
|
||||||
|
|
||||||
|
// ===== 드래그앤드랍 순서 변경 (SortableJS, 터치 지원) =====
|
||||||
|
const listEl = ref(null)
|
||||||
|
let sortable = null
|
||||||
|
function initSortable() {
|
||||||
|
if (sortable) {
|
||||||
|
sortable.destroy()
|
||||||
|
sortable = null
|
||||||
|
}
|
||||||
|
if (!listEl.value) return
|
||||||
|
sortable = Sortable.create(listEl.value, {
|
||||||
|
handle: '.drag-handle',
|
||||||
|
animation: 150,
|
||||||
|
onEnd: (evt) => {
|
||||||
|
if (evt.oldIndex === evt.newIndex) return
|
||||||
|
const arr = rows.value
|
||||||
|
const moved = arr.splice(evt.oldIndex, 1)[0]
|
||||||
|
arr.splice(evt.newIndex, 0, moved)
|
||||||
|
persistOrder(arr.map((r) => r.id))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function persistOrder(ids) {
|
||||||
|
try {
|
||||||
|
await accountApi.reorderWallets(activeType.value, ids)
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.response?.data?.message || '순서 저장에 실패했습니다.')
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleExpand(w) {
|
async function toggleExpand(w) {
|
||||||
if (expandedId.value === w.id) {
|
if (expandedId.value === w.id) {
|
||||||
expandedId.value = null
|
expandedId.value = null
|
||||||
@@ -196,7 +234,12 @@ function cardTypeLabel(v) {
|
|||||||
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
|
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(async () => {
|
||||||
|
await load()
|
||||||
|
await nextTick()
|
||||||
|
initSortable()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => sortable?.destroy())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -239,9 +282,10 @@ onMounted(load)
|
|||||||
<p v-if="error" class="msg error">{{ error }}</p>
|
<p v-if="error" class="msg error">{{ error }}</p>
|
||||||
<p v-if="loading" class="msg">불러오는 중...</p>
|
<p v-if="loading" class="msg">불러오는 중...</p>
|
||||||
|
|
||||||
<ul v-else-if="filtered.length" class="wallet-list">
|
<ul v-show="!loading && rows.length" ref="listEl" class="wallet-list">
|
||||||
<li v-for="w in filtered" :key="w.id" class="wallet-item">
|
<li v-for="w in rows" :key="w.id" :data-id="w.id" class="wallet-item">
|
||||||
<div class="wallet-row" @click="toggleExpand(w)">
|
<div class="wallet-row" @click="toggleExpand(w)">
|
||||||
|
<span class="drag-handle" title="드래그하여 순서 변경" @click.stop>≡</span>
|
||||||
<span class="chevron">{{ expandedId === w.id ? '▾' : '▸' }}</span>
|
<span class="chevron">{{ expandedId === w.id ? '▾' : '▸' }}</span>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="line1">
|
<div class="line1">
|
||||||
@@ -288,7 +332,7 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else-if="!loading" class="msg">등록된 항목이 없습니다.</p>
|
<p v-if="!loading && !rows.length" class="msg">등록된 항목이 없습니다.</p>
|
||||||
|
|
||||||
<!-- 추가/수정 모달 -->
|
<!-- 추가/수정 모달 -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
@@ -427,6 +471,17 @@ button.primary {
|
|||||||
.wallet-row:hover {
|
.wallet-row:hover {
|
||||||
background: var(--color-background-soft);
|
background: var(--color-background-soft);
|
||||||
}
|
}
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.45;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 0 0.15rem;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
.chevron {
|
.chevron {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|||||||
@@ -86,17 +86,18 @@ const expectedIncome = ref(null)
|
|||||||
const incomeDraft = ref('')
|
const incomeDraft = ref('')
|
||||||
async function loadIncome() {
|
async function loadIncome() {
|
||||||
try {
|
try {
|
||||||
const r = await accountApi.expectedIncome()
|
const r = await accountApi.expectedIncome({ year: year.value, month: month.value })
|
||||||
expectedIncome.value = r.expectedIncome
|
expectedIncome.value = r.expectedIncome
|
||||||
incomeDraft.value = r.expectedIncome ?? ''
|
incomeDraft.value = r.expectedIncome ?? ''
|
||||||
} catch {
|
} catch {
|
||||||
expectedIncome.value = null
|
expectedIncome.value = null
|
||||||
|
incomeDraft.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function saveIncome() {
|
async function saveIncome() {
|
||||||
try {
|
try {
|
||||||
const v = incomeDraft.value === '' || incomeDraft.value == null ? 0 : Number(incomeDraft.value)
|
const v = incomeDraft.value === '' || incomeDraft.value == null ? 0 : Number(incomeDraft.value)
|
||||||
const r = await accountApi.setExpectedIncome(v)
|
const r = await accountApi.setExpectedIncome({ year: year.value, month: month.value, expectedIncome: v })
|
||||||
expectedIncome.value = r.expectedIncome
|
expectedIncome.value = r.expectedIncome
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.response?.data?.message || '저장에 실패했습니다.')
|
alert(e.response?.data?.message || '저장에 실패했습니다.')
|
||||||
@@ -128,6 +129,7 @@ function prevMonth() {
|
|||||||
year.value -= 1
|
year.value -= 1
|
||||||
} else month.value -= 1
|
} else month.value -= 1
|
||||||
load()
|
load()
|
||||||
|
loadIncome()
|
||||||
}
|
}
|
||||||
function nextMonth() {
|
function nextMonth() {
|
||||||
if (month.value === 12) {
|
if (month.value === 12) {
|
||||||
@@ -135,6 +137,7 @@ function nextMonth() {
|
|||||||
year.value += 1
|
year.value += 1
|
||||||
} else month.value += 1
|
} else month.value += 1
|
||||||
load()
|
load()
|
||||||
|
loadIncome()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
|
|||||||
Reference in New Issue
Block a user