feat(ui): 태그관리 순서변경 토글, 고정지출 월/년 주기만
Deploy / deploy (push) Failing after 11m46s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-07-07 21:58:56 +09:00
parent 654fc44f48
commit 5beb999535
2 changed files with 54 additions and 23 deletions
+53 -10
View File
@@ -1,5 +1,5 @@
<script setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
import { onBeforeUnmount, onMounted, ref, nextTick, watch } from 'vue'
import Sortable from 'sortablejs'
import { accountApi } from '@/api/accountApi'
import { useDialog } from '@/composables/dialog'
@@ -11,6 +11,11 @@ const tags = ref([])
const loading = ref(false)
const error = ref(null)
const newName = ref('')
const reorderMode = ref(false)
function toggleReorder() {
reorderMode.value = !reorderMode.value
}
async function load() {
loading.value = true
@@ -88,17 +93,29 @@ async function removeTag(tag) {
}
}
onMounted(async () => {
await load()
await nextTick()
initSortable()
watch(reorderMode, async (on) => {
if (on) {
await nextTick()
initSortable()
} else {
if (sortable) { sortable.destroy(); sortable = null }
}
})
onMounted(load)
onBeforeUnmount(() => sortable?.destroy())
</script>
<template>
<section class="tag-admin">
<p class="hint">내가 등록한 태그는 나의 가계부 내역에서만 사용됩니다.</p>
<div class="top-row">
<p class="hint">내가 등록한 태그는 나의 가계부 내역에서만 사용됩니다.</p>
<button v-if="tags.length" type="button" class="text-btn reorder-btn" :class="{ active: reorderMode }" @click="toggleReorder">
{{ reorderMode ? '완료' : '순서변경' }}
</button>
</div>
<p v-if="reorderMode" class="hint sm"> 손잡이를 끌어 순서를 바꾸세요. 끝나면 <b>완료</b>.</p>
<form class="new-tag" @submit.prevent="addTag">
<input v-model="newName" type="text" placeholder="새 태그 이름 (예: 식비, 고정지출)" />
@@ -110,10 +127,10 @@ onBeforeUnmount(() => sortable?.destroy())
<ul v-show="!loading && tags.length" ref="listEl" class="tag-list">
<li v-for="t in tags" :key="t.id" :data-id="t.id" class="tag-row">
<span class="drag-handle" title="드래그하여 순서 변경"></span>
<input v-model="t.name" class="tag-name" @keyup.enter="saveTag(t)" />
<IconBtn icon="check" title="저장" @click="saveTag(t)" />
<IconBtn icon="trash" title="삭제" variant="danger" @click="removeTag(t)" />
<span v-if="reorderMode" class="drag-handle" title="드래그하여 순서 변경"></span>
<input v-model="t.name" class="tag-name" @keyup.enter="saveTag(t)" :disabled="reorderMode" />
<IconBtn v-if="!reorderMode" icon="check" title="저장" @click="saveTag(t)" />
<IconBtn v-if="!reorderMode" icon="trash" title="삭제" variant="danger" @click="removeTag(t)" />
</li>
</ul>
<p v-if="!loading && !tags.length" class="msg">등록된 태그가 없습니다.</p>
@@ -133,11 +150,37 @@ onBeforeUnmount(() => sortable?.destroy())
h1 {
font-size: 1.5rem;
}
.top-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.hint {
font-size: 0.85rem;
opacity: 0.7;
margin: 0.5rem 0 1rem;
}
.hint.sm {
margin-top: -0.5rem;
margin-bottom: 0.75rem;
}
.text-btn {
flex-shrink: 0;
font-size: 0.85rem;
padding: 0.25rem 0.6rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: transparent;
color: var(--color-text);
cursor: pointer;
white-space: nowrap;
}
.text-btn.active {
border-color: hsla(160, 100%, 37%, 1);
color: hsla(160, 100%, 37%, 1);
}
input {
padding: 0.5rem 0.7rem;
border: 1px solid var(--color-border);
+1 -13
View File
@@ -48,7 +48,6 @@ const form = reactive({
const submitting = ref(false)
const formError = ref(null)
const DOW = ['', '월', '화', '수', '목', '금', '토', '일']
const TYPES = [
{ value: 'EXPENSE', label: '지출', cls: 'chip-expense' },
@@ -56,8 +55,6 @@ const TYPES = [
{ value: 'TRANSFER', label: '이체', cls: 'chip-transfer' },
]
const FREQS = [
{ value: 'DAILY', label: '매일' },
{ value: 'WEEKLY', label: '매주' },
{ value: 'MONTHLY', label: '매월' },
{ value: 'YEARLY', label: '매년' },
]
@@ -102,21 +99,17 @@ function typeLabel(t) {
return t === 'INCOME' ? '수입' : t === 'TRANSFER' ? '이체' : '지출'
}
function freqLabel(r) {
if (r.frequency === 'DAILY') return '매일'
if (r.frequency === 'WEEKLY') return `매주 ${DOW[r.dayOfWeek] || ''}요일`
if (r.frequency === 'MONTHLY') return `매월 ${r.dayOfMonth}`
return `매년 ${r.monthOfYear}/${r.dayOfMonth}`
}
function payKey(r) {
if (r.frequency === 'DAILY') return 0
if (r.frequency === 'WEEKLY') return r.dayOfWeek || 0
if (r.frequency === 'YEARLY') return (r.monthOfYear || 0) * 100 + (r.dayOfMonth || 0)
return r.dayOfMonth || 0
}
const PERIODS = [
{ key: 'MONTH', label: '월간 거래', freqs: ['DAILY', 'WEEKLY', 'MONTHLY'] },
{ key: 'MONTH', label: '월간 거래', freqs: ['MONTHLY'] },
{ key: 'YEAR', label: '년간 거래', freqs: ['YEARLY'] },
]
const grouped = computed(() =>
@@ -391,11 +384,6 @@ onMounted(load)
</div>
</div>
<label v-if="form.frequency === 'WEEKLY'">요일
<select v-model.number="form.dayOfWeek" :disabled="submitting">
<option v-for="d in 7" :key="d" :value="d">{{ DOW[d] }}</option>
</select>
</label>
<label v-if="form.frequency === 'YEARLY'">
<input v-model.number="form.monthOfYear" type="number" min="1" max="12" :disabled="submitting" />
</label>