Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<NotificationListResponseDto> getNotifications(
@AuthenticationPrincipal(expression = "id") Long userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long, SseEmitter> 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) {
Expand Down Expand Up @@ -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());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,6 +23,7 @@ public class CommentService {

private final CommentRepository commentRepository;
private final PostRepository postRepository;
private final NotificationService notificationService;
private final Rq rq;

// 댓글 작성 로직
Expand All @@ -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));
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/back/domain/post/post/service/PostService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;

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

// 게시글 추천(좋아요) 토글 로직
@Transactional
public void toggleLike(Long postId) {
User user = rq.getActor(); // 현재 로그인한 사용자
Expand All @@ -158,6 +162,14 @@ public void toggleLike(Long postId) {
postLikeRepository.save(postLike);
post.increaseLikeCount();
}

// 게시글 작성자에게 알림 전송
notificationService.sendNotification(
post.getUser(),
post,
NotificationType.LIKE,
user.getNickname() + " 님이 추천을 남겼습니다."
);
}

// 태그 추가 메서드
Expand Down