Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
import com.back.domain.board.post.entity.Post;
import com.back.domain.board.comment.repository.CommentRepository;
import com.back.domain.board.post.repository.PostRepository;
import com.back.domain.notification.event.community.CommentCreatedEvent;
import com.back.domain.notification.event.community.ReplyCreatedEvent;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand All @@ -26,6 +29,7 @@ public class CommentService {
private final CommentRepository commentRepository;
private final UserRepository userRepository;
private final PostRepository postRepository;
private final ApplicationEventPublisher eventPublisher;

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

// Comment 저장 및 응답 반환
commentRepository.save(comment);

// 댓글 작성 이벤트 발행
eventPublisher.publishEvent(
new CommentCreatedEvent(
userId, // 댓글 작성자
post.getUser().getId(), // 게시글 작성자
postId,
comment.getId(),
request.content()
)
);

return CommentResponse.from(comment);
}

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

// 저장 및 응답 반환
commentRepository.save(reply);

// 대댓글 작성 이벤트 발행
eventPublisher.publishEvent(
new ReplyCreatedEvent(
userId, // 대댓글 작성자
parent.getUser().getId(), // 댓글 작성자
postId,
parentCommentId,
reply.getId(),
request.content()
)
);

return ReplyResponse.from(reply);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/notifications")
@Tag(name = "알림", description = "알림 관련 API")
@Tag(name = "Notification API", description = "알림 관련 API")
public class NotificationController {

private final NotificationService notificationService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.notification.event.community;

import lombok.Getter;

@Getter
public class CommentCreatedEvent extends CommunityNotificationEvent {
private final Long postId;
private final Long commentId;
private final String commentContent;

public CommentCreatedEvent(Long actorId, Long postAuthorId, Long postId,
Long commentId, String commentContent) {
super(
actorId,
postAuthorId,
postId,
"새 댓글",
"회원님의 게시글에 댓글이 달렸습니다"
);
this.postId = postId;
this.commentId = commentId;
this.commentContent = commentContent;
}

public String getContentPreview() {
if (commentContent == null || commentContent.isEmpty()) {
return "";
}
return commentContent.length() > 50
? commentContent.substring(0, 50) + "..."
: commentContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.back.domain.notification.event.community;

import lombok.Getter;

@Getter
public class CommentLikedEvent extends CommunityNotificationEvent {
private final Long postId;
private final Long commentId;
private final String commentContent;

public CommentLikedEvent(Long actorId, Long commentAuthorId, Long postId,
Long commentId, String commentContent) {
super(
actorId,
commentAuthorId,
commentId,
"좋아요",
"회원님의 댓글을 좋아합니다"
);
this.postId = postId;
this.commentId = commentId;
this.commentContent = commentContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.back.domain.notification.event.community;

import com.back.domain.notification.entity.NotificationType;
import lombok.Getter;

@Getter
public abstract class CommunityNotificationEvent {
private final Long actorId; // 행동한 사람 (댓글 작성자, 좋아요 누른 사람)
private final Long receiverId; // 알림 받을 사람 (게시글/댓글 작성자)
private final NotificationType targetType = NotificationType.COMMUNITY;
private final Long targetId; // 게시글 ID 또는 댓글 ID
private final String title;
private final String content;

protected CommunityNotificationEvent(Long actorId, Long receiverId, Long targetId,
String title, String content) {
this.actorId = actorId;
this.receiverId = receiverId;
this.targetId = targetId;
this.title = title;
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.back.domain.notification.event.community;

import com.back.domain.notification.service.NotificationService;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class CommunityNotificationEventListener {

private final NotificationService notificationService;
private final UserRepository userRepository;

// 댓글 작성 시 - 게시글 작성자에게 알림
@EventListener
@Async("notificationExecutor")
public void handleCommentCreated(CommentCreatedEvent event) {
log.info("[알림] 댓글 작성: postId={}, commentId={}, actorId={}",
event.getPostId(), event.getCommentId(), event.getActorId());

try {

User actor = userRepository.findById(event.getActorId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

User receiver = userRepository.findById(event.getReceiverId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

notificationService.createCommunityNotification(
receiver,
actor,
event.getTitle(),
event.getContent(),
"/posts/" + event.getPostId()
);

log.info("[알림] 댓글 작성 알림 전송 완료");

} catch (Exception e) {
log.error("[알림] 댓글 작성 알림 전송 실패: error={}", e.getMessage(), e);
}
}

// 대댓글 작성 시 - 댓글 작성자에게 알림
@EventListener
@Async("notificationExecutor")
public void handleReplyCreated(ReplyCreatedEvent event) {
log.info("[알림] 대댓글 작성: parentCommentId={}, replyId={}, actorId={}",
event.getParentCommentId(), event.getReplyId(), event.getActorId());

try {

User actor = userRepository.findById(event.getActorId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

User receiver = userRepository.findById(event.getReceiverId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

notificationService.createCommunityNotification(
receiver,
actor,
event.getTitle(),
event.getContent(),
"/posts/" + event.getPostId() + "#comment-" + event.getParentCommentId()
);

log.info("[알림] 대댓글 작성 알림 전송 완료");

} catch (Exception e) {
log.error("[알림] 대댓글 작성 알림 전송 실패: error={}", e.getMessage(), e);
}
}

// 게시글 좋아요 시 - 게시글 작성자에게 알림
@EventListener
@Async("notificationExecutor")
public void handlePostLiked(PostLikedEvent event) {
log.info("[알림] 게시글 좋아요: postId={}, actorId={}",
event.getPostId(), event.getActorId());

try {

User actor = userRepository.findById(event.getActorId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

User receiver = userRepository.findById(event.getReceiverId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

notificationService.createCommunityNotification(
receiver,
actor,
event.getTitle(),
event.getContent(),
"/posts/" + event.getPostId()
);

log.info("[알림] 게시글 좋아요 알림 전송 완료");

} catch (Exception e) {
log.error("[알림] 게시글 좋아요 알림 전송 실패: error={}", e.getMessage(), e);
}
}

// 댓글 좋아요 시 - 댓글 작성자에게 알림
@EventListener
@Async("notificationExecutor")
public void handleCommentLiked(CommentLikedEvent event) {
log.info("[알림] 댓글 좋아요: commentId={}, actorId={}",
event.getCommentId(), event.getActorId());

try {

User actor = userRepository.findById(event.getActorId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

User receiver = userRepository.findById(event.getReceiverId())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

notificationService.createCommunityNotification(
receiver,
actor,
event.getTitle(),
event.getContent(),
"/posts/" + event.getPostId() + "#comment-" + event.getCommentId()
);

log.info("[알림] 댓글 좋아요 알림 전송 완료");

} catch (Exception e) {
log.error("[알림] 댓글 좋아요 알림 전송 실패: error={}", e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.notification.event.community;

import lombok.Getter;

@Getter
public class PostLikedEvent extends CommunityNotificationEvent {
private final Long postId;
private final String postTitle;

public PostLikedEvent(Long actorId, Long postAuthorId, Long postId, String postTitle) {
super(
actorId,
postAuthorId,
postId,
"좋아요",
"회원님의 게시글을 좋아합니다"
);
this.postId = postId;
this.postTitle = postTitle;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.back.domain.notification.event.community;

import lombok.Getter;

@Getter
public class ReplyCreatedEvent extends CommunityNotificationEvent {
private final Long postId;
private final Long parentCommentId;
private final Long replyId;
private final String replyContent;

public ReplyCreatedEvent(Long actorId, Long commentAuthorId, Long postId,
Long parentCommentId, Long replyId, String replyContent) {
super(
actorId,
commentAuthorId,
parentCommentId,
"새 대댓글",
"회원님의 댓글에 답글이 달렸습니다"
);
this.postId = postId;
this.parentCommentId = parentCommentId;
this.replyId = replyId;
this.replyContent = replyContent;
}

public String getContentPreview() {
if (replyContent == null || replyContent.isEmpty()) {
return "";
}
return replyContent.length() > 50
? replyContent.substring(0, 50) + "..."
: replyContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.back.domain.notification.event.study;

import lombok.Getter;

import java.time.LocalDate;

@Getter
public class DailyGoalAchievedEvent extends StudyNotificationEvent {
private final LocalDate achievedDate;
private final int completedPlans;
private final int totalPlans;

public DailyGoalAchievedEvent(Long userId, LocalDate achievedDate,
int completedPlans, int totalPlans) {
super(
userId,
"일일 목표 달성 🎉",
String.format("오늘의 학습 계획을 모두 완료했습니다! (%d/%d)",
completedPlans, totalPlans)
);
this.achievedDate = achievedDate;
this.completedPlans = completedPlans;
this.totalPlans = totalPlans;
}
}
Loading