항해99
23.11.07 항해 99 16기 실전 프로젝트 30일차
김용글
2023. 11. 7. 23:21
오늘 공부한 것
* SSE 를 활용한 알림 기능 코드 작성
오늘은 어제에 이어서 알림 기능 코드를 작성했다
알림 구독 기능 수행, 전체 알림조회, 알림 삭제에 대한 API 를 만들고 코드를 작성했다
결론적으로 이야기하자면 알림 구독 기능 수행은 잘 되었으나, 전체 알림 조회 및 알림 삭제는 잘 이루어 지지않았다
전체 알림 조회 같은 경우는 에러를 뱉어냈고, 알림 삭제는 403 에러가 나오면서 삭제되는 기이한 현상이 일어났다
내일은 두가지 에러를 수정해봐야겠다
1. Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class NotifyController {
private final NotifyService notifyService;
public static Map<Long, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
// 알림 구독 기능 수행
@Operation(summary = "알림 구독 기능 수행", description = "알림 구독 기능 수행 api 입니다.")
@GetMapping(value ="/notify/subscribe", produces = "text/event-stream")
public SseEmitter subscribe (@AuthenticationPrincipal UserDetailsImpl userDetails) {
Long userId = userDetails.getUsers().getId();
SseEmitter sseEmitter = notifyService.subscribe(userId);
return sseEmitter;
}
// 전체 알림 조회
@Operation(summary = "전체 알림 조회", description = "전체 알림 조회 api 입니다.")
@GetMapping("/notify")
public List<NotifyResponseDto> notifyList(@AuthenticationPrincipal UserDetailsImpl userDetails) {
return notifyService.notifyList(userDetails.getUsers());
}
// 알림 삭제
@Operation(summary = "알림 삭제", description = "알림 삭제 api 입니다.")
@DeleteMapping("/notify/{notifyId}")
public MessageResponseDto notifyDelete (@PathVariable("notifyId") Long notifyId) throws IOException {
return notifyService.notifyDelete(notifyId);
}
}
2. Dto
@Getter
public class NotifyResponseDto {
private Long id;
private String content;
private LocalDateTime createAt;
public NotifyResponseDto(Notify notify) {
this.id = notify.getId();
this.content = notify.getContents();
this.createAt = notify.getCreatedAt();
}
}
3. Entity
@Entity
@Getter
@NoArgsConstructor
public class Notify extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "notify_id")
private Long id;
// 알림 내용 비어있지 않아야 하고 50자 이내
private String sender;
private String contents;
private String email;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "posts_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Posts posts;
public Notify (String sender, String contents, Posts posts) {
this.sender = sender;
this.contents = contents;
this.posts = posts;
}
public Notify (Users users) {
this.email = users.getEmail();
}
}
4. Repository
public interface NorifyRepository extends JpaRepository<Notify, Long> {
List<Notify> findAllByAndEmailOrderByCreatedAtDesc(Users users);
}
5. Service
@Service
@RequiredArgsConstructor
public class NotifyService {
private final NorifyRepository norifyRepository;
private final PostsRepository postsRepository;
private final CommentsRepository commentsRepository;
private static Map<Long, Integer> notifyCounts = new HashMap<>();
// 알림 구독 기능 수행
// Spring 에서 제공하는 SseEmitter 를 생성 후 저장한 다음
// 필요할 때마다 구독자가 생성한 SseEmitter를 불러와서 이벤트에 대한 응답 전송
// Controller 에서 가져온 수신자의 식별정보와 마지막 이벤트 식별자를 받음
public SseEmitter subscribe (Long usersId) {
// 현재 클라이언트를 위한 sseEmitter 생성
SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
try {
// 연결
sseEmitter.send(SseEmitter.event().name("connect"));
} catch (IOException e) {
e.getStackTrace();
}
// user 의 pk 값을 key 로 해서 sseEmitter 를 저장
NotifyController.sseEmitters.put(usersId, sseEmitter);
// 완료, 타임아웃, 에러 발생시 SseEmitter를 emitterRepository 에서 삭제하도록 설정
sseEmitter.onCompletion(() -> NotifyController.sseEmitters.remove(usersId));
sseEmitter.onTimeout(() -> NotifyController.sseEmitters.remove(usersId));
sseEmitter.onError((e) -> NotifyController.sseEmitters.remove(usersId));
// 생성된 SseEmitter를 반환하여 클라이언트에게 전달
// 클라이언트는 이를 통해 서버로부터 알림 이벤트를 수신 / 처리 가능
return sseEmitter;
}
// 댓글 알림 - 게시글 작성자에게
public void notifyComments (Long postId) {
Posts posts = postsRepository.findById(postId).orElseThrow(
()-> new CustomException(ErrorCode.POST_NOT_EXIST)); // 존재하지 않는 게시글 입니다
Comments comments = commentsRepository.findFirstByPosts_IdOrderByCreatedAtDesc(postId).orElseThrow(
()->new CustomException(ErrorCode.COMMENTS_NOT_EXIST)); //존재하지 않는 댓글입니다
Long userId = posts.getUsers().getId();
if (NotifyController.sseEmitters.containsKey(userId)) {
SseEmitter sseEmitters = NotifyController.sseEmitters.get(userId);
try {
Map<String, String> eventData = new HashMap<>();
eventData.put("message", "댓글이 달렸습니다");
eventData.put("sender", comments.getNickname());
eventData.put("contents", comments.getContents());
// DB 저장
Notify notify = new Notify(comments.getNickname(), comments.getContents(), posts);
norifyRepository.save(notify);
// 알림 개수 증가
notifyCounts.put(userId, notifyCounts.getOrDefault(userId, 0) + 1);
// 현재 알림 개수 전송
sseEmitters.send(SseEmitter.event().name("notifyCounts").data(notifyCounts.get(userId)));
} catch (IOException e) {
NotifyController.sseEmitters.remove(userId);
}
}
}
// 조회
public List<NotifyResponseDto> notifyList(Users users) {
List<Notify> notifyList = norifyRepository.findAllByAndEmailOrderByCreatedAtDesc(users);
List<NotifyResponseDto> notifyResponseDto = notifyList.stream()
.map(NotifyResponseDto::new)
.collect(Collectors.toList());
return notifyResponseDto;
}
// 알림 삭제
public MessageResponseDto notifyDelete(Long notifyId) throws IOException {
Notify notify = norifyRepository.findById(notifyId).orElseThrow(
() -> new CustomException(ErrorCode.NOTIFY_NOT_EXIST));
Long userId = notify.getPosts().getUsers().getId();
norifyRepository.delete(notify);
// 알림 개수 감소
if (notifyCounts.containsKey(userId)) {
int currentCount = notifyCounts.get(userId);
if (currentCount > 0) {
notifyCounts.put(userId, currentCount - 1);
}
}
// 현재 알림 개수 전송
SseEmitter sseEmitter = NotifyController.sseEmitters.get(userId);
sseEmitter.send(SseEmitter.event().name("notifyCounts").data(notifyCounts.get(userId)));
return new MessageResponseDto("알림이 삭제 되었습니다", 200);
}
}