- 예산: 예상 수입을 해당 월별로 조회/저장(월 이동 시 갱신) - 계좌 관리: SortableJS 드래그 정렬(터치 지원), 타입별 순서 저장 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user