세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
5.8 KiB
JavaScript
141 lines
5.8 KiB
JavaScript
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|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'|'body-1600'|'body-2560'|'lightbox'} kind - 파생 이미지 종류
|
|
* @returns {string} 파생 이미지 URL
|
|
*/
|
|
const getPostDerivativeImageUrl = (imageUrl, kind) => {
|
|
const normalizedUrl = String(imageUrl || '')
|
|
|
|
if (!postUploadImageUrlPattern.test(normalizedUrl)) {
|
|
return ''
|
|
}
|
|
|
|
const slashIndex = normalizedUrl.lastIndexOf('/')
|
|
const directory = slashIndex >= 0 ? normalizedUrl.slice(0, slashIndex) : ''
|
|
const fileName = slashIndex >= 0 ? normalizedUrl.slice(slashIndex + 1) : normalizedUrl
|
|
const dotIndex = fileName.lastIndexOf('.')
|
|
const stem = dotIndex >= 0 ? fileName.slice(0, dotIndex) : fileName
|
|
|
|
return `${directory}/thumbs/${stem}-${kind}.webp`
|
|
}
|
|
|
|
/**
|
|
* 게시물 카드 썸네일 URL인지 확인한다.
|
|
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
|
|
* @returns {boolean} 카드 썸네일 여부
|
|
*/
|
|
export const isPostCardThumbnailUrl = (imageUrl) => Boolean(
|
|
imageUrl && postCardThumbnailUrlPattern.test(String(imageUrl))
|
|
)
|
|
|
|
/**
|
|
* 페이지 커버 썸네일 URL인지 확인한다.
|
|
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
|
|
* @returns {boolean} 페이지 커버 썸네일 여부
|
|
*/
|
|
export const isPageCoverThumbnailUrl = (imageUrl) => Boolean(
|
|
imageUrl && pageCoverThumbnailUrlPattern.test(String(imageUrl))
|
|
)
|
|
|
|
/**
|
|
* 게시물 본문 표시용 파생 이미지 URL인지 확인한다.
|
|
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
|
|
* @returns {boolean} 본문 표시용 파생 이미지 여부
|
|
*/
|
|
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
|
|
* @returns {string} 본문 표시용 파생 이미지 URL
|
|
*/
|
|
export const getPostBodyThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body-1600')
|
|
|
|
/**
|
|
* 게시물 원본 이미지 URL에 대응하는 작은 본문 표시용 이미지 URL을 만든다.
|
|
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
|
* @returns {string} 작은 본문 표시용 파생 이미지 URL
|
|
*/
|
|
export const getPostBodyPreviewThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'body-720')
|
|
|
|
/**
|
|
* 본문 1600 파생 이미지 URL인지 확인한다.
|
|
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
|
|
* @returns {boolean} body-1600이면 true
|
|
*/
|
|
export const isPostBody1600ThumbnailUrl = (imageUrl) => /\/thumbs\/[^/?#]+-body-1600\.webp$/i.test(String(imageUrl || ''))
|
|
|
|
/**
|
|
* 본문 720 파생 이미지 URL인지 확인한다.
|
|
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
|
|
* @returns {boolean} body-720이면 true
|
|
*/
|
|
export const isPostBody720ThumbnailUrl = (imageUrl) => /\/thumbs\/[^/?#]+-body-720\.webp$/i.test(String(imageUrl || ''))
|
|
|
|
/**
|
|
* 게시물 원본 이미지 URL에 대응하는 라이트박스용 이미지 URL을 만든다.
|
|
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
|
* @returns {string} 라이트박스용 파생 이미지 URL
|
|
*/
|
|
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을 만든다.
|
|
* @param {string|null|undefined} imageUrl - 원본 이미지 URL
|
|
* @returns {string} 페이지 커버 썸네일 URL
|
|
*/
|
|
export const getPageCoverThumbnailUrl = (imageUrl) => getPostDerivativeImageUrl(imageUrl, 'page-cover')
|