feat: 예산 예상수입 월별 + 계좌 관리 드래그앤드랍 정렬
CI / build (push) Failing after 12m7s

- 예산: 예상 수입을 해당 월별로 조회/저장(월 이동 시 갱신)
- 계좌 관리: SortableJS 드래그 정렬(터치 지원), 타입별 순서 저장

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-01 22:54:41 +09:00
parent 252f00e527
commit 483cf755ed
3 changed files with 73 additions and 12 deletions
+61 -6
View File
@@ -1,6 +1,7 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import { accountApi } from '@/api/accountApi'
import InvestPortfolio from './InvestPortfolio.vue'
import IconBtn from '@/components/ui/IconBtn.vue'
@@ -41,9 +42,46 @@ const expandedId = ref(null)
const entriesByWallet = reactive({})
const loadingEntries = ref(false)
const filtered = computed(() => wallets.value.filter((w) => w.type === activeType.value))
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) {
if (expandedId.value === w.id) {
expandedId.value = null
@@ -196,7 +234,12 @@ function cardTypeLabel(v) {
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
}
onMounted(load)
onMounted(async () => {
await load()
await nextTick()
initSortable()
})
onBeforeUnmount(() => sortable?.destroy())
</script>
<template>
@@ -239,9 +282,10 @@ onMounted(load)
<p v-if="error" class="msg error">{{ error }}</p>
<p v-if="loading" class="msg">불러오는 중...</p>
<ul v-else-if="filtered.length" class="wallet-list">
<li v-for="w in filtered" :key="w.id" class="wallet-item">
<ul v-show="!loading && rows.length" ref="listEl" class="wallet-list">
<li v-for="w in rows" :key="w.id" :data-id="w.id" class="wallet-item">
<div class="wallet-row" @click="toggleExpand(w)">
<span class="drag-handle" title="드래그하여 순서 변경" @click.stop></span>
<span class="chevron">{{ expandedId === w.id ? '▾' : '▸' }}</span>
<div class="info">
<div class="line1">
@@ -288,7 +332,7 @@ onMounted(load)
</div>
</li>
</ul>
<p v-else-if="!loading" class="msg">등록된 항목이 없습니다.</p>
<p v-if="!loading && !rows.length" class="msg">등록된 항목이 없습니다.</p>
<!-- 추가/수정 모달 -->
<Teleport to="body">
@@ -427,6 +471,17 @@ button.primary {
.wallet-row:hover {
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 {
width: 1rem;
opacity: 0.6;
+5 -2
View File
@@ -86,17 +86,18 @@ const expectedIncome = ref(null)
const incomeDraft = ref('')
async function loadIncome() {
try {
const r = await accountApi.expectedIncome()
const r = await accountApi.expectedIncome({ year: year.value, month: month.value })
expectedIncome.value = r.expectedIncome
incomeDraft.value = r.expectedIncome ?? ''
} catch {
expectedIncome.value = null
incomeDraft.value = ''
}
}
async function saveIncome() {
try {
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
} catch (e) {
alert(e.response?.data?.message || '저장에 실패했습니다.')
@@ -128,6 +129,7 @@ function prevMonth() {
year.value -= 1
} else month.value -= 1
load()
loadIncome()
}
function nextMonth() {
if (month.value === 12) {
@@ -135,6 +137,7 @@ function nextMonth() {
year.value += 1
} else month.value += 1
load()
loadIncome()
}
function openCreate() {