import { existsSync } from 'node:fs' import { dirname, extname, join, posix } from 'node:path' export const POST_THUMBNAIL_WIDTH = 640 export const POST_THUMBNAIL_HEIGHT = 360 export const POST_THUMBNAIL_QUALITY = 82 const postUploadUrlPattern = /^\/uploads\/posts\/\d{4}\/\d{2}\/[^?#]+$/i const postThumbnailDirectoryName = 'thumbs' const postThumbnailSuffix = '-card' const postThumbnailExtension = '.webp' const supportedPostThumbnailExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp']) const supportedPostThumbnailTypes = new Set(['image/jpeg', 'image/png', 'image/webp']) /** * URL 경로 조각을 파일 시스템 경로용 문자열로 디코딩한다. * @param {string} value - URL 경로 조각 * @returns {string} 디코딩된 값 */ const decodeUrlPathPart = (value) => { try { return decodeURIComponent(value) } catch { return value } } /** * 게시물 이미지에서 카드 썸네일을 생성할 수 있는지 확인한다. * @param {string} mimeType - 파일 MIME 타입 * @param {string} fileName - 파일명 * @returns {boolean} 썸네일 생성 가능 여부 */ export const isPostThumbnailSource = (mimeType, fileName = '') => { if (supportedPostThumbnailTypes.has(mimeType)) { return true } return supportedPostThumbnailExtensions.has(extname(fileName).toLowerCase()) } /** * 원본 파일명에서 게시물 카드 썸네일 파일명을 만든다. * @param {string} fileName - 원본 파일명 * @returns {string} 썸네일 파일명 */ export const getPostThumbnailFileName = (fileName) => { const extension = extname(fileName) const stem = extension ? fileName.slice(0, -extension.length) : fileName return `${stem}${postThumbnailSuffix}${postThumbnailExtension}` } /** * 게시물 원본 이미지 URL에 대응하는 카드 썸네일 URL을 만든다. * @param {string|null|undefined} imageUrl - 원본 이미지 URL * @returns {string} 카드 썸네일 URL */ export const getPostThumbnailUrl = (imageUrl) => { if (!imageUrl || !postUploadUrlPattern.test(imageUrl)) { return '' } const directory = posix.dirname(imageUrl) const fileName = posix.basename(imageUrl) const extension = extname(fileName).toLowerCase() if (!supportedPostThumbnailExtensions.has(extension)) { return '' } return `${directory}/${postThumbnailDirectoryName}/${getPostThumbnailFileName(fileName)}` } /** * 게시물 카드 썸네일 URL의 디스크 경로를 조회한다. * @param {string} imageUrl - 원본 이미지 URL * @returns {string} 썸네일 디스크 경로 */ export const getPostThumbnailDiskPath = (imageUrl) => { const thumbnailUrl = getPostThumbnailUrl(imageUrl) if (!thumbnailUrl) { return '' } return join(process.cwd(), 'public', decodeUrlPathPart(thumbnailUrl.replace(/^\/+/, ''))) } /** * 게시물 원본 이미지 파일의 썸네일 저장 디렉터리 경로를 조회한다. * @param {string} imageFilePath - 원본 이미지 디스크 경로 * @returns {string} 썸네일 저장 디렉터리 경로 */ export const getPostThumbnailDirectoryPath = (imageFilePath) => join(dirname(imageFilePath), postThumbnailDirectoryName) /** * 게시물 원본 이미지 파일의 썸네일 저장 경로를 조회한다. * @param {string} imageFilePath - 원본 이미지 디스크 경로 * @returns {string} 썸네일 저장 경로 */ export const getPostThumbnailPathForFile = (imageFilePath) => { const fileName = imageFilePath.split(/[\\/]/).pop() || '' return join(getPostThumbnailDirectoryPath(imageFilePath), getPostThumbnailFileName(fileName)) } /** * 이미 생성된 게시물 카드 썸네일 URL을 조회한다. * @param {string|null|undefined} imageUrl - 원본 이미지 URL * @returns {string} 존재하는 썸네일 URL */ export const getExistingPostThumbnailUrl = (imageUrl) => { const thumbnailUrl = getPostThumbnailUrl(imageUrl) if (!thumbnailUrl) { return '' } return existsSync(getPostThumbnailDiskPath(imageUrl)) ? thumbnailUrl : '' }