From eb6bfcc6fd81b3fb80ce23d3131ba123c01fa6a7 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Sun, 8 Feb 2026 17:19:36 +0900 Subject: [PATCH 1/7] =?UTF-8?q?:recycle:Refactor:=20getLeaderMember()=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EC=A0=9C=EC=96=B4=EC=9E=90=20private?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD->=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=EB=A7=8C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/be/sportizebe/domain/club/entity/Club.java | 2 +- .../domain/notification/service/JoinClubRequestServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java index a62d0ff..cb0f630 100644 --- a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -50,7 +50,7 @@ public class Club extends BaseTimeEntity { * 동호회장(LEADER) 조회 * ClubMember에서 LEADER 역할을 가진 멤버를 찾아 반환 */ - public ClubMember getLeaderMember() { + private ClubMember getLeaderMember() { return members.stream() .filter(member -> member.getRole() == ClubMember.ClubRole.LEADER) .findFirst() diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java b/src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java index 6729ad9..8265f28 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java @@ -66,7 +66,7 @@ public JoinClubRequestResponse requestJoin(Long clubId, Long userId) { joinClubRequestRepository.save(joinRequest); // 동호회장에게 알림 전송 - User leader = club.getLeader(); + User leader = club.getLeader(); // 사용자 관련 정보 추출을 위해 getLeader() 사용 if (leader != null) { notificationService.createJoinRequestNotification(joinRequest, leader); } From 1ca41c8e14d9f68f8ce49707babd741e669113c6 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Sun, 8 Feb 2026 17:55:41 +0900 Subject: [PATCH 2/7] =?UTF-8?q?:recycle:Refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=A0=88=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0(=EB=B3=84=EB=8F=84=20DB=EC=A0=80=EC=9E=A5X)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/entity/Notification.java | 4 +--- .../service/NotificationServiceImpl.java | 12 ------------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java index c1c420e..18fc674 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java +++ b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java @@ -19,6 +19,7 @@ }) public class Notification extends BaseTimeEntity { + // TODO : 알람 종류 구체화해서 수정 public enum NotificationType { JOIN_REQUEST, // 가입 신청 (동호회장에게) JOIN_APPROVED, // 가입 승인 (신청자에게) @@ -39,9 +40,6 @@ public enum NotificationType { @Column(nullable = false) private NotificationType type; - @Column(nullable = false) - private String message; // 알림 메시지 - @Column(nullable = false) @Builder.Default private Boolean isRead = false; diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java index 24ff39a..d386983 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java @@ -25,14 +25,9 @@ public class NotificationServiceImpl implements NotificationService { @Override @Transactional public Notification createJoinRequestNotification(JoinClubRequest joinRequest, User receiver) { - String message = String.format("%s님이 %s 동호회에 가입을 신청했습니다.", - joinRequest.getUser().getNickname(), - joinRequest.getClub().getName()); - Notification notification = Notification.builder() .receiver(receiver) .type(Notification.NotificationType.JOIN_REQUEST) - .message(message) .joinClubRequest(joinRequest) .build(); @@ -47,13 +42,9 @@ public Notification createJoinRequestNotification(JoinClubRequest joinRequest, U @Override @Transactional public Notification createJoinApprovedNotification(JoinClubRequest joinRequest) { - String message = String.format("%s 동호회 가입이 승인되었습니다.", - joinRequest.getClub().getName()); - Notification notification = Notification.builder() .receiver(joinRequest.getUser()) .type(Notification.NotificationType.JOIN_APPROVED) - .message(message) .joinClubRequest(joinRequest) .build(); @@ -66,13 +57,10 @@ public Notification createJoinApprovedNotification(JoinClubRequest joinRequest) @Override @Transactional public Notification createJoinRejectedNotification(JoinClubRequest joinRequest) { - String message = String.format("%s 동호회 가입이 거절되었습니다.", - joinRequest.getClub().getName()); Notification notification = Notification.builder() .receiver(joinRequest.getUser()) .type(Notification.NotificationType.JOIN_REJECTED) - .message(message) .joinClubRequest(joinRequest) .build(); From 690f5cf00636786474f9212b4948374776ff15f4 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Sun, 8 Feb 2026 18:24:21 +0900 Subject: [PATCH 3/7] =?UTF-8?q?:recycle:Refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20dto=EC=97=90=20=EB=8F=99=ED=98=B8=ED=9A=8C?= =?UTF-8?q?,=20=EC=88=98=EC=8B=A0=EC=9E=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/NotificationResponse.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java b/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java index 33fc66a..0e10a04 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java +++ b/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java @@ -11,20 +11,23 @@ public record NotificationResponse( @Schema(description = "알림 ID") Long id, @Schema(description = "알림 타입") Notification.NotificationType type, - @Schema(description = "알림 메시지") String message, @Schema(description = "읽음 여부") Boolean isRead, @Schema(description = "관련 가입 신청 ID") Long joinRequestId, + @Schema(description = "동호회 이름") String clubName, + @Schema(description = "신청자 닉네임") String applicantNickname, @Schema(description = "관련 대상 ID (댓글, 채팅 등)") Long targetId, @Schema(description = "알림 생성 일시") LocalDateTime createdAt ) { public static NotificationResponse from(Notification notification) { + var joinRequest = notification.getJoinClubRequest(); + return NotificationResponse.builder() .id(notification.getId()) .type(notification.getType()) - .message(notification.getMessage()) .isRead(notification.getIsRead()) - .joinRequestId(notification.getJoinClubRequest() != null - ? notification.getJoinClubRequest().getId() : null) + .joinRequestId(joinRequest != null ? joinRequest.getId() : null) + .clubName(joinRequest != null ? joinRequest.getClub().getName() : null) + .applicantNickname(joinRequest != null ? joinRequest.getUser().getNickname() : null) .targetId(notification.getTargetId()) .createdAt(notification.getCreatedAt()) .build(); From 8eb7065fad0a21cd0c0b37ba988c8926e56117fd Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Mon, 9 Feb 2026 15:41:50 +0900 Subject: [PATCH 4/7] =?UTF-8?q?:sparkles:Feat:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EB=8C=93=EA=B8=80=20=EC=95=8C=EB=9E=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentServiceImpl.java | 5 +++++ .../dto/response/NotificationResponse.java | 9 +++++++- .../notification/entity/Notification.java | 10 +++++++-- .../service/NotificationService.java | 4 ++++ .../service/NotificationServiceImpl.java | 22 +++++++++++++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java b/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java index 81a4347..284e934 100644 --- a/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java @@ -6,6 +6,7 @@ import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.comment.exception.CommentErrorCode; import com.be.sportizebe.domain.comment.repository.CommentRepository; +import com.be.sportizebe.domain.notification.service.NotificationService; import com.be.sportizebe.domain.post.entity.Post; import com.be.sportizebe.domain.post.exception.PostErrorCode; import com.be.sportizebe.domain.post.repository.PostRepository; @@ -30,6 +31,7 @@ public class CommentServiceImpl implements CommentService { private final CommentRepository commentRepository; private final PostRepository postRepository; private final UserRepository userRepository; + private final NotificationService notificationService; @Override @CacheEvict(cacheNames = {"commentList", "commentCount"}, key = "#postId") @@ -64,6 +66,9 @@ public CommentResponse createComment(Long postId, CreateCommentRequest request, Comment comment = request.toEntity(post, user, parent); Comment savedComment = commentRepository.save(comment); + // 게시글 작성자에게 알림 전송 + notificationService.createCommentNotification(savedComment); + return CommentResponse.from(savedComment); } diff --git a/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java b/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java index 0e10a04..0ee280d 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java +++ b/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java @@ -15,11 +15,15 @@ public record NotificationResponse( @Schema(description = "관련 가입 신청 ID") Long joinRequestId, @Schema(description = "동호회 이름") String clubName, @Schema(description = "신청자 닉네임") String applicantNickname, - @Schema(description = "관련 대상 ID (댓글, 채팅 등)") Long targetId, + @Schema(description = "게시글 ID") Long postId, + @Schema(description = "게시글 제목") String postTitle, + @Schema(description = "댓글 작성자 닉네임") String commenterNickname, + @Schema(description = "관련 대상 ID (채팅 등)") Long targetId, @Schema(description = "알림 생성 일시") LocalDateTime createdAt ) { public static NotificationResponse from(Notification notification) { var joinRequest = notification.getJoinClubRequest(); + var comment = notification.getComment(); return NotificationResponse.builder() .id(notification.getId()) @@ -28,6 +32,9 @@ public static NotificationResponse from(Notification notification) { .joinRequestId(joinRequest != null ? joinRequest.getId() : null) .clubName(joinRequest != null ? joinRequest.getClub().getName() : null) .applicantNickname(joinRequest != null ? joinRequest.getUser().getNickname() : null) + .postId(comment != null ? comment.getPost().getId() : null) + .postTitle(comment != null ? comment.getPost().getTitle() : null) + .commenterNickname(comment != null ? comment.getUser().getNickname() : null) .targetId(notification.getTargetId()) .createdAt(notification.getCreatedAt()) .build(); diff --git a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java index 18fc674..31a0727 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java +++ b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.notification.entity; +import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.common.BaseTimeEntity; import jakarta.persistence.*; @@ -49,8 +50,13 @@ public enum NotificationType { @JoinColumn(name = "join_request_id") private JoinClubRequest joinClubRequest; - // 댓글 알림용 - targetId와 targetType으로 다형성 처리 - // COMMENT: postId, CHAT: chatRoomId 등 + // 댓글 알림용 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; + + // 기타 알림용 - targetId로 다형성 처리 + // CHAT: chatRoomId 등 private Long targetId; public void markAsRead() { diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java index 0494ae8..fe6367e 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.notification.service; +import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.notification.dto.response.NotificationResponse; import com.be.sportizebe.domain.notification.entity.JoinClubRequest; import com.be.sportizebe.domain.notification.entity.Notification; @@ -18,6 +19,9 @@ public interface NotificationService { // 가입 거절 알림 생성 및 웹소켓 전송 Notification createJoinRejectedNotification(JoinClubRequest joinRequest); + // 댓글 알림 생성 및 웹소켓 전송 + void createCommentNotification(Comment comment); + // 사용자의 모든 알림 조회 List getNotifications(User user); diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java index d386983..7bb53fe 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.notification.service; +import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.notification.dto.response.NotificationResponse; import com.be.sportizebe.domain.notification.entity.JoinClubRequest; import com.be.sportizebe.domain.notification.entity.Notification; @@ -104,6 +105,27 @@ public void markAsRead(Long notificationId, Long userId) { notification.markAsRead(); } + @Override + @Transactional + public void createCommentNotification(Comment comment) { + User postAuthor = comment.getPost().getUser(); + User commenter = comment.getUser(); + + // 자신의 게시글에 자신이 댓글을 단 경우 알림 생성하지 않음 + if (postAuthor.getId() == commenter.getId()) { + return; + } + + Notification notification = Notification.builder() + .receiver(postAuthor) + .type(Notification.NotificationType.COMMENT) + .comment(comment) + .build(); + + notificationRepository.save(notification); + sendNotificationToUser(postAuthor.getId(), notification); + } + /** * 웹소켓으로 알림 전송 */ From 5e857f0ed7f84cbf8e0a8b344a2c5f199eb71628 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Mon, 9 Feb 2026 15:43:42 +0900 Subject: [PATCH 5/7] =?UTF-8?q?:recycle:Refactor:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=83=80=EC=9E=85=20void=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/service/NotificationService.java | 6 +++--- .../service/NotificationServiceImpl.java | 13 +++---------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java index fe6367e..53c714c 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java @@ -11,13 +11,13 @@ public interface NotificationService { // 가입 신청 알림 생성 및 웹소켓 전송 - Notification createJoinRequestNotification(JoinClubRequest joinRequest, User receiver); + void createJoinRequestNotification(JoinClubRequest joinRequest, User receiver); // 가입 승인 알림 생성 및 웹소켓 전송 - Notification createJoinApprovedNotification(JoinClubRequest joinRequest); + void createJoinApprovedNotification(JoinClubRequest joinRequest); // 가입 거절 알림 생성 및 웹소켓 전송 - Notification createJoinRejectedNotification(JoinClubRequest joinRequest); + void createJoinRejectedNotification(JoinClubRequest joinRequest); // 댓글 알림 생성 및 웹소켓 전송 void createCommentNotification(Comment comment); diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java index 7bb53fe..025c480 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java @@ -25,7 +25,7 @@ public class NotificationServiceImpl implements NotificationService { @Override @Transactional - public Notification createJoinRequestNotification(JoinClubRequest joinRequest, User receiver) { + public void createJoinRequestNotification(JoinClubRequest joinRequest, User receiver) { Notification notification = Notification.builder() .receiver(receiver) .type(Notification.NotificationType.JOIN_REQUEST) @@ -36,13 +36,11 @@ public Notification createJoinRequestNotification(JoinClubRequest joinRequest, U // 웹소켓으로 실시간 알림 전송 sendNotificationToUser(receiver.getId(), notification); - - return notification; } @Override @Transactional - public Notification createJoinApprovedNotification(JoinClubRequest joinRequest) { + public void createJoinApprovedNotification(JoinClubRequest joinRequest) { Notification notification = Notification.builder() .receiver(joinRequest.getUser()) .type(Notification.NotificationType.JOIN_APPROVED) @@ -51,14 +49,11 @@ public Notification createJoinApprovedNotification(JoinClubRequest joinRequest) notificationRepository.save(notification); sendNotificationToUser(joinRequest.getUser().getId(), notification); - - return notification; } @Override @Transactional - public Notification createJoinRejectedNotification(JoinClubRequest joinRequest) { - + public void createJoinRejectedNotification(JoinClubRequest joinRequest) { Notification notification = Notification.builder() .receiver(joinRequest.getUser()) .type(Notification.NotificationType.JOIN_REJECTED) @@ -67,8 +62,6 @@ public Notification createJoinRejectedNotification(JoinClubRequest joinRequest) notificationRepository.save(notification); sendNotificationToUser(joinRequest.getUser().getId(), notification); - - return notification; } @Override From e36f591733451fdca03bb01bd43137c58cacd576 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Mon, 9 Feb 2026 16:04:31 +0900 Subject: [PATCH 6/7] =?UTF-8?q?:spakrles:Feat:=20=EB=8C=80=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentServiceImpl.java | 10 +++++-- .../notification/entity/Notification.java | 4 +-- .../service/NotificationService.java | 5 +++- .../service/NotificationServiceImpl.java | 26 +++++++++++++++++++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java b/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java index 284e934..a044a7a 100644 --- a/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/comment/service/CommentServiceImpl.java @@ -66,8 +66,14 @@ public CommentResponse createComment(Long postId, CreateCommentRequest request, Comment comment = request.toEntity(post, user, parent); Comment savedComment = commentRepository.save(comment); - // 게시글 작성자에게 알림 전송 - notificationService.createCommentNotification(savedComment); + // 알림 전송 + if (savedComment.getParent() != null) { + // 대댓글인 경우: 부모 댓글 작성자에게 알림 + notificationService.createReplyNotification(savedComment); + } else { + // 일반 댓글인 경우: 게시글 작성자에게 알림 + notificationService.createCommentNotification(savedComment); + } return CommentResponse.from(savedComment); } diff --git a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java index 31a0727..79b99e7 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java +++ b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java @@ -20,13 +20,13 @@ }) public class Notification extends BaseTimeEntity { - // TODO : 알람 종류 구체화해서 수정 public enum NotificationType { JOIN_REQUEST, // 가입 신청 (동호회장에게) JOIN_APPROVED, // 가입 승인 (신청자에게) JOIN_REJECTED, // 가입 거절 (신청자에게) CHAT, // 새 채팅 메시지 - COMMENT // 새 댓글 + COMMENT, // 새 댓글 (게시글 작성자에게) + REPLY // 대댓글 (부모 댓글 작성자에게) } @Id diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java index 53c714c..0040751 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java @@ -19,9 +19,12 @@ public interface NotificationService { // 가입 거절 알림 생성 및 웹소켓 전송 void createJoinRejectedNotification(JoinClubRequest joinRequest); - // 댓글 알림 생성 및 웹소켓 전송 + // 댓글 알림 생성 및 웹소켓 전송 (게시글 작성자에게) void createCommentNotification(Comment comment); + // 대댓글 알림 생성 및 웹소켓 전송 (부모 댓글 작성자에게) + void createReplyNotification(Comment reply); + // 사용자의 모든 알림 조회 List getNotifications(User user); diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java index 025c480..c080e82 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java @@ -119,6 +119,32 @@ public void createCommentNotification(Comment comment) { sendNotificationToUser(postAuthor.getId(), notification); } + @Override + @Transactional + public void createReplyNotification(Comment reply) { + Comment parentComment = reply.getParent(); + if (parentComment == null) { + return; + } + + User parentAuthor = parentComment.getUser(); + User replier = reply.getUser(); + + // 자신의 댓글에 자신이 대댓글을 단 경우 알림 생성하지 않음 + if (parentAuthor.getId() == replier.getId()) { + return; + } + + Notification notification = Notification.builder() + .receiver(parentAuthor) + .type(Notification.NotificationType.REPLY) + .comment(reply) + .build(); + + notificationRepository.save(notification); + sendNotificationToUser(parentAuthor.getId(), notification); + } + /** * 웹소켓으로 알림 전송 */ From 38d42335a1e35d970f5616a0e38602407824897e Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Mon, 9 Feb 2026 16:52:17 +0900 Subject: [PATCH 7/7] =?UTF-8?q?:sparkles:Feat:=20=EC=AA=BD=EC=A7=80=20?= =?UTF-8?q?=EC=95=8C=EB=9E=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/ChatStompController.java | 28 ++++++++++++++++++- .../dto/response/NotificationResponse.java | 7 ++++- .../notification/entity/Notification.java | 10 +++++-- .../service/NotificationService.java | 4 +++ .../service/NotificationServiceImpl.java | 19 +++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/chat/websocket/handler/ChatStompController.java b/src/main/java/com/be/sportizebe/domain/chat/websocket/handler/ChatStompController.java index 0953d85..86f78c5 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/websocket/handler/ChatStompController.java +++ b/src/main/java/com/be/sportizebe/domain/chat/websocket/handler/ChatStompController.java @@ -5,8 +5,13 @@ import com.be.sportizebe.domain.chat.dto.request.ChatSendRequest; import com.be.sportizebe.domain.chat.entity.ChatMessage; import com.be.sportizebe.domain.chat.entity.ChatRoom; +import com.be.sportizebe.domain.chat.entity.ChatRoomMember; +import com.be.sportizebe.domain.chat.repository.ChatRoomMemberRepository; import com.be.sportizebe.domain.chat.service.ChatMessageService; import com.be.sportizebe.domain.chat.service.ChatRoomService; +import com.be.sportizebe.domain.notification.service.NotificationService; +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -17,9 +22,12 @@ @RequiredArgsConstructor public class ChatStompController { private final ChatMessageService chatMessageService; - private final ChatRoomService chatRoomService; // ✅ 추가 + private final ChatRoomService chatRoomService; private final SimpMessagingTemplate messagingTemplate; private final ChatSessionRegistry registry; + private final NotificationService notificationService; + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final UserRepository userRepository; /** * 클라이언트가 SEND로 보낸 메세지를 받는 주소가 여기 @@ -45,6 +53,24 @@ public void send(ChatSendRequest req){ "/sub/chat/rooms/" + req.getRoomId(), // 채팅방 단위 브로드캐스트 ChatMessageResponse.from(saved) ); + + // 1:1 채팅방(쪽지)인 경우 상대방에게 알림 전송 + if (room.getChatRoomType() == ChatRoom.ChatRoomType.NOTE) { + sendNoteNotification(saved, req.getSenderUserId()); + } + } + + /** + * 쪽지 알림 전송 (발신자 제외한 상대방에게) + */ + private void sendNoteNotification(ChatMessage chatMessage, Long senderUserId) { + chatRoomMemberRepository.findAllByRoom_IdAndLeftAtIsNull(chatMessage.getRoom().getId()) + .stream() + .map(ChatRoomMember::getUserId) + .filter(userId -> !userId.equals(senderUserId)) + .findFirst() // 첫번째 사람만 선택 + .flatMap(userRepository::findById) + .ifPresent(receiver -> notificationService.createNoteNotification(chatMessage, receiver)); } @MessageMapping("/chat.join") diff --git a/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java b/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java index 0ee280d..75d5a1f 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java +++ b/src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java @@ -18,12 +18,15 @@ public record NotificationResponse( @Schema(description = "게시글 ID") Long postId, @Schema(description = "게시글 제목") String postTitle, @Schema(description = "댓글 작성자 닉네임") String commenterNickname, - @Schema(description = "관련 대상 ID (채팅 등)") Long targetId, + @Schema(description = "채팅방 ID") Long chatRoomId, + @Schema(description = "쪽지 발신자 닉네임") String senderNickname, + @Schema(description = "관련 대상 ID") Long targetId, @Schema(description = "알림 생성 일시") LocalDateTime createdAt ) { public static NotificationResponse from(Notification notification) { var joinRequest = notification.getJoinClubRequest(); var comment = notification.getComment(); + var chatMessage = notification.getChatMessage(); return NotificationResponse.builder() .id(notification.getId()) @@ -35,6 +38,8 @@ public static NotificationResponse from(Notification notification) { .postId(comment != null ? comment.getPost().getId() : null) .postTitle(comment != null ? comment.getPost().getTitle() : null) .commenterNickname(comment != null ? comment.getUser().getNickname() : null) + .chatRoomId(chatMessage != null ? chatMessage.getRoom().getId() : null) + .senderNickname(chatMessage != null ? chatMessage.getSenderNickname() : null) .targetId(notification.getTargetId()) .createdAt(notification.getCreatedAt()) .build(); diff --git a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java index 79b99e7..a1cb671 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java +++ b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.notification.entity; +import com.be.sportizebe.domain.chat.entity.ChatMessage; import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.common.BaseTimeEntity; @@ -26,7 +27,8 @@ public enum NotificationType { JOIN_REJECTED, // 가입 거절 (신청자에게) CHAT, // 새 채팅 메시지 COMMENT, // 새 댓글 (게시글 작성자에게) - REPLY // 대댓글 (부모 댓글 작성자에게) + REPLY, // 대댓글 (부모 댓글 작성자에게) + NOTE // 쪽지 (수신자에게) } @Id @@ -55,8 +57,12 @@ public enum NotificationType { @JoinColumn(name = "comment_id") private Comment comment; + // 쪽지 알림용 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "chat_message_id") + private ChatMessage chatMessage; + // 기타 알림용 - targetId로 다형성 처리 - // CHAT: chatRoomId 등 private Long targetId; public void markAsRead() { diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java index 0040751..1a558ad 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.notification.service; +import com.be.sportizebe.domain.chat.entity.ChatMessage; import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.notification.dto.response.NotificationResponse; import com.be.sportizebe.domain.notification.entity.JoinClubRequest; @@ -25,6 +26,9 @@ public interface NotificationService { // 대댓글 알림 생성 및 웹소켓 전송 (부모 댓글 작성자에게) void createReplyNotification(Comment reply); + // 쪽지 알림 생성 및 웹소켓 전송 (수신자에게) + void createNoteNotification(ChatMessage chatMessage, User receiver); + // 사용자의 모든 알림 조회 List getNotifications(User user); diff --git a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java index c080e82..8ea1189 100644 --- a/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.notification.service; +import com.be.sportizebe.domain.chat.entity.ChatMessage; import com.be.sportizebe.domain.comment.entity.Comment; import com.be.sportizebe.domain.notification.dto.response.NotificationResponse; import com.be.sportizebe.domain.notification.entity.JoinClubRequest; @@ -145,6 +146,24 @@ public void createReplyNotification(Comment reply) { sendNotificationToUser(parentAuthor.getId(), notification); } + @Override + @Transactional + public void createNoteNotification(ChatMessage chatMessage, User receiver) { + // 자신에게 보낸 경우 알림 생성하지 않음 + if (receiver.getId() == chatMessage.getSenderUserId()) { + return; + } + + Notification notification = Notification.builder() + .receiver(receiver) + .type(Notification.NotificationType.NOTE) + .chatMessage(chatMessage) + .build(); + + notificationRepository.save(notification); + sendNotificationToUser(receiver.getId(), notification); + } + /** * 웹소켓으로 알림 전송 */