- 화면이동 화살표(›) 제거 — 삭제 버튼 옆이라 오탭 위험 - 계좌 클릭 대신 '계좌명' 클릭 시에만 상세 이동(링크 색상 표시) - 순서변경은 기본 잠금 — '순서변경' 버튼으로 켤 때만 드래그 핸들·재정렬 활성 - 순서변경 모드에선 수정/삭제 버튼 숨겨 오작동 방지, 드래그 낙관적 반영 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, nextTick } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
@@ -35,6 +35,8 @@ const wallets = ref([])
|
|||||||
const networth = ref({ totalAssets: 0, totalLiabilities: 0, netWorth: 0 })
|
const networth = ref({ totalAssets: 0, totalLiabilities: 0, netWorth: 0 })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
// 순서변경(잠금) 모드 — 평소엔 잠금(드래그·핸들 숨김), 켤 때만 드래그 가능
|
||||||
|
const reorderMode = ref(false)
|
||||||
// 종류별 그룹(탭 제거) — TABS 순서로, 계좌가 있는 종류만 노출
|
// 종류별 그룹(탭 제거) — TABS 순서로, 계좌가 있는 종류만 노출
|
||||||
const groups = computed(() =>
|
const groups = computed(() =>
|
||||||
TABS.map((t) => ({ ...t, wallets: wallets.value.filter((w) => w.type === t.key) })).filter(
|
TABS.map((t) => ({ ...t, wallets: wallets.value.filter((w) => w.type === t.key) })).filter(
|
||||||
@@ -79,6 +81,7 @@ function destroySortables() {
|
|||||||
}
|
}
|
||||||
function initSortables() {
|
function initSortables() {
|
||||||
destroySortables()
|
destroySortables()
|
||||||
|
if (!reorderMode.value) return // 잠금 상태에선 드래그 비활성화
|
||||||
document.querySelectorAll('.wallet-group-list').forEach((el) => {
|
document.querySelectorAll('.wallet-group-list').forEach((el) => {
|
||||||
sortables.push(
|
sortables.push(
|
||||||
Sortable.create(el, {
|
Sortable.create(el, {
|
||||||
@@ -88,12 +91,21 @@ function initSortables() {
|
|||||||
if (evt.oldIndex === evt.newIndex) return
|
if (evt.oldIndex === evt.newIndex) return
|
||||||
const type = el.dataset.type
|
const type = el.dataset.type
|
||||||
const ids = [...el.querySelectorAll(':scope > .wallet-item')].map((li) => Number(li.dataset.id))
|
const ids = [...el.querySelectorAll(':scope > .wallet-item')].map((li) => Number(li.dataset.id))
|
||||||
|
// 낙관적 반영: Sortable 이 옮긴 DOM 순서에 맞춰 데이터도 재정렬(Vue 재렌더 정합)
|
||||||
|
const byId = new Map(wallets.value.map((w) => [w.id, w]))
|
||||||
|
const moved = ids.map((id) => byId.get(id)).filter(Boolean)
|
||||||
|
wallets.value = [...moved, ...wallets.value.filter((w) => w.type !== type)]
|
||||||
persistOrder(type, ids)
|
persistOrder(type, ids)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 순서변경 모드 토글 시 드래그 바인딩/해제
|
||||||
|
watch(reorderMode, async () => {
|
||||||
|
await nextTick()
|
||||||
|
initSortables()
|
||||||
|
})
|
||||||
async function persistOrder(type, ids) {
|
async function persistOrder(type, ids) {
|
||||||
try {
|
try {
|
||||||
await accountApi.reorderWallets(type, ids)
|
await accountApi.reorderWallets(type, ids)
|
||||||
@@ -282,9 +294,15 @@ onBeforeUnmount(destroySortables)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wallet-head">
|
<div class="wallet-head">
|
||||||
|
<button
|
||||||
|
v-if="wallets.length > 1"
|
||||||
|
type="button" class="text-btn reorder-btn" :class="{ active: reorderMode }"
|
||||||
|
@click="reorderMode = !reorderMode"
|
||||||
|
>{{ reorderMode ? '완료' : '순서변경' }}</button>
|
||||||
<IconBtn class="tab-add" icon="plus" title="계좌 추가" variant="primary" @click="openCreate" />
|
<IconBtn class="tab-add" icon="plus" title="계좌 추가" variant="primary" @click="openCreate" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="reorderMode" class="msg reorder-hint">≡ 손잡이를 끌어 순서를 바꾸세요. 끝나면 <b>완료</b>를 누르세요.</p>
|
||||||
<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>
|
||||||
|
|
||||||
@@ -293,11 +311,14 @@ onBeforeUnmount(destroySortables)
|
|||||||
<h3 class="wg-title">{{ g.label }}</h3>
|
<h3 class="wg-title">{{ g.label }}</h3>
|
||||||
<ul class="wallet-list wallet-group-list" :data-type="g.key">
|
<ul class="wallet-list wallet-group-list" :data-type="g.key">
|
||||||
<li v-for="w in g.wallets" :key="w.id" :data-id="w.id" class="wallet-item">
|
<li v-for="w in g.wallets" :key="w.id" :data-id="w.id" class="wallet-item">
|
||||||
<div class="wallet-row" @click="goDetail(w)">
|
<div class="wallet-row">
|
||||||
<span class="drag-handle" title="드래그하여 순서 변경" @click.stop>≡</span>
|
<span v-if="reorderMode" class="drag-handle" title="드래그하여 순서 변경">≡</span>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="line1">
|
<div class="line1">
|
||||||
<span class="name">{{ w.name }}</span>
|
<span
|
||||||
|
class="name" :class="{ link: !reorderMode }"
|
||||||
|
@click="!reorderMode && goDetail(w)"
|
||||||
|
>{{ w.name }}</span>
|
||||||
<span v-if="w.type === 'CARD'" class="badge">{{ cardTypeLabel(w.cardType) }}</span>
|
<span v-if="w.type === 'CARD'" class="badge">{{ cardTypeLabel(w.cardType) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sub">
|
<span class="sub">
|
||||||
@@ -320,11 +341,10 @@ onBeforeUnmount(destroySortables)
|
|||||||
<div v-if="w.type === 'INVEST'" class="balance">{{ won(w.investedAmount) }}</div>
|
<div v-if="w.type === 'INVEST'" class="balance">{{ won(w.investedAmount) }}</div>
|
||||||
<div v-else class="balance" :class="{ neg: w.balance < 0 }">{{ won(w.balance) }}</div>
|
<div v-else class="balance" :class="{ neg: w.balance < 0 }">{{ won(w.balance) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" @click.stop>
|
<div v-if="!reorderMode" class="actions">
|
||||||
<IconBtn icon="edit" title="수정" size="sm" @click="openEdit(w)" />
|
<IconBtn icon="edit" title="수정" size="sm" @click="openEdit(w)" />
|
||||||
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="remove(w)" />
|
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="remove(w)" />
|
||||||
</div>
|
</div>
|
||||||
<span class="go-chevron" aria-hidden="true">›</span>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -470,11 +490,33 @@ button.primary {
|
|||||||
}
|
}
|
||||||
.wallet-head {
|
.wallet-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
.tab-add {
|
.tab-add {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.text-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
opacity: 0.75;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.reorder-btn.active {
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
font-weight: 700;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.reorder-hint {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
}
|
}
|
||||||
.wallet-group {
|
.wallet-group {
|
||||||
margin-bottom: 1.4rem;
|
margin-bottom: 1.4rem;
|
||||||
@@ -487,12 +529,6 @@ button.primary {
|
|||||||
padding-bottom: 0.3rem;
|
padding-bottom: 0.3rem;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
.go-chevron {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
opacity: 0.35;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
padding-left: 0.15rem;
|
|
||||||
}
|
|
||||||
.wallet-list {
|
.wallet-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
@@ -506,10 +542,6 @@ button.primary {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
padding: 0.7rem 0;
|
padding: 0.7rem 0;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.wallet-row:hover {
|
|
||||||
background: var(--color-background-soft);
|
|
||||||
}
|
}
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
@@ -663,6 +695,13 @@ button.primary {
|
|||||||
.name {
|
.name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.name.link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
}
|
||||||
|
.name.link:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|||||||
Reference in New Issue
Block a user