미디어 원본 사용처 집계 개선

This commit is contained in:
2026-07-03 15:08:30 +09:00
parent 243dfbc5e2
commit 55efae810f
8 changed files with 185 additions and 16 deletions

View File

@@ -710,6 +710,87 @@ const getPostBodyThumbnailMediaUsage = (url, ownerByThumbnailUrl, posts, pages,
}))
}
/**
* 원본 URL에서 파생된 URL 목록을 반환한다.
* @param {string} originalUrl - 원본 미디어 URL
* @param {Map<string, string>} ownerByDerivativeUrl - 파생 URL별 원본 URL 맵
* @returns {Array<string>} 파생 URL 목록
*/
const getOwnedDerivativeUrls = (originalUrl, ownerByDerivativeUrl) => [...ownerByDerivativeUrl.entries()]
.filter(([, ownerUrl]) => ownerUrl === originalUrl)
.map(([derivativeUrl]) => derivativeUrl)
/**
* 원본 이미지에 연결된 파생 이미지 사용처를 조회한다.
* @param {string} originalUrl - 원본 미디어 URL
* @param {Map<string, string>} ownerByDerivativeUrl - 파생 URL별 원본 URL 맵
* @param {Array<Object>} posts - 게시물 목록
* @param {Array<Object>} pages - 페이지 목록
* @param {Object} siteSettings - 사이트 설정
* @param {Array<Object>} tags - 태그 목록
* @param {{ location: string, label: string, onlyLocation?: string }} options - 사용처 표시 옵션
* @returns {Array<Object>} 원본에 연결할 파생 이미지 사용처 목록
*/
const getOriginalDerivativeMediaUsage = (
originalUrl,
ownerByDerivativeUrl,
posts,
pages,
siteSettings,
tags,
options
) => getOwnedDerivativeUrls(originalUrl, ownerByDerivativeUrl)
.flatMap((derivativeUrl) => getDirectMediaUsage(derivativeUrl, posts, pages, siteSettings, tags)
.filter((usage) => !options.onlyLocation || usage.location === options.onlyLocation)
.map((usage) => ({
...usage,
location: options.location,
label: options.label,
sourceUrl: originalUrl,
derivativeUrl
})))
/**
* 원본 미디어 URL의 직접·파생 사용처를 함께 조회한다.
* @param {string} url - 미디어 URL
* @param {Array<Object>} posts - 게시물 목록
* @param {Array<Object>} pages - 페이지 목록
* @param {Object} siteSettings - 사이트 설정
* @param {Array<Object>} tags - 태그 목록
* @param {{ thumbnailOwnerByUrl: Map<string, string>, pageCoverThumbnailOwnerByUrl: Map<string, string>, bodyThumbnailOwnerByUrl: Map<string, string> }} ownerMaps - 파생 URL별 원본 URL 맵 묶음
* @returns {Array<Object>} 사용처 목록
*/
const getOriginalMediaUsage = (url, posts, pages, siteSettings, tags, ownerMaps) => [
...getDirectMediaUsage(url, posts, pages, siteSettings, tags),
...getOriginalDerivativeMediaUsage(url, ownerMaps.thumbnailOwnerByUrl, posts, pages, siteSettings, tags, {
location: 'featuredImageThumbnail',
label: '목록 카드 썸네일',
onlyLocation: 'featuredImage'
}),
...getOriginalDerivativeMediaUsage(url, ownerMaps.pageCoverThumbnailOwnerByUrl, posts, pages, siteSettings, tags, {
location: 'pageCoverThumbnail',
label: '페이지 커버 썸네일',
onlyLocation: 'featuredImage'
}),
...getOriginalDerivativeMediaUsage(url, ownerMaps.bodyThumbnailOwnerByUrl, posts, pages, siteSettings, tags, {
location: 'contentBodyImage',
label: '본문 표시 이미지',
onlyLocation: 'content'
})
]
/**
* 원본 삭제 시 함께 제거할 자동 생성 파생 URL 목록을 반환한다.
* @param {string} originalUrl - 원본 미디어 URL
* @param {{ thumbnailOwnerByUrl: Map<string, string>, pageCoverThumbnailOwnerByUrl: Map<string, string>, bodyThumbnailOwnerByUrl: Map<string, string> }} ownerMaps - 파생 URL별 원본 URL 맵 묶음
* @returns {Array<string>} 함께 제거할 파생 URL 목록
*/
const getOwnedGeneratedMediaUrls = (originalUrl, ownerMaps) => [...new Set([
...getOwnedDerivativeUrls(originalUrl, ownerMaps.thumbnailOwnerByUrl),
...getOwnedDerivativeUrls(originalUrl, ownerMaps.pageCoverThumbnailOwnerByUrl),
...getOwnedDerivativeUrls(originalUrl, ownerMaps.bodyThumbnailOwnerByUrl)
])]
/**
* 게시물 업로드 이미지의 카드 썸네일 상태를 조회한다.
* @param {Object} item - 미디어 항목
@@ -885,11 +966,16 @@ export const listMediaItems = async () => {
const thumbnailOwnerByUrl = buildPostThumbnailOwnerMap(items)
const pageCoverThumbnailOwnerByUrl = buildPageCoverThumbnailOwnerMap(items)
const bodyThumbnailOwnerByUrl = buildPostBodyThumbnailOwnerMap(items)
const ownerMaps = {
thumbnailOwnerByUrl,
pageCoverThumbnailOwnerByUrl,
bodyThumbnailOwnerByUrl
}
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, tags)
const directUsage = getOriginalMediaUsage(item.url, posts, pages, siteSettings, tags, ownerMaps)
const thumbnailUsage = getPostThumbnailMediaUsage(item.url, thumbnailOwnerByUrl, posts, pages, siteSettings)
const pageCoverThumbnailUsage = getPageCoverThumbnailMediaUsage(item.url, pageCoverThumbnailOwnerByUrl, posts, pages, siteSettings)
const bodyThumbnailUsage = getPostBodyThumbnailMediaUsage(item.url, bodyThumbnailOwnerByUrl, posts, pages, siteSettings)
@@ -1046,11 +1132,19 @@ export const deleteMediaItem = async (url) => {
listAdminTags()
])
const items = await readMediaDirectory(uploadRoot)
const thumbnailOwnerByUrl = buildPostThumbnailOwnerMap(items)
const pageCoverThumbnailOwnerByUrl = buildPageCoverThumbnailOwnerMap(items)
const bodyThumbnailOwnerByUrl = buildPostBodyThumbnailOwnerMap(items)
const ownerMaps = {
thumbnailOwnerByUrl,
pageCoverThumbnailOwnerByUrl,
bodyThumbnailOwnerByUrl
}
const usage = [
...getDirectMediaUsage(url, posts, pages, siteSettings, tags),
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(items), posts, pages, siteSettings),
...getPageCoverThumbnailMediaUsage(url, buildPageCoverThumbnailOwnerMap(items), posts, pages, siteSettings),
...getPostBodyThumbnailMediaUsage(url, buildPostBodyThumbnailOwnerMap(items), posts, pages, siteSettings)
...getOriginalMediaUsage(url, posts, pages, siteSettings, tags, ownerMaps),
...getPostThumbnailMediaUsage(url, thumbnailOwnerByUrl, posts, pages, siteSettings),
...getPageCoverThumbnailMediaUsage(url, pageCoverThumbnailOwnerByUrl, posts, pages, siteSettings),
...getPostBodyThumbnailMediaUsage(url, bodyThumbnailOwnerByUrl, posts, pages, siteSettings)
]
if (usage.length) {
@@ -1067,8 +1161,15 @@ export const deleteMediaItem = async (url) => {
})
}
const ownedGeneratedUrls = getOwnedGeneratedMediaUrls(url, ownerMaps)
await rm(resolveMediaPath(url))
await deleteMediaMetadata(url)
for (const generatedUrl of ownedGeneratedUrls) {
await rm(resolveMediaPath(generatedUrl), { force: true })
await deleteMediaMetadata(generatedUrl)
}
}
/**
@@ -1092,11 +1193,19 @@ export const renameMediaItem = async (url, name) => {
listAdminTags()
])
const items = await readMediaDirectory(uploadRoot)
const thumbnailOwnerByUrl = buildPostThumbnailOwnerMap(items)
const pageCoverThumbnailOwnerByUrl = buildPageCoverThumbnailOwnerMap(items)
const bodyThumbnailOwnerByUrl = buildPostBodyThumbnailOwnerMap(items)
const ownerMaps = {
thumbnailOwnerByUrl,
pageCoverThumbnailOwnerByUrl,
bodyThumbnailOwnerByUrl
}
const usage = [
...getDirectMediaUsage(url, posts, pages, siteSettings, tags),
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(items), posts, pages, siteSettings),
...getPageCoverThumbnailMediaUsage(url, buildPageCoverThumbnailOwnerMap(items), posts, pages, siteSettings),
...getPostBodyThumbnailMediaUsage(url, buildPostBodyThumbnailOwnerMap(items), posts, pages, siteSettings)
...getOriginalMediaUsage(url, posts, pages, siteSettings, tags, ownerMaps),
...getPostThumbnailMediaUsage(url, thumbnailOwnerByUrl, posts, pages, siteSettings),
...getPageCoverThumbnailMediaUsage(url, pageCoverThumbnailOwnerByUrl, posts, pages, siteSettings),
...getPostBodyThumbnailMediaUsage(url, bodyThumbnailOwnerByUrl, posts, pages, siteSettings)
]
if (usage.length) {