Skip to content

Commit c246557

Browse files
committed
Merge branch 'main' of https://github.com/prgrms-web-devcourse-final-project/WEB5_6_NeogulCoder_BE into refactor/EA3-211-study-currentcount
2 parents 692cc65 + f49bf74 commit c246557

File tree

14 files changed

+430
-89
lines changed

14 files changed

+430
-89
lines changed

src/main/java/grep/neogulcoder/NeogulCoderApplication.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package grep.neogulcoder;
22

3+
import java.util.TimeZone;
4+
35
import jakarta.annotation.PostConstruct;
46
import org.springframework.boot.SpringApplication;
57
import org.springframework.boot.autoconfigure.SpringBootApplication;
8+
import org.springframework.scheduling.annotation.EnableAsync;
69
import org.springframework.retry.annotation.EnableRetry;
710
import org.springframework.scheduling.annotation.EnableScheduling;
811

9-
import java.util.TimeZone;
10-
12+
@EnableAsync
1113
@EnableScheduling
1214
@EnableRetry
1315
@SpringBootApplication
Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,37 @@
11
package grep.neogulcoder.domain.users.controller;
22

3-
import grep.neogulcoder.domain.users.controller.dto.request.PasswordRequest;
4-
import grep.neogulcoder.domain.users.controller.dto.request.SignUpRequest;
5-
import grep.neogulcoder.domain.users.controller.dto.request.UpdatePasswordRequest;
3+
import grep.neogulcoder.domain.users.controller.dto.request.*;
64
import grep.neogulcoder.domain.users.controller.dto.response.AllUserResponse;
75
import grep.neogulcoder.domain.users.controller.dto.response.UserResponse;
8-
import grep.neogulcoder.domain.users.service.EmailVerificationService;
6+
import grep.neogulcoder.domain.users.exception.PasswordNotMatchException;
7+
import grep.neogulcoder.domain.users.exception.code.UserErrorCode;
8+
import grep.neogulcoder.domain.users.service.MailService;
99
import grep.neogulcoder.domain.users.service.UserService;
1010
import grep.neogulcoder.global.auth.Principal;
1111
import grep.neogulcoder.global.response.ApiResponse;
1212
import jakarta.validation.Valid;
13-
import java.io.IOException;
14-
import java.util.List;
1513
import lombok.RequiredArgsConstructor;
1614
import org.springframework.http.HttpStatus;
1715
import org.springframework.http.MediaType;
1816
import org.springframework.http.ResponseEntity;
1917
import org.springframework.security.core.annotation.AuthenticationPrincipal;
20-
import org.springframework.web.bind.annotation.DeleteMapping;
21-
import org.springframework.web.bind.annotation.GetMapping;
22-
import org.springframework.web.bind.annotation.PathVariable;
23-
import org.springframework.web.bind.annotation.PostMapping;
24-
import org.springframework.web.bind.annotation.PutMapping;
25-
import org.springframework.web.bind.annotation.RequestBody;
26-
import org.springframework.web.bind.annotation.RequestMapping;
27-
import org.springframework.web.bind.annotation.RequestParam;
28-
import org.springframework.web.bind.annotation.ResponseStatus;
29-
import org.springframework.web.bind.annotation.RestController;
18+
import org.springframework.web.bind.annotation.*;
3019
import org.springframework.web.multipart.MultipartFile;
3120

21+
import java.io.IOException;
22+
import java.time.LocalDateTime;
23+
import java.util.List;
24+
3225
@RestController
3326
@RequiredArgsConstructor
3427
@RequestMapping("/api/users")
3528
public class UserController implements UserSpecification {
3629

3730
private final UserService usersService;
38-
private final EmailVerificationService verificationService;
31+
private final MailService mailService;
3932

4033
@GetMapping("/me")
41-
public ResponseEntity<ApiResponse<UserResponse>> get(
42-
@AuthenticationPrincipal Principal principal) {
34+
public ResponseEntity<ApiResponse<UserResponse>> get(@AuthenticationPrincipal Principal principal) {
4335
UserResponse userResponse = usersService.getUserResponse(principal.getUserId());
4436
return ResponseEntity.ok(ApiResponse.success(userResponse));
4537
}
@@ -58,52 +50,55 @@ public ResponseEntity<ApiResponse<List<AllUserResponse>>> getAll() {
5850

5951
@PutMapping(value = "/update/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
6052
public ResponseEntity<ApiResponse<Void>> updateProfile(
61-
@AuthenticationPrincipal Principal principal,
62-
@RequestParam(value = "nickname", required = false) String nickname,
63-
@RequestParam(value = "profileImage", required = false) MultipartFile profileImage
53+
@AuthenticationPrincipal Principal principal,
54+
@RequestParam(value = "nickname", required = false) String nickname,
55+
@RequestParam(value = "profileImage", required = false) MultipartFile profileImage
6456
) throws IOException {
6557
usersService.updateProfile(principal.getUserId(), nickname, profileImage);
6658
return ResponseEntity.ok(ApiResponse.noContent());
6759
}
6860

6961
@PutMapping("/update/password")
70-
public ResponseEntity<ApiResponse<Void>> updatePassword(
71-
@AuthenticationPrincipal Principal principal,
72-
@Valid @RequestBody UpdatePasswordRequest request) {
62+
public ResponseEntity<ApiResponse<Void>> updatePassword(@AuthenticationPrincipal Principal principal,
63+
@Valid @RequestBody UpdatePasswordRequest request) {
7364
usersService.updatePassword(principal.getUserId(), request.getPassword(),
74-
request.getNewPassword(), request.getNewPasswordCheck());
65+
request.getNewPassword(), request.getNewPasswordCheck());
7566
return ResponseEntity.ok(ApiResponse.noContent());
7667
}
7768

7869
@DeleteMapping("/delete/me")
7970
public ResponseEntity<ApiResponse<Void>> delete(@AuthenticationPrincipal Principal principal,
80-
@RequestBody @Valid PasswordRequest request) {
71+
@RequestBody @Valid PasswordRequest request) {
8172
usersService.deleteUser(principal.getUserId(), request.getPassword(), request.getPasswordCheck());
8273
return ResponseEntity.ok(ApiResponse.noContent());
8374
}
8475

8576
@PostMapping("/signup")
8677
@ResponseStatus(HttpStatus.CREATED)
8778
public ResponseEntity<ApiResponse<Void>> signUp(@Valid @RequestBody SignUpRequest request) {
79+
if (isNotMatchPassword(request.getPassword(), request.getPasswordCheck())) {
80+
throw new PasswordNotMatchException(UserErrorCode.PASSWORD_MISMATCH);
81+
}
82+
8883
usersService.signUp(request);
8984
return ResponseEntity.ok(ApiResponse.noContent());
9085
}
9186

9287
@PostMapping("/mail/send")
93-
public ResponseEntity<ApiResponse<Void>> sendCode(@RequestParam String email) {
94-
verificationService.sendVerificationEmail(email);
88+
public ResponseEntity<ApiResponse<Void>> sendCode(@Valid @RequestBody SendCodeToEmailRequest request) {
89+
LocalDateTime currentDateTime = LocalDateTime.now();
90+
mailService.sendCodeTo(request.getEmail(), currentDateTime);
9591
return ResponseEntity.ok(ApiResponse.noContent());
9692
}
9793

9894
@PostMapping("/mail/verify")
99-
public ResponseEntity<ApiResponse<Void>> verifyCode(
100-
@RequestParam String email,
101-
@RequestParam String code
102-
) {
103-
boolean result = verificationService.verifyCode(email, code);
104-
return result ?
105-
ResponseEntity.ok(ApiResponse.noContent()) :
106-
ResponseEntity.ok(ApiResponse.badRequest());
95+
public ResponseEntity<ApiResponse<Boolean>> verifyCode(@Valid @RequestBody EmailVerifyRequest request) {
96+
LocalDateTime currentDateTime = LocalDateTime.now();
97+
boolean verified = mailService.verifyEmailCode(request.getEmail(), currentDateTime);
98+
return ResponseEntity.ok(ApiResponse.success(verified));
10799
}
108100

101+
private boolean isNotMatchPassword(String password, String passwordCheck) {
102+
return !password.equals(passwordCheck);
103+
}
109104
}

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

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

3-
import grep.neogulcoder.domain.users.controller.dto.request.PasswordRequest;
4-
import grep.neogulcoder.domain.users.controller.dto.request.SignUpRequest;
5-
import grep.neogulcoder.domain.users.controller.dto.request.UpdatePasswordRequest;
3+
import grep.neogulcoder.domain.users.controller.dto.request.*;
64
import grep.neogulcoder.domain.users.controller.dto.response.AllUserResponse;
75
import grep.neogulcoder.domain.users.controller.dto.response.UserResponse;
86
import grep.neogulcoder.global.auth.Principal;
@@ -48,15 +46,12 @@ ResponseEntity<ApiResponse<Void>> delete(@AuthenticationPrincipal Principal prin
4846
@Operation(summary = "이메일 인증 코드 발송", description = "입력한 이메일 주소로 인증 코드를 발송합니다.")
4947
ResponseEntity<ApiResponse<Void>> sendCode(
5048
@Parameter(description = "인증 코드를 보낼 이메일 주소", required = true, example = "[email protected]")
51-
@RequestParam String email
49+
SendCodeToEmailRequest request
5250
);
5351

5452
@Operation(summary = "이메일 인증 코드 검증", description = "사용자가 입력한 인증 코드가 올바른지 검증합니다.")
55-
ResponseEntity<ApiResponse<Void>> verifyCode(
53+
ResponseEntity<ApiResponse<Boolean>> verifyCode(
5654
@Parameter(description = "인증 요청한 이메일 주소", required = true, example = "[email protected]")
57-
@RequestParam String email,
58-
59-
@Parameter(description = "사용자가 입력한 인증 코드", required = true, example = "123456")
60-
@RequestParam String code
55+
EmailVerifyRequest request
6156
);
6257
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package grep.neogulcoder.domain.users.controller.dto.request;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class EmailVerifyRequest {
9+
10+
@NotBlank(message = "이메일은 필수입니다")
11+
@Email(message = "올바른 이메일 형식이어야 합니다")
12+
private String email;
13+
14+
public EmailVerifyRequest() {
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package grep.neogulcoder.domain.users.controller.dto.request;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class SendCodeToEmailRequest {
9+
10+
@NotBlank(message = "이메일은 필수입니다")
11+
@Email(message = "올바른 이메일 형식이어야 합니다")
12+
private String email;
13+
14+
public SendCodeToEmailRequest() {}
15+
16+
}

src/main/java/grep/neogulcoder/domain/users/entity/User.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class User extends BaseEntity {
3232
@Enumerated(EnumType.STRING)
3333
private Role role;
3434

35-
public static User UserInit(String email, String password, String nickname) {
35+
public static User create(String email, String password, String nickname) {
3636
return User.builder()
3737
.email(email)
3838
.password(password)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package grep.neogulcoder.domain.users.exception;
2+
3+
import grep.neogulcoder.global.exception.business.BusinessException;
4+
import grep.neogulcoder.global.response.code.ErrorCode;
5+
6+
public class MailSendException extends BusinessException {
7+
public MailSendException(ErrorCode errorCode) {
8+
super(errorCode);
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package grep.neogulcoder.domain.users.exception;
2+
3+
import grep.neogulcoder.global.exception.business.BusinessException;
4+
import grep.neogulcoder.global.response.code.ErrorCode;
5+
6+
public class MailTaskRejectedException extends BusinessException {
7+
public MailTaskRejectedException(ErrorCode errorCode) {
8+
super(errorCode);
9+
}
10+
}

src/main/java/grep/neogulcoder/domain/users/exception/code/UserErrorCode.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
@Getter
88
public enum UserErrorCode implements ErrorCode {
99

10-
USER_NOT_FOUND("U001",HttpStatus.NOT_FOUND,"회원을 찾을 수 없습니다."),
10+
USER_NOT_FOUND("U001", HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."),
1111
PASSWORD_MISMATCH("U002", HttpStatus.BAD_REQUEST, "비밀번호를 다시 확인해주세요."),
1212
PASSWORD_UNCHECKED("U003", HttpStatus.BAD_REQUEST, "비밀번호와 비밀번호 확인이 다릅니다"),
13-
IS_DUPLICATED_MALI("U004", HttpStatus.BAD_REQUEST,"이미 존재하는 이메일입니다."),
14-
IS_DUPLICATED_NICKNAME("U005", HttpStatus.BAD_REQUEST,"이미 존재하는 닉네임입니다."),
15-
UNACTIVATED_USER("U006", HttpStatus.BAD_REQUEST,"탈퇴된 회원입니다."),
16-
NOT_VERIFIED_EMAIL("U007", HttpStatus.BAD_REQUEST, "메일 인증이 되지 않은 이메일입니다.");
17-
13+
IS_DUPLICATED_MALI("U004", HttpStatus.BAD_REQUEST, "이미 존재하는 이메일입니다."),
14+
IS_DUPLICATED_NICKNAME("U005", HttpStatus.BAD_REQUEST, "이미 존재하는 닉네임입니다."),
15+
UNACTIVATED_USER("U006", HttpStatus.BAD_REQUEST, "탈퇴된 회원입니다."),
16+
NOT_VERIFIED_EMAIL("U007", HttpStatus.BAD_REQUEST, "이메일 혹은 인증 코드가 잘못 입력 되었습니다."),
17+
MAIL_SEND_EXCEPTION("U008", HttpStatus.BAD_REQUEST, "메일 전송에 실패 하였습니다."),
18+
MAIL_SEND_QUEUE_IS_FULL("U009", HttpStatus.SERVICE_UNAVAILABLE, "메일 전송 작업 큐가 가득 찼습니다.");
1819

1920
private final String code;
2021
private final HttpStatus status;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package grep.neogulcoder.domain.users.service;
2+
3+
import grep.neogulcoder.domain.users.exception.EmailDuplicationException;
4+
import grep.neogulcoder.domain.users.exception.MailSendException;
5+
import grep.neogulcoder.domain.users.exception.NotVerifiedEmailException;
6+
import grep.neogulcoder.domain.users.exception.code.UserErrorCode;
7+
import grep.neogulcoder.domain.users.repository.UserRepository;
8+
import jakarta.mail.MessagingException;
9+
import jakarta.mail.internet.MimeMessage;
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.mail.javamail.JavaMailSender;
13+
import org.springframework.mail.javamail.MimeMessageHelper;
14+
import org.springframework.scheduling.annotation.Async;
15+
import org.springframework.scheduling.annotation.Scheduled;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
19+
import java.time.LocalDateTime;
20+
import java.util.Optional;
21+
import java.util.Random;
22+
import java.util.Set;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
25+
@Slf4j
26+
@Transactional
27+
@Service
28+
@RequiredArgsConstructor
29+
public class MailService {
30+
31+
private final ConcurrentHashMap<String, LocalDateTime> emailExpiredTimeMap = new ConcurrentHashMap<>();
32+
private final Set<String> verifiedEmailSet = ConcurrentHashMap.newKeySet();
33+
private final JavaMailSender mailSender;
34+
private final UserRepository userRepository;
35+
36+
private static final int VERIFY_LIMIT_MINUTE = 5;
37+
38+
@Scheduled(cron = "0 0 0 * * * ")
39+
public void clearStores() {
40+
emailExpiredTimeMap.clear();
41+
verifiedEmailSet.clear();
42+
log.info("회원 인증 코드 저장 Map, Set Clear");
43+
}
44+
45+
@Async("mailExecutor")
46+
public void sendCodeTo(String email, LocalDateTime currentDateTime) {
47+
if (isDuplicateEmail(email)) {
48+
throw new EmailDuplicationException(UserErrorCode.IS_DUPLICATED_MALI);
49+
}
50+
51+
LocalDateTime expiredDateTime = currentDateTime.plusMinutes(VERIFY_LIMIT_MINUTE);
52+
emailExpiredTimeMap.put(email, expiredDateTime);
53+
sendCodeTo(email);
54+
}
55+
56+
public boolean verifyEmailCode(String email, LocalDateTime currentDateTime) {
57+
LocalDateTime expiredTime = Optional.ofNullable(emailExpiredTimeMap.get(email))
58+
.orElseThrow(() -> new NotVerifiedEmailException(UserErrorCode.NOT_VERIFIED_EMAIL));
59+
60+
boolean isVerify = currentDateTime.isBefore(expiredTime);
61+
if (isVerify) {
62+
verifiedEmailSet.add(email);
63+
}
64+
return isVerify;
65+
}
66+
67+
public boolean confirmNotVerifiedEmail(String email) {
68+
return !verifiedEmailSet.contains(email);
69+
}
70+
71+
private boolean isDuplicateEmail(String email) {
72+
return userRepository.findByEmail(email).isPresent();
73+
}
74+
75+
private void sendCodeTo(String email) {
76+
String code = generateRandomIntCode();
77+
MimeMessage mimeMessage = mailSender.createMimeMessage();
78+
79+
try {
80+
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
81+
helper.setTo(email);
82+
helper.setSubject("[wibby] 이메일 인증 코드");
83+
String htmlContent = getContent(code);
84+
helper.setText(htmlContent, true);
85+
86+
mailSender.send(mimeMessage);
87+
} catch (MessagingException e) {
88+
throw new MailSendException(UserErrorCode.MAIL_SEND_EXCEPTION);
89+
}
90+
}
91+
92+
private String generateRandomIntCode() {
93+
return String.format("%06d", new Random().nextInt(1000000));
94+
}
95+
96+
private String getContent(String code) {
97+
return "<div style='font-family:Arial,sans-serif;line-height:1.6;color:#333;'>"
98+
+ " <h2 style='color:#4CAF50;'>[wibby] 이메일 인증</h2>"
99+
+ " <p>이용해 주셔서 감사 합니다!</p>"
100+
+ " <p>아래 인증 코드를 입력해 주세요</p>"
101+
+ " <div style='font-size:24px;font-weight:bold;"
102+
+ " padding:10px 20px;"
103+
+ " background:#f4f4f4;"
104+
+ " border:1px solid #ddd;"
105+
+ " display:inline-block;"
106+
+ " margin:20px 0;'>"
107+
+ code
108+
+ " </div>"
109+
+ " <p style='font-size:12px;color:#888;'>이 코드는 5분간 유효합니다.</p>"
110+
+ "</div>";
111+
}
112+
}

0 commit comments

Comments
 (0)