Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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">
|
||||||
|
<div class="top-row">
|
||||||
<p class="hint">내가 등록한 태그는 나의 가계부 내역에서만 사용됩니다.</p>
|
<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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user