feat(member): 회원 썸네일 최소 해상도와 설정 보정 추가
아바타 업로드 시 최소 해상도 조건을 검증하고 리사이즈/품질 설정값을 안전 범위로 보정해 운영 설정 오입력에도 안정적으로 동작하도록 개선한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,6 +14,8 @@ MEMBER_SESSION_SECRET=replace-with-random-password
|
||||
# Upload
|
||||
UPLOAD_DIR=/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
AVATAR_MIN_WIDTH=96
|
||||
AVATAR_MIN_HEIGHT=96
|
||||
AVATAR_MAX_WIDTH=512
|
||||
AVATAR_MAX_HEIGHT=512
|
||||
AVATAR_WEBP_QUALITY=82
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-05-11 v0.0.75
|
||||
|
||||
### 회원 썸네일 최소 해상도/설정 방어 강화
|
||||
|
||||
회원 썸네일은 너무 작은 원본이 올라오면 헤더/설정 화면에서 품질 저하가 크게 보이므로 최소 해상도 제한을 추가했다. 또한 운영 환경 변수 오입력으로 리사이즈/품질 값이 비정상이어도 업로드가 깨지지 않도록 서버에서 값 범위를 보정(clamp)해 안정성을 높였다.
|
||||
|
||||
## 2026-05-11 v0.0.74
|
||||
|
||||
### 회원 썸네일 업로드 표준화(WebP + 리사이즈)
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
| server/api/auth/logout.post.js | 회원 로그아웃 API |
|
||||
| server/api/auth/profile.get.js | 회원 프로필 조회 API |
|
||||
| server/api/auth/profile.put.js | 회원 프로필 수정 API |
|
||||
| server/api/auth/avatar.post.js | 회원 썸네일 업로드 API(WebP 변환, 최대 해상도 리사이즈) |
|
||||
| server/api/auth/avatar.post.js | 회원 썸네일 업로드 API(WebP 변환, 최소 해상도 검증, 최대 해상도 리사이즈, 품질 보정) |
|
||||
| server/api/auth/avatar.delete.js | 회원 썸네일 삭제 API |
|
||||
| server/api/auth/check-username.get.js | 닉네임 중복 확인 API |
|
||||
| server/api/auth/password.put.js | 회원 비밀번호 변경 API |
|
||||
@@ -198,7 +198,7 @@
|
||||
| 파일 | 기능 |
|
||||
|------|------|
|
||||
| package.json | Nuxt 실행 스크립트와 의존성 |
|
||||
| nuxt.config.js | Nuxt 앱 설정, `tailwindcss.cssPath`로 `main.css` 단일 엔트리, Tailwind 모듈, 관리자 QA를 위한 개발 도구 비활성화, 회원 썸네일 리사이즈/품질 런타임 설정 |
|
||||
| nuxt.config.js | Nuxt 앱 설정, `tailwindcss.cssPath`로 `main.css` 단일 엔트리, Tailwind 모듈, 관리자 QA를 위한 개발 도구 비활성화, 회원 썸네일 최소/최대 해상도·품질 런타임 설정 |
|
||||
| tailwind.config.js | Tailwind 테마 설정 |
|
||||
| assets/css/main.css | 전역 스타일, `#fcfcfc` 단일 배경 기준, 좌측 사이드바/그리드 전환 애니메이션, 네비게이션 세로 바 hover 효과 |
|
||||
| composables/useMenuState.js | 좌측 메뉴 열림 상태·`closeMenu`(모바일 백드롭 등) |
|
||||
|
||||
@@ -345,7 +345,8 @@ components/content/
|
||||
- `PUT /api/auth/password` - 회원 비밀번호 변경
|
||||
- `DELETE /api/auth/account` - 회원 탈퇴
|
||||
|
||||
> 회원 썸네일 이미지는 `/uploads/members/avatars/YYYY/MM` 경로로 저장하며, 업로드 시 WebP로 변환하고 `AVATAR_MAX_WIDTH`/`AVATAR_MAX_HEIGHT` 기준으로 자동 리사이즈한다.
|
||||
> 회원 썸네일 이미지는 `/uploads/members/avatars/YYYY/MM` 경로로 저장하며, 업로드 시 WebP로 변환하고 `AVATAR_MIN_WIDTH`/`AVATAR_MIN_HEIGHT` 최소 해상도 검증 후 `AVATAR_MAX_WIDTH`/`AVATAR_MAX_HEIGHT` 기준으로 자동 리사이즈한다.
|
||||
> `AVATAR_WEBP_QUALITY`는 1~100 범위로 보정하며, 최대 해상도 설정이 최소 해상도보다 작으면 서버에서 최소값 이상으로 자동 보정한다.
|
||||
> 회원 썸네일을 새로 업로드하거나 제거/탈퇴할 때 기존 회원 썸네일 파일과 메타데이터는 자동 정리한다.
|
||||
|
||||
### 관리자 API (`/admin/api/`)
|
||||
@@ -546,6 +547,8 @@ MEMBER_SESSION_SECRET=replace-with-random-password
|
||||
# Upload
|
||||
UPLOAD_DIR=/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
AVATAR_MIN_WIDTH=96
|
||||
AVATAR_MIN_HEIGHT=96
|
||||
AVATAR_MAX_WIDTH=512
|
||||
AVATAR_MAX_HEIGHT=512
|
||||
AVATAR_WEBP_QUALITY=82
|
||||
@@ -585,6 +588,6 @@ APP_PORT=43118
|
||||
|
||||
## 버전 관리
|
||||
|
||||
- 현재 버전: v0.0.74
|
||||
- 현재 버전: v0.0.75
|
||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v0.0.75
|
||||
|
||||
- 회원 썸네일 업로드 시 최소 해상도 제한(`AVATAR_MIN_WIDTH`, `AVATAR_MIN_HEIGHT`)을 추가해 너무 작은 이미지를 차단.
|
||||
- 썸네일 품질과 리사이즈 설정값이 비정상일 때 서버에서 안전 범위로 보정(clamp)하도록 방어 로직을 추가.
|
||||
- 최대 해상도 설정이 최소 해상도보다 작게 들어와도 자동 보정되도록 처리.
|
||||
|
||||
## v0.0.74
|
||||
|
||||
- 회원 썸네일 업로드 시 원본 포맷과 관계없이 WebP로 변환 저장하도록 수정.
|
||||
|
||||
@@ -53,6 +53,8 @@ export default defineNuxtConfig({
|
||||
memberSessionSecret: process.env.MEMBER_SESSION_SECRET || '',
|
||||
uploadDir: process.env.UPLOAD_DIR || '/uploads',
|
||||
maxFileSize: Number(process.env.MAX_FILE_SIZE || 10485760),
|
||||
avatarMinWidth: Number(process.env.AVATAR_MIN_WIDTH || 96),
|
||||
avatarMinHeight: Number(process.env.AVATAR_MIN_HEIGHT || 96),
|
||||
avatarMaxWidth: Number(process.env.AVATAR_MAX_WIDTH || 512),
|
||||
avatarMaxHeight: Number(process.env.AVATAR_MAX_HEIGHT || 512),
|
||||
avatarWebpQuality: Number(process.env.AVATAR_WEBP_QUALITY || 82),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "0.0.74",
|
||||
"version": "0.0.75",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -25,6 +25,29 @@ const sanitizePathPart = (value) => value
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
|
||||
/**
|
||||
* 숫자 설정값을 최소/최대 범위로 보정한다.
|
||||
* @param {number} value - 원본 값
|
||||
* @param {number} minimum - 최소값
|
||||
* @param {number} maximum - 최대값
|
||||
* @returns {number} 보정된 값
|
||||
*/
|
||||
const clampNumber = (value, minimum, maximum) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return minimum
|
||||
}
|
||||
|
||||
if (value < minimum) {
|
||||
return minimum
|
||||
}
|
||||
|
||||
if (value > maximum) {
|
||||
return maximum
|
||||
}
|
||||
|
||||
return Math.round(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 썸네일 업로드 API
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
@@ -42,9 +65,11 @@ 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 avatarMinWidth = clampNumber(Number(config.avatarMinWidth || 96), 1, 4096)
|
||||
const avatarMinHeight = clampNumber(Number(config.avatarMinHeight || 96), 1, 4096)
|
||||
const avatarMaxWidth = clampNumber(Number(config.avatarMaxWidth || 512), avatarMinWidth, 4096)
|
||||
const avatarMaxHeight = clampNumber(Number(config.avatarMaxHeight || 512), avatarMinHeight, 4096)
|
||||
const avatarWebpQuality = clampNumber(Number(config.avatarWebpQuality || 82), 1, 100)
|
||||
const formData = await readMultipartFormData(event)
|
||||
const file = (formData || []).find((part) => part.name === 'file' && part.filename)
|
||||
|
||||
@@ -91,6 +116,13 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (metadata.width < avatarMinWidth || metadata.height < avatarMinHeight) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `최소 ${avatarMinWidth}x${avatarMinHeight} 이상 이미지만 업로드할 수 있습니다.`
|
||||
})
|
||||
}
|
||||
|
||||
const resizedBuffer = await sharp(file.data)
|
||||
.rotate()
|
||||
.resize({
|
||||
|
||||
Reference in New Issue
Block a user