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> <script setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue' import { onBeforeUnmount, onMounted, ref, nextTick, watch } from 'vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { accountApi } from '@/api/accountApi' import { accountApi } from '@/api/accountApi'
import { useDialog } from '@/composables/dialog' import { useDialog } from '@/composables/dialog'
@@ -11,6 +11,11 @@ const tags = ref([])
const loading = ref(false) const loading = ref(false)
const error = ref(null) const error = ref(null)
const newName = ref('') const newName = ref('')
const reorderMode = ref(false)
function toggleReorder() {
reorderMode.value = !reorderMode.value
}
async function load() { async function load() {
loading.value = true loading.value = true
@@ -88,17 +93,29 @@ async function removeTag(tag) {
} }
} }
onMounted(async () => { watch(reorderMode, async (on) => {
await load() if (on) {
await nextTick() await nextTick()
initSortable() initSortable()
} else {
if (sortable) { sortable.destroy(); sortable = null }
}
}) })
onMounted(load)
onBeforeUnmount(() => sortable?.destroy()) onBeforeUnmount(() => sortable?.destroy())
</script> </script>
<template> <template>
<section class="tag-admin"> <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"> <form class="new-tag" @submit.prevent="addTag">
<input v-model="newName" type="text" placeholder="새 태그 이름 (예: 식비, 고정지출)" /> <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"> <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"> <li v-for="t in tags" :key="t.id" :data-id="t.id" class="tag-row">
<span class="drag-handle" title="드래그하여 순서 변경"></span> <span v-if="reorderMode" class="drag-handle" title="드래그하여 순서 변경"></span>
<input v-model="t.name" class="tag-name" @keyup.enter="saveTag(t)" /> <input v-model="t.name" class="tag-name" @keyup.enter="saveTag(t)" :disabled="reorderMode" />
<IconBtn icon="check" title="저장" @click="saveTag(t)" /> <IconBtn v-if="!reorderMode" icon="check" title="저장" @click="saveTag(t)" />
<IconBtn icon="trash" title="삭제" variant="danger" @click="removeTag(t)" /> <IconBtn v-if="!reorderMode" icon="trash" title="삭제" variant="danger" @click="removeTag(t)" />
</li> </li>
</ul> </ul>
<p v-if="!loading && !tags.length" class="msg">등록된 태그가 없습니다.</p> <p v-if="!loading && !tags.length" class="msg">등록된 태그가 없습니다.</p>
@@ -133,11 +150,37 @@ onBeforeUnmount(() => sortable?.destroy())
h1 { h1 {
font-size: 1.5rem; font-size: 1.5rem;
} }
.top-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.hint { .hint {
font-size: 0.85rem; font-size: 0.85rem;
opacity: 0.7; opacity: 0.7;
margin: 0.5rem 0 1rem; 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 { input {
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
+1 -13
View File
@@ -48,7 +48,6 @@ const form = reactive({
const submitting = ref(false) const submitting = ref(false)
const formError = ref(null) const formError = ref(null)
const DOW = ['', '월', '화', '수', '목', '금', '토', '일']
const TYPES = [ const TYPES = [
{ value: 'EXPENSE', label: '지출', cls: 'chip-expense' }, { value: 'EXPENSE', label: '지출', cls: 'chip-expense' },
@@ -56,8 +55,6 @@ const TYPES = [
{ value: 'TRANSFER', label: '이체', cls: 'chip-transfer' }, { value: 'TRANSFER', label: '이체', cls: 'chip-transfer' },
] ]
const FREQS = [ const FREQS = [
{ value: 'DAILY', label: '매일' },
{ value: 'WEEKLY', label: '매주' },
{ value: 'MONTHLY', label: '매월' }, { value: 'MONTHLY', label: '매월' },
{ value: 'YEARLY', label: '매년' }, { value: 'YEARLY', label: '매년' },
] ]
@@ -102,21 +99,17 @@ function typeLabel(t) {
return t === 'INCOME' ? '수입' : t === 'TRANSFER' ? '이체' : '지출' return t === 'INCOME' ? '수입' : t === 'TRANSFER' ? '이체' : '지출'
} }
function freqLabel(r) { function freqLabel(r) {
if (r.frequency === 'DAILY') return '매일'
if (r.frequency === 'WEEKLY') return `매주 ${DOW[r.dayOfWeek] || ''}요일`
if (r.frequency === 'MONTHLY') return `매월 ${r.dayOfMonth}` if (r.frequency === 'MONTHLY') return `매월 ${r.dayOfMonth}`
return `매년 ${r.monthOfYear}/${r.dayOfMonth}` return `매년 ${r.monthOfYear}/${r.dayOfMonth}`
} }
function payKey(r) { 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) if (r.frequency === 'YEARLY') return (r.monthOfYear || 0) * 100 + (r.dayOfMonth || 0)
return r.dayOfMonth || 0 return r.dayOfMonth || 0
} }
const PERIODS = [ const PERIODS = [
{ key: 'MONTH', label: '월간 거래', freqs: ['DAILY', 'WEEKLY', 'MONTHLY'] }, { key: 'MONTH', label: '월간 거래', freqs: ['MONTHLY'] },
{ key: 'YEAR', label: '년간 거래', freqs: ['YEARLY'] }, { key: 'YEAR', label: '년간 거래', freqs: ['YEARLY'] },
] ]
const grouped = computed(() => const grouped = computed(() =>
@@ -391,11 +384,6 @@ onMounted(load)
</div> </div>
</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'"> <label v-if="form.frequency === 'YEARLY'">
<input v-model.number="form.monthOfYear" type="number" min="1" max="12" :disabled="submitting" /> <input v-model.number="form.monthOfYear" type="number" min="1" max="12" :disabled="submitting" />
</label> </label>