feat: 프로필 사진 — 구글 아바타 동기화 + 사용자 지정 이미지
- member.google_picture(구글 아바타 URL), profile_image(사용자 지정 data URL) 컬럼 추가(멱등) - 구글 로그인 시 picture 클레임 추출 → 신규 저장, 기존은 변경 시 동기화 - PUT /api/auth/profile-image: 사용자 지정 사진 설정/해제(빈값=해제→구글 폴백), data URL·용량 검증 - MemberResponse 에 googlePicture/profileImage 노출(표시 우선순위 프론트 처리) - 관리자 목록(findAll)은 무거운 profile_image 제외해 가볍게 조회 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6256d3e63d
commit
98f5f51112
@@ -186,6 +186,7 @@ public class AuthService {
|
||||
boolean emailVerified = "true".equals(String.valueOf(info.get("email_verified")));
|
||||
String name = info.get("name") != null ? String.valueOf(info.get("name"))
|
||||
: (email != null ? email.split("@")[0] : "사용자");
|
||||
String picture = info.get("picture") != null ? String.valueOf(info.get("picture")) : null;
|
||||
|
||||
// 1) 구글 sub 로 이미 연결/가입된 계정 조회
|
||||
Member member = memberMapper.findByGoogleId(sub);
|
||||
@@ -216,6 +217,7 @@ public class AuthService {
|
||||
.provider("GOOGLE")
|
||||
.providerId(sub)
|
||||
.googleId(sub)
|
||||
.googlePicture(picture)
|
||||
.role("USER")
|
||||
.status("ACTIVE")
|
||||
.build();
|
||||
@@ -225,6 +227,11 @@ public class AuthService {
|
||||
if (!"ACTIVE".equals(member.getStatus())) {
|
||||
throw new ApiException(HttpStatus.FORBIDDEN, "사용할 수 없는 계정입니다.");
|
||||
}
|
||||
// 구글 아바타가 바뀌었으면 동기화(신규 가입은 이미 반영됨 → 변경 없을 때 불필요한 UPDATE 생략)
|
||||
if (picture != null && !picture.equals(member.getGooglePicture())) {
|
||||
memberMapper.updateGooglePicture(member.getId(), picture);
|
||||
member.setGooglePicture(picture);
|
||||
}
|
||||
return issueSession(member, rememberMe);
|
||||
}
|
||||
|
||||
@@ -335,6 +342,31 @@ public class AuthService {
|
||||
return MemberResponse.from(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 사진(사용자 지정) 설정/해제. null/빈 값이면 해제 → 구글 사진으로 폴백.
|
||||
* data URL(base64 이미지)만 허용하고 과도한 크기는 거절한다.
|
||||
*/
|
||||
@Transactional
|
||||
public MemberResponse updateProfileImage(Long memberId, String image) {
|
||||
Member member = memberMapper.findById(memberId);
|
||||
if (member == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.");
|
||||
}
|
||||
String value = (image == null || image.isBlank()) ? null : image.trim();
|
||||
if (value != null) {
|
||||
if (!value.startsWith("data:image/")) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "이미지 형식이 올바르지 않습니다.");
|
||||
}
|
||||
if (value.length() > 700_000) { // 약 500KB 이미지 상한 (base64 약 33% 팽창 고려)
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "이미지 용량이 너무 큽니다. 더 작은 사진을 사용하세요.");
|
||||
}
|
||||
}
|
||||
memberMapper.updateProfileImage(memberId, value);
|
||||
member.setProfileImage(value);
|
||||
log.info("[profile] image {} for {}", value == null ? "cleared" : "updated", member.getLoginId());
|
||||
return MemberResponse.from(member);
|
||||
}
|
||||
|
||||
/** 프로필 이름 변경 시 활성 세션(Redis + DB 백업)의 표시 이름을 맞춘다. */
|
||||
private void syncSessionName(String token, String name) {
|
||||
if (token == null || token.isBlank()) {
|
||||
|
||||
Reference in New Issue
Block a user