From d222bba009a6b62bb85fce45d362cd34ceb85284 Mon Sep 17 00:00:00 2001 From: SeokGeunHo Date: Wed, 24 Sep 2025 16:28:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C,=20=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 24 ++++++-- .../notification/entity/Notification.java | 4 ++ .../service/NotificationService.java | 58 +++++++++++++++++-- .../post/comment/service/CommentService.java | 11 ++++ .../domain/post/post/service/PostService.java | 12 ++++ 5 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/back/domain/notification/controller/NotificationController.java b/src/main/java/com/back/domain/notification/controller/NotificationController.java index eb85c581..a4fc78c5 100644 --- a/src/main/java/com/back/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/back/domain/notification/controller/NotificationController.java @@ -3,20 +3,27 @@ import com.back.domain.notification.dto.NotificationGoResponseDto; import com.back.domain.notification.dto.NotificationListResponseDto; import com.back.domain.notification.dto.NotificationSettingDto; -import com.back.domain.notification.service.NotificationSettingService; import com.back.domain.notification.dto.NotificationSettingUpdateRequestDto; -import jakarta.validation.Valid; import com.back.domain.notification.service.NotificationService; +import com.back.domain.notification.service.NotificationSettingService; import com.back.global.rsData.RsData; +import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RestController @RequestMapping("/api/me") @@ -27,6 +34,13 @@ public class NotificationController { private final NotificationService notificationService; private final NotificationSettingService notificationSettingService; + // SSE 연결 + // produces = "text/event-stream": 응답 형식이 SSE임을 명시 + @GetMapping(value = "/subscribe", produces = "text/event-stream") + public SseEmitter subscribe() { + return notificationService.subscribe(); + } + @GetMapping("/notifications") public RsData getNotifications( @AuthenticationPrincipal(expression = "id") Long userId, diff --git a/src/main/java/com/back/domain/notification/entity/Notification.java b/src/main/java/com/back/domain/notification/entity/Notification.java index 51917453..b2f07028 100644 --- a/src/main/java/com/back/domain/notification/entity/Notification.java +++ b/src/main/java/com/back/domain/notification/entity/Notification.java @@ -42,6 +42,10 @@ public class Notification { @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; + // 알림 메시지 + @Column(name = "message", nullable = false, columnDefinition = "TEXT") + private String message; + public void markRead() { this.read = true; } diff --git a/src/main/java/com/back/domain/notification/service/NotificationService.java b/src/main/java/com/back/domain/notification/service/NotificationService.java index 018784ed..fb806ed3 100644 --- a/src/main/java/com/back/domain/notification/service/NotificationService.java +++ b/src/main/java/com/back/domain/notification/service/NotificationService.java @@ -4,22 +4,46 @@ import com.back.domain.notification.dto.NotificationItemDto; import com.back.domain.notification.dto.NotificationListResponseDto; import com.back.domain.notification.entity.Notification; +import com.back.domain.notification.enums.NotificationType; import com.back.domain.notification.repository.NotificationRepository; +import com.back.domain.post.post.entity.Post; +import com.back.domain.user.entity.User; import com.back.global.exception.ServiceException; +import com.back.global.rq.Rq; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Service @RequiredArgsConstructor public class NotificationService { private final NotificationRepository notificationRepository; + private final Rq rq; + + // 연결을 관리하기 위한 Map (key: userId) + // ConcurrentHashMap: 멀티스레드 환경에서 컬렉션을 안전하게 사용 가능 + private final Map emitters = new ConcurrentHashMap<>(); + + // 구독 (클라이언트 연결 유지) + public SseEmitter subscribe() { + User user = rq.getActor(); // 현재 로그인한 사용자의 정보 가져오기 + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + emitters.put(user.getId(), emitter); + + // 연결 종료 시 제거 + emitter.onCompletion(() -> emitters.remove(user.getId())); + emitter.onTimeout(() -> emitters.remove(user.getId())); + + return emitter; + } @Transactional(readOnly = true) public NotificationListResponseDto getNotifications(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) { @@ -63,4 +87,30 @@ public NotificationGoResponseDto markAsReadAndGetPostLink(Long userId, Long noti String apiUrl = "/api/posts/" + postId; return new NotificationGoResponseDto(postId, apiUrl); } + + // 알림 생성 및 전송 + @Transactional + public void sendNotification(User user, Post post, NotificationType type, String message) { + Notification notification = Notification.builder() + .user(user) + .post(post) + .type(type) + .message(message) + .build(); + + notificationRepository.save(notification); + + // 실시간 전송 + SseEmitter emitter = emitters.get(user.getId()); + if (emitter != null) { + try { + emitter.send(SseEmitter.event() + .name("notification") + .data(notification)); + } catch (Exception e) { + // 전송 실패 시 연결 종료 및 제거 + emitters.remove(user.getId()); + } + } + } } diff --git a/src/main/java/com/back/domain/post/comment/service/CommentService.java b/src/main/java/com/back/domain/post/comment/service/CommentService.java index 67fa8fbe..6e9b11c2 100644 --- a/src/main/java/com/back/domain/post/comment/service/CommentService.java +++ b/src/main/java/com/back/domain/post/comment/service/CommentService.java @@ -1,5 +1,7 @@ package com.back.domain.post.comment.service; +import com.back.domain.notification.enums.NotificationType; +import com.back.domain.notification.service.NotificationService; import com.back.domain.post.comment.dto.request.CommentCreateRequestDto; import com.back.domain.post.comment.dto.request.CommentUpdateRequestDto; import com.back.domain.post.comment.dto.response.CommentResponseDto; @@ -21,6 +23,7 @@ public class CommentService { private final CommentRepository commentRepository; private final PostRepository postRepository; + private final NotificationService notificationService; private final Rq rq; // 댓글 작성 로직 @@ -37,6 +40,14 @@ public CommentResponseDto createComment(Long postId, CommentCreateRequestDto req .content(reqBody.content()) .build(); + // 게시글 작성자에게 알림 전송 + notificationService.sendNotification( + post.getUser(), + post, + NotificationType.COMMENT, + user.getNickname() + " 님이 댓글을 남겼습니다." + ); + return new CommentResponseDto(commentRepository.save(comment)); } diff --git a/src/main/java/com/back/domain/post/post/service/PostService.java b/src/main/java/com/back/domain/post/post/service/PostService.java index 153b86c2..7f4a390a 100644 --- a/src/main/java/com/back/domain/post/post/service/PostService.java +++ b/src/main/java/com/back/domain/post/post/service/PostService.java @@ -1,5 +1,7 @@ package com.back.domain.post.post.service; +import com.back.domain.notification.enums.NotificationType; +import com.back.domain.notification.service.NotificationService; import com.back.domain.post.category.entity.Category; import com.back.domain.post.category.repository.CategoryRepository; import com.back.domain.post.post.dto.request.PostCreateRequestDto; @@ -31,6 +33,7 @@ public class PostService { private final CategoryRepository categoryRepository; private final TagRepository tagRepository; private final PostLikeRepository postLikeRepository; + private final NotificationService notificationService; private final Rq rq; // 게시글 작성 로직 @@ -134,6 +137,7 @@ public void deletePost(Long postId) { // postRepository.delete(post); } + // 게시글 추천(좋아요) 토글 로직 @Transactional public void toggleLike(Long postId) { User user = rq.getActor(); // 현재 로그인한 사용자 @@ -158,6 +162,14 @@ public void toggleLike(Long postId) { postLikeRepository.save(postLike); post.increaseLikeCount(); } + + // 게시글 작성자에게 알림 전송 + notificationService.sendNotification( + post.getUser(), + post, + NotificationType.LIKE, + user.getNickname() + " 님이 추천을 남겼습니다." + ); } // 태그 추가 메서드