본문 이미지 최적화 및 백필 보정

This commit is contained in:
2026-07-01 17:35:19 +09:00
parent 943794ae34
commit 78ab394f7a
18 changed files with 437 additions and 20 deletions

View File

@@ -5,7 +5,7 @@ import {
isImageUrl,
parseImageMarkdownLine
} from '../../lib/markdown-image.js'
import { getPostBodyThumbnailUrl } from '../../lib/post-card-thumbnail-url.js'
import { getPostBodyPreviewThumbnailUrl, getPostBodyThumbnailUrl } from '../../lib/post-card-thumbnail-url.js'
import { readEditableTextFromElement, setEditableCaretOffset } from '../../lib/markdown-inline.js'
import {
applyLiveSelectionDelete,
@@ -1057,6 +1057,32 @@ const contentLightboxImages = computed(() => {
*/
const getContentDisplayImageUrl = (imageUrl) => getPostBodyThumbnailUrl(imageUrl) || imageUrl || ''
/**
* 게시물 본문 이미지의 반응형 srcset을 만든다.
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
* @returns {string} srcset 문자열
*/
const getContentDisplayImageSrcset = (imageUrl) => {
const previewUrl = getPostBodyPreviewThumbnailUrl(imageUrl)
const bodyUrl = getPostBodyThumbnailUrl(imageUrl)
return [
previewUrl ? `${previewUrl} 720w` : '',
bodyUrl ? `${bodyUrl} 1600w` : ''
].filter(Boolean).join(', ')
}
/**
* 갤러리 표시용 이미지 sizes 값을 만든다.
* @param {number} rowLength - 갤러리 행 이미지 개수
* @returns {string} sizes 속성
*/
const getGalleryImageSizes = (rowLength) => {
const columnWidth = rowLength >= 3 ? '33vw' : rowLength === 2 ? '50vw' : '100vw'
return `(max-width: 768px) 100vw, ${columnWidth}`
}
/**
* 갤러리 이미지를 행 단위로 최대 3개씩 나눈다.
* @param {Array<Object>} images - 갤러리 이미지 목록
@@ -1118,6 +1144,8 @@ const onGalleryImageError = (event, fallbackUrl) => {
return
}
image.removeAttribute('srcset')
image.removeAttribute('sizes')
image.src = fallbackUrl
}
@@ -3233,6 +3261,7 @@ onBeforeUnmount(() => {
</div>
<ProseImage
:src="getContentDisplayImageUrl(block.url)"
:srcset="getContentDisplayImageSrcset(block.url)"
:fallback-src="block.url"
:alt="getImageAltAttribute(block)"
:caption="getImageDisplayCaption(block)"
@@ -3244,6 +3273,7 @@ onBeforeUnmount(() => {
<ProseImage
v-else-if="block.type === 'image'"
:src="getContentDisplayImageUrl(block.url)"
:srcset="getContentDisplayImageSrcset(block.url)"
:fallback-src="block.url"
:alt="getImageAltAttribute(block)"
:caption="getImageDisplayCaption(block)"
@@ -3447,6 +3477,8 @@ onBeforeUnmount(() => {
<img
class="content-markdown-renderer__gallery-image h-full w-full object-cover transition-transform hover:scale-[1.02]"
:src="getContentDisplayImageUrl(image.url)"
:srcset="getContentDisplayImageSrcset(image.url) || undefined"
:sizes="getContentDisplayImageSrcset(image.url) ? getGalleryImageSizes(row.images.length) : undefined"
:alt="getImageAltAttribute(image)"
loading="lazy"
decoding="async"

View File

@@ -10,6 +10,14 @@ const props = defineProps({
type: String,
default: ''
},
srcset: {
type: String,
default: ''
},
sizes: {
type: String,
default: '(max-width: 768px) 100vw, 760px'
},
alt: {
type: String,
default: ''
@@ -35,6 +43,7 @@ const loadFailed = ref(false)
const currentSrc = ref(props.src)
const hasRenderableSrc = computed(() => String(currentSrc.value || '').trim().length > 0)
const currentSrcset = computed(() => currentSrc.value === props.src ? props.srcset : '')
const errorLabel = computed(() => {
const trimmed = String(props.fallbackSrc || props.src || '').trim()
@@ -111,6 +120,8 @@ const activateImage = () => {
v-if="hasRenderableSrc && !loadFailed"
class="prose-image__media w-full object-cover"
:src="currentSrc"
:srcset="currentSrcset || undefined"
:sizes="currentSrcset ? sizes : undefined"
:alt="alt"
loading="lazy"
decoding="async"