v1.4.3: 관리자 UI·홈·미디어 개선
- 관리자 라이트 테마 격리, 대시보드 활성 링크, 로그인 우측 정렬 - 대시보드 통계 추이 차트·툴팁, 홈 Latest/Featured 보정 - 미디어 종류·미사용 필터, 비디오 프레임 썸네일 - NAS 운영 업데이트 절차 문서 추가
This commit is contained in:
@@ -16,6 +16,8 @@ const isThumbnailFolderPath = (folder) => folder === MEDIA_THUMBNAIL_ROOT || Str
|
||||
const activeTab = ref('library')
|
||||
const searchText = ref('')
|
||||
const activeFolder = ref('')
|
||||
const activeMediaKind = ref('all')
|
||||
const showUnusedOnly = ref(false)
|
||||
const isCreateFolderModalOpen = ref(false)
|
||||
const createFolderModalName = ref('')
|
||||
const deletingFolder = ref('')
|
||||
@@ -70,6 +72,38 @@ const thumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item
|
||||
|
||||
const scopeItems = computed(() => (activeTab.value === 'thumbnails' ? thumbnailMediaItems.value : libraryMediaItems.value))
|
||||
|
||||
const mediaKindFilterOptions = computed(() => {
|
||||
const baseItems = scopeItems.value.filter((item) => {
|
||||
const folder = activeFolder.value
|
||||
|
||||
return activeTab.value === 'thumbnails'
|
||||
? true
|
||||
: (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))
|
||||
})
|
||||
const countByKind = baseItems.reduce((counts, item) => {
|
||||
const kind = getMediaItemKind(item)
|
||||
counts[kind] = (counts[kind] || 0) + 1
|
||||
return counts
|
||||
}, {})
|
||||
|
||||
return [
|
||||
{ id: 'all', label: '전체', count: baseItems.length },
|
||||
{ id: 'image', label: '이미지', count: countByKind.image || 0 },
|
||||
{ id: 'video', label: '영상', count: countByKind.video || 0 },
|
||||
{ id: 'audio', label: '음악', count: countByKind.audio || 0 },
|
||||
{ id: 'file', label: '파일', count: countByKind.file || 0 }
|
||||
]
|
||||
})
|
||||
|
||||
const unusedMediaCount = computed(() => scopeItems.value.filter((item) => {
|
||||
const folder = activeFolder.value
|
||||
const matchesFolder = activeTab.value === 'thumbnails'
|
||||
? true
|
||||
: (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))
|
||||
|
||||
return matchesFolder && !isMediaItemLocked(item)
|
||||
}).length)
|
||||
|
||||
/**
|
||||
* 상단 탭 전환 시 목록 상태를 초기화한다.
|
||||
* @param {'library' | 'thumbnails'} tab - 선택 탭
|
||||
@@ -83,10 +117,31 @@ const setActiveTab = (tab) => {
|
||||
activeTab.value = tab
|
||||
activeFolder.value = ''
|
||||
searchText.value = ''
|
||||
activeMediaKind.value = 'all'
|
||||
showUnusedOnly.value = false
|
||||
clearMediaSelection()
|
||||
closeMediaDetail()
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 종류 필터를 선택한다.
|
||||
* @param {'all'|'image'|'video'|'audio'|'file'} kind - 선택할 미디어 종류
|
||||
* @returns {void}
|
||||
*/
|
||||
const setMediaKindFilter = (kind) => {
|
||||
activeMediaKind.value = kind
|
||||
clearMediaSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* 미사용 미디어만 보기 필터를 토글한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const toggleUnusedMediaFilter = () => {
|
||||
showUnusedOnly.value = !showUnusedOnly.value
|
||||
clearMediaSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 시각을 짧은 로캘 문자열로 표시한다.
|
||||
* @param {string | null} iso - ISO 시각
|
||||
@@ -164,6 +219,8 @@ const folderMediaCounts = computed(() => normalizedFolders.value.reduce((counts,
|
||||
const filteredMediaItems = computed(() => {
|
||||
const query = searchText.value.trim().toLowerCase()
|
||||
const folder = activeFolder.value
|
||||
const mediaKind = activeMediaKind.value
|
||||
const unusedOnly = showUnusedOnly.value
|
||||
const base = scopeItems.value
|
||||
|
||||
return base.filter((item) => {
|
||||
@@ -175,8 +232,10 @@ const filteredMediaItems = computed(() => {
|
||||
item.name,
|
||||
...usageTitles
|
||||
].some((value) => String(value || '').toLowerCase().includes(query))
|
||||
const matchesKind = mediaKind === 'all' || getMediaItemKind(item) === mediaKind
|
||||
const matchesUsage = !unusedOnly || !isMediaItemLocked(item)
|
||||
|
||||
return matchesFolder && matchesQuery
|
||||
return matchesFolder && matchesQuery && matchesKind && matchesUsage
|
||||
})
|
||||
})
|
||||
|
||||
@@ -225,6 +284,8 @@ const isMediaSelected = (item) => selectedMediaUrls.value.includes(item.url)
|
||||
*/
|
||||
const selectFolder = (folder) => {
|
||||
activeFolder.value = folder
|
||||
activeMediaKind.value = 'all'
|
||||
showUnusedOnly.value = false
|
||||
selectedMediaUrls.value = []
|
||||
lastSelectedIndex.value = -1
|
||||
}
|
||||
@@ -583,6 +644,32 @@ const deleteMedia = async (item) => {
|
||||
:placeholder="activeTab === 'thumbnails' ? '파일명, 게시물 제목(사용처) 검색' : '파일명, 게시물 제목(사용처) 검색'"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeTab === 'library'"
|
||||
class="admin-media__filters flex flex-wrap gap-1.5"
|
||||
>
|
||||
<button
|
||||
v-for="option in mediaKindFilterOptions"
|
||||
:key="option.id"
|
||||
class="admin-media__kind-filter rounded-full border px-3 py-1.5 text-xs font-semibold transition"
|
||||
:class="activeMediaKind === option.id ? 'border-[#15171a] bg-[#15171a] text-white' : 'border-line bg-white text-muted hover:text-ink'"
|
||||
type="button"
|
||||
@click="setMediaKindFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
<span class="ml-1 opacity-70">{{ option.count }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="admin-media__unused-filter rounded-full border px-3 py-1.5 text-xs font-semibold transition"
|
||||
:class="showUnusedOnly ? 'border-[#15171a] bg-[#15171a] text-white' : 'border-line bg-white text-muted hover:text-ink'"
|
||||
type="button"
|
||||
:aria-pressed="showUnusedOnly"
|
||||
@click="toggleUnusedMediaFilter"
|
||||
>
|
||||
미사용
|
||||
<span class="ml-1 opacity-70">{{ unusedMediaCount }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-media__layout mt-8 grid gap-5 lg:grid-cols-[16rem_minmax(0,1fr)]">
|
||||
@@ -733,6 +820,11 @@ const deleteMedia = async (item) => {
|
||||
:src="item.url"
|
||||
:alt="item.title"
|
||||
>
|
||||
<AdminMediaVideoThumbnail
|
||||
v-else-if="getMediaItemKind(item) === 'video'"
|
||||
:src="item.url"
|
||||
:alt="item.title"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="admin-media__image flex aspect-square w-full items-center justify-center bg-surface text-xs font-bold uppercase tracking-[0.18em] text-muted"
|
||||
|
||||
Reference in New Issue
Block a user