라이트박스 이미지 파생 파일 적용
This commit is contained in:
@@ -5,7 +5,11 @@ import {
|
||||
isImageUrl,
|
||||
parseImageMarkdownLine
|
||||
} from '../../lib/markdown-image.js'
|
||||
import { getPostBodyPreviewThumbnailUrl, getPostBodyThumbnailUrl } from '../../lib/post-card-thumbnail-url.js'
|
||||
import {
|
||||
getPostBodyPreviewThumbnailUrl,
|
||||
getPostBodyThumbnailUrl,
|
||||
getPostLightboxThumbnailUrl
|
||||
} from '../../lib/post-card-thumbnail-url.js'
|
||||
import { readEditableTextFromElement, setEditableCaretOffset } from '../../lib/markdown-inline.js'
|
||||
import {
|
||||
applyLiveSelectionDelete,
|
||||
@@ -1057,6 +1061,13 @@ const contentLightboxImages = computed(() => {
|
||||
*/
|
||||
const getContentDisplayImageUrl = (imageUrl) => getPostBodyThumbnailUrl(imageUrl) || imageUrl || ''
|
||||
|
||||
/**
|
||||
* 게시물 라이트박스용 이미지 URL을 조회한다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
* @returns {string} 라이트박스용 이미지 URL
|
||||
*/
|
||||
const getContentLightboxImageUrl = (imageUrl) => getPostLightboxThumbnailUrl(imageUrl) || imageUrl || ''
|
||||
|
||||
/**
|
||||
* 게시물 본문 이미지의 반응형 srcset을 만든다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
@@ -1149,6 +1160,22 @@ const onGalleryImageError = (event, fallbackUrl) => {
|
||||
image.src = fallbackUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이트박스용 파생 이미지 로드 실패 시 원본 이미지로 되돌린다.
|
||||
* @param {Event} event - 이미지 로드 이벤트
|
||||
* @param {string} fallbackUrl - 원본 이미지 URL
|
||||
* @returns {void}
|
||||
*/
|
||||
const onLightboxImageError = (event, fallbackUrl) => {
|
||||
const image = event.target
|
||||
|
||||
if (!(image instanceof HTMLImageElement) || !fallbackUrl || image.src.endsWith(fallbackUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
image.src = fallbackUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 이미지 셀의 flex 비율 스타일을 만든다.
|
||||
* @param {Object} block - 갤러리 블록
|
||||
@@ -3621,8 +3648,9 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<img
|
||||
class="content-markdown-renderer__lightbox-image max-h-[84vh] max-w-[92vw] object-contain"
|
||||
:src="activeLightboxImage.url"
|
||||
:src="getContentLightboxImageUrl(activeLightboxImage.url)"
|
||||
:alt="getImageAltAttribute(activeLightboxImage)"
|
||||
@error="onLightboxImageError($event, activeLightboxImage.url)"
|
||||
>
|
||||
<button
|
||||
v-if="activeLightboxImages.length > 1"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 업데이트 요약
|
||||
|
||||
## v1.10.8
|
||||
|
||||
- 게시물 이미지 크게 보기에서 원본 대신 2560px 라이트박스용 WebP 이미지를 우선 사용하도록 바꿨다.
|
||||
|
||||
## v1.10.7
|
||||
|
||||
- 게시물 본문 이미지에 720px 표시용 WebP를 추가하고 공개 본문에서 화면 폭에 맞는 이미지를 고르도록 `srcset`을 적용했다.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 배포 가이드
|
||||
|
||||
> 로컬 기준 v1.10.7에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
|
||||
> 로컬 기준 v1.10.8에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
|
||||
|
||||
## 빌드 유형
|
||||
|
||||
@@ -403,12 +403,12 @@ 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`) 일괄 생성 |
|
||||
| `npm run images:backfill-post-body-thumbnails` | 로컬 Node/npm 환경에서 게시물 본문 표시용 WebP 이미지(`thumbs/*-body.webp`, `thumbs/*-body-720.webp`, `thumbs/*-lightbox.webp`) 일괄 생성 |
|
||||
| `sh scripts/backfill-post-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 카드 썸네일 일괄 생성 |
|
||||
| `sh scripts/backfill-post-body-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 본문 표시용 WebP 이미지 일괄 생성 |
|
||||
| `npm run navigation:refresh-recommended-favicons` | 추천 사이트 파비콘 캐시를 수동으로 전체 갱신 |
|
||||
|
||||
본문 표시용 WebP 이미지(`thumbs/*-body.webp`, `thumbs/*-body-720.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.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이 없어도 된다.
|
||||
|
||||
Homepage 위젯 연동은 `docs/homepage-widget.md`를 참고한다. Docker에서 Homepage가 같은 네트워크에 있으면 `http://sori-studio:3000/api/homepage-widget`을 사용한다.
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-07-01 v1.10.8 — 라이트박스도 원본 대신 큰 파생 이미지를 우선한다
|
||||
|
||||
본문의 크게 보기 기능은 원본 파일 자체를 내려받게 하는 것보다 독자가 이미지를 선명하게 확인할 수 있는 크기를 안정적으로 제공하는 쪽이 목적에 맞다. 따라서 라이트박스에는 2560px WebP 파생 이미지를 우선 사용하고, 별도 원본 보기 UI는 두지 않는다. 파생 이미지가 아직 생성되지 않았거나 로드에 실패한 경우에만 화면 깨짐을 막기 위해 원본 fallback을 유지한다.
|
||||
|
||||
## 2026-07-01 v1.10.7 — 본문 이미지는 720px와 1600px를 함께 제공한다
|
||||
|
||||
본문 이미지가 모두 1600px 표시용 WebP만 사용하면 모바일 화면이나 갤러리 작은 셀에서도 필요 이상의 이미지를 내려받을 수 있다. 원본은 라이트박스용으로 유지하고, 공개 본문 표시에는 720px와 1600px WebP를 `srcset`으로 함께 제공해 브라우저가 화면 폭에 맞는 파일을 고르게 한다. 기존 게시물은 백필 명령이 두 파생 이미지의 누락 여부를 함께 확인하도록 정리한다.
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
| server/routes/feed.xml.get.js | 공개 RSS 2.0 피드 별칭(`/feed.xml`) |
|
||||
| server/routes/rss.get.js | 공개 RSS 2.0 피드 별칭(`/rss`) |
|
||||
| server/plugins/site-custom-code.js | 공개 Nuxt HTML 응답에 사이트 설정 헤더·푸터 코드 삽입(`/admin`, `/api`, `/uploads`, `/_nuxt`, `/ads.txt` 제외) |
|
||||
| server/utils/post-thumbnail-image.js | 게시물 업로드 이미지의 카드 썸네일·본문 표시용 1600px/720px WebP 이미지 URL·디스크 경로·생성 가능 여부·본문 이미지 추출 규칙 |
|
||||
| server/utils/post-thumbnail-image.js | 게시물 업로드 이미지의 카드 썸네일·본문 표시용 1600px/720px WebP·라이트박스용 2560px WebP 이미지 URL·디스크 경로·생성 가능 여부·본문 이미지 추출 규칙 |
|
||||
| server/utils/sitemap.js | 공개 sitemap XML·robots.txt 생성 |
|
||||
| server/utils/rss-feed.js | 공개 발행글 기반 RSS 2.0 XML 생성, 게시물 이미지 Media RSS 썸네일 출력 |
|
||||
|
||||
@@ -151,7 +151,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, 단일 이미지·갤러리 통합 라이트박스, 갤러리 비율 기반 행 레이아웃, 라이브 이미지·갤러리 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 주석 감소 |
|
||||
|
||||
@@ -108,9 +108,10 @@
|
||||
- 관리자 미디어 업로드 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한다.
|
||||
- 공개 게시물 응답은 원본 대표 이미지 `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를 생성한다.
|
||||
- 기존 본문 이미지는 로컬에서 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서 `sh scripts/backfill-post-body-thumbnails-production.sh`로 전체 게시물 본문 URL을 스캔해 누락된 본문 표시용 WebP, 작은 본문 표시용 WebP, 라이트박스용 WebP를 생성한다.
|
||||
|
||||
### 공개 목록·상세의 발행일 표시
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
## 프론트엔드 개발
|
||||
|
||||
- 공개 게시물 본문 이미지 최적화 후속 진행.
|
||||
- 현재 게시물 저장 시 본문 표시용 WebP(`thumbs/*-body.webp`, 1600px)와 작은 본문 표시용 WebP(`thumbs/*-body-720.webp`, 720px)가 생성되고 공개 본문에서 `srcset`으로 우선 표시됨.
|
||||
- 라이트박스는 현재 원본 URL을 사용하므로, 초고용량 원본이 많은 경우 큰 파생 이미지와 원본 보기 옵션 분리 검토.
|
||||
- 업로드 총량 제한 및 메모리 사용량 개선.
|
||||
- 현재 관리자 업로드는 파일별 제한만 있고 한 번에 올리는 파일 개수·총 용량 제한이 없음.
|
||||
- 서버는 multipart 전체를 메모리에 받은 뒤 검사하므로, 고용량 다중 업로드 시 메모리 피크가 생길 수 있음.
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.10.8
|
||||
|
||||
- 게시물 본문 이미지 저장 시 라이트박스용 WebP(`thumbs/*-lightbox.webp`, 2560px) 생성 추가.
|
||||
- 공개 본문 라이트박스가 원본 대신 라이트박스용 파생 이미지를 우선 표시하도록 수정.
|
||||
- 라이트박스 원본 보기 UI는 추가하지 않고 파생 이미지 로드 실패 시에만 원본 fallback 유지.
|
||||
- 기존 본문 이미지 백필이 1600px·720px·라이트박스용 파생 이미지를 모두 확인하고 누락분을 생성하도록 수정.
|
||||
- 본문 이미지 최적화 후속 과제 완료로 `docs/todo.md`에서 제거.
|
||||
|
||||
## v1.10.7
|
||||
|
||||
- 게시물 본문 이미지 저장 시 720px 작은 본문 표시용 WebP(`thumbs/*-body-720.webp`) 생성 추가.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const postCardThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-card\.webp$/i
|
||||
const postBodyThumbnailUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-body(?:-720)?\.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
|
||||
|
||||
/**
|
||||
* 게시물 원본 이미지 URL에 대응하는 파생 이미지 URL을 만든다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
* @param {'body'|'body-720'} kind - 파생 이미지 종류
|
||||
* @param {'body'|'body-720'|'lightbox'} kind - 파생 이미지 종류
|
||||
* @returns {string} 파생 이미지 URL
|
||||
*/
|
||||
const getPostDerivativeImageUrl = (imageUrl, kind) => {
|
||||
@@ -55,3 +55,10 @@ export const getPostBodyThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(i
|
||||
* @returns {string} 작은 본문 표시용 파생 이미지 URL
|
||||
*/
|
||||
export const getPostBodyPreviewThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body-720')
|
||||
|
||||
/**
|
||||
* 게시물 원본 이미지 URL에 대응하는 라이트박스용 이미지 URL을 만든다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
* @returns {string} 라이트박스용 파생 이미지 URL
|
||||
*/
|
||||
export const getPostLightboxThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'lightbox')
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.7",
|
||||
"version": "1.10.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.7",
|
||||
"version": "1.10.8",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.7",
|
||||
"version": "1.10.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
extractPostImageUrlsFromContent,
|
||||
getExistingPostBodyPreviewThumbnailUrl,
|
||||
getExistingPostBodyThumbnailUrl,
|
||||
getExistingPostLightboxThumbnailUrl,
|
||||
getPostBodyThumbnailUrl
|
||||
} from '../server/utils/post-thumbnail-image.js'
|
||||
|
||||
@@ -88,7 +89,11 @@ let skippedCount = 0
|
||||
let failedCount = 0
|
||||
|
||||
for (const imageUrl of imageUrls) {
|
||||
if (getExistingPostBodyThumbnailUrl(imageUrl) && getExistingPostBodyPreviewThumbnailUrl(imageUrl)) {
|
||||
if (
|
||||
getExistingPostBodyThumbnailUrl(imageUrl)
|
||||
&& getExistingPostBodyPreviewThumbnailUrl(imageUrl)
|
||||
&& getExistingPostLightboxThumbnailUrl(imageUrl)
|
||||
) {
|
||||
skippedCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -5,17 +5,25 @@ import sharp from 'sharp'
|
||||
import {
|
||||
getPostBodyPreviewThumbnailUrl,
|
||||
getPostBodyThumbnailUrl,
|
||||
getPostLightboxThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
isPostCardThumbnailUrl
|
||||
} from '../../lib/post-card-thumbnail-url.js'
|
||||
|
||||
export { getPostBodyPreviewThumbnailUrl, getPostBodyThumbnailUrl, isPostBodyThumbnailUrl, isPostCardThumbnailUrl }
|
||||
export {
|
||||
getPostBodyPreviewThumbnailUrl,
|
||||
getPostBodyThumbnailUrl,
|
||||
getPostLightboxThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
isPostCardThumbnailUrl
|
||||
}
|
||||
|
||||
export const POST_THUMBNAIL_WIDTH = 640
|
||||
export const POST_THUMBNAIL_HEIGHT = 360
|
||||
export const POST_THUMBNAIL_QUALITY = 82
|
||||
export const POST_BODY_IMAGE_WIDTH = 1600
|
||||
export const POST_BODY_PREVIEW_IMAGE_WIDTH = 720
|
||||
export const POST_LIGHTBOX_IMAGE_WIDTH = 2560
|
||||
export const POST_BODY_IMAGE_QUALITY = 84
|
||||
|
||||
const postUploadUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/[^/?#]+$/i
|
||||
@@ -131,6 +139,21 @@ export const getPostBodyPreviewThumbnailDiskPath = (imageUrl) => {
|
||||
return join(process.cwd(), 'public', decodeUrlPathPart(thumbnailUrl.replace(/^\/+/, '')))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 라이트박스용 이미지 URL의 디스크 경로를 조회한다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
* @returns {string} 라이트박스용 이미지 디스크 경로
|
||||
*/
|
||||
export const getPostLightboxThumbnailDiskPath = (imageUrl) => {
|
||||
const thumbnailUrl = getPostLightboxThumbnailUrl(imageUrl)
|
||||
|
||||
if (!thumbnailUrl) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return join(process.cwd(), 'public', decodeUrlPathPart(thumbnailUrl.replace(/^\/+/, '')))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 원본 이미지 URL의 디스크 경로를 조회한다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
@@ -207,6 +230,21 @@ export const getExistingPostBodyPreviewThumbnailUrl = (imageUrl) => {
|
||||
return existsSync(getPostBodyPreviewThumbnailDiskPath(imageUrl)) ? thumbnailUrl : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미 생성된 게시물 라이트박스용 이미지 URL을 조회한다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
* @returns {string} 존재하는 라이트박스용 이미지 URL
|
||||
*/
|
||||
export const getExistingPostLightboxThumbnailUrl = (imageUrl) => {
|
||||
const thumbnailUrl = getPostLightboxThumbnailUrl(imageUrl)
|
||||
|
||||
if (!thumbnailUrl) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return existsSync(getPostLightboxThumbnailDiskPath(imageUrl)) ? thumbnailUrl : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 대표 이미지의 카드 썸네일을 생성하거나 다시 생성한다.
|
||||
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
||||
@@ -254,8 +292,9 @@ export const createPostBodyThumbnailForImageUrl = async (imageUrl) => {
|
||||
const thumbnailUrl = getPostBodyThumbnailUrl(imageUrl)
|
||||
const thumbnailPath = getPostBodyThumbnailDiskPath(imageUrl)
|
||||
const previewThumbnailPath = getPostBodyPreviewThumbnailDiskPath(imageUrl)
|
||||
const lightboxThumbnailPath = getPostLightboxThumbnailDiskPath(imageUrl)
|
||||
|
||||
if (!sourcePath || !thumbnailUrl || !thumbnailPath || !previewThumbnailPath) {
|
||||
if (!sourcePath || !thumbnailUrl || !thumbnailPath || !previewThumbnailPath || !lightboxThumbnailPath) {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -287,6 +326,16 @@ export const createPostBodyThumbnailForImageUrl = async (imageUrl) => {
|
||||
})
|
||||
.webp({ quality: POST_BODY_IMAGE_QUALITY })
|
||||
.toFile(previewThumbnailPath)
|
||||
|
||||
await sharp(sourcePath)
|
||||
.rotate()
|
||||
.resize({
|
||||
width: POST_LIGHTBOX_IMAGE_WIDTH,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({ quality: POST_BODY_IMAGE_QUALITY })
|
||||
.toFile(lightboxThumbnailPath)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user