미디어 원본 사용처 집계 개선
This commit is contained in:
@@ -26,9 +26,11 @@ import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OP
|
||||
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
|
||||
import { buildToggleOpenerLine } from '../../lib/markdown-toggle.js'
|
||||
import {
|
||||
getPostBodyPreviewThumbnailUrl,
|
||||
isPageCoverThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
isPostCardThumbnailUrl
|
||||
isPostCardThumbnailUrl,
|
||||
withGeneratedImageCacheVersion
|
||||
} from '../../lib/post-card-thumbnail-url.js'
|
||||
import { getTextareaCaretCoordinates } from '../../lib/textarea-caret-coordinates.js'
|
||||
import {
|
||||
@@ -1840,6 +1842,47 @@ const isMediaItemSelectableForTarget = (item) => {
|
||||
return kind === mediaPickerTarget.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 모달에서 사용할 가벼운 이미지 미리보기 URL을 반환한다.
|
||||
* 선택·삽입 값은 원본 URL을 유지하고, 화면 표시만 파생 이미지를 우선 사용한다.
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {string} 미리보기 URL
|
||||
*/
|
||||
const getMediaPickerPreviewUrl = (item) => {
|
||||
const url = String(item?.url || '')
|
||||
|
||||
if (getMediaItemKind(item) !== 'image') {
|
||||
return url
|
||||
}
|
||||
|
||||
if (item?.bodyThumbnailStatus?.hasThumbnail) {
|
||||
return withGeneratedImageCacheVersion(getPostBodyPreviewThumbnailUrl(url), `${item?.updatedAt || ''}-${item?.size || ''}`)
|
||||
}
|
||||
|
||||
if (item?.thumbnailStatus?.hasThumbnail && item.thumbnailStatus.thumbnailUrl) {
|
||||
return withGeneratedImageCacheVersion(item.thumbnailStatus.thumbnailUrl, `${item?.updatedAt || ''}-${item?.size || ''}`)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 선택 모달 파생 이미지 로드 실패 시 원본 URL로 되돌린다.
|
||||
* @param {Event} event - 이미지 오류 이벤트
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {void}
|
||||
*/
|
||||
const onMediaPickerImageError = (event, item) => {
|
||||
const image = event.target
|
||||
|
||||
if (!(image instanceof HTMLImageElement) || image.dataset.fallbackApplied === 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
image.dataset.fallbackApplied = 'true'
|
||||
image.src = String(item?.url || '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 드롭된 파일이 현재 미디어 선택 대상에 맞는지 확인한다.
|
||||
* @param {File} file - 브라우저 파일
|
||||
@@ -3775,8 +3818,11 @@ const handleKeydown = (event) => {
|
||||
<img
|
||||
v-if="getMediaItemKind(item) === 'image'"
|
||||
class="admin-markdown-editor__media-thumb aspect-[4/3] w-full bg-[#f6f7f8] object-cover"
|
||||
:src="item.url"
|
||||
:src="getMediaPickerPreviewUrl(item)"
|
||||
:alt="item.name || ''"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@error="onMediaPickerImageError($event, item)"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-07-03 v1.10.25 — 원본 사용 여부는 파생 이미지 사용까지 포함한다
|
||||
|
||||
본문 이미지는 공개 화면에서 원본 대신 `body-1600`, `body-720`, `body-2560` 파생 이미지를 사용한다. 하지만 운영자는 원본을 백업·다운로드 기준으로 보관하므로, 원본 URL이 직접 본문에 없다는 이유만으로 미사용으로 분류하면 실제 사용 중인 이미지 묶음을 일괄 삭제할 위험이 생긴다. 따라서 관리자 미디어 사용처 계산은 원본 URL 직접 참조와 함께 원본에서 생성된 카드·페이지 커버·본문 파생 이미지 URL 참조를 원본 사용처로 합산한다. 반대로 원본이 정말 미사용이라 삭제될 때는 연결된 자동 생성 파생 이미지까지 함께 정리해 시스템 파일이 남지 않게 한다.
|
||||
|
||||
글쓰기 이미지 선택 모달은 삽입해야 하는 값이 원본 URL이므로 라이브러리에는 원본 파일만 보여 준다. 다만 썸네일 표시는 원본을 내려받을 필요가 없어서, 이미 생성된 720px 본문 파생 이미지나 카드 썸네일을 우선 표시하고 실패했을 때만 원본으로 되돌린다.
|
||||
|
||||
## 2026-07-03 v1.10.24 — 글쓰기 저장 기준은 실제 저장 필드만 본다
|
||||
|
||||
관리자 글쓰기 폼은 저장 payload에 태그 표시명 보조값(`tagNames`)을 함께 담아 새 태그 이름을 만들 수 있게 한다. 하지만 기존 태그의 표시명은 태그 API가 늦게 도착하면 슬러그에서 실제 이름으로 바뀔 수 있고, 이 값까지 미저장 비교에 포함하면 사용자가 아무것도 하지 않아도 비공개·멤버십 글에서 이탈 경고와 `Update` 활성화가 발생한다. 따라서 미저장 비교는 제목, 본문, 상태, 태그 슬러그처럼 실제 게시물 저장 상태를 바꾸는 필드만 기준으로 한다.
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
| components/admin/AdminMediaVideoThumbnail.vue | 관리자 미디어 목록 비디오 항목의 초반 프레임 캔버스 썸네일 |
|
||||
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(왼쪽 상태 텍스트·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·신규 임시 슬러그·저장 중 추가 변경 재자동저장·발행 저장 중 이탈 확인·발행·예약·멤버십·비공개 상태 저장, 미저장 비교에서 태그 표시명 보조값 제외, 공개 상태 전환 시 빈 발행 시각 현재 시각 보정, 발행 모달(중앙 배치), 좌우 설정 패널(작은 화면은 오른쪽 고정 오버레이), 오른쪽 `View Post` 링크, 오른쪽 하단 본문 통계(단어·문자·공백·읽기 시간·블록·이미지), 미리보기 emit·미저장 이탈 가드, 자동 생성 썸네일을 숨기는 원본 전용 대표 이미지 선택, 대표 이미지 본문 상단 표시 토글, 추천 글 토글, 사이트 설정 기준 태그 최대 개수 제한, 태그 색상 배지 다중 입력·최근 사용 태그 5개 우선 추천·메인 태그 드롭다운·부분 검색 추천, 새 태그 입력명 유지와 `/`→`-` 슬러그 변환 |
|
||||
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 게시글 작성과 같은 전체 화면 에디터·상단 저장 툴바·접이식 오른쪽 설정 패널, 페이지 공개 상태 선택, HTML 문서 기본 모드, 홈 Pages 진열장 노출 메타와 등록된 페이지 섹션 검색형 선택, 빈 본문/`!`+Tab HTML 골격 자동 완성, 항상 보이는 일반 텍스트/HTML 모드 선택, 한글 제목 영문 슬러그 자동 변환, HTML textarea 커서 위치 파일 URL 삽입, HTML 태그 내부 스마트 따옴표 정규화 |
|
||||
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령(`/표` 기본 표 템플릿 포함)·미디어 모달(이미지·갤러리·비디오·오디오·파일, 업로드 탭 기본·첫 순서, 업로드 완료 파일 자동 선택, 카드·본문·라이트박스·Pages 커버 자동 생성 이미지 숨김, 업로드 중 중복 업로드·닫기 차단·로딩 표시), 코드 블록 본문 슬래시 명령 비활성화, 커서 블록 컨텍스트·선택 텍스트 색상 컨텍스트 `block-panel` emit, 에디터 본문 `Cmd/Ctrl+Z` 히스토리와 선택 위치 복원, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭·지원 블록 밖 커서 이동 시 닫기·미디어 모달 중 유지, 인용 마지막 줄 아래 방향키 외부 문단 포커스 이동, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연 |
|
||||
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령(`/표` 기본 표 템플릿 포함)·미디어 모달(이미지·갤러리·비디오·오디오·파일, 업로드 탭 기본·첫 순서, 업로드 완료 파일 자동 선택, 카드·본문·라이트박스·Pages 커버 자동 생성 이미지 숨김, 원본 URL 선택 유지·목록 미리보기는 720px 본문 파생 이미지 또는 카드 썸네일 우선, 업로드 중 중복 업로드·닫기 차단·로딩 표시), 코드 블록 본문 슬래시 명령 비활성화, 커서 블록 컨텍스트·선택 텍스트 색상 컨텍스트 `block-panel` emit, 에디터 본문 `Cmd/Ctrl+Z` 히스토리와 선택 위치 복원, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭·지원 블록 밖 커서 이동 시 닫기·미디어 모달 중 유지, 인용 마지막 줄 아래 방향키 외부 문단 포커스 이동, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연 |
|
||||
| components/admin/AdminEditorBlockPanel.vue | 게시물 설정 사이드바 오버레이 블록 설정(선택 텍스트 색상, 이미지·갤러리·링크·iframe·HTML·인용 배경색·콜아웃 제목·아이콘·배경색·코드·토글), 갤러리 선택 이미지 강조 |
|
||||
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/HTML 블록, 콜아웃 Emoji on/off·이모지 프리셋·인용과 같은 배경 프리셋 선택(우측 고정 설정 패널), 갤러리 복수 미디어 선택·이미지 수별 열 배치·삽입 위치 표시 드래그 순서 변경, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 |
|
||||
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상, 태그 헤더 이미지 업로드·삭제, 오버레이 제목·본문 편집, 수정 화면 변경 저장 버튼 변경사항 기준 활성화) |
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
- 본문 표시용 이미지는 1600px `thumbs/*-body-1600.webp`와 720px `thumbs/*-body-720.webp`를 함께 생성하고, 공개 본문 단일 이미지·갤러리는 `srcset`으로 화면 폭에 맞는 파일을 우선 사용한다.
|
||||
- 라이트박스용 이미지는 2560px `thumbs/*-body-2560.webp`를 생성해 크게 보기 화면에서 우선 사용한다. 별도 원본 보기 UI는 제공하지 않으며, 파생 이미지 로드 실패 시에만 원본으로 fallback한다.
|
||||
- 기존 운영 볼륨에 생성된 `thumbs/*-body.webp`, `thumbs/*-lightbox.webp`는 레거시 본문 파생 이미지로 계속 인식해 관리자 미디어의 본문 이미지 탭과 사용처 연결을 유지한다. 새 저장·재생성·백필은 해상도형 파일명만 만든다.
|
||||
- 관리자 미디어의 원본 이미지 사용처는 원본 URL 직접 참조뿐 아니라 해당 원본에서 생성된 카드·페이지 커버·본문 파생 이미지 URL 사용처까지 함께 반영한다. 따라서 공개 본문이 `body-1600` 같은 파생 이미지를 사용하더라도 원본은 미사용 삭제 대상에서 제외된다. 원본이 실제로 미사용이라 삭제될 때는 연결된 카드·페이지 커버·본문 자동 생성 이미지도 함께 삭제한다.
|
||||
- 카드·본문·라이트박스·Pages 커버 파생 이미지는 임시 파일에 먼저 생성하고 Sharp 메타데이터로 읽기 검증한 뒤 최종 파일로 교체한다. 생성 또는 검증에 실패하면 깨진 파생 파일이 남지 않도록 대상 파일을 제거하고 공개 화면은 원본 fallback을 사용한다. 파생 이미지는 같은 URL로 재생성될 수 있으므로 `/uploads/posts/YYYY/MM/thumbs/*.webp` 응답은 `no-cache`로 재검증하고, 공개 본문과 관리자 미디어 미리보기에는 파생 이미지 캐시 버전 쿼리를 붙여 기존 브라우저 캐시를 우회한다.
|
||||
- 공개 게시물 응답은 원본 대표 이미지 `featuredImage`를 유지하고, 대응 썸네일 파일이 실제로 존재할 때만 `featuredImageThumbnail`을 추가한다.
|
||||
- 기존 대표 이미지는 로컬에서 `npm run images:backfill-post-thumbnails`, NAS 운영에서 `sh scripts/backfill-post-thumbnails-production.sh`로 게시물 대표 이미지 URL만 스캔해 누락된 카드 썸네일을 생성한다. 관리자 미디어의 원본 이미지 상세에서도 카드 썸네일을 개별 생성·재생성할 수 있다.
|
||||
@@ -203,7 +204,7 @@ layouts/
|
||||
- 관리자 글 목록의 대표 이미지 미리보기는 작은 카드 썸네일이 있으면 `featuredImageThumbnail`을 우선 사용하고, 없을 때만 원본 `featuredImage`를 fallback으로 사용한다.
|
||||
- 관리자 글쓰기의 Tags 입력은 배지형 다중 입력을 유지하며, 오른쪽 트리거로 태그 추천 목록을 열 수 있다. 입력이 비어 있으면 공개 발행글 기준 최근 사용 태그 5개를 먼저 표시한 뒤 메인 태그를 이어서 보여 주고, 입력 중에는 기존 태그 이름·슬러그 부분 일치 결과를 추천한다. 추천 목록은 방향키와 Enter로 선택할 수 있다. 선택된 태그 배지는 태그 고유 색상을 반영한다. 게시물별 태그 최대 개수는 기본 5개이며, 사이트 설정 POST 설정의 `postTagLimit` 값으로 1~10개 범위에서 조절한다.
|
||||
- 관리자 글쓰기 상단 왼쪽 상태 영역은 `Published`, `Scheduled`, `Members`, `Private`, 초안 저장 상태 등 현재 상태 텍스트만 표시하며, 공개 게시물 이동은 오른쪽 설정 패널의 `View Post` 링크에서만 제공한다. 오른쪽 설정 패널 하단에는 본문 기준 단어 수, 공백 제외 문자 수와 공백 수, 예상 읽기 시간, 블록 수, 이미지 수를 작은 통계로 표시한다.
|
||||
- 관리자 미디어 검색창은 글·멤버 목록과 같은 돋보기 아이콘 포함 입력 스타일을 사용한다. 미디어 라이브러리 탭에서는 파일 추가 버튼으로 `/admin/api/uploads`에 직접 업로드할 수 있고, 현재 폴더를 보고 있으면 업로드 후 해당 폴더로 배치한다. 미디어 관리 화면은 `/admin/api/media?paged=1` 응답을 사용해 서버에서 탭·폴더·파일 종류·미사용·검색 조건을 적용한 뒤 50·100·200개 단위 페이지네이션을 제공하며 번호와 이전·다음 버튼으로 이동한다. 현재 페이지 선택은 화면에 표시된 삭제 가능 항목만 대상으로 하며, 선택 삭제는 사용 중이거나 회원 프로필에 연결된 잠금 항목을 제외하고 삭제한다.
|
||||
- 관리자 미디어 검색창은 글·멤버 목록과 같은 돋보기 아이콘 포함 입력 스타일을 사용한다. 미디어 라이브러리 탭에서는 파일 추가 버튼으로 `/admin/api/uploads`에 직접 업로드할 수 있고, 현재 폴더를 보고 있으면 업로드 후 해당 폴더로 배치한다. 미디어 관리 화면은 `/admin/api/media?paged=1` 응답을 사용해 서버에서 탭·폴더·파일 종류·미사용·검색 조건을 적용한 뒤 50·100·200개 단위 페이지네이션을 제공하며 번호와 이전·다음 버튼으로 이동한다. 현재 페이지 선택은 화면에 표시된 삭제 가능 항목만 대상으로 하며, 선택 삭제는 사용 중이거나 회원 프로필에 연결된 잠금 항목을 제외하고 삭제한다. 미디어 그리드와 글쓰기 이미지 선택 모달은 원본 삽입·상세 확인은 유지하되, 목록 미리보기에는 가능한 작은 본문 720px 파생 이미지나 카드 썸네일을 우선 사용한다.
|
||||
- 메뉴 관리 항목은 `네비게이션`으로 표시한다.
|
||||
- 게시글·페이지·태그·미디어·네비게이션·멤버·설정 메뉴는 전용 SVG 아이콘을 사용한다.
|
||||
- 멤버 메뉴는 우측에 현재 총 멤버 수를 표시한다.
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.10.25
|
||||
|
||||
- 관리자 미디어 원본 이미지 사용처 계산에 카드·페이지 커버·본문 파생 이미지 사용처를 함께 반영하도록 수정.
|
||||
- 파생 이미지가 본문에서 사용 중인 원본 파일이 미사용으로 분류되어 일괄 삭제 대상에 들어가던 문제 수정.
|
||||
- 미사용 원본 이미지를 삭제할 때 연결된 카드·페이지 커버·본문 자동 생성 이미지도 함께 삭제되도록 수정.
|
||||
- 글쓰기 이미지 선택 모달의 라이브러리 그리드가 원본 대신 720px 본문 파생 이미지나 카드 썸네일을 우선 표시하도록 수정.
|
||||
|
||||
## v1.10.24
|
||||
|
||||
- 관리자 글쓰기 미저장 변경 비교에서 태그 표시명 보조값을 제외해 태그 API 로딩만으로 비공개 글 이탈 경고가 뜨던 문제 수정.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.24",
|
||||
"version": "1.10.25",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.24",
|
||||
"version": "1.10.25",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.24",
|
||||
"version": "1.10.25",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user