diff --git a/docs/history.md b/docs/history.md index 6cbd9cd..3767653 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,11 @@ # 의사결정 이력 +## 2026-05-11 v0.0.76 + +### 회원 썸네일 중앙 1:1 강제 크롭 + +회원이 세로형/가로형 이미지를 올려도 헤더와 설정 화면의 아바타는 동일한 비율이어야 UI가 안정적이므로, 업로드 시점에 중앙 기준으로 1:1 정사각형 크롭을 강제했다. 이렇게 하면 클라이언트별 개별 크롭 로직 없이 서버 저장본 자체가 일관된 아바타 규격을 가진다. + ## 2026-05-11 v0.0.75 ### 회원 썸네일 최소 해상도/설정 방어 강화 diff --git a/docs/map.md b/docs/map.md index 5a4744b..c245a2e 100644 --- a/docs/map.md +++ b/docs/map.md @@ -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 변환, 최소 해상도 검증, 중앙 1:1 강제 크롭, 품질 보정) | | server/api/auth/avatar.delete.js | 회원 썸네일 삭제 API | | server/api/auth/check-username.get.js | 닉네임 중복 확인 API | | server/api/auth/password.put.js | 회원 비밀번호 변경 API | diff --git a/docs/spec.md b/docs/spec.md index 7edc04b..b377354 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -345,7 +345,8 @@ components/content/ - `PUT /api/auth/password` - 회원 비밀번호 변경 - `DELETE /api/auth/account` - 회원 탈퇴 -> 회원 썸네일 이미지는 `/uploads/members/avatars/YYYY/MM` 경로로 저장하며, 업로드 시 WebP로 변환하고 `AVATAR_MIN_WIDTH`/`AVATAR_MIN_HEIGHT` 최소 해상도 검증 후 `AVATAR_MAX_WIDTH`/`AVATAR_MAX_HEIGHT` 기준으로 자동 리사이즈한다. +> 회원 썸네일 이미지는 `/uploads/members/avatars/YYYY/MM` 경로로 저장하며, 업로드 시 WebP로 변환하고 `AVATAR_MIN_WIDTH`/`AVATAR_MIN_HEIGHT` 최소 해상도 검증 후 중앙 기준 1:1 정사각형으로 크롭한다. +> 최종 저장 크기는 `AVATAR_MAX_WIDTH`, `AVATAR_MAX_HEIGHT` 중 작은 값을 사용해 `N x N` 정사각형으로 맞춘다. > `AVATAR_WEBP_QUALITY`는 1~100 범위로 보정하며, 최대 해상도 설정이 최소 해상도보다 작으면 서버에서 최소값 이상으로 자동 보정한다. > 회원 썸네일을 새로 업로드하거나 제거/탈퇴할 때 기존 회원 썸네일 파일과 메타데이터는 자동 정리한다. @@ -588,6 +589,6 @@ APP_PORT=43118 ## 버전 관리 -- 현재 버전: v0.0.75 +- 현재 버전: v0.0.76 - 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가 - 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정 diff --git a/docs/update.md b/docs/update.md index ff3fa18..0b66926 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v0.0.76 + +- 회원 썸네일 업로드 시 원본 비율과 관계없이 중앙 기준 1:1 정사각형으로 강제 크롭하도록 변경. +- 크롭 결과 해상도는 `AVATAR_MAX_WIDTH`, `AVATAR_MAX_HEIGHT` 중 작은 값을 사용해 정사각형(`N x N`)으로 저장하도록 정리. + ## v0.0.75 - 회원 썸네일 업로드 시 최소 해상도 제한(`AVATAR_MIN_WIDTH`, `AVATAR_MIN_HEIGHT`)을 추가해 너무 작은 이미지를 차단. diff --git a/package.json b/package.json index 5c312cc..d06f6dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "0.0.75", + "version": "0.0.76", "private": true, "type": "module", "imports": { diff --git a/server/api/auth/avatar.post.js b/server/api/auth/avatar.post.js index 08406cc..46a4d30 100644 --- a/server/api/auth/avatar.post.js +++ b/server/api/auth/avatar.post.js @@ -69,6 +69,7 @@ export default defineEventHandler(async (event) => { 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 avatarSquareSize = Math.min(avatarMaxWidth, avatarMaxHeight) const avatarWebpQuality = clampNumber(Number(config.avatarWebpQuality || 82), 1, 100) const formData = await readMultipartFormData(event) const file = (formData || []).find((part) => part.name === 'file' && part.filename) @@ -126,10 +127,10 @@ export default defineEventHandler(async (event) => { const resizedBuffer = await sharp(file.data) .rotate() .resize({ - width: avatarMaxWidth, - height: avatarMaxHeight, - fit: 'inside', - withoutEnlargement: true + width: avatarSquareSize, + height: avatarSquareSize, + fit: 'cover', + position: 'centre' }) .webp({ quality: avatarWebpQuality