- POST /auth/verify-password, PUT /auth/profile(세션 표시 이름 동기화) - MemberMapper.updateProfile, AuthSessionMapper.updateName - 단위테스트(24): CardNotificationParser(8), AuthService(16, Mockito) - CI(.gitea): 테스트 리포트 아티팩트 업로드(clean build가 곧 테스트 게이트) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -74,4 +74,22 @@ public class AuthController {
|
||||
authService.changePassword(current.getId(), req);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** 가입정보 변경 진입 전 비밀번호 재인증 */
|
||||
@PostMapping("/verify-password")
|
||||
public ResponseEntity<Void> verifyPassword(
|
||||
@Valid @RequestBody com.sb.web.auth.dto.PasswordVerifyRequest req,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
authService.verifyPassword(current.getId(), req.getPassword());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** 가입정보(이름/이메일) 변경 */
|
||||
@PutMapping("/profile")
|
||||
public MemberResponse updateProfile(
|
||||
@Valid @RequestBody com.sb.web.auth.dto.ProfileUpdateRequest req,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current,
|
||||
HttpServletRequest request) {
|
||||
return authService.updateProfile(current.getId(), AuthInterceptor.resolveToken(request), req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.sb.web.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 비밀번호 재인증 요청. 가입정보 변경 화면 진입 전 본인 확인용.
|
||||
*/
|
||||
@Data
|
||||
public class PasswordVerifyRequest {
|
||||
|
||||
@NotBlank(message = "비밀번호를 입력하세요.")
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sb.web.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 가입정보(프로필) 변경 요청. 비밀번호 인증을 통과한 뒤 호출된다.
|
||||
* 변경 대상: 이름, 이메일 (아이디/가입유형은 변경 불가).
|
||||
*/
|
||||
@Data
|
||||
public class ProfileUpdateRequest {
|
||||
|
||||
@NotBlank(message = "이름을 입력하세요.")
|
||||
@Size(max = 100, message = "이름은 100자 이하여야 합니다.")
|
||||
private String name;
|
||||
|
||||
@Email(message = "이메일 형식이 올바르지 않습니다.")
|
||||
@Size(max = 255, message = "이메일은 255자 이하여야 합니다.")
|
||||
private String email;
|
||||
}
|
||||
@@ -18,6 +18,9 @@ public interface AuthSessionMapper {
|
||||
|
||||
int updateExpiry(@Param("token") String token, @Param("expiresAt") LocalDateTime expiresAt);
|
||||
|
||||
/** 프로필 변경 시 백업 세션의 표시 이름 동기화 */
|
||||
int updateName(@Param("token") String token, @Param("name") String name);
|
||||
|
||||
int delete(@Param("token") String token);
|
||||
|
||||
int deleteExpired(@Param("now") LocalDateTime now);
|
||||
|
||||
@@ -28,6 +28,9 @@ public interface MemberMapper {
|
||||
|
||||
int updatePassword(@Param("id") Long id, @Param("password") String password);
|
||||
|
||||
/** 프로필(이름/이메일) 변경 */
|
||||
int updateProfile(@Param("id") Long id, @Param("name") String name, @Param("email") String email);
|
||||
|
||||
int updateRole(@Param("id") Long id, @Param("role") String role);
|
||||
|
||||
int updateStatus(@Param("id") Long id, @Param("status") String status);
|
||||
|
||||
@@ -186,6 +186,71 @@ public class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 재인증 (가입정보 변경 진입 전 본인 확인). 일치하지 않으면 401.
|
||||
*/
|
||||
public void verifyPassword(Long memberId, String password) {
|
||||
Member member = memberMapper.findById(memberId);
|
||||
if (member == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.");
|
||||
}
|
||||
if (!"LOCAL".equals(member.getProvider()) || member.getPassword() == null) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "소셜 로그인 계정은 비밀번호 인증을 사용할 수 없습니다.");
|
||||
}
|
||||
if (password == null || !passwordEncoder.matches(password, member.getPassword())) {
|
||||
throw new ApiException(HttpStatus.UNAUTHORIZED, "비밀번호가 올바르지 않습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 가입정보(이름/이메일) 변경. 비밀번호 인증을 통과한 화면에서 호출된다.
|
||||
* 변경 후 세션(Redis + DB 백업)의 표시 이름도 동기화한다.
|
||||
*/
|
||||
@Transactional
|
||||
public MemberResponse updateProfile(Long memberId, String token, com.sb.web.auth.dto.ProfileUpdateRequest req) {
|
||||
Member member = memberMapper.findById(memberId);
|
||||
if (member == null) {
|
||||
throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.");
|
||||
}
|
||||
String name = req.getName() == null ? null : req.getName().trim();
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "이름을 입력하세요.");
|
||||
}
|
||||
String email = (req.getEmail() == null || req.getEmail().isBlank()) ? null : req.getEmail().trim();
|
||||
memberMapper.updateProfile(memberId, name, email);
|
||||
member.setName(name);
|
||||
member.setEmail(email);
|
||||
syncSessionName(token, name);
|
||||
log.info("[profile] updated for {} (name={})", member.getLoginId(), name);
|
||||
return MemberResponse.from(member);
|
||||
}
|
||||
|
||||
/** 프로필 이름 변경 시 활성 세션(Redis + DB 백업)의 표시 이름을 맞춘다. */
|
||||
private void syncSessionName(String token, String name) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String key = SESSION_PREFIX + token;
|
||||
try {
|
||||
Object cached = redisTemplate.opsForValue().get(key);
|
||||
if (cached instanceof SessionUser u) {
|
||||
u.setName(name);
|
||||
Long ttl = redisTemplate.getExpire(key, java.util.concurrent.TimeUnit.SECONDS);
|
||||
Duration remain = (ttl != null && ttl > 0)
|
||||
? Duration.ofSeconds(ttl)
|
||||
: (u.isRememberMe() ? REMEMBER_TTL : SESSION_TTL);
|
||||
redisTemplate.opsForValue().set(key, u, remain);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[profile] Redis 세션 이름 동기화 실패(무시): {}", e.toString());
|
||||
}
|
||||
try {
|
||||
authSessionMapper.updateName(token, name);
|
||||
} catch (Exception ignore) {
|
||||
// DB 백업 동기화 실패해도 다음 로그인 시 갱신됨
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 (본인). 현재 비밀번호 검증 후 새 비밀번호로 교체.
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
UPDATE auth_session SET expires_at = #{expiresAt} WHERE token = #{token}
|
||||
</update>
|
||||
|
||||
<update id="updateName">
|
||||
UPDATE auth_session SET name = #{name} WHERE token = #{token}
|
||||
</update>
|
||||
|
||||
<delete id="delete">
|
||||
DELETE FROM auth_session WHERE token = #{token}
|
||||
</delete>
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
UPDATE member SET password = #{password}, updated_at = NOW() WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateProfile">
|
||||
UPDATE member SET name = #{name}, email = #{email}, updated_at = NOW() WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateRole">
|
||||
UPDATE member SET role = #{role}, updated_at = NOW() WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
Reference in New Issue
Block a user