From 711d5bdfa600de806d5b88fa5cd40e868cabf829 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Thu, 30 Jan 2025 17:50:25 +0900 Subject: [PATCH 1/5] =?UTF-8?q?CLAP-117=20feat:=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=A0=84=EC=86=A1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/admin/SendInvitationController.java | 26 ++++++ .../web/dto/admin/SendInvitationRequest.java | 9 ++ .../adapter/outbound/api/EmailClient.java | 11 +++ .../constant/NotificationType.java | 3 +- .../inbound/admin/SendInvitationUsecase.java | 7 ++ .../service/admin/SendInvitationService.java | 47 ++++++++++ .../server/domain/model/member/Member.java | 4 + src/main/resources/templates/invitation.html | 89 +++++++++++++++++++ 8 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/main/java/clap/server/adapter/inbound/web/admin/SendInvitationController.java create mode 100644 src/main/java/clap/server/adapter/inbound/web/dto/admin/SendInvitationRequest.java create mode 100644 src/main/java/clap/server/application/port/inbound/admin/SendInvitationUsecase.java create mode 100644 src/main/java/clap/server/application/service/admin/SendInvitationService.java create mode 100644 src/main/resources/templates/invitation.html diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/SendInvitationController.java b/src/main/java/clap/server/adapter/inbound/web/admin/SendInvitationController.java new file mode 100644 index 00000000..c82d59b4 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/admin/SendInvitationController.java @@ -0,0 +1,26 @@ +package clap.server.adapter.inbound.web.admin; + +import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; +import clap.server.application.port.inbound.admin.SendInvitationUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "05. Admin") +@WebAdapter +@RequiredArgsConstructor +@RequestMapping("/api/managements") +public class SendInvitationController { + private final SendInvitationUsecase sendInvitationUsecase; + + @Operation(summary = "회원 초대 이메일 발송 API") + @Secured("ROLE_ADMIN") + @PostMapping("/members/invite") + public void sendInvitation(@RequestBody @Valid SendInvitationRequest request) { + sendInvitationUsecase.sendInvitation(request); + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/SendInvitationRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/SendInvitationRequest.java new file mode 100644 index 00000000..8456c26a --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/SendInvitationRequest.java @@ -0,0 +1,9 @@ +package clap.server.adapter.inbound.web.dto.admin; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record SendInvitationRequest( + @Schema(description = "회원 ID", required = true) + @NotNull Long memberId +) {} diff --git a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java index 979233b0..2389ea98 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java @@ -67,6 +67,17 @@ else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) { body = templateEngine.process("processor-assign", context); } + else if (request.notificationType() == NotificationType.INVITATION) { + helper.setTo(request.email()); + helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다."); + + context.setVariable("invitationLink", "https://example.com/reset-password"); //TODO:비밀번호 설정 링크로 변경 예정 + context.setVariable("initialPassword", request.message()); + context.setVariable("receiverName", request.senderName()); + + body = templateEngine.process("invitation", context); + } + else { helper.setTo(request.email()); helper.setSubject("[TaskFlow 알림] 댓글이 작성되었습니다."); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/notification/constant/NotificationType.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/notification/constant/NotificationType.java index de07356c..955db493 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/notification/constant/NotificationType.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/notification/constant/NotificationType.java @@ -10,7 +10,8 @@ public enum NotificationType { TASK_REQUESTED("작업 요청"), STATUS_SWITCHED("상태 전환"), PROCESSOR_ASSIGNED("처리자 할당"), - PROCESSOR_CHANGED("처리자 변경"); + PROCESSOR_CHANGED("처리자 변경"), + INVITATION("회원가입 초대"); private final String description; } diff --git a/src/main/java/clap/server/application/port/inbound/admin/SendInvitationUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/SendInvitationUsecase.java new file mode 100644 index 00000000..64f394df --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/admin/SendInvitationUsecase.java @@ -0,0 +1,7 @@ +package clap.server.application.port.inbound.admin; + +import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; + +public interface SendInvitationUsecase { + void sendInvitation(SendInvitationRequest request); +} diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java new file mode 100644 index 00000000..1d6a110a --- /dev/null +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -0,0 +1,47 @@ + package clap.server.application.service.admin; + + import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; + import clap.server.adapter.outbound.api.dto.SendWebhookRequest; + import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; + import clap.server.application.port.inbound.admin.SendInvitationUsecase; + import clap.server.application.port.outbound.member.LoadMemberPort; + import clap.server.application.port.outbound.member.CommandMemberPort; + import clap.server.application.port.outbound.webhook.SendEmailPort; + import clap.server.domain.model.member.Member; + import clap.server.exception.ApplicationException; + import clap.server.exception.code.MemberErrorCode; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; + + @Service + @RequiredArgsConstructor + public class SendInvitationService implements SendInvitationUsecase { + private final LoadMemberPort loadMemberPort; + private final CommandMemberPort commandMemberPort; + private final SendEmailPort sendEmailPort; + + @Override + public void sendInvitation(SendInvitationRequest request) { + // 회원 조회 + Member member = loadMemberPort.findById(request.memberId()) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + + // 회원 상태를 PENDING으로 변경 + member.setStatusPending(); + + // 변경된 회원 저장 + commandMemberPort.save(member); + + // 이메일 전송 + sendEmailPort.sendEmail( + new SendWebhookRequest( + member.getMemberInfo().getEmail(), + NotificationType.INVITATION, // 알림 유형 + "회원가입 초대", // 작업 이름 + member.getMemberInfo().getName(), // 회원 이름 + member.getPassword(), // 초기 비밀번호 + null + ) + ); + } + } diff --git a/src/main/java/clap/server/domain/model/member/Member.java b/src/main/java/clap/server/domain/model/member/Member.java index c1e2f682..6fb956f8 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -56,4 +56,8 @@ public String getNickname() { public boolean isReviewer() { return this.memberInfo != null && this.memberInfo.isReviewer(); } + + public void setStatusPending() { + this.status = MemberStatus.PENDING; + } } diff --git a/src/main/resources/templates/invitation.html b/src/main/resources/templates/invitation.html new file mode 100644 index 00000000..89090b4d --- /dev/null +++ b/src/main/resources/templates/invitation.html @@ -0,0 +1,89 @@ + + + + + TaskFlow 초대 이메일 + + + +
+
+ TaskFlow 초대 서비스 +
+
+

안녕하세요, 님!

+

TaskFlow 회원가입 초대 메일입니다.

+ + +
+ +
+ + From 22f8e429c888dd5fefd21bd4a02ef95dc6a70d6f Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Fri, 31 Jan 2025 13:07:44 +0900 Subject: [PATCH 2/5] =?UTF-8?q?CLAP-117=20rename:setStatusPending->changeS?= =?UTF-8?q?tatusToPending=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/admin/SendInvitationService.java | 4 +++- src/main/java/clap/server/domain/model/member/Member.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java index 1d6a110a..e9badccf 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -12,6 +12,7 @@ import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; + import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -21,13 +22,14 @@ public class SendInvitationService implements SendInvitationUsecase { private final SendEmailPort sendEmailPort; @Override + @Transactional public void sendInvitation(SendInvitationRequest request) { // 회원 조회 Member member = loadMemberPort.findById(request.memberId()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); // 회원 상태를 PENDING으로 변경 - member.setStatusPending(); + member.changeStatusToPending(); // 변경된 회원 저장 commandMemberPort.save(member); diff --git a/src/main/java/clap/server/domain/model/member/Member.java b/src/main/java/clap/server/domain/model/member/Member.java index 6fb956f8..3be98d52 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -57,7 +57,7 @@ public boolean isReviewer() { return this.memberInfo != null && this.memberInfo.isReviewer(); } - public void setStatusPending() { + public void changeStatusToPending() { this.status = MemberStatus.PENDING; } } From 6fc0da5a100d648c1e0d9fe03da55900afea799b Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Fri, 31 Jan 2025 20:10:35 +0900 Subject: [PATCH 3/5] =?UTF-8?q?CLAP-117=20fix:=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=9B=B9=ED=9B=85=EA=B3=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../outbound/api/InvitationEmailClient.java | 47 +++++++++++ .../email/SendInvitationEmailPort.java | 7 ++ .../service/admin/SendInvitationService.java | 78 +++++++++---------- 3 files changed, 90 insertions(+), 42 deletions(-) create mode 100644 src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java create mode 100644 src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java diff --git a/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java b/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java new file mode 100644 index 00000000..05229e45 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java @@ -0,0 +1,47 @@ +package clap.server.adapter.outbound.api; + +import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; +import clap.server.application.port.outbound.email.SendInvitationEmailPort; +import clap.server.common.annotation.architecture.ExternalApiAdapter; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.NotificationErrorCode; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +@ExternalApiAdapter +@RequiredArgsConstructor +public class InvitationEmailClient implements SendInvitationEmailPort { + private final SpringTemplateEngine templateEngine; + private final JavaMailSender mailSender; + + @Override + public void sendInvitationEmail(SendInvitationRequest request, String memberEmail, String initialPassword) { + try { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + + // 이메일 설정 + helper.setTo(memberEmail); + helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다."); + + // Thymeleaf 컨텍스트 설정 + Context context = new Context(); + context.setVariable("invitationLink", "https://example.com/reset-password"); // TODO: 링크 변경 필요 + context.setVariable("initialPassword", initialPassword); + context.setVariable("receiverName", "사용자 이름"); // 사용자 이름 필요 + + // 이메일 템플릿 처리 + String body = templateEngine.process("invitation", context); + helper.setText(body, true); + + // 이메일 전송 + mailSender.send(mimeMessage); + } catch (Exception e) { + throw new ApplicationException(NotificationErrorCode.EMAIL_SEND_FAILED); + } + } +} diff --git a/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java b/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java new file mode 100644 index 00000000..40778238 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java @@ -0,0 +1,7 @@ +package clap.server.application.port.outbound.email; + +import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; + +public interface SendInvitationEmailPort { + void sendInvitationEmail(SendInvitationRequest request, String memberEmail, String initialPassword); +} diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java index e9badccf..1854e5ef 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -1,49 +1,43 @@ - package clap.server.application.service.admin; +package clap.server.application.service.admin; - import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; - import clap.server.adapter.outbound.api.dto.SendWebhookRequest; - import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; - import clap.server.application.port.inbound.admin.SendInvitationUsecase; - import clap.server.application.port.outbound.member.LoadMemberPort; - import clap.server.application.port.outbound.member.CommandMemberPort; - import clap.server.application.port.outbound.webhook.SendEmailPort; - import clap.server.domain.model.member.Member; - import clap.server.exception.ApplicationException; - import clap.server.exception.code.MemberErrorCode; - import lombok.RequiredArgsConstructor; - import org.springframework.stereotype.Service; - import org.springframework.transaction.annotation.Transactional; +import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; +import clap.server.application.port.inbound.admin.SendInvitationUsecase; +import clap.server.application.port.outbound.email.SendInvitationEmailPort; +import clap.server.application.port.outbound.member.CommandMemberPort; +import clap.server.application.port.outbound.member.LoadMemberPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.member.Member; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; - @Service - @RequiredArgsConstructor - public class SendInvitationService implements SendInvitationUsecase { - private final LoadMemberPort loadMemberPort; - private final CommandMemberPort commandMemberPort; - private final SendEmailPort sendEmailPort; +@ApplicationService +@RequiredArgsConstructor +public class SendInvitationService implements SendInvitationUsecase { + private final LoadMemberPort loadMemberPort; + private final CommandMemberPort commandMemberPort; + private final SendInvitationEmailPort sendInvitationEmailPort; - @Override - @Transactional - public void sendInvitation(SendInvitationRequest request) { - // 회원 조회 - Member member = loadMemberPort.findById(request.memberId()) - .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + @Override + @Transactional + public void sendInvitation(SendInvitationRequest request) { + // 회원 조회 + Member member = loadMemberPort.findById(request.memberId()) + .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); - // 회원 상태를 PENDING으로 변경 - member.changeStatusToPending(); + // 회원 상태를 PENDING으로 변경 + member.changeStatusToPending(); - // 변경된 회원 저장 - commandMemberPort.save(member); + // 변경된 회원 저장 + commandMemberPort.save(member); - // 이메일 전송 - sendEmailPort.sendEmail( - new SendWebhookRequest( - member.getMemberInfo().getEmail(), - NotificationType.INVITATION, // 알림 유형 - "회원가입 초대", // 작업 이름 - member.getMemberInfo().getName(), // 회원 이름 - member.getPassword(), // 초기 비밀번호 - null - ) - ); - } + // 초대 이메일 전송 + sendInvitationEmailPort.sendInvitationEmail( + request, + member.getMemberInfo().getEmail(), + member.getPassword() //TODO: 초기 비밀번호 생성 로직 추가 + ); } +} From 73ffc2661799ef17acc6d7c6badceb1ce2c90793 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Fri, 31 Jan 2025 22:56:17 +0900 Subject: [PATCH 4/5] =?UTF-8?q?CLAP-117=20fix:=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=83=9D=EC=84=B1=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 --- .../outbound/api/InvitationEmailClient.java | 8 +++---- .../email/SendInvitationEmailPort.java | 4 +--- .../service/admin/SendInvitationService.java | 21 ++++++++++++------- .../utils/InitialPasswordGenerator.java | 15 +++++-------- .../server/domain/model/member/Member.java | 4 ++-- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java b/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java index 05229e45..00062ba8 100644 --- a/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java @@ -19,20 +19,18 @@ public class InvitationEmailClient implements SendInvitationEmailPort { private final JavaMailSender mailSender; @Override - public void sendInvitationEmail(SendInvitationRequest request, String memberEmail, String initialPassword) { + public void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword) { try { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); - // 이메일 설정 helper.setTo(memberEmail); helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다."); - // Thymeleaf 컨텍스트 설정 Context context = new Context(); - context.setVariable("invitationLink", "https://example.com/reset-password"); // TODO: 링크 변경 필요 + context.setVariable("invitationLink", "https://example.com/reset-password"); //TODO: 비밀번호 재설정 링크로 변경 context.setVariable("initialPassword", initialPassword); - context.setVariable("receiverName", "사용자 이름"); // 사용자 이름 필요 + context.setVariable("receiverName", receiverName); // 이메일 템플릿 처리 String body = templateEngine.process("invitation", context); diff --git a/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java b/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java index 40778238..c04aecce 100644 --- a/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java +++ b/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java @@ -1,7 +1,5 @@ package clap.server.application.port.outbound.email; -import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; - public interface SendInvitationEmailPort { - void sendInvitationEmail(SendInvitationRequest request, String memberEmail, String initialPassword); + void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword); } diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java index 1854e5ef..5dad4459 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -6,11 +6,12 @@ import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.common.utils.InitialPasswordGenerator; import clap.server.domain.model.member.Member; import clap.server.exception.ApplicationException; import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; @ApplicationService @@ -19,6 +20,8 @@ public class SendInvitationService implements SendInvitationUsecase { private final LoadMemberPort loadMemberPort; private final CommandMemberPort commandMemberPort; private final SendInvitationEmailPort sendInvitationEmailPort; + private final InitialPasswordGenerator passwordGenerator; + private final PasswordEncoder passwordEncoder; @Override @Transactional @@ -27,17 +30,21 @@ public void sendInvitation(SendInvitationRequest request) { Member member = loadMemberPort.findById(request.memberId()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); - // 회원 상태를 PENDING으로 변경 - member.changeStatusToPending(); + // 초기 비밀번호 생성 + String initialPassword = passwordGenerator.generateRandomPassword(8); + String encodedPassword = passwordEncoder.encode(initialPassword); - // 변경된 회원 저장 + // 회원 비밀번호 업데이트 + member.resetPassword(encodedPassword); commandMemberPort.save(member); - // 초대 이메일 전송 + // 회원 상태를 APPROVAL_REQUEST으로 변경 + member.changeStatusToAPPROVAL_REQUEST(); + sendInvitationEmailPort.sendInvitationEmail( - request, member.getMemberInfo().getEmail(), - member.getPassword() //TODO: 초기 비밀번호 생성 로직 추가 + member.getMemberInfo().getName(), + initialPassword ); } } diff --git a/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java b/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java index 175e5984..3e5ed23f 100644 --- a/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java +++ b/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java @@ -1,20 +1,14 @@ package clap.server.common.utils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import java.security.SecureRandom; - +@Component public class InitialPasswordGenerator { - - @Value("${password.policy.characters}") + @Value("${password.policy.characters:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()}") private String characters; - private static final int PASSWORD_LENGTH = 8; - - private InitialPasswordGenerator() { - throw new IllegalStateException("Utility class"); - } - public String generateRandomPassword(int length) { if (length <= 0) { throw new IllegalArgumentException("Password length must be greater than 0"); @@ -24,10 +18,11 @@ public String generateRandomPassword(int length) { StringBuilder password = new StringBuilder(length); for (int i = 0; i < length; i++) { - int randomIndex = secureRandom.nextInt(PASSWORD_LENGTH); + int randomIndex = secureRandom.nextInt(characters.length()); password.append(characters.charAt(randomIndex)); } return password.toString(); } } + diff --git a/src/main/java/clap/server/domain/model/member/Member.java b/src/main/java/clap/server/domain/model/member/Member.java index 3be98d52..0f8f3df8 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -57,7 +57,7 @@ public boolean isReviewer() { return this.memberInfo != null && this.memberInfo.isReviewer(); } - public void changeStatusToPending() { - this.status = MemberStatus.PENDING; + public void changeStatusToAPPROVAL_REQUEST() { + this.status = MemberStatus.APPROVAL_REQUEST; } } From 3a00db9b731a527b46297d7cb44e6865a12b0155 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Sun, 2 Feb 2025 11:18:02 +0900 Subject: [PATCH 5/5] =?UTF-8?q?CLAP-117=20fix:EmailClient=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EC=B4=88=EB=8C=80=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/outbound/api/EmailClient.java | 41 +++++++++++------ .../outbound/api/InvitationEmailClient.java | 45 ------------------- .../email/SendInvitationEmailPort.java | 5 --- .../port/outbound/webhook/SendEmailPort.java | 3 ++ .../service/admin/SendInvitationService.java | 6 +-- 5 files changed, 33 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java delete mode 100644 src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java diff --git a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java index 2389ea98..36d4f7cb 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java @@ -36,8 +36,7 @@ public void sendEmail(SendWebhookRequest request) { context.setVariable("title", request.taskName()); body = templateEngine.process("task-request", context); - } - else if (request.notificationType() == NotificationType.STATUS_SWITCHED) { + } else if (request.notificationType() == NotificationType.STATUS_SWITCHED) { helper.setTo(request.email()); helper.setSubject("[TaskFlow 알림] 작업 상태가 변경되었습니다."); @@ -45,9 +44,7 @@ else if (request.notificationType() == NotificationType.STATUS_SWITCHED) { context.setVariable("title", request.taskName()); body = templateEngine.process("status-switch", context); - } - - else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) { + } else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) { helper.setTo(request.email()); helper.setSubject("[TaskFlow 알림] 작업 담당자가 변경되었습니다."); @@ -55,9 +52,7 @@ else if (request.notificationType() == NotificationType.PROCESSOR_CHANGED) { context.setVariable("title", request.taskName()); body = templateEngine.process("processor-change", context); - } - - else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) { + } else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) { helper.setTo(request.email()); helper.setSubject("[TaskFlow 알림] 작업 담당자가 지정되었습니다."); @@ -65,9 +60,7 @@ else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) { context.setVariable("title", request.taskName()); body = templateEngine.process("processor-assign", context); - } - - else if (request.notificationType() == NotificationType.INVITATION) { + } else if (request.notificationType() == NotificationType.INVITATION) { helper.setTo(request.email()); helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다."); @@ -76,9 +69,7 @@ else if (request.notificationType() == NotificationType.INVITATION) { context.setVariable("receiverName", request.senderName()); body = templateEngine.process("invitation", context); - } - - else { + } else { helper.setTo(request.email()); helper.setSubject("[TaskFlow 알림] 댓글이 작성되었습니다."); @@ -94,4 +85,26 @@ else if (request.notificationType() == NotificationType.INVITATION) { throw new ApplicationException(NotificationErrorCode.EMAIL_SEND_FAILED); } } + + public void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword) { + try { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + + helper.setTo(memberEmail); + helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다."); + + Context context = new Context(); + context.setVariable("invitationLink", "https://example.com/reset-password"); // TODO: 비밀번호 재설정 링크로 변경 + context.setVariable("initialPassword", initialPassword); + context.setVariable("receiverName", receiverName); + + String body = templateEngine.process("invitation", context); + helper.setText(body, true); + + mailSender.send(mimeMessage); + } catch (Exception e) { + throw new ApplicationException(NotificationErrorCode.EMAIL_SEND_FAILED); + } + } } diff --git a/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java b/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java deleted file mode 100644 index 00062ba8..00000000 --- a/src/main/java/clap/server/adapter/outbound/api/InvitationEmailClient.java +++ /dev/null @@ -1,45 +0,0 @@ -package clap.server.adapter.outbound.api; - -import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; -import clap.server.application.port.outbound.email.SendInvitationEmailPort; -import clap.server.common.annotation.architecture.ExternalApiAdapter; -import clap.server.exception.ApplicationException; -import clap.server.exception.code.NotificationErrorCode; -import jakarta.mail.internet.MimeMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; -import org.thymeleaf.context.Context; -import org.thymeleaf.spring6.SpringTemplateEngine; - -@ExternalApiAdapter -@RequiredArgsConstructor -public class InvitationEmailClient implements SendInvitationEmailPort { - private final SpringTemplateEngine templateEngine; - private final JavaMailSender mailSender; - - @Override - public void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword) { - try { - MimeMessage mimeMessage = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); - - helper.setTo(memberEmail); - helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다."); - - Context context = new Context(); - context.setVariable("invitationLink", "https://example.com/reset-password"); //TODO: 비밀번호 재설정 링크로 변경 - context.setVariable("initialPassword", initialPassword); - context.setVariable("receiverName", receiverName); - - // 이메일 템플릿 처리 - String body = templateEngine.process("invitation", context); - helper.setText(body, true); - - // 이메일 전송 - mailSender.send(mimeMessage); - } catch (Exception e) { - throw new ApplicationException(NotificationErrorCode.EMAIL_SEND_FAILED); - } - } -} diff --git a/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java b/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java deleted file mode 100644 index c04aecce..00000000 --- a/src/main/java/clap/server/application/port/outbound/email/SendInvitationEmailPort.java +++ /dev/null @@ -1,5 +0,0 @@ -package clap.server.application.port.outbound.email; - -public interface SendInvitationEmailPort { - void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword); -} diff --git a/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java b/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java index e69f7094..0a3b7b6c 100644 --- a/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java +++ b/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java @@ -5,4 +5,7 @@ public interface SendEmailPort { void sendEmail(SendWebhookRequest request); + + void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword); + } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java index 5dad4459..d58c1b8b 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -2,9 +2,9 @@ import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest; import clap.server.application.port.inbound.admin.SendInvitationUsecase; -import clap.server.application.port.outbound.email.SendInvitationEmailPort; import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; +import clap.server.application.port.outbound.webhook.SendEmailPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.utils.InitialPasswordGenerator; import clap.server.domain.model.member.Member; @@ -19,7 +19,7 @@ public class SendInvitationService implements SendInvitationUsecase { private final LoadMemberPort loadMemberPort; private final CommandMemberPort commandMemberPort; - private final SendInvitationEmailPort sendInvitationEmailPort; + private final SendEmailPort sendEmailPort; private final InitialPasswordGenerator passwordGenerator; private final PasswordEncoder passwordEncoder; @@ -41,7 +41,7 @@ public void sendInvitation(SendInvitationRequest request) { // 회원 상태를 APPROVAL_REQUEST으로 변경 member.changeStatusToAPPROVAL_REQUEST(); - sendInvitationEmailPort.sendInvitationEmail( + sendEmailPort.sendInvitationEmail( member.getMemberInfo().getEmail(), member.getMemberInfo().getName(), initialPassword