페이지 커버 썸네일 최적화
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
normalizeUploadMaxTotalSize
|
||||
} from '~/lib/upload-size-limit.js'
|
||||
import {
|
||||
isPageCoverThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
isPostCardThumbnailUrl
|
||||
} from '~/lib/post-card-thumbnail-url.js'
|
||||
@@ -90,6 +91,13 @@ const isThumbnailDiskItem = (item) => Boolean(item?.url?.includes('/members/avat
|
||||
*/
|
||||
const isPostCardThumbnailDiskItem = (item) => isPostCardThumbnailUrl(item?.url)
|
||||
|
||||
/**
|
||||
* 페이지 커버 썸네일 디스크 경로 여부
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 페이지 커버 썸네일이면 true
|
||||
*/
|
||||
const isPageCoverThumbnailDiskItem = (item) => isPageCoverThumbnailUrl(item?.url)
|
||||
|
||||
/**
|
||||
* 게시물 본문 표시용 이미지 디스크 경로 여부
|
||||
* @param {Object} item - 미디어 항목
|
||||
@@ -121,7 +129,7 @@ const getPostBodyThumbnailResolutionLabel = (item) => {
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 시스템 관리 항목이면 true
|
||||
*/
|
||||
const isSystemManagedMediaItem = (item) => Boolean(item?.avatarOwner) || isPostCardThumbnailDiskItem(item) || isPostBodyThumbnailDiskItem(item)
|
||||
const isSystemManagedMediaItem = (item) => Boolean(item?.avatarOwner) || isPostCardThumbnailDiskItem(item) || isPageCoverThumbnailDiskItem(item) || isPostBodyThumbnailDiskItem(item)
|
||||
|
||||
/**
|
||||
* 파일명 변경·삭제·드래그 이동이 제한되는지 여부
|
||||
@@ -135,7 +143,7 @@ const isMediaItemLocked = (item) => Boolean(item?.usage?.length) || Boolean(item
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 변경 가능 여부
|
||||
*/
|
||||
const canRenameMediaItem = (item) => !isMediaItemLocked(item) && !isPostCardThumbnailDiskItem(item) && !isPostBodyThumbnailDiskItem(item)
|
||||
const canRenameMediaItem = (item) => !isMediaItemLocked(item) && !isPostCardThumbnailDiskItem(item) && !isPageCoverThumbnailDiskItem(item) && !isPostBodyThumbnailDiskItem(item)
|
||||
|
||||
/**
|
||||
* 미디어 논리 폴더를 변경할 수 있는지 확인한다.
|
||||
@@ -167,6 +175,10 @@ const getMediaBadgeLabel = (item) => {
|
||||
return '카드'
|
||||
}
|
||||
|
||||
if (item?.thumbnailStatus?.role === 'pageCover') {
|
||||
return '페이지'
|
||||
}
|
||||
|
||||
if (item?.bodyThumbnailStatus?.role === 'body') {
|
||||
return '본문'
|
||||
}
|
||||
@@ -196,6 +208,10 @@ const getMediaBadgeClass = (item) => {
|
||||
return 'bg-sky-700 text-white'
|
||||
}
|
||||
|
||||
if (item?.thumbnailStatus?.role === 'pageCover') {
|
||||
return 'bg-cyan-800 text-white'
|
||||
}
|
||||
|
||||
if (item?.bodyThumbnailStatus?.role === 'body') {
|
||||
return 'bg-violet-700 text-white'
|
||||
}
|
||||
@@ -226,6 +242,18 @@ const getMediaUsageBadgeCount = (item) => {
|
||||
*/
|
||||
const canRegeneratePostThumbnail = (item) => (
|
||||
item?.thumbnailStatus?.role === 'original'
|
||||
&& item?.thumbnailStatus?.variant !== 'pageCover'
|
||||
&& (item.thumbnailStatus.isFallbackActive || item.thumbnailStatus.hasThumbnail)
|
||||
)
|
||||
|
||||
/**
|
||||
* 페이지 커버 썸네일을 생성할 수 있는 원본 이미지인지 확인한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {boolean} 생성 가능 여부
|
||||
*/
|
||||
const canRegeneratePageCoverThumbnail = (item) => (
|
||||
item?.thumbnailStatus?.role === 'original'
|
||||
&& item?.thumbnailStatus?.variant === 'pageCover'
|
||||
&& (item.thumbnailStatus.isFallbackActive || item.thumbnailStatus.hasThumbnail)
|
||||
)
|
||||
|
||||
@@ -246,9 +274,9 @@ const canRegeneratePostBodyThumbnail = (item) => (
|
||||
*/
|
||||
const getMediaItemKind = (item) => item?.kind || 'image'
|
||||
|
||||
const libraryMediaItems = computed(() => (mediaItems.value || []).filter((item) => !isThumbnailDiskItem(item) && !isPostCardThumbnailDiskItem(item) && !isPostBodyThumbnailDiskItem(item)))
|
||||
const libraryMediaItems = computed(() => (mediaItems.value || []).filter((item) => !isThumbnailDiskItem(item) && !isPostCardThumbnailDiskItem(item) && !isPageCoverThumbnailDiskItem(item) && !isPostBodyThumbnailDiskItem(item)))
|
||||
|
||||
const postThumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isPostCardThumbnailDiskItem(item)))
|
||||
const postThumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isPostCardThumbnailDiskItem(item) || isPageCoverThumbnailDiskItem(item)))
|
||||
|
||||
const postBodyThumbnailMediaItems = computed(() => (mediaItems.value || []).filter((item) => isPostBodyThumbnailDiskItem(item)))
|
||||
|
||||
@@ -298,7 +326,7 @@ const activeSystemMediaTitle = computed(() => {
|
||||
|
||||
const activeSystemMediaHint = computed(() => {
|
||||
if (activeTab.value === 'postThumbnails') {
|
||||
return '게시물 목록 카드에서 쓰는 자동 생성 이미지입니다. 원본 대표 이미지가 사용 중이고 이 썸네일 파일이 있으면 목록에서는 원본 대신 이 파일을 불러옵니다. 원본에 연결된 썸네일은 사용 중으로 표시되며, 썸네일이 없는 원본은 일반 미디어 상세에서 다시 생성 필요 상태로 구분합니다.'
|
||||
return '게시물 목록 카드와 홈 Pages 진열장에서 쓰는 자동 생성 이미지입니다. 원본 이미지가 사용 중이고 이 썸네일 파일이 있으면 공개 목록에서는 원본 대신 이 파일을 불러옵니다. 원본에 연결된 썸네일은 사용 중으로 표시되며, 썸네일이 없는 원본은 일반 미디어 상세에서 다시 생성 필요 상태로 구분합니다.'
|
||||
}
|
||||
|
||||
if (activeTab.value === 'postBodyThumbnails') {
|
||||
@@ -1020,6 +1048,36 @@ const regeneratePostThumbnail = async (item) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택한 원본 이미지의 페이지 커버 썸네일을 다시 생성한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const regeneratePageCoverThumbnail = async (item) => {
|
||||
if (!canRegeneratePageCoverThumbnail(item)) {
|
||||
return
|
||||
}
|
||||
|
||||
regeneratingThumbnailUrl.value = item.url
|
||||
|
||||
try {
|
||||
await $fetch('/admin/api/media/thumbnail', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
url: item.url,
|
||||
kind: 'pageCover'
|
||||
}
|
||||
})
|
||||
await refresh()
|
||||
selectedMediaUrl.value = item.url
|
||||
showToast('success', '페이지 커버 썸네일을 생성했습니다.')
|
||||
} catch (error) {
|
||||
showToast('error', error?.data?.message || '페이지 커버 썸네일을 생성하지 못했습니다.')
|
||||
} finally {
|
||||
regeneratingThumbnailUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택한 원본 이미지의 본문 표시용 이미지를 다시 생성한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
@@ -1534,18 +1592,29 @@ watch(filteredMediaUrls, (urls) => {
|
||||
<dd class="admin-media__info-value mt-1">{{ formatFileSize(selectedMedia.size) }}</dd>
|
||||
</div>
|
||||
<div v-if="selectedMedia.thumbnailStatus" class="admin-media__info-row">
|
||||
<dt class="admin-media__info-label text-xs font-semibold text-muted">카드 썸네일</dt>
|
||||
<dt class="admin-media__info-label text-xs font-semibold text-muted">
|
||||
{{ selectedMedia.thumbnailStatus.variant === 'pageCover' || selectedMedia.thumbnailStatus.role === 'pageCover' ? '페이지 커버 썸네일' : '카드 썸네일' }}
|
||||
</dt>
|
||||
<dd class="admin-media__info-value mt-1 grid gap-1 break-all">
|
||||
<span v-if="selectedMedia.thumbnailStatus.role === 'card'">
|
||||
자동 생성된 목록 카드용 썸네일입니다.
|
||||
</span>
|
||||
<span v-else-if="selectedMedia.thumbnailStatus.role === 'pageCover'">
|
||||
자동 생성된 Pages 진열장용 커버 썸네일입니다.
|
||||
</span>
|
||||
<template v-else>
|
||||
<span v-if="selectedMedia.thumbnailStatus.hasThumbnail">
|
||||
생성됨: {{ selectedMedia.thumbnailStatus.thumbnailUrl }}
|
||||
</span>
|
||||
<span v-else-if="selectedMedia.thumbnailStatus.isFallbackActive && selectedMedia.thumbnailStatus.variant === 'pageCover'" class="font-semibold text-amber-700">
|
||||
페이지 커버 썸네일 없음. 현재 Pages 진열장에서 원본 이미지를 불러옵니다.
|
||||
</span>
|
||||
<span v-else-if="selectedMedia.thumbnailStatus.isFallbackActive" class="font-semibold text-amber-700">
|
||||
썸네일 없음. 현재 목록에서 원본 이미지를 불러옵니다.
|
||||
</span>
|
||||
<span v-else-if="selectedMedia.thumbnailStatus.variant === 'pageCover'">
|
||||
페이지 커버 썸네일 없음. 홈 Pages 진열장에서 사용되면 원본으로 대체됩니다.
|
||||
</span>
|
||||
<span v-else>
|
||||
썸네일 없음. 대표 이미지로 사용되면 원본으로 대체됩니다.
|
||||
</span>
|
||||
@@ -1562,6 +1631,15 @@ watch(filteredMediaUrls, (urls) => {
|
||||
>
|
||||
{{ regeneratingThumbnailUrl === selectedMedia.url ? '생성 중' : (selectedMedia.thumbnailStatus.hasThumbnail ? '카드 썸네일 다시 생성' : '카드 썸네일 생성') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canRegeneratePageCoverThumbnail(selectedMedia)"
|
||||
class="admin-media__page-cover-thumbnail-regenerate mt-1 justify-self-start rounded border border-line bg-white px-3 py-1.5 text-xs font-semibold text-ink transition hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50"
|
||||
type="button"
|
||||
:disabled="regeneratingThumbnailUrl === selectedMedia.url"
|
||||
@click="regeneratePageCoverThumbnail(selectedMedia)"
|
||||
>
|
||||
{{ regeneratingThumbnailUrl === selectedMedia.url ? '생성 중' : (selectedMedia.thumbnailStatus.hasThumbnail ? '페이지 커버 썸네일 다시 생성' : '페이지 커버 썸네일 생성') }}
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="selectedMedia.bodyThumbnailStatus" class="admin-media__info-row">
|
||||
|
||||
@@ -567,7 +567,7 @@ const pageOrderRemovedCount = computed(() => pageOrderRemovedPageIds.value.size)
|
||||
* @param {Object} page - 관리자 페이지
|
||||
* @returns {string} 썸네일 URL
|
||||
*/
|
||||
const getPageOrderCardImage = (page) => String(page?.featuredImage || page?.listingImage || '').trim()
|
||||
const getPageOrderCardImage = (page) => String(page?.listingImageThumbnail || page?.featuredImage || page?.listingImage || '').trim()
|
||||
|
||||
/**
|
||||
* 페이지를 현재 섹션에서 제거 목록에 추가한다.
|
||||
|
||||
Reference in New Issue
Block a user