본문 이미지 최적화·라이트박스·번호형 페이지네이션 반영 (v1.10.3)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import { createError } from 'h3'
|
||||
import { getSiteSettings, listAdminPosts, listAdminTags, listPages } from '../repositories/content-repository'
|
||||
import { getPostgresClient } from '../repositories/postgres-client'
|
||||
import {
|
||||
getPostBodyThumbnailUrl,
|
||||
getPostThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
isPostCardThumbnailUrl,
|
||||
isPostThumbnailSource
|
||||
} from './post-thumbnail-image.js'
|
||||
@@ -85,12 +87,13 @@ 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 hasPostBodyThumbnail = uniqueUrls.some((u) => isPostBodyThumbnailUrl(u))
|
||||
const hasNonAvatar = uniqueUrls.some((u) => !isMemberAvatarPublicUrl(u))
|
||||
|
||||
if (hasPostCardThumbnail) {
|
||||
if (hasPostCardThumbnail || hasPostBodyThumbnail) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '게시물 카드 썸네일은 원본 이미지에 연결된 자동 생성 파일이라 폴더를 변경할 수 없습니다.'
|
||||
message: '게시물 자동 생성 이미지는 원본 이미지에 연결된 파일이라 폴더를 변경할 수 없습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -542,29 +545,53 @@ const getDirectMediaUsage = (url, posts, pages, siteSettings, tags = []) => [
|
||||
...getTagMediaUsage(url, tags)
|
||||
]
|
||||
|
||||
/**
|
||||
* 게시물 파생 이미지 URL별 원본 URL 맵을 만든다.
|
||||
* @param {Array<Object>} items - 미디어 항목 목록
|
||||
* @param {(url: string) => string} getDerivativeUrl - 원본 URL에서 파생 URL을 만드는 함수
|
||||
* @param {(url: string) => boolean} isDerivativeUrl - 파생 URL 여부 확인 함수
|
||||
* @returns {Map<string, string>} 파생 이미지 URL별 원본 URL
|
||||
*/
|
||||
const buildPostDerivativeOwnerMap = (items, getDerivativeUrl, isDerivativeUrl) => {
|
||||
const itemUrlSet = new Set(items.map((item) => item.url))
|
||||
const ownerByDerivativeUrl = new Map()
|
||||
|
||||
for (const item of items) {
|
||||
if (isDerivativeUrl(item.url) || !isPostThumbnailSource('', item.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const derivativeUrl = getDerivativeUrl(item.url)
|
||||
|
||||
if (derivativeUrl && itemUrlSet.has(derivativeUrl)) {
|
||||
ownerByDerivativeUrl.set(derivativeUrl, item.url)
|
||||
}
|
||||
}
|
||||
|
||||
return ownerByDerivativeUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 썸네일 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()
|
||||
const buildPostThumbnailOwnerMap = (items) => buildPostDerivativeOwnerMap(
|
||||
items,
|
||||
getPostThumbnailUrl,
|
||||
isPostCardThumbnailUrl
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
/**
|
||||
* 본문 표시용 이미지 URL별 원본 URL 맵을 만든다.
|
||||
* @param {Array<Object>} items - 미디어 항목 목록
|
||||
* @returns {Map<string, string>} 본문 표시용 이미지 URL별 원본 URL
|
||||
*/
|
||||
const buildPostBodyThumbnailOwnerMap = (items) => buildPostDerivativeOwnerMap(
|
||||
items,
|
||||
getPostBodyThumbnailUrl,
|
||||
isPostBodyThumbnailUrl
|
||||
)
|
||||
|
||||
/**
|
||||
* 게시물 카드 썸네일의 간접 사용처를 조회한다.
|
||||
@@ -592,6 +619,32 @@ const getPostThumbnailMediaUsage = (url, ownerByThumbnailUrl, posts, pages, site
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 본문 표시용 이미지의 간접 사용처를 조회한다.
|
||||
* @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 getPostBodyThumbnailMediaUsage = (url, ownerByThumbnailUrl, posts, pages, siteSettings) => {
|
||||
const originalUrl = ownerByThumbnailUrl.get(url)
|
||||
|
||||
if (!originalUrl) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getDirectMediaUsage(originalUrl, posts, pages, siteSettings)
|
||||
.filter((usage) => usage.location === 'content')
|
||||
.map((usage) => ({
|
||||
...usage,
|
||||
location: 'contentBodyImage',
|
||||
label: '본문 표시 이미지',
|
||||
sourceUrl: originalUrl
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 업로드 이미지의 카드 썸네일 상태를 조회한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
@@ -600,7 +653,7 @@ const getPostThumbnailMediaUsage = (url, ownerByThumbnailUrl, posts, pages, site
|
||||
* @returns {Object|null} 카드 썸네일 상태
|
||||
*/
|
||||
const getPostThumbnailStatus = (item, itemUrlSet, directUsage) => {
|
||||
if (isPostCardThumbnailUrl(item.url) || !isPostThumbnailSource('', item.name)) {
|
||||
if (isPostCardThumbnailUrl(item.url) || isPostBodyThumbnailUrl(item.url) || !isPostThumbnailSource('', item.name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -621,6 +674,35 @@ const getPostThumbnailStatus = (item, itemUrlSet, directUsage) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 업로드 이미지의 본문 표시용 이미지 상태를 조회한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @param {Set<string>} itemUrlSet - 전체 미디어 URL 집합
|
||||
* @param {Array<Object>} directUsage - 원본 URL 직접 사용처
|
||||
* @returns {Object|null} 본문 표시용 이미지 상태
|
||||
*/
|
||||
const getPostBodyThumbnailStatus = (item, itemUrlSet, directUsage) => {
|
||||
if (isPostCardThumbnailUrl(item.url) || isPostBodyThumbnailUrl(item.url) || !isPostThumbnailSource('', item.name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const thumbnailUrl = getPostBodyThumbnailUrl(item.url)
|
||||
|
||||
if (!thumbnailUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasThumbnail = itemUrlSet.has(thumbnailUrl)
|
||||
const isContentImage = directUsage.some((usage) => usage.location === 'content')
|
||||
|
||||
return {
|
||||
role: 'original',
|
||||
thumbnailUrl,
|
||||
hasThumbnail,
|
||||
isFallbackActive: isContentImage && !hasThumbnail
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이트 설정에서 미디어 URL 사용처 조회
|
||||
* @param {string} url - 미디어 URL
|
||||
@@ -705,20 +787,24 @@ export const listMediaItems = async () => {
|
||||
const avatarOwnerByUrl = await getAvatarOwnersByUrls(items.map((item) => item.url))
|
||||
const itemUrlSet = new Set(items.map((item) => item.url))
|
||||
const thumbnailOwnerByUrl = buildPostThumbnailOwnerMap(items)
|
||||
const bodyThumbnailOwnerByUrl = buildPostBodyThumbnailOwnerMap(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, tags)
|
||||
const thumbnailUsage = getPostThumbnailMediaUsage(item.url, thumbnailOwnerByUrl, posts, pages, siteSettings)
|
||||
const bodyThumbnailUsage = getPostBodyThumbnailMediaUsage(item.url, bodyThumbnailOwnerByUrl, posts, pages, siteSettings)
|
||||
const originalUrl = thumbnailOwnerByUrl.get(item.url) || ''
|
||||
const bodyOriginalUrl = bodyThumbnailOwnerByUrl.get(item.url) || ''
|
||||
|
||||
return {
|
||||
...item,
|
||||
category,
|
||||
usage: [
|
||||
...directUsage,
|
||||
...thumbnailUsage
|
||||
...thumbnailUsage,
|
||||
...bodyThumbnailUsage
|
||||
],
|
||||
thumbnailStatus: originalUrl
|
||||
? {
|
||||
@@ -726,6 +812,12 @@ export const listMediaItems = async () => {
|
||||
originalUrl
|
||||
}
|
||||
: getPostThumbnailStatus(item, itemUrlSet, directUsage),
|
||||
bodyThumbnailStatus: bodyOriginalUrl
|
||||
? {
|
||||
role: 'body',
|
||||
originalUrl: bodyOriginalUrl
|
||||
}
|
||||
: getPostBodyThumbnailStatus(item, itemUrlSet, directUsage),
|
||||
avatarOwner
|
||||
}
|
||||
})
|
||||
@@ -848,9 +940,11 @@ export const deleteMediaItem = async (url) => {
|
||||
getSiteSettings(),
|
||||
listAdminTags()
|
||||
])
|
||||
const items = await readMediaDirectory(uploadRoot)
|
||||
const usage = [
|
||||
...getDirectMediaUsage(url, posts, pages, siteSettings, tags),
|
||||
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(await readMediaDirectory(uploadRoot)), posts, pages, siteSettings)
|
||||
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(items), posts, pages, siteSettings),
|
||||
...getPostBodyThumbnailMediaUsage(url, buildPostBodyThumbnailOwnerMap(items), posts, pages, siteSettings)
|
||||
]
|
||||
|
||||
if (usage.length) {
|
||||
@@ -878,10 +972,10 @@ export const deleteMediaItem = async (url) => {
|
||||
* @returns {Promise<Object>} 변경된 미디어 항목
|
||||
*/
|
||||
export const renameMediaItem = async (url, name) => {
|
||||
if (isPostCardThumbnailUrl(url)) {
|
||||
if (isPostCardThumbnailUrl(url) || isPostBodyThumbnailUrl(url)) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
message: '게시물 카드 썸네일은 원본 이미지와의 연결을 유지하기 위해 파일명을 변경할 수 없습니다.'
|
||||
message: '게시물 자동 생성 이미지는 원본 이미지와의 연결을 유지하기 위해 파일명을 변경할 수 없습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -891,9 +985,11 @@ export const renameMediaItem = async (url, name) => {
|
||||
getSiteSettings(),
|
||||
listAdminTags()
|
||||
])
|
||||
const items = await readMediaDirectory(uploadRoot)
|
||||
const usage = [
|
||||
...getDirectMediaUsage(url, posts, pages, siteSettings, tags),
|
||||
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(await readMediaDirectory(uploadRoot)), posts, pages, siteSettings)
|
||||
...getPostThumbnailMediaUsage(url, buildPostThumbnailOwnerMap(items), posts, pages, siteSettings),
|
||||
...getPostBodyThumbnailMediaUsage(url, buildPostBodyThumbnailOwnerMap(items), posts, pages, siteSettings)
|
||||
]
|
||||
|
||||
if (usage.length) {
|
||||
|
||||
Reference in New Issue
Block a user