Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 이메일 템플릿 하드코딩 개선 및 외부 관리로 전환 완료 #580

Merged
merged 7 commits into from
Oct 1, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void processApplicationMemberCreated(ApplicationMemberCreationDto dto) {
String finalPassword = manageMemberPasswordUseCase.generateOrRetrievePassword(member.getPassword());
member.updatePassword(finalPassword, passwordEncoder);
registerMemberPort.save(member);
emailService.broadcastEmailToApprovedMember(member, finalPassword);
emailService.sendAccountCreationEmail(member, finalPassword);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public String resendMemberPassword(String memberId) {
member.updatePassword(newPassword, passwordEncoder);
registerMemberPort.save(member);

emailService.broadcastEmailToApprovedMember(member, newPassword);
emailService.sendNewPasswordEmail(member, newPassword);
return member.getId();
}

Expand All @@ -45,7 +45,7 @@ public String requestMemberPasswordReset(MemberResetPasswordRequestDto requestDt
Member member = validateResetPasswordRequest(requestDto);
String code = verificationService.generateVerificationCode();
verificationService.saveVerificationCode(member.getId(), code);
emailService.sendPasswordResetEmail(member, code);
emailService.sendPasswordResetCodeEmail(member, code);
return member.getId();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public String registerMember(MemberRequestDto requestDto) {
member.updatePassword(finalPassword, passwordEncoder);
registerMemberPort.save(member);
createPositionByMember(member);
emailService.broadcastEmailToApprovedMember(member, finalPassword);
emailService.sendAccountCreationEmail(member, finalPassword);
return member.getId();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ public void sendBatchEmail(List<EmailTask> emailTasks) throws MessagingException
messageHelper.setTo(task.getTo());
messageHelper.setSubject(task.getSubject());
messageHelper.setText(task.getContent(), true);

setImageInTemplate(messageHelper, task.getTemplateType());

if (task.getFiles() != null) {
for (File file : task.getFiles()) {
messageHelper.addAttachment(MimeUtility.encodeText(file.getName(), "UTF-8", "B"), file);
Expand All @@ -91,7 +93,7 @@ public void sendBatchEmail(List<EmailTask> emailTasks) throws MessagingException
}

private void setImageInTemplate(MimeMessageHelper messageHelper, EmailTemplateType templateType) throws MessagingException {
if (Objects.requireNonNull(templateType) == EmailTemplateType.NORMAL) {
if (Objects.equals(templateType.getTemplateName(), "clabEmail.html")) {
messageHelper.addInline("image-1", new ClassPathResource("images/image-1.png"));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,41 @@
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import page.clab.api.domain.memberManagement.member.domain.Member;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.global.common.email.domain.EmailTemplateType;
import page.clab.api.global.common.email.dto.request.EmailDto;
import page.clab.api.global.common.email.exception.MessageSendingFailedException;
import page.clab.api.global.util.FileUtil;
import page.clab.api.global.config.EmailTemplateProperties;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {

private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final SpringTemplateEngine springTemplateEngine;
private final EmailAsyncService emailAsyncService;
private final EmailTemplateProperties emailTemplateProperties;
private final SpringTemplateEngine springTemplateEngine;

@Value("${resource.file.path}")
private String filePath;

public List<String> broadcastEmail(EmailDto emailDto, List<MultipartFile> multipartFiles) {
List<File> convertedFiles = multipartFiles != null && !multipartFiles.isEmpty()
? convertMultipartFiles(multipartFiles)
: new ArrayList<>();

List<String> successfulAddresses = Collections.synchronizedList(new ArrayList<>());

emailDto.getTo().parallelStream().forEach(address -> {
try {
Member recipient = externalRetrieveMemberUseCase.findByEmail(address);
String emailContent = generateEmailContent(emailDto, recipient.getName());
emailAsyncService.sendEmailAsync(address, emailDto.getSubject(), emailContent, convertedFiles, emailDto.getEmailTemplateType());
successfulAddresses.add(address);
} catch (MessagingException e) {
throw new MessageSendingFailedException(address + "에게 이메일을 보내는데 실패했습니다.");
}
});
return successfulAddresses;
}
public void sendAccountCreationEmail(Member member, String password) {
EmailTemplateProperties.Template template = emailTemplateProperties.getTemplate(EmailTemplateType.ACCOUNT_CREATION);

String subject = template.getSubject();
String content = template.getContent()
.replace("{{id}}", member.getId())
.replace("{{password}}", password);

EmailDto emailDto = EmailDto.create(
List.of(member.getEmail()),
subject,
content,
EmailTemplateType.ACCOUNT_CREATION
);

public void broadcastEmailToApprovedMember(Member member, String password) {
String subject = "C-Lab 계정 발급 안내";
String content = """
정식으로 C-Lab의 일원이 된 것을 축하드립니다.
C-Lab과 함께하는 동안 불타는 열정으로 모든 원하는 목표를 이루어 내시기를 바라고,
훗날, 당신의 합류가 C-Lab에겐 최고의 행운이었다고 기억되기를 희망합니다.

로그인을 위해 아래의 계정 정보를 확인해주세요.
ID: %s
Password: %s
로그인 후 비밀번호를 변경해주세요.
""".formatted(member.getId(), password);
EmailDto emailDto = EmailDto.create(List.of(member.getEmail()), subject, content, EmailTemplateType.NORMAL);
try {
String emailContent = generateEmailContent(emailDto, member.getName());
emailAsyncService.sendEmailAsync(member.getEmail(), emailDto.getSubject(), emailContent, null, emailDto.getEmailTemplateType());
Expand All @@ -75,45 +46,49 @@ public void broadcastEmailToApprovedMember(Member member, String password) {
}
}

public void sendPasswordResetEmail(Member member, String code) {
String subject = "C-Lab 비밀번호 재발급 인증 안내";
String content = """
C-Lab 비밀번호 재발급 인증 안내 메일입니다.
인증번호는 %s입니다.
해당 인증번호를 비밀번호 재설정 페이지에 입력해주세요.
재설정시 비밀번호는 인증번호로 대체됩니다.
""".formatted(code);
EmailDto emailDto = EmailDto.create(List.of(member.getEmail()), subject, content, EmailTemplateType.NORMAL);
public void sendPasswordResetCodeEmail(Member member, String code) {
EmailTemplateProperties.Template template = emailTemplateProperties.getTemplate(EmailTemplateType.PASSWORD_RESET_CODE);

String subject = template.getSubject();
String content = template.getContent()
.replace("{{code}}", code);

EmailDto emailDto = EmailDto.create(
List.of(member.getEmail()),
subject,
content,
EmailTemplateType.PASSWORD_RESET_CODE
);

try {
broadcastEmail(emailDto, null);
String emailContent = generateEmailContent(emailDto, member.getName());
emailAsyncService.sendEmailAsync(member.getEmail(), emailDto.getSubject(), emailContent, null, emailDto.getEmailTemplateType());
} catch (Exception e) {
throw new MessageSendingFailedException(member.getEmail() + " 비밀번호 재발급 인증 메일 전송에 실패했습니다.");
}
}

private List<File> convertMultipartFiles(List<MultipartFile> multipartFiles) {
List<File> convertedFiles = new ArrayList<>();
for (MultipartFile multipartFile : multipartFiles) {
File file = convertMultipartFileToFile(multipartFile);
convertedFiles.add(file);
}
return convertedFiles;
}
public void sendNewPasswordEmail(Member member, String newPassword) {
EmailTemplateProperties.Template template = emailTemplateProperties.getTemplate(EmailTemplateType.NEW_PASSWORD);

private File convertMultipartFileToFile(MultipartFile multipartFile) {
String originalFilename = multipartFile.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
String path = filePath + File.separator + "temp" + File.separator + FileUtil.makeFileName(extension);
path = path.replace("/", File.separator).replace("\\", File.separator);
File file = new File(path);
FileUtil.ensureParentDirectoryExists(file, filePath);
String subject = template.getSubject();
String content = template.getContent()
.replace("{{id}}", member.getId())
.replace("{{password}}", newPassword);

EmailDto emailDto = EmailDto.create(
List.of(member.getEmail()),
subject,
content,
EmailTemplateType.NEW_PASSWORD
);

try {
multipartFile.transferTo(file);
} catch (IOException e) {
throw new RuntimeException("Failed to convert MultipartFile to File", e);
String emailContent = generateEmailContent(emailDto, member.getName());
emailAsyncService.sendEmailAsync(member.getEmail(), emailDto.getSubject(), emailContent, null, emailDto.getEmailTemplateType());
} catch (MessagingException e) {
throw new MessageSendingFailedException(member.getEmail() + " 비밀번호 재설정 안내 메일 전송에 실패했습니다.");
}
return file;
}

private String generateEmailContent(EmailDto emailDto, String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package page.clab.api.global.common.email.domain;

import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum EmailTemplateType {

NORMAL("NORMAL", "기본", "clabEmail.html");
ACCOUNT_CREATION("account-creation", "계정 생성", "clabEmail.html"),
PASSWORD_RESET_CODE("password-reset-code", "비밀번호 재설정", "clabEmail.html"),
NEW_PASSWORD("new-password", "새 비밀번호", "clabEmail.html");

@Enumerated(EnumType.STRING)
private final String key;
private final String description;
private final String templateName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class EmailDto {
private String content;

@NotNull(message = "{notNull.email.templateType}")
@Schema(description = "이메일 템플릿", example = "NORMAL")
@Schema(description = "이메일 템플릿", example = "ACCOUNT_CREATION")
private EmailTemplateType emailTemplateType;

public static EmailDto create(List<String> to, String subject, String content, EmailTemplateType emailTemplateType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package page.clab.api.global.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import page.clab.api.global.common.email.domain.EmailTemplateType;

import java.util.Map;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "email")
public class EmailTemplateProperties {

private Map<String, Template> templates;

public Template getTemplate(EmailTemplateType templateType) {
Template template = templates.get(templateType.getKey());
if (template == null) {
throw new IllegalArgumentException("템플릿을 찾을 수 없습니다: " + templateType);
}
return template;
}

@Getter
@Setter
public static class Template {
private String subject;
private String content;
}
}
2 changes: 1 addition & 1 deletion src/main/resources/templates/clabEmail.html
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ <h1 class="v-font-size" style="margin: 0px; color: #2a2c32; line-height: 140%; t
<td class="v-container-padding-padding" style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:'Montserrat',sans-serif;" align="center">

<div class="v-font-size" style="font-size: 14px; color: #908f8f; line-height: 200%; text-align: center; word-wrap: break-word;">
<p style="line-height: 200%;">C-Lab  |  경기대학교 컴퓨터공학부 개발보안동아리</p>
<p style="line-height: 200%;">C-Lab  |  경기대학교 AI컴퓨터공학부 개발동아리</p>
</div>

</td>
Expand Down