feat(member): 회원 썸네일 업로드를 WebP 리사이즈로 표준화
회원 아바타 업로드 시 원본 포맷을 WebP로 변환하고 최대 해상도/품질을 환경변수로 제어해 저장 용량과 전송 비용을 줄인다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { extname, join } from 'node:path'
|
||||
import { join } from 'node:path'
|
||||
import { createError, readMultipartFormData } from 'h3'
|
||||
import sharp from 'sharp'
|
||||
import { getPostgresClient } from '../../repositories/postgres-client'
|
||||
import { updateMemberProfile, getUserById } from '../../repositories/member-repository'
|
||||
import { requireMemberSession } from '../../utils/member-auth'
|
||||
@@ -24,21 +25,6 @@ const sanitizePathPart = (value) => value
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
|
||||
/**
|
||||
* 파일 확장자 조회
|
||||
* @param {Object} file - multipart 파일 파트
|
||||
* @returns {string} 확장자
|
||||
*/
|
||||
const getUploadExtension = (file) => {
|
||||
const extension = extname(file.filename || '').toLowerCase()
|
||||
|
||||
if (allowedImageTypes.has(file.type)) {
|
||||
return allowedImageTypes.get(file.type)
|
||||
}
|
||||
|
||||
return extension
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 썸네일 업로드 API
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
@@ -56,6 +42,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const maxFileSize = Number(config.maxFileSize || 10485760)
|
||||
const avatarMaxWidth = Number(config.avatarMaxWidth || 512)
|
||||
const avatarMaxHeight = Number(config.avatarMaxHeight || 512)
|
||||
const avatarWebpQuality = Number(config.avatarWebpQuality || 82)
|
||||
const formData = await readMultipartFormData(event)
|
||||
const file = (formData || []).find((part) => part.name === 'file' && part.filename)
|
||||
|
||||
@@ -90,12 +79,32 @@ export default defineEventHandler(async (event) => {
|
||||
await mkdir(directoryPath, { recursive: true })
|
||||
|
||||
const originalName = sanitizePathPart(file.filename.replace(/\.[^.]+$/g, '')) || 'avatar'
|
||||
const extension = getUploadExtension(file)
|
||||
const fileName = `${originalName}-${randomUUID()}${extension}`
|
||||
const fileName = `${originalName}-${randomUUID()}.webp`
|
||||
const filePath = join(directoryPath, fileName)
|
||||
const avatarUrl = `${uploadBaseUrl}/members/avatars/${year}/${month}/${fileName}`
|
||||
const metadata = await sharp(file.data).metadata()
|
||||
|
||||
await writeFile(filePath, file.data)
|
||||
if (!metadata.width || !metadata.height) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '이미지 메타데이터를 읽을 수 없습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
const resizedBuffer = await sharp(file.data)
|
||||
.rotate()
|
||||
.resize({
|
||||
width: avatarMaxWidth,
|
||||
height: avatarMaxHeight,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({
|
||||
quality: avatarWebpQuality
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
await writeFile(filePath, resizedBuffer)
|
||||
|
||||
await updateMemberProfile({
|
||||
userId: session.userId,
|
||||
|
||||
Reference in New Issue
Block a user