파생 이미지 캐시 재검증 적용

This commit is contained in:
2026-07-02 10:46:09 +09:00
parent ffd6cbb822
commit 486c8bfe76
12 changed files with 95 additions and 19 deletions

View File

@@ -8,7 +8,8 @@ import {
import {
getPostBodyPreviewThumbnailUrl,
getPostBodyThumbnailUrl,
getPostLightboxThumbnailUrl
getPostLightboxThumbnailUrl,
withGeneratedImageCacheVersion
} from '../../lib/post-card-thumbnail-url.js'
import { readEditableTextFromElement, setEditableCaretOffset } from '../../lib/markdown-inline.js'
import {
@@ -1059,14 +1060,26 @@ const contentLightboxImages = computed(() => {
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} 표시용 이미지 URL
*/
const getContentDisplayImageUrl = (imageUrl) => getPostBodyThumbnailUrl(imageUrl) || imageUrl || ''
const getContentDisplayImageUrl = (imageUrl) => {
const bodyThumbnailUrl = getPostBodyThumbnailUrl(imageUrl)
return bodyThumbnailUrl
? withGeneratedImageCacheVersion(bodyThumbnailUrl)
: imageUrl || ''
}
/**
* 게시물 라이트박스용 이미지 URL을 조회한다.
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} 라이트박스용 이미지 URL
*/
const getContentLightboxImageUrl = (imageUrl) => getPostLightboxThumbnailUrl(imageUrl) || imageUrl || ''
const getContentLightboxImageUrl = (imageUrl) => {
const lightboxThumbnailUrl = getPostLightboxThumbnailUrl(imageUrl)
return lightboxThumbnailUrl
? withGeneratedImageCacheVersion(lightboxThumbnailUrl)
: imageUrl || ''
}
/**
* 게시물 본문 이미지의 반응형 srcset을 만든다.
@@ -1078,8 +1091,8 @@ const getContentDisplayImageSrcset = (imageUrl) => {
const bodyUrl = getPostBodyThumbnailUrl(imageUrl)
return [
previewUrl ? `${previewUrl} 720w` : '',
bodyUrl ? `${bodyUrl} 1600w` : ''
previewUrl ? `${withGeneratedImageCacheVersion(previewUrl)} 720w` : '',
bodyUrl ? `${withGeneratedImageCacheVersion(bodyUrl)} 1600w` : ''
].filter(Boolean).join(', ')
}

View File

@@ -1,5 +1,10 @@
# 업데이트 요약
## v1.10.14
- 자동 생성 이미지가 한 번 깨져 보인 뒤 같은 URL 캐시 때문에 계속 깨져 보일 수 있던 흐름을 줄였다.
- 본문·라이트박스·카드·Pages 커버 자동 생성 이미지는 표시할 때 캐시 버전을 붙이고, 서버에서도 매번 재검증하도록 바꿨다.
## v1.10.13
- 글쓰기 이미지 선택창에 자동 생성된 본문·라이트박스·카드·Pages 커버 썸네일이 보이지 않도록 정리했다.

View File

@@ -1,6 +1,6 @@
# 배포 가이드
> 로컬 기준 v1.10.11에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
> 로컬 기준 v1.10.14에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
## 빌드 유형

View File

@@ -1,5 +1,9 @@
# 의사결정 이력
## 2026-07-02 v1.10.14 — 재생성 가능한 이미지는 즉시 재검증한다
게시물 파생 이미지는 원본 업로드와 달리 같은 URL의 파일 내용을 다시 만들 수 있다. 기존 `max-age=3600` 캐시는 평소 성능에는 유리하지만, 한 번 깨진 파생 이미지 응답이 브라우저에 저장되면 파일을 재생성해도 같은 URL이라 화면에서는 계속 깨져 보일 수 있다. 따라서 `thumbs` 아래 자동 생성 WebP는 `no-cache`로 매번 서버 재검증을 거치게 하고, 공개 본문과 관리자 미디어 미리보기에는 캐시 버전 쿼리를 붙여 이미 저장된 깨진 브라우저 캐시도 우회한다.
## 2026-07-02 v1.10.13 — 글쓰기 미디어 선택은 원본만 허용한다
본문 표시용·라이트박스용·카드용·Pages 커버용 WebP는 공개 화면 최적화를 위한 시스템 파생 파일이다. 이 파일이 글쓰기 미디어 선택창에 노출되면 관리자가 원본 대신 파생 파일을 본문에 삽입할 수 있고, 파생 파일 생성이 실패한 경우 공개 화면도 깨진 URL을 직접 참조하게 된다. 따라서 글쓰기와 대표 이미지 선택창은 원본만 보여 주고, 서버 저장 검증도 자동 생성 이미지 URL을 대표/커버 이미지로 받지 않는다. 파생 파일은 임시 파일에 먼저 만들고 읽기 검증 후 교체해, 실패한 산출물이 남아 원본 fallback을 방해하지 않게 한다.

View File

@@ -26,7 +26,7 @@
| 파일 | 용도 |
|------|------|
| lib/upload-size-limit.js | 관리자 미디어 업로드 종류별 최대 바이트 판별·1회 파일 수/총량 제한 정규화·용량 문구(서버·클라이언트 공용) |
| lib/post-card-thumbnail-url.js | 게시물 카드 썸네일 `thumbs/*-card.webp`, Pages 커버 썸네일 `thumbs/*-page-cover.webp`, 본문 표시용 1600px/720px·라이트박스용 2560px 파생 이미지 URL 판별(대표 이미지·본문 이미지·갤러리·Pages 커버 선택 및 서버 검증 공용) |
| lib/post-card-thumbnail-url.js | 게시물 카드 썸네일 `thumbs/*-card.webp`, Pages 커버 썸네일 `thumbs/*-page-cover.webp`, 본문 표시용 1600px/720px·라이트박스용 2560px 파생 이미지 URL 판별과 자동 생성 이미지 캐시 버전 URL 생성(대표 이미지·본문 이미지·갤러리·Pages 커버 선택 및 서버 검증 공용) |
| lib/external-favicon-url.js | 외부 URL origin 기준 Google `gstatic.com/faviconV2` 파비콘 URL 생성(내부 경로는 빈 문자열) |
| lib/navigation-url.js | 네비게이션 URL 접두사 분리·합성·정규화(`https://` 자동 보정, `/` 내부 경로) |
| lib/recommended-site-icon-url.js | 추천 사이트 아이콘 URL 우선순위(수동 썸네일 > 서버 캐시 > 외부 프록시) |
@@ -90,7 +90,7 @@
|------|------|
| server/middleware/admin-api-session.js | `/admin/api/*` 요청마다 관리자 세션과 현재 DB 권한(`owner`/`admin`) 재확인 |
| server/middleware/html-page-renderer.js | HTML 문서 모드 고정 페이지(`/pages/:slug`)를 Nuxt 렌더링 대신 `text/html` 원문으로 응답 |
| server/routes/uploads/[...path].get.js | 런타임 업로드 파일 제공 API(`/app/public/uploads` 볼륨 파일을 `/uploads/**`로 스트리밍, 원본 장기 캐시·파생 이미지 단기 캐시·조건부 요청 304 지원) |
| server/routes/uploads/[...path].get.js | 런타임 업로드 파일 제공 API(`/app/public/uploads` 볼륨 파일을 `/uploads/**`로 스트리밍, 원본 장기 캐시·파생 이미지 재검증 캐시·조건부 요청 304 지원) |
| server/routes/sitemap.xml.get.js | 공개 sitemap XML(`/sitemap.xml`) |
| server/routes/robots.txt.get.js | 공개 robots.txt(`/robots.txt`, sitemap 위치 안내) |
| server/routes/rss.xml.get.js | 공개 RSS 2.0 피드(`/rss.xml`) |
@@ -154,7 +154,7 @@
| 파일 | 화면 위치 |
|------|-----------|
| components/content/ContentRenderer.vue | 게시물/페이지 본문 |
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 표준 마크다운 표 파싱·렌더링(셀 내부 인라인 링크와 `<br>` 줄바꿈 포함), 공개 게시물 인아티클 광고를 본문 길이에 따라 0~2회 삽입, Obsidian식 위첨자·아래첨자 인라인 렌더링, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 `>` 즉시 인용 변환과 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용·콜아웃 멀티라인 편집 줄 범위 포커스와 위/아래 방향키 외부 문단 이탈, 인용 Backspace 문단 복귀, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 본문 표시용 720w/1600w WebP `srcset` 우선 표시와 원본 fallback, 단일 이미지·갤러리 통합 라이트박스의 2560px WebP 우선 표시, 갤러리 비율 기반 행 레이아웃, 라이브 이미지·갤러리 hover 삭제 액션, 리스트 마커 파란 계열 통일과 여러 줄 항목 첫 줄 기준 정렬 |
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 표준 마크다운 표 파싱·렌더링(셀 내부 인라인 링크와 `<br>` 줄바꿈 포함), 공개 게시물 인아티클 광고를 본문 길이에 따라 0~2회 삽입, Obsidian식 위첨자·아래첨자 인라인 렌더링, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 `>` 즉시 인용 변환과 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용·콜아웃 멀티라인 편집 줄 범위 포커스와 위/아래 방향키 외부 문단 이탈, 인용 Backspace 문단 복귀, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 본문 표시용 720w/1600w WebP `srcset` 캐시 버전 우선 표시와 원본 fallback, 단일 이미지·갤러리 통합 라이트박스의 2560px WebP 캐시 버전 우선 표시, 갤러리 비율 기반 행 레이아웃, 라이브 이미지·갤러리 hover 삭제 액션, 리스트 마커 파란 계열 통일과 여러 줄 항목 첫 줄 기준 정렬 |
| components/content/ContentMarkdownEditableInline.vue | 라이브 모드 공통 인라인 편집 영역, 일반 인라인 마크다운·Obsidian식 첨자 렌더링과 plain text 멀티라인 본문·끝 줄바꿈 보존, 한글 IME 조합 확정 Enter의 블록별 동작 연결, 위/아래 인접 블록 이동 시 편집 스크롤 추적, Shift 위/아래 인접 블록 선택 확장·단계적 `Cmd/Ctrl+A` 처리, 멀티라인 Enter 텍스트 값 치환, 첫 줄 빈 줄 포함 줄바꿈 유지 |
| components/content/ContentMarkdownInlineSegments.vue | 공개·라이브 본문 공통 인라인 세그먼트 렌더링(링크·굵게·기울임·인라인 코드·첨자·인라인 색상), 링크·색상 내부 중첩 인라인 렌더링, 사용자 화면 fragment 주석 반복을 줄이는 단일 루트 구조 |
| components/content/ContentMarkdownBlockGroup.vue | Markdown 본문 블록 단위 렌더 그룹, 라이브 삽입 영역을 조건부로만 DOM에 추가해 공개 화면 fragment 주석 감소 |

View File

@@ -109,7 +109,7 @@
- 카드 썸네일은 640×360 기준 `cover` 리사이즈와 WebP 품질 82를 사용한다. GIF·동영상·문서 파일은 썸네일을 자동 생성하지 않는다.
- 본문 표시용 이미지는 1600px `thumbs/*-body.webp`와 720px `thumbs/*-body-720.webp`를 함께 생성하고, 공개 본문 단일 이미지·갤러리는 `srcset`으로 화면 폭에 맞는 파일을 우선 사용한다.
- 라이트박스용 이미지는 2560px `thumbs/*-lightbox.webp`를 생성해 크게 보기 화면에서 우선 사용한다. 별도 원본 보기 UI는 제공하지 않으며, 파생 이미지 로드 실패 시에만 원본으로 fallback한다.
- 카드·본문·라이트박스·Pages 커버 파생 이미지는 임시 파일에 먼저 생성하고 Sharp 메타데이터로 읽기 검증한 뒤 최종 파일로 교체한다. 생성 또는 검증에 실패하면 깨진 파생 파일이 남지 않도록 대상 파일을 제거하고 공개 화면은 원본 fallback을 사용한다.
- 카드·본문·라이트박스·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만 스캔해 누락된 카드 썸네일을 생성한다. 관리자 미디어의 원본 이미지 상세에서도 카드 썸네일을 개별 생성·재생성할 수 있다.
- 기존 본문 이미지는 로컬에서 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서 `sh scripts/backfill-post-body-thumbnails-production.sh`로 전체 게시물 본문 URL을 스캔해 누락된 본문 표시용 WebP, 작은 본문 표시용 WebP, 라이트박스용 WebP를 생성한다.
@@ -920,7 +920,7 @@ components/content/
- 관리자 미디어 업로드 API는 이미지·비디오·오디오·문서 확장자를 허용한다(에디터 슬래시·미디어 모달과 동일 목록).
- 업로드 파일 크기 제한은 종류별 환경 변수를 따른다. 이미지·아바타·로고 등은 `MAX_FILE_SIZE`(기본 10MB), 비디오는 `MAX_VIDEO_FILE_SIZE`(기본 200MB), 오디오는 `MAX_AUDIO_FILE_SIZE`(기본 50MB), 문서·ZIP 등은 `MAX_DOCUMENT_FILE_SIZE`(기본 50MB). 관리자 게시물·페이지용 미디어 업로드의 1회 파일 수와 전체 용량은 사이트 설정 POST 설정의 업로드 제한을 따른다.
- 로컬 개발 업로드 파일은 `public/uploads/posts/YYYY/MM/` 아래 저장하고 `/uploads/posts/YYYY/MM/filename` URL로 제공한다.
- 관리자 미디어 화면 상단에 **미디어 라이브러리**·**카드 썸네일**·**본문 이미지**·**프로필 이미지** 탭을 두어, 라이브러리 탭에서는 원본 업로드 파일만, 카드 썸네일 탭에서는 `/posts/YYYY/MM/thumbs/*-card.webp` 파일만, 본문 이미지 탭에서는 `/posts/YYYY/MM/thumbs/*-body.webp`, `*-body-720.webp`, `*-lightbox.webp` 파일만, 프로필 이미지 탭에서는 `/members/avatars/` 파일만 검색·탐색한다. 본문 이미지 탭의 상세 정보는 1600px, 720px, 2560px 용도를 구분해 표시한다. 라이브러리 탭의 파일 종류 필터는 `전체/이미지/영상/음악/파일`을 표시하고 사용 상태 필터는 중복 `전체` 배지를 피하기 위해 `미사용` 토글만 표시한다. 시스템 파생 탭에서는 전체/미사용 배지형 필터를 표시하고, 목록 카드 오른쪽 위에는 실제 사용 개수 숫자 배지를 표시한다. 카드 썸네일 탭과 본문 이미지 탭에서는 모든 항목이 해당 파생 파일이므로 각각 `카드`, `본문` 배지는 표시하지 않는다.
- 관리자 미디어 화면 상단에 **미디어 라이브러리**·**카드 썸네일**·**본문 이미지**·**프로필 이미지** 탭을 두어, 라이브러리 탭에서는 원본 업로드 파일만, 카드 썸네일 탭에서는 `/posts/YYYY/MM/thumbs/*-card.webp`와 `*-page-cover.webp` 파일만, 본문 이미지 탭에서는 `/posts/YYYY/MM/thumbs/*-body.webp`, `*-body-720.webp`, `*-lightbox.webp` 파일만, 프로필 이미지 탭에서는 `/members/avatars/` 파일만 검색·탐색한다. 본문 이미지 탭의 상세 정보는 1600px, 720px, 2560px 용도를 구분해 표시한다. 라이브러리 탭의 파일 종류 필터는 `전체/이미지/영상/음악/파일`을 표시하고 사용 상태 필터는 중복 `전체` 배지를 피하기 위해 `미사용` 토글만 표시한다. 시스템 파생 탭에서는 전체/미사용 배지형 필터를 표시하고, 목록 카드 오른쪽 위에는 실제 사용 개수 숫자 배지를 표시한다. 카드 썸네일 탭과 본문 이미지 탭에서는 모든 항목이 해당 파생 파일이므로 각각 `카드`, `본문` 배지는 표시하지 않는다. 파생 이미지 목록·상세 미리보기는 파일 수정 시각과 용량을 캐시 버전으로 붙여 재생성 직후에도 새 파일을 확인할 수 있게 한다.
- 미디어 라이브러리 탭은 왼쪽 폴더 트리와 오른쪽 고밀도 썸네일 갤러리, 검색, 파일명 변경, 개별 삭제를 제공한다.
- 관리자는 폴더 추가 버튼으로 모달에서 새 폴더·하위 폴더 이름을 입력해 만들 수 있으며, `미분류`·`썸네일`(및 그 하위)을 제외한 폴더는 목록에서 삭제할 수 있다. 폴더 삭제 시 해당 경로 및 하위 경로로 분류돼 있던 미디어 메타는 모두 `미분류`로 되돌린다.
- 썸네일 본문(이미지·파일명) 한 번 클릭 시 상세(미리보기) 모달이 열리고, 썸네일 좌측 상단 **선택 토글**로 개별 선택한다. Shift+클릭으로 범위 선택이 가능하다. 현재 필터 결과의 전체 선택은 사용 중이 아니어서 삭제 가능한 항목만 대상으로 한다.

View File

@@ -1,5 +1,12 @@
# 업데이트 이력
## v1.10.14
- 게시물 자동 생성 이미지(`card`, `page-cover`, `body`, `body-720`, `lightbox`) 표시 URL에 캐시 버전을 붙여 기존 깨진 브라우저 캐시를 우회하도록 수정.
- `/uploads/posts/**/thumbs/*.webp` 파생 이미지 응답 캐시를 `no-cache`로 낮춰 같은 URL 재생성 후에도 브라우저가 서버에 재검증하도록 수정.
- 관리자 미디어 화면의 파생 이미지 미리보기는 파일 수정 시각·용량 기준 캐시 버전을 사용하도록 수정.
- 관리자 미디어 본문 이미지 탭 안내 문구를 720px·1600px·2560px 파생 이미지 구조에 맞게 정리.
## v1.10.13
- 글쓰기 이미지·갤러리 선택 모달에서 게시물 카드·본문 1600px·본문 720px·라이트박스·Pages 커버 자동 생성 이미지를 숨기고 원본만 선택하도록 수정.

View File

@@ -2,6 +2,7 @@ const postCardThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^
const pageCoverThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-page-cover\.webp$/i
const postBodyThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-(?:body(?:-720)?|lightbox)\.webp$/i
const postUploadImageUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/[^/?#]+\.(?:jpe?g|png|webp)$/i
const generatedImageCacheVersion = '20260702-1'
/**
* 게시물 원본 이미지 URL에 대응하는 파생 이미지 URL을 만든다.
@@ -52,6 +53,36 @@ export const isPostBodyThumbnailUrl = (imageUrl) => Boolean(
imageUrl && postBodyThumbnailUrlPattern.test(String(imageUrl))
)
/**
* 자동 생성 이미지 URL인지 확인한다.
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
* @returns {boolean} 자동 생성 이미지이면 true
*/
export const isGeneratedPostImageUrl = (imageUrl) => isPostCardThumbnailUrl(imageUrl)
|| isPageCoverThumbnailUrl(imageUrl)
|| isPostBodyThumbnailUrl(imageUrl)
/**
* 자동 생성 이미지 URL에 캐시 우회 쿼리를 붙인다.
* @param {string|null|undefined} imageUrl - 이미지 URL
* @param {string} [version] - 캐시 버전
* @returns {string} 표시용 이미지 URL
*/
export const withGeneratedImageCacheVersion = (imageUrl, version = generatedImageCacheVersion) => {
const url = String(imageUrl || '')
if (!isGeneratedPostImageUrl(url)) {
return url
}
const hashIndex = url.indexOf('#')
const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url
const hash = hashIndex >= 0 ? url.slice(hashIndex) : ''
const separator = baseUrl.includes('?') ? '&' : '?'
return `${baseUrl}${separator}v=${encodeURIComponent(version)}${hash}`
}
/**
* 게시물 원본 이미지 URL에 대응하는 본문 표시용 이미지 URL을 만든다.
* @param {string|null|undefined} imageUrl - 원본 이미지 URL

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "1.10.13",
"version": "1.10.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "1.10.13",
"version": "1.10.14",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.10.13",
"version": "1.10.14",
"private": true,
"type": "module",
"imports": {

View File

@@ -10,7 +10,8 @@ import {
import {
isPageCoverThumbnailUrl,
isPostBodyThumbnailUrl,
isPostCardThumbnailUrl
isPostCardThumbnailUrl,
withGeneratedImageCacheVersion
} from '~/lib/post-card-thumbnail-url.js'
definePageMeta({
@@ -131,6 +132,21 @@ const getPostBodyThumbnailResolutionLabel = (item) => {
*/
const isSystemManagedMediaItem = (item) => Boolean(item?.avatarOwner) || isPostCardThumbnailDiskItem(item) || isPageCoverThumbnailDiskItem(item) || isPostBodyThumbnailDiskItem(item)
/**
* 미디어 미리보기 이미지 URL을 반환한다.
* @param {Object} item - 미디어 항목
* @returns {string} 미리보기 URL
*/
const getMediaPreviewUrl = (item) => {
const url = String(item?.url || '')
if (!isPostCardThumbnailDiskItem(item) && !isPageCoverThumbnailDiskItem(item) && !isPostBodyThumbnailDiskItem(item)) {
return url
}
return withGeneratedImageCacheVersion(url, `${item?.updatedAt || ''}-${item?.size || ''}`)
}
/**
* 파일명 변경·삭제·드래그 이동이 제한되는지 여부
* @param {Object} item - 미디어 항목
@@ -330,7 +346,7 @@ const activeSystemMediaHint = computed(() => {
}
if (activeTab.value === 'postBodyThumbnails') {
return '게시물 본문을 빠르게 보여주기 위해 원본 이미지에서 자동 생성한 WebP 이미지입니다. 본문에는 이 파일을 우선 표시하고, 크게 보기에서는 원본 이미지를 사용합니다. 원본과 연결되어 있어 이름 변경과 폴더 이동은 제한됩니다.'
return '게시물 본문을 빠르게 보여주기 위해 원본 이미지에서 자동 생성한 WebP 이미지입니다. 본문에는 720px·1600px 파일을 우선 표시하고, 크게 보기에는 2560px 파일을 우선 사용합니다. 원본과 연결되어 있어 이름 변경과 폴더 이동은 제한됩니다.'
}
return '회원 프로필에서 쓰는 이미지는 연결 회원이 있을 때만 삭제·이름 변경이 막힙니다. 프로필에서 바꾸거나 해제해도 디스크 파일은 삭제되지 않으며 이 목록에 남습니다. 목록이 바로 안 바뀌면 페이지를 새로고침하세요. 관리자는 필요 시 삭제·다운로드로 정리할 수 있습니다.'
@@ -1437,7 +1453,7 @@ watch(filteredMediaUrls, (urls) => {
<img
v-if="getMediaItemKind(item) === 'image'"
class="admin-media__image aspect-square w-full bg-surface object-cover"
:src="item.url"
:src="getMediaPreviewUrl(item)"
:alt="item.title"
>
<AdminMediaVideoThumbnail
@@ -1533,7 +1549,7 @@ watch(filteredMediaUrls, (urls) => {
<img
v-if="getMediaItemKind(selectedMedia) === 'image'"
class="admin-media__preview-image max-h-[72vh] max-w-full object-contain"
:src="selectedMedia.url"
:src="getMediaPreviewUrl(selectedMedia)"
:alt="selectedMedia.title"
>
<video

View File

@@ -29,7 +29,7 @@ const regeneratableUploadPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/
*/
const getUploadCacheControl = (pathname) => {
if (regeneratableUploadPattern.test(pathname)) {
return 'public, max-age=3600, stale-while-revalidate=86400'
return 'public, no-cache'
}
return 'public, max-age=31536000, immutable'