관리자 미디어 API 페이지 응답 적용

This commit is contained in:
2026-07-02 15:36:55 +09:00
parent 00cb4a293d
commit 0576069427
9 changed files with 365 additions and 101 deletions

View File

@@ -80,10 +80,82 @@ const { openMenuId, closeMenu } = useAdminRowMenu()
*/
const getFolderMenuId = (folder) => `folder:${folder}`
const { data: mediaItems, refresh } = await useFetch('/admin/api/media', {
default: () => []
const mediaQuery = computed(() => ({
paged: '1',
tab: activeTab.value,
page: mediaPage.value,
pageSize: mediaPageSize.value,
search: searchText.value.trim() || undefined,
folder: activeFolder.value || undefined,
kind: activeMediaKind.value,
unused: showUnusedOnly.value ? '1' : undefined
}))
const { data: mediaResponse, refresh } = await useFetch('/admin/api/media', {
query: mediaQuery,
default: () => ({
items: [],
pagination: {
page: 1,
pageSize: 50,
total: 0,
pageCount: 1
},
counts: {
tabs: {
library: 0,
postThumbnails: 0,
postBodyThumbnails: 0,
thumbnails: 0
},
folders: {},
kinds: {
all: 0,
image: 0,
video: 0,
audio: 0,
file: 0
},
usage: {
all: 0,
unused: 0
}
}
})
})
const mediaItems = computed(() => Array.isArray(mediaResponse.value)
? mediaResponse.value
: (mediaResponse.value?.items || []))
const mediaPagination = computed(() => Array.isArray(mediaResponse.value)
? {
page: 1,
pageSize: mediaPageSize.value,
total: mediaItems.value.length,
pageCount: Math.max(1, Math.ceil(mediaItems.value.length / mediaPageSize.value))
}
: (mediaResponse.value?.pagination || {
page: 1,
pageSize: mediaPageSize.value,
total: 0,
pageCount: 1
}))
const mediaCounts = computed(() => Array.isArray(mediaResponse.value)
? {
tabs: {},
folders: {},
kinds: {},
usage: {}
}
: (mediaResponse.value?.counts || {
tabs: {},
folders: {},
kinds: {},
usage: {}
}))
/**
* 썸네일 디스크 경로 여부
* @param {Object} item - 미디어 항목
@@ -338,42 +410,18 @@ const canRegeneratePostBodyThumbnail = (item) => (
*/
const getMediaItemKind = (item) => item?.kind || 'image'
const libraryMediaItems = computed(() => (mediaItems.value || []).filter((item) => !isThumbnailDiskItem(item) && !isPostCardThumbnailDiskItem(item) && !isPageCoverThumbnailDiskItem(item) && !isPostBodyThumbnailDiskItem(item)))
const postThumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isPostCardThumbnailDiskItem(item) || isPageCoverThumbnailDiskItem(item)))
const postBodyThumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isPostBodyThumbnailDiskItem(item)))
const thumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isThumbnailDiskItem(item)))
const scopeItems = computed(() => {
if (activeTab.value === 'thumbnails') {
return thumbnailMediaItems.value
}
if (activeTab.value === 'postThumbnails') {
return postThumbnailMediaItems.value
}
if (activeTab.value === 'postBodyThumbnails') {
return postBodyThumbnailMediaItems.value
}
return libraryMediaItems.value
})
const systemManagedTab = computed(() => ['thumbnails', 'postThumbnails', 'postBodyThumbnails'].includes(activeTab.value))
const activeSystemMediaCount = computed(() => {
if (activeTab.value === 'postThumbnails') {
return postThumbnailMediaItems.value.length
return mediaCounts.value.tabs?.postThumbnails || 0
}
if (activeTab.value === 'postBodyThumbnails') {
return postBodyThumbnailMediaItems.value.length
return mediaCounts.value.tabs?.postBodyThumbnails || 0
}
return thumbnailMediaItems.value.length
return mediaCounts.value.tabs?.thumbnails || 0
})
const activeSystemMediaTitle = computed(() => {
@@ -401,21 +449,10 @@ const activeSystemMediaHint = computed(() => {
})
const mediaKindFilterOptions = computed(() => {
const baseItems = scopeItems.value.filter((item) => {
const folder = activeFolder.value
return systemManagedTab.value
? 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
}, {})
const countByKind = mediaCounts.value.kinds || {}
return [
{ id: 'all', label: '전체', count: baseItems.length },
{ id: 'all', label: '전체', count: countByKind.all || 0 },
{ id: 'image', label: '이미지', count: countByKind.image || 0 },
{ id: 'video', label: '영상', count: countByKind.video || 0 },
{ id: 'audio', label: '음악', count: countByKind.audio || 0 },
@@ -424,20 +461,11 @@ const mediaKindFilterOptions = computed(() => {
})
const mediaUsageFilterOptions = computed(() => {
const folder = activeFolder.value
const mediaKind = activeMediaKind.value
const baseItems = scopeItems.value.filter((item) => {
const matchesFolder = systemManagedTab.value
? true
: (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))
const matchesKind = mediaKind === 'all' || getMediaItemKind(item) === mediaKind
return matchesFolder && matchesKind
})
const usageCounts = mediaCounts.value.usage || {}
return [
{ id: 'all', label: '전체', count: baseItems.length, unusedOnly: false },
{ id: 'unused', label: '미사용', count: baseItems.filter((item) => !isMediaItemLocked(item)).length, unusedOnly: true }
{ id: 'all', label: '전체', count: usageCounts.all || 0, unusedOnly: false },
{ id: 'unused', label: '미사용', count: usageCounts.unused || 0, unusedOnly: true }
]
})
@@ -542,6 +570,7 @@ const selectedMedia = computed(() => mediaItems.value.find((item) => item.url ==
const normalizedFolders = computed(() => {
const folderSet = new Set(['미분류'])
const folderCounts = mediaCounts.value.folders || {}
mediaFolders.value.forEach((folder) => {
String(folder || '').split('/').filter(Boolean).reduce((parentPath, segment) => {
@@ -551,8 +580,8 @@ const normalizedFolders = computed(() => {
}, '')
})
libraryMediaItems.value.forEach((item) => {
String(item.category || '미분류').split('/').filter(Boolean).reduce((parentPath, segment) => {
Object.keys(folderCounts).forEach((folder) => {
String(folder || '미분류').split('/').filter(Boolean).reduce((parentPath, segment) => {
const nextPath = parentPath ? `${parentPath}/${segment}` : segment
folderSet.add(nextPath)
return nextPath
@@ -563,46 +592,27 @@ const normalizedFolders = computed(() => {
})
const folderMediaCounts = computed(() => normalizedFolders.value.reduce((counts, folder) => {
counts[folder] = libraryMediaItems.value.filter((item) => item.category === folder || item.category?.startsWith(`${folder}/`)).length
counts[folder] = mediaCounts.value.folders?.[folder] || 0
return 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) => {
const matchesFolder = systemManagedTab.value
? true
: (!folder || item.category === folder || item.category?.startsWith(`${folder}/`))
const usageTitles = item.usage?.map((usage) => usage.title) || []
const matchesQuery = !query || [
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 && matchesKind && matchesUsage
})
})
const filteredMediaItems = computed(() => mediaItems.value)
const filteredMediaUrls = computed(() => filteredMediaItems.value.map((item) => item.url))
const mediaPageCount = computed(() => Math.max(1, Math.ceil(filteredMediaItems.value.length / mediaPageSize.value)))
const mediaPageCount = computed(() => Math.max(1, Number(mediaPagination.value.pageCount) || 1))
const mediaPageStartIndex = computed(() => filteredMediaItems.value.length
? (mediaPage.value - 1) * mediaPageSize.value
const mediaPageTotal = computed(() => Number(mediaPagination.value.total) || 0)
const mediaPageStartIndex = computed(() => mediaPageTotal.value
? ((Number(mediaPagination.value.page) || mediaPage.value) - 1) * mediaPageSize.value
: 0)
const mediaPageDisplayStart = computed(() => filteredMediaItems.value.length ? mediaPageStartIndex.value + 1 : 0)
const mediaPageDisplayStart = computed(() => mediaPageTotal.value ? mediaPageStartIndex.value + 1 : 0)
const mediaPageEndIndex = computed(() => Math.min(mediaPageStartIndex.value + mediaPageSize.value, filteredMediaItems.value.length))
const mediaPageEndIndex = computed(() => Math.min(mediaPageStartIndex.value + filteredMediaItems.value.length, mediaPageTotal.value))
const paginatedMediaItems = computed(() => filteredMediaItems.value.slice(mediaPageStartIndex.value, mediaPageEndIndex.value))
const paginatedMediaItems = computed(() => filteredMediaItems.value)
const filteredSelectableMediaItems = computed(() => paginatedMediaItems.value.filter((item) => !isMediaItemLocked(item)))
@@ -660,7 +670,8 @@ const mediaPaginationItems = computed(() => {
*/
const setMediaPage = (page) => {
mediaPage.value = Math.min(Math.max(Number(page) || 1, 1), mediaPageCount.value)
lastSelectedIndex.value = -1
clearMediaSelection()
closeMediaDetail()
}
/**
@@ -671,7 +682,8 @@ const setMediaPage = (page) => {
const setMediaPageSize = (event) => {
mediaPageSize.value = Number(event.target.value) || 50
mediaPage.value = 1
lastSelectedIndex.value = -1
clearMediaSelection()
closeMediaDetail()
}
const selectedDeletableMediaItems = computed(() => {
@@ -1300,13 +1312,23 @@ watch(filteredMediaUrls, (urls) => {
watch([activeTab, activeFolder, activeMediaKind, showUnusedOnly, searchText], () => {
mediaPage.value = 1
lastSelectedIndex.value = -1
clearMediaSelection()
closeMediaDetail()
})
watch(mediaPageCount, (pageCount) => {
if (mediaPage.value > pageCount) {
mediaPage.value = pageCount
lastSelectedIndex.value = -1
clearMediaSelection()
closeMediaDetail()
}
})
watch(() => mediaPagination.value.page, (page) => {
const normalizedPage = Number(page) || 1
if (normalizedPage !== mediaPage.value) {
mediaPage.value = normalizedPage
}
})
</script>
@@ -1432,7 +1454,7 @@ watch(mediaPageCount, (pageCount) => {
@drop.prevent="dropMediaOnFolder('미분류')"
>
<span>전체 미디어</span>
<span>{{ libraryMediaItems.length }}</span>
<span>{{ mediaCounts.tabs?.library || 0 }}</span>
</button>
<div class="admin-media__folder-list mt-3 grid gap-1">
@@ -1511,7 +1533,7 @@ watch(mediaPageCount, (pageCount) => {
{{ systemManagedTab ? activeSystemMediaTitle : (activeFolder || '전체 미디어') }}
</h2>
<p class="admin-media__folder-summary mt-1 text-xs text-muted">
{{ filteredMediaItems.length }}개 중 {{ mediaPageDisplayStart }}-{{ mediaPageEndIndex }}개 표시
{{ mediaPageTotal }}개 중 {{ mediaPageDisplayStart }}-{{ mediaPageEndIndex }}개 표시
</p>
</div>
<div class="admin-media__selection-actions flex flex-wrap items-center justify-end gap-2">