Skip to content

Commit cdfb3f6

Browse files
authored
Merge pull request #199 from prgrms-web-devcourse-final-project/Feat/188
Feat: 알림 발생 지점 연동 (#188)
2 parents a4528bf + 0ad11e7 commit cdfb3f6

30 files changed

+1044
-29
lines changed

src/main/java/com/back/domain/board/comment/service/CommentService.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
import com.back.domain.board.post.entity.Post;
1010
import com.back.domain.board.comment.repository.CommentRepository;
1111
import com.back.domain.board.post.repository.PostRepository;
12+
import com.back.domain.notification.event.community.CommentCreatedEvent;
13+
import com.back.domain.notification.event.community.ReplyCreatedEvent;
1214
import com.back.domain.user.entity.User;
1315
import com.back.domain.user.repository.UserRepository;
1416
import com.back.global.exception.CustomException;
1517
import com.back.global.exception.ErrorCode;
1618
import lombok.RequiredArgsConstructor;
19+
import org.springframework.context.ApplicationEventPublisher;
1720
import org.springframework.data.domain.Page;
1821
import org.springframework.data.domain.Pageable;
1922
import org.springframework.stereotype.Service;
@@ -26,6 +29,7 @@ public class CommentService {
2629
private final CommentRepository commentRepository;
2730
private final UserRepository userRepository;
2831
private final PostRepository postRepository;
32+
private final ApplicationEventPublisher eventPublisher;
2933

3034
/**
3135
* 댓글 생성 서비스
@@ -48,6 +52,18 @@ public CommentResponse createComment(Long postId, CommentRequest request, Long u
4852

4953
// Comment 저장 및 응답 반환
5054
commentRepository.save(comment);
55+
56+
// 댓글 작성 이벤트 발행
57+
eventPublisher.publishEvent(
58+
new CommentCreatedEvent(
59+
userId, // 댓글 작성자
60+
post.getUser().getId(), // 게시글 작성자
61+
postId,
62+
comment.getId(),
63+
request.content()
64+
)
65+
);
66+
5167
return CommentResponse.from(comment);
5268
}
5369

@@ -159,6 +175,19 @@ public ReplyResponse createReply(Long postId, Long parentCommentId, CommentReque
159175

160176
// 저장 및 응답 반환
161177
commentRepository.save(reply);
178+
179+
// 대댓글 작성 이벤트 발행
180+
eventPublisher.publishEvent(
181+
new ReplyCreatedEvent(
182+
userId, // 대댓글 작성자
183+
parent.getUser().getId(), // 댓글 작성자
184+
postId,
185+
parentCommentId,
186+
reply.getId(),
187+
request.content()
188+
)
189+
);
190+
162191
return ReplyResponse.from(reply);
163192
}
164193
}

src/main/java/com/back/domain/notification/controller/NotificationController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
@RestController
3030
@RequiredArgsConstructor
3131
@RequestMapping("/api/notifications")
32-
@Tag(name = "알림", description = "알림 관련 API")
32+
@Tag(name = "Notification API", description = "알림 관련 API")
3333
public class NotificationController {
3434

3535
private final NotificationService notificationService;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.back.domain.notification.event.community;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class CommentCreatedEvent extends CommunityNotificationEvent {
7+
private final Long postId;
8+
private final Long commentId;
9+
private final String commentContent;
10+
11+
public CommentCreatedEvent(Long actorId, Long postAuthorId, Long postId,
12+
Long commentId, String commentContent) {
13+
super(
14+
actorId,
15+
postAuthorId,
16+
postId,
17+
"새 댓글",
18+
"회원님의 게시글에 댓글이 달렸습니다"
19+
);
20+
this.postId = postId;
21+
this.commentId = commentId;
22+
this.commentContent = commentContent;
23+
}
24+
25+
public String getContentPreview() {
26+
if (commentContent == null || commentContent.isEmpty()) {
27+
return "";
28+
}
29+
return commentContent.length() > 50
30+
? commentContent.substring(0, 50) + "..."
31+
: commentContent;
32+
}
33+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.back.domain.notification.event.community;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class CommentLikedEvent extends CommunityNotificationEvent {
7+
private final Long postId;
8+
private final Long commentId;
9+
private final String commentContent;
10+
11+
public CommentLikedEvent(Long actorId, Long commentAuthorId, Long postId,
12+
Long commentId, String commentContent) {
13+
super(
14+
actorId,
15+
commentAuthorId,
16+
commentId,
17+
"좋아요",
18+
"회원님의 댓글을 좋아합니다"
19+
);
20+
this.postId = postId;
21+
this.commentId = commentId;
22+
this.commentContent = commentContent;
23+
}
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.back.domain.notification.event.community;
2+
3+
import com.back.domain.notification.entity.NotificationType;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public abstract class CommunityNotificationEvent {
8+
private final Long actorId; // 행동한 사람 (댓글 작성자, 좋아요 누른 사람)
9+
private final Long receiverId; // 알림 받을 사람 (게시글/댓글 작성자)
10+
private final NotificationType targetType = NotificationType.COMMUNITY;
11+
private final Long targetId; // 게시글 ID 또는 댓글 ID
12+
private final String title;
13+
private final String content;
14+
15+
protected CommunityNotificationEvent(Long actorId, Long receiverId, Long targetId,
16+
String title, String content) {
17+
this.actorId = actorId;
18+
this.receiverId = receiverId;
19+
this.targetId = targetId;
20+
this.title = title;
21+
this.content = content;
22+
}
23+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.back.domain.notification.event.community;
2+
3+
import com.back.domain.notification.service.NotificationService;
4+
import com.back.domain.user.entity.User;
5+
import com.back.domain.user.repository.UserRepository;
6+
import com.back.global.exception.CustomException;
7+
import com.back.global.exception.ErrorCode;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.context.event.EventListener;
11+
import org.springframework.scheduling.annotation.Async;
12+
import org.springframework.stereotype.Component;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
@Slf4j
17+
public class CommunityNotificationEventListener {
18+
19+
private final NotificationService notificationService;
20+
private final UserRepository userRepository;
21+
22+
// 댓글 작성 시 - 게시글 작성자에게 알림
23+
@EventListener
24+
@Async("notificationExecutor")
25+
public void handleCommentCreated(CommentCreatedEvent event) {
26+
log.info("[알림] 댓글 작성: postId={}, commentId={}, actorId={}",
27+
event.getPostId(), event.getCommentId(), event.getActorId());
28+
29+
try {
30+
31+
User actor = userRepository.findById(event.getActorId())
32+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
33+
34+
User receiver = userRepository.findById(event.getReceiverId())
35+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
36+
37+
notificationService.createCommunityNotification(
38+
receiver,
39+
actor,
40+
event.getTitle(),
41+
event.getContent(),
42+
"/posts/" + event.getPostId()
43+
);
44+
45+
log.info("[알림] 댓글 작성 알림 전송 완료");
46+
47+
} catch (Exception e) {
48+
log.error("[알림] 댓글 작성 알림 전송 실패: error={}", e.getMessage(), e);
49+
}
50+
}
51+
52+
// 대댓글 작성 시 - 댓글 작성자에게 알림
53+
@EventListener
54+
@Async("notificationExecutor")
55+
public void handleReplyCreated(ReplyCreatedEvent event) {
56+
log.info("[알림] 대댓글 작성: parentCommentId={}, replyId={}, actorId={}",
57+
event.getParentCommentId(), event.getReplyId(), event.getActorId());
58+
59+
try {
60+
61+
User actor = userRepository.findById(event.getActorId())
62+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
63+
64+
User receiver = userRepository.findById(event.getReceiverId())
65+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
66+
67+
notificationService.createCommunityNotification(
68+
receiver,
69+
actor,
70+
event.getTitle(),
71+
event.getContent(),
72+
"/posts/" + event.getPostId() + "#comment-" + event.getParentCommentId()
73+
);
74+
75+
log.info("[알림] 대댓글 작성 알림 전송 완료");
76+
77+
} catch (Exception e) {
78+
log.error("[알림] 대댓글 작성 알림 전송 실패: error={}", e.getMessage(), e);
79+
}
80+
}
81+
82+
// 게시글 좋아요 시 - 게시글 작성자에게 알림
83+
@EventListener
84+
@Async("notificationExecutor")
85+
public void handlePostLiked(PostLikedEvent event) {
86+
log.info("[알림] 게시글 좋아요: postId={}, actorId={}",
87+
event.getPostId(), event.getActorId());
88+
89+
try {
90+
91+
User actor = userRepository.findById(event.getActorId())
92+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
93+
94+
User receiver = userRepository.findById(event.getReceiverId())
95+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
96+
97+
notificationService.createCommunityNotification(
98+
receiver,
99+
actor,
100+
event.getTitle(),
101+
event.getContent(),
102+
"/posts/" + event.getPostId()
103+
);
104+
105+
log.info("[알림] 게시글 좋아요 알림 전송 완료");
106+
107+
} catch (Exception e) {
108+
log.error("[알림] 게시글 좋아요 알림 전송 실패: error={}", e.getMessage(), e);
109+
}
110+
}
111+
112+
// 댓글 좋아요 시 - 댓글 작성자에게 알림
113+
@EventListener
114+
@Async("notificationExecutor")
115+
public void handleCommentLiked(CommentLikedEvent event) {
116+
log.info("[알림] 댓글 좋아요: commentId={}, actorId={}",
117+
event.getCommentId(), event.getActorId());
118+
119+
try {
120+
121+
User actor = userRepository.findById(event.getActorId())
122+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
123+
124+
User receiver = userRepository.findById(event.getReceiverId())
125+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
126+
127+
notificationService.createCommunityNotification(
128+
receiver,
129+
actor,
130+
event.getTitle(),
131+
event.getContent(),
132+
"/posts/" + event.getPostId() + "#comment-" + event.getCommentId()
133+
);
134+
135+
log.info("[알림] 댓글 좋아요 알림 전송 완료");
136+
137+
} catch (Exception e) {
138+
log.error("[알림] 댓글 좋아요 알림 전송 실패: error={}", e.getMessage(), e);
139+
}
140+
}
141+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.notification.event.community;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class PostLikedEvent extends CommunityNotificationEvent {
7+
private final Long postId;
8+
private final String postTitle;
9+
10+
public PostLikedEvent(Long actorId, Long postAuthorId, Long postId, String postTitle) {
11+
super(
12+
actorId,
13+
postAuthorId,
14+
postId,
15+
"좋아요",
16+
"회원님의 게시글을 좋아합니다"
17+
);
18+
this.postId = postId;
19+
this.postTitle = postTitle;
20+
}
21+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.back.domain.notification.event.community;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class ReplyCreatedEvent extends CommunityNotificationEvent {
7+
private final Long postId;
8+
private final Long parentCommentId;
9+
private final Long replyId;
10+
private final String replyContent;
11+
12+
public ReplyCreatedEvent(Long actorId, Long commentAuthorId, Long postId,
13+
Long parentCommentId, Long replyId, String replyContent) {
14+
super(
15+
actorId,
16+
commentAuthorId,
17+
parentCommentId,
18+
"새 대댓글",
19+
"회원님의 댓글에 답글이 달렸습니다"
20+
);
21+
this.postId = postId;
22+
this.parentCommentId = parentCommentId;
23+
this.replyId = replyId;
24+
this.replyContent = replyContent;
25+
}
26+
27+
public String getContentPreview() {
28+
if (replyContent == null || replyContent.isEmpty()) {
29+
return "";
30+
}
31+
return replyContent.length() > 50
32+
? replyContent.substring(0, 50) + "..."
33+
: replyContent;
34+
}
35+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.back.domain.notification.event.study;
2+
3+
import lombok.Getter;
4+
5+
import java.time.LocalDate;
6+
7+
@Getter
8+
public class DailyGoalAchievedEvent extends StudyNotificationEvent {
9+
private final LocalDate achievedDate;
10+
private final int completedPlans;
11+
private final int totalPlans;
12+
13+
public DailyGoalAchievedEvent(Long userId, LocalDate achievedDate,
14+
int completedPlans, int totalPlans) {
15+
super(
16+
userId,
17+
"일일 목표 달성 🎉",
18+
String.format("오늘의 학습 계획을 모두 완료했습니다! (%d/%d)",
19+
completedPlans, totalPlans)
20+
);
21+
this.achievedDate = achievedDate;
22+
this.completedPlans = completedPlans;
23+
this.totalPlans = totalPlans;
24+
}
25+
}

0 commit comments

Comments
 (0)