From e0b27a43963d4d52aaa50787ce8dd62f868d1f7d Mon Sep 17 00:00:00 2001 From: KoungQ Date: Fri, 29 May 2026 16:42:52 +0900 Subject: [PATCH 01/15] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20ddl=20auto=20=EC=84=A4=EC=A0=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index ce445a3..66a8f9a 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -13,7 +13,7 @@ spring: port: 6379 jpa: hibernate: - ddl-auto: validate + ddl-auto: update show-sql: true jwt: @@ -40,4 +40,4 @@ springdoc: firebase: service-account: - path: ${FIREBASE_SERVICE_ACCOUNT_PATH:src/main/resources/firebase/dorumdorum-9ce75-firebase-adminsdk-fbsvc-8d66c0dccb.json} + path: ${FIREBASE_SERVICE_ACCOUNT_PATH:secrets/firebase-service-account.json} From 04d0be27d8f83159c2f79997448032f901f1f7b3 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 14:17:24 +0900 Subject: [PATCH 02/15] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/usecase/SignUpUseCase.java | 11 ++++++- .../usecase/VerifyEmailUseCase.java | 5 ++- .../domain/user/domain/entity/User.java | 4 +++ .../repository/EmailVerifiedRepository.java | 8 +++++ .../RedisEmailVerifiedRepository.java | 32 +++++++++++++++++++ .../code/status/UserErrorStatus.java | 1 + src/main/resources/application.yml | 18 +++++++++++ 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/project/dorumdorum/domain/user/domain/repository/EmailVerifiedRepository.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SignUpUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SignUpUseCase.java index 8a0c82f..83a61cb 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SignUpUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SignUpUseCase.java @@ -2,6 +2,7 @@ import com.project.dorumdorum.domain.user.application.dto.request.SignUpRequest; import com.project.dorumdorum.domain.user.domain.entity.User; +import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository; import com.project.dorumdorum.domain.user.domain.service.UserService; import com.project.dorumdorum.global.exception.RestApiException; import lombok.RequiredArgsConstructor; @@ -9,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.DUPLICATE_SIGN_UP_INFO; +import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.EMAIL_NOT_VERIFIED; import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus._PASSWORD_NOT_MATCHES; @Service @@ -17,15 +19,21 @@ public class SignUpUseCase { private final UserService userService; + private final EmailVerifiedRepository emailVerifiedRepository; /** * 회원가입 처리 + * - 이메일 인증 완료 여부를 검증 * - 비밀번호 확인 여부를 검증 * - 중복 이메일 가입을 차단 * - 사용자를 저장하고 생성된 사용자 번호를 반환 */ public String execute(SignUpRequest request) { - if(!request.isCheckedPassword()) { + if (!emailVerifiedRepository.existsByEmail(request.email())) { + throw new RestApiException(EMAIL_NOT_VERIFIED); + } + + if (!request.isCheckedPassword()) { throw new RestApiException(_PASSWORD_NOT_MATCHES); } @@ -35,6 +43,7 @@ public String execute(SignUpRequest request) { } User savedUser = userService.save(request); + emailVerifiedRepository.delete(request.email()); return savedUser.getUserNo(); } } diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyEmailUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyEmailUseCase.java index 4369bde..bbf8ab5 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyEmailUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyEmailUseCase.java @@ -1,5 +1,6 @@ package com.project.dorumdorum.domain.user.application.usecase; +import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository; import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -9,13 +10,15 @@ public class VerifyEmailUseCase { private final EmailVerificationService emailVerificationService; + private final EmailVerifiedRepository emailVerifiedRepository; /** * 이메일 인증 코드 검증 * - 이메일과 인증 코드의 일치 여부를 확인 - * - 유효하지 않으면 예외를 발생 + * - 검증 성공 시 회원가입 가능 상태를 Redis에 저장 */ public void execute(String email, String code) { emailVerificationService.verifyCode(email, code); + emailVerifiedRepository.save(email); } } diff --git a/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java b/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java index 7e790a5..8aac584 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java +++ b/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java @@ -67,4 +67,8 @@ public void updateFirebaseToken(String firebaseToken) { this.firebaseToken = firebaseToken; } + public void updatePassword(String encodedPassword) { + this.password = encodedPassword; + } + } diff --git a/src/main/java/com/project/dorumdorum/domain/user/domain/repository/EmailVerifiedRepository.java b/src/main/java/com/project/dorumdorum/domain/user/domain/repository/EmailVerifiedRepository.java new file mode 100644 index 0000000..d1b79f3 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/domain/repository/EmailVerifiedRepository.java @@ -0,0 +1,8 @@ +package com.project.dorumdorum.domain.user.domain.repository; + +public interface EmailVerifiedRepository { + + void save(String email); + boolean existsByEmail(String email); + void delete(String email); +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java new file mode 100644 index 0000000..e9d3289 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java @@ -0,0 +1,32 @@ +package com.project.dorumdorum.domain.user.infra.repository; + +import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +@Repository +@RequiredArgsConstructor +public class RedisEmailVerifiedRepository implements EmailVerifiedRepository { + + private final RedisTemplate redisTemplate; + private static final String EMAIL_VERIFIED = "EMAIL_VERIFIED:"; + private static final String VERIFIED_VALUE = "verified"; + + @Override + public void save(String email) { + redisTemplate.opsForValue().set(EMAIL_VERIFIED + email, VERIFIED_VALUE, Duration.ofMinutes(10)); + } + + @Override + public boolean existsByEmail(String email) { + return Boolean.TRUE.equals(redisTemplate.hasKey(EMAIL_VERIFIED + email)); + } + + @Override + public void delete(String email) { + redisTemplate.delete(EMAIL_VERIFIED + email); + } +} diff --git a/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java b/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java index 9c52f86..a4b23d4 100644 --- a/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java +++ b/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java @@ -15,6 +15,7 @@ public enum UserErrorStatus implements BaseCodeInterface { EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "ROOM002", "가입되지 않은 이메일입니다."), AGE_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON400", "나이 계산 파싱 오류입니다."), FAILED_SEND_VERIFY_CODE(HttpStatus.INTERNAL_SERVER_ERROR, "MAIL001", "인증번호 전송에 실패하였습니다."), + EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "USER002", "이메일 인증이 완료되지 않았습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8e9c973..15e169e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -58,6 +58,12 @@ exclude-auth-path-patterns: method: POST - path-pattern: /api/email/verify method: POST + - path-pattern: /api/email/password-reset/send + method: POST + - path-pattern: /api/email/password-reset/verify + method: POST + - path-pattern: /api/users/password/reset + method: POST - path-pattern: /swagger-ui.html method: GET - path-pattern: /swagger-ui/index.html @@ -93,6 +99,12 @@ exclude-blacklist-path-patterns: method: POST - path-pattern: /api/email/verify method: POST + - path-pattern: /api/email/password-reset/send + method: POST + - path-pattern: /api/email/password-reset/verify + method: POST + - path-pattern: /api/users/password/reset + method: POST - path-pattern: /swagger-ui.html method: GET - path-pattern: /swagger-ui/index.html @@ -130,6 +142,12 @@ exclude-whitelist-path-patterns: method: POST - path-pattern: /api/email/verify method: POST + - path-pattern: /api/email/password-reset/send + method: POST + - path-pattern: /api/email/password-reset/verify + method: POST + - path-pattern: /api/users/password/reset + method: POST - path-pattern: /swagger-ui.html method: GET - path-pattern: /swagger-ui/index.html From 5f090bf29fff0d0b8c5f0573f882b1a5186f2e95 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 14:18:24 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ResetPasswordRequest.java | 14 ++++++ .../usecase/ResetPasswordUseCase.java | 44 +++++++++++++++++++ .../SendPasswordResetEmailUseCase.java | 39 ++++++++++++++++ .../VerifyPasswordResetCodeUseCase.java | 29 ++++++++++++ .../PasswordResetCodeRepository.java | 10 +++++ .../PasswordResetVerifiedRepository.java | 8 ++++ .../RedisPasswordResetCodeRepository.java | 32 ++++++++++++++ .../RedisPasswordResetVerifiedRepository.java | 32 ++++++++++++++ .../user/ui/ResetPasswordController.java | 25 +++++++++++ .../ui/SendPasswordResetEmailController.java | 23 ++++++++++ .../ui/VerifyPasswordResetCodeController.java | 24 ++++++++++ .../user/ui/spec/ResetPasswordApiSpec.java | 21 +++++++++ .../spec/SendPasswordResetEmailApiSpec.java | 20 +++++++++ .../spec/VerifyPasswordResetCodeApiSpec.java | 21 +++++++++ 14 files changed, 342 insertions(+) create mode 100644 src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetCodeRepository.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetVerifiedRepository.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetCodeRepository.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetVerifiedRepository.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/ui/ResetPasswordController.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/ui/spec/ResetPasswordApiSpec.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/ui/spec/SendPasswordResetEmailApiSpec.java create mode 100644 src/main/java/com/project/dorumdorum/domain/user/ui/spec/VerifyPasswordResetCodeApiSpec.java diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java new file mode 100644 index 0000000..646c1da --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java @@ -0,0 +1,14 @@ +package com.project.dorumdorum.domain.user.application.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record ResetPasswordRequest( + @Email @NotBlank String email, + @NotBlank String newPassword, + @NotBlank String newPasswordCheck +) { + public boolean isPasswordMatch() { + return newPassword.equals(newPasswordCheck); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java new file mode 100644 index 0000000..88c4e38 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java @@ -0,0 +1,44 @@ +package com.project.dorumdorum.domain.user.application.usecase; + +import com.project.dorumdorum.domain.user.application.dto.request.ResetPasswordRequest; +import com.project.dorumdorum.domain.user.domain.entity.User; +import com.project.dorumdorum.domain.user.domain.repository.PasswordResetVerifiedRepository; +import com.project.dorumdorum.domain.user.domain.service.UserService; +import com.project.dorumdorum.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.FAILED_EMAIL_VERIFICATION; +import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus._PASSWORD_NOT_MATCHES; + +@Service +@RequiredArgsConstructor +public class ResetPasswordUseCase { + + private final UserService userService; + private final PasswordResetVerifiedRepository passwordResetVerifiedRepository; + private final PasswordEncoder passwordEncoder; + + /** + * 비밀번호 재설정 + * - 비밀번호 일치 여부 확인 + * - 이메일 인증 완료 여부 확인 + * - 비밀번호 업데이트 + */ + @Transactional + public void execute(ResetPasswordRequest request) { + if (!request.isPasswordMatch()) { + throw new RestApiException(_PASSWORD_NOT_MATCHES); + } + + if (!passwordResetVerifiedRepository.existsByEmail(request.email())) { + throw new RestApiException(FAILED_EMAIL_VERIFICATION); + } + + User user = userService.findByEmail(request.email()); + passwordResetVerifiedRepository.delete(request.email()); + user.updatePassword(passwordEncoder.encode(request.newPassword())); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java new file mode 100644 index 0000000..e40821d --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java @@ -0,0 +1,39 @@ +package com.project.dorumdorum.domain.user.application.usecase; + +import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; +import com.project.dorumdorum.domain.user.domain.service.UserService; +import com.project.dorumdorum.global.exception.RestApiException; +import com.project.dorumdorum.global.util.SecureRandomGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.INVALID_EMAIL_DOMAIN; +import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.EMAIL_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class SendPasswordResetEmailUseCase { + + private final UserService userService; + private final EmailVerificationService emailVerificationService; + private final SecureRandomGenerator secureRandomGenerator; + + /** + * 비밀번호 재설정 인증 코드 발송 + * - 가입된 이메일인지 확인 + * - 허용된 대학 이메일 도메인인지 검증 + * - 인증 코드를 생성해 메일로 발송 + */ + public void send(String email) { + if (!userService.isAlreadyRegistered(email)) { + throw new RestApiException(EMAIL_NOT_FOUND); + } + + if (!emailVerificationService.isAllowedUniversityEmail(email)) { + throw new RestApiException(INVALID_EMAIL_DOMAIN); + } + + String code = secureRandomGenerator.generate(); + emailVerificationService.sendCode(email, code); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java new file mode 100644 index 0000000..ad35ce6 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java @@ -0,0 +1,29 @@ +package com.project.dorumdorum.domain.user.application.usecase; + +import com.project.dorumdorum.domain.user.domain.repository.PasswordResetCodeRepository; +import com.project.dorumdorum.domain.user.domain.repository.PasswordResetVerifiedRepository; +import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; +import com.project.dorumdorum.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.FAILED_EMAIL_VERIFICATION; + +@Service +@RequiredArgsConstructor +public class VerifyPasswordResetCodeUseCase { + + private final PasswordResetCodeRepository passwordResetCodeRepository; + private final PasswordResetVerifiedRepository passwordResetVerifiedRepository; + private final EmailVerificationService emailVerificationService; + + /** + * 비밀번호 재설정 인증 코드 검증 + * - 인증 코드 일치 여부 확인 + * - 검증 성공 시 비밀번호 재설정 가능 상태를 Redis에 저장 + */ + public void execute(String email, String code) { + emailVerificationService.verifyCode(email, code); + passwordResetVerifiedRepository.save(email); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetCodeRepository.java b/src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetCodeRepository.java new file mode 100644 index 0000000..35feb42 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetCodeRepository.java @@ -0,0 +1,10 @@ +package com.project.dorumdorum.domain.user.domain.repository; + +import java.util.Optional; + +public interface PasswordResetCodeRepository { + + void save(String email, String code); + Optional findByEmail(String email); + void delete(String email); +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetVerifiedRepository.java b/src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetVerifiedRepository.java new file mode 100644 index 0000000..0846b40 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/domain/repository/PasswordResetVerifiedRepository.java @@ -0,0 +1,8 @@ +package com.project.dorumdorum.domain.user.domain.repository; + +public interface PasswordResetVerifiedRepository { + + void save(String email); + boolean existsByEmail(String email); + void delete(String email); +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetCodeRepository.java b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetCodeRepository.java new file mode 100644 index 0000000..1b8e697 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetCodeRepository.java @@ -0,0 +1,32 @@ +package com.project.dorumdorum.domain.user.infra.repository; + +import com.project.dorumdorum.domain.user.domain.repository.PasswordResetCodeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class RedisPasswordResetCodeRepository implements PasswordResetCodeRepository { + + private final RedisTemplate redisTemplate; + private static final String PASSWORD_RESET = "PASSWORD_RESET:"; + + @Override + public void save(String email, String code) { + redisTemplate.opsForValue().set(PASSWORD_RESET + email, code, Duration.ofMinutes(10)); + } + + @Override + public Optional findByEmail(String email) { + return Optional.ofNullable(redisTemplate.opsForValue().get(PASSWORD_RESET + email)); + } + + @Override + public void delete(String email) { + redisTemplate.delete(PASSWORD_RESET + email); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetVerifiedRepository.java b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetVerifiedRepository.java new file mode 100644 index 0000000..4237186 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisPasswordResetVerifiedRepository.java @@ -0,0 +1,32 @@ +package com.project.dorumdorum.domain.user.infra.repository; + +import com.project.dorumdorum.domain.user.domain.repository.PasswordResetVerifiedRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +@Repository +@RequiredArgsConstructor +public class RedisPasswordResetVerifiedRepository implements PasswordResetVerifiedRepository { + + private final RedisTemplate redisTemplate; + private static final String PASSWORD_RESET_VERIFIED = "PASSWORD_RESET_VERIFIED:"; + private static final String VERIFIED_VALUE = "verified"; + + @Override + public void save(String email) { + redisTemplate.opsForValue().set(PASSWORD_RESET_VERIFIED + email, VERIFIED_VALUE, Duration.ofMinutes(10)); + } + + @Override + public boolean existsByEmail(String email) { + return Boolean.TRUE.equals(redisTemplate.hasKey(PASSWORD_RESET_VERIFIED + email)); + } + + @Override + public void delete(String email) { + redisTemplate.delete(PASSWORD_RESET_VERIFIED + email); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/ResetPasswordController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/ResetPasswordController.java new file mode 100644 index 0000000..38dc5e3 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/ResetPasswordController.java @@ -0,0 +1,25 @@ +package com.project.dorumdorum.domain.user.ui; + +import com.project.dorumdorum.domain.user.application.dto.request.ResetPasswordRequest; +import com.project.dorumdorum.domain.user.application.usecase.ResetPasswordUseCase; +import com.project.dorumdorum.domain.user.ui.spec.ResetPasswordApiSpec; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class ResetPasswordController implements ResetPasswordApiSpec { + + private final ResetPasswordUseCase resetPasswordUseCase; + + @Override + public ResponseEntity reset( + @RequestBody @Valid ResetPasswordRequest request + ) { + resetPasswordUseCase.execute(request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java new file mode 100644 index 0000000..9958daf --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java @@ -0,0 +1,23 @@ +package com.project.dorumdorum.domain.user.ui; + +import com.project.dorumdorum.domain.user.application.usecase.SendPasswordResetEmailUseCase; +import com.project.dorumdorum.domain.user.ui.spec.SendPasswordResetEmailApiSpec; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class SendPasswordResetEmailController implements SendPasswordResetEmailApiSpec { + + private final SendPasswordResetEmailUseCase sendPasswordResetEmailUseCase; + + @Override + public ResponseEntity send( + @RequestParam String email + ) { + sendPasswordResetEmailUseCase.send(email); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java new file mode 100644 index 0000000..d4324e3 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java @@ -0,0 +1,24 @@ +package com.project.dorumdorum.domain.user.ui; + +import com.project.dorumdorum.domain.user.application.usecase.VerifyPasswordResetCodeUseCase; +import com.project.dorumdorum.domain.user.ui.spec.VerifyPasswordResetCodeApiSpec; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class VerifyPasswordResetCodeController implements VerifyPasswordResetCodeApiSpec { + + private final VerifyPasswordResetCodeUseCase verifyPasswordResetCodeUseCase; + + @Override + public ResponseEntity verify( + @RequestParam String email, + @RequestParam String code + ) { + verifyPasswordResetCodeUseCase.execute(email, code); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/spec/ResetPasswordApiSpec.java b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/ResetPasswordApiSpec.java new file mode 100644 index 0000000..46276d5 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/ResetPasswordApiSpec.java @@ -0,0 +1,21 @@ +package com.project.dorumdorum.domain.user.ui.spec; + +import com.project.dorumdorum.domain.user.application.dto.request.ResetPasswordRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "User") +public interface ResetPasswordApiSpec { + + @Operation( + summary = "비밀번호 재설정 API", + description = "이메일 인증 완료 후 새로운 비밀번호로 변경합니다." + ) + @PostMapping("/api/users/password/reset") + ResponseEntity reset( + @RequestBody ResetPasswordRequest request + ); +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/spec/SendPasswordResetEmailApiSpec.java b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/SendPasswordResetEmailApiSpec.java new file mode 100644 index 0000000..d536e41 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/SendPasswordResetEmailApiSpec.java @@ -0,0 +1,20 @@ +package com.project.dorumdorum.domain.user.ui.spec; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; + +@Tag(name = "User") +public interface SendPasswordResetEmailApiSpec { + + @Operation( + summary = "비밀번호 재설정 인증 이메일 발송 API", + description = "가입된 학교 이메일 주소로 비밀번호 재설정 인증 코드를 전송합니다." + ) + @PostMapping("/api/email/password-reset/send") + ResponseEntity send( + @Parameter(description = "비밀번호를 재설정할 사용자 이메일 주소") String email + ); +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/spec/VerifyPasswordResetCodeApiSpec.java b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/VerifyPasswordResetCodeApiSpec.java new file mode 100644 index 0000000..5907aff --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/VerifyPasswordResetCodeApiSpec.java @@ -0,0 +1,21 @@ +package com.project.dorumdorum.domain.user.ui.spec; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; + +@Tag(name = "User") +public interface VerifyPasswordResetCodeApiSpec { + + @Operation( + summary = "비밀번호 재설정 인증 코드 검증 API", + description = "이메일로 받은 인증 코드를 검증합니다. 성공 시 비밀번호 재설정이 가능한 상태로 전환됩니다." + ) + @PostMapping("/api/email/password-reset/verify") + ResponseEntity verify( + @Parameter(description = "인증할 사용자 이메일") String email, + @Parameter(description = "이메일로 전송된 인증 코드") String code + ); +} From 112797ced9e6ecc50dd5dc32509753242b7908b2 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 14:27:25 +0900 Subject: [PATCH 04/15] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20DI=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/usecase/VerifyPasswordResetCodeUseCase.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java index ad35ce6..6ab9f50 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/VerifyPasswordResetCodeUseCase.java @@ -1,19 +1,14 @@ package com.project.dorumdorum.domain.user.application.usecase; -import com.project.dorumdorum.domain.user.domain.repository.PasswordResetCodeRepository; import com.project.dorumdorum.domain.user.domain.repository.PasswordResetVerifiedRepository; import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; -import com.project.dorumdorum.global.exception.RestApiException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.FAILED_EMAIL_VERIFICATION; - @Service @RequiredArgsConstructor public class VerifyPasswordResetCodeUseCase { - private final PasswordResetCodeRepository passwordResetCodeRepository; private final PasswordResetVerifiedRepository passwordResetVerifiedRepository; private final EmailVerificationService emailVerificationService; From bf9f075813fe62a8935e8be419dae317e3683244 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:18:38 +0900 Subject: [PATCH 05/15] =?UTF-8?q?fix:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/CalendarEventResponse.java | 9 ++++++++- .../application/mapper/CalendarEventMapper.java | 2 ++ .../calendar/domain/entity/CalendarEvent.java | 15 +++++++++++++++ .../calendar/domain/entity/CalendarEventType.java | 8 ++++++++ .../domain/room/domain/service/RoomService.java | 3 +-- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEventType.java diff --git a/src/main/java/com/project/dorumdorum/domain/calendar/application/dto/response/CalendarEventResponse.java b/src/main/java/com/project/dorumdorum/domain/calendar/application/dto/response/CalendarEventResponse.java index 4f67c14..8a4ca6c 100644 --- a/src/main/java/com/project/dorumdorum/domain/calendar/application/dto/response/CalendarEventResponse.java +++ b/src/main/java/com/project/dorumdorum/domain/calendar/application/dto/response/CalendarEventResponse.java @@ -1,9 +1,16 @@ package com.project.dorumdorum.domain.calendar.application.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.project.dorumdorum.domain.calendar.domain.entity.CalendarEventType; + import java.time.LocalDate; +import java.time.LocalTime; public record CalendarEventResponse( LocalDate date, - String title + String title, + String content, + @JsonFormat(pattern = "HH:mm") LocalTime time, + CalendarEventType type ) { } diff --git a/src/main/java/com/project/dorumdorum/domain/calendar/application/mapper/CalendarEventMapper.java b/src/main/java/com/project/dorumdorum/domain/calendar/application/mapper/CalendarEventMapper.java index acdcb87..33b6096 100644 --- a/src/main/java/com/project/dorumdorum/domain/calendar/application/mapper/CalendarEventMapper.java +++ b/src/main/java/com/project/dorumdorum/domain/calendar/application/mapper/CalendarEventMapper.java @@ -10,6 +10,8 @@ @Mapper(componentModel = "spring") public interface CalendarEventMapper { @Mapping(target = "date", source = "eventDate") + @Mapping(target = "time", source = "eventTime") + @Mapping(target = "type", source = "eventType") CalendarEventResponse toResponse(CalendarEvent event); List toResponseList(List events); } diff --git a/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java b/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java index 5f352e2..ea7e5d9 100644 --- a/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java +++ b/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java @@ -8,7 +8,12 @@ import jakarta.persistence.Table; import lombok.*; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; + +import javax.annotation.Nullable; import java.time.LocalDate; +import java.time.LocalTime; @Entity @Getter @@ -27,4 +32,14 @@ public class CalendarEvent extends BaseEntity { @Column(nullable = false) private String title; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private LocalTime eventTime; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private CalendarEventType eventType; } diff --git a/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEventType.java b/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEventType.java new file mode 100644 index 0000000..ed0674e --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEventType.java @@ -0,0 +1,8 @@ +package com.project.dorumdorum.domain.calendar.domain.entity; + +public enum CalendarEventType { + CHECK, // 점호 + CLEAN, // 청소 + NOTICE, // 공지 + EVENT // 행사 +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java index da9c00c..ad6934a 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java @@ -57,8 +57,7 @@ public List searchByCursor( } public FindRoomsResponse findMyRoom(String userNo) { - return roomRepository.findMyRoom(userNo) - .orElseThrow(() -> new RestApiException(ROOM_NOT_FOUND)); + return roomRepository.findMyRoom(userNo).orElse(null); } public List findLikedRooms(String userNo) { From 765d82bf205c3ddaea80d0db12f9589a83912c2b Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:18:48 +0900 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=85=EB=A0=A5=EA=B0=92=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/dto/request/ResetPasswordRequest.java | 4 +++- .../domain/user/application/dto/request/SignUpRequest.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java index 646c1da..2cb9bea 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/ResetPasswordRequest.java @@ -2,10 +2,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; public record ResetPasswordRequest( @Email @NotBlank String email, - @NotBlank String newPassword, + @NotBlank @Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.") @Pattern(regexp = "^[A-Za-z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]+$", message = "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.") String newPassword, @NotBlank String newPasswordCheck ) { public boolean isPasswordMatch() { diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/SignUpRequest.java b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/SignUpRequest.java index 16bd329..8bf199f 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/SignUpRequest.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/SignUpRequest.java @@ -5,6 +5,8 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -15,7 +17,7 @@ public record SignUpRequest( @NotBlank String name, @NotBlank String nickname, @Email @NotBlank String email, - @NotBlank String password, + @NotBlank @Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.") @Pattern(regexp = "^[A-Za-z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]+$", message = "비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.") String password, @NotBlank String passwordCheck, @NotNull Gender gender, @NotBlank String studentNo, From d9c024795c3878b8165d356bb804e438af2417a3 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:22:57 +0900 Subject: [PATCH 07/15] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/usecase/ResetPasswordUseCase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java index 88c4e38..d9b37f1 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/ResetPasswordUseCase.java @@ -38,7 +38,7 @@ public void execute(ResetPasswordRequest request) { } User user = userService.findByEmail(request.email()); - passwordResetVerifiedRepository.delete(request.email()); user.updatePassword(passwordEncoder.encode(request.newPassword())); + passwordResetVerifiedRepository.delete(request.email()); } } From 24e0de49495b8f7502be33eccf96bd44895e19a8 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:25:34 +0900 Subject: [PATCH 08/15] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=97=B4=EA=B1=B0=20=EA=B3=B5=EA=B2=A9=20=EB=B0=A9=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/SendPasswordResetEmailUseCase.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java index e40821d..eb0fa78 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Service; import static com.project.dorumdorum.global.exception.code.status.AuthErrorStatus.INVALID_EMAIL_DOMAIN; -import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.EMAIL_NOT_FOUND; @Service @RequiredArgsConstructor @@ -20,19 +19,18 @@ public class SendPasswordResetEmailUseCase { /** * 비밀번호 재설정 인증 코드 발송 - * - 가입된 이메일인지 확인 * - 허용된 대학 이메일 도메인인지 검증 - * - 인증 코드를 생성해 메일로 발송 + * - 가입된 이메일인 경우에만 실제로 코드를 발송 (미가입 이메일도 동일한 200 응답 반환) */ public void send(String email) { - if (!userService.isAlreadyRegistered(email)) { - throw new RestApiException(EMAIL_NOT_FOUND); - } - if (!emailVerificationService.isAllowedUniversityEmail(email)) { throw new RestApiException(INVALID_EMAIL_DOMAIN); } + if (!userService.isAlreadyRegistered(email)) { + return; + } + String code = secureRandomGenerator.generate(); emailVerificationService.sendCode(email, code); } From 3729f0954e09fab310d934376e6f4eaa462cd85f Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:28:07 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EB=B0=8B=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/SendPasswordResetEmailUseCase.java | 2 ++ .../application/usecase/SendVerificationEmailUseCase.java | 2 ++ src/main/resources/application.yml | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java index eb0fa78..896bf0d 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendPasswordResetEmailUseCase.java @@ -3,6 +3,7 @@ import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; import com.project.dorumdorum.domain.user.domain.service.UserService; import com.project.dorumdorum.global.exception.RestApiException; +import com.project.dorumdorum.global.ratelimit.RateLimited; import com.project.dorumdorum.global.util.SecureRandomGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ public class SendPasswordResetEmailUseCase { * - 허용된 대학 이메일 도메인인지 검증 * - 가입된 이메일인 경우에만 실제로 코드를 발송 (미가입 이메일도 동일한 200 응답 반환) */ + @RateLimited(tag = "password-reset-email", key = "#email") public void send(String email) { if (!emailVerificationService.isAllowedUniversityEmail(email)) { throw new RestApiException(INVALID_EMAIL_DOMAIN); diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendVerificationEmailUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendVerificationEmailUseCase.java index 08d2308..835c85a 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendVerificationEmailUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/SendVerificationEmailUseCase.java @@ -3,6 +3,7 @@ import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; import com.project.dorumdorum.domain.user.domain.service.UserService; import com.project.dorumdorum.global.exception.RestApiException; +import com.project.dorumdorum.global.ratelimit.RateLimited; import com.project.dorumdorum.global.util.SecureRandomGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -24,6 +25,7 @@ public class SendVerificationEmailUseCase { * - 허용된 대학 이메일 도메인인지 검증 * - 인증 코드를 생성해 메일로 발송 */ + @RateLimited(tag = "verification-email", key = "#email") public void send(String email) { if (userService.isAlreadyRegistered(email)) { throw new RestApiException(DUPLICATE_EMAIL); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 15e169e..12cd74f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -211,3 +211,11 @@ rate-limit: permits-per-window: ${CHAT_RATE_LIMIT_PER_WINDOW:30} window-millis: ${CHAT_RATE_LIMIT_WINDOW_MILLIS:10000} ttl-seconds: ${CHAT_RATE_LIMIT_TTL_SECONDS:20} + password-reset-email: + permits-per-window: 1 + window-millis: 30000 + ttl-seconds: 30 + verification-email: + permits-per-window: 1 + window-millis: 30000 + ttl-seconds: 30 From e38de9ac74ede760785f1b8198dd453ce3e61bea Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:32:04 +0900 Subject: [PATCH 10/15] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=B2=B4=EB=A5=98=20=EC=8B=9C=EA=B0=84=20=EC=97=B0?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/infra/repository/RedisEmailVerifiedRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java index e9d3289..d98654b 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/user/infra/repository/RedisEmailVerifiedRepository.java @@ -17,7 +17,7 @@ public class RedisEmailVerifiedRepository implements EmailVerifiedRepository { @Override public void save(String email) { - redisTemplate.opsForValue().set(EMAIL_VERIFIED + email, VERIFIED_VALUE, Duration.ofMinutes(10)); + redisTemplate.opsForValue().set(EMAIL_VERIFIED + email, VERIFIED_VALUE, Duration.ofMinutes(30)); } @Override From 9a03a5163a175c163be40c223fd910aeb5e239cc Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:39:59 +0900 Subject: [PATCH 11/15] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/ui/SendPasswordResetEmailController.java | 5 ++++- .../domain/user/ui/SendVerificationEmailController.java | 5 ++++- .../dorumdorum/domain/user/ui/VerifyEmailController.java | 7 +++++-- .../domain/user/ui/VerifyPasswordResetCodeController.java | 7 +++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java index 9958daf..e74ba11 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/SendPasswordResetEmailController.java @@ -2,11 +2,14 @@ import com.project.dorumdorum.domain.user.application.usecase.SendPasswordResetEmailUseCase; import com.project.dorumdorum.domain.user.ui.spec.SendPasswordResetEmailApiSpec; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Validated @RestController @RequiredArgsConstructor public class SendPasswordResetEmailController implements SendPasswordResetEmailApiSpec { @@ -15,7 +18,7 @@ public class SendPasswordResetEmailController implements SendPasswordResetEmailA @Override public ResponseEntity send( - @RequestParam String email + @NotBlank @RequestParam String email ) { sendPasswordResetEmailUseCase.send(email); return ResponseEntity.ok().build(); diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/SendVerificationEmailController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/SendVerificationEmailController.java index 0c57548..a6dc007 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/ui/SendVerificationEmailController.java +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/SendVerificationEmailController.java @@ -2,11 +2,14 @@ import com.project.dorumdorum.domain.user.application.usecase.SendVerificationEmailUseCase; import com.project.dorumdorum.domain.user.ui.spec.SendVerificationEmailApiSpec; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Validated @RestController @RequiredArgsConstructor public class SendVerificationEmailController implements SendVerificationEmailApiSpec { @@ -15,7 +18,7 @@ public class SendVerificationEmailController implements SendVerificationEmailApi @Override public ResponseEntity send( - @RequestParam String email + @NotBlank @RequestParam String email ) { sendVerificationEmailUseCase.send(email); return ResponseEntity.ok().build(); diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyEmailController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyEmailController.java index 8705963..ca21f6e 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyEmailController.java +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyEmailController.java @@ -2,11 +2,14 @@ import com.project.dorumdorum.domain.user.application.usecase.VerifyEmailUseCase; import com.project.dorumdorum.domain.user.ui.spec.VerifyEmailApiSpec; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Validated @RestController @RequiredArgsConstructor public class VerifyEmailController implements VerifyEmailApiSpec { @@ -15,8 +18,8 @@ public class VerifyEmailController implements VerifyEmailApiSpec { @Override public ResponseEntity verifyEmail( - @RequestParam String email, - @RequestParam String code + @NotBlank @RequestParam String email, + @NotBlank @RequestParam String code ) { verifyEmailUseCase.execute(email, code); return ResponseEntity.ok().build(); diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java index d4324e3..d1a206d 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/VerifyPasswordResetCodeController.java @@ -2,11 +2,14 @@ import com.project.dorumdorum.domain.user.application.usecase.VerifyPasswordResetCodeUseCase; import com.project.dorumdorum.domain.user.ui.spec.VerifyPasswordResetCodeApiSpec; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Validated @RestController @RequiredArgsConstructor public class VerifyPasswordResetCodeController implements VerifyPasswordResetCodeApiSpec { @@ -15,8 +18,8 @@ public class VerifyPasswordResetCodeController implements VerifyPasswordResetCod @Override public ResponseEntity verify( - @RequestParam String email, - @RequestParam String code + @NotBlank @RequestParam String email, + @NotBlank @RequestParam String code ) { verifyPasswordResetCodeUseCase.execute(email, code); return ResponseEntity.ok().build(); From 97eba3c5da395cd694d80fad6ee1c71a62f2bdd1 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:42:01 +0900 Subject: [PATCH 12/15] =?UTF-8?q?fix:=20Discord=20=EC=9B=B9=ED=9B=85=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dorumdorum/global/alert/DiscordAlertSender.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/project/dorumdorum/global/alert/DiscordAlertSender.java b/src/main/java/com/project/dorumdorum/global/alert/DiscordAlertSender.java index 1528471..976b13a 100644 --- a/src/main/java/com/project/dorumdorum/global/alert/DiscordAlertSender.java +++ b/src/main/java/com/project/dorumdorum/global/alert/DiscordAlertSender.java @@ -28,8 +28,13 @@ public class DiscordAlertSender { private final HttpClient httpClient = HttpClient.newHttpClient(); public void send(SystemAlert alert) { - if (deduplicationService.isDuplicate(alert)) { - log.debug("[Alert] 중복 알림 건너뜀. title={}", alert.title()); + try { + if (deduplicationService.isDuplicate(alert)) { + log.debug("[Alert] 중복 알림 건너뜀. title={}", alert.title()); + return; + } + } catch (IllegalStateException e) { + log.warn("[Alert] Redis 비가용 상태로 중복 검사 건너뜀. title={}", alert.title()); return; } From 194ca802d91510f09bbb9756c4434fd9ed620647 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:44:31 +0900 Subject: [PATCH 13/15] =?UTF-8?q?test:=20=EA=B8=B0=EC=A1=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../calendar/unit/response/CalendarEventResponseTest.java | 2 +- .../calendar/unit/ui/LoadCalendarEventsControllerTest.java | 2 +- .../calendar/unit/usecase/LoadCalendarEventsUseCaseTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/project/dorumdorum/domain/calendar/unit/response/CalendarEventResponseTest.java b/src/test/java/com/project/dorumdorum/domain/calendar/unit/response/CalendarEventResponseTest.java index 49b06e3..f24240e 100644 --- a/src/test/java/com/project/dorumdorum/domain/calendar/unit/response/CalendarEventResponseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/calendar/unit/response/CalendarEventResponseTest.java @@ -14,7 +14,7 @@ class CalendarEventResponseTest { @Test void record_AccessorsReturnValues() { LocalDate date = LocalDate.of(2026, 2, 14); - CalendarEventResponse response = new CalendarEventResponse(date, "event"); + CalendarEventResponse response = new CalendarEventResponse(date, "event", null, null, null); assertThat(response.date()).isEqualTo(date); assertThat(response.title()).isEqualTo("event"); diff --git a/src/test/java/com/project/dorumdorum/domain/calendar/unit/ui/LoadCalendarEventsControllerTest.java b/src/test/java/com/project/dorumdorum/domain/calendar/unit/ui/LoadCalendarEventsControllerTest.java index befbe66..5e7cdc3 100644 --- a/src/test/java/com/project/dorumdorum/domain/calendar/unit/ui/LoadCalendarEventsControllerTest.java +++ b/src/test/java/com/project/dorumdorum/domain/calendar/unit/ui/LoadCalendarEventsControllerTest.java @@ -29,7 +29,7 @@ class LoadCalendarEventsControllerTest { void loadCalendarEvents_ReturnsUseCaseResult() { LocalDate start = LocalDate.of(2026, 1, 1); LocalDate end = LocalDate.of(2026, 1, 31); - List payload = List.of(new CalendarEventResponse(start, "event")); + List payload = List.of(new CalendarEventResponse(start, "event", null, null, null)); when(loadCalendarEventsUseCase.execute(start, end)).thenReturn(payload); ResponseEntity> response = controller.loadCalendarEvents(start, end); diff --git a/src/test/java/com/project/dorumdorum/domain/calendar/unit/usecase/LoadCalendarEventsUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/calendar/unit/usecase/LoadCalendarEventsUseCaseTest.java index 2dbae3c..10ed4ec 100644 --- a/src/test/java/com/project/dorumdorum/domain/calendar/unit/usecase/LoadCalendarEventsUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/calendar/unit/usecase/LoadCalendarEventsUseCaseTest.java @@ -34,7 +34,7 @@ void execute_LoadsEventsAndMapsResponse() { List events = List.of( CalendarEvent.builder().eventNo("e1").eventDate(start).title("title").build() ); - List responses = List.of(new CalendarEventResponse(start, "title")); + List responses = List.of(new CalendarEventResponse(start, "title", null, null, null)); when(calendarEventService.loadBetween(start, end)).thenReturn(events); when(calendarEventMapper.toResponseList(events)).thenReturn(responses); From ab9fd8105f7fbb769840b4dfac7a489772794439 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:52:40 +0900 Subject: [PATCH 14/15] =?UTF-8?q?fix:=20CI=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=EC=97=90=20Redis=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=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 --- .github/workflows/cicd.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index d5a6a93..c19616b 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -17,6 +17,17 @@ jobs: name: Test runs-on: ubuntu-latest + services: + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout uses: actions/checkout@v4 From 4a17361818d2974681e813f2e797c345a5b54a83 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 31 May 2026 15:59:29 +0900 Subject: [PATCH 15/15] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/room/unit/service/RoomServiceTest.java | 6 +++--- .../domain/user/unit/usecase/SignUpUseCaseTest.java | 6 ++++++ .../domain/user/unit/usecase/VerifyEmailUseCaseTest.java | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java index 4aa5939..7f4441f 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java @@ -80,9 +80,9 @@ void searchByCursor_DelegatesToRepository() { } @Test - @DisplayName("Should throw when my room is not found") - void findMyRoom_WhenMissing_Throws() { + @DisplayName("Should return null when my room is not found") + void findMyRoom_WhenMissing_ReturnsNull() { when(roomRepository.findMyRoom("u1")).thenReturn(Optional.empty()); - assertThatThrownBy(() -> service.findMyRoom("u1")).isInstanceOf(RestApiException.class); + assertThat(service.findMyRoom("u1")).isNull(); } } diff --git a/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/SignUpUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/SignUpUseCaseTest.java index 81e376e..1be8a1a 100644 --- a/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/SignUpUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/SignUpUseCaseTest.java @@ -3,6 +3,7 @@ import com.project.dorumdorum.domain.user.application.dto.request.SignUpRequest; import com.project.dorumdorum.domain.user.application.usecase.SignUpUseCase; import com.project.dorumdorum.domain.user.domain.entity.User; +import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository; import com.project.dorumdorum.domain.user.domain.service.UserService; import com.project.dorumdorum.domain.user.fixture.RequestFixture; import com.project.dorumdorum.domain.user.fixture.UserFixture; @@ -28,6 +29,8 @@ class SignUpUseCaseTest { @Mock private UserService userService; @Mock + private EmailVerifiedRepository emailVerifiedRepository; + @Mock private DomainEventLogger domainEventLogger; @InjectMocks @@ -40,6 +43,7 @@ void execute_WithValidRequest_SavesUser() { SignUpRequest request = RequestFixture.createValidSignUpRequest(); User savedUser = UserFixture.createDefaultUser(); + when(emailVerifiedRepository.existsByEmail(request.email())).thenReturn(true); when(userService.isAlreadyRegistered(request.email())).thenReturn(false); when(userService.save(request)).thenReturn(savedUser); @@ -70,6 +74,7 @@ void execute_WithPasswordMismatch_ThrowsPasswordNotMatchesException() { void execute_WithAlreadyRegisteredEmail_ThrowsAlreadyRegisteredEmailException() { // Arrange SignUpRequest request = RequestFixture.createValidSignUpRequest(); + when(emailVerifiedRepository.existsByEmail(request.email())).thenReturn(true); when(userService.isAlreadyRegistered(request.email())).thenReturn(true); // Act & Assert @@ -87,6 +92,7 @@ void execute_WithValidRequest_CallsUserServiceInCorrectOrder() { SignUpRequest request = RequestFixture.createValidSignUpRequest(); User savedUser = UserFixture.createDefaultUser(); + when(emailVerifiedRepository.existsByEmail(request.email())).thenReturn(true); when(userService.isAlreadyRegistered(request.email())).thenReturn(false); when(userService.save(request)).thenReturn(savedUser); diff --git a/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/VerifyEmailUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/VerifyEmailUseCaseTest.java index 4357f65..0dd03ad 100644 --- a/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/VerifyEmailUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/user/unit/usecase/VerifyEmailUseCaseTest.java @@ -1,6 +1,7 @@ package com.project.dorumdorum.domain.user.unit.usecase; import com.project.dorumdorum.domain.user.application.usecase.VerifyEmailUseCase; +import com.project.dorumdorum.domain.user.domain.repository.EmailVerifiedRepository; import com.project.dorumdorum.domain.user.domain.service.EmailVerificationService; import com.project.dorumdorum.global.logging.DomainEventLogger; import org.junit.jupiter.api.DisplayName; @@ -19,6 +20,8 @@ class VerifyEmailUseCaseTest { @Mock private EmailVerificationService emailVerificationService; @Mock + private EmailVerifiedRepository emailVerifiedRepository; + @Mock private DomainEventLogger domainEventLogger; @InjectMocks