From d1880fa2f6eb896121afbf0d377ec51d8b3ce323 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:06:43 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Feat:=20=EC=95=84=EC=9D=B4=EB=94=94=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 15 ++++++- .../user/controller/AuthControllerDocs.java | 2 +- ...tionRequest.java => sendEmailRequest.java} | 2 +- .../back/domain/user/service/AuthService.java | 45 +++++++++++++++++++ .../domain/user/service/EmailService.java | 15 +++++++ 5 files changed, 75 insertions(+), 4 deletions(-) rename src/main/java/com/back/domain/user/dto/{ResendVerificationRequest.java => sendEmailRequest.java} (80%) diff --git a/src/main/java/com/back/domain/user/controller/AuthController.java b/src/main/java/com/back/domain/user/controller/AuthController.java index a05220dd..21a0e09e 100644 --- a/src/main/java/com/back/domain/user/controller/AuthController.java +++ b/src/main/java/com/back/domain/user/controller/AuthController.java @@ -9,7 +9,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Map; @@ -50,7 +49,7 @@ public ResponseEntity> verifyEmail( // 인증 메일 재발송 @PostMapping("/email/verify") public ResponseEntity> resendVerificationEmail( - @Valid @RequestBody ResendVerificationRequest request + @Valid @RequestBody sendEmailRequest request ) { authService.resendVerificationEmail(request.email()); return ResponseEntity @@ -100,4 +99,16 @@ public ResponseEntity>> refreshToken( Map.of("accessToken", newAccessToken) )); } + + @PostMapping("/username/recover") + public ResponseEntity> recoverUsername( + @Valid @RequestBody sendEmailRequest request + ) { + authService.recoverUsername(request.email()); + return ResponseEntity + .ok(RsData.success( + "아이디를 이메일로 전송했습니다.", + null + )); + } } diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index 550f188e..0ee3b743 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -305,7 +305,7 @@ ResponseEntity> verifyEmail( ) }) ResponseEntity> resendVerificationEmail( - @Valid @RequestBody ResendVerificationRequest request + @Valid @RequestBody sendEmailRequest request ); @Operation( diff --git a/src/main/java/com/back/domain/user/dto/ResendVerificationRequest.java b/src/main/java/com/back/domain/user/dto/sendEmailRequest.java similarity index 80% rename from src/main/java/com/back/domain/user/dto/ResendVerificationRequest.java rename to src/main/java/com/back/domain/user/dto/sendEmailRequest.java index 7ab4501a..2e4ce751 100644 --- a/src/main/java/com/back/domain/user/dto/ResendVerificationRequest.java +++ b/src/main/java/com/back/domain/user/dto/sendEmailRequest.java @@ -3,7 +3,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public record ResendVerificationRequest( +public record sendEmailRequest( @NotBlank @Email String email ) { } diff --git a/src/main/java/com/back/domain/user/service/AuthService.java b/src/main/java/com/back/domain/user/service/AuthService.java index 1d8eb089..e01a197c 100644 --- a/src/main/java/com/back/domain/user/service/AuthService.java +++ b/src/main/java/com/back/domain/user/service/AuthService.java @@ -267,6 +267,24 @@ public String refreshToken(HttpServletRequest request, HttpServletResponse respo ); } + /** + * 아이디 찾기 서비스 + * 1. 이메일로 사용자 조회 + * 2. username 일부 마스킹 처리 + * 3. 이메일 발송 + */ + public void recoverUsername(String email) { + // 사용자 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // username 일부 마스킹 처리 + String maskedUsername = maskUsername(user.getUsername()); + + // 이메일 발송 + emailService.sendUsernameEmail(user.getEmail(), maskedUsername); + } + /** * 회원가입 시 중복 검증 * - username, email, nickname @@ -296,4 +314,31 @@ private String resolveRefreshToken(HttpServletRequest request) { } return null; } + + /** + * username 일부 마스킹 처리 + * - 1~2글자 → 첫 글자만 보이고 나머지는 * (ex. a*) + * - 3~4글자 → 앞 1글자 + * + 뒤 1글자 (ex. a*c, a**d) + * - 5글자 이상 → 앞 2글자 + * + 뒤 2글자 (ex. ab*de, ab**ef) + */ + private String maskUsername(String username) { + if (username.length() <= 2) { + return username.charAt(0) + "*"; + } + int length = username.length(); + if (length <= 2) { + // 1~2글자 → 첫 글자만 보이고 나머지는 * + return username.charAt(0) + "*".repeat(length - 1); + } else if (length <= 4) { + // 3~4글자 → 앞 1글자 + * + 뒤 1글자 + return username.charAt(0) + + "*".repeat(length - 2) + + username.charAt(length - 1); + } else { + // 5글자 이상 → 앞 2글자 + * + 뒤 2글자 + return username.substring(0, 2) + + "*".repeat(length - 4) + + username.substring(length - 2); + } + } } diff --git a/src/main/java/com/back/domain/user/service/EmailService.java b/src/main/java/com/back/domain/user/service/EmailService.java index 10fb9615..5fb7f13d 100644 --- a/src/main/java/com/back/domain/user/service/EmailService.java +++ b/src/main/java/com/back/domain/user/service/EmailService.java @@ -42,6 +42,21 @@ public void sendVerificationEmail(String toEmail, String token) { sendHtmlEmail(toEmail, subject, htmlContent); } + // 아이디 찾기 메일 전송 + public void sendUsernameEmail(String toEmail, String maskedUsername) { + String subject = "[Catfe] 아이디 찾기 안내"; + String htmlContent = """ +

안녕하세요, Catfe입니다.

+

회원님의 로그인 아이디는 다음과 같습니다.

+
+

%s

+
+

감사합니다.

+ """.formatted(maskedUsername); + + sendHtmlEmail(toEmail, subject, htmlContent); + } + // HTML 이메일 전송 공통 메서드 private void sendHtmlEmail(String toEmail, String subject, String htmlContent) { try { From b60594428a4c59117a9d84592530b1d3c3cafc63 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:14:31 +0900 Subject: [PATCH 02/10] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerTest.java | 67 +++++++++++++++++++ .../domain/user/service/AuthServiceTest.java | 57 ++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java index 8ab78a74..4ed82334 100644 --- a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java @@ -745,4 +745,71 @@ void refreshToken_expiredToken() throws Exception { .andExpect(jsonPath("$.code").value("AUTH_005")) .andExpect(jsonPath("$.message").value("만료된 리프레시 토큰입니다.")); } + + // ======================== 아이디 찾기 테스트 ======================== + + @Test + @DisplayName("정상 아이디 찾기 성공 → 200 OK") + void recoverUsername_success() throws Exception { + // given: 유저 생성 + User user = User.createUser("recoveruser", "recover@example.com", + passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + String body = """ + { + "email": "recover@example.com" + } + """; + + // when & then + mvc.perform(post("/api/auth/username/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.message").value("아이디를 이메일로 전송했습니다.")) + .andExpect(jsonPath("$.data").isEmpty()); + } + + @Test + @DisplayName("아이디 찾기 실패 - 존재하지 않는 이메일 → 404 Not Found") + void recoverUsername_userNotFound() throws Exception { + String body = """ + { + "email": "notfound@example.com" + } + """; + + mvc.perform(post("/api/auth/username/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("USER_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 사용자입니다.")); + } + + @Test + @DisplayName("아이디 찾기 실패 - 이메일 필드 누락 → 400 Bad Request") + void recoverUsername_missingField() throws Exception { + // given: 잘못된 요청 (이메일 필드 없음) + String body = """ + { + } + """; + + mvc.perform(post("/api/auth/username/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("COMMON_400")) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); + } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/user/service/AuthServiceTest.java b/src/test/java/com/back/domain/user/service/AuthServiceTest.java index b907c8ff..6b987541 100644 --- a/src/test/java/com/back/domain/user/service/AuthServiceTest.java +++ b/src/test/java/com/back/domain/user/service/AuthServiceTest.java @@ -5,6 +5,7 @@ import com.back.domain.user.dto.UserRegisterRequest; import com.back.domain.user.dto.UserResponse; import com.back.domain.user.entity.User; +import com.back.domain.user.entity.UserProfile; import com.back.domain.user.entity.UserStatus; import com.back.domain.user.repository.UserProfileRepository; import com.back.domain.user.repository.UserRepository; @@ -536,4 +537,60 @@ void refreshToken_invalidToken() { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.INVALID_REFRESH_TOKEN.getMessage()); } + + // ======================== 아이디 찾기 테스트 ======================== + + @Test + @DisplayName("정상 아이디 찾기 성공 → 이메일 발송") + void recoverUsername_success() { + // given: 회원가입으로 사용자 생성 + UserRegisterRequest request = new UserRegisterRequest( + "findme", "findme@example.com", "P@ssw0rd!", "닉네임" + ); + UserResponse response = authService.register(request); + + // when: 아이디 찾기 실행 + authService.recoverUsername("findme@example.com"); + + // then: 이메일 발송이 호출되었는지 확인 + verify(emailService, times(1)) + .sendUsernameEmail(eq("findme@example.com"), anyString()); + } + + @Test + @DisplayName("아이디 찾기 실패 - 존재하지 않는 이메일 → USER_NOT_FOUND") + void recoverUsername_userNotFound() { + // when & then + assertThatThrownBy(() -> + authService.recoverUsername("notfound@example.com") + ).isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("아이디 마스킹 규칙 검증") + void recoverUsername_maskingRules() { + // given: 길이가 다른 username 케이스 + User shortUser = User.createUser("a", "short@example.com", passwordEncoder.encode("P@ssw0rd!")); + shortUser.setUserProfile(new UserProfile(shortUser, "닉", null, null, null, 0)); + userRepository.save(shortUser); + + User midUser = User.createUser("abc", "mid@example.com", passwordEncoder.encode("P@ssw0rd!")); + midUser.setUserProfile(new UserProfile(midUser, "닉", null, null, null, 0)); + userRepository.save(midUser); + + User longUser = User.createUser("abcdef", "long@example.com", passwordEncoder.encode("P@ssw0rd!")); + longUser.setUserProfile(new UserProfile(longUser, "닉", null, null, null, 0)); + userRepository.save(longUser); + + // when + authService.recoverUsername("short@example.com"); + authService.recoverUsername("mid@example.com"); + authService.recoverUsername("long@example.com"); + + // then: 마스킹된 값으로 이메일 발송 확인 + verify(emailService).sendUsernameEmail(eq("short@example.com"), eq("a*")); + verify(emailService).sendUsernameEmail(eq("mid@example.com"), eq("a*c")); + verify(emailService).sendUsernameEmail(eq("long@example.com"), eq("ab**ef")); + } } \ No newline at end of file From 6c1d62308567f7e29a3cb51c718ec93741583bb7 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:15:42 +0900 Subject: [PATCH 03/10] =?UTF-8?q?Docs:=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerDocs.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index 0ee3b743..a160d73c 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -13,6 +13,7 @@ import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -690,4 +691,75 @@ ResponseEntity>> refreshToken( HttpServletRequest request, HttpServletResponse response ); + + @Operation( + summary = "아이디 찾기", + description = "사용자가 이메일을 입력하면, 해당 이메일로 가입된 계정의 아이디(username)를 일부 마스킹 처리하여 이메일로 전송합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "아이디 찾기 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": true, + "code": "SUCCESS_200", + "message": "아이디를 이메일로 전송했습니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 사용자", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "USER_001", + "message": "존재하지 않는 사용자입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 (이메일 누락 등)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_400", + "message": "잘못된 요청입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_500", + "message": "서버 오류가 발생했습니다.", + "data": null + } + """) + ) + ) + }) + @PostMapping("/username/recover") + ResponseEntity> recoverUsername( + @Valid @RequestBody sendEmailRequest request + ); } From 4388712915de823d30f15498e562c0c414cc6d89 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:28:21 +0900 Subject: [PATCH 04/10] =?UTF-8?q?Feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=AC=EC=84=A4=EC=A0=95=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 14 +++++++++++++ .../back/domain/user/service/AuthService.java | 18 ++++++++++++++++ .../domain/user/service/EmailService.java | 21 +++++++++++++++++++ .../domain/user/service/TokenService.java | 16 ++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/src/main/java/com/back/domain/user/controller/AuthController.java b/src/main/java/com/back/domain/user/controller/AuthController.java index 21a0e09e..e6578d55 100644 --- a/src/main/java/com/back/domain/user/controller/AuthController.java +++ b/src/main/java/com/back/domain/user/controller/AuthController.java @@ -100,6 +100,7 @@ public ResponseEntity>> refreshToken( )); } + // 아이디 찾기 @PostMapping("/username/recover") public ResponseEntity> recoverUsername( @Valid @RequestBody sendEmailRequest request @@ -111,4 +112,17 @@ public ResponseEntity> recoverUsername( null )); } + + // 비밀번호 재설정 요청 + @PostMapping("/password/recover") + public ResponseEntity> recoverPassword( + @Valid @RequestBody sendEmailRequest request + ) { + authService.recoverPassword(request.email()); + return ResponseEntity + .ok(RsData.success( + "비밀번호 재설정 링크를 이메일로 전송했습니다.", + null + )); + } } diff --git a/src/main/java/com/back/domain/user/service/AuthService.java b/src/main/java/com/back/domain/user/service/AuthService.java index e01a197c..e3a41f48 100644 --- a/src/main/java/com/back/domain/user/service/AuthService.java +++ b/src/main/java/com/back/domain/user/service/AuthService.java @@ -285,6 +285,24 @@ public void recoverUsername(String email) { emailService.sendUsernameEmail(user.getEmail(), maskedUsername); } + /** + * 비밀번호 재설정 요청 서비스 + * 1. 이메일로 사용자 조회 + * 2. 비밀번호 재설정 토큰 생성 + * 3. 이메일 발송 + */ + public void recoverPassword(String email) { + // 사용자 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 비밀번호 재설정 토큰 생성 + String resetToken = tokenService.createPasswordResetToken(user.getId()); + + // 이메일 발송 + emailService.sendPasswordResetEmail(user.getEmail(), resetToken); + } + /** * 회원가입 시 중복 검증 * - username, email, nickname diff --git a/src/main/java/com/back/domain/user/service/EmailService.java b/src/main/java/com/back/domain/user/service/EmailService.java index 5fb7f13d..10e80abb 100644 --- a/src/main/java/com/back/domain/user/service/EmailService.java +++ b/src/main/java/com/back/domain/user/service/EmailService.java @@ -57,6 +57,27 @@ public void sendUsernameEmail(String toEmail, String maskedUsername) { sendHtmlEmail(toEmail, subject, htmlContent); } + // 비밀번호 재설정 메일 전송 + public void sendPasswordResetEmail(String toEmail, String token) { + String subject = "[Catfe] 비밀번호 재설정 안내"; + String resetUrl = FRONTEND_BASE_URL + "/reset-password?token=" + token; + + String htmlContent = """ +

안녕하세요, Catfe입니다.

+

아래 버튼을 클릭하여 비밀번호를 재설정해 주세요.

+
+

+ 비밀번호 재설정하기 +

+
+

이 링크는 1시간 동안만 유효합니다.

+ """.formatted(resetUrl); + + sendHtmlEmail(toEmail, subject, htmlContent); + } + // HTML 이메일 전송 공통 메서드 private void sendHtmlEmail(String toEmail, String subject, String htmlContent) { try { diff --git a/src/main/java/com/back/domain/user/service/TokenService.java b/src/main/java/com/back/domain/user/service/TokenService.java index 353d168a..8f11cc9f 100644 --- a/src/main/java/com/back/domain/user/service/TokenService.java +++ b/src/main/java/com/back/domain/user/service/TokenService.java @@ -29,6 +29,22 @@ public void deleteEmailVerificationToken(String token) { deleteToken(EMAIL_VERIFICATION_PREFIX, token); } + // -------------------- 비밀번호 재설정 토큰 -------------------- + private static final String PASSWORD_RESET_PREFIX = "password:reset:"; + private static final long PASSWORD_RESET_EXPIRATION_MINUTES = 60; // 1시간 + + public String createPasswordResetToken(Long userId) { + return createToken(PASSWORD_RESET_PREFIX, userId, PASSWORD_RESET_EXPIRATION_MINUTES); + } + + public Long getUserIdByPasswordResetToken(String token) { + return getUserIdByToken(PASSWORD_RESET_PREFIX, token); + } + + public void deletePasswordResetToken(String token) { + deleteToken(PASSWORD_RESET_PREFIX, token); + } + // -------------------- 내부 공통 로직 -------------------- private String createToken(String prefix, Long userId, long ttlMinutes) { String token = UUID.randomUUID().toString(); From 02ef78ae058031e20c609ed172e6d8cea27a0982 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:32:31 +0900 Subject: [PATCH 05/10] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerTest.java | 69 +++++++++++++++++++ .../domain/user/service/AuthServiceTest.java | 27 ++++++++ 2 files changed, 96 insertions(+) diff --git a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java index 4ed82334..17312ff5 100644 --- a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java @@ -812,4 +812,73 @@ void recoverUsername_missingField() throws Exception { .andExpect(jsonPath("$.code").value("COMMON_400")) .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); } + + // ======================== 비밀번호 재설정 요청 컨트롤러 테스트 ======================== + + @Test + @DisplayName("정상 비밀번호 재설정 요청 → 200 OK") + void recoverPassword_success() throws Exception { + // given: 가입된 사용자 생성 + User user = User.createUser("pwuser", "pw@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + String body = """ + { + "email": "pw@example.com" + } + """; + + // when & then + mvc.perform(post("/api/auth/password/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.message").value("비밀번호 재설정 링크를 이메일로 전송했습니다.")) + .andExpect(jsonPath("$.data").isEmpty()); + } + + @Test + @DisplayName("비밀번호 재설정 요청 실패 - 존재하지 않는 사용자 → 404 Not Found") + void recoverPassword_userNotFound() throws Exception { + // given: 존재하지 않는 이메일 사용 + String body = """ + { + "email": "notfound@example.com" + } + """; + + // when & then + mvc.perform(post("/api/auth/password/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("USER_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 사용자입니다.")); + } + + @Test + @DisplayName("비밀번호 재설정 요청 실패 - 이메일 필드 누락 → 400 Bad Request") + void recoverPassword_missingField() throws Exception { + // given: 잘못된 요청 (이메일 필드 없음) + String body = """ + { + } + """; + + // when & then + mvc.perform(post("/api/auth/password/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("COMMON_400")) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); + } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/user/service/AuthServiceTest.java b/src/test/java/com/back/domain/user/service/AuthServiceTest.java index 6b987541..6524948f 100644 --- a/src/test/java/com/back/domain/user/service/AuthServiceTest.java +++ b/src/test/java/com/back/domain/user/service/AuthServiceTest.java @@ -593,4 +593,31 @@ void recoverUsername_maskingRules() { verify(emailService).sendUsernameEmail(eq("mid@example.com"), eq("a*c")); verify(emailService).sendUsernameEmail(eq("long@example.com"), eq("ab**ef")); } + + // ======================== 비밀번호 재설정 요청 테스트 ======================== + + @Test + @DisplayName("정상 비밀번호 재설정 요청 → 이메일 발송 성공") + void recoverPassword_success() { + // given: 가입된 사용자 생성 + User user = User.createUser("pwuser", "pw@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + // when + authService.recoverPassword("pw@example.com"); + + // then: 이메일 발송 호출 확인 + verify(emailService, times(1)) + .sendPasswordResetEmail(eq("pw@example.com"), anyString()); + } + + @Test + @DisplayName("비밀번호 재설정 요청 실패 - 존재하지 않는 사용자 → USER_NOT_FOUND") + void recoverPassword_userNotFound() { + // when & then + assertThatThrownBy(() -> authService.recoverPassword("notfound@example.com")) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); + } } \ No newline at end of file From 211349b8489db04248eafc3aa701ba9f2afc55f6 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:34:04 +0900 Subject: [PATCH 06/10] =?UTF-8?q?Docs:=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 6 +- .../user/controller/AuthControllerDocs.java | 75 ++++++++++++++++++- ...mailRequest.java => SendEmailRequest.java} | 2 +- 3 files changed, 77 insertions(+), 6 deletions(-) rename src/main/java/com/back/domain/user/dto/{sendEmailRequest.java => SendEmailRequest.java} (84%) diff --git a/src/main/java/com/back/domain/user/controller/AuthController.java b/src/main/java/com/back/domain/user/controller/AuthController.java index e6578d55..69d9e86f 100644 --- a/src/main/java/com/back/domain/user/controller/AuthController.java +++ b/src/main/java/com/back/domain/user/controller/AuthController.java @@ -49,7 +49,7 @@ public ResponseEntity> verifyEmail( // 인증 메일 재발송 @PostMapping("/email/verify") public ResponseEntity> resendVerificationEmail( - @Valid @RequestBody sendEmailRequest request + @Valid @RequestBody SendEmailRequest request ) { authService.resendVerificationEmail(request.email()); return ResponseEntity @@ -103,7 +103,7 @@ public ResponseEntity>> refreshToken( // 아이디 찾기 @PostMapping("/username/recover") public ResponseEntity> recoverUsername( - @Valid @RequestBody sendEmailRequest request + @Valid @RequestBody SendEmailRequest request ) { authService.recoverUsername(request.email()); return ResponseEntity @@ -116,7 +116,7 @@ public ResponseEntity> recoverUsername( // 비밀번호 재설정 요청 @PostMapping("/password/recover") public ResponseEntity> recoverPassword( - @Valid @RequestBody sendEmailRequest request + @Valid @RequestBody SendEmailRequest request ) { authService.recoverPassword(request.email()); return ResponseEntity diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index a160d73c..e9bb372c 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -306,7 +306,7 @@ ResponseEntity> verifyEmail( ) }) ResponseEntity> resendVerificationEmail( - @Valid @RequestBody sendEmailRequest request + @Valid @RequestBody SendEmailRequest request ); @Operation( @@ -760,6 +760,77 @@ ResponseEntity>> refreshToken( }) @PostMapping("/username/recover") ResponseEntity> recoverUsername( - @Valid @RequestBody sendEmailRequest request + @Valid @RequestBody SendEmailRequest request + ); + + @Operation( + summary = "비밀번호 재설정 요청", + description = "사용자가 가입한 이메일을 입력하면, 해당 이메일로 비밀번호 재설정 링크가 발송됩니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "비밀번호 재설정 메일 발송 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": true, + "code": "SUCCESS_200", + "message": "비밀번호 재설정 메일을 전송했습니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 사용자", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "USER_001", + "message": "존재하지 않는 사용자입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 (이메일 누락 등)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_400", + "message": "잘못된 요청입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_500", + "message": "서버 오류가 발생했습니다.", + "data": null + } + """) + ) + ) + }) + @PostMapping("/password/recover") + ResponseEntity> recoverPassword( + @Valid @RequestBody SendEmailRequest request ); } diff --git a/src/main/java/com/back/domain/user/dto/sendEmailRequest.java b/src/main/java/com/back/domain/user/dto/SendEmailRequest.java similarity index 84% rename from src/main/java/com/back/domain/user/dto/sendEmailRequest.java rename to src/main/java/com/back/domain/user/dto/SendEmailRequest.java index 2e4ce751..e8ed90e9 100644 --- a/src/main/java/com/back/domain/user/dto/sendEmailRequest.java +++ b/src/main/java/com/back/domain/user/dto/SendEmailRequest.java @@ -3,7 +3,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public record sendEmailRequest( +public record SendEmailRequest( @NotBlank @Email String email ) { } From 9a241fddec5c5ebc6ddf7b01d809efdc93f084e5 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:45:19 +0900 Subject: [PATCH 07/10] =?UTF-8?q?Comment:=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/back/domain/user/dto/LoginResponse.java | 6 ++++++ .../java/com/back/domain/user/dto/SendEmailRequest.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/com/back/domain/user/dto/LoginResponse.java b/src/main/java/com/back/domain/user/dto/LoginResponse.java index a92b45e7..2bb350b4 100644 --- a/src/main/java/com/back/domain/user/dto/LoginResponse.java +++ b/src/main/java/com/back/domain/user/dto/LoginResponse.java @@ -1,5 +1,11 @@ package com.back.domain.user.dto; +/** + * 사용자 로그인 응답을 나타내는 DTO + * + * @param accessToken 사용자 인증에 사용되는 JWT 토큰 + * @param user 로그인한 사용자 정보 + */ public record LoginResponse( String accessToken, UserResponse user diff --git a/src/main/java/com/back/domain/user/dto/SendEmailRequest.java b/src/main/java/com/back/domain/user/dto/SendEmailRequest.java index e8ed90e9..b1c6d60c 100644 --- a/src/main/java/com/back/domain/user/dto/SendEmailRequest.java +++ b/src/main/java/com/back/domain/user/dto/SendEmailRequest.java @@ -3,6 +3,11 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +/** + * 이메일 전송 요청을 나타내는 DTO + * + * @param email 이메일 주소 + */ public record SendEmailRequest( @NotBlank @Email String email ) { From db320ee547242415b392ca6951362b955534383f Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:09:20 +0900 Subject: [PATCH 08/10] =?UTF-8?q?Feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=AC=EC=84=A4=EC=A0=95=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 13 +++++++ .../domain/user/dto/PasswordResetRequest.java | 15 ++++++++ .../back/domain/user/service/AuthService.java | 34 +++++++++++++++++++ .../com/back/global/exception/ErrorCode.java | 3 +- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/back/domain/user/dto/PasswordResetRequest.java diff --git a/src/main/java/com/back/domain/user/controller/AuthController.java b/src/main/java/com/back/domain/user/controller/AuthController.java index 69d9e86f..2d13ead7 100644 --- a/src/main/java/com/back/domain/user/controller/AuthController.java +++ b/src/main/java/com/back/domain/user/controller/AuthController.java @@ -125,4 +125,17 @@ public ResponseEntity> recoverPassword( null )); } + + // 비밀번호 재설정 + @PostMapping("/password/reset") + public ResponseEntity> resetPassword( + @Valid @RequestBody PasswordResetRequest request + ) { + authService.resetPassword(request.token(), request.newPassword()); + return ResponseEntity + .ok(RsData.success( + "비밀번호가 성공적으로 재설정되었습니다.", + null + )); + } } diff --git a/src/main/java/com/back/domain/user/dto/PasswordResetRequest.java b/src/main/java/com/back/domain/user/dto/PasswordResetRequest.java new file mode 100644 index 00000000..2b6ba4e0 --- /dev/null +++ b/src/main/java/com/back/domain/user/dto/PasswordResetRequest.java @@ -0,0 +1,15 @@ +package com.back.domain.user.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * 비밀번호 재설정 요청 DTO + * + * @param token 비밀번호 재설정 토큰 + * @param newPassword 새 비밀번호 + */ +public record PasswordResetRequest( + @NotBlank String token, + @NotBlank String newPassword +) { +} diff --git a/src/main/java/com/back/domain/user/service/AuthService.java b/src/main/java/com/back/domain/user/service/AuthService.java index e3a41f48..011baf6b 100644 --- a/src/main/java/com/back/domain/user/service/AuthService.java +++ b/src/main/java/com/back/domain/user/service/AuthService.java @@ -303,6 +303,40 @@ public void recoverPassword(String email) { emailService.sendPasswordResetEmail(user.getEmail(), resetToken); } + /** + * 비밀번호 재설정 서비스 + * 1. 토큰 검증 + * 2. 사용자 조회 + * 3. 비밀번호 정책 검증 + * 4. 비밀번호 변경 + * 5. 토큰 삭제 + */ + public void resetPassword(String token, String newPassword) { + // 토큰 존재 여부 확인 + if (token == null || token.isEmpty()) { + throw new CustomException(ErrorCode.BAD_REQUEST); + } + + // 토큰으로 사용자 ID 조회 + Long userId = tokenService.getUserIdByPasswordResetToken(token); + if (userId == null) { + throw new CustomException(ErrorCode.INVALID_PASSWORD_RESET_TOKEN); + } + + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 비밀번호 정책 검증 + PasswordValidator.validate(newPassword); + + // 비밀번호 변경 + user.setPassword(passwordEncoder.encode(newPassword)); + + // 토큰 삭제 (재사용 방지) + tokenService.deletePasswordResetToken(token); + } + /** * 회원가입 시 중복 검증 * - username, email, nickname diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 3b25db35..a948a8b6 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -97,7 +97,8 @@ public enum ErrorCode { // ======================== 토큰 관련 ======================== INVALID_EMAIL_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN_001", "유효하지 않은 이메일 인증 토큰입니다."), - ALREADY_VERIFIED(HttpStatus.CONFLICT, "TOKEN_002", "이미 인증된 계정입니다."); + ALREADY_VERIFIED(HttpStatus.CONFLICT, "TOKEN_002", "이미 인증된 계정입니다."), + INVALID_PASSWORD_RESET_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN_003", "유효하지 않은 비밀번호 재설정 토큰입니다."); private final HttpStatus status; private final String code; From 7551941ed97b320f53efb54de5f340ec3e8847c7 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:15:12 +0900 Subject: [PATCH 09/10] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerTest.java | 102 ++++++++++++++++++ .../domain/user/service/AuthServiceTest.java | 61 +++++++++++ 2 files changed, 163 insertions(+) diff --git a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java index 17312ff5..01f661a0 100644 --- a/src/test/java/com/back/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/back/domain/user/controller/AuthControllerTest.java @@ -881,4 +881,106 @@ void recoverPassword_missingField() throws Exception { .andExpect(jsonPath("$.code").value("COMMON_400")) .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); } + + // ======================== 비밀번호 재설정 컨트롤러 테스트 ======================== + + @Test + @DisplayName("정상 비밀번호 재설정 성공 → 200 OK") + void resetPassword_success() throws Exception { + // given: 가입된 사용자 + User user = User.createUser("resetuser", "reset@example.com", passwordEncoder.encode("OldPass123!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + String token = tokenService.createPasswordResetToken(user.getId()); + + String body = """ + { + "token": "%s", + "newPassword": "NewPass123!" + } + """.formatted(token); + + // when & then + mvc.perform(post("/api/auth/password/reset") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.message").value("비밀번호가 성공적으로 재설정되었습니다.")) + .andExpect(jsonPath("$.data").isEmpty()); + } + + @Test + @DisplayName("비밀번호 재설정 실패 - 유효하지 않은 토큰 → 401 Unauthorized") + void resetPassword_invalidToken() throws Exception { + // given: 가입된 사용자 + String body = """ + { + "token": "fake-token", + "newPassword": "NewPass123!" + } + """; + + // when & then + mvc.perform(post("/api/auth/password/reset") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("TOKEN_003")) + .andExpect(jsonPath("$.message").value("유효하지 않은 비밀번호 재설정 토큰입니다.")); + } + + @Test + @DisplayName("비밀번호 재설정 실패 - 비밀번호 정책 위반 → 400 Bad Request") + void resetPassword_invalidPassword() throws Exception { + // given: 가입된 사용자 + 토큰 + User user = User.createUser("resetuser2", "reset2@example.com", passwordEncoder.encode("OldPass123!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + String token = tokenService.createPasswordResetToken(user.getId()); + + String body = """ + { + "token": "%s", + "newPassword": "weakpw" + } + """.formatted(token); + + // when & then + mvc.perform(post("/api/auth/password/reset") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("USER_005")) + .andExpect(jsonPath("$.message").value("비밀번호는 최소 8자 이상, 숫자/특수문자를 포함해야 합니다.")); + } + + @Test + @DisplayName("비밀번호 재설정 실패 - 요청 필드 누락 → 400 Bad Request") + void resetPassword_missingField() throws Exception { + // given: 잘못된 요청 (토큰 필드 누락) + String body = """ + { + "token": "" + } + """; + + // when & then + mvc.perform(post("/api/auth/password/reset") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("COMMON_400")) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); + } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/user/service/AuthServiceTest.java b/src/test/java/com/back/domain/user/service/AuthServiceTest.java index 6524948f..6457c8c3 100644 --- a/src/test/java/com/back/domain/user/service/AuthServiceTest.java +++ b/src/test/java/com/back/domain/user/service/AuthServiceTest.java @@ -620,4 +620,65 @@ void recoverPassword_userNotFound() { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); } + + // ======================== 비밀번호 재설정 테스트 ======================== + + @Test + @DisplayName("정상 비밀번호 재설정 성공 → 비밀번호 변경 및 토큰 삭제") + void resetPassword_success() { + // given: 가입된 사용자 + User user = User.createUser("resetuser", "reset@example.com", passwordEncoder.encode("OldPass123!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + // 비밀번호 재설정 토큰 생성 + String token = tokenService.createPasswordResetToken(user.getId()); + + // when + authService.resetPassword(token, "NewPass123!"); + + // then: 비밀번호 변경 확인 + User updated = userRepository.findById(user.getId()).orElseThrow(); + assertThat(passwordEncoder.matches("NewPass123!", updated.getPassword())).isTrue(); + + // 토큰이 삭제되었는지 확인 + Long result = tokenService.getUserIdByPasswordResetToken(token); + assertThat(result).isNull(); + } + + @Test + @DisplayName("비밀번호 재설정 실패 - 유효하지 않은 토큰 → INVALID_PASSWORD_RESET_TOKEN") + void resetPassword_invalidToken() { + assertThatThrownBy(() -> authService.resetPassword("fake-token", "NewPass123!")) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_PASSWORD_RESET_TOKEN.getMessage()); + } + + @Test + @DisplayName("비밀번호 재설정 실패 - 존재하지 않는 사용자 → USER_NOT_FOUND") + void resetPassword_userNotFound() { + // given: 토큰은 만들었지만 사용자 ID는 없는 값으로 설정 + String token = tokenService.createPasswordResetToken(99999L); + + // when & then + assertThatThrownBy(() -> authService.resetPassword(token, "NewPass123!")) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("비밀번호 재설정 실패 - 비밀번호 정책 위반 → INVALID_PASSWORD") + void resetPassword_invalidPassword() { + // given: 가입된 사용자 + 토큰 + User user = User.createUser("resetuser2", "reset2@example.com", passwordEncoder.encode("OldPass123!")); + user.setUserProfile(new UserProfile(user, "닉네임", null, null, null, 0)); + userRepository.save(user); + + String token = tokenService.createPasswordResetToken(user.getId()); + + // when & then + assertThatThrownBy(() -> authService.resetPassword(token, "weakpw")) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_PASSWORD.getMessage()); + } } \ No newline at end of file From bb0185636d88eb39726833e6edc88ebc4d366bc9 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:16:14 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Docs:=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthControllerDocs.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java index e9bb372c..323913ef 100644 --- a/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java +++ b/src/main/java/com/back/domain/user/controller/AuthControllerDocs.java @@ -833,4 +833,90 @@ ResponseEntity> recoverUsername( ResponseEntity> recoverPassword( @Valid @RequestBody SendEmailRequest request ); + + @Operation( + summary = "비밀번호 재설정", + description = "비밀번호 재설정 토큰과 새로운 비밀번호를 입력받아 계정 비밀번호를 변경합니다. 토큰은 1시간 동안만 유효합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "비밀번호 재설정 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": true, + "code": "SUCCESS_200", + "message": "비밀번호가 성공적으로 재설정되었습니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 (필드 누락, 비밀번호 정책 위반)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_400", + "message": "잘못된 요청입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "401", + description = "유효하지 않은 비밀번호 재설정 토큰", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "TOKEN_003", + "message": "유효하지 않은 비밀번호 재설정 토큰입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 사용자", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "USER_001", + "message": "존재하지 않는 사용자입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_500", + "message": "서버 오류가 발생했습니다.", + "data": null + } + """) + ) + ) + }) + @PostMapping("/password/reset") + ResponseEntity> resetPassword( + @Valid @RequestBody PasswordResetRequest request + ); }