From 09b1a1fe4fcf41636a4a243ff0f399316ea2ccb8 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 1 Aug 2024 06:33:00 +0900 Subject: [PATCH 01/18] =?UTF-8?q?Chore:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=B3=84=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmc/suppin/global/security/config/WebSecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java b/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java index bb0f6a9..30a97c4 100644 --- a/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java +++ b/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java @@ -102,6 +102,7 @@ public SecurityFilterChain securityFilterChainDefault(HttpSecurity http) throws return http.build(); } + // 인증 및 인가가 필요한 엔드포인트에 적용되는 RequestMatcher private RequestMatcher[] requestHasRoleUser() { List requestMatchers = List.of( antMatcher("/api/v1/members/**"), @@ -110,6 +111,7 @@ private RequestMatcher[] requestHasRoleUser() { return requestMatchers.toArray(RequestMatcher[]::new); } + // permitAll 권한을 가진 엔드포인트에 적용되는 RequestMatcher private RequestMatcher[] requestPermitAll() { List requestMatchers = List.of( antMatcher("/"), @@ -118,7 +120,7 @@ private RequestMatcher[] requestPermitAll() { antMatcher("/v3/api-docs/**"), antMatcher("/api/v1/members/login/**"), antMatcher("/api/v1/members/join"), - antMatcher("/api/v1/survey/reply/**") + antMatcher("/api/v1/survey/reply/**") // 설문조사 응답 시 적용 ); return requestMatchers.toArray(RequestMatcher[]::new); } From 1ef30c500cb03fe305e68088c3076c2ebb850bb5 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 1 Aug 2024 18:54:15 +0900 Subject: [PATCH 02/18] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=82=AC=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../suppin/member/controller/MemberApi.java | 70 ++++++------------- .../controller/dto/MemberRequestDTO.java | 7 ++ .../controller/dto/MemberResponseDTO.java | 10 +++ .../member/converter/MemberConverter.java | 6 ++ .../member/service/command/MemberService.java | 8 ++- 5 files changed, 49 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java index 7fe644b..c722057 100644 --- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java +++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java @@ -38,19 +38,27 @@ public ResponseEntity> join(@Reques } // 아이디 중복 체크 - @PostMapping("/checkUserId") + @GetMapping("/checkUserId") @Operation(summary = "아이디 중복 체크 API", description = "request : userId, response: 중복이면 false, 중복 아니면 true") - public ResponseEntity> checkUserId(@RequestBody MemberRequestDTO.IdConfirmDTO request) { + public ResponseEntity> checkUserId(@RequestParam MemberRequestDTO.IdConfirmDTO request) { boolean checkUserId = memberService.confirmUserId(request); return ResponseEntity.ok(ApiResponse.of(MemberConverter.toIdConfirmResultDTO(checkUserId))); } + // 이메일 중복 체크 + @GetMapping("/checkEmail") + @Operation(summary = "이메일 중복 체크 API", description = "request : email, response: 중복이면 false, 중복 아니면 true") + public ResponseEntity> checkEmail(@RequestParam MemberRequestDTO.EmailConfirmDTO request) { + boolean checkEmail = memberService.confirmEmail(request); + + return ResponseEntity.ok(ApiResponse.of(MemberConverter.toEmailConfirmResultDTO(checkEmail))); + } + // 회원탈퇴 @DeleteMapping("/delete") - @Operation(summary = "회원탈퇴 API", description = "로그인 시 발급받은 토큰으로 인가 필요") - public ResponseEntity> deleteMember( - @CurrentAccount Account account) { + @Operation(summary = "회원탈퇴 API", description = "로그인 시 발급받은 토큰으로 인가 필요, Authentication 헤더에 토큰을 넣어서 요청") + public ResponseEntity> deleteMember(@CurrentAccount Account account) { memberService.deleteMember(account.id()); return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); } @@ -64,54 +72,18 @@ public ResponseEntity> login(@Re return ResponseEntity.ok(ApiResponse.of(response)); } - /** - * TODO: 로그아웃, 비밀번호 변경, 회원정보 상세 조회, 회원정보 수정 API - */ - // // 로그아웃 // @PostMapping("/logout") -// @Operation(summary = "로그아웃 API", description = "JWT 토큰을 헤더에 포함시켜 보내주시면 됩니다.") -// public ApiResponse logout(@AuthenticationPrincipal MemberDetails memberDetails) { -// if (memberDetails == null) { -// return ApiResponse.onFailure("403", "인증된 사용자만 로그아웃할 수 있습니다.", null); -// } -// memberCommandService.logout(memberDetails.getUserId()); -// return ApiResponse.onSuccess(null, SuccessStatus.MEMBER_LOGOUT_SUCCESS); -// } -// -// // 비밀번호 변경 -// @PutMapping("/changePassword") -// @Operation(summary = "비밀번호 변경 API", description = "request : userId, password, newPassword") -// public ApiResponse changePassword(@AuthenticationPrincipal MemberDetails memberDetails, @RequestBody @Valid MemberRequestDTO.ChangePasswordDTO request) { -// if (memberDetails == null) { -// return ApiResponse.onFailure("403", "인증된 사용자만 비밀번호를 변경할 수 있습니다.", null); -// } -// memberCommandService.changePassword(memberDetails.getUserId(), request); -// return ApiResponse.onSuccess(null, SuccessStatus.MEMBER_CHANGE_PASSWORD_SUCCESS); -// } -// -// // 회원정보 상세 조회(마이페이지) -// @GetMapping("/info") -// @Operation(summary = "회원정보 상세 조회 API", description = "JWT 토큰을 헤더에 포함시켜 보내주시면 됩니다.") -// public ApiResponse getMemberInfo(@AuthenticationPrincipal MemberDetails memberDetails) { -// if (memberDetails == null) { -// return ApiResponse.onFailure("403", "인증된 사용자만 조회할 수 있습니다.", null); -// } -// Member member = memberCommandService.getMemberInfo(memberDetails.getUserId()); -// return ApiResponse.onSuccess(MemberConverter.toMemberInfoDTO(member), SuccessStatus.MEMBER_INFO_SUCCESS); -// } -// -// // 회원정보 수정 -// @PutMapping("/info/update") -// @Operation(summary = "회원정보 수정 API", description = "request : userId, name, phone, email") -// public ApiResponse updateMemberInfo(@AuthenticationPrincipal MemberDetails memberDetails, @RequestBody @Valid MemberRequestDTO.UpdateMemberInfoDTO request) { -// if (memberDetails == null) { -// return ApiResponse.onFailure("403", "인증된 사용자만 수정할 수 있습니다.", null); -// } -// memberCommandService.updateMemberInfo(memberDetails.getUserId(), request); -// return ApiResponse.onSuccess(null, SuccessStatus.MEMBER_UPDATE_SUCCESS); +// @Operation(summary = "로그아웃 API", description = "로그인 시 발급받은 토큰으로 인가 필요, Authentication 헤더에 토큰을 넣어서 요청") +// public ResponseEntity> logout(@CurrentAccount Account account) { +// memberService.logout(account.id()); +// return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); // } + + // TODO: 로그아웃, 비밀번호 변경, 회원정보 상세 조회, 회원정보 수정 API + + // TODO: 아이디 찾기, 비밀번호 찾기 API 구현 필요 } diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java index 91b4604..ad7e1bd 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java @@ -40,6 +40,13 @@ public static class IdConfirmDTO { private String userId; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class EmailConfirmDTO { + private String email; + } + @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java index 86bbad1..aa709a2 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java @@ -27,6 +27,15 @@ public static class JoinResultDTO { @AllArgsConstructor public static class IdConfirmResultDTO { Boolean checkUserId; + + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class EmailConfirmResultDTO { + Boolean checkEmail; } @Getter @@ -36,5 +45,6 @@ public static class IdConfirmResultDTO { public static class LoginResponseDTO { private String token; private String userId; + } } diff --git a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java index 14e6820..897c887 100644 --- a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java +++ b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java @@ -37,6 +37,12 @@ public static MemberResponseDTO.IdConfirmResultDTO toIdConfirmResultDTO(boolean .build(); } + public static MemberResponseDTO.EmailConfirmResultDTO toEmailConfirmResultDTO(boolean checkEmail) { + return MemberResponseDTO.EmailConfirmResultDTO.builder() + .checkEmail(checkEmail) + .build(); + } + public static MemberResponseDTO.LoginResponseDTO toLoginResponseDTO(String token, Member member) { return MemberResponseDTO.LoginResponseDTO.builder() .token(token) diff --git a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java index 6740516..72d54bc 100644 --- a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java @@ -65,10 +65,9 @@ public Boolean confirmUserId(MemberRequestDTO.IdConfirmDTO request) { /** * 이메일 중복 확인 */ - public Boolean confirmEmail(MemberRequestDTO.JoinDTO request) { + public Boolean confirmEmail(MemberRequestDTO.EmailConfirmDTO request) { // 이메일 중복 체크 - validateDuplicateEmail(request); - return true; + return !memberRepository.existsByEmail(request.getEmail()); } /** @@ -129,4 +128,7 @@ private void validateDuplicateEmail(MemberRequestDTO.JoinDTO request) { } + public void logout(Long memberId) { + + } } From b4189b7ed1a7a5f8e4329c9923c13951b68caba7 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 21:52:27 +0900 Subject: [PATCH 03/18] =?UTF-8?q?Feat:=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B4=80=EB=A0=A8=20=EA=B3=B5=ED=86=B5=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/cmc/suppin/global/response/ApiResponse.java | 4 ++++ .../java/com/cmc/suppin/global/response/ResponseCode.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cmc/suppin/global/response/ApiResponse.java b/src/main/java/com/cmc/suppin/global/response/ApiResponse.java index 7540613..1893f18 100644 --- a/src/main/java/com/cmc/suppin/global/response/ApiResponse.java +++ b/src/main/java/com/cmc/suppin/global/response/ApiResponse.java @@ -23,6 +23,10 @@ public static ApiResponse of(T data) { return new ApiResponse<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMessage(), data); } + public static ApiResponse confirm(T data) { + return new ApiResponse<>(ResponseCode.CONFIRM.getCode(), ResponseCode.CONFIRM.getMessage(), data); + } + public static ApiResponse of(ResponseCode responseCode, T data) { return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), data); } diff --git a/src/main/java/com/cmc/suppin/global/response/ResponseCode.java b/src/main/java/com/cmc/suppin/global/response/ResponseCode.java index 092e3c7..b8cc642 100644 --- a/src/main/java/com/cmc/suppin/global/response/ResponseCode.java +++ b/src/main/java/com/cmc/suppin/global/response/ResponseCode.java @@ -6,7 +6,8 @@ public enum ResponseCode { SUCCESS("200", "정상 처리되었습니다."), - CREATE("201", "정상적으로 생성되었습니다."); + CREATE("201", "정상적으로 생성되었습니다."), + CONFIRM("202", "검증 및 확인 되었습니다."); private final String code; private final String message; From a90c9355169287b07d2b105bd52ef3ee6b12d2fb Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 21:53:02 +0900 Subject: [PATCH 04/18] =?UTF-8?q?Feat:=20permitAll=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmc/suppin/global/security/config/WebSecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java b/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java index 30a97c4..99cd108 100644 --- a/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java +++ b/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java @@ -106,7 +106,7 @@ public SecurityFilterChain securityFilterChainDefault(HttpSecurity http) throws private RequestMatcher[] requestHasRoleUser() { List requestMatchers = List.of( antMatcher("/api/v1/members/**"), - antMatcher(PATCH, "/api/members") + antMatcher(PATCH, "/api/v1/members") ); return requestMatchers.toArray(RequestMatcher[]::new); } @@ -120,6 +120,8 @@ private RequestMatcher[] requestPermitAll() { antMatcher("/v3/api-docs/**"), antMatcher("/api/v1/members/login/**"), antMatcher("/api/v1/members/join"), + antMatcher("/api/v1/members/checkUserId"), + antMatcher("/api/v1/members/checkEmail"), antMatcher("/api/v1/survey/reply/**") // 설문조사 응답 시 적용 ); return requestMatchers.toArray(RequestMatcher[]::new); From e242e8196d6ca4a7a54d602bf93f59e56c6ee159 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 21:54:10 +0900 Subject: [PATCH 05/18] =?UTF-8?q?Feat:=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B3=80=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../suppin/member/controller/MemberApi.java | 42 +++++++++++------ .../controller/dto/MemberRequestDTO.java | 10 +++++ .../com/cmc/suppin/member/domain/Member.java | 4 ++ .../member/service/command/MemberService.java | 45 ++++++++++++++++--- 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java index c722057..2bacc8e 100644 --- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java +++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java @@ -40,19 +40,19 @@ public ResponseEntity> join(@Reques // 아이디 중복 체크 @GetMapping("/checkUserId") @Operation(summary = "아이디 중복 체크 API", description = "request : userId, response: 중복이면 false, 중복 아니면 true") - public ResponseEntity> checkUserId(@RequestParam MemberRequestDTO.IdConfirmDTO request) { - boolean checkUserId = memberService.confirmUserId(request); + public ResponseEntity> checkUserId(@RequestParam String userId) { + boolean checkUserId = memberService.confirmUserId(userId); - return ResponseEntity.ok(ApiResponse.of(MemberConverter.toIdConfirmResultDTO(checkUserId))); + return ResponseEntity.ok(ApiResponse.confirm(MemberConverter.toIdConfirmResultDTO(checkUserId))); } // 이메일 중복 체크 @GetMapping("/checkEmail") @Operation(summary = "이메일 중복 체크 API", description = "request : email, response: 중복이면 false, 중복 아니면 true") - public ResponseEntity> checkEmail(@RequestParam MemberRequestDTO.EmailConfirmDTO request) { - boolean checkEmail = memberService.confirmEmail(request); + public ResponseEntity> checkEmail(@RequestParam String email) { + boolean checkEmail = memberService.confirmEmail(email); - return ResponseEntity.ok(ApiResponse.of(MemberConverter.toEmailConfirmResultDTO(checkEmail))); + return ResponseEntity.ok(ApiResponse.confirm(MemberConverter.toEmailConfirmResultDTO(checkEmail))); } // 회원탈퇴 @@ -72,13 +72,29 @@ public ResponseEntity> login(@Re return ResponseEntity.ok(ApiResponse.of(response)); } -// // 로그아웃 -// @PostMapping("/logout") -// @Operation(summary = "로그아웃 API", description = "로그인 시 발급받은 토큰으로 인가 필요, Authentication 헤더에 토큰을 넣어서 요청") -// public ResponseEntity> logout(@CurrentAccount Account account) { -// memberService.logout(account.id()); -// return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); -// } + // 로그아웃 + @PostMapping("/logout") + @Operation(summary = "로그아웃 API", description = "로그인 시 발급받은 토큰으로 인가 필요, Authentication 헤더에 토큰을 넣어서 요청") + public ResponseEntity> logout(@CurrentAccount Account account) { + memberService.logout(account.id()); + return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); + } + + // 비밀번호 변경 + @PostMapping("/password/update") + @Operation(summary = "비밀번호 변경 API", description = "request : password, newPassword") + public ResponseEntity> updatePassword(@RequestBody @Valid MemberRequestDTO.PasswordUpdateDTO request, @CurrentAccount Account account) { + memberService.updatePassword(request, account.id()); + return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); + } + + // 현재 비밀번호 확인 + @GetMapping("/password/check") + @Operation(summary = "현재 비밀번호 확인 API", description = "request : password") + public ResponseEntity> checkPassword(@RequestParam String password, @CurrentAccount Account account) { + memberService.checkPassword(password, account.id()); + return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); + } // TODO: 로그아웃, 비밀번호 변경, 회원정보 상세 조회, 회원정보 수정 API diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java index ad7e1bd..3213193 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java @@ -54,4 +54,14 @@ public static class LoginRequestDTO { private String userId; private String password; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class PasswordUpdateDTO { + private String password; + private String newPassword; + } + + } diff --git a/src/main/java/com/cmc/suppin/member/domain/Member.java b/src/main/java/com/cmc/suppin/member/domain/Member.java index 7343681..0f7a5d3 100644 --- a/src/main/java/com/cmc/suppin/member/domain/Member.java +++ b/src/main/java/com/cmc/suppin/member/domain/Member.java @@ -65,5 +65,9 @@ public boolean isDeleted() { public void delete() { this.status = UserStatus.DELETED; } + + public void updatePassword(String encode) { + this.password = encode; + } } diff --git a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java index 72d54bc..4f447fc 100644 --- a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java @@ -14,6 +14,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -57,17 +58,17 @@ public Member join(MemberRequestDTO.JoinDTO request) { /** * ID 중복 확인 */ - public Boolean confirmUserId(MemberRequestDTO.IdConfirmDTO request) { + public Boolean confirmUserId(String userId) { // 아이디 중복 체크 - return !memberRepository.existsByUserId(request.getUserId()); + return !memberRepository.existsByUserId(userId); } /** * 이메일 중복 확인 */ - public Boolean confirmEmail(MemberRequestDTO.EmailConfirmDTO request) { + public Boolean confirmEmail(String email) { // 이메일 중복 체크 - return !memberRepository.existsByEmail(request.getEmail()); + return !memberRepository.existsByEmail(email); } /** @@ -102,7 +103,20 @@ public MemberResponseDTO.LoginResponseDTO login(MemberRequestDTO.LoginRequestDTO .build(); } - // 검증 메서드 + /** + * 로그아웃 + */ + public void logout(Long accountId) { + // 현재 인증된 사용자의 토큰을 무효화하는 로직 + String accessToken = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString(); + if (jwtTokenProvider.validateAccessToken(accessToken)) { + jwtTokenProvider.addTokenToBlacklist(accessToken); // 토큰 블랙리스트에 추가 + } + } + + /** + * 검증 메서드 + */ private boolean isValidPassword(String password) { return password.matches("(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}"); } @@ -127,8 +141,27 @@ private void validateDuplicateEmail(MemberRequestDTO.JoinDTO request) { } } + public void validatePasswordFormat(String password) { + if (!password.matches("(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}")) { + throw new MemberException(MemberErrorCode.PASSWORD_FORMAT_NOT_MATCHED); + } + } - public void logout(Long memberId) { + public void updatePassword(MemberRequestDTO.PasswordUpdateDTO request, Long id) { + validatePasswordFormat(request.getNewPassword()); + Member member = getMember(id); + + if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new MemberException(MemberErrorCode.PASSWORD_CONFIRM_NOT_MATCHED); + } + member.updatePassword(passwordEncoder.encode(request.getNewPassword())); + } + + public void checkPassword(String password, Long id) { + Member member = getMember(id); + if (!passwordEncoder.matches(password, member.getPassword())) { + throw new MemberException(MemberErrorCode.PASSWORD_CONFIRM_NOT_MATCHED); + } } } From b11c6fec97cddf3fd97ec2b13195fd138252b515 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 21:54:38 +0900 Subject: [PATCH 06/18] =?UTF-8?q?Feat:=20password=20=EA=B4=80=EB=A0=A8=20E?= =?UTF-8?q?rrorCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cmc/suppin/global/exception/MemberErrorCode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java b/src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java index 6dc4bb6..a525134 100644 --- a/src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java +++ b/src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java @@ -9,10 +9,12 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND("mem-404/01", HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."), VALIDATION_FAILED("mem-400/01", HttpStatus.BAD_REQUEST, "입력값에 대한 검증에 실패했습니다."), MEMBER_ALREADY_DELETED("mem-400/02", HttpStatus.BAD_REQUEST, "탈퇴한 회원입니다."), - PASSWORD_CONFIRM_NOT_MATCHED("mem-400/03", HttpStatus.BAD_REQUEST, "비밀번호가 확인이 일치하지 않습니다."), + PASSWORD_CONFIRM_NOT_MATCHED("mem-400/03", HttpStatus.BAD_REQUEST, "비밀번호가 잘못 입력되었습니다."), + PASSWORD_FORMAT_NOT_MATCHED("mem-400/04", HttpStatus.BAD_REQUEST, "비밀번호는 8자 이상 20자 이하로 입력해주세요."), DUPLICATE_MEMBER_EMAIL("mem-409/01", HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."), DUPLICATE_NICKNAME("mem-409/01", HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."); + private final String code; private final HttpStatus status; private final String message; From 358a21f8828164f97f4c9c8b8f0b98839c74cf21 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 21:55:20 +0900 Subject: [PATCH 07/18] =?UTF-8?q?Feat:=20=ED=86=A0=ED=81=B0=20=EB=B8=94?= =?UTF-8?q?=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=9A=A9=20HashSet=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 --- .../global/security/jwt/JwtTokenProvider.java | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/cmc/suppin/global/security/jwt/JwtTokenProvider.java b/src/main/java/com/cmc/suppin/global/security/jwt/JwtTokenProvider.java index b3c703d..7adb40f 100644 --- a/src/main/java/com/cmc/suppin/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/cmc/suppin/global/security/jwt/JwtTokenProvider.java @@ -3,6 +3,7 @@ import com.cmc.suppin.global.security.user.UserDetailsImpl; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; @@ -17,10 +18,7 @@ import javax.crypto.SecretKey; import java.time.Instant; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import static org.springframework.util.StringUtils.hasText; @@ -33,17 +31,15 @@ public class JwtTokenProvider { private static final String AUTHENTICATION_CLAIM_NAME = "roles"; private static final String AUTHENTICATION_SCHEME = "Bearer "; + // 토큰 블랙리스트 관리용 Set + private final Set tokenBlacklist = new HashSet<>(); + @Value("${JWT_SECRET_KEY}") private String secretKey; @Value("${ACCESS_EXPIRY_SECONDS}") private int accessExpirySeconds; -// @Value("${jwt.refresh-expiry-seconds}") -// private int refreshExpirySeconds; - -// private final RedisKeyRepository redisKeyRepository; - public String createAccessToken(UserDetailsImpl userDetails) { Instant now = Instant.now(); Instant expirationTime = now.plusSeconds(accessExpirySeconds); @@ -64,17 +60,6 @@ public String createAccessToken(UserDetailsImpl userDetails) { .compact(); } -// public String createRefreshToken() { -// Instant now = Instant.now(); -// Instant expirationTime = now.plusSeconds(refreshExpirySeconds); -// -// return Jwts.builder() -// .issuedAt(Date.from(now)) -// .expiration(Date.from(expirationTime)) -// .signWith(extractSecretKey()) -// .compact(); -// } - /** * 권한 체크 */ @@ -121,12 +106,31 @@ private Claims verifyAndExtractClaims(String accessToken) { .getPayload(); } + /* public void validateAccessToken(String accessToken) { Jwts.parser() .verifyWith(extractSecretKey()) .build() .parse(accessToken); } + */ + + public boolean validateAccessToken(String accessToken) { + if (isTokenBlacklisted(accessToken)) { + return false; + } + + try { + Jwts.parser() + .verifyWith(extractSecretKey()) + .build() + .parse(accessToken); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.error("Invalid JWT token: {}", e.getMessage()); + return false; + } + } /** * SecretKey 추출 @@ -134,6 +138,17 @@ public void validateAccessToken(String accessToken) { private SecretKey extractSecretKey() { return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); } + + /** + * 토큰 블랙리스트 관리 + */ + public void addTokenToBlacklist(String token) { + tokenBlacklist.add(token); + } + + public boolean isTokenBlacklisted(String token) { + return tokenBlacklist.contains(token); + } } From 0f2dce3a29e9db425d02d3db313a9782dce73f38 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 22:13:43 +0900 Subject: [PATCH 08/18] =?UTF-8?q?Refactor:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=A6=9D=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cmc/suppin/global/response/ApiResponse.java | 12 ++++++++++-- .../com/cmc/suppin/member/controller/MemberApi.java | 4 ++-- .../member/controller/dto/MemberResponseDTO.java | 8 ++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cmc/suppin/global/response/ApiResponse.java b/src/main/java/com/cmc/suppin/global/response/ApiResponse.java index 1893f18..4194662 100644 --- a/src/main/java/com/cmc/suppin/global/response/ApiResponse.java +++ b/src/main/java/com/cmc/suppin/global/response/ApiResponse.java @@ -23,15 +23,23 @@ public static ApiResponse of(T data) { return new ApiResponse<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMessage(), data); } + public static ApiResponse of(ResponseCode responseCode, T data) { + return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), data); + } + + public static ApiResponse of(ResponseCode responseCode) { + return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), null); + } + public static ApiResponse confirm(T data) { return new ApiResponse<>(ResponseCode.CONFIRM.getCode(), ResponseCode.CONFIRM.getMessage(), data); } - public static ApiResponse of(ResponseCode responseCode, T data) { + public static ApiResponse confirm(ResponseCode responseCode, T data) { return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), data); } - public static ApiResponse of(ResponseCode responseCode) { + public static ApiResponse confirm(ResponseCode responseCode) { return new ApiResponse<>(responseCode.getCode(), responseCode.getMessage(), null); } } diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java index 2bacc8e..476c1af 100644 --- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java +++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java @@ -91,9 +91,9 @@ public ResponseEntity> updatePassword(@RequestBody @Valid Memb // 현재 비밀번호 확인 @GetMapping("/password/check") @Operation(summary = "현재 비밀번호 확인 API", description = "request : password") - public ResponseEntity> checkPassword(@RequestParam String password, @CurrentAccount Account account) { + public ResponseEntity> checkPassword(@RequestParam String password, @CurrentAccount Account account) { memberService.checkPassword(password, account.id()); - return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); + return ResponseEntity.ok(ApiResponse.confirm(ResponseCode.CONFIRM)); } diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java index aa709a2..4774b74 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java @@ -47,4 +47,12 @@ public static class LoginResponseDTO { private String userId; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CheckPasswordDTO { + private Boolean checkPassword; + } } From e5cc9a3bff9ea4c4a9574829ce30e20d0cfe3843 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 23:05:32 +0900 Subject: [PATCH 09/18] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../suppin/member/controller/MemberApi.java | 7 ++++ .../controller/dto/MemberResponseDTO.java | 13 +++++++ .../member/converter/MemberConverter.java | 10 +++++ .../member/service/command/MemberService.java | 38 +++++++++++-------- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java index 476c1af..e7f7530 100644 --- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java +++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java @@ -96,6 +96,13 @@ public ResponseEntity> checkPass return ResponseEntity.ok(ApiResponse.confirm(ResponseCode.CONFIRM)); } + // 회원정보 상세 조회 + @GetMapping("/me") + @Operation(summary = "회원정보 상세 조회 API", description = "로그인 시 발급받은 토큰으로 인가 필요, Authentication 헤더에 토큰을 넣어서 요청") + public ResponseEntity> getUserDetail(@CurrentAccount Account account) { + MemberResponseDTO.MemberDetailsDTO memberDetails = memberService.getMemberDetails(account.id()); + return ResponseEntity.ok(ApiResponse.of(memberDetails)); + } // TODO: 로그아웃, 비밀번호 변경, 회원정보 상세 조회, 회원정보 수정 API diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java index 4774b74..ac427eb 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberResponseDTO.java @@ -18,6 +18,7 @@ public static class JoinResultDTO { String userId; String name; String email; + String phoneNumber; LocalDateTime createdAt; } @@ -55,4 +56,16 @@ public static class LoginResponseDTO { public static class CheckPasswordDTO { private Boolean checkPassword; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class MemberDetailsDTO { + private String userId; + private String name; + private String email; + private String phoneNumber; + private LocalDateTime createdAt; + } } diff --git a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java index 897c887..21727da 100644 --- a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java +++ b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java @@ -49,4 +49,14 @@ public static MemberResponseDTO.LoginResponseDTO toLoginResponseDTO(String token .userId(member.getUserId()) .build(); } + + public static MemberResponseDTO.MemberDetailsDTO toMemberDetailsDTO(Member member) { + return MemberResponseDTO.MemberDetailsDTO.builder() + .userId(member.getUserId()) + .name(member.getName()) + .email(member.getEmail()) + .phoneNumber(member.getPhoneNumber()) + .createdAt(member.getCreatedAt()) + .build(); + } } diff --git a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java index 4f447fc..f8d8dc5 100644 --- a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java @@ -114,6 +114,28 @@ public void logout(Long accountId) { } } + private Member getMember(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + public MemberResponseDTO.MemberDetailsDTO getMemberDetails(Long id) { + Member member = memberRepository.findById(id) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return MemberConverter.toMemberDetailsDTO(member); + } + + public void updatePassword(MemberRequestDTO.PasswordUpdateDTO request, Long id) { + validatePasswordFormat(request.getNewPassword()); + Member member = getMember(id); + + if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new MemberException(MemberErrorCode.PASSWORD_CONFIRM_NOT_MATCHED); + } + + member.updatePassword(passwordEncoder.encode(request.getNewPassword())); + } + /** * 검증 메서드 */ @@ -130,11 +152,6 @@ private void validateMember(String userId) { } } - private Member getMember(Long memberId) { - return memberRepository.findById(memberId) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - } - private void validateDuplicateEmail(MemberRequestDTO.JoinDTO request) { if (Boolean.TRUE.equals(memberRepository.existsByEmail(request.getEmail()))) { throw new MemberException(MemberErrorCode.DUPLICATE_MEMBER_EMAIL); @@ -147,17 +164,6 @@ public void validatePasswordFormat(String password) { } } - public void updatePassword(MemberRequestDTO.PasswordUpdateDTO request, Long id) { - validatePasswordFormat(request.getNewPassword()); - Member member = getMember(id); - - if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { - throw new MemberException(MemberErrorCode.PASSWORD_CONFIRM_NOT_MATCHED); - } - - member.updatePassword(passwordEncoder.encode(request.getNewPassword())); - } - public void checkPassword(String password, Long id) { Member member = getMember(id); if (!passwordEncoder.matches(password, member.getPassword())) { From 700a322c7bce29e94621389e024eda69341400b1 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sat, 3 Aug 2024 23:21:03 +0900 Subject: [PATCH 10/18] =?UTF-8?q?Refactor:=20Member=20status=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=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 --- .../security/service/MemberDetailsService.java | 2 +- .../cmc/suppin/member/controller/MemberApi.java | 2 -- .../member/domain/repository/MemberRepository.java | 8 +++++--- .../member/service/command/MemberService.java | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java b/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java index 378d38b..4ab42d2 100644 --- a/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java +++ b/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java @@ -26,7 +26,7 @@ public class MemberDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { - Member member = memberRepository.findByUserId(userId) + Member member = memberRepository.findByUserIdAndStatusNot(userId, "DELETED") .orElseThrow(() -> new SecurityException(MEMBER_NOT_FOUND)); if (member.isDeleted()) { diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java index e7f7530..4acceb6 100644 --- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java +++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java @@ -104,8 +104,6 @@ public ResponseEntity> getUserDe return ResponseEntity.ok(ApiResponse.of(memberDetails)); } - // TODO: 로그아웃, 비밀번호 변경, 회원정보 상세 조회, 회원정보 수정 API - // TODO: 아이디 찾기, 비밀번호 찾기 API 구현 필요 diff --git a/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java b/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java index 985a7cd..7334e61 100644 --- a/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java @@ -7,11 +7,13 @@ public interface MemberRepository extends JpaRepository { - Boolean existsByUserId(String userId); + Boolean existsByUserIdAndStatusNot(String userId, String status); - Boolean existsByEmail(String email); + Boolean existsByEmailAndStatusNot(String email, String status); - Optional findByUserId(String userId); + Optional findByUserIdAndStatusNot(String userId, String status); + + Optional findByIdAndStatusNot(Long id, String status); void deleteByUserId(String userId); } diff --git a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java index f8d8dc5..9c7cecf 100644 --- a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/command/MemberService.java @@ -36,7 +36,7 @@ public class MemberService { */ public Member join(MemberRequestDTO.JoinDTO request) { // 중복된 아이디 체크 - if (memberRepository.existsByUserId(request.getUserId())) { + if (memberRepository.existsByUserIdAndStatusNot(request.getUserId(), "DELETED")) { throw new IllegalArgumentException("이미 존재하는 유저입니다."); } @@ -60,7 +60,7 @@ public Member join(MemberRequestDTO.JoinDTO request) { */ public Boolean confirmUserId(String userId) { // 아이디 중복 체크 - return !memberRepository.existsByUserId(userId); + return !memberRepository.existsByUserIdAndStatusNot(userId, "DELETED"); } /** @@ -68,7 +68,7 @@ public Boolean confirmUserId(String userId) { */ public Boolean confirmEmail(String email) { // 이메일 중복 체크 - return !memberRepository.existsByEmail(email); + return !memberRepository.existsByEmailAndStatusNot(email, "DELETED"); } /** @@ -115,12 +115,12 @@ public void logout(Long accountId) { } private Member getMember(Long memberId) { - return memberRepository.findById(memberId) + return memberRepository.findByIdAndStatusNot(memberId, "DELETED") .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); } public MemberResponseDTO.MemberDetailsDTO getMemberDetails(Long id) { - Member member = memberRepository.findById(id) + Member member = memberRepository.findByIdAndStatusNot(id, "DELETED") .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); return MemberConverter.toMemberDetailsDTO(member); } @@ -144,7 +144,7 @@ private boolean isValidPassword(String password) { } private void validateMember(String userId) { - Member member = memberRepository.findByUserId(userId) + Member member = memberRepository.findByUserIdAndStatusNot(userId, "DELETED") .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); if (member.isDeleted()) { @@ -153,7 +153,7 @@ private void validateMember(String userId) { } private void validateDuplicateEmail(MemberRequestDTO.JoinDTO request) { - if (Boolean.TRUE.equals(memberRepository.existsByEmail(request.getEmail()))) { + if (Boolean.TRUE.equals(memberRepository.existsByEmailAndStatusNot(request.getEmail(), "DELETED"))) { throw new MemberException(MemberErrorCode.DUPLICATE_MEMBER_EMAIL); } } From 6724244bd9164ee89193e1ba48a3801fb01649cf Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 01:43:52 +0900 Subject: [PATCH 11/18] =?UTF-8?q?Chore:=20Javamail=20dependency=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 --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 8e3b645..b94a84b 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.2' + implementation 'org.springframework.boot:spring-boot-starter-mail' + compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' From 1d8fddf4d14aaa94464b0299e83fc7ce641e93aa Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 01:44:40 +0900 Subject: [PATCH 12/18] =?UTF-8?q?Chore:=20Javamail=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?yml=20=ED=8C=8C=EC=9D=BC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 881f5bf..1a2c453 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,6 +15,24 @@ spring: show_sql: true format_sql: true + mail: + host: smtp.gmail.com + port: 587 + username: ${SPRING_MAIL_USERNAME} + password: ${SPRING_MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + ssl: + trust: smtp.gmail.com + protocol: smtp + default-encoding: UTF-8 + test-connection: false + jwt: token: secret: ${JWT_TOKEN_SECRET} From d531dfd78d5c502e8ef667ce1fe193522b433c7c Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 01:45:28 +0900 Subject: [PATCH 13/18] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=A6=9D=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmc/suppin/global/config/MailConfig.java | 35 ++++++++++ .../suppin/member/controller/MemberApi.java | 19 +++++- .../controller/dto/MemberRequestDTO.java | 22 +++++++ .../member/domain/EmailVerificationToken.java | 35 ++++++++++ .../EmailVerificationTokenRepository.java | 12 ++++ .../service/{command => }/MemberService.java | 65 ++++++++++++++++--- 6 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/cmc/suppin/global/config/MailConfig.java create mode 100644 src/main/java/com/cmc/suppin/member/domain/EmailVerificationToken.java create mode 100644 src/main/java/com/cmc/suppin/member/domain/repository/EmailVerificationTokenRepository.java rename src/main/java/com/cmc/suppin/member/service/{command => }/MemberService.java (73%) diff --git a/src/main/java/com/cmc/suppin/global/config/MailConfig.java b/src/main/java/com/cmc/suppin/global/config/MailConfig.java new file mode 100644 index 0000000..e5e79c1 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/config/MailConfig.java @@ -0,0 +1,35 @@ +package com.cmc.suppin.global.config; + +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MailConfig { + private final JavaMailSender javaMailSender; + + public boolean sendMail(String toEmail, String code) { + try { + MimeMessage message = javaMailSender.createMimeMessage(); + message.setFrom(new InternetAddress("suppindev@gmail.com", "Suppin", "UTF-8")); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setSubject("Suppin 인증번호"); // 제목 + helper.setTo(toEmail); // 받는사람 + + String emailBody = String.format("Suppin 회원가입 시 이메일 인증번호는 %s입니다.", code); + helper.setText(emailBody, true); + + javaMailSender.send(message); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } +} diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java index 4acceb6..b9f4212 100644 --- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java +++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java @@ -8,7 +8,7 @@ import com.cmc.suppin.member.controller.dto.MemberResponseDTO; import com.cmc.suppin.member.converter.MemberConverter; import com.cmc.suppin.member.domain.Member; -import com.cmc.suppin.member.service.command.MemberService; +import com.cmc.suppin.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -37,6 +37,22 @@ public ResponseEntity> join(@Reques return ResponseEntity.ok(ApiResponse.of(MemberConverter.toJoinResultDTO(member))); } + // 이메일 인증번호 요청(회원가입 시) + @PostMapping("/join/email/auth") + @Operation(summary = "이메일 인증번호 요청(회원가입 시) API", description = "request : email(이메일을 입력하면 해당 이메일로 인증번호 전송), response: 인증번호 전송 성공 시 true, 실패 시 false") + public ResponseEntity> requestEmailAuth(@RequestBody @Valid MemberRequestDTO.EmailRequestDTO request) { + memberService.requestEmailVerification(request.getEmail()); + return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); + } + + // 이메일 인증번호 확인(회원가입 시) + @PostMapping("/join/email/verification") + @Operation(summary = "이메일 인증번호 확인 API", description = "request : email, verificationCode, response: 인증번호 일치 시 true, 불일치 시 false") + public ResponseEntity> verifyEmailCode(@RequestBody @Valid MemberRequestDTO.EmailVerificationDTO request) { + memberService.verifyEmailCode(request.getEmail(), request.getVerificationCode()); + return ResponseEntity.ok(ApiResponse.confirm(ResponseCode.CONFIRM)); + } + // 아이디 중복 체크 @GetMapping("/checkUserId") @Operation(summary = "아이디 중복 체크 API", description = "request : userId, response: 중복이면 false, 중복 아니면 true") @@ -55,6 +71,7 @@ public ResponseEntity> chec return ResponseEntity.ok(ApiResponse.confirm(MemberConverter.toEmailConfirmResultDTO(checkEmail))); } + // 회원탈퇴 @DeleteMapping("/delete") @Operation(summary = "회원탈퇴 API", description = "로그인 시 발급받은 토큰으로 인가 필요, Authentication 헤더에 토큰을 넣어서 요청") diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java index 3213193..871ddd2 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java @@ -31,6 +31,9 @@ public static class JoinDTO { @NotBlank(message = "휴대폰 번호를 입력해주세요") private String phone; + + @NotBlank(message = "이메일 인증번호를 입력해주세요") + private String verificationCode; } @Getter @@ -63,5 +66,24 @@ public static class PasswordUpdateDTO { private String newPassword; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class EmailRequestDTO { + @NotBlank(message = "이메일을 입력해주세요") + @Email(message = "이메일 형식이 올바르지 않습니다.") + private String email; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class EmailVerificationDTO { + @NotBlank(message = "이메일을 입력해주세요") + @Email(message = "이메일 형식이 올바르지 않습니다.") + private String email; + @NotBlank(message = "인증번호를 입력해주세요") + private String verificationCode; + } } diff --git a/src/main/java/com/cmc/suppin/member/domain/EmailVerificationToken.java b/src/main/java/com/cmc/suppin/member/domain/EmailVerificationToken.java new file mode 100644 index 0000000..ee52117 --- /dev/null +++ b/src/main/java/com/cmc/suppin/member/domain/EmailVerificationToken.java @@ -0,0 +1,35 @@ +package com.cmc.suppin.member.domain; + +import com.cmc.suppin.global.domain.BaseDateTimeEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "email_verification_tokens") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EmailVerificationToken extends BaseDateTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Email + private String email; + + private String token; + + private LocalDateTime expiryDate; + + public boolean isExpired() { + return expiryDate.isBefore(LocalDateTime.now()); + } +} diff --git a/src/main/java/com/cmc/suppin/member/domain/repository/EmailVerificationTokenRepository.java b/src/main/java/com/cmc/suppin/member/domain/repository/EmailVerificationTokenRepository.java new file mode 100644 index 0000000..dab3334 --- /dev/null +++ b/src/main/java/com/cmc/suppin/member/domain/repository/EmailVerificationTokenRepository.java @@ -0,0 +1,12 @@ +package com.cmc.suppin.member.domain.repository; + +import com.cmc.suppin.member.domain.EmailVerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EmailVerificationTokenRepository extends JpaRepository { + Optional findByEmailAndToken(String email, String token); + + void deleteByEmail(String email); +} diff --git a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java b/src/main/java/com/cmc/suppin/member/service/MemberService.java similarity index 73% rename from src/main/java/com/cmc/suppin/member/service/command/MemberService.java rename to src/main/java/com/cmc/suppin/member/service/MemberService.java index 9c7cecf..c0c5cb5 100644 --- a/src/main/java/com/cmc/suppin/member/service/command/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/MemberService.java @@ -1,12 +1,16 @@ -package com.cmc.suppin.member.service.command; +package com.cmc.suppin.member.service; +import com.cmc.suppin.global.config.MailConfig; +import com.cmc.suppin.global.enums.UserStatus; import com.cmc.suppin.global.exception.MemberErrorCode; import com.cmc.suppin.global.security.jwt.JwtTokenProvider; import com.cmc.suppin.global.security.user.UserDetailsImpl; import com.cmc.suppin.member.controller.dto.MemberRequestDTO; import com.cmc.suppin.member.controller.dto.MemberResponseDTO; import com.cmc.suppin.member.converter.MemberConverter; +import com.cmc.suppin.member.domain.EmailVerificationToken; import com.cmc.suppin.member.domain.Member; +import com.cmc.suppin.member.domain.repository.EmailVerificationTokenRepository; import com.cmc.suppin.member.domain.repository.MemberRepository; import com.cmc.suppin.member.exception.MemberException; import lombok.RequiredArgsConstructor; @@ -19,6 +23,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + @Service @Slf4j @RequiredArgsConstructor @@ -30,13 +38,30 @@ public class MemberService { private final JwtTokenProvider jwtTokenProvider; private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + private final MailConfig mailConfig; + + public boolean requestEmailVerification(String email) { + String code = generateVerificationCode(); + saveVerificationToken(email, code); + return mailConfig.sendMail(email, code); + } + + public boolean verifyEmailCode(String email, String code) { + return verifyToken(email, code); + } /** * 회원가입 */ public Member join(MemberRequestDTO.JoinDTO request) { + // 이메일 인증 체크 + if (!verifyEmailCode(request.getEmail(), request.getVerificationCode())) { + throw new IllegalArgumentException("이메일 인증이 필요합니다."); + } + // 중복된 아이디 체크 - if (memberRepository.existsByUserIdAndStatusNot(request.getUserId(), "DELETED")) { + if (memberRepository.existsByUserIdAndStatusNot(request.getUserId(), UserStatus.DELETED)) { throw new IllegalArgumentException("이미 존재하는 유저입니다."); } @@ -60,7 +85,7 @@ public Member join(MemberRequestDTO.JoinDTO request) { */ public Boolean confirmUserId(String userId) { // 아이디 중복 체크 - return !memberRepository.existsByUserIdAndStatusNot(userId, "DELETED"); + return !memberRepository.existsByUserIdAndStatusNot(userId, UserStatus.DELETED); } /** @@ -68,7 +93,7 @@ public Boolean confirmUserId(String userId) { */ public Boolean confirmEmail(String email) { // 이메일 중복 체크 - return !memberRepository.existsByEmailAndStatusNot(email, "DELETED"); + return !memberRepository.existsByEmailAndStatusNot(email, UserStatus.DELETED); } /** @@ -115,12 +140,12 @@ public void logout(Long accountId) { } private Member getMember(Long memberId) { - return memberRepository.findByIdAndStatusNot(memberId, "DELETED") + return memberRepository.findByIdAndStatusNot(memberId, UserStatus.DELETED) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); } public MemberResponseDTO.MemberDetailsDTO getMemberDetails(Long id) { - Member member = memberRepository.findByIdAndStatusNot(id, "DELETED") + Member member = memberRepository.findByIdAndStatusNot(id, UserStatus.DELETED) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); return MemberConverter.toMemberDetailsDTO(member); } @@ -136,6 +161,30 @@ public void updatePassword(MemberRequestDTO.PasswordUpdateDTO request, Long id) member.updatePassword(passwordEncoder.encode(request.getNewPassword())); } + private String generateVerificationCode() { + return UUID.randomUUID().toString().substring(0, 6); + } + + private void saveVerificationToken(String email, String code) { + EmailVerificationToken verificationToken = EmailVerificationToken.builder() + .email(email) + .token(code) + .expiryDate(LocalDateTime.now().plusHours(1)) + .build(); + + emailVerificationTokenRepository.deleteByEmail(email); + emailVerificationTokenRepository.save(verificationToken); + } + + private boolean verifyToken(String email, String token) { + Optional verificationTokenOpt = emailVerificationTokenRepository.findByEmailAndToken(email, token); + if (verificationTokenOpt.isPresent()) { + EmailVerificationToken verificationToken = verificationTokenOpt.get(); + return !verificationToken.isExpired(); + } + return false; + } + /** * 검증 메서드 */ @@ -144,7 +193,7 @@ private boolean isValidPassword(String password) { } private void validateMember(String userId) { - Member member = memberRepository.findByUserIdAndStatusNot(userId, "DELETED") + Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); if (member.isDeleted()) { @@ -153,7 +202,7 @@ private void validateMember(String userId) { } private void validateDuplicateEmail(MemberRequestDTO.JoinDTO request) { - if (Boolean.TRUE.equals(memberRepository.existsByEmailAndStatusNot(request.getEmail(), "DELETED"))) { + if (Boolean.TRUE.equals(memberRepository.existsByEmailAndStatusNot(request.getEmail(), UserStatus.DELETED))) { throw new MemberException(MemberErrorCode.DUPLICATE_MEMBER_EMAIL); } } From c4ccc466b6302bfba179d5f8154cc99f44e1a9ed Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 01:46:25 +0900 Subject: [PATCH 14/18] =?UTF-8?q?Fix:=20findByUserIdAndStatusNot=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/service/MemberDetailsService.java | 3 ++- .../member/domain/repository/MemberRepository.java | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java b/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java index 4ab42d2..61d2708 100644 --- a/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java +++ b/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java @@ -1,5 +1,6 @@ package com.cmc.suppin.global.security.service; +import com.cmc.suppin.global.enums.UserStatus; import com.cmc.suppin.global.security.exception.SecurityException; import com.cmc.suppin.global.security.user.UserDetailsImpl; import com.cmc.suppin.member.domain.Member; @@ -26,7 +27,7 @@ public class MemberDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { - Member member = memberRepository.findByUserIdAndStatusNot(userId, "DELETED") + Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED) .orElseThrow(() -> new SecurityException(MEMBER_NOT_FOUND)); if (member.isDeleted()) { diff --git a/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java b/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java index 7334e61..8545dfd 100644 --- a/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java @@ -1,5 +1,6 @@ package com.cmc.suppin.member.domain.repository; +import com.cmc.suppin.global.enums.UserStatus; import com.cmc.suppin.member.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,13 +8,13 @@ public interface MemberRepository extends JpaRepository { - Boolean existsByUserIdAndStatusNot(String userId, String status); + Boolean existsByUserIdAndStatusNot(String userId, UserStatus status); - Boolean existsByEmailAndStatusNot(String email, String status); + Boolean existsByEmailAndStatusNot(String email, UserStatus status); - Optional findByUserIdAndStatusNot(String userId, String status); + Optional findByUserIdAndStatusNot(String userId, UserStatus status); - Optional findByIdAndStatusNot(Long id, String status); + Optional findByIdAndStatusNot(Long id, UserStatus status); void deleteByUserId(String userId); } From 242d6f82f172ff9effa973de5cd3ed35549bc4e8 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 01:48:56 +0900 Subject: [PATCH 15/18] =?UTF-8?q?Fix:=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=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 --- .../cmc/suppin/global/security/config/WebSecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java b/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java index 99cd108..fcee56a 100644 --- a/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java +++ b/src/main/java/com/cmc/suppin/global/security/config/WebSecurityConfig.java @@ -119,7 +119,7 @@ private RequestMatcher[] requestPermitAll() { antMatcher("/actuator/**"), antMatcher("/v3/api-docs/**"), antMatcher("/api/v1/members/login/**"), - antMatcher("/api/v1/members/join"), + antMatcher("/api/v1/members/join/**"), antMatcher("/api/v1/members/checkUserId"), antMatcher("/api/v1/members/checkEmail"), antMatcher("/api/v1/survey/reply/**") // 설문조사 응답 시 적용 From 9f6d07ffb6e8c5bbb239294fc52ba2d4d463ddd2 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 01:50:21 +0900 Subject: [PATCH 16/18] =?UTF-8?q?Fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cmc/suppin/member/service/MemberService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cmc/suppin/member/service/MemberService.java b/src/main/java/com/cmc/suppin/member/service/MemberService.java index c0c5cb5..44c0629 100644 --- a/src/main/java/com/cmc/suppin/member/service/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/MemberService.java @@ -23,9 +23,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.Optional; -import java.util.UUID; @Service @Slf4j @@ -162,7 +162,9 @@ public void updatePassword(MemberRequestDTO.PasswordUpdateDTO request, Long id) } private String generateVerificationCode() { - return UUID.randomUUID().toString().substring(0, 6); + SecureRandom random = new SecureRandom(); + int code = random.nextInt(900000) + 100000; // 6자리 숫자 생성 (100000 ~ 999999) + return String.valueOf(code); } private void saveVerificationToken(String email, String code) { From dba8774f726811383c30ef350c3b1516622a62e3 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Sun, 4 Aug 2024 02:13:37 +0900 Subject: [PATCH 17/18] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmc/suppin/global/config/MailConfig.java | 51 ++++++++++++++++-- .../resources/static/images/suppin-logo.png | Bin 0 -> 7873 bytes 2 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/static/images/suppin-logo.png diff --git a/src/main/java/com/cmc/suppin/global/config/MailConfig.java b/src/main/java/com/cmc/suppin/global/config/MailConfig.java index e5e79c1..32c8dc2 100644 --- a/src/main/java/com/cmc/suppin/global/config/MailConfig.java +++ b/src/main/java/com/cmc/suppin/global/config/MailConfig.java @@ -3,10 +3,13 @@ import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ClassPathResource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; + @Component @RequiredArgsConstructor public class MailConfig { @@ -15,21 +18,59 @@ public class MailConfig { public boolean sendMail(String toEmail, String code) { try { MimeMessage message = javaMailSender.createMimeMessage(); - message.setFrom(new InternetAddress("suppindev@gmail.com", "Suppin", "UTF-8")); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - helper.setSubject("Suppin 인증번호"); // 제목 - helper.setTo(toEmail); // 받는사람 + helper.setFrom(new InternetAddress("suppindev@gmail.com", "Suppin", "UTF-8")); + helper.setTo(toEmail); + helper.setSubject("Suppin 인증번호"); + + String emailBody = String.format( + "" + + "" + + "" + + "" + + "" + + "Suppin Email Verification" + + "" + + "" + + "
" + + "
" + + "\"Suppin" + + "

[Suppin] 인증번호를 안내해 드립니다.

" + + "
" + + "
" + + "

안녕하세요, Suppin을 이용해주셔서 감사합니다 :)

" + + "

Suppin 회원가입을 위해 인증번호를 안내해 드립니다. 아래 인증번호를 입력하여 이메일 인증을 완료해 주세요.

" + + String.format("
%s
", code) + + "" + + "" + + "" + + "" + + "" + + "
인증 번호" + code + "
요청 일시" + LocalDateTime.now().toString() + "
" + + "
" + + "
" + + "

감사합니다.

" + + "

※ 본 메일은 Suppin 서비스 이용에 관한 안내 메일입니다.

" + + "
" + + "
" + + "" + + "" + ); - String emailBody = String.format("Suppin 회원가입 시 이메일 인증번호는 %s입니다.", code); helper.setText(emailBody, true); + // Add inline image + ClassPathResource logoImage = new ClassPathResource("static/images/suppin-logo.png"); + helper.addInline("suppinLogo", logoImage); + javaMailSender.send(message); } catch (Exception e) { e.printStackTrace(); return false; } - return true; } } + + diff --git a/src/main/resources/static/images/suppin-logo.png b/src/main/resources/static/images/suppin-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3a4829980a47c13e24940152b34752f0fd50e958 GIT binary patch literal 7873 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 Date: Sun, 4 Aug 2024 02:26:14 +0900 Subject: [PATCH 18/18] =?UTF-8?q?Fix:=20=EC=9D=91=EB=8B=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=8F=AC=EB=A7=B7=20=EB=B0=8F=20emailBody=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmc/suppin/global/config/MailConfig.java | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/cmc/suppin/global/config/MailConfig.java b/src/main/java/com/cmc/suppin/global/config/MailConfig.java index 32c8dc2..56ea75d 100644 --- a/src/main/java/com/cmc/suppin/global/config/MailConfig.java +++ b/src/main/java/com/cmc/suppin/global/config/MailConfig.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; @Component @RequiredArgsConstructor @@ -24,41 +25,50 @@ public boolean sendMail(String toEmail, String code) { helper.setTo(toEmail); helper.setSubject("Suppin 인증번호"); - String emailBody = String.format( - "" + - "" + - "" + - "" + - "" + - "Suppin Email Verification" + - "" + - "" + - "

" + - "
" + - "\"Suppin" + - "

[Suppin] 인증번호를 안내해 드립니다.

" + - "
" + - "
" + - "

안녕하세요, Suppin을 이용해주셔서 감사합니다 :)

" + - "

Suppin 회원가입을 위해 인증번호를 안내해 드립니다. 아래 인증번호를 입력하여 이메일 인증을 완료해 주세요.

" + - String.format("
%s
", code) + - "" + - "" + - "" + - "" + - "" + - "
인증 번호" + code + "
요청 일시" + LocalDateTime.now().toString() + "
" + - "
" + - "
" + - "

감사합니다.

" + - "

※ 본 메일은 Suppin 서비스 이용에 관한 안내 메일입니다.

" + - "
" + - "
" + - "" + - "" - ); + // Format the current date and time + String formattedDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd, HH:mm")); - helper.setText(emailBody, true); + // Use StringBuilder to construct the HTML email body + StringBuilder emailBody = new StringBuilder(); + emailBody.append("") + .append("") + .append("") + .append("") + .append("") + .append("Suppin Email Verification") + .append("") + .append("") + .append("
") + .append("
") + .append("\"Suppin") + .append("

[Suppin] 인증번호를 안내해 드립니다.

") + .append("
") + .append("
") + .append("

안녕하세요, Suppin을 이용해주셔서 감사합니다 :)

") + .append("

Suppin 회원가입을 위해 인증번호를 안내해 드립니다. 아래 인증번호를 입력하여 이메일 인증을 완료해 주세요.

") + .append("
") + .append(code) + .append("
") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("
인증 번호") + .append(code) + .append("
요청 일시") + .append(formattedDateTime) + .append("
") + .append("
") + .append("
") + .append("

감사합니다.

") + .append("

※ 본 메일은 Suppin 서비스 이용에 관한 안내 메일입니다.

") + .append("
") + .append("
") + .append("") + .append(""); + + helper.setText(emailBody.toString(), true); // Add inline image ClassPathResource logoImage = new ClassPathResource("static/images/suppin-logo.png"); @@ -74,3 +84,5 @@ public boolean sendMail(String toEmail, String code) { } + +