Skip to content

Commit b98551d

Browse files
authored
[feat] 게시글 추천, 댓글 작성 시 알림 이벤트 구현
[feat] 게시글 추천, 댓글 작성 시 알림 이벤트 구현
2 parents b2d9b2b + d222bba commit b98551d

File tree

5 files changed

+100
-9
lines changed

5 files changed

+100
-9
lines changed

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@
33
import com.back.domain.notification.dto.NotificationGoResponseDto;
44
import com.back.domain.notification.dto.NotificationListResponseDto;
55
import com.back.domain.notification.dto.NotificationSettingDto;
6-
import com.back.domain.notification.service.NotificationSettingService;
76
import com.back.domain.notification.dto.NotificationSettingUpdateRequestDto;
8-
import jakarta.validation.Valid;
97
import com.back.domain.notification.service.NotificationService;
8+
import com.back.domain.notification.service.NotificationSettingService;
109
import com.back.global.rsData.RsData;
10+
import jakarta.validation.Valid;
1111
import jakarta.validation.constraints.Max;
1212
import jakarta.validation.constraints.Min;
13+
import java.time.LocalDateTime;
1314
import lombok.RequiredArgsConstructor;
1415
import org.springframework.format.annotation.DateTimeFormat;
1516
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1617
import org.springframework.validation.annotation.Validated;
17-
import org.springframework.web.bind.annotation.*;
18-
19-
import java.time.LocalDateTime;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.PatchMapping;
20+
import org.springframework.web.bind.annotation.PathVariable;
21+
import org.springframework.web.bind.annotation.PostMapping;
22+
import org.springframework.web.bind.annotation.RequestBody;
23+
import org.springframework.web.bind.annotation.RequestMapping;
24+
import org.springframework.web.bind.annotation.RequestParam;
25+
import org.springframework.web.bind.annotation.RestController;
26+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
2027

2128
@RestController
2229
@RequestMapping("/api/me")
@@ -27,6 +34,13 @@ public class NotificationController {
2734
private final NotificationService notificationService;
2835
private final NotificationSettingService notificationSettingService;
2936

37+
// SSE 연결
38+
// produces = "text/event-stream": 응답 형식이 SSE임을 명시
39+
@GetMapping(value = "/subscribe", produces = "text/event-stream")
40+
public SseEmitter subscribe() {
41+
return notificationService.subscribe();
42+
}
43+
3044
@GetMapping("/notifications")
3145
public RsData<NotificationListResponseDto> getNotifications(
3246
@AuthenticationPrincipal(expression = "id") Long userId,

src/main/java/com/back/domain/notification/entity/Notification.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ public class Notification {
4242
@Column(name = "created_at", nullable = false, updatable = false)
4343
private LocalDateTime createdAt;
4444

45+
// 알림 메시지
46+
@Column(name = "message", nullable = false, columnDefinition = "TEXT")
47+
private String message;
48+
4549
public void markRead() {
4650
this.read = true;
4751
}

src/main/java/com/back/domain/notification/service/NotificationService.java

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,46 @@
44
import com.back.domain.notification.dto.NotificationItemDto;
55
import com.back.domain.notification.dto.NotificationListResponseDto;
66
import com.back.domain.notification.entity.Notification;
7+
import com.back.domain.notification.enums.NotificationType;
78
import com.back.domain.notification.repository.NotificationRepository;
9+
import com.back.domain.post.post.entity.Post;
10+
import com.back.domain.user.entity.User;
811
import com.back.global.exception.ServiceException;
12+
import com.back.global.rq.Rq;
13+
import java.time.LocalDateTime;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.concurrent.ConcurrentHashMap;
918
import lombok.RequiredArgsConstructor;
1019
import org.springframework.data.domain.PageRequest;
1120
import org.springframework.stereotype.Service;
1221
import org.springframework.transaction.annotation.Transactional;
13-
14-
import java.time.LocalDateTime;
15-
import java.util.ArrayList;
16-
import java.util.List;
22+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
1723

1824
@Service
1925
@RequiredArgsConstructor
2026
public class NotificationService {
2127

2228
private final NotificationRepository notificationRepository;
29+
private final Rq rq;
30+
31+
// 연결을 관리하기 위한 Map (key: userId)
32+
// ConcurrentHashMap: 멀티스레드 환경에서 컬렉션을 안전하게 사용 가능
33+
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
34+
35+
// 구독 (클라이언트 연결 유지)
36+
public SseEmitter subscribe() {
37+
User user = rq.getActor(); // 현재 로그인한 사용자의 정보 가져오기
38+
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
39+
emitters.put(user.getId(), emitter);
40+
41+
// 연결 종료 시 제거
42+
emitter.onCompletion(() -> emitters.remove(user.getId()));
43+
emitter.onTimeout(() -> emitters.remove(user.getId()));
44+
45+
return emitter;
46+
}
2347

2448
@Transactional(readOnly = true)
2549
public NotificationListResponseDto getNotifications(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) {
@@ -63,4 +87,30 @@ public NotificationGoResponseDto markAsReadAndGetPostLink(Long userId, Long noti
6387
String apiUrl = "/api/posts/" + postId;
6488
return new NotificationGoResponseDto(postId, apiUrl);
6589
}
90+
91+
// 알림 생성 및 전송
92+
@Transactional
93+
public void sendNotification(User user, Post post, NotificationType type, String message) {
94+
Notification notification = Notification.builder()
95+
.user(user)
96+
.post(post)
97+
.type(type)
98+
.message(message)
99+
.build();
100+
101+
notificationRepository.save(notification);
102+
103+
// 실시간 전송
104+
SseEmitter emitter = emitters.get(user.getId());
105+
if (emitter != null) {
106+
try {
107+
emitter.send(SseEmitter.event()
108+
.name("notification")
109+
.data(notification));
110+
} catch (Exception e) {
111+
// 전송 실패 시 연결 종료 및 제거
112+
emitters.remove(user.getId());
113+
}
114+
}
115+
}
66116
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.back.domain.post.comment.service;
22

3+
import com.back.domain.notification.enums.NotificationType;
4+
import com.back.domain.notification.service.NotificationService;
35
import com.back.domain.post.comment.dto.request.CommentCreateRequestDto;
46
import com.back.domain.post.comment.dto.request.CommentUpdateRequestDto;
57
import com.back.domain.post.comment.dto.response.CommentResponseDto;
@@ -21,6 +23,7 @@ public class CommentService {
2123

2224
private final CommentRepository commentRepository;
2325
private final PostRepository postRepository;
26+
private final NotificationService notificationService;
2427
private final Rq rq;
2528

2629
// 댓글 작성 로직
@@ -37,6 +40,14 @@ public CommentResponseDto createComment(Long postId, CommentCreateRequestDto req
3740
.content(reqBody.content())
3841
.build();
3942

43+
// 게시글 작성자에게 알림 전송
44+
notificationService.sendNotification(
45+
post.getUser(),
46+
post,
47+
NotificationType.COMMENT,
48+
user.getNickname() + " 님이 댓글을 남겼습니다."
49+
);
50+
4051
return new CommentResponseDto(commentRepository.save(comment));
4152
}
4253

src/main/java/com/back/domain/post/post/service/PostService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.back.domain.post.post.service;
22

3+
import com.back.domain.notification.enums.NotificationType;
4+
import com.back.domain.notification.service.NotificationService;
35
import com.back.domain.post.category.entity.Category;
46
import com.back.domain.post.category.repository.CategoryRepository;
57
import com.back.domain.post.post.dto.request.PostCreateRequestDto;
@@ -31,6 +33,7 @@ public class PostService {
3133
private final CategoryRepository categoryRepository;
3234
private final TagRepository tagRepository;
3335
private final PostLikeRepository postLikeRepository;
36+
private final NotificationService notificationService;
3437
private final Rq rq;
3538

3639
// 게시글 작성 로직
@@ -134,6 +137,7 @@ public void deletePost(Long postId) {
134137
// postRepository.delete(post);
135138
}
136139

140+
// 게시글 추천(좋아요) 토글 로직
137141
@Transactional
138142
public void toggleLike(Long postId) {
139143
User user = rq.getActor(); // 현재 로그인한 사용자
@@ -158,6 +162,14 @@ public void toggleLike(Long postId) {
158162
postLikeRepository.save(postLike);
159163
post.increaseLikeCount();
160164
}
165+
166+
// 게시글 작성자에게 알림 전송
167+
notificationService.sendNotification(
168+
post.getUser(),
169+
post,
170+
NotificationType.LIKE,
171+
user.getNickname() + " 님이 추천을 남겼습니다."
172+
);
161173
}
162174

163175
// 태그 추가 메서드

0 commit comments

Comments
 (0)