feat: 가입정보 변경·비밀번호 재인증 API + 백엔드 단위테스트·CI 게이트

- 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:
ByungCheol
2026-06-06 14:08:01 +09:00
co-authored by Claude Opus 4.8
parent 5e2bdae37d
commit e592c174bf
11 changed files with 527 additions and 0 deletions
@@ -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 백업 동기화 실패해도 다음 로그인 시 갱신됨
}
}
/**
* 비밀번호 변경 (본인). 현재 비밀번호 검증 후 새 비밀번호로 교체.
*/