본문 이미지 파생 파일명 정리

This commit is contained in:
2026-07-02 11:09:14 +09:00
parent 486c8bfe76
commit 7c9b5e9278
14 changed files with 59 additions and 23 deletions

View File

@@ -1,5 +1,10 @@
# 업데이트 요약
## v1.10.15
- 새 본문 이미지 자동 생성 파일명을 `body-1600`, `body-720`, `body-2560`처럼 해상도 기준으로 정리했다.
- 기존 `body`, `lightbox` 파일은 관리자 미디어에서 계속 본문 이미지로 관리되도록 호환을 유지했다.
## v1.10.14
- 자동 생성 이미지가 한 번 깨져 보인 뒤 같은 URL 캐시 때문에 계속 깨져 보일 수 있던 흐름을 줄였다.

View File

@@ -1,6 +1,6 @@
# 배포 가이드
> 로컬 기준 v1.10.14에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
> 로컬 기준 v1.10.15에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
## 빌드 유형
@@ -403,14 +403,14 @@ docker compose --env-file .env.production restart sori-studio-db
| 명령 | 용도 |
|------|------|
| `npm run images:backfill-post-thumbnails` | 로컬 Node/npm 환경에서 게시물 대표 이미지용 카드 썸네일(`thumbs/*-card.webp`) 일괄 생성 |
| `npm run images:backfill-post-body-thumbnails` | 로컬 Node/npm 환경에서 게시물 본문 표시용 WebP 이미지(`thumbs/*-body.webp`, `thumbs/*-body-720.webp`, `thumbs/*-lightbox.webp`) 일괄 생성 |
| `npm run images:backfill-post-body-thumbnails` | 로컬 Node/npm 환경에서 게시물 본문 표시용 WebP 이미지(`thumbs/*-body-1600.webp`, `thumbs/*-body-720.webp`, `thumbs/*-body-2560.webp`) 일괄 생성 |
| `npm run images:backfill-page-cover-thumbnails` | 로컬 Node/npm 환경에서 홈 Pages 진열장 커버용 WebP 이미지(`thumbs/*-page-cover.webp`) 일괄 생성 |
| `sh scripts/backfill-post-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 카드 썸네일 일괄 생성 |
| `sh scripts/backfill-post-body-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 본문 표시용 WebP 이미지 일괄 생성 |
| `sh scripts/backfill-page-cover-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 Pages 커버 썸네일 일괄 생성 |
| `npm run navigation:refresh-recommended-favicons` | 추천 사이트 파비콘 캐시를 수동으로 전체 갱신 |
본문 표시용 WebP 이미지(`thumbs/*-body.webp`, `thumbs/*-body-720.webp`, `thumbs/*-lightbox.webp`)는 게시물 저장 시 생성된다. 기존 게시물의 과거 이미지는 로컬에서는 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서는 `sh scripts/backfill-post-body-thumbnails-production.sh`로 일괄 생성할 수 있다. production 래퍼는 `docker compose up -d sori-studio` 후 실행 중인 앱 컨테이너 안의 Node로 백필 스크립트를 실행하므로 NAS 호스트에 npm이 없어도 된다.
본문 표시용 WebP 이미지(`thumbs/*-body-1600.webp`, `thumbs/*-body-720.webp`, `thumbs/*-body-2560.webp`)는 게시물 저장 시 생성된다. 기존 게시물의 과거 이미지는 로컬에서는 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서는 `sh scripts/backfill-post-body-thumbnails-production.sh`로 일괄 생성할 수 있다. production 래퍼는 `docker compose up -d sori-studio` 후 실행 중인 앱 컨테이너 안의 Node로 백필 스크립트를 실행하므로 NAS 호스트에 npm이 없어도 된다.
Pages 진열장 커버용 WebP 이미지(`thumbs/*-page-cover.webp`, 450×600)는 페이지 저장 시 생성된다. 기존 홈 노출 페이지의 과거 이미지는 로컬에서는 `npm run images:backfill-page-cover-thumbnails`, NAS 운영에서는 `sh scripts/backfill-page-cover-thumbnails-production.sh`로 일괄 생성할 수 있다.

View File

@@ -1,5 +1,9 @@
# 의사결정 이력
## 2026-07-02 v1.10.15 — 본문 파생 이미지는 해상도형 파일명을 쓴다
`body`와 `lightbox` 같은 용도명 파일명은 관리자 미디어에서 실제 크기를 바로 판단하기 어렵다. 본문 이미지는 관리자가 용량과 사용처를 자주 확인하는 자산이므로 새 생성 규칙을 `body-1600`, `body-720`, `body-2560` 해상도형 이름으로 바꾼다. 이미 운영 볼륨에 생성된 `body`, `lightbox` 파일은 삭제하지 않고 레거시 본문 이미지로 계속 분류해 사용처 연결과 삭제 잠금을 유지하며, 백필 명령으로 새 해상도형 파일을 생성할 수 있게 한다.
## 2026-07-02 v1.10.14 — 재생성 가능한 이미지는 즉시 재검증한다
게시물 파생 이미지는 원본 업로드와 달리 같은 URL의 파일 내용을 다시 만들 수 있다. 기존 `max-age=3600` 캐시는 평소 성능에는 유리하지만, 한 번 깨진 파생 이미지 응답이 브라우저에 저장되면 파일을 재생성해도 같은 URL이라 화면에서는 계속 깨져 보일 수 있다. 따라서 `thumbs` 아래 자동 생성 WebP는 `no-cache`로 매번 서버 재검증을 거치게 하고, 공개 본문과 관리자 미디어 미리보기에는 캐시 버전 쿼리를 붙여 이미 저장된 깨진 브라우저 캐시도 우회한다.

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 판별과 자동 생성 이미지 캐시 버전 URL 생성(대표 이미지·본문 이미지·갤러리·Pages 커버 선택 및 서버 검증 공용) |
| lib/post-card-thumbnail-url.js | 게시물 카드 썸네일 `thumbs/*-card.webp`, Pages 커버 썸네일 `thumbs/*-page-cover.webp`, 본문 표시용 `body-1600`/`body-720`·라이트박스용 `body-2560` 파생 이미지 URL 판별과 레거시 `body`/`lightbox` 판별, 자동 생성 이미지 캐시 버전 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 우선순위(수동 썸네일 > 서버 캐시 > 외부 프록시) |

View File

@@ -107,12 +107,13 @@
- 관리자 미디어 업로드 API는 JPG·PNG·WebP 파일을 `/uploads/posts/YYYY/MM/원본파일`에 저장한다. 업로드만으로는 카드 썸네일을 만들지 않고, 해당 이미지가 게시물 대표 이미지로 저장될 때 목록 카드용 WebP 썸네일을 `/uploads/posts/YYYY/MM/thumbs/원본파일명-card.webp`에 생성한다.
- 카드 썸네일은 640×360 기준 `cover` 리사이즈와 WebP 품질 82를 사용한다. GIF·동영상·문서 파일은 썸네일을 자동 생성하지 않는다.
- 본문 표시용 이미지는 1600px `thumbs/*-body.webp`와 720px `thumbs/*-body-720.webp`를 함께 생성하고, 공개 본문 단일 이미지·갤러리는 `srcset`으로 화면 폭에 맞는 파일을 우선 사용한다.
- 라이트박스용 이미지는 2560px `thumbs/*-lightbox.webp`를 생성해 크게 보기 화면에서 우선 사용한다. 별도 원본 보기 UI는 제공하지 않으며, 파생 이미지 로드 실패 시에만 원본으로 fallback한다.
- 본문 표시용 이미지는 1600px `thumbs/*-body-1600.webp`와 720px `thumbs/*-body-720.webp`를 함께 생성하고, 공개 본문 단일 이미지·갤러리는 `srcset`으로 화면 폭에 맞는 파일을 우선 사용한다.
- 라이트박스용 이미지는 2560px `thumbs/*-body-2560.webp`를 생성해 크게 보기 화면에서 우선 사용한다. 별도 원본 보기 UI는 제공하지 않으며, 파생 이미지 로드 실패 시에만 원본으로 fallback한다.
- 기존 운영 볼륨에 생성된 `thumbs/*-body.webp`, `thumbs/*-lightbox.webp`는 레거시 본문 파생 이미지로 계속 인식해 관리자 미디어의 본문 이미지 탭과 사용처 연결을 유지한다. 새 저장·재생성·백필은 해상도형 파일명만 만든다.
- 카드·본문·라이트박스·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를 생성한다.
- 기존 본문 이미지는 로컬에서 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서 `sh scripts/backfill-post-body-thumbnails-production.sh`로 전체 게시물 본문 URL을 스캔해 누락된 1600px 본문 표시용 WebP, 720px 작은 본문 표시용 WebP, 2560px 라이트박스용 WebP를 생성한다.
- 기존 Pages 진열장 커버 이미지는 로컬에서 `npm run images:backfill-page-cover-thumbnails`, NAS 운영에서 `sh scripts/backfill-page-cover-thumbnails-production.sh`로 홈 노출 페이지의 누락된 3:4 WebP 썸네일을 생성한다.
### 공개 목록·상세의 발행일 표시
@@ -270,7 +271,7 @@ components/content/
- 파일: `:::file` ~ `:::` (`url`, `title`, `description`, `name`, `size`) — 다운로드 링크 카드
- 렌더링: `ProseVideo.vue`, `ProseAudio.vue`, `ProseFile.vue`
- 관리자 슬래시: `/video`, `/audio`, `/file`로 빈 템플릿 삽입 후 URL·메타 수정
- 관리자 미디어 화면은 미디어 라이브러리·카드 썸네일·본문 이미지·프로필 이미지 탭으로 구분한다. 미디어 라이브러리 탭은 원본 업로드 파일만 표시하고, 게시물 목록용 `thumbs/*-card.webp` 파생 파일은 카드 썸네일 탭에만, 본문용 `thumbs/*-body.webp`·`*-body-720.webp`·`*-lightbox.webp` 파생 파일은 본문 이미지 탭에만 표시한다. 게시물 글쓰기의 대표 이미지와 본문 `/이미지`·`/갤러리` 선택 모달은 파생 파일을 숨기고 원본 이미지만 선택하게 한다. 본문 미디어 모달은 업로드 탭을 먼저 열고, 업로드 완료 후 라이브러리에서 새 파일을 자동 선택한다.
- 관리자 미디어 화면은 미디어 라이브러리·카드 썸네일·본문 이미지·프로필 이미지 탭으로 구분한다. 미디어 라이브러리 탭은 원본 업로드 파일만 표시하고, 게시물 목록용 `thumbs/*-card.webp` 파생 파일은 카드 썸네일 탭에만, 본문용 `thumbs/*-body-1600.webp`·`*-body-720.webp`·`*-body-2560.webp` 파생 파일과 레거시 `*-body.webp`·`*-lightbox.webp` 파일은 본문 이미지 탭에만 표시한다. 게시물 글쓰기의 대표 이미지와 본문 `/이미지`·`/갤러리` 선택 모달은 파생 파일을 숨기고 원본 이미지만 선택하게 한다. 본문 미디어 모달은 업로드 탭을 먼저 열고, 업로드 완료 후 라이브러리에서 새 파일을 자동 선택한다.
- 게시물 생성·수정 API는 `thumbs/*-card.webp` 카드 썸네일 URL을 대표 이미지로 저장하지 못하게 검증한다. 카드 썸네일은 원본 대표 이미지 저장 후 목록 표시용으로만 자동 생성한다.
- 링크·iframe·HTML 블록은 위 블록과 불필요하게 벌어지지 않도록 상단 외부 여백 없이 표시하고, 다음 블록과 구분되는 하단 여백만 유지한다.
- 관리자 미디어 화면의 미디어 라이브러리 탭은 전체·이미지·영상·음악·파일 종류 필터와 미사용 필터를 제공한다. 미사용은 게시물·페이지·사이트 설정·회원 프로필에서 참조되지 않는 항목을 의미한다. 비디오 항목은 브라우저에서 초반 프레임을 캔버스로 추출해 목록 썸네일로 표시하고, 추출 실패 시 `video` placeholder를 유지한다.
@@ -776,7 +777,7 @@ components/content/
- 저장된 태그가 없는 게시물에는 상세 메타 행·홈 Latest·`/posts` 목록 카드에 태그 배지나 더미 라벨을 표시하지 않는다.
- 검색엔진 노출 제외가 켜진 글은 robots 메타를 `noindex, nofollow`로 출력한다.
- 공개 상세 화면의 `og:image`와 Twitter large image 카드는 대표 이미지를 기본값으로 사용한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![](url)` 또는 파일명 캡션 토글 시 `![](url "파일명")` 형식으로 저장한다. 단독 이미지 파일 URL(`jpg`, `png`, `webp`, `gif`, `avif`, `svg`) 한 줄은 임베드가 아니라 이미지 블록으로 렌더링한다. 게시물 저장 시 `/uploads/posts/YYYY/MM/` 아래의 본문 `jpg`·`png`·`webp` 이미지는 원본 옆 1600px `thumbs/*-body.webp`, 720px `thumbs/*-body-720.webp`, 2560px `thumbs/*-lightbox.webp` 파생 이미지로 자동 생성한다. 공개 본문 단일 이미지와 갤러리는 720px·1600px 표시용 이미지를 우선 사용하고 실패하면 원본 URL로 대체하며, 라이트박스는 2560px 파생 이미지를 우선 사용하고 실패하면 원본 URL로 대체한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![](url)` 또는 파일명 캡션 토글 시 `![](url "파일명")` 형식으로 저장한다. 단독 이미지 파일 URL(`jpg`, `png`, `webp`, `gif`, `avif`, `svg`) 한 줄은 임베드가 아니라 이미지 블록으로 렌더링한다. 게시물 저장 시 `/uploads/posts/YYYY/MM/` 아래의 본문 `jpg`·`png`·`webp` 이미지는 원본 옆 1600px `thumbs/*-body-1600.webp`, 720px `thumbs/*-body-720.webp`, 2560px `thumbs/*-body-2560.webp` 파생 이미지로 자동 생성한다. 공개 본문 단일 이미지와 갤러리는 720px·1600px 표시용 이미지를 우선 사용하고 실패하면 원본 URL로 대체하며, 라이트박스는 2560px 파생 이미지를 우선 사용하고 실패하면 원본 URL로 대체한다.
- 이미지/갤러리 삽입 시 캡션은 기본 비우며, 블록 설정 패널에서 **파일명을 캡션으로 사용** 토글로 이미지 아래에 URL 파일명을 표시한다.
- 라이브 모드 이미지 블록은 hover/focus 시 우측 상단에 `삭제` 버튼만 표시한다. 이미지 URL·캡션·파일명 캡션 사용 여부 수정은 해당 이미지 블록에 포커스했을 때 오른쪽 이미지 설정 패널에서 처리한다.
- 이미지 블록 표시 옵션은 `regular`, `wide`, `full` 값을 사용하며 `regular`는 width 옵션을 생략한다.
@@ -920,7 +921,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`와 `*-page-cover.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-1600.webp`, `*-body-720.webp`, `*-body-2560.webp` 파일과 레거시 `*-body.webp`, `*-lightbox.webp` 파일만, 프로필 이미지 탭에서는 `/members/avatars/` 파일만 검색·탐색한다. 본문 이미지 탭의 상세 정보는 1600px, 720px, 2560px 용도를 구분해 표시한다. 라이브러리 탭의 파일 종류 필터는 `전체/이미지/영상/음악/파일`을 표시하고 사용 상태 필터는 중복 `전체` 배지를 피하기 위해 `미사용` 토글만 표시한다. 시스템 파생 탭에서는 전체/미사용 배지형 필터를 표시하고, 목록 카드 오른쪽 위에는 실제 사용 개수 숫자 배지를 표시한다. 카드 썸네일 탭과 본문 이미지 탭에서는 모든 항목이 해당 파생 파일이므로 각각 `카드`, `본문` 배지는 표시하지 않는다. 파생 이미지 목록·상세 미리보기는 파일 수정 시각과 용량을 캐시 버전으로 붙여 재생성 직후에도 새 파일을 확인할 수 있게 한다.
- 미디어 라이브러리 탭은 왼쪽 폴더 트리와 오른쪽 고밀도 썸네일 갤러리, 검색, 파일명 변경, 개별 삭제를 제공한다.
- 관리자는 폴더 추가 버튼으로 모달에서 새 폴더·하위 폴더 이름을 입력해 만들 수 있으며, `미분류`·`썸네일`(및 그 하위)을 제외한 폴더는 목록에서 삭제할 수 있다. 폴더 삭제 시 해당 경로 및 하위 경로로 분류돼 있던 미디어 메타는 모두 `미분류`로 되돌린다.
- 썸네일 본문(이미지·파일명) 한 번 클릭 시 상세(미리보기) 모달이 열리고, 썸네일 좌측 상단 **선택 토글**로 개별 선택한다. Shift+클릭으로 범위 선택이 가능하다. 현재 필터 결과의 전체 선택은 사용 중이 아니어서 삭제 가능한 항목만 대상으로 한다.

View File

@@ -11,9 +11,6 @@
- 업로드 총량 제한 및 메모리 사용량 개선.
- 관리자 업로드는 사이트 설정에서 1회 최대 파일 수·전체 용량을 조절하고 클라이언트·서버 양쪽에서 검사함.
- 서버는 아직 multipart 전체를 메모리에 받은 뒤 검사하므로, 고용량 다중 업로드의 메모리 피크를 더 줄이려면 스트리밍 업로드 파서 도입 검토.
- 본문 파생 이미지 파일명 정리 검토.
- 현재 본문 파생 이미지는 `*-body.webp`(1600px), `*-body-720.webp`(720px), `*-lightbox.webp`(2560px) 이름을 사용함.
- 운영 볼륨에 이미 생성된 파일이 있으므로 `*-body-1600.webp`, `*-body-2560.webp`처럼 해상도형 이름으로 바꾸려면 신규 생성 규칙, 기존 파일 백필, 레거시 파일 정리 절차를 함께 설계해야 함.
- 공개 통합 검색 인덱스 개선.
- 현재 검색은 게시물·페이지 `content`에 `lower(content)` 부분 일치를 수행함.
- 게시물이 수백~수천 개로 늘면 검색 모달 입력마다 DB 스캔 비용이 커질 수 있으므로 PostgreSQL full-text search, `pg_trgm`, 검색용 정규화 컬럼 중 하나로 전환 검토.

View File

@@ -1,5 +1,12 @@
# 업데이트 이력
## v1.10.15
- 새 게시물 본문 파생 이미지 파일명을 `*-body-1600.webp`, `*-body-720.webp`, `*-body-2560.webp` 해상도형 규칙으로 정리.
- 기존 `*-body.webp`, `*-lightbox.webp` 파일은 레거시 본문 이미지로 계속 인식해 관리자 미디어 사용처와 삭제 잠금을 유지하도록 보강.
- 본문 이미지 백필은 해상도형 새 파일이 없으면 다시 생성하도록 유지해 기존 운영 이미지의 새 규칙 전환을 지원.
- 본문 파생 이미지 파일명 정리 검토 과제를 완료 처리.
## v1.10.14
- 게시물 자동 생성 이미지(`card`, `page-cover`, `body`, `body-720`, `lightbox`) 표시 URL에 캐시 버전을 붙여 기존 깨진 브라우저 캐시를 우회하도록 수정.

View File

@@ -1,13 +1,13 @@
const postCardThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-card\.webp$/i
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 postBodyThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-(?:body(?:-(?:720|1600|2560))?|lightbox)\.webp$/i
const postUploadImageUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/[^/?#]+\.(?:jpe?g|png|webp)$/i
const generatedImageCacheVersion = '20260702-1'
/**
* 게시물 원본 이미지 URL에 대응하는 파생 이미지 URL을 만든다.
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @param {'body'|'body-720'|'lightbox'} kind - 파생 이미지 종류
* @param {'body'|'body-720'|'body-1600'|'body-2560'|'lightbox'} kind - 파생 이미지 종류
* @returns {string} 파생 이미지 URL
*/
const getPostDerivativeImageUrl = (imageUrl, kind) => {
@@ -88,7 +88,7 @@ export const withGeneratedImageCacheVersion = (imageUrl, version = generatedImag
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} 본문 표시용 파생 이미지 URL
*/
export const getPostBodyThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body')
export const getPostBodyThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body-1600')
/**
* 게시물 원본 이미지 URL에 대응하는 작은 본문 표시용 이미지 URL을 만든다.
@@ -102,7 +102,21 @@ export const getPostBodyPreviewThumbnailUrl = (imageUrl) => getPostDerivativeIma
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} 라이트박스용 파생 이미지 URL
*/
export const getPostLightboxThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'lightbox')
export const getPostLightboxThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body-2560')
/**
* 레거시 본문 표시용 이미지 URL을 만든다.
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} 레거시 본문 표시용 이미지 URL
*/
export const getLegacyPostBodyThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body')
/**
* 레거시 라이트박스용 이미지 URL을 만든다.
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} 레거시 라이트박스용 이미지 URL
*/
export const getLegacyPostLightboxThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'lightbox')
/**
* 페이지 커버 원본 이미지 URL에 대응하는 홈 진열장 썸네일 URL을 만든다.

4
package-lock.json generated
View File

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

View File

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

View File

@@ -118,7 +118,7 @@ const getPostBodyThumbnailResolutionLabel = (item) => {
return '720px'
}
if (url.endsWith('-lightbox.webp')) {
if (url.endsWith('-body-2560.webp') || url.endsWith('-lightbox.webp')) {
return '2560px'
}

View File

@@ -20,7 +20,7 @@ const contentTypes = new Map([
['.svg', 'image/svg+xml'],
['.ico', 'image/x-icon']
])
const regeneratableUploadPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-(?:card|page-cover|body|body-720|lightbox)\.webp$/i
const regeneratableUploadPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-(?:card|page-cover|body|body-720|body-1600|body-2560|lightbox)\.webp$/i
/**
* 업로드 파일 캐시 정책을 반환한다.

View File

@@ -5,6 +5,8 @@ import { getSiteSettings, listAdminPosts, listAdminTags, listPages } from '../re
import { getPostgresClient } from '../repositories/postgres-client'
import {
getPageCoverThumbnailUrl,
getLegacyPostBodyThumbnailUrl,
getLegacyPostLightboxThumbnailUrl,
getPostBodyPreviewThumbnailUrl,
getPostBodyThumbnailUrl,
getPostLightboxThumbnailUrl,
@@ -608,7 +610,9 @@ const buildPostBodyThumbnailOwnerMap = (items) => {
const bodyDerivativeUrlGetters = [
getPostBodyThumbnailUrl,
getPostBodyPreviewThumbnailUrl,
getPostLightboxThumbnailUrl
getPostLightboxThumbnailUrl,
getLegacyPostBodyThumbnailUrl,
getLegacyPostLightboxThumbnailUrl
]
for (const item of items) {

View File

@@ -4,6 +4,8 @@ import { dirname, extname, join, posix } from 'node:path'
import sharp from 'sharp'
import {
getPageCoverThumbnailUrl,
getLegacyPostBodyThumbnailUrl,
getLegacyPostLightboxThumbnailUrl,
getPostBodyPreviewThumbnailUrl,
getPostBodyThumbnailUrl,
getPostLightboxThumbnailUrl,
@@ -14,6 +16,8 @@ import {
export {
getPageCoverThumbnailUrl,
getLegacyPostBodyThumbnailUrl,
getLegacyPostLightboxThumbnailUrl,
getPostBodyPreviewThumbnailUrl,
getPostBodyThumbnailUrl,
getPostLightboxThumbnailUrl,