Skip to content

Commit adab570

Browse files
authored
refactor: 이메일 발송 비동기식으로 전환 (DASOMBE-13)
* refactor: 이메일 전송 비동기 처리 (DASOMBE-13) * refactor: 이메일 전송 오류시 로그 저장 DB생성, 이메일 템플릿 분기처리 (DASOMBE-13) * refactor: 이메일 로그 builder 패턴으로 인스턴스 초기화 완료 (DASOMBE-13) * refactor: 성공여부도 저장 가능하게 수정 (DASOMBE-13) * refactor: 빌더 패턴 EmailLog엔티티 안으로 이동, 에러메시지 저장 로직 try-catch문 밖으로 이동 (DASOMBE-13)
1 parent 98de3f8 commit adab570

File tree

11 files changed

+318
-107
lines changed

11 files changed

+318
-107
lines changed

src/main/java/dmu/dasom/api/ApiApplication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.scheduling.annotation.EnableAsync;
67

78
@EnableJpaAuditing
89
@SpringBootApplication
10+
@EnableAsync
911
public class ApiApplication {
10-
1112
public static void main(String[] args) {
1213
SpringApplication.run(ApiApplication.class, args);
1314
}

src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,7 @@ public void sendEmailsToApplicants(MailType mailType) {
132132
}
133133

134134
for (Applicant applicant : applicants) {
135-
try {
136-
emailService.sendEmail(applicant.getEmail(), applicant.getName(), mailType);
137-
} catch (MessagingException e) {
138-
System.err.println("Failed to send email to: " + applicant.getEmail());
139-
}
135+
emailService.sendEmail(applicant.getEmail(), applicant.getName(), mailType);
140136
}
141137
}
142138

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dmu.dasom.api.domain.google.entity;
2+
3+
import dmu.dasom.api.domain.google.enums.MailSendStatus;
4+
import jakarta.persistence.*;
5+
import lombok.*;
6+
import org.hibernate.annotations.CreationTimestamp;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Entity
11+
@Getter
12+
@Builder
13+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
14+
@AllArgsConstructor
15+
public class EmailLog {
16+
17+
@Id
18+
@GeneratedValue(strategy = GenerationType.IDENTITY)
19+
private Long id;
20+
21+
@Column(nullable = false)
22+
private String recipientEmail;
23+
24+
@Enumerated(EnumType.STRING)
25+
@Column(nullable = false)
26+
private MailSendStatus status;
27+
28+
private String errorMessage;
29+
30+
@CreationTimestamp
31+
private LocalDateTime sentAt;
32+
33+
public static EmailLog of(String recipientEmail, MailSendStatus status, String errorMessage) {
34+
return EmailLog.builder()
35+
.recipientEmail(recipientEmail)
36+
.status(status)
37+
.errorMessage(errorMessage)
38+
.build();
39+
}
40+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dmu.dasom.api.domain.google.enums;
2+
3+
public enum MailSendStatus {
4+
SUCCESS,
5+
FAILURE
6+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dmu.dasom.api.domain.google.enums;
2+
3+
import dmu.dasom.api.domain.common.exception.CustomException;
4+
import dmu.dasom.api.domain.common.exception.ErrorCode;
5+
import lombok.Getter;
6+
import lombok.RequiredArgsConstructor;
7+
8+
import java.util.Arrays;
9+
10+
@Getter
11+
@RequiredArgsConstructor
12+
public enum MailTemplate {
13+
DOCUMENT_RESULT(MailType.DOCUMENT_RESULT, "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 서류 결과 안내", "document-result-email"),
14+
FINAL_RESULT(MailType.FINAL_RESULT, "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 최종 면접 결과 안내", "final-result-email");
15+
16+
private final MailType mailType;
17+
private final String subject;
18+
private final String templateName;
19+
20+
public static MailTemplate getMailType(MailType mailType) {
21+
return Arrays.stream(values())
22+
.filter(template -> template.getMailType() == mailType)
23+
.findFirst()
24+
.orElseThrow(() -> new CustomException(ErrorCode.MAIL_TYPE_NOT_VALID));
25+
}
26+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dmu.dasom.api.domain.google.repository;
2+
3+
import dmu.dasom.api.domain.google.entity.EmailLog;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface EmailLogRepository extends JpaRepository<EmailLog, Long> {
7+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dmu.dasom.api.domain.google.service;
2+
3+
import dmu.dasom.api.domain.google.entity.EmailLog;
4+
import dmu.dasom.api.domain.google.enums.MailSendStatus;
5+
import dmu.dasom.api.domain.google.repository.EmailLogRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Service;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
public class EmailLogService {
13+
14+
private final EmailLogRepository emailLogRepository;
15+
16+
@Async
17+
public void logEmailSending(String recipientEmail, MailSendStatus status, String errorMessage) {
18+
EmailLog emailLog = EmailLog.of(recipientEmail, status, errorMessage);
19+
emailLogRepository.save(emailLog);
20+
}
21+
}

src/main/java/dmu/dasom/api/domain/google/service/EmailService.java

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,79 +2,78 @@
22

33
import dmu.dasom.api.domain.common.exception.CustomException;
44
import dmu.dasom.api.domain.common.exception.ErrorCode;
5+
import dmu.dasom.api.domain.google.enums.MailSendStatus;
6+
import dmu.dasom.api.domain.google.enums.MailTemplate;
57
import dmu.dasom.api.domain.google.enums.MailType;
68
import jakarta.mail.MessagingException;
79
import jakarta.mail.internet.MimeMessage;
810
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
912
import org.springframework.beans.factory.annotation.Value;
1013
import org.springframework.mail.javamail.JavaMailSender;
1114
import org.springframework.mail.javamail.MimeMessageHelper;
15+
import org.springframework.scheduling.annotation.Async;
1216
import org.springframework.stereotype.Service;
1317
import org.thymeleaf.TemplateEngine;
1418
import org.thymeleaf.context.Context;
1519

20+
@Slf4j
1621
@RequiredArgsConstructor
1722
@Service
1823
public class EmailService {
1924

2025
private final TemplateEngine templateEngine;
2126
private final JavaMailSender javaMailSender;
27+
private final EmailLogService emailLogService;
2228

2329
@Value("${spring.mail.username}")
2430
private String from;
2531

26-
public void sendEmail(String to, String name, MailType mailType) throws MessagingException {
27-
if (mailType == null){
28-
throw new CustomException(ErrorCode.MAIL_TYPE_NOT_VALID);
29-
}
32+
@Async
33+
public void sendEmail(String to, String name, MailType mailType) {
34+
MailSendStatus mailSendStatus = MailSendStatus.SUCCESS;
35+
String errorMessage = null;
36+
try {
37+
if (mailType == null) {
38+
throw new CustomException(ErrorCode.MAIL_TYPE_NOT_VALID);
39+
}
3040

31-
// 메일 제목 및 템플릿 설정
32-
String subject;
33-
String emailContent;
34-
String buttonUrl = "https://dmu-dasom.or.kr/recruit/result";
35-
String buttonText;
41+
// 메일 템플릿 조회
42+
MailTemplate mailTemplate = MailTemplate.getMailType(mailType);
43+
String buttonUrl = "https://dmu-dasom.or.kr/recruit/result";
3644

37-
switch (mailType) {
38-
case DOCUMENT_RESULT -> {
39-
subject = "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 서류 결과 안내";
40-
emailContent = "먼저 다솜 34기에 많은 관심을 두고 지원해 주셔서 감사드리며,<br>" +
41-
"내부 서류 평가 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.<br>" +
42-
"서류 전형 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다.";
43-
buttonText = "서류 결과 확인하기";
44-
}
45-
case FINAL_RESULT -> {
46-
subject = "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 최종 면접 결과 안내";
47-
emailContent = "먼저 다솜 34기에 많은 관심을 두고 지원해 주셔서 감사드리며,<br>" +
48-
"최종 면접 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.<br>" +
49-
"최종 면접 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다.";
50-
buttonText = "최종 결과 확인하기";
51-
}
52-
default -> throw new IllegalStateException("Unexpected value: " + mailType);
53-
}
45+
// HTML 템플릿에 전달할 데이터 설정
46+
Context context = new Context();
47+
context.setVariable("name", name); // 지원자 이름 전달
48+
context.setVariable("buttonUrl", buttonUrl); // 버튼 링크 전달
5449

55-
// HTML 템플릿에 전달할 데이터 설정
56-
Context context = new Context();
57-
context.setVariable("name", name); // 지원자 이름 전달
58-
context.setVariable("emailContent", emailContent); // 이메일 내용 전달
59-
context.setVariable("buttonUrl", buttonUrl); // 버튼 링크 전달
60-
context.setVariable("buttonText", buttonText);
6150

62-
// HTML 템플릿 처리
63-
String htmlBody = templateEngine.process("email-template", context);
51+
// HTML 템플릿 처리
52+
String htmlBody = templateEngine.process(mailTemplate.getTemplateName(), context);
6453

65-
// 이메일 생성 및 전송
66-
MimeMessage message = javaMailSender.createMimeMessage();
67-
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
54+
// 이메일 생성 및 전송
55+
MimeMessage message = javaMailSender.createMimeMessage();
56+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
6857

69-
helper.setTo(to);
70-
helper.setSubject(subject);
71-
helper.setText(htmlBody, true);
72-
helper.setFrom((from != null && !from.isEmpty()) ? from : "[email protected]");
58+
helper.setTo(to);
59+
helper.setSubject(mailTemplate.getSubject());
60+
helper.setText(htmlBody, true);
61+
helper.setFrom((from != null && !from.isEmpty()) ? from : "[email protected]");
7362

74-
// Content-Type을 명시적으로 설정
75-
message.setContent(htmlBody, "text/html; charset=utf-8");
63+
// Content-Type을 명시적으로 설정
64+
message.setContent(htmlBody, "text/html; charset=utf-8");
7665

77-
javaMailSender.send(message);
66+
javaMailSender.send(message);
67+
log.info("Email sent successfull {}", to);
68+
} catch (MessagingException e) {
69+
log.error("Failed to send email to {}: {}", to, e.getMessage());
70+
mailSendStatus = MailSendStatus.FAILURE;
71+
errorMessage = e.getMessage();
72+
} catch (CustomException e) {
73+
log.error("Email sending error for {}: {}", to, e.getMessage());
74+
mailSendStatus = MailSendStatus.FAILURE;
75+
errorMessage = e.getMessage();
76+
}
77+
emailLogService.logEmailSending(to, mailSendStatus, errorMessage);
7878
}
79-
8079
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<!DOCTYPE html>
2+
<html lang="ko">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
7+
<meta name="supported-color-schemes" content="light dark">
8+
<title>이메일 안내</title>
9+
</head>
10+
<body style="
11+
background-color: #17171B;
12+
color: white !important;
13+
font-family: 'Pretendard', sans-serif;
14+
display: flex;
15+
justify-content: center;
16+
align-items: center;
17+
height: 100vh;
18+
margin: 0;">
19+
<div style="
20+
width: 740px;
21+
height: 680px;
22+
background: #17171B;
23+
padding: 30px;
24+
position: relative;">
25+
<div style="
26+
display: flex;
27+
justify-content: center;
28+
position: relative;
29+
margin-bottom: 20px;">
30+
<div style="
31+
width: 100%;
32+
display: flex;
33+
justify-content: space-between;
34+
align-items: center;">
35+
<div style="
36+
flex: 1;
37+
border-top: 1px solid #00B493;
38+
margin: 0 10px;">
39+
40+
</div>
41+
<img src="https://dmu-dasom.or.kr/static/media/dasomLogo.c82d220d8093c3cb8d7fc0b148cafcd1.svg" alt="로고" style="
42+
width: 21px;
43+
height: 24px;
44+
border-radius: 3px;">
45+
<div style="
46+
flex: 1;
47+
border-top: 1px solid #00B493;
48+
margin: 0 10px;">
49+
</div>
50+
</div>
51+
</div>
52+
53+
<div style="
54+
font-size: 48px;
55+
font-weight: 900;
56+
color: #00B493;
57+
margin-bottom: 30px;">
58+
DASOM
59+
</div>
60+
<div style="
61+
font-size: 20px;
62+
font-weight: 600;
63+
margin-bottom: 5px;
64+
color: white !important;"
65+
th:text="${name} + '님 안녕하세요.'">
66+
</div>
67+
<div style="
68+
font-size: 20px;
69+
font-weight: 600;
70+
margin-bottom: 5px;
71+
color: white !important;">
72+
컴퓨터공학부 전공동아리 다솜입니다.
73+
</div>
74+
75+
<div style="
76+
font-size: 16px;
77+
font-weight: 400;
78+
line-height: 2.5;
79+
text-align: right;
80+
margin-bottom: 40px;
81+
color: white !important;">
82+
먼저 다솜 35기에 많은 관심을 두고 지원해 주셔서 감사드리며,<br>
83+
내부 서류 평가 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.<br>
84+
서류 전형 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다.
85+
</div>
86+
87+
<div style="display: flex;
88+
justify-content: flex-end;
89+
margin-bottom: 40px;">
90+
<a th:href="${buttonUrl}"
91+
style="
92+
background: #00B493;
93+
color: white !important;
94+
font-size: 16px;
95+
font-weight: 400;
96+
padding: 10px 20px;
97+
border-radius: 5px;
98+
display: flex;
99+
align-items: center;
100+
text-decoration: none;">
101+
<span>서류 결과 확인하기</span>
102+
<span style="
103+
border: solid white;
104+
border-width: 0 2px 2px 0;
105+
display: inline-block;
106+
padding: 5px;
107+
transform: rotate(-45deg);
108+
margin-left: 10px;">
109+
</span>
110+
</a>
111+
</div>
112+
113+
<div style="font-size: 16px;
114+
font-weight: 400;
115+
line-height: 1.5;
116+
color: white !important;">
117+
또한, 문의 사항은 본 메일에 회신 또는 아래 번호로 편하게 연락 부탁드립니다.<br>
118+
010-6361-3481
119+
</div>
120+
</div>
121+
</body>
122+
</html>

0 commit comments

Comments
 (0)