일반 태그 배지 목록 정리
This commit is contained in:
@@ -12,14 +12,47 @@ const deletingGeneralTagId = ref('')
|
||||
const toast = ref(null)
|
||||
let toastTimer = null
|
||||
const generalTagQuery = ref('')
|
||||
const generalTagSearchResults = ref([])
|
||||
const generalTagSearchLoading = ref(false)
|
||||
const generalTagSortMode = ref('recent')
|
||||
|
||||
const { data: tags, refresh } = await useFetch('/admin/api/tags', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const managedTags = computed(() => tags.value.filter((tag) => tag.tagType === 'managed'))
|
||||
const generalTags = computed(() => tags.value.filter((tag) => tag.tagType === 'general'))
|
||||
const filteredGeneralTags = computed(() => {
|
||||
const keyword = generalTagQuery.value.trim().toLowerCase()
|
||||
const sortedTags = [...generalTags.value].sort((a, b) => {
|
||||
if (generalTagSortMode.value === 'count') {
|
||||
const countDiff = Number(b.postCount || 0) - Number(a.postCount || 0)
|
||||
if (countDiff !== 0) {
|
||||
return countDiff
|
||||
}
|
||||
}
|
||||
|
||||
if (generalTagSortMode.value === 'name') {
|
||||
return a.name.localeCompare(b.name, 'ko')
|
||||
}
|
||||
|
||||
const aTime = new Date(a.lastUsedAt || a.updatedAt || 0).getTime()
|
||||
const bTime = new Date(b.lastUsedAt || b.updatedAt || 0).getTime()
|
||||
|
||||
if (aTime !== bTime) {
|
||||
return bTime - aTime
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name, 'ko')
|
||||
})
|
||||
|
||||
if (!keyword) {
|
||||
return sortedTags
|
||||
}
|
||||
|
||||
return sortedTags.filter((tag) =>
|
||||
tag.name.toLowerCase().includes(keyword) ||
|
||||
tag.slug.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
/** 서버 기준 메인 태그 id 순서(정렬 저장 버튼 활성 비교용) */
|
||||
const baselineManagedTagIds = ref([])
|
||||
@@ -184,27 +217,16 @@ const saveManagedOrder = async () => {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const searchGeneralTags = async () => {
|
||||
const keyword = generalTagQuery.value.trim()
|
||||
if (!keyword) {
|
||||
generalTagSearchResults.value = []
|
||||
return
|
||||
}
|
||||
generalTagQuery.value = generalTagQuery.value.trim()
|
||||
}
|
||||
|
||||
generalTagSearchLoading.value = true
|
||||
|
||||
try {
|
||||
generalTagSearchResults.value = await $fetch('/admin/api/tags', {
|
||||
query: {
|
||||
tagType: 'general',
|
||||
q: keyword,
|
||||
limit: 30
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
showToast('error', error?.data?.message || '일반 태그 검색에 실패했습니다.')
|
||||
} finally {
|
||||
generalTagSearchLoading.value = false
|
||||
}
|
||||
/**
|
||||
* 일반 태그 정렬 기준을 변경한다.
|
||||
* @param {'recent'|'count'|'name'} mode - 정렬 기준
|
||||
* @returns {void}
|
||||
*/
|
||||
const setGeneralTagSortMode = (mode) => {
|
||||
generalTagSortMode.value = mode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,7 +254,6 @@ const promoteToMainTag = async (tag) => {
|
||||
}
|
||||
})
|
||||
await refreshTagsFromServer()
|
||||
await searchGeneralTags()
|
||||
showToast('success', `"${tag.name}" 태그를 메인 태그로 전환했습니다.`)
|
||||
} catch (error) {
|
||||
showToast('error', error?.data?.message || '메인 태그 전환에 실패했습니다.')
|
||||
@@ -266,7 +287,6 @@ const demoteToGeneralTag = async (tag) => {
|
||||
}
|
||||
})
|
||||
await refreshTagsFromServer()
|
||||
await searchGeneralTags()
|
||||
showToast('success', `"${tag.name}" 태그를 일반 태그로 변경했습니다.`)
|
||||
} catch (error) {
|
||||
showToast('error', error?.data?.message || '일반 태그 변경에 실패했습니다.')
|
||||
@@ -292,7 +312,6 @@ const deleteGeneralTag = async (tag) => {
|
||||
method: 'DELETE'
|
||||
})
|
||||
await refreshTagsFromServer()
|
||||
await searchGeneralTags()
|
||||
showToast('success', `"${tag.name}" 일반 태그를 삭제했습니다.`)
|
||||
} catch (error) {
|
||||
showToast('error', error?.data?.message || '일반 태그를 삭제하지 못했습니다.')
|
||||
@@ -323,7 +342,7 @@ onBeforeUnmount(() => {
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-muted">
|
||||
메인 태그는 홈페이지 카테고리 영역에서 사용되며 드래그 정렬이 가능합니다. 일반 태그는 검색으로 찾아 메인 태그로 전환할 수 있습니다.
|
||||
메인 태그는 홈페이지 카테고리 영역에서 사용되며 드래그 정렬이 가능합니다. 일반 태그는 아래 목록에서 확인하고 필요할 때 메인 태그로 전환할 수 있습니다.
|
||||
</p>
|
||||
|
||||
<div class="admin-tags__table mt-6 overflow-hidden border border-line">
|
||||
@@ -404,44 +423,65 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div class="admin-tags__table mt-8 overflow-hidden border border-line">
|
||||
<div class="border-b border-line bg-[#f7f7f5] px-4 py-2.5">
|
||||
<p class="text-xs font-semibold uppercase text-muted">일반 태그 검색</p>
|
||||
<p class="text-xs font-semibold uppercase text-muted">일반 태그</p>
|
||||
</div>
|
||||
<div class="space-y-3 bg-white p-4">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<input
|
||||
v-model="generalTagQuery"
|
||||
type="text"
|
||||
class="h-10 min-w-0 flex-1 rounded border border-line px-3 text-sm outline-none focus:border-[#8e9cac]"
|
||||
placeholder="일반 태그 이름 또는 슬러그 검색"
|
||||
placeholder="일반 태그 이름 또는 슬러그 필터"
|
||||
@keydown.enter.prevent="searchGeneralTags"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="h-10 rounded border border-line bg-white px-4 text-sm font-semibold disabled:opacity-50"
|
||||
:disabled="generalTagSearchLoading"
|
||||
@click="searchGeneralTags"
|
||||
>
|
||||
{{ generalTagSearchLoading ? '검색 중' : '검색' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="generalTagSearchResults.length" class="divide-y divide-line rounded border border-line">
|
||||
<div v-for="tag in generalTagSearchResults" :key="tag.id" class="flex items-center gap-3 px-3 py-2.5">
|
||||
<span class="h-4 w-1 rounded-full" :style="{ backgroundColor: tag.color }" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-ink">{{ tag.name }}</p>
|
||||
<p class="truncate text-xs text-muted">{{ tag.slug }}</p>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 rounded border border-line bg-[#f7f7f5] p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-line px-3 py-1.5 text-xs font-semibold disabled:opacity-50"
|
||||
:disabled="promotingTagId === tag.id"
|
||||
@click="promoteToMainTag(tag)"
|
||||
class="rounded px-3 py-1.5 text-xs font-semibold"
|
||||
:class="generalTagSortMode === 'recent' ? 'bg-[#15171a] text-white' : 'text-muted'"
|
||||
@click="setGeneralTagSortMode('recent')"
|
||||
>
|
||||
{{ promotingTagId === tag.id ? '전환 중' : '메인 태그로 전환' }}
|
||||
최근 사용순
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700 disabled:opacity-50"
|
||||
class="rounded px-3 py-1.5 text-xs font-semibold"
|
||||
:class="generalTagSortMode === 'count' ? 'bg-[#15171a] text-white' : 'text-muted'"
|
||||
@click="setGeneralTagSortMode('count')"
|
||||
>
|
||||
많이 사용순
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded px-3 py-1.5 text-xs font-semibold"
|
||||
:class="generalTagSortMode === 'name' ? 'bg-[#15171a] text-white' : 'text-muted'"
|
||||
@click="setGeneralTagSortMode('name')"
|
||||
>
|
||||
이름순
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredGeneralTags.length" class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="tag in filteredGeneralTags"
|
||||
:key="tag.id"
|
||||
class="admin-tags__general-badge group inline-flex max-w-full items-center gap-2 rounded-full border border-line bg-[#f7f7f5] px-3 py-2 text-sm"
|
||||
:title="tag.slug"
|
||||
>
|
||||
<span class="h-3 w-1 rounded-full" :style="{ backgroundColor: tag.color }" />
|
||||
<span class="truncate font-semibold text-ink">{{ tag.name }}</span>
|
||||
<span class="text-xs text-muted">{{ tag.postCount || 0 }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-line bg-white px-2 py-1 text-[11px] font-semibold disabled:opacity-50"
|
||||
:disabled="promotingTagId === tag.id"
|
||||
@click="promoteToMainTag(tag)"
|
||||
>
|
||||
{{ promotingTagId === tag.id ? '전환 중' : '메인' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-red-200 bg-white px-2 py-1 text-[11px] font-semibold text-red-700 disabled:opacity-50"
|
||||
:disabled="deletingGeneralTagId === tag.id"
|
||||
@click="deleteGeneralTag(tag)"
|
||||
>
|
||||
@@ -449,8 +489,8 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="generalTagQuery.trim() && !generalTagSearchLoading" class="text-sm text-muted">
|
||||
검색 결과가 없습니다.
|
||||
<p v-else class="text-sm text-muted">
|
||||
{{ generalTagQuery.trim() ? '일치하는 일반 태그가 없습니다.' : '아직 일반 태그가 없습니다.' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user