항해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);
    }
}