관리자 미디어 API 페이지 응답 적용
This commit is contained in:
@@ -1,13 +1,239 @@
|
||||
import { requireAdminSession } from '../../../utils/admin-auth'
|
||||
import { listMediaItems } from '../../../utils/media-library'
|
||||
import {
|
||||
isPageCoverThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
isPostCardThumbnailUrl
|
||||
} from '../../../../lib/post-card-thumbnail-url.js'
|
||||
|
||||
/** 관리자 미디어 페이지 표시 개수 허용값 */
|
||||
const mediaPageSizeOptions = [50, 100, 200]
|
||||
|
||||
/**
|
||||
* 미디어 항목 종류를 반환한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {'image'|'video'|'audio'|'file'} 미디어 종류
|
||||
*/
|
||||
const getMediaItemKind = (item) => item?.kind || 'image'
|
||||
|
||||
/**
|
||||
* 미디어 항목 잠금 여부를 반환한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 잠금 여부
|
||||
*/
|
||||
const isMediaItemLocked = (item) => Boolean(item?.usage?.length) || Boolean(item?.avatarOwner)
|
||||
|
||||
/**
|
||||
* 프로필 이미지 파일인지 확인한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 프로필 이미지이면 true
|
||||
*/
|
||||
const isProfileThumbnailItem = (item) => Boolean(item?.url?.includes('/members/avatars/'))
|
||||
|
||||
/**
|
||||
* 관리자 미디어 탭 id를 정규화한다.
|
||||
* @param {unknown} value - 요청 탭
|
||||
* @returns {'library'|'postThumbnails'|'postBodyThumbnails'|'thumbnails'} 탭 id
|
||||
*/
|
||||
const normalizeMediaTab = (value) => {
|
||||
const tab = String(value || 'library')
|
||||
|
||||
return ['library', 'postThumbnails', 'postBodyThumbnails', 'thumbnails'].includes(tab)
|
||||
? tab
|
||||
: 'library'
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 미디어 종류 필터를 정규화한다.
|
||||
* @param {unknown} value - 요청 미디어 종류
|
||||
* @returns {'all'|'image'|'video'|'audio'|'file'} 미디어 종류
|
||||
*/
|
||||
const normalizeMediaKind = (value) => {
|
||||
const kind = String(value || 'all')
|
||||
|
||||
return ['all', 'image', 'video', 'audio', 'file'].includes(kind)
|
||||
? kind
|
||||
: 'all'
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 미디어 페이지 표시 개수를 정규화한다.
|
||||
* @param {unknown} value - 요청 표시 개수
|
||||
* @returns {number} 표시 개수
|
||||
*/
|
||||
const normalizeMediaPageSize = (value) => {
|
||||
const pageSize = Number(value) || 50
|
||||
|
||||
return mediaPageSizeOptions.includes(pageSize) ? pageSize : 50
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 미디어 탭별 항목인지 확인한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @param {'library'|'postThumbnails'|'postBodyThumbnails'|'thumbnails'} tab - 탭 id
|
||||
* @returns {boolean} 탭에 속하면 true
|
||||
*/
|
||||
const matchesMediaTab = (item, tab) => {
|
||||
const url = item?.url
|
||||
|
||||
if (tab === 'thumbnails') {
|
||||
return isProfileThumbnailItem(item)
|
||||
}
|
||||
|
||||
if (tab === 'postThumbnails') {
|
||||
return isPostCardThumbnailUrl(url) || isPageCoverThumbnailUrl(url)
|
||||
}
|
||||
|
||||
if (tab === 'postBodyThumbnails') {
|
||||
return isPostBodyThumbnailUrl(url)
|
||||
}
|
||||
|
||||
return !isProfileThumbnailItem(item)
|
||||
&& !isPostCardThumbnailUrl(url)
|
||||
&& !isPageCoverThumbnailUrl(url)
|
||||
&& !isPostBodyThumbnailUrl(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 폴더와 하위 폴더에 포함되는지 확인한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @param {string} folder - 폴더 경로
|
||||
* @returns {boolean} 포함 여부
|
||||
*/
|
||||
const matchesMediaFolder = (item, folder) => !folder
|
||||
|| item.category === folder
|
||||
|| item.category?.startsWith(`${folder}/`)
|
||||
|
||||
/**
|
||||
* 검색어와 미디어 항목이 일치하는지 확인한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @param {string} query - 소문자 검색어
|
||||
* @returns {boolean} 일치 여부
|
||||
*/
|
||||
const matchesMediaQuery = (item, query) => {
|
||||
if (!query) {
|
||||
return true
|
||||
}
|
||||
|
||||
const usageTitles = Array.isArray(item?.usage)
|
||||
? item.usage.map((usage) => usage.title)
|
||||
: []
|
||||
|
||||
return [
|
||||
item?.name,
|
||||
item?.title,
|
||||
...usageTitles
|
||||
].some((value) => String(value || '').toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 폴더별 개수를 계산한다.
|
||||
* @param {Array<Object>} items - 미디어 항목 목록
|
||||
* @returns {Record<string, number>} 폴더별 개수
|
||||
*/
|
||||
const countMediaFolders = (items) => items.reduce((counts, item) => {
|
||||
const segments = String(item.category || '미분류').split('/').filter(Boolean)
|
||||
|
||||
if (!segments.length) {
|
||||
counts['미분류'] = (counts['미분류'] || 0) + 1
|
||||
return counts
|
||||
}
|
||||
|
||||
segments.reduce((parentPath, segment) => {
|
||||
const folder = parentPath ? `${parentPath}/${segment}` : segment
|
||||
counts[folder] = (counts[folder] || 0) + 1
|
||||
return folder
|
||||
}, '')
|
||||
|
||||
return counts
|
||||
}, {})
|
||||
|
||||
/**
|
||||
* 미디어 종류별 개수를 계산한다.
|
||||
* @param {Array<Object>} items - 미디어 항목 목록
|
||||
* @returns {Record<string, number>} 종류별 개수
|
||||
*/
|
||||
const countMediaKinds = (items) => items.reduce((counts, item) => {
|
||||
const kind = getMediaItemKind(item)
|
||||
counts[kind] = (counts[kind] || 0) + 1
|
||||
counts.all += 1
|
||||
return counts
|
||||
}, {
|
||||
all: 0,
|
||||
image: 0,
|
||||
video: 0,
|
||||
audio: 0,
|
||||
file: 0
|
||||
})
|
||||
|
||||
/**
|
||||
* 미디어 페이지 응답을 구성한다.
|
||||
* @param {Array<Object>} items - 전체 미디어 목록
|
||||
* @param {Record<string, unknown>} query - 요청 쿼리
|
||||
* @returns {Object} 페이지 미디어 응답
|
||||
*/
|
||||
const createPagedMediaResponse = (items, query) => {
|
||||
const tab = normalizeMediaTab(query.tab)
|
||||
const pageSize = normalizeMediaPageSize(query.pageSize)
|
||||
const requestedPage = Math.max(1, Number(query.page) || 1)
|
||||
const mediaKind = normalizeMediaKind(query.kind)
|
||||
const searchQuery = String(query.search || '').trim().toLowerCase()
|
||||
const folder = tab === 'library' ? String(query.folder || '').trim() : ''
|
||||
const unusedOnly = query.unused === '1' || query.unused === 'true'
|
||||
|
||||
const libraryItems = items.filter((item) => matchesMediaTab(item, 'library'))
|
||||
const tabItems = items.filter((item) => matchesMediaTab(item, tab))
|
||||
const folderItems = tab === 'library'
|
||||
? tabItems.filter((item) => matchesMediaFolder(item, folder))
|
||||
: tabItems
|
||||
const kindItems = folderItems.filter((item) => mediaKind === 'all' || getMediaItemKind(item) === mediaKind)
|
||||
const usageFilteredItems = unusedOnly
|
||||
? kindItems.filter((item) => !isMediaItemLocked(item))
|
||||
: kindItems
|
||||
const filteredItems = usageFilteredItems.filter((item) => matchesMediaQuery(item, searchQuery))
|
||||
const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize))
|
||||
const page = Math.min(requestedPage, pageCount)
|
||||
const startIndex = (page - 1) * pageSize
|
||||
|
||||
return {
|
||||
items: filteredItems.slice(startIndex, startIndex + pageSize),
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total: filteredItems.length,
|
||||
pageCount
|
||||
},
|
||||
counts: {
|
||||
tabs: {
|
||||
library: libraryItems.length,
|
||||
postThumbnails: items.filter((item) => matchesMediaTab(item, 'postThumbnails')).length,
|
||||
postBodyThumbnails: items.filter((item) => matchesMediaTab(item, 'postBodyThumbnails')).length,
|
||||
thumbnails: items.filter((item) => matchesMediaTab(item, 'thumbnails')).length
|
||||
},
|
||||
folders: countMediaFolders(libraryItems),
|
||||
kinds: countMediaKinds(folderItems),
|
||||
usage: {
|
||||
all: kindItems.length,
|
||||
unused: kindItems.filter((item) => !isMediaItemLocked(item)).length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 미디어 목록 API
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
* @returns {Promise<Array<Object>>} 미디어 목록
|
||||
* @returns {Promise<Array<Object>|Object>} 미디어 목록 또는 페이지 응답
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireAdminSession(event)
|
||||
|
||||
return listMediaItems()
|
||||
const items = await listMediaItems()
|
||||
const query = getQuery(event)
|
||||
|
||||
if (query.paged === '1' || query.paged === 'true') {
|
||||
return createPagedMediaResponse(items, query)
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user