Skip to content

Commit 711d5bd

Browse files
Sihun23Sihun23
authored andcommitted
CLAP-117 feat:이메일 전송 API 구현
1 parent e4c5d32 commit 711d5bd

File tree

8 files changed

+195
-1
lines changed

8 files changed

+195
-1
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package clap.server.adapter.inbound.web.admin;
2+
3+
import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest;
4+
import clap.server.application.port.inbound.admin.SendInvitationUsecase;
5+
import clap.server.common.annotation.architecture.WebAdapter;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.access.annotation.Secured;
11+
import org.springframework.web.bind.annotation.*;
12+
13+
@Tag(name = "05. Admin")
14+
@WebAdapter
15+
@RequiredArgsConstructor
16+
@RequestMapping("/api/managements")
17+
public class SendInvitationController {
18+
private final SendInvitationUsecase sendInvitationUsecase;
19+
20+
@Operation(summary = "회원 초대 이메일 발송 API")
21+
@Secured("ROLE_ADMIN")
22+
@PostMapping("/members/invite")
23+
public void sendInvitation(@RequestBody @Valid SendInvitationRequest request) {
24+
sendInvitationUsecase.sendInvitation(request);
25+
}
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package clap.server.adapter.inbound.web.dto.admin;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
public record SendInvitationRequest(
7+
@Schema(description = "회원 ID", required = true)
8+
@NotNull Long memberId
9+
) {}

src/main/java/clap/server/adapter/outbound/api/EmailClient.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ else if (request.notificationType() == NotificationType.PROCESSOR_ASSIGNED) {
6767
body = templateEngine.process("processor-assign", context);
6868
}
6969

70+
else if (request.notificationType() == NotificationType.INVITATION) {
71+
helper.setTo(request.email());
72+
helper.setSubject("[TaskFlow 초대] 회원가입을 환영합니다.");
73+
74+
context.setVariable("invitationLink", "https://example.com/reset-password"); //TODO:비밀번호 설정 링크로 변경 예정
75+
context.setVariable("initialPassword", request.message());
76+
context.setVariable("receiverName", request.senderName());
77+
78+
body = templateEngine.process("invitation", context);
79+
}
80+
7081
else {
7182
helper.setTo(request.email());
7283
helper.setSubject("[TaskFlow 알림] 댓글이 작성되었습니다.");

src/main/java/clap/server/adapter/outbound/persistense/entity/notification/constant/NotificationType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ public enum NotificationType {
1010
TASK_REQUESTED("작업 요청"),
1111
STATUS_SWITCHED("상태 전환"),
1212
PROCESSOR_ASSIGNED("처리자 할당"),
13-
PROCESSOR_CHANGED("처리자 변경");
13+
PROCESSOR_CHANGED("처리자 변경"),
14+
INVITATION("회원가입 초대");
1415

1516
private final String description;
1617
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package clap.server.application.port.inbound.admin;
2+
3+
import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest;
4+
5+
public interface SendInvitationUsecase {
6+
void sendInvitation(SendInvitationRequest request);
7+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package clap.server.application.service.admin;
2+
3+
import clap.server.adapter.inbound.web.dto.admin.SendInvitationRequest;
4+
import clap.server.adapter.outbound.api.dto.SendWebhookRequest;
5+
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
6+
import clap.server.application.port.inbound.admin.SendInvitationUsecase;
7+
import clap.server.application.port.outbound.member.LoadMemberPort;
8+
import clap.server.application.port.outbound.member.CommandMemberPort;
9+
import clap.server.application.port.outbound.webhook.SendEmailPort;
10+
import clap.server.domain.model.member.Member;
11+
import clap.server.exception.ApplicationException;
12+
import clap.server.exception.code.MemberErrorCode;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.stereotype.Service;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
public class SendInvitationService implements SendInvitationUsecase {
19+
private final LoadMemberPort loadMemberPort;
20+
private final CommandMemberPort commandMemberPort;
21+
private final SendEmailPort sendEmailPort;
22+
23+
@Override
24+
public void sendInvitation(SendInvitationRequest request) {
25+
// 회원 조회
26+
Member member = loadMemberPort.findById(request.memberId())
27+
.orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND));
28+
29+
// 회원 상태를 PENDING으로 변경
30+
member.setStatusPending();
31+
32+
// 변경된 회원 저장
33+
commandMemberPort.save(member);
34+
35+
// 이메일 전송
36+
sendEmailPort.sendEmail(
37+
new SendWebhookRequest(
38+
member.getMemberInfo().getEmail(),
39+
NotificationType.INVITATION, // 알림 유형
40+
"회원가입 초대", // 작업 이름
41+
member.getMemberInfo().getName(), // 회원 이름
42+
member.getPassword(), // 초기 비밀번호
43+
null
44+
)
45+
);
46+
}
47+
}

src/main/java/clap/server/domain/model/member/Member.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,8 @@ public String getNickname() {
5656
public boolean isReviewer() {
5757
return this.memberInfo != null && this.memberInfo.isReviewer();
5858
}
59+
60+
public void setStatusPending() {
61+
this.status = MemberStatus.PENDING;
62+
}
5963
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<!DOCTYPE html>
2+
<html lang="ko" xmlns:th="http://www.w3.org/1999/xhtml">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>TaskFlow 초대 이메일</title>
6+
<style>
7+
/* CSS 스타일 */
8+
body {
9+
font-family: Arial, sans-serif;
10+
line-height: 1.6;
11+
background-color: #f9f9f9;
12+
margin: 0;
13+
padding: 0;
14+
}
15+
.email-container {
16+
max-width: 500px;
17+
margin: 20px auto;
18+
background: #ffffff;
19+
border: 1px solid #eaeaea;
20+
border-radius: 8px;
21+
overflow: hidden;
22+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
23+
}
24+
.header {
25+
background-color: #0052cc;
26+
color: #ffffff;
27+
padding: 15px;
28+
text-align: center;
29+
}
30+
.content {
31+
padding: 20px;
32+
color: #333333;
33+
}
34+
.content p {
35+
margin: 10px 0;
36+
}
37+
.cta-button {
38+
text-align: center;
39+
margin: 20px 0;
40+
}
41+
.cta-button a {
42+
background-color: #0052cc;
43+
color: #ffffff;
44+
text-decoration: none;
45+
padding: 10px 20px;
46+
border-radius: 5px;
47+
font-weight: bold;
48+
}
49+
.cta-button a:hover {
50+
background-color: #0041a7;
51+
}
52+
.footer {
53+
text-align: center;
54+
padding: 10px;
55+
font-size: 0.9em;
56+
color: #777777;
57+
background-color: #f4f4f4;
58+
border-top: 1px solid #eaeaea;
59+
}
60+
.footer .taskflow {
61+
font-size: 1.2em; /* 글자 크기 조정 */
62+
font-weight: bold; /* 글자 굵게 */
63+
}
64+
</style>
65+
</head>
66+
<body>
67+
<div class="email-container">
68+
<div class="header">
69+
TaskFlow 초대 서비스
70+
</div>
71+
<div class="content">
72+
<p>안녕하세요, <strong th:text="${receiverName}"></strong>님!</p>
73+
<p>TaskFlow 회원가입 초대 메일입니다.</p>
74+
<ul>
75+
<li>초대 링크: <a href="https://example.com/register" target="_blank" th:href="${invitationLink}">회원가입 링크</a></li>
76+
<li>초기 비밀번호: <strong th:text="${initialPassword}"></strong></li>
77+
</ul>
78+
<div class="cta-button">
79+
<a href="https://example.com/register" target="_blank" th:href="${invitationLink}">지금 가입하기</a>
80+
</div>
81+
</div>
82+
<div class="footer">
83+
<span class="taskflow">TaskFlow</span><br>
84+
스마트한 업무 관리를 위한<br>
85+
"혁신적인 서비스"
86+
</div>
87+
</div>
88+
</body>
89+
</html>

0 commit comments

Comments
 (0)