Skip to content

Commit

Permalink
Merge pull request #205 from HongYeseul/feat/notification
Browse files Browse the repository at this point in the history
[feat/notification] 댓글 작성시 이메일 알림 기능 추가
  • Loading branch information
HongYeseul authored Oct 29, 2024
2 parents a6718aa + ea57ab3 commit 44afd4b
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 4 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ dependencies {

// 모니터링을 위한 패키지
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// 이메일 알림 전송용
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}

compileJava {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import chzzk.grassdiary.domain.comment.dto.CommentResponseDTO;
import chzzk.grassdiary.domain.member.entity.Member;
import chzzk.grassdiary.domain.member.entity.MemberDAO;
import chzzk.grassdiary.domain.notification.CommentCreatedEvent;
import chzzk.grassdiary.global.common.error.exception.SystemException;
import chzzk.grassdiary.global.common.response.ClientErrorCode;
import java.util.ArrayList;
Expand All @@ -18,20 +19,29 @@
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class CommentService {

private static final int MAX_LENGTH = 70;
private static final String ELLIPSIS = "...";
private static final Logger log = LoggerFactory.getLogger(CommentService.class);

private final CommentDAO commentDAO;
private final DiaryDAO diaryDAO;
private final MemberDAO memberDAO;

private final ApplicationEventPublisher eventPublisher;

@Transactional
public CommentResponseDTO save(Long diaryId, CommentSaveRequestDTO requestDTO, Long logInMemberId) {
public CommentResponseDTO save(Long diaryId, CommentSaveRequestDTO requestDTO, final Long logInMemberId) {
Member member = getMemberById(logInMemberId);
Diary diary = getDiaryById(diaryId);
Comment parentComment = getParentCommentById(requestDTO.parentCommentId());
Expand All @@ -41,9 +51,36 @@ public CommentResponseDTO save(Long diaryId, CommentSaveRequestDTO requestDTO, L
Comment comment = requestDTO.toEntity(member, diary, parentComment, commentDepth);
commentDAO.save(comment);

// 댓글에 대한 이메일 알람
Member diaryAuthor = diary.getMember();
if (!logInMemberId.equals(diaryAuthor.getId())) {
log.info("댓글 알람 시작: {}", diaryAuthor);
String commentContent = truncateContent(comment.getContent());

CommentCreatedEvent event = new CommentCreatedEvent(this,
diaryId,
diaryAuthor.getNickname(),
diaryAuthor.getEmail(),
commentContent,
diary.getCreatedAt(),
member.getNickname(),
comment.getCreatedAt()
);
eventPublisher.publishEvent(event);
}

return CommentResponseDTO.from(comment);
}

private String truncateContent(String content) {
if (content == null) {
return "";
}
return content.length() > MAX_LENGTH
? content.substring(0, MAX_LENGTH) + ELLIPSIS
: content;
}

@Transactional
public CommentResponseDTO update(Long commentId, CommentUpdateRequestDTO requestDTO, Long logInMemberId) {
Member member = getMemberById(logInMemberId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package chzzk.grassdiary.domain.notification;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;

import java.time.LocalDateTime;

@Getter
@Slf4j
public class CommentCreatedEvent extends ApplicationEvent {
private final Long diaryId;
private final String authorName;
private final String diaryAuthorEmail;
private final String commentContent;
private final LocalDateTime diaryCreatedAt;
private final String commentCreatedBy;
private final LocalDateTime commentCreatedAt;

public CommentCreatedEvent(Object source,
Long diaryId,
String authorName,
String diaryAuthorEmail,
String commentContent,
LocalDateTime diaryCreatedAt,
String commentCreatedBy,
LocalDateTime commentCreatedAt
) {
super(source);
this.diaryId = diaryId;
this.authorName = authorName;
this.diaryAuthorEmail = diaryAuthorEmail;
this.commentContent = commentContent;
this.diaryCreatedAt = diaryCreatedAt;
this.commentCreatedBy = commentCreatedBy;
this.commentCreatedAt = commentCreatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package chzzk.grassdiary.domain.notification;

import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class EmailNotificationListener {

private final EmailService emailService;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleCommentCreatedEvent(CommentCreatedEvent event) throws MessagingException {
emailService.sendCommentNotification(event);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package chzzk.grassdiary.domain.notification;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;

@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {

private final JavaMailSender mailSender;

private final SpringTemplateEngine templateEngine;

public void sendCommentNotification(CommentCreatedEvent event) {
try {
log.info("Sending comment notification for comment {}", event);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

Context context = new Context();
context.setVariable("recipientName", event.getAuthorName());
context.setVariable("diaryDate", event.getDiaryCreatedAt());
context.setVariable("commenterName", event.getCommentCreatedBy());
context.setVariable("commentDate", event.getCommentCreatedAt());
context.setVariable("commentContent", event.getCommentContent());
context.setVariable("diaryUrl", "https://grassdiary.site/diary/" + event.getDiaryId());
// context.setVariable("unsubscribeUrl", "http://your-domain.com/unsubscribe");

String htmlContent = templateEngine.process("comment-notification", context);

helper.setTo(event.getDiaryAuthorEmail());
helper.setSubject("[잔디 일기] 새로운 댓글 알림");
helper.setText(htmlContent, true);

ClassPathResource imageResource = new ClassPathResource("static/images/grass-diary-logo.png");
helper.addInline("headerImage", imageResource);

mailSender.send(message);
log.info("{}님께 [댓글 알림] 이메일을 보냈습니다.", event.getAuthorName());

} catch (MessagingException e) {
throw new RuntimeException("댓글 이메일 보내기가 실패했습니다.", e);
}
}
}
11 changes: 10 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v1/userinfo
jwt.access.secret-key=${JWT_ACCESS_SECRET_KEY}
jwt.access.expiration=1800000
client.login-success-redirect-uri=http://localhost:3000/main
client.login-success-redirect-uri=https://grassdiary.site/main

# aws-s3-config
cloud.aws.credentials.accessKey=${S3_ACCESS_PUBLIC_KEY}
Expand All @@ -40,3 +40,12 @@ cloud.aws.stack.auto=false

# persistence context of JPA remains open until the HTTP request is completed
spring.jpa.open-in-view=false

# email
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${MAIL_USER_ID}
spring.mail.password=${MAIL_USER_PW}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.default-encoding=UTF-8
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 108 additions & 0 deletions src/main/resources/templates/comment-notification.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>새로운 댓글 알림</title>
<style>
.email-container {
max-width: 600px;
margin: 0 auto;
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: #333333;
}
.button-container {
text-align: center;
margin-top: 20px;
}
.header {
background-color: #f8f9fa;
padding: 20px;
text-align: center;
border-radius: 5px 5px 0 0;
}
.header-image {
width: 200px;
height: auto;
margin-bottom: 15px;
}
.content {
padding: 20px;
background-color: #ffffff;
border: 1px solid #e9ecef;
}
.diary-date {
font-size: 18px;
color: #495057;
margin-bottom: 15px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
}
.comment-box {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
.commenter-info {
font-weight: bold;
color: #495057;
margin-bottom: 10px;
}
.comment-text {
color: #666666;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #029764;
color: #ffffff;
text-decoration: none;
border-radius: 5px;
margin-top: 20px;
}
.footer {
text-align: center;
padding: 20px;
font-size: 12px;
color: #6c757d;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<img class="header-image" th:src="'cid:headerImage'" alt="헤더 이미지"/>
</div>

<div class="content">
<p th:text="${recipientName} + '님, 일기에 새로운 댓글이 달렸어요!'">홍길동님, 일기에 새로운 댓글이 달렸어요!</p>

<div class="diary-date">
<strong>일기 작성일:</strong>
<span th:text="${#temporals.format(diaryDate, 'yyyy년 MM월 dd일')}">2024년 03월 21일</span>
</div>

<div class="comment-box">
<div class="commenter-info">
<span th:text="${commenterName}">댓글 작성자</span>님의 댓글
<span th:text="${#temporals.format(commentDate, 'yyyy-MM-dd HH:mm')}">2024-03-21 14:30</span>
</div>
<div class="comment-text">
<p th:text="${commentContent}">댓글 내용이 여기에 표시됩니다.</p>
</div>
</div>

<div class="button-container">
<a th:href="${diaryUrl}" class="button">새로운 댓글 보러가기</a>
</div>
</div>

<div class="footer">
<p>본 메일은 자동으로 발송되는 알림 메일입니다.</p>
<!-- <p>더 이상 알림을 받고 싶지 않으시다면 <a th:href="${unsubscribeUrl}">여기</a>를 클릭하세요.</p>-->
</div>
</div>
</body>
</html>

0 comments on commit 44afd4b

Please sign in to comment.