관리자 미디어 카드 썸네일 탭 분리
This commit is contained in:
@@ -3,6 +3,11 @@ import { basename, dirname, extname, join, relative } from 'node:path'
|
||||
import { createError } from 'h3'
|
||||
import { getSiteSettings, listAdminPosts, listPages } from '../repositories/content-repository'
|
||||
import { getPostgresClient } from '../repositories/postgres-client'
|
||||
import {
|
||||
getPostThumbnailUrl,
|
||||
isPostCardThumbnailUrl,
|
||||
isPostThumbnailSource
|
||||
} from './post-thumbnail-image.js'
|
||||
|
||||
const uploadRoot = join(process.cwd(), 'public', 'uploads')
|
||||
const mediaFilePattern = /\.(jpe?g|png|webp|gif|mp4|webm|mov|mp3|wav|ogg|m4a|pdf|zip|txt|csv|docx|xlsx|pptx)$/i
|
||||
@@ -79,8 +84,16 @@ const excludeThumbnailFolderPaths = (paths) => paths.filter((pathValue) => pathV
|
||||
const assertCategoryMoveAllowed = (urls, normalizedCategory) => {
|
||||
const uniqueUrls = [...new Set(urls.filter(Boolean))]
|
||||
const hasAvatar = uniqueUrls.some((u) => isMemberAvatarPublicUrl(u))
|
||||
const hasPostCardThumbnail = uniqueUrls.some((u) => isPostCardThumbnailUrl(u))
|
||||
const hasNonAvatar = uniqueUrls.some((u) => !isMemberAvatarPublicUrl(u))
|
||||
|
||||
if (hasPostCardThumbnail) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '게시물 카드 썸네일은 원본 이미지에 연결된 자동 생성 파일이라 폴더를 변경할 수 없습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasNonAvatar && normalizedCategory === MEDIA_THUMBNAIL_ROOT) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
@@ -489,6 +502,98 @@ const getMediaUsage = (url, posts, pages) => {
|
||||
return [...postUsages, ...pageUsages]
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 URL이 실제 사용처에서 쓰이는지 조회한다.
|
||||
* @param {string} url - 미디어 URL
|
||||
* @param {Array<Object>} posts - 게시물 목록
|
||||
* @param {Array<Object>} pages - 페이지 목록
|
||||
* @param {Object} siteSettings - 사이트 설정
|
||||
* @returns {Array<Object>} 사용처 목록
|
||||
*/
|
||||
const getDirectMediaUsage = (url, posts, pages, siteSettings) => [
|
||||
...getMediaUsage(url, posts, pages),
|
||||
...getSiteSettingsMediaUsage(url, siteSettings)
|
||||
]
|
||||
|
||||
/**
|
||||
* 카드 썸네일 URL별 원본 URL 맵을 만든다.
|
||||
* @param {Array<Object>} items - 미디어 항목 목록
|
||||
* @returns {Map<string, string>} 썸네일 URL별 원본 URL
|
||||
*/
|
||||
const buildPostThumbnailOwnerMap = (items) => {
|
||||
const itemUrlSet = new Set(items.map((item) => item.url))
|
||||
const ownerByThumbnailUrl = new Map()
|
||||
|
||||
for (const item of items) {
|
||||
if (isPostCardThumbnailUrl(item.url) || !isPostThumbnailSource('', item.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const thumbnailUrl = getPostThumbnailUrl(item.url)
|
||||
|
||||
if (thumbnailUrl && itemUrlSet.has(thumbnailUrl)) {
|
||||
ownerByThumbnailUrl.set(thumbnailUrl, item.url)
|
||||
}
|
||||
}
|
||||
|
||||
return ownerByThumbnailUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 카드 썸네일의 간접 사용처를 조회한다.
|
||||
* @param {string} url - 카드 썸네일 URL
|
||||
* @param {Map<string, string>} ownerByThumbnailUrl - 썸네일 URL별 원본 URL
|
||||
* @param {Array<Object>} posts - 게시물 목록
|
||||
* @param {Array<Object>} pages - 페이지 목록
|
||||
* @param {Object} siteSettings - 사이트 설정
|
||||
* @returns {Array<Object>} 간접 사용처 목록
|
||||
*/
|
||||
const getPostThumbnailMediaUsage = (url, ownerByThumbnailUrl, posts, pages, siteSettings) => {
|
||||
const originalUrl = ownerByThumbnailUrl.get(url)
|
||||
|
||||
if (!originalUrl) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getDirectMediaUsage(originalUrl, posts, pages, siteSettings)
|
||||
.filter((usage) => usage.location === 'featuredImage')
|
||||
.map((usage) => ({
|
||||
...usage,
|
||||
location: 'featuredImageThumbnail',
|
||||
label: '목록 카드 썸네일',
|
||||
sourceUrl: originalUrl
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 업로드 이미지의 카드 썸네일 상태를 조회한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @param {Set<string>} itemUrlSet - 전체 미디어 URL 집합
|
||||
* @param {Array<Object>} directUsage - 원본 URL 직접 사용처
|
||||
* @returns {Object|null} 카드 썸네일 상태
|
||||
*/
|
||||
const getPostThumbnailStatus = (item, itemUrlSet, directUsage) => {
|
||||
if (isPostCardThumbnailUrl(item.url) || !isPostThumbnailSource('', item.name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const thumbnailUrl = getPostThumbnailUrl(item.url)
|
||||
|
||||
if (!thumbnailUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasThumbnail = itemUrlSet.has(thumbnailUrl)
|
||||
const isFeaturedImage = directUsage.some((usage) => usage.location === 'featuredImage')
|
||||
|
||||
return {
|
||||
role: 'original',
|
||||
thumbnailUrl,
|
||||
hasThumbnail,
|
||||
isFallbackActive: isFeaturedImage && !hasThumbnail
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이트 설정에서 미디어 URL 사용처 조회
|
||||
* @param {string} url - 미디어 URL
|
||||
@@ -570,18 +675,29 @@ export const listMediaItems = async () => {
|
||||
getSiteSettings()
|
||||
])
|
||||
const avatarOwnerByUrl = await getAvatarOwnersByUrls(items.map((item) => item.url))
|
||||
const itemUrlSet = new Set(items.map((item) => item.url))
|
||||
const thumbnailOwnerByUrl = buildPostThumbnailOwnerMap(items)
|
||||
const itemsWithUsage = items.map((item) => {
|
||||
const rawCategory = metadataMap[item.url]?.category ?? item.category
|
||||
const category = normalizeStoredDisplayCategory(item.url, rawCategory)
|
||||
const avatarOwner = avatarOwnerByUrl.get(item.url) || null
|
||||
const directUsage = getDirectMediaUsage(item.url, posts, pages, siteSettings)
|
||||
const thumbnailUsage = getPostThumbnailMediaUsage(item.url, thumbnailOwnerByUrl, posts, pages, siteSettings)
|
||||
const originalUrl = thumbnailOwnerByUrl.get(item.url) || ''
|
||||
|
||||
return {
|
||||
...item,
|
||||
category,
|
||||
usage: [
|
||||
...getMediaUsage(item.url, posts, pages),
|
||||
...getSiteSettingsMediaUsage(item.url, siteSettings)
|
||||
...directUsage,
|
||||
...thumbnailUsage
|
||||
],
|
||||
thumbnailStatus: originalUrl
|
||||
? {
|
||||
role: 'card',
|
||||
originalUrl
|
||||
}
|
||||
: getPostThumbnailStatus(item, itemUrlSet, directUsage),
|
||||
avatarOwner
|
||||
}
|
||||
})
|
||||
@@ -704,8 +820,8 @@ export const deleteMediaItem = async (url) => {
|
||||
getSiteSettings()
|
||||
])
|
||||
const usage = [
|
||||
...getMediaUsage(url, posts, pages),
|
||||
...getSiteSettingsMediaUsage(url, siteSettings)
|
||||
...getDirectMediaUsage(url, posts, pages, siteSettings),
|
||||
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(await readMediaDirectory(uploadRoot)), posts, pages, siteSettings)
|
||||
]
|
||||
|
||||
if (usage.length) {
|
||||
@@ -733,14 +849,21 @@ export const deleteMediaItem = async (url) => {
|
||||
* @returns {Promise<Object>} 변경된 미디어 항목
|
||||
*/
|
||||
export const renameMediaItem = async (url, name) => {
|
||||
if (isPostCardThumbnailUrl(url)) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
message: '게시물 카드 썸네일은 원본 이미지와의 연결을 유지하기 위해 파일명을 변경할 수 없습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
const [posts, pages, siteSettings] = await Promise.all([
|
||||
listAdminPosts(),
|
||||
listPages(),
|
||||
getSiteSettings()
|
||||
])
|
||||
const usage = [
|
||||
...getMediaUsage(url, posts, pages),
|
||||
...getSiteSettingsMediaUsage(url, siteSettings)
|
||||
...getDirectMediaUsage(url, posts, pages, siteSettings),
|
||||
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(await readMediaDirectory(uploadRoot)), posts, pages, siteSettings)
|
||||
]
|
||||
|
||||
if (usage.length) {
|
||||
|
||||
@@ -5,7 +5,8 @@ export const POST_THUMBNAIL_WIDTH = 640
|
||||
export const POST_THUMBNAIL_HEIGHT = 360
|
||||
export const POST_THUMBNAIL_QUALITY = 82
|
||||
|
||||
const postUploadUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/[^?#]+$/i
|
||||
const postUploadUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/[^/?#]+$/i
|
||||
const postThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-card\.webp$/i
|
||||
const postThumbnailDirectoryName = 'thumbs'
|
||||
const postThumbnailSuffix = '-card'
|
||||
const postThumbnailExtension = '.webp'
|
||||
@@ -72,6 +73,13 @@ export const getPostThumbnailUrl = (imageUrl) => {
|
||||
return `${directory}/${postThumbnailDirectoryName}/${getPostThumbnailFileName(fileName)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 카드 썸네일 URL인지 확인한다.
|
||||
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
|
||||
* @returns {boolean} 카드 썸네일 여부
|
||||
*/
|
||||
export const isPostCardThumbnailUrl = (imageUrl) => Boolean(imageUrl && postThumbnailUrlPattern.test(imageUrl))
|
||||
|
||||
/**
|
||||
* 게시물 카드 썸네일 URL의 디스크 경로를 조회한다.
|
||||
* @param {string} imageUrl - 원본 이미지 URL
|
||||
|
||||
Reference in New Issue
Block a user