From 36706d02c9b20ad17b35a29faed822319c6bf4e1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 5 Jul 2025 17:15:50 +0900 Subject: [PATCH 01/40] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/annotation/ValidPassword.java | 15 ++++++++++++ .../PasswordConstraintValidator.java | 24 +++++++++++++++++++ .../flipnote/user/model/UserRegisterDto.java | 3 ++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/main/java/project/flipnote/common/annotation/ValidPassword.java create mode 100644 src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java diff --git a/src/main/java/project/flipnote/common/annotation/ValidPassword.java b/src/main/java/project/flipnote/common/annotation/ValidPassword.java new file mode 100644 index 00000000..86ae31ff --- /dev/null +++ b/src/main/java/project/flipnote/common/annotation/ValidPassword.java @@ -0,0 +1,15 @@ +package project.flipnote.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import project.flipnote.common.validator.PasswordConstraintValidator; + +@Constraint(validatedBy = PasswordConstraintValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPassword { +} diff --git a/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java b/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java new file mode 100644 index 00000000..58173c38 --- /dev/null +++ b/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java @@ -0,0 +1,24 @@ +package project.flipnote.common.validator; + +import java.util.Objects; +import java.util.regex.Pattern; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import project.flipnote.common.annotation.ValidPassword; + +public class PasswordConstraintValidator implements ConstraintValidator { + + private static final String PASSWORD_PATTERN = + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$^*()_\\-])[A-Za-z\\d!@#$^*()_\\-]{8,16}$"; + private static final Pattern pattern = Pattern.compile(PASSWORD_PATTERN); + + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + if (Objects.isNull(password)) { + return false; + } + + return pattern.matcher(password).matches(); + } +} \ No newline at end of file diff --git a/src/main/java/project/flipnote/user/model/UserRegisterDto.java b/src/main/java/project/flipnote/user/model/UserRegisterDto.java index f471b479..57e8faa5 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterDto.java +++ b/src/main/java/project/flipnote/user/model/UserRegisterDto.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import project.flipnote.common.annotation.ValidPassword; public class UserRegisterDto { @@ -10,7 +11,7 @@ public record Request( @Email @NotEmpty String email, - @NotEmpty + @ValidPassword String password, @NotEmpty From f04637f71569a4d65c863f2b991e243ce2212a61 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 5 Jul 2025 17:26:05 +0900 Subject: [PATCH 02/40] =?UTF-8?q?Style:=20=EC=BD=94=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/GlobalExceptionHandler.java | 4 +++- .../project/flipnote/common/response/ApiResponse.java | 6 +++--- .../common/security/filter/ExceptionHandlerFilter.java | 2 +- .../java/project/flipnote/common/util/CookieUtil.java | 9 ++++++++- .../common/validator/PasswordConstraintValidator.java | 2 +- .../java/project/flipnote/user/service/UserService.java | 2 +- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java b/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java index e9387584..2e428fb6 100644 --- a/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java @@ -39,7 +39,9 @@ public ResponseEntity> handleGeneralError(Exception exception) } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity>> handleValidationError(MethodArgumentNotValidException exception) { + public ResponseEntity>> handleValidationError( + MethodArgumentNotValidException exception + ) { return ResponseEntity .badRequest() .body(ApiResponse.error(CommonErrorCode.INVALID_INPUT_VALUE, exception.getBindingResult())); diff --git a/src/main/java/project/flipnote/common/response/ApiResponse.java b/src/main/java/project/flipnote/common/response/ApiResponse.java index 332b2711..d21efcac 100644 --- a/src/main/java/project/flipnote/common/response/ApiResponse.java +++ b/src/main/java/project/flipnote/common/response/ApiResponse.java @@ -5,15 +5,15 @@ import org.springframework.validation.BindingResult; -import project.flipnote.common.exception.ErrorCode; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import project.flipnote.common.exception.ErrorCode; + @Getter @Builder -public class ApiResponse { +public class ApiResponse { private final int status; private final String code; diff --git a/src/main/java/project/flipnote/common/security/filter/ExceptionHandlerFilter.java b/src/main/java/project/flipnote/common/security/filter/ExceptionHandlerFilter.java index 7c3ac21f..fd9ca058 100644 --- a/src/main/java/project/flipnote/common/security/filter/ExceptionHandlerFilter.java +++ b/src/main/java/project/flipnote/common/security/filter/ExceptionHandlerFilter.java @@ -41,4 +41,4 @@ private void setErrorResponse(HttpServletResponse response, SecurityException ex response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); } -} \ No newline at end of file +} diff --git a/src/main/java/project/flipnote/common/util/CookieUtil.java b/src/main/java/project/flipnote/common/util/CookieUtil.java index baa1d102..7f6f5337 100644 --- a/src/main/java/project/flipnote/common/util/CookieUtil.java +++ b/src/main/java/project/flipnote/common/util/CookieUtil.java @@ -8,7 +8,14 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class CookieUtil { - public static void addCookie(HttpServletResponse response, String name, String value, int maxAge, boolean httpOnly, String path) { + public static void addCookie( + HttpServletResponse response, + String name, + String value, + int maxAge, + boolean httpOnly, + String path + ) { Cookie cookie = new Cookie(name, value); cookie.setMaxAge(maxAge); cookie.setHttpOnly(httpOnly); diff --git a/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java b/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java index 58173c38..14acecca 100644 --- a/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java +++ b/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java @@ -21,4 +21,4 @@ public boolean isValid(String password, ConstraintValidatorContext context) { return pattern.matcher(password).matches(); } -} \ No newline at end of file +} diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index e40852ca..7427ef7b 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -5,9 +5,9 @@ import lombok.RequiredArgsConstructor; import project.flipnote.common.exception.BizException; -import project.flipnote.user.model.UserRegisterDto; import project.flipnote.user.entity.User; import project.flipnote.user.exception.UserErrorCode; +import project.flipnote.user.model.UserRegisterDto; import project.flipnote.user.repository.UserRepository; @RequiredArgsConstructor From bbf72a642c6c62a41dc60a910220419a39fa0006 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 5 Jul 2025 17:26:20 +0900 Subject: [PATCH 03/40] =?UTF-8?q?Test:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EA=B8=B0=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PasswordConstraintValidatorTest.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java diff --git a/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java b/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java new file mode 100644 index 00000000..17308aa5 --- /dev/null +++ b/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java @@ -0,0 +1,57 @@ +package project.flipnote.common.validator; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.validation.ConstraintValidatorContext; + +@DisplayName("비밀번호 유효성 검증기 단위 테스트") +@ExtendWith(MockitoExtension.class) +class PasswordConstraintValidatorTest { + + PasswordConstraintValidator validator = new PasswordConstraintValidator(); + + @Mock + ConstraintValidatorContext context; + + @Test + @DisplayName("유효한 비밀번호는 true를 반환한다") + void validPassword() { + assertThat(validator.isValid("Abc12345!", context)).isTrue(); + assertThat(validator.isValid("A1b2c3d4@", context)).isTrue(); + assertThat(validator.isValid("aB1!abcd", context)).isTrue(); + } + + @Test + @DisplayName("영문, 숫자, 특수문자 중 하나라도 빠지면 false") + void invalidPassword_missingType() { + assertThat(validator.isValid("abcdefgh", context)).isFalse(); + assertThat(validator.isValid("12345678!", context)).isFalse(); + assertThat(validator.isValid("Abcdefgh1", context)).isFalse(); + } + + @Test + @DisplayName("허용되지 않은 특수문자 포함 시 false") + void invalidPassword_wrongSpecialChar() { + assertThat(validator.isValid("Abc12345%", context)).isFalse(); + assertThat(validator.isValid("Abc12345?", context)).isFalse(); + } + + @Test + @DisplayName("길이 제한 위반 시 false") + void invalidPassword_length() { + assertThat(validator.isValid("A1!a", context)).isFalse(); + assertThat(validator.isValid("Abcdefghijk1!2345", context)).isFalse(); + } + + @Test + @DisplayName("null 입력 시 false") + void nullPassword() { + assertThat(validator.isValid(null, context)).isFalse(); + } +} From a56a9a193605168fa57695c4820419f56e4cd37c Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 5 Jul 2025 17:34:01 +0900 Subject: [PATCH 04/40] =?UTF-8?q?Feat:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20=ED=95=98=EC=9D=B4=ED=94=88(-)?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20=ED=9B=84=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/user/model/UserRegisterDto.java | 4 ++++ src/main/java/project/flipnote/user/service/UserService.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/user/model/UserRegisterDto.java b/src/main/java/project/flipnote/user/model/UserRegisterDto.java index 57e8faa5..4082c1ef 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterDto.java +++ b/src/main/java/project/flipnote/user/model/UserRegisterDto.java @@ -28,6 +28,10 @@ public record Request( String profileImageUrl ) { + + public String getCleanedPhone() { + return phone == null ? null : phone.replaceAll("-", ""); + } } public record Response( diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 7427ef7b..4c452503 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -27,7 +27,7 @@ public UserRegisterDto.Response register(UserRegisterDto.Request req) { .name(req.name()) .nickname(req.nickname()) .smsAgree(req.smsAgree()) - .phone(req.phone()) + .phone(req.getCleanedPhone()) .profileImageUrl(req.profileImageUrl()) .build(); userRepository.save(user); From 3fac57da4861eb2163a60427f1fae0f43e9545f1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 5 Jul 2025 17:51:43 +0900 Subject: [PATCH 05/40] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=ED=9C=B4=EB=8C=80=EC=A0=84=ED=99=94=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/common/annotation/ValidPhone.java | 15 +++++++++++++++ .../validator/PhoneConstraintValidator.java | 18 ++++++++++++++++++ .../flipnote/user/model/UserRegisterDto.java | 3 ++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/main/java/project/flipnote/common/annotation/ValidPhone.java create mode 100644 src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java diff --git a/src/main/java/project/flipnote/common/annotation/ValidPhone.java b/src/main/java/project/flipnote/common/annotation/ValidPhone.java new file mode 100644 index 00000000..5aaaedb6 --- /dev/null +++ b/src/main/java/project/flipnote/common/annotation/ValidPhone.java @@ -0,0 +1,15 @@ +package project.flipnote.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import project.flipnote.common.validator.PasswordConstraintValidator; + +@Constraint(validatedBy = PasswordConstraintValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPhone { +} diff --git a/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java b/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java new file mode 100644 index 00000000..fad2c334 --- /dev/null +++ b/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java @@ -0,0 +1,18 @@ +package project.flipnote.common.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import project.flipnote.common.annotation.ValidPhone; + +public class PhoneConstraintValidator implements ConstraintValidator { + + private static final String PHONE_PATTERN = "^010-\\d{4}-\\d{4}$"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null || value.isBlank()) { + return false; + } + return value.matches(PHONE_PATTERN); + } +} diff --git a/src/main/java/project/flipnote/user/model/UserRegisterDto.java b/src/main/java/project/flipnote/user/model/UserRegisterDto.java index 4082c1ef..4b2b89fb 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterDto.java +++ b/src/main/java/project/flipnote/user/model/UserRegisterDto.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import project.flipnote.common.annotation.ValidPassword; +import project.flipnote.common.annotation.ValidPhone; public class UserRegisterDto { @@ -23,7 +24,7 @@ public record Request( @NotNull Boolean smsAgree, - @NotEmpty + @ValidPhone String phone, String profileImageUrl From f8aab934daaebeaf5b3b8ea319a61c8ace61a84c Mon Sep 17 00:00:00 2001 From: dungbik Date: Sat, 5 Jul 2025 17:52:11 +0900 Subject: [PATCH 06/40] =?UTF-8?q?Test:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EA=B8=B0=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PhoneConstraintValidatorTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java diff --git a/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java b/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java new file mode 100644 index 00000000..a129018e --- /dev/null +++ b/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java @@ -0,0 +1,65 @@ +package project.flipnote.common.validator; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.validation.ConstraintValidatorContext; + +@DisplayName("휴대폰 번호 유효성 검증기 단위 테스트") +@ExtendWith(MockitoExtension.class) +class PhoneConstraintValidatorTest { + + PhoneConstraintValidator validator = new PhoneConstraintValidator(); + + @Mock + ConstraintValidatorContext context; + + @Test + @DisplayName("010-1234-5678 형식은 true를 반환한다") + void validPhoneWithHyphens() { + assertThat(validator.isValid("010-1234-5678", context)).isTrue(); + assertThat(validator.isValid("010-4567-1238", context)).isTrue(); + } + + @Test + @DisplayName("01012345678 형식은 false를 반환한다") + void validPhoneWithoutHyphens() { + assertThat(validator.isValid("01012345678", context)).isFalse(); + } + + @Test + @DisplayName("010-123-4567 등 잘못된 자리수는 false") + void invalidPhoneWrongDigits() { + assertThat(validator.isValid("010-123-4567", context)).isFalse(); + assertThat(validator.isValid("010-12345-6789", context)).isFalse(); + assertThat(validator.isValid("010-12345-678", context)).isFalse(); + assertThat(validator.isValid("010-12345-67890", context)).isFalse(); + } + + @Test + @DisplayName("010이 아닌 번호는 false") + void invalidPhoneNot010() { + assertThat(validator.isValid("011-1234-5678", context)).isFalse(); + assertThat(validator.isValid("019-1234-5678", context)).isFalse(); + } + + @Test + @DisplayName("숫자가 아닌 문자가 포함되면 false") + void invalidPhoneWithLetters() { + assertThat(validator.isValid("010-ABCD-5678", context)).isFalse(); + assertThat(validator.isValid("0101234abcd", context)).isFalse(); + } + + @Test + @DisplayName("null 또는 빈 문자열은 false") + void nullOrEmpty() { + assertThat(validator.isValid(null, context)).isFalse(); + assertThat(validator.isValid("", context)).isFalse(); + assertThat(validator.isValid(" ", context)).isFalse(); + } +} From 2a828a1628767afdbcbf8049280c92db83694a6f Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 13:01:34 +0900 Subject: [PATCH 07/40] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +- .../auth/constants/VerificationConstants.java | 10 +++ .../auth/controller/AuthController.java | 8 +++ .../auth/exception/AuthErrorCode.java | 3 +- .../EmailVerificationEventListener.java | 37 ++++++++++ .../auth/model/EmailVerificationDto.java | 14 ++++ .../EmailVerificationRedisRepository.java | 23 ++++++ .../flipnote/auth/service/AuthService.java | 33 ++++++++- .../flipnote/common/config/AsyncConfig.java | 29 ++++++++ .../common/config/AsyncProperties.java | 17 +++++ .../flipnote/common/config/RedisConfig.java | 20 ++++++ .../common/exception/EmailSendException.java | 7 ++ .../security/config/SecurityConfig.java | 2 +- .../event/EmailVerificationSendEvent.java | 8 +++ .../flipnote/infra/config/ResendConfig.java | 20 ++++++ .../infra/config/ResendProperties.java | 16 +++++ .../flipnote/infra/email/EmailService.java | 6 ++ .../infra/email/ResendEmailService.java | 47 ++++++++++++ src/main/resources/application.yml | 27 ++++++- .../templates/email/email-verification.html | 72 +++++++++++++++++++ 20 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 src/main/java/project/flipnote/auth/constants/VerificationConstants.java create mode 100644 src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java create mode 100644 src/main/java/project/flipnote/auth/model/EmailVerificationDto.java create mode 100644 src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java create mode 100644 src/main/java/project/flipnote/common/config/AsyncConfig.java create mode 100644 src/main/java/project/flipnote/common/config/AsyncProperties.java create mode 100644 src/main/java/project/flipnote/common/config/RedisConfig.java create mode 100644 src/main/java/project/flipnote/common/exception/EmailSendException.java create mode 100644 src/main/java/project/flipnote/event/EmailVerificationSendEvent.java create mode 100644 src/main/java/project/flipnote/infra/config/ResendConfig.java create mode 100644 src/main/java/project/flipnote/infra/config/ResendProperties.java create mode 100644 src/main/java/project/flipnote/infra/email/EmailService.java create mode 100644 src/main/java/project/flipnote/infra/email/ResendEmailService.java create mode 100644 src/main/resources/templates/email/email-verification.html diff --git a/build.gradle b/build.gradle index 8f44e7d6..bad173c4 100644 --- a/build.gradle +++ b/build.gradle @@ -28,9 +28,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-aop' + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' implementation 'io.jsonwebtoken:jjwt:0.12.6' + implementation 'com.resend:resend-java:4.1.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/project/flipnote/auth/constants/VerificationConstants.java b/src/main/java/project/flipnote/auth/constants/VerificationConstants.java new file mode 100644 index 00000000..ad5219ad --- /dev/null +++ b/src/main/java/project/flipnote/auth/constants/VerificationConstants.java @@ -0,0 +1,10 @@ +package project.flipnote.auth.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class VerificationConstants { + public static final int CODE_LENGTH = 6; + public static final int CODE_TTL_MINUTES = 5; +} diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index 9a879c05..ea7ad2f5 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.model.EmailVerificationDto; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginDto; import project.flipnote.auth.service.AuthService; @@ -40,4 +41,11 @@ public ResponseEntity logout(HttpServletResponse servletResponse) { return ResponseEntity.ok().build(); } + + @PostMapping("/email") + public ResponseEntity sendEmailVerificationCode(@Valid @RequestBody EmailVerificationDto.Request req) { + authService.sendEmailVerificationCode(req); + + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java index 34dcefda..952c10e9 100644 --- a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java +++ b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java @@ -10,7 +10,8 @@ @RequiredArgsConstructor public enum AuthErrorCode implements ErrorCode { - INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "이메일 또는 비밀번호가 올바르지 않습니다."); + INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "이메일 또는 비밀번호가 올바르지 않습니다."), + ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_002", "이미 발급된 인증번호가 있습니다. 잠시 후 다시 시도해 주세요."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java new file mode 100644 index 00000000..e919f176 --- /dev/null +++ b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java @@ -0,0 +1,37 @@ +package project.flipnote.auth.listener; + +import org.springframework.context.event.EventListener; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.common.exception.EmailSendException; +import project.flipnote.event.EmailVerificationSendEvent; +import project.flipnote.infra.email.EmailService; + +@Slf4j +@RequiredArgsConstructor +@Component +public class EmailVerificationEventListener { + + private final EmailService emailService; + + @Async + @Retryable( + retryFor = { EmailSendException.class }, + backoff = @Backoff(delay = 2000, multiplier = 2) + ) + @EventListener + public void handleEmailVerificationSendEvent(EmailVerificationSendEvent event) { + emailService.sendEmailVerificationCode(event.to(), event.code(), event.ttl()); + } + + @Recover + public void recover(EmailSendException ex, EmailVerificationSendEvent event) { + log.error("이메일 인증번호 전송 3회 실패: to={}", event.to(), ex); + } +} diff --git a/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java b/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java new file mode 100644 index 00000000..26583238 --- /dev/null +++ b/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java @@ -0,0 +1,14 @@ +package project.flipnote.auth.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; + +public class EmailVerificationDto { + + public record Request( + + @Email @NotEmpty + String email + ) { + } +} diff --git a/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java b/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java new file mode 100644 index 00000000..049f5a1e --- /dev/null +++ b/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java @@ -0,0 +1,23 @@ +package project.flipnote.auth.repository; + +import java.time.Duration; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Repository +public class EmailVerificationRedisRepository { + + private final RedisTemplate emailRedisTemplate; + + public void saveCode(String email, String code, int ttl) { + emailRedisTemplate.opsForValue().set(email, code, Duration.ofMinutes(ttl)); + } + + public boolean existCode(String email) { + return emailRedisTemplate.hasKey(email); + } +} diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index 403fc95a..701a4a66 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -1,16 +1,22 @@ package project.flipnote.auth.service; -import java.util.UUID; +import java.security.SecureRandom; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.model.EmailVerificationDto; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginDto; +import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.jwt.JwtComponent; +import project.flipnote.event.EmailVerificationSendEvent; +import project.flipnote.infra.email.EmailService; import project.flipnote.user.entity.User; import project.flipnote.user.repository.UserRepository; @@ -21,6 +27,10 @@ public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtComponent jwtComponent; + private final EmailVerificationRedisRepository emailVerificationRedisRepository; + private final ApplicationEventPublisher eventPublisher; + + private static final SecureRandom random = new SecureRandom(); public TokenPair login(UserLoginDto.Request req) { User user = findByEmailOrThrow(req); @@ -36,4 +46,25 @@ private User findByEmailOrThrow(UserLoginDto.Request req) { return userRepository.findByEmail(req.email()) .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); } + + public void sendEmailVerificationCode(EmailVerificationDto.Request req) { + final String email = req.email(); + + if (emailVerificationRedisRepository.existCode(email)) { + throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); + } + + final String code = generateVerificationCode(VerificationConstants.CODE_LENGTH); + int ttl = VerificationConstants.CODE_TTL_MINUTES; + + emailVerificationRedisRepository.saveCode(email, code, ttl); + + eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code, ttl)); + } + + private String generateVerificationCode(int length) { + int origin = (int) Math.pow(10, length - 1); + int bound = (int) Math.pow(10, length); + return String.valueOf(random.nextInt(origin, bound)); + } } diff --git a/src/main/java/project/flipnote/common/config/AsyncConfig.java b/src/main/java/project/flipnote/common/config/AsyncConfig.java new file mode 100644 index 00000000..6e272b4c --- /dev/null +++ b/src/main/java/project/flipnote/common/config/AsyncConfig.java @@ -0,0 +1,29 @@ +package project.flipnote.common.config; + +import java.util.concurrent.Executor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@EnableAsync +@Configuration +public class AsyncConfig implements AsyncConfigurer { + + private final AsyncProperties asyncProperties; + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(asyncProperties.getCorePoolSize()); + executor.setMaxPoolSize(asyncProperties.getMaxPoolSize()); + executor.setQueueCapacity(asyncProperties.getQueueCapacity()); + executor.setThreadNamePrefix("Async-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/project/flipnote/common/config/AsyncProperties.java b/src/main/java/project/flipnote/common/config/AsyncProperties.java new file mode 100644 index 00000000..12f2f6eb --- /dev/null +++ b/src/main/java/project/flipnote/common/config/AsyncProperties.java @@ -0,0 +1,17 @@ +package project.flipnote.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@ConfigurationProperties(prefix = "async") +@Component +public class AsyncProperties { + private int corePoolSize; + private int maxPoolSize; + private int queueCapacity; +} diff --git a/src/main/java/project/flipnote/common/config/RedisConfig.java b/src/main/java/project/flipnote/common/config/RedisConfig.java new file mode 100644 index 00000000..ea4e10dc --- /dev/null +++ b/src/main/java/project/flipnote/common/config/RedisConfig.java @@ -0,0 +1,20 @@ +package project.flipnote.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate emailRedisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} diff --git a/src/main/java/project/flipnote/common/exception/EmailSendException.java b/src/main/java/project/flipnote/common/exception/EmailSendException.java new file mode 100644 index 00000000..b23fd8c7 --- /dev/null +++ b/src/main/java/project/flipnote/common/exception/EmailSendException.java @@ -0,0 +1,7 @@ +package project.flipnote.common.exception; + +public class EmailSendException extends RuntimeException { + public EmailSendException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java index 1e01b80f..5578ee31 100644 --- a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java +++ b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java @@ -57,7 +57,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.POST, "/*/users").permitAll() - .requestMatchers(HttpMethod.POST, "/*/auth/login").permitAll() + .requestMatchers(HttpMethod.POST, "/*/auth/login", "/*/auth/email").permitAll() .requestMatchers( "/v3/api-docs/**", "/v3/api-docs", diff --git a/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java b/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java new file mode 100644 index 00000000..cc2c8faa --- /dev/null +++ b/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java @@ -0,0 +1,8 @@ +package project.flipnote.event; + +public record EmailVerificationSendEvent( + String to, + String code, + int ttl +) { +} diff --git a/src/main/java/project/flipnote/infra/config/ResendConfig.java b/src/main/java/project/flipnote/infra/config/ResendConfig.java new file mode 100644 index 00000000..d8b242d3 --- /dev/null +++ b/src/main/java/project/flipnote/infra/config/ResendConfig.java @@ -0,0 +1,20 @@ +package project.flipnote.infra.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.resend.Resend; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Configuration +public class ResendConfig { + + private final ResendProperties resendProperties; + + @Bean + public Resend resend() { + return new Resend(resendProperties.getApiKey()); + } +} diff --git a/src/main/java/project/flipnote/infra/config/ResendProperties.java b/src/main/java/project/flipnote/infra/config/ResendProperties.java new file mode 100644 index 00000000..d23df1de --- /dev/null +++ b/src/main/java/project/flipnote/infra/config/ResendProperties.java @@ -0,0 +1,16 @@ +package project.flipnote.infra.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@ConfigurationProperties("resend") +@Component +public class ResendProperties { + private String fromEmail; + private String apiKey; +} diff --git a/src/main/java/project/flipnote/infra/email/EmailService.java b/src/main/java/project/flipnote/infra/email/EmailService.java new file mode 100644 index 00000000..4cd13d38 --- /dev/null +++ b/src/main/java/project/flipnote/infra/email/EmailService.java @@ -0,0 +1,6 @@ +package project.flipnote.infra.email; + +public interface EmailService { + + void sendEmailVerificationCode(String to, String code, int ttl); +} diff --git a/src/main/java/project/flipnote/infra/email/ResendEmailService.java b/src/main/java/project/flipnote/infra/email/ResendEmailService.java new file mode 100644 index 00000000..2bb56edf --- /dev/null +++ b/src/main/java/project/flipnote/infra/email/ResendEmailService.java @@ -0,0 +1,47 @@ +package project.flipnote.infra.email; + +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +import com.resend.Resend; +import com.resend.core.exception.ResendException; +import com.resend.services.emails.model.CreateEmailOptions; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.common.exception.EmailSendException; +import project.flipnote.infra.config.ResendProperties; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ResendEmailService implements EmailService { + + private final ResendProperties resendProperties; + private final Resend resend; + private final SpringTemplateEngine templateEngine; + + @Override + public void sendEmailVerificationCode(String to, String code, int ttl) { + Context context = new Context(); + context.setVariable("code", code); + context.setVariable("validMinutes", ttl); + + String html = templateEngine.process("email/email-verification", context); + + CreateEmailOptions params = CreateEmailOptions.builder() + .from(resendProperties.getFromEmail()) + .to(to) + .subject("이메일 인증번호 안내") + .html(html) + .build(); + + try { + resend.emails().send(params); + } catch (ResendException e) { + log.error("이메일 인증번호 발송 실패: to={}, code={}, ttl={}분", to, code, ttl, e); + throw new EmailSendException(e); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7b70fd35..6e8bf178 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,8 +5,8 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/flipnote?useSSL=false&allowPublicKeyRetrieval=true - username: ${MYSQL_USERNAME:root} - password: ${MYSQL_PASSWORD:root} + username: + password: jpa: open-in-view: false @@ -20,6 +20,19 @@ spring: use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect + thymeleaf: + prefix: classpath:/templates/ + suffix: .html + mode: HTML + encoding: UTF-8 + cache: false + + data: + redis: + host: localhost + password: + port: 6379 + management: endpoints: web: @@ -28,7 +41,15 @@ management: - health jwt: - secret: ${JWT_SECRET:55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729} + secret: access-token-expiration: 1h refresh-token-expiration: 7d +async: + core-pool-size: 4 + max-pool-size: 10 + queue-capacity: 100 + +resend: + api-key: + from-email: onboarding@resend.dev diff --git a/src/main/resources/templates/email/email-verification.html b/src/main/resources/templates/email/email-verification.html new file mode 100644 index 00000000..e431f8bb --- /dev/null +++ b/src/main/resources/templates/email/email-verification.html @@ -0,0 +1,72 @@ + + + + + 이메일 인증 + + + +
+
이메일 인증번호 안내
+
+ 아래 인증번호를 입력해 주세요.
+ 인증번호는 분간 유효합니다. +
+
123456
+ +
+ + From 5fcb80102c6b6c635b5c2e6612bb830c7978f0a8 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 13:07:47 +0900 Subject: [PATCH 08/40] =?UTF-8?q?Chore:=20application.yml=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 26 ++++++++++++++++++++++++ src/main/resources/application-test.yml | 15 +++++++------- src/main/resources/application.yml | 11 ++++------ 3 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/application-local.yml diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..5b9c5942 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,26 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/flipnote?useSSL=false&allowPublicKeyRetrieval=true + username: root + password: root + + jpa: + hibernate: + ddl-auto: update + + properties: + hibernate: + show_sql: true + format_sql: true + use_sql_comments: true + + data: + redis: + host: localhost + port: 6379 + +jwt: + secret: 55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729 + +resend: + api-key: dummy_api_key diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 44a93de9..dd52b838 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,7 +1,4 @@ spring: - application: - name: flipnote-test - datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver @@ -9,7 +6,6 @@ spring: password: jpa: - open-in-view: false hibernate: ddl-auto: create-drop @@ -18,9 +14,14 @@ spring: show_sql: true format_sql: true use_sql_comments: true - dialect: org.hibernate.dialect.H2Dialect + + data: + redis: + host: localhost + port: 6379 jwt: secret: ${JWT_SECRET:55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729} - access-token-expiration: 1h - refresh-token-expiration: 7d \ No newline at end of file + +resend: + api-key: dummy_api_key diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6e8bf178..7fdcbfb3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,20 +4,17 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/flipnote?useSSL=false&allowPublicKeyRetrieval=true + url: username: password: jpa: open-in-view: false hibernate: - ddl-auto: update + ddl-auto: validate properties: hibernate: - show_sql: true - format_sql: true - use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect thymeleaf: @@ -29,9 +26,9 @@ spring: data: redis: - host: localhost + host: password: - port: 6379 + port: management: endpoints: From 0c0ac3a472d1f7366b50546b7531dcbb2f8b3f4f Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 13:08:00 +0900 Subject: [PATCH 09/40] =?UTF-8?q?Chore:=20local=EC=9A=A9=20docker-compose.?= =?UTF-8?q?yml=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.local.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docker-compose.local.yml diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..c52ec783 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,17 @@ +services: + flipnote-mysql: + image: mysql:latest + container_name: flipnote-mysql + ports: + - "3306:3306" + environment: + MYSQL_USERNAME: root + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: flipnote + TZ: Asia/Seoul + + flipnote-redis: + image: redis:latest + container_name: flipnote-redis + ports: + - "6379:6379" From d1d36a82fef1f9f87105460db3d1dad38c424faa Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 14:19:18 +0900 Subject: [PATCH 10/40] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=9C=EC=B6=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/constants/AuthRedisKey.java | 15 ++++++ .../auth/controller/AuthController.java | 10 ++++ .../auth/exception/AuthErrorCode.java | 4 +- .../EmailVerificationEventListener.java | 3 +- .../model/EmailVerificationConfirmDto.java | 21 +++++++++ .../auth/model/EmailVerificationDto.java | 4 +- .../flipnote/auth/model/UserLoginDto.java | 6 +-- .../EmailVerificationRedisRepository.java | 46 +++++++++++++++++-- .../flipnote/auth/service/AuthService.java | 26 ++++++++--- .../flipnote/common/constants/RedisKeys.java | 17 +++++++ .../security/config/SecurityConfig.java | 5 +- .../event/EmailVerificationSendEvent.java | 3 +- .../flipnote/user/model/UserRegisterDto.java | 8 ++-- 13 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 src/main/java/project/flipnote/auth/constants/AuthRedisKey.java create mode 100644 src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java create mode 100644 src/main/java/project/flipnote/common/constants/RedisKeys.java diff --git a/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java b/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java new file mode 100644 index 00000000..ad91fddb --- /dev/null +++ b/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java @@ -0,0 +1,15 @@ +package project.flipnote.auth.constants; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import project.flipnote.common.constants.RedisKeys; + +@Getter +@AllArgsConstructor +public enum AuthRedisKey implements RedisKeys { + EMAIL_CODE("auth:email:code:%s", VerificationConstants.CODE_TTL_MINUTES * 60), + EMAIL_VERIFIED("auth:email:verified:%s", 600); + + private final String pattern; + private final int ttlSeconds; +} diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index ea7ad2f5..2160a0f7 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.model.EmailVerificationConfirmDto; import project.flipnote.auth.model.EmailVerificationDto; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginDto; @@ -48,4 +49,13 @@ public ResponseEntity sendEmailVerificationCode(@Valid @RequestBody EmailV return ResponseEntity.ok().build(); } + + @PostMapping("/email/confirm") + public ResponseEntity confirmEmailVerificationCode( + @Valid @RequestBody EmailVerificationConfirmDto.Request req + ) { + authService.confirmEmailVerificationCode(req); + + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java index 952c10e9..e70e066f 100644 --- a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java +++ b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java @@ -11,7 +11,9 @@ public enum AuthErrorCode implements ErrorCode { INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "이메일 또는 비밀번호가 올바르지 않습니다."), - ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_002", "이미 발급된 인증번호가 있습니다. 잠시 후 다시 시도해 주세요."); + ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_002", "이미 발급된 인증번호가 있습니다. 잠시 후 다시 시도해 주세요."), + NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "발급된 인증번호가 없습니다. 인증번호를 먼저 요청해 주세요."), + INVALID_VERIFICATION_CODE(HttpStatus.FORBIDDEN, "AUTH_004", "잘못된 인증번호입니다. 입력한 인증번호를 확인해 주세요."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java index e919f176..ac04bd56 100644 --- a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.common.exception.EmailSendException; import project.flipnote.event.EmailVerificationSendEvent; import project.flipnote.infra.email.EmailService; @@ -27,7 +28,7 @@ public class EmailVerificationEventListener { ) @EventListener public void handleEmailVerificationSendEvent(EmailVerificationSendEvent event) { - emailService.sendEmailVerificationCode(event.to(), event.code(), event.ttl()); + emailService.sendEmailVerificationCode(event.to(), event.code(), VerificationConstants.CODE_TTL_MINUTES); } @Recover diff --git a/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java b/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java new file mode 100644 index 00000000..f9a940e9 --- /dev/null +++ b/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java @@ -0,0 +1,21 @@ +package project.flipnote.auth.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import project.flipnote.auth.constants.VerificationConstants; + +public class EmailVerificationConfirmDto { + + public record Request( + + @Email @NotBlank + String email, + + @NotBlank + @Size(min = VerificationConstants.CODE_LENGTH, max = VerificationConstants.CODE_LENGTH) + String code + ) { + + } +} diff --git a/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java b/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java index 26583238..b274d231 100644 --- a/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java +++ b/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java @@ -1,13 +1,13 @@ package project.flipnote.auth.model; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; public class EmailVerificationDto { public record Request( - @Email @NotEmpty + @Email @NotBlank String email ) { } diff --git a/src/main/java/project/flipnote/auth/model/UserLoginDto.java b/src/main/java/project/flipnote/auth/model/UserLoginDto.java index ea8dc2b7..a17a13cd 100644 --- a/src/main/java/project/flipnote/auth/model/UserLoginDto.java +++ b/src/main/java/project/flipnote/auth/model/UserLoginDto.java @@ -1,16 +1,16 @@ package project.flipnote.auth.model; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; public class UserLoginDto { public record Request( - @Email @NotEmpty + @Email @NotBlank String email, - @NotEmpty + @NotBlank String password ) { } diff --git a/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java b/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java index 049f5a1e..18ea4665 100644 --- a/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java +++ b/src/main/java/project/flipnote/auth/repository/EmailVerificationRedisRepository.java @@ -1,11 +1,13 @@ package project.flipnote.auth.repository; import java.time.Duration; +import java.util.Optional; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.constants.AuthRedisKey; @RequiredArgsConstructor @Repository @@ -13,11 +15,49 @@ public class EmailVerificationRedisRepository { private final RedisTemplate emailRedisTemplate; - public void saveCode(String email, String code, int ttl) { - emailRedisTemplate.opsForValue().set(email, code, Duration.ofMinutes(ttl)); + public void saveCode(String email, String code) { + String key = AuthRedisKey.EMAIL_CODE.key(email); + Duration ttl = AuthRedisKey.EMAIL_CODE.getTtl(); + + emailRedisTemplate.opsForValue().set(key, code, ttl); } public boolean existCode(String email) { - return emailRedisTemplate.hasKey(email); + String key = AuthRedisKey.EMAIL_CODE.key(email); + + return emailRedisTemplate.hasKey(key); + } + + public Optional findCode(String email) { + String key = AuthRedisKey.EMAIL_CODE.key(email); + + String code = emailRedisTemplate.opsForValue().get(key); + + return Optional.ofNullable(code); + } + + public void deleteCode(String email) { + String key = AuthRedisKey.EMAIL_CODE.key(email); + + emailRedisTemplate.delete(key); + } + + public void markAsVerified(String email) { + String key = AuthRedisKey.EMAIL_VERIFIED.key(email); + Duration ttl = AuthRedisKey.EMAIL_VERIFIED.getTtl(); + + emailRedisTemplate.opsForValue().set(key, "1", ttl); + } + + public boolean isVerified(String email) { + String key = AuthRedisKey.EMAIL_VERIFIED.key(email); + + return emailRedisTemplate.hasKey(key); + } + + public void deleteVerified(String email) { + String key = AuthRedisKey.EMAIL_VERIFIED.key(email); + + emailRedisTemplate.delete(key); } } diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index 701a4a66..e327525d 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -1,6 +1,7 @@ package project.flipnote.auth.service; import java.security.SecureRandom; +import java.util.Objects; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; @@ -9,6 +10,7 @@ import lombok.RequiredArgsConstructor; import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.model.EmailVerificationConfirmDto; import project.flipnote.auth.model.EmailVerificationDto; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginDto; @@ -16,7 +18,6 @@ import project.flipnote.common.exception.BizException; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.event.EmailVerificationSendEvent; -import project.flipnote.infra.email.EmailService; import project.flipnote.user.entity.User; import project.flipnote.user.repository.UserRepository; @@ -55,16 +56,29 @@ public void sendEmailVerificationCode(EmailVerificationDto.Request req) { } final String code = generateVerificationCode(VerificationConstants.CODE_LENGTH); - int ttl = VerificationConstants.CODE_TTL_MINUTES; - emailVerificationRedisRepository.saveCode(email, code, ttl); + emailVerificationRedisRepository.saveCode(email, code); - eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code, ttl)); + eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code)); + } + + public void confirmEmailVerificationCode(EmailVerificationConfirmDto.Request req) { + String email = req.email(); + + String code = emailVerificationRedisRepository.findCode(email) + .orElseThrow(() -> new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE)); + + if (!Objects.equals(req.code(), code)) { + throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); + } + + emailVerificationRedisRepository.deleteCode(email); + emailVerificationRedisRepository.markAsVerified(email); } private String generateVerificationCode(int length) { - int origin = (int) Math.pow(10, length - 1); - int bound = (int) Math.pow(10, length); + int origin = (int)Math.pow(10, length - 1); + int bound = (int)Math.pow(10, length); return String.valueOf(random.nextInt(origin, bound)); } } diff --git a/src/main/java/project/flipnote/common/constants/RedisKeys.java b/src/main/java/project/flipnote/common/constants/RedisKeys.java new file mode 100644 index 00000000..0094e217 --- /dev/null +++ b/src/main/java/project/flipnote/common/constants/RedisKeys.java @@ -0,0 +1,17 @@ +package project.flipnote.common.constants; + +import java.time.Duration; + +public interface RedisKeys { + String getPattern(); + + int getTtlSeconds(); + + default String key(Object... args) { + return String.format(getPattern(), args); + } + + default Duration getTtl() { + return Duration.ofSeconds(getTtlSeconds()); + } +} diff --git a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java index 5578ee31..24346771 100644 --- a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java +++ b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java @@ -57,7 +57,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.POST, "/*/users").permitAll() - .requestMatchers(HttpMethod.POST, "/*/auth/login", "/*/auth/email").permitAll() + .requestMatchers( + HttpMethod.POST, + "/*/auth/login", "/*/auth/email", "/*/auth/email/confirm" + ).permitAll() .requestMatchers( "/v3/api-docs/**", "/v3/api-docs", diff --git a/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java b/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java index cc2c8faa..fee1844d 100644 --- a/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java +++ b/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java @@ -2,7 +2,6 @@ public record EmailVerificationSendEvent( String to, - String code, - int ttl + String code ) { } diff --git a/src/main/java/project/flipnote/user/model/UserRegisterDto.java b/src/main/java/project/flipnote/user/model/UserRegisterDto.java index 4b2b89fb..68e4f164 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterDto.java +++ b/src/main/java/project/flipnote/user/model/UserRegisterDto.java @@ -1,7 +1,7 @@ package project.flipnote.user.model; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import project.flipnote.common.annotation.ValidPassword; import project.flipnote.common.annotation.ValidPhone; @@ -9,16 +9,16 @@ public class UserRegisterDto { public record Request( - @Email @NotEmpty + @Email @NotBlank String email, @ValidPassword String password, - @NotEmpty + @NotBlank String name, - @NotEmpty + @NotBlank String nickname, @NotNull From 2adf5376f2a1dce4bcb682a34beeef9073600803 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 14:29:14 +0900 Subject: [PATCH 11/40] =?UTF-8?q?Fix:=20=EA=B2=80=EC=A6=9D=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=97=90=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/common/annotation/ValidPassword.java | 4 ++++ .../project/flipnote/common/annotation/ValidPhone.java | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/project/flipnote/common/annotation/ValidPassword.java b/src/main/java/project/flipnote/common/annotation/ValidPassword.java index 86ae31ff..aa59d5f5 100644 --- a/src/main/java/project/flipnote/common/annotation/ValidPassword.java +++ b/src/main/java/project/flipnote/common/annotation/ValidPassword.java @@ -6,10 +6,14 @@ import java.lang.annotation.Target; import jakarta.validation.Constraint; +import jakarta.validation.Payload; import project.flipnote.common.validator.PasswordConstraintValidator; @Constraint(validatedBy = PasswordConstraintValidator.class) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidPassword { + String message() default "비밀번호 형식이 올바르지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; } diff --git a/src/main/java/project/flipnote/common/annotation/ValidPhone.java b/src/main/java/project/flipnote/common/annotation/ValidPhone.java index 5aaaedb6..b5250098 100644 --- a/src/main/java/project/flipnote/common/annotation/ValidPhone.java +++ b/src/main/java/project/flipnote/common/annotation/ValidPhone.java @@ -6,10 +6,14 @@ import java.lang.annotation.Target; import jakarta.validation.Constraint; -import project.flipnote.common.validator.PasswordConstraintValidator; +import jakarta.validation.Payload; +import project.flipnote.common.validator.PhoneConstraintValidator; -@Constraint(validatedBy = PasswordConstraintValidator.class) +@Constraint(validatedBy = PhoneConstraintValidator.class) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidPhone { + String message() default "휴대전화 번호 형식이 올바르지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; } From 54a0cce9c06db38751dae23c3d3bd67a6c36eadb Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 14:29:59 +0900 Subject: [PATCH 12/40] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=EC=9D=B8=EC=A7=80=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/exception/AuthErrorCode.java | 3 ++- .../project/flipnote/auth/service/AuthService.java | 10 ++++++++++ .../project/flipnote/user/service/UserService.java | 12 ++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java index e70e066f..85270d6f 100644 --- a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java +++ b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java @@ -13,7 +13,8 @@ public enum AuthErrorCode implements ErrorCode { INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "이메일 또는 비밀번호가 올바르지 않습니다."), ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_002", "이미 발급된 인증번호가 있습니다. 잠시 후 다시 시도해 주세요."), NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "발급된 인증번호가 없습니다. 인증번호를 먼저 요청해 주세요."), - INVALID_VERIFICATION_CODE(HttpStatus.FORBIDDEN, "AUTH_004", "잘못된 인증번호입니다. 입력한 인증번호를 확인해 주세요."); + INVALID_VERIFICATION_CODE(HttpStatus.FORBIDDEN, "AUTH_004", "잘못된 인증번호입니다. 입력한 인증번호를 확인해 주세요."), + UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_005", "인증되지 않은 이메일입니다. 이메일 인증을 완료해 주세요."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index e327525d..573579ce 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -76,6 +76,16 @@ public void confirmEmailVerificationCode(EmailVerificationConfirmDto.Request req emailVerificationRedisRepository.markAsVerified(email); } + public void validateEmail(String email) { + if (!emailVerificationRedisRepository.isVerified(email)) { + throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL); + } + } + + public void deleteVerifiedEmail(String email) { + emailVerificationRedisRepository.deleteVerified(email); + } + private String generateVerificationCode(int length) { int origin = (int)Math.pow(10, length - 1); int bound = (int)Math.pow(10, length); diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 4c452503..6d9045b8 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.service.AuthService; import project.flipnote.common.exception.BizException; import project.flipnote.user.entity.User; import project.flipnote.user.exception.UserErrorCode; @@ -16,13 +17,18 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final AuthService authService; public UserRegisterDto.Response register(UserRegisterDto.Request req) { - validateEmailDuplicate(req.email()); + String email = req.email(); + + validateEmailDuplicate(email); validatePhoneDuplicate(req.phone()); + authService.validateEmail(email); + User user = User.builder() - .email(req.email()) + .email(email) .password(passwordEncoder.encode(req.password())) .name(req.name()) .nickname(req.nickname()) @@ -32,6 +38,8 @@ public UserRegisterDto.Response register(UserRegisterDto.Request req) { .build(); userRepository.save(user); + authService.deleteVerifiedEmail(email); + return UserRegisterDto.Response.from(user.getId()); } From 567bb86bd0b13cbd9793b4dd086f437f06a36258 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 14:31:41 +0900 Subject: [PATCH 13/40] =?UTF-8?q?Chore:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84=EC=B9=98=20=EC=A0=95?= =?UTF-8?q?=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/{ => auth}/event/EmailVerificationSendEvent.java | 2 +- .../flipnote/auth/listener/EmailVerificationEventListener.java | 2 +- src/main/java/project/flipnote/auth/service/AuthService.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/project/flipnote/{ => auth}/event/EmailVerificationSendEvent.java (66%) diff --git a/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java b/src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java similarity index 66% rename from src/main/java/project/flipnote/event/EmailVerificationSendEvent.java rename to src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java index fee1844d..cdccde4e 100644 --- a/src/main/java/project/flipnote/event/EmailVerificationSendEvent.java +++ b/src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java @@ -1,4 +1,4 @@ -package project.flipnote.event; +package project.flipnote.auth.event; public record EmailVerificationSendEvent( String to, diff --git a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java index ac04bd56..2ca146d8 100644 --- a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java @@ -11,7 +11,7 @@ import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.common.exception.EmailSendException; -import project.flipnote.event.EmailVerificationSendEvent; +import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.infra.email.EmailService; @Slf4j diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index 573579ce..ecf13524 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -17,7 +17,7 @@ import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.jwt.JwtComponent; -import project.flipnote.event.EmailVerificationSendEvent; +import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.user.entity.User; import project.flipnote.user.repository.UserRepository; From 19812024f20a27061642f01d43da3ac9223c219f Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 15:48:38 +0900 Subject: [PATCH 14/40] =?UTF-8?q?Feat:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EA=B8=B0=20null=20=ED=97=88=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/validator/PhoneConstraintValidator.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java b/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java index fad2c334..e048b0c3 100644 --- a/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java +++ b/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java @@ -1,5 +1,7 @@ package project.flipnote.common.validator; +import java.util.Objects; + import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import project.flipnote.common.annotation.ValidPhone; @@ -9,10 +11,11 @@ public class PhoneConstraintValidator implements ConstraintValidator Date: Sun, 6 Jul 2025 15:50:12 +0900 Subject: [PATCH 15/40] =?UTF-8?q?Test:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A8=EC=9C=84=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 --- .../EmailVerificationEventListener.java | 2 +- .../flipnote/auth/service/AuthService.java | 2 +- .../flipnote/user/model/UserRegisterDto.java | 1 + .../flipnote/user/service/UserService.java | 4 +- .../user/service/UserServiceTest.java | 134 ++++++++++++++++++ 5 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/test/java/project/flipnote/user/service/UserServiceTest.java diff --git a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java index 2ca146d8..82018f55 100644 --- a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java @@ -10,8 +10,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.VerificationConstants; -import project.flipnote.common.exception.EmailSendException; import project.flipnote.auth.event.EmailVerificationSendEvent; +import project.flipnote.common.exception.EmailSendException; import project.flipnote.infra.email.EmailService; @Slf4j diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index ecf13524..df1a73c8 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import project.flipnote.auth.constants.VerificationConstants; +import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.model.EmailVerificationConfirmDto; import project.flipnote.auth.model.EmailVerificationDto; @@ -17,7 +18,6 @@ import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.jwt.JwtComponent; -import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.user.entity.User; import project.flipnote.user.repository.UserRepository; diff --git a/src/main/java/project/flipnote/user/model/UserRegisterDto.java b/src/main/java/project/flipnote/user/model/UserRegisterDto.java index 68e4f164..0eeb729e 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterDto.java +++ b/src/main/java/project/flipnote/user/model/UserRegisterDto.java @@ -9,6 +9,7 @@ public class UserRegisterDto { public record Request( + @Email @NotBlank String email, diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 6d9045b8..04f9b351 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -36,11 +36,11 @@ public UserRegisterDto.Response register(UserRegisterDto.Request req) { .phone(req.getCleanedPhone()) .profileImageUrl(req.profileImageUrl()) .build(); - userRepository.save(user); + User savedUser = userRepository.save(user); authService.deleteVerifiedEmail(email); - return UserRegisterDto.Response.from(user.getId()); + return UserRegisterDto.Response.from(savedUser.getId()); } private void validateEmailDuplicate(String email) { diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java new file mode 100644 index 00000000..8e887d52 --- /dev/null +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -0,0 +1,134 @@ +package project.flipnote.user.service; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.service.AuthService; +import project.flipnote.common.exception.BizException; +import project.flipnote.user.entity.User; +import project.flipnote.user.exception.UserErrorCode; +import project.flipnote.user.model.UserRegisterDto; +import project.flipnote.user.repository.UserRepository; + +@DisplayName("회원 서비스 단위 테스트") +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @InjectMocks + private UserService userService; + + @Mock + private UserRepository userRepository; + + @Mock + private AuthService authService; + + @Mock + private PasswordEncoder passwordEncoder; + + private User user; + + @BeforeEach + void init() { + user = User.builder() + .email("test@test.com") + .password("testPass") + .name("테스트") + .nickname("테스트") + .smsAgree(false) + .phone("010-1234-5678") + .profileImageUrl(null) + .build(); + + ReflectionTestUtils.setField(user, "id", 1L); + } + + @DisplayName("회원가입 테스트") + @Nested + class Register { + + @DisplayName("회원가입 성공") + @Test + void success_register() { + UserRegisterDto.Request req = new UserRegisterDto.Request( + "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" + ); + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(userRepository.existsByPhone(any(String.class))).willReturn(false); + given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass"); + given(userRepository.save(any(User.class))).willReturn(user); + + UserRegisterDto.Response res = userService.register(req); + + assertThat(res.userId()).isEqualTo(user.getId()); + + verify(authService, times(1)).validateEmail(any(String.class)); + verify(authService, times(1)).deleteVerifiedEmail(any(String.class)); + } + + @DisplayName("이메일 중복 시 예외 발생") + @Test + void fail_register_duplicateEmail() { + UserRegisterDto.Request req = new UserRegisterDto.Request( + "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" + ); + + given(userRepository.existsByEmail(any(String.class))).willReturn(true); + + BizException exception = assertThrows(BizException.class, () -> userService.register(req)); + assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.DUPLICATE_EMAIL); + + verify(userRepository, never()).existsByPhone(any(String.class)); + verify(userRepository, never()).save(any(User.class)); + } + + @DisplayName("전화번호 중복 시 예외 발생") + @Test + void fail_register_duplicatePhone() { + UserRegisterDto.Request req = new UserRegisterDto.Request( + "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" + ); + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(userRepository.existsByPhone(any(String.class))).willReturn(true); + + BizException exception = assertThrows(BizException.class, () -> userService.register(req)); + assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.DUPLICATE_PHONE); + + verify(userRepository, never()).save(any()); + } + + @DisplayName("이메일 인증이 안 된 경우 예외 발생") + @Test + void fail_register_unverifiedEmail() { + UserRegisterDto.Request req = new UserRegisterDto.Request( + "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" + ); + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(userRepository.existsByPhone(any(String.class))).willReturn(false); + doThrow(new BizException(AuthErrorCode.UNVERIFIED_EMAIL)) + .when(authService).validateEmail(any(String.class)); + + BizException exception = assertThrows(BizException.class, () -> userService.register(req)); + assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.UNVERIFIED_EMAIL); + + verify(userRepository, never()).save(any(User.class)); + } + } + +} From e8f4654c90e44f7d13f8fd53e626c4a94b7242c1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 15:51:33 +0900 Subject: [PATCH 16/40] =?UTF-8?q?Test:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EA=B8=B0=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validator/PhoneConstraintValidatorTest.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java b/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java index a129018e..3615d655 100644 --- a/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java +++ b/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java @@ -26,6 +26,12 @@ void validPhoneWithHyphens() { assertThat(validator.isValid("010-4567-1238", context)).isTrue(); } + @Test + @DisplayName("null은 true를 반환한다") + void validPhoneNull() { + assertThat(validator.isValid(null, context)).isTrue(); + } + @Test @DisplayName("01012345678 형식은 false를 반환한다") void validPhoneWithoutHyphens() { @@ -56,9 +62,8 @@ void invalidPhoneWithLetters() { } @Test - @DisplayName("null 또는 빈 문자열은 false") - void nullOrEmpty() { - assertThat(validator.isValid(null, context)).isFalse(); + @DisplayName("빈 문자열은 false") + void invalidPhoneEmpty() { assertThat(validator.isValid("", context)).isFalse(); assertThat(validator.isValid(" ", context)).isFalse(); } From 5d6441800e0a7e60822b79927cb701662da96af4 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:09:29 +0900 Subject: [PATCH 17/40] =?UTF-8?q?Feat:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20null=20=EB=8F=84=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/user/service/UserService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 04f9b351..715e74fa 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -1,5 +1,7 @@ package project.flipnote.user.service; +import java.util.Objects; + import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -50,8 +52,12 @@ private void validateEmailDuplicate(String email) { } private void validatePhoneDuplicate(String phone) { + if (Objects.isNull(phone)) { + return; + } + if (userRepository.existsByPhone(phone)) { - throw new BizException(UserErrorCode.DUPLICATE_PHONE); + throw new BizException(UserErrorCode.DUPLICATE_PHONE); } } } From a4cb73b356219f45e78fb04d470742017690f1ea Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:10:14 +0900 Subject: [PATCH 18/40] =?UTF-8?q?Test:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EB=B2=88=ED=98=B8=20null=EC=9D=BC=20=EB=95=8C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=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 --- .../user/service/UserServiceTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index 8e887d52..3eb7503a 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -80,6 +80,25 @@ void success_register() { verify(authService, times(1)).deleteVerifiedEmail(any(String.class)); } + @DisplayName("회원가입 성공 - 폰 번호가 null일 때") + @Test + void success_register_phoneIsNull() { + UserRegisterDto.Request req = new UserRegisterDto.Request( + "test@test.com", "testPass", "테스트", "테스트", false, null, null + ); + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass"); + given(userRepository.save(any(User.class))).willReturn(user); + + UserRegisterDto.Response res = userService.register(req); + + assertThat(res.userId()).isEqualTo(user.getId()); + + verify(authService, times(1)).validateEmail(any(String.class)); + verify(authService, times(1)).deleteVerifiedEmail(any(String.class)); + } + @DisplayName("이메일 중복 시 예외 발생") @Test void fail_register_duplicateEmail() { From dec163cb2a36a8a0cbac7c3202f067b0ff1d5e52 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:15:25 +0900 Subject: [PATCH 19/40] =?UTF-8?q?Fix:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=EC=B2=B4=ED=81=AC=EC=99=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=98=EB=8A=94=20=EA=B0=92=EC=9D=84=20?= =?UTF-8?q?=EC=9D=BC=EA=B4=80=EC=84=B1=20=EC=9E=88=EA=B2=8C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/user/service/UserService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 715e74fa..520bc773 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -4,6 +4,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import project.flipnote.auth.service.AuthService; @@ -23,9 +24,10 @@ public class UserService { public UserRegisterDto.Response register(UserRegisterDto.Request req) { String email = req.email(); + String phone = req.getCleanedPhone(); validateEmailDuplicate(email); - validatePhoneDuplicate(req.phone()); + validatePhoneDuplicate(phone); authService.validateEmail(email); @@ -35,7 +37,7 @@ public UserRegisterDto.Response register(UserRegisterDto.Request req) { .name(req.name()) .nickname(req.nickname()) .smsAgree(req.smsAgree()) - .phone(req.getCleanedPhone()) + .phone(phone) .profileImageUrl(req.profileImageUrl()) .build(); User savedUser = userRepository.save(user); From de5a4ad7d1e02529955344dfd3eba78eebf64bf7 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:16:34 +0900 Subject: [PATCH 20/40] =?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=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=ED=9A=9F?= =?UTF-8?q?=EC=88=98=20=EB=AA=85=EC=8B=9C=EC=A0=81=20=EC=84=A4=EC=A0=95=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 --- .../flipnote/auth/listener/EmailVerificationEventListener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java index 82018f55..34572f19 100644 --- a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java @@ -23,6 +23,7 @@ public class EmailVerificationEventListener { @Async @Retryable( + maxAttempts = 3, retryFor = { EmailSendException.class }, backoff = @Backoff(delay = 2000, multiplier = 2) ) From 61f1fed1318fa0a6d81d2e4874d7d7f637b908ab Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:19:08 +0900 Subject: [PATCH 21/40] =?UTF-8?q?Feat:=20=EC=8A=A4=EB=A0=88=EB=93=9C=20?= =?UTF-8?q?=ED=92=80=20=EC=84=A4=EC=A0=95=EC=97=90=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=A6=9D(@Min)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/common/config/AsyncProperties.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/project/flipnote/common/config/AsyncProperties.java b/src/main/java/project/flipnote/common/config/AsyncProperties.java index 12f2f6eb..95705076 100644 --- a/src/main/java/project/flipnote/common/config/AsyncProperties.java +++ b/src/main/java/project/flipnote/common/config/AsyncProperties.java @@ -3,6 +3,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import jakarta.validation.constraints.Min; import lombok.Getter; import lombok.Setter; @@ -11,7 +12,13 @@ @ConfigurationProperties(prefix = "async") @Component public class AsyncProperties { + + @Min(value = 1) private int corePoolSize; + + @Min(value = 1) private int maxPoolSize; + + @Min(value = 1) private int queueCapacity; } From 65d4d39862910246df5d068166c16c1235aa0f90 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:22:12 +0900 Subject: [PATCH 22/40] =?UTF-8?q?Feat:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=ED=82=A4=20=EC=83=9D=EC=84=B1=EC=8B=9C=20args=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/common/constants/RedisKeys.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/project/flipnote/common/constants/RedisKeys.java b/src/main/java/project/flipnote/common/constants/RedisKeys.java index 0094e217..03c00cef 100644 --- a/src/main/java/project/flipnote/common/constants/RedisKeys.java +++ b/src/main/java/project/flipnote/common/constants/RedisKeys.java @@ -8,6 +8,10 @@ public interface RedisKeys { int getTtlSeconds(); default String key(Object... args) { + if (args == null || args.length == 0) { + throw new IllegalArgumentException("Arguments cannot be null or empty"); + } + return String.format(getPattern(), args); } From 8a47f7c1d339a234a843c3c4504a4cfbdec3d55c Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:27:11 +0900 Subject: [PATCH 23/40] =?UTF-8?q?Feat:=20Resend=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D(@N?= =?UTF-8?q?otEmpty)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/infra/config/ResendProperties.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/project/flipnote/infra/config/ResendProperties.java b/src/main/java/project/flipnote/infra/config/ResendProperties.java index d23df1de..8bc4c59f 100644 --- a/src/main/java/project/flipnote/infra/config/ResendProperties.java +++ b/src/main/java/project/flipnote/infra/config/ResendProperties.java @@ -3,6 +3,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @@ -11,6 +12,10 @@ @ConfigurationProperties("resend") @Component public class ResendProperties { + + @NotEmpty private String fromEmail; + + @NotEmpty private String apiKey; } From 4a533d04b12bbaa247156317c8ead2e39304994e Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:30:01 +0900 Subject: [PATCH 24/40] =?UTF-8?q?Chore:=20JWT=20=EC=8B=9C=ED=81=AC?= =?UTF-8?q?=EB=A6=BF=20=EA=B8=B0=EB=B3=B8=20=EA=B0=92=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 2 +- src/main/resources/application-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 5b9c5942..a13729af 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -20,7 +20,7 @@ spring: port: 6379 jwt: - secret: 55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729 + secret: dummy_secret resend: api-key: dummy_api_key diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index dd52b838..02612d77 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -21,7 +21,7 @@ spring: port: 6379 jwt: - secret: ${JWT_SECRET:55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729} + secret: dummy_secret resend: api-key: dummy_api_key From 24a6aa0d4f12f92e98602a9075f35066ad9ad481 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:33:50 +0900 Subject: [PATCH 25/40] =?UTF-8?q?Feat:=20=EC=8A=A4=EB=A0=88=EB=93=9C=20?= =?UTF-8?q?=ED=92=80,=20Resend=20=EC=84=A4=EC=A0=95=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/common/config/AsyncProperties.java | 10 ++++++---- .../flipnote/infra/config/ResendProperties.java | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/project/flipnote/common/config/AsyncProperties.java b/src/main/java/project/flipnote/common/config/AsyncProperties.java index 95705076..71eaefa1 100644 --- a/src/main/java/project/flipnote/common/config/AsyncProperties.java +++ b/src/main/java/project/flipnote/common/config/AsyncProperties.java @@ -2,23 +2,25 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; -import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; import lombok.Getter; import lombok.Setter; @Getter @Setter +@Validated @ConfigurationProperties(prefix = "async") @Component public class AsyncProperties { - @Min(value = 1) + @Positive private int corePoolSize; - @Min(value = 1) + @Positive private int maxPoolSize; - @Min(value = 1) + @Positive private int queueCapacity; } diff --git a/src/main/java/project/flipnote/infra/config/ResendProperties.java b/src/main/java/project/flipnote/infra/config/ResendProperties.java index 8bc4c59f..c654b110 100644 --- a/src/main/java/project/flipnote/infra/config/ResendProperties.java +++ b/src/main/java/project/flipnote/infra/config/ResendProperties.java @@ -2,6 +2,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; @@ -9,6 +10,7 @@ @Getter @Setter +@Validated @ConfigurationProperties("resend") @Component public class ResendProperties { From 51a9f8aa69dbca29b530a9969a872a74fc109fcc Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 16:34:24 +0900 Subject: [PATCH 26/40] =?UTF-8?q?Chore:=20application-test.yml=20=EC=97=90?= =?UTF-8?q?=20jwt=20secret=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 02612d77..a0ef278f 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -21,7 +21,7 @@ spring: port: 6379 jwt: - secret: dummy_secret + secret: 55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729 resend: api-key: dummy_api_key From 68b6db4c550ef370f858a3e55489d4ebb2aebab4 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 19:44:37 +0900 Subject: [PATCH 27/40] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=95=98=EB=82=98=EC=9D=98=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=EC=9C=BC=EB=A1=9C=20=EB=AC=B6=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/project/flipnote/user/service/UserService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 520bc773..9db339cc 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -15,6 +15,7 @@ import project.flipnote.user.repository.UserRepository; @RequiredArgsConstructor +@Transactional(readOnly = true) @Service public class UserService { @@ -22,6 +23,7 @@ public class UserService { private final PasswordEncoder passwordEncoder; private final AuthService authService; + @Transactional public UserRegisterDto.Response register(UserRegisterDto.Request req) { String email = req.email(); String phone = req.getCleanedPhone(); From 9979018dee39240b7305006efc6500ebf79a615b Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:05:24 +0900 Subject: [PATCH 28/40] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=86=A1=EC=8B=A0=EC=9E=90=EC=99=80=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- src/main/resources/templates/email/email-verification.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7fdcbfb3..dc350b9a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -49,4 +49,4 @@ async: resend: api-key: - from-email: onboarding@resend.dev + from-email: FlipNote diff --git a/src/main/resources/templates/email/email-verification.html b/src/main/resources/templates/email/email-verification.html index e431f8bb..143a40a2 100644 --- a/src/main/resources/templates/email/email-verification.html +++ b/src/main/resources/templates/email/email-verification.html @@ -64,8 +64,7 @@
123456
From b71ba66808ea1878ceca7be9b230e35cf9902f7e Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:15:54 +0900 Subject: [PATCH 29/40] =?UTF-8?q?Feat&Refactor:=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=9D=BC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/exception/AuthErrorCode.java | 3 ++- .../flipnote/auth/service/AuthService.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java index 85270d6f..dd351fd0 100644 --- a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java +++ b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java @@ -14,7 +14,8 @@ public enum AuthErrorCode implements ErrorCode { ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_002", "이미 발급된 인증번호가 있습니다. 잠시 후 다시 시도해 주세요."), NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "발급된 인증번호가 없습니다. 인증번호를 먼저 요청해 주세요."), INVALID_VERIFICATION_CODE(HttpStatus.FORBIDDEN, "AUTH_004", "잘못된 인증번호입니다. 입력한 인증번호를 확인해 주세요."), - UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_005", "인증되지 않은 이메일입니다. 이메일 인증을 완료해 주세요."); + UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_005", "인증되지 않은 이메일입니다. 이메일 인증을 완료해 주세요."), + EXISTING_EMAIL(HttpStatus.CONFLICT, "AUTH_006", "이미 가입된 이메일입니다. 다른 이메일을 사용해 주세요."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index df1a73c8..a97530b9 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -51,6 +51,9 @@ private User findByEmailOrThrow(UserLoginDto.Request req) { public void sendEmailVerificationCode(EmailVerificationDto.Request req) { final String email = req.email(); + validateEmailIsAvailable(email); + validateVerificationCodeNotExists(email); + if (emailVerificationRedisRepository.existCode(email)) { throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); } @@ -62,6 +65,18 @@ public void sendEmailVerificationCode(EmailVerificationDto.Request req) { eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code)); } + private void validateEmailIsAvailable(String email) { + if (userRepository.existsByEmail(email)) { + throw new BizException(AuthErrorCode.EXISTING_EMAIL); + } + } + + private void validateVerificationCodeNotExists(String email) { + if (emailVerificationRedisRepository.existCode(email)) { + throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); + } + } + public void confirmEmailVerificationCode(EmailVerificationConfirmDto.Request req) { String email = req.email(); From 461b512f5662926c2a561ed2f759ac9817db3839 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:17:31 +0900 Subject: [PATCH 30/40] =?UTF-8?q?Refactor:=20=EC=9D=B8=EC=A6=9D=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B0=9C=EA=B8=89=20=EC=97=AC=EB=B6=80=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/project/flipnote/auth/service/AuthService.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index a97530b9..a4ef62d7 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -54,10 +54,6 @@ public void sendEmailVerificationCode(EmailVerificationDto.Request req) { validateEmailIsAvailable(email); validateVerificationCodeNotExists(email); - if (emailVerificationRedisRepository.existCode(email)) { - throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); - } - final String code = generateVerificationCode(VerificationConstants.CODE_LENGTH); emailVerificationRedisRepository.saveCode(email, code); From 4d2eebab2621ee8128317e59f07736ad938d12b3 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:25:07 +0900 Subject: [PATCH 31/40] =?UTF-8?q?Refactor:=20AuthService=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/service/AuthService.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index a4ef62d7..e7cb2f6b 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -36,9 +36,7 @@ public class AuthService { public TokenPair login(UserLoginDto.Request req) { User user = findByEmailOrThrow(req); - if (!passwordEncoder.matches(req.password(), user.getPassword())) { - throw new BizException(AuthErrorCode.INVALID_CREDENTIALS); - } + validatePasswordMatch(req.password(), user.getPassword()); return jwtComponent.generateTokenPair(user.getEmail(), user.getId(), user.getRole().name()); } @@ -48,6 +46,12 @@ private User findByEmailOrThrow(UserLoginDto.Request req) { .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); } + private void validatePasswordMatch(String rawPassword, String encodedPassword) { + if (!passwordEncoder.matches(rawPassword, encodedPassword)) { + throw new BizException(AuthErrorCode.INVALID_CREDENTIALS); + } + } + public void sendEmailVerificationCode(EmailVerificationDto.Request req) { final String email = req.email(); @@ -76,17 +80,25 @@ private void validateVerificationCodeNotExists(String email) { public void confirmEmailVerificationCode(EmailVerificationConfirmDto.Request req) { String email = req.email(); - String code = emailVerificationRedisRepository.findCode(email) - .orElseThrow(() -> new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE)); + String code = findVerificationCodeOrThrow(email); - if (!Objects.equals(req.code(), code)) { - throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); - } + validateVerificationCode(req.code(), code); emailVerificationRedisRepository.deleteCode(email); emailVerificationRedisRepository.markAsVerified(email); } + private String findVerificationCodeOrThrow(String email) { + return emailVerificationRedisRepository.findCode(email) + .orElseThrow(() -> new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE)); + } + + private void validateVerificationCode(String inputCode, String savedCode) { + if (!Objects.equals(inputCode, savedCode)) { + throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); + } + } + public void validateEmail(String email) { if (!emailVerificationRedisRepository.isVerified(email)) { throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL); From 19fc7fea42d0bb7a885740a5138c8a4c6a0b8c65 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:28:52 +0900 Subject: [PATCH 32/40] =?UTF-8?q?Fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=97=90=EC=84=9C=20=EC=9D=B8=EC=A6=9D=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/infra/email/ResendEmailService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/infra/email/ResendEmailService.java b/src/main/java/project/flipnote/infra/email/ResendEmailService.java index 2bb56edf..dbc4b415 100644 --- a/src/main/java/project/flipnote/infra/email/ResendEmailService.java +++ b/src/main/java/project/flipnote/infra/email/ResendEmailService.java @@ -40,7 +40,7 @@ public void sendEmailVerificationCode(String to, String code, int ttl) { try { resend.emails().send(params); } catch (ResendException e) { - log.error("이메일 인증번호 발송 실패: to={}, code={}, ttl={}분", to, code, ttl, e); + log.error("이메일 인증번호 발송 실패: to={}, ttl={}분", to, ttl, e); throw new EmailSendException(e); } } From d335090da97a578c721e9bb456bb67a5d32cbf74 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:55:57 +0900 Subject: [PATCH 33/40] =?UTF-8?q?Test:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthServiceTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/java/project/flipnote/auth/service/AuthServiceTest.java diff --git a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java new file mode 100644 index 00000000..43ece726 --- /dev/null +++ b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java @@ -0,0 +1,86 @@ +package project.flipnote.auth.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import project.flipnote.auth.event.EmailVerificationSendEvent; +import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.model.EmailVerificationDto; +import project.flipnote.auth.repository.EmailVerificationRedisRepository; +import project.flipnote.common.exception.BizException; +import project.flipnote.user.repository.UserRepository; + +@DisplayName("인증 서비스 단위 테스트") +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @InjectMocks + AuthService authService; + + @Mock + EmailVerificationRedisRepository emailVerificationRedisRepository; + + @Mock + UserRepository userRepository; + + @Mock + ApplicationEventPublisher eventPublisher; + + @DisplayName("이메일 인증번호 전송 테스트") + @Nested + class SendEmailVerificationCode { + + @DisplayName("성공") + @Test + void success() { + EmailVerificationDto.Request req = new EmailVerificationDto.Request("test@test.com"); + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(emailVerificationRedisRepository.existCode(any(String.class))).willReturn(false); + + authService.sendEmailVerificationCode(req); + + verify(emailVerificationRedisRepository, times(1)).saveCode(any(String.class), any(String.class)); + verify(eventPublisher, times(1)).publishEvent(any(EmailVerificationSendEvent.class)); + } + + @DisplayName("가입된 이메일인 경우 예외 발생") + @Test + void fail_existingEmail() { + EmailVerificationDto.Request req = new EmailVerificationDto.Request("test@test.com"); + + given(userRepository.existsByEmail(any(String.class))).willReturn(true); + + BizException exception = assertThrows(BizException.class, () -> authService.sendEmailVerificationCode(req)); + assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.EXISTING_EMAIL); + + verify(emailVerificationRedisRepository, never()).saveCode(any(String.class), any(String.class)); + verify(eventPublisher, never()).publishEvent(any(EmailVerificationSendEvent.class)); + } + + @DisplayName("이미 발급된 인증번호가 존재할 경우 예외 발생") + @Test + void fail_alreadyIssuedVerificationCode() { + EmailVerificationDto.Request req = new EmailVerificationDto.Request("test@test.com"); + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(emailVerificationRedisRepository.existCode(any(String.class))).willReturn(true); + + BizException exception = assertThrows(BizException.class, () -> authService.sendEmailVerificationCode(req)); + assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); + + verify(emailVerificationRedisRepository, never()).saveCode(any(String.class), any(String.class)); + verify(eventPublisher, never()).publishEvent(any(EmailVerificationSendEvent.class)); + } + } +} \ No newline at end of file From e17e93da4ac7411044d1a4f3ae388e25745ea7c1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 21:57:49 +0900 Subject: [PATCH 34/40] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20=EA=B0=84=EA=B2=B0?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/user/service/UserServiceTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index 3eb7503a..dfe30af5 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -60,9 +60,9 @@ void init() { @Nested class Register { - @DisplayName("회원가입 성공") + @DisplayName("성공") @Test - void success_register() { + void success() { UserRegisterDto.Request req = new UserRegisterDto.Request( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -80,9 +80,9 @@ void success_register() { verify(authService, times(1)).deleteVerifiedEmail(any(String.class)); } - @DisplayName("회원가입 성공 - 폰 번호가 null일 때") + @DisplayName("휴대전화 번호가 null일 때 성공") @Test - void success_register_phoneIsNull() { + void success_ifPhoneIsNull() { UserRegisterDto.Request req = new UserRegisterDto.Request( "test@test.com", "testPass", "테스트", "테스트", false, null, null ); @@ -101,7 +101,7 @@ void success_register_phoneIsNull() { @DisplayName("이메일 중복 시 예외 발생") @Test - void fail_register_duplicateEmail() { + void fail_duplicateEmail() { UserRegisterDto.Request req = new UserRegisterDto.Request( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -117,7 +117,7 @@ void fail_register_duplicateEmail() { @DisplayName("전화번호 중복 시 예외 발생") @Test - void fail_register_duplicatePhone() { + void fail_duplicatePhone() { UserRegisterDto.Request req = new UserRegisterDto.Request( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -133,7 +133,7 @@ void fail_register_duplicatePhone() { @DisplayName("이메일 인증이 안 된 경우 예외 발생") @Test - void fail_register_unverifiedEmail() { + void fail_unverifiedEmail() { UserRegisterDto.Request req = new UserRegisterDto.Request( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); From 950a9fd6b097abbe3774f9c439eda48f0b735604 Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 22:07:37 +0900 Subject: [PATCH 35/40] =?UTF-8?q?Test:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B0=84=EC=A0=91=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/auth/service/AuthServiceTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java index 43ece726..58d03b68 100644 --- a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java +++ b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java @@ -13,6 +13,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; +import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.model.EmailVerificationDto; @@ -52,6 +53,12 @@ void success() { verify(emailVerificationRedisRepository, times(1)).saveCode(any(String.class), any(String.class)); verify(eventPublisher, times(1)).publishEvent(any(EmailVerificationSendEvent.class)); + + int codeLength = VerificationConstants.CODE_LENGTH; + verify(emailVerificationRedisRepository).saveCode( + eq(req.email()), + argThat(code -> code.length() == codeLength && code.matches("\\d{%s}".formatted(codeLength))) + ); } @DisplayName("가입된 이메일인 경우 예외 발생") From db8265048f5463125634d606dd81fdbc63b53bdc Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 6 Jul 2025 22:15:17 +0900 Subject: [PATCH 36/40] =?UTF-8?q?Test:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthServiceTest.java | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java index 58d03b68..59649284 100644 --- a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java +++ b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.util.Optional; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -16,6 +18,7 @@ import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.model.EmailVerificationConfirmDto; import project.flipnote.auth.model.EmailVerificationDto; import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; @@ -90,4 +93,62 @@ void fail_alreadyIssuedVerificationCode() { verify(eventPublisher, never()).publishEvent(any(EmailVerificationSendEvent.class)); } } -} \ No newline at end of file + + @DisplayName("이메일 인증번호 확인 테스트") + @Nested + class ConfirmEmailVerificationCode { + + @DisplayName("성공") + @Test + void success() { + EmailVerificationConfirmDto.Request req + = new EmailVerificationConfirmDto.Request("test@test.com", "123456"); + + given(emailVerificationRedisRepository.findCode("test@test.com")) + .willReturn(Optional.of("123456")); + + authService.confirmEmailVerificationCode(req); + + verify(emailVerificationRedisRepository, times(1)).deleteCode(any(String.class)); + verify(emailVerificationRedisRepository, times(1)).markAsVerified(any(String.class)); + } + + @DisplayName("발급된 인증번호가 없는 경우 예외 발생") + @Test + void fail_notIssuedVerificationCode() { + EmailVerificationConfirmDto.Request req + = new EmailVerificationConfirmDto.Request("test@test.com", "123456"); + + given(emailVerificationRedisRepository.findCode("test@test.com")).willReturn(Optional.empty()); + + BizException exception = assertThrows( + BizException.class, + () -> authService.confirmEmailVerificationCode(req) + ); + assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE); + + verify(emailVerificationRedisRepository, never()).deleteCode(any(String.class)); + verify(emailVerificationRedisRepository, never()).markAsVerified(any(String.class)); + } + + @DisplayName("잘못된 인증번호인 경우 예외 발생") + @Test + void fail_invalidVerificationCode() { + EmailVerificationConfirmDto.Request req + = new EmailVerificationConfirmDto.Request("test@test.com", "123456"); + + given(emailVerificationRedisRepository.findCode("test@test.com")) + .willReturn(Optional.of("654321")); + + BizException exception = assertThrows( + BizException.class, + () -> authService.confirmEmailVerificationCode(req) + ); + assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_VERIFICATION_CODE); + + verify(emailVerificationRedisRepository, never()).deleteCode(any(String.class)); + verify(emailVerificationRedisRepository, never()).markAsVerified(any(String.class)); + } + + } +} From 5503f7b6f40db2730dabb195ba9df6693954a39d Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 8 Jul 2025 00:15:09 +0900 Subject: [PATCH 37/40] =?UTF-8?q?Refactor:=20UserRegisterDto=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD,=20=EC=9D=91=EB=8B=B5=20=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 7 +-- .../flipnote/user/model/UserRegisterDto.java | 47 ------------------- .../user/model/UserRegisterRequest.java | 34 ++++++++++++++ .../user/model/UserRegisterResponse.java | 10 ++++ .../flipnote/user/service/UserService.java | 7 +-- .../user/service/UserServiceTest.java | 17 +++---- 6 files changed, 61 insertions(+), 61 deletions(-) delete mode 100644 src/main/java/project/flipnote/user/model/UserRegisterDto.java create mode 100644 src/main/java/project/flipnote/user/model/UserRegisterRequest.java create mode 100644 src/main/java/project/flipnote/user/model/UserRegisterResponse.java diff --git a/src/main/java/project/flipnote/user/controller/UserController.java b/src/main/java/project/flipnote/user/controller/UserController.java index 32e08f0c..a98e67ee 100644 --- a/src/main/java/project/flipnote/user/controller/UserController.java +++ b/src/main/java/project/flipnote/user/controller/UserController.java @@ -9,7 +9,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.user.model.UserRegisterDto; +import project.flipnote.user.model.UserRegisterRequest; +import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.service.UserService; @RequiredArgsConstructor @@ -20,8 +21,8 @@ public class UserController { private final UserService userService; @PostMapping - public ResponseEntity register(@Valid @RequestBody UserRegisterDto.Request req) { - UserRegisterDto.Response res = userService.register(req); + public ResponseEntity register(@Valid @RequestBody UserRegisterRequest req) { + UserRegisterResponse res = userService.register(req); return ResponseEntity.status(HttpStatus.CREATED).body(res); } } diff --git a/src/main/java/project/flipnote/user/model/UserRegisterDto.java b/src/main/java/project/flipnote/user/model/UserRegisterDto.java deleted file mode 100644 index 0eeb729e..00000000 --- a/src/main/java/project/flipnote/user/model/UserRegisterDto.java +++ /dev/null @@ -1,47 +0,0 @@ -package project.flipnote.user.model; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import project.flipnote.common.annotation.ValidPassword; -import project.flipnote.common.annotation.ValidPhone; - -public class UserRegisterDto { - - public record Request( - - @Email @NotBlank - String email, - - @ValidPassword - String password, - - @NotBlank - String name, - - @NotBlank - String nickname, - - @NotNull - Boolean smsAgree, - - @ValidPhone - String phone, - - String profileImageUrl - ) { - - public String getCleanedPhone() { - return phone == null ? null : phone.replaceAll("-", ""); - } - } - - public record Response( - Long userId - ) { - - public static Response from(Long userId) { - return new Response(userId); - } - } -} diff --git a/src/main/java/project/flipnote/user/model/UserRegisterRequest.java b/src/main/java/project/flipnote/user/model/UserRegisterRequest.java new file mode 100644 index 00000000..6a75aa8d --- /dev/null +++ b/src/main/java/project/flipnote/user/model/UserRegisterRequest.java @@ -0,0 +1,34 @@ +package project.flipnote.user.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import project.flipnote.common.annotation.ValidPassword; +import project.flipnote.common.annotation.ValidPhone; + +public record UserRegisterRequest( + @Email @NotBlank + String email, + + @ValidPassword + String password, + + @NotBlank + String name, + + @NotBlank + String nickname, + + @NotNull + Boolean smsAgree, + + @ValidPhone + String phone, + + String profileImageUrl +) { + + public String getCleanedPhone() { + return phone == null ? null : phone.replaceAll("-", ""); + } +} diff --git a/src/main/java/project/flipnote/user/model/UserRegisterResponse.java b/src/main/java/project/flipnote/user/model/UserRegisterResponse.java new file mode 100644 index 00000000..9b9f1a2a --- /dev/null +++ b/src/main/java/project/flipnote/user/model/UserRegisterResponse.java @@ -0,0 +1,10 @@ +package project.flipnote.user.model; + +public record UserRegisterResponse( + Long userId +) { + + public static UserRegisterResponse from(Long userId) { + return new UserRegisterResponse(userId); + } +} diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 9db339cc..05c00e0c 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -11,7 +11,8 @@ import project.flipnote.common.exception.BizException; import project.flipnote.user.entity.User; import project.flipnote.user.exception.UserErrorCode; -import project.flipnote.user.model.UserRegisterDto; +import project.flipnote.user.model.UserRegisterRequest; +import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.repository.UserRepository; @RequiredArgsConstructor @@ -24,7 +25,7 @@ public class UserService { private final AuthService authService; @Transactional - public UserRegisterDto.Response register(UserRegisterDto.Request req) { + public UserRegisterResponse register(UserRegisterRequest req) { String email = req.email(); String phone = req.getCleanedPhone(); @@ -46,7 +47,7 @@ public UserRegisterDto.Response register(UserRegisterDto.Request req) { authService.deleteVerifiedEmail(email); - return UserRegisterDto.Response.from(savedUser.getId()); + return UserRegisterResponse.from(savedUser.getId()); } private void validateEmailDuplicate(String email) { diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index dfe30af5..67b8617a 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -20,7 +20,8 @@ import project.flipnote.common.exception.BizException; import project.flipnote.user.entity.User; import project.flipnote.user.exception.UserErrorCode; -import project.flipnote.user.model.UserRegisterDto; +import project.flipnote.user.model.UserRegisterRequest; +import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.repository.UserRepository; @DisplayName("회원 서비스 단위 테스트") @@ -63,7 +64,7 @@ class Register { @DisplayName("성공") @Test void success() { - UserRegisterDto.Request req = new UserRegisterDto.Request( + UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -72,7 +73,7 @@ void success() { given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass"); given(userRepository.save(any(User.class))).willReturn(user); - UserRegisterDto.Response res = userService.register(req); + UserRegisterResponse res = userService.register(req); assertThat(res.userId()).isEqualTo(user.getId()); @@ -83,7 +84,7 @@ void success() { @DisplayName("휴대전화 번호가 null일 때 성공") @Test void success_ifPhoneIsNull() { - UserRegisterDto.Request req = new UserRegisterDto.Request( + UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, null, null ); @@ -91,7 +92,7 @@ void success_ifPhoneIsNull() { given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass"); given(userRepository.save(any(User.class))).willReturn(user); - UserRegisterDto.Response res = userService.register(req); + UserRegisterResponse res = userService.register(req); assertThat(res.userId()).isEqualTo(user.getId()); @@ -102,7 +103,7 @@ void success_ifPhoneIsNull() { @DisplayName("이메일 중복 시 예외 발생") @Test void fail_duplicateEmail() { - UserRegisterDto.Request req = new UserRegisterDto.Request( + UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -118,7 +119,7 @@ void fail_duplicateEmail() { @DisplayName("전화번호 중복 시 예외 발생") @Test void fail_duplicatePhone() { - UserRegisterDto.Request req = new UserRegisterDto.Request( + UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -134,7 +135,7 @@ void fail_duplicatePhone() { @DisplayName("이메일 인증이 안 된 경우 예외 발생") @Test void fail_unverifiedEmail() { - UserRegisterDto.Request req = new UserRegisterDto.Request( + UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); From 9caf7d699c2e3deb4a773237cd7370e2b9fa9001 Mon Sep 17 00:00:00 2001 From: dungbik Date: Tue, 8 Jul 2025 00:28:32 +0900 Subject: [PATCH 38/40] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=EA=B4=80=EB=A0=A8=20DTO=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD,=20=EC=9D=91=EB=8B=B5=20=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 8 ++++---- ...a => EmailVerificationConfirmRequest.java} | 18 +++++++----------- ...Dto.java => EmailVerificationRequest.java} | 11 ++++------- .../flipnote/auth/service/AuthService.java | 8 ++++---- .../auth/service/AuthServiceTest.java | 19 ++++++++----------- 5 files changed, 27 insertions(+), 37 deletions(-) rename src/main/java/project/flipnote/auth/model/{EmailVerificationConfirmDto.java => EmailVerificationConfirmRequest.java} (51%) rename src/main/java/project/flipnote/auth/model/{EmailVerificationDto.java => EmailVerificationRequest.java} (56%) diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index 2160a0f7..ff93aeb2 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -9,8 +9,8 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.auth.model.EmailVerificationConfirmDto; -import project.flipnote.auth.model.EmailVerificationDto; +import project.flipnote.auth.model.EmailVerificationConfirmRequest; +import project.flipnote.auth.model.EmailVerificationRequest; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginDto; import project.flipnote.auth.service.AuthService; @@ -44,7 +44,7 @@ public ResponseEntity logout(HttpServletResponse servletResponse) { } @PostMapping("/email") - public ResponseEntity sendEmailVerificationCode(@Valid @RequestBody EmailVerificationDto.Request req) { + public ResponseEntity sendEmailVerificationCode(@Valid @RequestBody EmailVerificationRequest req) { authService.sendEmailVerificationCode(req); return ResponseEntity.ok().build(); @@ -52,7 +52,7 @@ public ResponseEntity sendEmailVerificationCode(@Valid @RequestBody EmailV @PostMapping("/email/confirm") public ResponseEntity confirmEmailVerificationCode( - @Valid @RequestBody EmailVerificationConfirmDto.Request req + @Valid @RequestBody EmailVerificationConfirmRequest req ) { authService.confirmEmailVerificationCode(req); diff --git a/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java b/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmRequest.java similarity index 51% rename from src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java rename to src/main/java/project/flipnote/auth/model/EmailVerificationConfirmRequest.java index f9a940e9..0e05b1e6 100644 --- a/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmDto.java +++ b/src/main/java/project/flipnote/auth/model/EmailVerificationConfirmRequest.java @@ -5,17 +5,13 @@ import jakarta.validation.constraints.Size; import project.flipnote.auth.constants.VerificationConstants; -public class EmailVerificationConfirmDto { +public record EmailVerificationConfirmRequest( - public record Request( + @Email @NotBlank + String email, - @Email @NotBlank - String email, - - @NotBlank - @Size(min = VerificationConstants.CODE_LENGTH, max = VerificationConstants.CODE_LENGTH) - String code - ) { - - } + @NotBlank + @Size(min = VerificationConstants.CODE_LENGTH, max = VerificationConstants.CODE_LENGTH) + String code +) { } diff --git a/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java b/src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java similarity index 56% rename from src/main/java/project/flipnote/auth/model/EmailVerificationDto.java rename to src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java index b274d231..0364de35 100644 --- a/src/main/java/project/flipnote/auth/model/EmailVerificationDto.java +++ b/src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java @@ -3,12 +3,9 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public class EmailVerificationDto { +public record EmailVerificationRequest( - public record Request( - - @Email @NotBlank - String email - ) { - } + @Email @NotBlank + String email +) { } diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index e7cb2f6b..e17152f5 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -11,8 +11,8 @@ import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.exception.AuthErrorCode; -import project.flipnote.auth.model.EmailVerificationConfirmDto; -import project.flipnote.auth.model.EmailVerificationDto; +import project.flipnote.auth.model.EmailVerificationConfirmRequest; +import project.flipnote.auth.model.EmailVerificationRequest; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginDto; import project.flipnote.auth.repository.EmailVerificationRedisRepository; @@ -52,7 +52,7 @@ private void validatePasswordMatch(String rawPassword, String encodedPassword) { } } - public void sendEmailVerificationCode(EmailVerificationDto.Request req) { + public void sendEmailVerificationCode(EmailVerificationRequest req) { final String email = req.email(); validateEmailIsAvailable(email); @@ -77,7 +77,7 @@ private void validateVerificationCodeNotExists(String email) { } } - public void confirmEmailVerificationCode(EmailVerificationConfirmDto.Request req) { + public void confirmEmailVerificationCode(EmailVerificationConfirmRequest req) { String email = req.email(); String code = findVerificationCodeOrThrow(email); diff --git a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java index 59649284..8af9a35d 100644 --- a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java +++ b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java @@ -18,8 +18,8 @@ import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.exception.AuthErrorCode; -import project.flipnote.auth.model.EmailVerificationConfirmDto; -import project.flipnote.auth.model.EmailVerificationDto; +import project.flipnote.auth.model.EmailVerificationConfirmRequest; +import project.flipnote.auth.model.EmailVerificationRequest; import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; import project.flipnote.user.repository.UserRepository; @@ -47,7 +47,7 @@ class SendEmailVerificationCode { @DisplayName("성공") @Test void success() { - EmailVerificationDto.Request req = new EmailVerificationDto.Request("test@test.com"); + EmailVerificationRequest req = new EmailVerificationRequest("test@test.com"); given(userRepository.existsByEmail(any(String.class))).willReturn(false); given(emailVerificationRedisRepository.existCode(any(String.class))).willReturn(false); @@ -67,7 +67,7 @@ void success() { @DisplayName("가입된 이메일인 경우 예외 발생") @Test void fail_existingEmail() { - EmailVerificationDto.Request req = new EmailVerificationDto.Request("test@test.com"); + EmailVerificationRequest req = new EmailVerificationRequest("test@test.com"); given(userRepository.existsByEmail(any(String.class))).willReturn(true); @@ -81,7 +81,7 @@ void fail_existingEmail() { @DisplayName("이미 발급된 인증번호가 존재할 경우 예외 발생") @Test void fail_alreadyIssuedVerificationCode() { - EmailVerificationDto.Request req = new EmailVerificationDto.Request("test@test.com"); + EmailVerificationRequest req = new EmailVerificationRequest("test@test.com"); given(userRepository.existsByEmail(any(String.class))).willReturn(false); given(emailVerificationRedisRepository.existCode(any(String.class))).willReturn(true); @@ -101,8 +101,7 @@ class ConfirmEmailVerificationCode { @DisplayName("성공") @Test void success() { - EmailVerificationConfirmDto.Request req - = new EmailVerificationConfirmDto.Request("test@test.com", "123456"); + EmailVerificationConfirmRequest req = new EmailVerificationConfirmRequest("test@test.com", "123456"); given(emailVerificationRedisRepository.findCode("test@test.com")) .willReturn(Optional.of("123456")); @@ -116,8 +115,7 @@ void success() { @DisplayName("발급된 인증번호가 없는 경우 예외 발생") @Test void fail_notIssuedVerificationCode() { - EmailVerificationConfirmDto.Request req - = new EmailVerificationConfirmDto.Request("test@test.com", "123456"); + EmailVerificationConfirmRequest req = new EmailVerificationConfirmRequest("test@test.com", "123456"); given(emailVerificationRedisRepository.findCode("test@test.com")).willReturn(Optional.empty()); @@ -134,8 +132,7 @@ void fail_notIssuedVerificationCode() { @DisplayName("잘못된 인증번호인 경우 예외 발생") @Test void fail_invalidVerificationCode() { - EmailVerificationConfirmDto.Request req - = new EmailVerificationConfirmDto.Request("test@test.com", "123456"); + EmailVerificationConfirmRequest req = new EmailVerificationConfirmRequest("test@test.com", "123456"); given(emailVerificationRedisRepository.findCode("test@test.com")) .willReturn(Optional.of("654321")); From 98b9df0843773fc427dc5713983bf031610316a0 Mon Sep 17 00:00:00 2001 From: dungbik Date: Fri, 11 Jul 2025 10:56:09 +0900 Subject: [PATCH 39/40] =?UTF-8?q?Feat:=20=ED=9C=B4=EB=8C=80=EC=A0=84?= =?UTF-8?q?=ED=99=94=20=EC=95=94=ED=98=B8=ED=99=94=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/service/AuthService.java | 3 + .../common/config/AsyncProperties.java | 2 +- .../common/crypto/AesCryptoConfig.java | 22 +++++ .../common/crypto/AesCryptoConverter.java | 80 +++++++++++++++++++ .../common/crypto/AesCryptoProperties.java | 25 ++++++ .../common/security/jwt/JwtProperties.java | 2 +- .../annotation/ValidPassword.java | 4 +- .../annotation/ValidPhone.java | 4 +- .../PasswordConstraintValidator.java | 4 +- .../validator/PhoneConstraintValidator.java | 4 +- .../infra/config/ResendProperties.java | 2 +- .../project/flipnote/user/entity/User.java | 19 +---- .../user/model/UserRegisterRequest.java | 4 +- src/main/resources/application-local.yml | 26 ------ src/main/resources/application.yml | 38 +++++---- .../PasswordConstraintValidatorTest.java | 1 + .../PhoneConstraintValidatorTest.java | 1 + .../resources/application-test.yml | 12 ++- 18 files changed, 178 insertions(+), 75 deletions(-) create mode 100644 src/main/java/project/flipnote/common/crypto/AesCryptoConfig.java create mode 100644 src/main/java/project/flipnote/common/crypto/AesCryptoConverter.java create mode 100644 src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java rename src/main/java/project/flipnote/common/{ => validation}/annotation/ValidPassword.java (81%) rename src/main/java/project/flipnote/common/{ => validation}/annotation/ValidPhone.java (82%) rename src/main/java/project/flipnote/common/{ => validation}/validator/PasswordConstraintValidator.java (84%) rename src/main/java/project/flipnote/common/{ => validation}/validator/PhoneConstraintValidator.java (79%) delete mode 100644 src/main/resources/application-local.yml rename src/{main => test}/resources/application-test.yml (54%) diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index e17152f5..f8426723 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.exception.AuthErrorCode; @@ -21,6 +22,7 @@ import project.flipnote.user.entity.User; import project.flipnote.user.repository.UserRepository; +@Slf4j @RequiredArgsConstructor @Service public class AuthService { @@ -37,6 +39,7 @@ public TokenPair login(UserLoginDto.Request req) { User user = findByEmailOrThrow(req); validatePasswordMatch(req.password(), user.getPassword()); + log.error("{}", user.getPhone()); return jwtComponent.generateTokenPair(user.getEmail(), user.getId(), user.getRole().name()); } diff --git a/src/main/java/project/flipnote/common/config/AsyncProperties.java b/src/main/java/project/flipnote/common/config/AsyncProperties.java index 71eaefa1..a0913b8a 100644 --- a/src/main/java/project/flipnote/common/config/AsyncProperties.java +++ b/src/main/java/project/flipnote/common/config/AsyncProperties.java @@ -11,7 +11,7 @@ @Getter @Setter @Validated -@ConfigurationProperties(prefix = "async") +@ConfigurationProperties(prefix = "app.async") @Component public class AsyncProperties { diff --git a/src/main/java/project/flipnote/common/crypto/AesCryptoConfig.java b/src/main/java/project/flipnote/common/crypto/AesCryptoConfig.java new file mode 100644 index 00000000..4f233933 --- /dev/null +++ b/src/main/java/project/flipnote/common/crypto/AesCryptoConfig.java @@ -0,0 +1,22 @@ +package project.flipnote.common.crypto; + +import java.nio.charset.StandardCharsets; + +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Configuration +public class AesCryptoConfig { + + private final AesCryptoProperties aesCryptoProperties; + + @Bean + public SecretKeySpec aesSecretKeySpec() { + return new SecretKeySpec(aesCryptoProperties.getKey().getBytes(StandardCharsets.UTF_8), "AES"); + } +} diff --git a/src/main/java/project/flipnote/common/crypto/AesCryptoConverter.java b/src/main/java/project/flipnote/common/crypto/AesCryptoConverter.java new file mode 100644 index 00000000..9e256078 --- /dev/null +++ b/src/main/java/project/flipnote/common/crypto/AesCryptoConverter.java @@ -0,0 +1,80 @@ +package project.flipnote.common.crypto; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Converter +@Component +public class AesCryptoConverter implements AttributeConverter { + + private final SecretKeySpec aesSecretKeySpec; + + private static final int IV_SIZE_BYTES = 16; + private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; + + @Override + public String convertToDatabaseColumn(String attribute) { + if (!StringUtils.hasText(attribute)) { + return attribute; + } + + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + + byte[] iv = new byte[IV_SIZE_BYTES]; + new SecureRandom().nextBytes(iv); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.ENCRYPT_MODE, aesSecretKeySpec, ivParameterSpec); + byte[] encrypted = cipher.doFinal(attribute.getBytes(StandardCharsets.UTF_8)); + + byte[] combined = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length); + + return Base64.getEncoder().encodeToString(combined); + } catch (Exception e) { + throw new IllegalStateException("데이터 암호화에 실패했습니다.", e); + } + } + + @Override + public String convertToEntityAttribute(String dbData) { + if (!StringUtils.hasText(dbData)) { + return dbData; + } + + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + + byte[] combined = Base64.getDecoder().decode(dbData); + + byte[] iv = new byte[IV_SIZE_BYTES]; + System.arraycopy(combined, 0, iv, 0, iv.length); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + + byte[] encrypted = new byte[combined.length - iv.length]; + System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length); + + cipher.init(Cipher.DECRYPT_MODE, aesSecretKeySpec, ivParameterSpec); + byte[] decrypted = cipher.doFinal(encrypted); + + return new String(decrypted, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new IllegalStateException("데이터 복호화에 실패했습니다.", e); + } + } +} diff --git a/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java b/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java new file mode 100644 index 00000000..430c7294 --- /dev/null +++ b/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java @@ -0,0 +1,25 @@ +package project.flipnote.common.crypto; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Validated +@ConfigurationProperties(prefix = "app.encryption") +@Component +public class AesCryptoProperties { + + @NotBlank + @Pattern( + regexp = "^.{32}$", + message = "암호화 키는 32바이트여야 합니다." + ) + private String key; +} diff --git a/src/main/java/project/flipnote/common/security/jwt/JwtProperties.java b/src/main/java/project/flipnote/common/security/jwt/JwtProperties.java index 9c53f486..1eee3eb5 100644 --- a/src/main/java/project/flipnote/common/security/jwt/JwtProperties.java +++ b/src/main/java/project/flipnote/common/security/jwt/JwtProperties.java @@ -13,7 +13,7 @@ @Getter @Setter @Component -@ConfigurationProperties(prefix = "jwt") +@ConfigurationProperties(prefix = "app.jwt") public class JwtProperties { private String secret; private Duration accessTokenExpiration; diff --git a/src/main/java/project/flipnote/common/annotation/ValidPassword.java b/src/main/java/project/flipnote/common/validation/annotation/ValidPassword.java similarity index 81% rename from src/main/java/project/flipnote/common/annotation/ValidPassword.java rename to src/main/java/project/flipnote/common/validation/annotation/ValidPassword.java index aa59d5f5..ce0f4aa8 100644 --- a/src/main/java/project/flipnote/common/annotation/ValidPassword.java +++ b/src/main/java/project/flipnote/common/validation/annotation/ValidPassword.java @@ -1,4 +1,4 @@ -package project.flipnote.common.annotation; +package project.flipnote.common.validation.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -7,7 +7,7 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; -import project.flipnote.common.validator.PasswordConstraintValidator; +import project.flipnote.common.validation.validator.PasswordConstraintValidator; @Constraint(validatedBy = PasswordConstraintValidator.class) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) diff --git a/src/main/java/project/flipnote/common/annotation/ValidPhone.java b/src/main/java/project/flipnote/common/validation/annotation/ValidPhone.java similarity index 82% rename from src/main/java/project/flipnote/common/annotation/ValidPhone.java rename to src/main/java/project/flipnote/common/validation/annotation/ValidPhone.java index b5250098..552cc6c1 100644 --- a/src/main/java/project/flipnote/common/annotation/ValidPhone.java +++ b/src/main/java/project/flipnote/common/validation/annotation/ValidPhone.java @@ -1,4 +1,4 @@ -package project.flipnote.common.annotation; +package project.flipnote.common.validation.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -7,7 +7,7 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; -import project.flipnote.common.validator.PhoneConstraintValidator; +import project.flipnote.common.validation.validator.PhoneConstraintValidator; @Constraint(validatedBy = PhoneConstraintValidator.class) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) diff --git a/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java b/src/main/java/project/flipnote/common/validation/validator/PasswordConstraintValidator.java similarity index 84% rename from src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java rename to src/main/java/project/flipnote/common/validation/validator/PasswordConstraintValidator.java index 14acecca..a789feff 100644 --- a/src/main/java/project/flipnote/common/validator/PasswordConstraintValidator.java +++ b/src/main/java/project/flipnote/common/validation/validator/PasswordConstraintValidator.java @@ -1,11 +1,11 @@ -package project.flipnote.common.validator; +package project.flipnote.common.validation.validator; import java.util.Objects; import java.util.regex.Pattern; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -import project.flipnote.common.annotation.ValidPassword; +import project.flipnote.common.validation.annotation.ValidPassword; public class PasswordConstraintValidator implements ConstraintValidator { diff --git a/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java b/src/main/java/project/flipnote/common/validation/validator/PhoneConstraintValidator.java similarity index 79% rename from src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java rename to src/main/java/project/flipnote/common/validation/validator/PhoneConstraintValidator.java index e048b0c3..636d294e 100644 --- a/src/main/java/project/flipnote/common/validator/PhoneConstraintValidator.java +++ b/src/main/java/project/flipnote/common/validation/validator/PhoneConstraintValidator.java @@ -1,10 +1,10 @@ -package project.flipnote.common.validator; +package project.flipnote.common.validation.validator; import java.util.Objects; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -import project.flipnote.common.annotation.ValidPhone; +import project.flipnote.common.validation.annotation.ValidPhone; public class PhoneConstraintValidator implements ConstraintValidator { diff --git a/src/main/java/project/flipnote/infra/config/ResendProperties.java b/src/main/java/project/flipnote/infra/config/ResendProperties.java index c654b110..7b9268c1 100644 --- a/src/main/java/project/flipnote/infra/config/ResendProperties.java +++ b/src/main/java/project/flipnote/infra/config/ResendProperties.java @@ -11,7 +11,7 @@ @Getter @Setter @Validated -@ConfigurationProperties("resend") +@ConfigurationProperties("app.resend") @Component public class ResendProperties { diff --git a/src/main/java/project/flipnote/user/entity/User.java b/src/main/java/project/flipnote/user/entity/User.java index 98e9afe7..24e71942 100644 --- a/src/main/java/project/flipnote/user/entity/User.java +++ b/src/main/java/project/flipnote/user/entity/User.java @@ -1,32 +1,20 @@ package project.flipnote.user.entity; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import project.flipnote.common.crypto.AesCryptoConverter; import project.flipnote.common.entity.SoftDeletableEntity; @Getter @@ -53,7 +41,8 @@ public class User extends SoftDeletableEntity { private String profileImageUrl; - @Column(unique = true) + @Convert(converter = AesCryptoConverter.class) + @Column(unique = true, length = 1024) private String phone; private boolean smsAgree; diff --git a/src/main/java/project/flipnote/user/model/UserRegisterRequest.java b/src/main/java/project/flipnote/user/model/UserRegisterRequest.java index 6a75aa8d..24e1cac5 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterRequest.java +++ b/src/main/java/project/flipnote/user/model/UserRegisterRequest.java @@ -3,8 +3,8 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import project.flipnote.common.annotation.ValidPassword; -import project.flipnote.common.annotation.ValidPhone; +import project.flipnote.common.validation.annotation.ValidPassword; +import project.flipnote.common.validation.annotation.ValidPhone; public record UserRegisterRequest( @Email @NotBlank diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml deleted file mode 100644 index a13729af..00000000 --- a/src/main/resources/application-local.yml +++ /dev/null @@ -1,26 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://localhost:3306/flipnote?useSSL=false&allowPublicKeyRetrieval=true - username: root - password: root - - jpa: - hibernate: - ddl-auto: update - - properties: - hibernate: - show_sql: true - format_sql: true - use_sql_comments: true - - data: - redis: - host: localhost - port: 6379 - -jwt: - secret: dummy_secret - -resend: - api-key: dummy_api_key diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc350b9a..9329300b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,9 +4,9 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: - username: - password: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} jpa: open-in-view: false @@ -26,9 +26,9 @@ spring: data: redis: - host: - password: - port: + host: ${SPRING_DATA_REDIS_HOST} + password: ${SPRING_DATA_REDIS_PASSWORD} + port: ${SPRING_DATA_REDIS_PORT} management: endpoints: @@ -37,16 +37,20 @@ management: include: - health -jwt: - secret: - access-token-expiration: 1h - refresh-token-expiration: 7d +app: + jwt: + secret: ${APP_JWT_SECRET} + access-token-expiration: 1h + refresh-token-expiration: 7d -async: - core-pool-size: 4 - max-pool-size: 10 - queue-capacity: 100 + async: + core-pool-size: 4 + max-pool-size: 10 + queue-capacity: 100 -resend: - api-key: - from-email: FlipNote + resend: + api-key: ${APP_RESEND_API_KEY} + from-email: FlipNote + + encryption: + key: ${APP_ENCRYPTION_KEY} diff --git a/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java b/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java index 17308aa5..17a5e1a6 100644 --- a/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java +++ b/src/test/java/project/flipnote/common/validator/PasswordConstraintValidatorTest.java @@ -9,6 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import jakarta.validation.ConstraintValidatorContext; +import project.flipnote.common.validation.validator.PasswordConstraintValidator; @DisplayName("비밀번호 유효성 검증기 단위 테스트") @ExtendWith(MockitoExtension.class) diff --git a/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java b/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java index 3615d655..c8a8ca34 100644 --- a/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java +++ b/src/test/java/project/flipnote/common/validator/PhoneConstraintValidatorTest.java @@ -9,6 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import jakarta.validation.ConstraintValidatorContext; +import project.flipnote.common.validation.validator.PhoneConstraintValidator; @DisplayName("휴대폰 번호 유효성 검증기 단위 테스트") @ExtendWith(MockitoExtension.class) diff --git a/src/main/resources/application-test.yml b/src/test/resources/application-test.yml similarity index 54% rename from src/main/resources/application-test.yml rename to src/test/resources/application-test.yml index a0ef278f..1618bf1e 100644 --- a/src/main/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -20,8 +20,12 @@ spring: host: localhost port: 6379 -jwt: - secret: 55ca298dcfc216e215622e3f48a251abaa4e8bb973074f065ab170e311acc15811d01a2407290c3ac143648196306d4a6f666a4ed364d3df633e08eb184bb0aea0f2edde4fd2d7fa68ea95ddbc421ff532ce47bde775975911042d665bc22d88a9fa26a03bb4d25530b8cdeb1247d87c9e3efcd721e368b0566b00a43308a729 +app: + jwt: + secret: fBwvzR4gS8pL7C2R5E9Q3aV6y9wB2b8HkM5nP3sUaX7gS1rV4wE8yD2bF6jN9kL4 -resend: - api-key: dummy_api_key + resend: + api-key: dummy_api_key + + encryption: + key: 49015f426db2b8477d5251fd3de971ae From cf3a53e43362ebb98b43ed66cf640003b817a300 Mon Sep 17 00:00:00 2001 From: dungbik Date: Fri, 11 Jul 2025 11:32:50 +0900 Subject: [PATCH 40/40] =?UTF-8?q?Feat:=20AES=20=ED=82=A4=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/common/crypto/AesCryptoProperties.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java b/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java index 430c7294..82669da1 100644 --- a/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java +++ b/src/main/java/project/flipnote/common/crypto/AesCryptoProperties.java @@ -18,8 +18,8 @@ public class AesCryptoProperties { @NotBlank @Pattern( - regexp = "^.{32}$", - message = "암호화 키는 32바이트여야 합니다." + regexp = "^[A-Za-z0-9+/]{32}$", + message = "암호화 키는 32자의 Base64 문자열이어야 합니다." ) private String key; }