Skip to content

Commit 4c2e7e9

Browse files
committed
fix: 메일 전송 인증 오류 수정
1 parent cd5f9e1 commit 4c2e7e9

File tree

5 files changed

+81
-41
lines changed

5 files changed

+81
-41
lines changed

src/main/java/grep/neogulcoder/domain/users/controller/UserController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ public ResponseEntity<ApiResponse<Void>> sendCode(@Valid @RequestBody SendCodeTo
9494
@PostMapping("/mail/verify")
9595
public ResponseEntity<ApiResponse<Boolean>> verifyCode(@Valid @RequestBody EmailVerifyRequest request) {
9696
LocalDateTime currentDateTime = LocalDateTime.now();
97-
boolean verified = mailService.verifyEmailCode(request.getEmail(), currentDateTime);
98-
return ResponseEntity.ok(ApiResponse.success(verified));
97+
boolean verified = mailService.verifyEmailCode(request, currentDateTime);
98+
return ResponseEntity.ok(ApiResponse.success("인증 성공 여부", verified));
9999
}
100100

101101
private boolean isNotMatchPassword(String password, String passwordCheck) {

src/main/java/grep/neogulcoder/domain/users/controller/dto/request/EmailVerifyRequest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import jakarta.validation.constraints.Email;
44
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Builder;
56
import lombok.Getter;
67

78
@Getter
@@ -11,6 +12,22 @@ public class EmailVerifyRequest {
1112
@Email(message = "올바른 이메일 형식이어야 합니다")
1213
private String email;
1314

15+
@NotBlank(message = "인증코드는 필수입니다")
16+
private String code;
17+
1418
public EmailVerifyRequest() {
1519
}
20+
21+
@Builder
22+
private EmailVerifyRequest(String email, String code) {
23+
this.email = email;
24+
this.code = code;
25+
}
26+
27+
public static EmailVerifyRequest of(String email, String code){
28+
return EmailVerifyRequest.builder()
29+
.email(email)
30+
.code(code)
31+
.build();
32+
}
1633
}

src/main/java/grep/neogulcoder/domain/users/service/MailService.java

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package grep.neogulcoder.domain.users.service;
22

3-
import grep.neogulcoder.domain.users.exception.EmailDuplicationException;
3+
import grep.neogulcoder.domain.users.controller.dto.request.EmailVerifyRequest;
44
import grep.neogulcoder.domain.users.exception.MailSendException;
5-
import grep.neogulcoder.domain.users.exception.NotVerifiedEmailException;
65
import grep.neogulcoder.domain.users.exception.code.UserErrorCode;
7-
import grep.neogulcoder.domain.users.repository.UserRepository;
86
import jakarta.mail.MessagingException;
97
import jakarta.mail.internet.MimeMessage;
108
import lombok.RequiredArgsConstructor;
@@ -17,9 +15,10 @@
1715
import org.springframework.transaction.annotation.Transactional;
1816

1917
import java.time.LocalDateTime;
20-
import java.util.Optional;
18+
import java.util.Objects;
2119
import java.util.Random;
2220
import java.util.Set;
21+
import java.util.concurrent.CompletableFuture;
2322
import java.util.concurrent.ConcurrentHashMap;
2423

2524
@Slf4j
@@ -28,34 +27,42 @@
2827
@RequiredArgsConstructor
2928
public class MailService {
3029

31-
private final ConcurrentHashMap<String, LocalDateTime> emailExpiredTimeMap = new ConcurrentHashMap<>();
30+
private final ConcurrentHashMap<EmailVerification, LocalDateTime> verificationExpiredTimeMap = new ConcurrentHashMap<>();
3231
private final Set<String> verifiedEmailSet = ConcurrentHashMap.newKeySet();
3332
private final JavaMailSender mailSender;
34-
private final UserRepository userRepository;
3533

3634
private static final int VERIFY_LIMIT_MINUTE = 5;
3735

3836
@Scheduled(cron = "0 0 0 * * * ")
3937
public void clearStores() {
40-
emailExpiredTimeMap.clear();
38+
verificationExpiredTimeMap.clear();
4139
verifiedEmailSet.clear();
4240
log.info("회원 인증 코드 저장 Map, Set Clear");
4341
}
4442

4543
@Async("mailExecutor")
46-
public void sendCodeTo(String email, LocalDateTime currentDateTime) {
44+
public CompletableFuture<String> sendCodeTo(String email, LocalDateTime currentDateTime) {
4745
LocalDateTime expiredDateTime = currentDateTime.plusMinutes(VERIFY_LIMIT_MINUTE);
48-
emailExpiredTimeMap.put(email, expiredDateTime);
49-
sendCodeTo(email);
46+
String code = generateRandomIntCode();
47+
48+
verificationExpiredTimeMap.put(new EmailVerification(email, code), expiredDateTime);
49+
sendCodeTo(email, code);
50+
return CompletableFuture.completedFuture(code);
5051
}
5152

52-
public boolean verifyEmailCode(String email, LocalDateTime currentDateTime) {
53-
LocalDateTime expiredTime = Optional.ofNullable(emailExpiredTimeMap.get(email))
54-
.orElseThrow(() -> new NotVerifiedEmailException(UserErrorCode.NOT_VERIFIED_EMAIL));
53+
public boolean verifyEmailCode(EmailVerifyRequest request, LocalDateTime currentDateTime) {
54+
String email = request.getEmail();
55+
String code = request.getCode();
56+
EmailVerification emailVerification = new EmailVerification(email, code);
57+
58+
LocalDateTime expiredTime = verificationExpiredTimeMap.get(emailVerification);
59+
if (expiredTime == null) {
60+
return false;
61+
}
5562

5663
boolean isVerify = currentDateTime.isBefore(expiredTime);
5764
if (isVerify) {
58-
verifiedEmailSet.add(email);
65+
verifiedEmailSet.add(request.getEmail());
5966
}
6067
return isVerify;
6168
}
@@ -64,15 +71,9 @@ public boolean confirmNotVerifiedEmail(String email) {
6471
return !verifiedEmailSet.contains(email);
6572
}
6673

67-
private boolean isDuplicateEmail(String email) {
68-
return userRepository.findByEmail(email).isPresent();
69-
}
70-
71-
private void sendCodeTo(String email) {
72-
String code = generateRandomIntCode();
73-
MimeMessage mimeMessage = mailSender.createMimeMessage();
74-
74+
private void sendCodeTo(String email, String code) {
7575
try {
76+
MimeMessage mimeMessage = mailSender.createMimeMessage();
7677
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
7778
helper.setTo(email);
7879
helper.setSubject("[wibby] 이메일 인증 코드");
@@ -105,4 +106,26 @@ private String getContent(String code) {
105106
+ " <p style='font-size:12px;color:#888;'>이 코드는 5분간 유효합니다.</p>"
106107
+ "</div>";
107108
}
109+
110+
private static class EmailVerification {
111+
private String email;
112+
private String code;
113+
114+
private EmailVerification(String email, String code) {
115+
this.email = email;
116+
this.code = code;
117+
}
118+
119+
@Override
120+
public boolean equals(Object o) {
121+
if (o == null || getClass() != o.getClass()) return false;
122+
EmailVerification that = (EmailVerification) o;
123+
return Objects.equals(email, that.email) && Objects.equals(code, that.code);
124+
}
125+
126+
@Override
127+
public int hashCode() {
128+
return Objects.hash(email, code);
129+
}
130+
}
108131
}

src/main/java/grep/neogulcoder/global/response/ApiResponse.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public static <T> ApiResponse<T> error(String message) {
2525
return new ApiResponse<>(CommonCode.BAD_REQUEST.getCode(), message, null);
2626
}
2727

28+
public static <T> ApiResponse<T> success(String message, T data) {
29+
return ApiResponse.of(CommonCode.OK.getCode(), message, data);
30+
}
31+
2832
public static <T> ApiResponse<T> success(T data) {
2933
return ApiResponse.of(CommonCode.OK.getCode(), CommonCode.OK.getMessage(), data);
3034
}
@@ -33,10 +37,6 @@ public static <T> ApiResponse<T> success(String message) {
3337
return ApiResponse.of(CommonCode.OK.getCode(), message, null);
3438
}
3539

36-
public static <T> ApiResponse<T> successWithCode(T data){
37-
return ApiResponse.of(CommonCode.OK.getCode(), CommonCode.OK.getMessage(), data);
38-
}
39-
4040
public static <T> ApiResponse<T> noContent(){
4141
return ApiResponse.of(CommonCode.NO_CONTENT.getCode(), CommonCode.NO_CONTENT.getMessage(), null);
4242
}
@@ -45,10 +45,6 @@ public static <T> ApiResponse<T> create(){
4545
return ApiResponse.of(CommonCode.CREATED.getCode(), CommonCode.CREATED.getMessage(), null);
4646
}
4747

48-
public static <T> ApiResponse<T> badRequest() {
49-
return ApiResponse.of(CommonCode.BAD_REQUEST.getCode(), CommonCode.BAD_REQUEST.getMessage(), null);
50-
}
51-
5248
public static <T> ApiResponse<T> errorWithoutData(Code code) {
5349
return ApiResponse.of(code.getCode(), code.getMessage(), null);
5450
}

src/test/java/grep/neogulcoder/domain/users/service/MailServiceTest.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package grep.neogulcoder.domain.users.service;
22

33
import grep.neogulcoder.domain.IntegrationTestSupport;
4+
import grep.neogulcoder.domain.users.controller.dto.request.EmailVerifyRequest;
45
import grep.neogulcoder.domain.users.repository.UserRepository;
56
import jakarta.mail.internet.MimeMessage;
67
import lombok.extern.slf4j.Slf4j;
@@ -14,6 +15,7 @@
1415

1516
import java.time.LocalDateTime;
1617
import java.util.List;
18+
import java.util.concurrent.ExecutionException;
1719
import java.util.concurrent.Executor;
1820
import java.util.concurrent.ThreadPoolExecutor;
1921
import java.util.stream.Stream;
@@ -43,7 +45,7 @@ void sendCodeTo() {
4345
//given
4446
JavaMailSender spyMailSender = spy(mailSender);
4547
doNothing().when(spyMailSender).send(any(MimeMessage.class));
46-
MailService spyMailService = new MailService(spyMailSender, userRepository);
48+
MailService spyMailService = new MailService(spyMailSender);
4749

4850
String testEmail = "[email protected]";
4951
LocalDateTime dateTime = LocalDateTime.of(2025, 9, 7, 16, 30);
@@ -66,39 +68,41 @@ void realSendToCode() {
6668

6769
@DisplayName("회원의 이메일로 전송된 인증 코드는 5분 이내 검증 가능 하다.")
6870
@Test
69-
void verifyEmailCode() {
71+
void verifyEmailCode() throws ExecutionException, InterruptedException {
7072
//given
7173
JavaMailSender spyMailSender = spy(mailSender);
7274
doNothing().when(spyMailSender).send(any(MimeMessage.class));
73-
MailService spyMailService = new MailService(spyMailSender, userRepository);
75+
MailService spyMailService = new MailService(spyMailSender);
7476

7577
String testEmail = "[email protected]";
7678
LocalDateTime requestDateTime = LocalDateTime.of(2025, 9, 7, 16, 30);
7779
LocalDateTime verifyDateTime = LocalDateTime.of(2025, 9, 7, 16, 34, 59);
7880

7981
//when
80-
spyMailService.sendCodeTo(testEmail, requestDateTime);
81-
boolean isVerify = spyMailService.verifyEmailCode(testEmail, verifyDateTime);
82+
String code = spyMailService.sendCodeTo(testEmail, requestDateTime).get();
83+
EmailVerifyRequest request = EmailVerifyRequest.of(testEmail, code);
84+
boolean isVerify = spyMailService.verifyEmailCode(request, verifyDateTime);
8285

8386
//then
8487
assertThat(isVerify).isTrue();
8588
}
8689

8790
@DisplayName("회원의 이메일로 전송된 인증 코드는 5분 이후 검증 시 실패 한다.")
8891
@Test
89-
void verifyEmailCode_WhenExpired_ThenFailed() {
92+
void verifyEmailCode_WhenExpired_ThenFailed() throws ExecutionException, InterruptedException {
9093
//given
9194
JavaMailSender spyMailSender = spy(mailSender);
9295
doNothing().when(spyMailSender).send(any(MimeMessage.class));
93-
MailService spyMailService = new MailService(spyMailSender, userRepository);
96+
MailService spyMailService = new MailService(spyMailSender);
9497

9598
String testEmail = "[email protected]";
9699
LocalDateTime requestDateTime = LocalDateTime.of(2025, 9, 7, 16, 30);
97100
LocalDateTime verifyDateTime = LocalDateTime.of(2025, 9, 7, 16, 35);
98101

99102
//when
100-
spyMailService.sendCodeTo(testEmail, requestDateTime);
101-
boolean isVerify = spyMailService.verifyEmailCode(testEmail, verifyDateTime);
103+
String code = spyMailService.sendCodeTo(testEmail, requestDateTime).get();
104+
EmailVerifyRequest request = EmailVerifyRequest.of(testEmail, code);
105+
boolean isVerify = spyMailService.verifyEmailCode(request, verifyDateTime);
102106

103107
//then
104108
assertThat(isVerify).isFalse();

0 commit comments

Comments
 (0)