From 164bc2131fde28f61f7a32e980879c5cac56538a Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 24 Jul 2026 13:56:21 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AF=B8=EB=94=94=EC=96=B4=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=EC=9E=AC=EC=83=9D=EC=84=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useAdminMediaThumbnailRegeneration.js | 150 ++++++++++++++++++ docs/map.md | 1 + docs/todo.md | 1 - docs/update.md | 6 + package-lock.json | 4 +- package.json | 2 +- pages/admin/media/index.vue | 138 ++-------------- 7 files changed, 175 insertions(+), 127 deletions(-) create mode 100644 composables/useAdminMediaThumbnailRegeneration.js diff --git a/composables/useAdminMediaThumbnailRegeneration.js b/composables/useAdminMediaThumbnailRegeneration.js new file mode 100644 index 0000000..5c6554a --- /dev/null +++ b/composables/useAdminMediaThumbnailRegeneration.js @@ -0,0 +1,150 @@ +import { ref } from 'vue' + +/** + * 관리자 미디어 화면의 썸네일 재생성 상태와 액션을 만든다. + * @param {Object} options - 재생성 옵션 + * @param {Function} options.refresh - 미디어 목록 새로고침 함수 + * @param {Object} options.selectedMediaUrl - 선택된 미디어 URL ref + * @param {Function} options.showToast - 관리자 토스트 표시 함수 + * @returns {Object} 썸네일 생성 가능 판별과 재생성 액션 + */ +export const useAdminMediaThumbnailRegeneration = ({ + refresh, + selectedMediaUrl, + showToast +}) => { + const regeneratingThumbnailUrl = ref('') + const regeneratingBodyThumbnailUrl = ref('') + + /** + * 카드 썸네일을 생성할 수 있는 원본 이미지인지 확인한다. + * @param {Object} item - 미디어 항목 + * @returns {boolean} 생성 가능 여부 + */ + 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) + ) + + /** + * 본문 표시용 이미지를 생성할 수 있는 원본 이미지인지 확인한다. + * @param {Object} item - 미디어 항목 + * @returns {boolean} 생성 가능 여부 + */ + const canRegeneratePostBodyThumbnail = (item) => ( + item?.bodyThumbnailStatus?.role === 'original' + && (item.bodyThumbnailStatus.isFallbackActive || item.bodyThumbnailStatus.hasThumbnail) + ) + + /** + * 선택한 원본 이미지의 대표 이미지 파일을 다시 생성한다. + * @param {Object} item - 미디어 항목 + * @returns {Promise} + */ + const regeneratePostThumbnail = async (item) => { + if (!canRegeneratePostThumbnail(item)) { + return + } + + regeneratingThumbnailUrl.value = item.url + + try { + await $fetch('/admin/api/media/thumbnail', { + method: 'POST', + body: { + url: item.url + } + }) + await refresh() + selectedMediaUrl.value = item.url + showToast('success', '대표 이미지 파일을 생성했습니다.') + } catch (error) { + showToast('error', error?.data?.message || '대표 이미지 파일을 생성하지 못했습니다.') + } finally { + regeneratingThumbnailUrl.value = '' + } + } + + /** + * 선택한 원본 이미지의 페이지 커버 썸네일을 다시 생성한다. + * @param {Object} item - 미디어 항목 + * @returns {Promise} + */ + 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 - 미디어 항목 + * @returns {Promise} + */ + const regeneratePostBodyThumbnail = async (item) => { + if (!canRegeneratePostBodyThumbnail(item)) { + return + } + + regeneratingBodyThumbnailUrl.value = item.url + + try { + await $fetch('/admin/api/media/thumbnail', { + method: 'POST', + body: { + url: item.url, + kind: 'body' + } + }) + await refresh() + selectedMediaUrl.value = item.url + showToast('success', '본문 이미지를 생성했습니다.') + } catch (error) { + showToast('error', error?.data?.message || '본문 이미지를 생성하지 못했습니다.') + } finally { + regeneratingBodyThumbnailUrl.value = '' + } + } + + return { + canRegeneratePageCoverThumbnail, + canRegeneratePostBodyThumbnail, + canRegeneratePostThumbnail, + regeneratePageCoverThumbnail, + regeneratePostBodyThumbnail, + regeneratePostThumbnail, + regeneratingBodyThumbnailUrl, + regeneratingThumbnailUrl + } +} diff --git a/docs/map.md b/docs/map.md index a0f2508..c0720fc 100644 --- a/docs/map.md +++ b/docs/map.md @@ -25,6 +25,7 @@ | composables/useAdminPostTags.js | 관리자 게시물 폼 태그 파싱·추천·선택·삭제·키보드 처리 상태 | | composables/useAdminFeaturedImagePicker.js | 관리자 게시물 폼 대표 이미지 선택 모달의 검색·페이지네이션·업로드·선택 상태 | | composables/useAdminPostPublishState.js | 관리자 게시물 폼 발행·예약·발행취소 상태와 날짜 입력 상태 | +| composables/useAdminMediaThumbnailRegeneration.js | 관리자 미디어 화면 대표 이미지·페이지 커버·본문 이미지 재생성 가능 여부와 API 호출 상태 | ## 공유 라이브러리(서버·클라이언트 공통) diff --git a/docs/todo.md b/docs/todo.md index b414179..6cfb374 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -5,7 +5,6 @@ - 거대 컴포넌트 책임 분리 진행. - `components/admin/AdminMarkdownEditor.vue`: 미디어 모달, 슬래시 명령, 히스토리·선택 복원, 라이브/소스 전환 로직을 단계적으로 composable 또는 하위 컴포넌트로 분리. - `components/content/ContentMarkdownRenderer.vue`: 파싱·블록 렌더링·라이브 편집 중개·라이트박스·광고 삽입 책임을 분리. - - `pages/admin/media/index.vue`: 재생성 액션을 작은 단위로 분리. - 미디어 카탈로그 전환 코드 수명 정리. - 운영 `MEDIA_CATALOG_MODE=database` 안정화 후 `shadow`/`filesystem` 런타임 분기 축소 가능 여부 검토. - fallback은 운영 안정성에 필요하므로 제거 전 reconcile·로그·테스트 기준을 먼저 정리. diff --git a/docs/update.md b/docs/update.md index e66b4a8..c4c6a54 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.11.47 + +- 관리자 미디어 화면의 대표 이미지·페이지 커버·본문 이미지 재생성 상태와 API 호출을 `useAdminMediaThumbnailRegeneration` composable로 분리. +- 썸네일 생성 가능 여부 판별과 생성 중 URL 상태를 한 경로에서 관리하도록 정리. +- 미디어 화면은 재생성 버튼 이벤트 연결만 담당하도록 축소. + ## v1.11.46 - 관리자 미디어 화면의 상세 모달 렌더링을 `AdminMediaDetailModal` 컴포넌트로 분리. diff --git a/package-lock.json b/package-lock.json index dda2adc..f408050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.46", + "version": "1.11.47", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.46", + "version": "1.11.47", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index e7cf050..cd55e28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.46", + "version": "1.11.47", "private": true, "type": "module", "imports": { diff --git a/pages/admin/media/index.vue b/pages/admin/media/index.vue index afeadc9..d3b2874 100644 --- a/pages/admin/media/index.vue +++ b/pages/admin/media/index.vue @@ -53,8 +53,6 @@ const editingUrl = ref('') const editingName = ref('') const editingCategory = ref('') const deletingUrl = ref('') -const regeneratingThumbnailUrl = ref('') -const regeneratingBodyThumbnailUrl = ref('') const selectedMediaUrl = ref('') const selectedMediaUrls = ref([]) const lastSelectedIndex = ref(-1) @@ -123,6 +121,21 @@ const { data: mediaResponse, refresh } = await useFetch('/admin/api/media', { } }) +const { + canRegeneratePageCoverThumbnail, + canRegeneratePostBodyThumbnail, + canRegeneratePostThumbnail, + regeneratePageCoverThumbnail, + regeneratePostBodyThumbnail, + regeneratePostThumbnail, + regeneratingBodyThumbnailUrl, + regeneratingThumbnailUrl +} = useAdminMediaThumbnailRegeneration({ + refresh, + selectedMediaUrl, + showToast +}) + const mediaItems = computed(() => Array.isArray(mediaResponse.value) ? mediaResponse.value : (mediaResponse.value?.items || [])) @@ -397,38 +410,6 @@ const getMediaUsageBadgeCount = (item) => { return usageCount + avatarOwnerCount } -/** - * 카드 썸네일을 생성할 수 있는 원본 이미지인지 확인한다. - * @param {Object} item - 미디어 항목 - * @returns {boolean} 생성 가능 여부 - */ -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) -) - -/** - * 본문 표시용 이미지를 생성할 수 있는 원본 이미지인지 확인한다. - * @param {Object} item - 미디어 항목 - * @returns {boolean} 생성 가능 여부 - */ -const canRegeneratePostBodyThumbnail = (item) => ( - item?.bodyThumbnailStatus?.role === 'original' - && (item.bodyThumbnailStatus.isFallbackActive || item.bodyThumbnailStatus.hasThumbnail) -) - /** * 미디어 항목 종류를 반환한다. * @param {Object} item - 미디어 항목 @@ -1103,95 +1084,6 @@ const deleteMedia = async (item) => { } } -/** - * 선택한 원본 이미지의 대표 이미지 파일을 다시 생성한다. - * @param {Object} item - 미디어 항목 - * @returns {Promise} - */ -const regeneratePostThumbnail = async (item) => { - if (!canRegeneratePostThumbnail(item)) { - return - } - - regeneratingThumbnailUrl.value = item.url - - try { - await $fetch('/admin/api/media/thumbnail', { - method: 'POST', - body: { - url: item.url - } - }) - await refresh() - selectedMediaUrl.value = item.url - showToast('success', '대표 이미지 파일을 생성했습니다.') - } catch (error) { - showToast('error', error?.data?.message || '대표 이미지 파일을 생성하지 못했습니다.') - } finally { - regeneratingThumbnailUrl.value = '' - } -} - -/** - * 선택한 원본 이미지의 페이지 커버 썸네일을 다시 생성한다. - * @param {Object} item - 미디어 항목 - * @returns {Promise} - */ -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 - 미디어 항목 - * @returns {Promise} - */ -const regeneratePostBodyThumbnail = async (item) => { - if (!canRegeneratePostBodyThumbnail(item)) { - return - } - - regeneratingBodyThumbnailUrl.value = item.url - - try { - await $fetch('/admin/api/media/thumbnail', { - method: 'POST', - body: { - url: item.url, - kind: 'body' - } - }) - await refresh() - selectedMediaUrl.value = item.url - showToast('success', '본문 이미지를 생성했습니다.') - } catch (error) { - showToast('error', error?.data?.message || '본문 이미지를 생성하지 못했습니다.') - } finally { - regeneratingBodyThumbnailUrl.value = '' - } -} - /** * 선택한 미디어 중 삭제 가능한 항목을 한 번에 삭제한다. * @returns {Promise}