최근 태그 추천과 미디어 셀렉트 정리
This commit is contained in:
@@ -95,6 +95,9 @@ const { data: siteSettings } = useFetch('/admin/api/settings', {
|
||||
})
|
||||
|
||||
const defaultTagColor = '#15171a'
|
||||
const recentTagSuggestionLimit = 5
|
||||
const defaultTagSuggestionLimit = 8
|
||||
const emptyTagSuggestionLimit = 13
|
||||
/** @type {number} 한국어 본문 예상 읽기 속도(분당 공백 제외 문자 수) */
|
||||
const READING_CHARS_PER_MINUTE = 600
|
||||
|
||||
@@ -506,25 +509,77 @@ const availableAdminTags = computed(() => Array.isArray(adminTags.value) ? admin
|
||||
|
||||
const managedTagOptions = computed(() => availableAdminTags.value.filter((tag) => tag.tagType === 'managed'))
|
||||
|
||||
/**
|
||||
* 태그의 최근 사용 시각을 timestamp로 변환한다.
|
||||
* @param {Object} tag - 태그 항목
|
||||
* @returns {number} 최근 사용 timestamp
|
||||
*/
|
||||
const getTagLastUsedTime = (tag) => {
|
||||
const timestamp = Date.parse(tag?.lastUsedAt || '')
|
||||
return Number.isFinite(timestamp) ? timestamp : 0
|
||||
}
|
||||
|
||||
const recentTagOptions = computed(() => availableAdminTags.value
|
||||
.filter((tag) => getTagLastUsedTime(tag) > 0)
|
||||
.sort((left, right) => getTagLastUsedTime(right) - getTagLastUsedTime(left))
|
||||
.slice(0, recentTagSuggestionLimit))
|
||||
|
||||
const recentTagSlugSet = computed(() => new Set(recentTagOptions.value.map((tag) => String(tag.slug || '').toLowerCase())))
|
||||
|
||||
/**
|
||||
* 태그 추천 후보를 선택 가능한 항목으로 정리한다.
|
||||
* @param {Array<Object>} sourceTags - 원본 태그 목록
|
||||
* @param {string} keyword - 검색어
|
||||
* @returns {Array<Object>} 선택 가능한 추천 태그
|
||||
*/
|
||||
const filterTagSuggestionSource = (sourceTags, keyword = '') => sourceTags.filter((tag) => {
|
||||
if (!canAddMoreTags.value) {
|
||||
return false
|
||||
}
|
||||
if (!tag?.slug || selectedTagKeys.value.has(tag.slug.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
return tag.name.toLowerCase().includes(keyword) || tag.slug.toLowerCase().includes(keyword)
|
||||
})
|
||||
|
||||
/**
|
||||
* 중복 태그를 제거한다.
|
||||
* @param {Array<Object>} tags - 태그 목록
|
||||
* @returns {Array<Object>} 중복 제거된 태그 목록
|
||||
*/
|
||||
const dedupeTagsBySlug = (tags) => {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
|
||||
for (const tag of tags) {
|
||||
const slugKey = String(tag?.slug || '').toLowerCase()
|
||||
|
||||
if (!slugKey || seen.has(slugKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
seen.add(slugKey)
|
||||
out.push(tag)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const tagSuggestionOptions = computed(() => {
|
||||
const keyword = normalizeTagToken(tagInput.value)
|
||||
const sourceTags = keyword ? availableAdminTags.value : managedTagOptions.value
|
||||
|
||||
return sourceTags
|
||||
.filter((tag) => {
|
||||
if (!canAddMoreTags.value) {
|
||||
return false
|
||||
}
|
||||
if (!tag?.slug || selectedTagKeys.value.has(tag.slug.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
if (keyword) {
|
||||
return filterTagSuggestionSource(availableAdminTags.value, keyword).slice(0, defaultTagSuggestionLimit)
|
||||
}
|
||||
|
||||
return tag.name.toLowerCase().includes(keyword) || tag.slug.toLowerCase().includes(keyword)
|
||||
})
|
||||
.slice(0, 8)
|
||||
return dedupeTagsBySlug([
|
||||
...filterTagSuggestionSource(recentTagOptions.value),
|
||||
...filterTagSuggestionSource(managedTagOptions.value)
|
||||
]).slice(0, emptyTagSuggestionLimit)
|
||||
})
|
||||
|
||||
const hasTagSuggestions = computed(() => isTagSuggestionsOpen.value && tagSuggestionOptions.value.length > 0)
|
||||
@@ -930,7 +985,10 @@ const createTagBadgeStyle = (color) => ({
|
||||
* @returns {string} 보조 라벨
|
||||
*/
|
||||
const getTagSuggestionMeta = (tag) => {
|
||||
const typeLabel = tag.tagType === 'managed' ? '메인' : '일반'
|
||||
const slugKey = String(tag?.slug || '').toLowerCase()
|
||||
const typeLabel = recentTagSlugSet.value.has(slugKey)
|
||||
? '최근'
|
||||
: (tag.tagType === 'managed' ? '메인' : '일반')
|
||||
return tag.name.toLowerCase() === tag.slug.toLowerCase() ? typeLabel : `${typeLabel} · ${tag.slug}`
|
||||
}
|
||||
|
||||
@@ -1986,7 +2044,7 @@ defineExpose({
|
||||
type="button"
|
||||
:aria-expanded="hasTagSuggestions"
|
||||
:disabled="!canAddMoreTags"
|
||||
aria-label="메인 태그 목록 열기"
|
||||
aria-label="태그 추천 목록 열기"
|
||||
@mousedown.prevent="toggleTagSuggestions"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-down size-5" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user