노약자를 위한 AI 말동무 서비스, POPPET 서비스의 개발 일대기입니다.
사용자에게 노약자가 특정 기간동안 AI와 대화한 내용의 요약본을 이메일로 전송하는 기능을 구현했습니다.
https://velog.io/@dooo_it_ly/SpringBoot-JavaMailSender을-이용한-비동기-이메일-발송-기능-구현
👆앞선 포스트👆에서 이메일 발송 기능 구현을 다루었습니다.
추가적으로 이메일을 전송하는 과정에서 예쁜 템플릿을 적용하기 위해 디자이너님께서 피그마로 열심히 템플릿을 만들어주셨습니다.
따라서, 저는 그 템플릿에 동적으로 텍스트를 작성해서 이메일을 발송해야 했는데요, 이 템플릿을 HTML/CSS 파일로 저장하면 손쉽게 특정 태그의 값을 변경해서 전송할 수 있지만 아쉽게도 HTML파일로 저장할 수 있는 환경이 아니었습니다.
그래서 차안으로, 그 템플릿 파일을 PDF로 저장한 다음 동적으로 직접 글씨를 작성하는 방법을 택했습니다.
동적 텍스트 작성을 위해 사용한 라이브러리는 ApachePdfBox입니다.
implementation 'org.apache.pdfbox:pdfbox:2.0.29' # pdf 편집을 위함
implementation 'org.apache.pdfbox:pdfbox-tools:2.0.29' # pdf -> img 변환을 위함
텍스트 작성을 위한 주요 흐름은 다음과 같습니다
PDF를 이미지로 변경하는 이유는, 이메일 본문에 인라인 이미지로 삽입하기 위함입니다!
각 흐름에 대해서 하나씩 천천히 살펴보겠습니다
public class EmailTemplateService {
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final int fontSize = 18;
public ByteArrayResource makeEmailBackground(User user, ChatRoom chatRoom) {
// 배경용 PDF 파일 로드
PDDocument doc = loadPDDocument("email/email_background.pdf");
// 배경용 PDF 편집
drawBackgroundImg(doc, user, chatRoom);
// 이미지로 변환 후 반환
return parsePdfToImg(doc);
}
...
}
resources/email/email_background.pdf
로 저장합니다private PDDocument loadPDDocument(String path){
ClassPathResource pdfResource = new ClassPathResource(path);
try (InputStream inputStream = pdfResource.getInputStream()) {
return PDDocument.load(inputStream);
} catch (IOException e) {
log.error("[*] 이메일 배경 PDF 파일 로드 중 오류 발생, {}", e.getMessage());
return null;
}
}
PDPageContentSteream
이라는 객체를 통해 진행됩니다.ContentStream
이라는 클래스를 만들어 관리했습니다.@Getter
public class ContentStream {
private PDPageContentStream pageContentStream;
private PDFont font;
private float fontSpace;
public ContentStream(PDPageContentStream pageContentStream, PDFont font) {
this.pageContentStream = pageContentStream;
this.font = font;
}
public void setFontSize(int fontSize){
try {
pageContentStream.setFont(font, fontSize);
this.fontSpace = (float)(fontSize + 2)/2; # 글씨 여백을 위함
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void setColor(float r, float g, float b){
try {
this.pageContentStream.setNonStrokingColor(r, g, b);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 글씨 작성을 위한 함수 (1줄)
public void writeText(float x, float y, String text){
try {
pageContentStream.beginText();
pageContentStream.newLineAtOffset(x, y + fontSpace);
pageContentStream.showText(text);
pageContentStream.endText();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 글씨 작성을 위한 함수 (줄바꿈)
public void writeWrappedText(float x, float y, int lineHeight, List<String> texts){
try {
pageContentStream.beginText();
pageContentStream.newLineAtOffset(x, y + fontSpace);
for (String text : texts) {
pageContentStream.showText(text);
pageContentStream.newLineAtOffset(0, -lineHeight);
}
pageContentStream.endText();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void close(){
try {
this.pageContentStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
정말 본격적으로, 배경 PDF 파일에 글씨를 작성하는 과정은 다음과 같습니다
pageContentStream
생성resources/fonts/
에 첨부한 폰트 파일을 가져와 PDFont
타입 변수 생성ContentStream
생성 private void drawBackgroundImg(PDDocument doc, User user, ChatRoom chatRoom) {
PDPage page = doc.getPage(0);
try {
PDPageContentStream pageContentStream = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true);
InputStream fontFile = loadFontFile("fonts/Pretendard.ttf");
PDFont font = PDType0Font.load(doc, fontFile);
ContentStream contentStream = new ContentStream(pageContentStream, font);
contentStream.setFontSize(fontSize);
contentStream.setColor(0.3f, 0.3f, 0.3f);
drawUserInfo(contentStream, user.getUsername(), user.getEmailPeriod().getValue());
drawDateTime(contentStream, chatRoom.getCreatedAt());
drawSummary(contentStream, chatRoom.getSummary());
contentStream.close();
} catch (IOException e) {
log.error("[*] 이메일 배경 PDF 편집 중 오류 발생, {}", e.getMessage());
}
}
이제 draw@@ 함수를 이용해 정해진 위치에 글씨를 작성하면 됩니다. 아래는 관련 함수 중 한 함수의 코드입니다.
writeText()라는 함수를 통해, 작성할 글씨의 X값, Y값, 작성할 글씨 텍스트 내용을 인자로 넣으면 글씨를 작성할 수 있습니다.
여기서 사용할 X, Y의 값은 피그마에서 확인할 수 있습니다.
ApachePdfBox의 좌표는 왼쪽 아래 모서리를 기준으로 합니다.
따라서, 피그마에서 대지 기준 요소의 위치를 확인해 수기로 위치를 넣어줍니다 ㅎㅎ. .. ㅎㅎ 때문에 1x으로 배경 요소를 추출하는 것이 중요합니다
private void drawDateTime(ContentStream contentStream, LocalDateTime createdAt) {
int createdAtX = 308;
int createdAtY = 488;
int nowY = 454;
contentStream.writeText(createdAtX, createdAtY, dateTimeFormatter.format(createdAt));
contentStream.writeText(createdAtX, nowY, dateTimeFormatter.format(LocalDateTime.now()));
}
ByteArrayResource
) 형태로 반환합니다PDDocument.load()
함수를 사용하기 위해 라이브러리 2.x 버전을 사용했습니다private ByteArrayResource parsePdfToImg(PDDocument doc) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
doc.close();
// 메모리에 파일 저장
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
PDDocument loadedDoc = PDDocument.load(bais);
// 파일 로드 후 이미지로 변환
PDFRenderer renderer = new PDFRenderer(loadedDoc);
BufferedImage image = renderer.renderImageWithDPI(0, 300);
// 이미지 output 로드
ByteArrayOutputStream imageOut = new ByteArrayOutputStream();
ImageIO.write(image, "png", imageOut);
ByteArrayResource imageResource = new ByteArrayResource(imageOut.toByteArray());
loadedDoc.close();
return imageResource;
} catch (IOException e) {
log.error("[*] 이메일 배경 PDF을 이미지로 변환 중 오류 발생, {}", e.getMessage());
return null;
}
}
기존에 작성했던 이메일 발송 서비스 코드에 배경 적용 코드를 추가 및 수정했습니다
EmailSendEventListener
에서 이미지 배경 생성 함수 호출EmailSenderService
를 통해 각 이메일에 이미지를 인라인으로 첨부해 전송하도록 수정@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEmailSendEvent(EmailSendEvent event) {
User user = event.getUser();
ChatRoom chatRoom = event.getChatRoom();
// 배경 이미지 생성 함수 호출
ByteArrayResource body = emailTemplateService.makeEmailBackground(user, chatRoom);
for (Email email : user.getEmails()) {
try {
emailSendService.sendEmail(email.getEmailAddress(), body);
} catch (Exception e) {
log.error("[*] {}으로 이메일 전송 중 오류 발생", email.getEmailAddress(), e);
}
}
}
public void sendEmail(String to, ByteArrayResource body) throws MessagingException {
String subject = "POPPET";
// message 설정
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); // multipart 첨부 가능하도록 설정
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
// image 배경 설정
mimeMessageHelper.setText("<html><body><img src='cid:image' style='width:1200px; height:auto;'/></body></html>", true);
mimeMessageHelper.addInline("image", body, "image/png");
// mail 전송
mailSender.send(mimeMessage);
}
템플릿을 적용한 결과입니다. 이미지가 본문에 바로 적용되었습니다!
작성된 텍스트는 모두 테스트를 위한 더미이니 무시해주세요 ㅎㅎ
ApachePdfBox로는 이보다 더 다양한 draw 기능들이 있으니 이러한 동적 텍스트 작성 기능을 구현해야 하시는 경우, 해당 라이브러리를 한 번쯤 사용해보셔도 좋을 것 같습니다