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 475e8679..43613287 100644 --- a/src/main/java/com/back/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/back/domain/notification/controller/NotificationController.java @@ -1,12 +1,9 @@ package com.back.domain.notification.controller; -import com.back.domain.notification.dto.NotificationCreateRequest; import com.back.domain.notification.dto.NotificationResponse; import com.back.domain.notification.dto.NotificationListResponse; import com.back.domain.notification.entity.Notification; -import com.back.domain.notification.entity.NotificationSettingType; import com.back.domain.notification.service.NotificationService; -import com.back.domain.studyroom.entity.Room; import com.back.domain.studyroom.repository.RoomRepository; import com.back.domain.user.entity.User; import com.back.domain.user.repository.UserRepository; @@ -37,116 +34,7 @@ public class NotificationController { private final UserRepository userRepository; private final RoomRepository roomRepository; - @Operation( - summary = "알림 전송", - description = "USER/ROOM/COMMUNITY/SYSTEM 타입별 알림 생성 및 전송\n\n" + - "- USER: 개인 알림 (actorId, targetId 필수)\n" + - "- ROOM: 스터디룸 알림 (actorId, targetId(roomId) 필수)\n" + - "- COMMUNITY: 커뮤니티 알림 (actorId, targetId 필수)\n" + - "- SYSTEM: 시스템 전체 알림 (actorId, targetId 불필요)" - ) - @PostMapping - public ResponseEntity> createNotification( - @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody NotificationCreateRequest request) { - - log.info("알림 전송 요청 - 타입: {}, 제목: {}", request.targetType(), request.title()); - - // targetType 검증 - if (!isValidTargetType(request.targetType())) { - throw new CustomException(ErrorCode.NOTIFICATION_INVALID_TARGET_TYPE); - } - - // SYSTEM이 아닌 경우 필수 필드 검증 - if (!"SYSTEM".equals(request.targetType())) { - if (request.actorId() == null) { - throw new CustomException(ErrorCode.NOTIFICATION_MISSING_ACTOR); - } - if (request.targetId() == null) { - throw new CustomException(ErrorCode.NOTIFICATION_MISSING_TARGET); - } - } - - Notification notification = switch (request.targetType()) { - case "USER" -> { - // 수신자 조회 - User receiver = userRepository.findById(request.targetId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 발신자 조회 - User actor = userRepository.findById(request.actorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 개인 알림 생성 - yield notificationService.createPersonalNotification( - receiver, - actor, - request.title(), - request.message(), - request.redirectUrl(), - NotificationSettingType.SYSTEM - ); - } - case "ROOM" -> { - // 스터디룸 조회 - Room room = roomRepository.findById(request.targetId()) - .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - - // 발신자 조회 - User actor = userRepository.findById(request.actorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 스터디룸 알림 생성 - yield notificationService.createRoomNotification( - room, - actor, - request.title(), - request.message(), - request.redirectUrl(), - NotificationSettingType.ROOM_NOTICE - ); - } - case "COMMUNITY" -> { - // 수신자 조회 (리뷰/게시글 작성자) - User receiver = userRepository.findById(request.targetId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 발신자 조회 (댓글/좋아요 작성자) - User actor = userRepository.findById(request.actorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 커뮤니티 알림 생성 - yield notificationService.createCommunityNotification( - receiver, - actor, - request.title(), - request.message(), - request.redirectUrl(), - NotificationSettingType.POST_COMMENT - ); - } - case "SYSTEM" -> { - // 시스템 알림은 발신자/수신자 없음 - yield notificationService.createSystemNotification( - request.title(), - request.message(), - request.redirectUrl() - ); - } - default -> throw new CustomException(ErrorCode.NOTIFICATION_INVALID_TARGET_TYPE); - }; - - NotificationResponse response = NotificationResponse.from(notification); - - return ResponseEntity.ok(RsData.success("알림 전송 성공", response)); - } - - @Operation( - summary = "알림 목록 조회", - description = "사용자의 알림 목록 조회 (페이징)\n\n" + - "- unreadOnly=true: 읽지 않은 알림만\n" + - "- unreadOnly=false: 모든 알림" - ) + @Operation(summary = "알림 목록 조회") @GetMapping public ResponseEntity> getNotifications( @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, @@ -157,13 +45,10 @@ public ResponseEntity> getNotifications( log.info("알림 목록 조회 - 유저 ID: {}, 읽지 않은 것만: {}", userDetails.getUserId(), unreadOnly); Pageable pageable = PageRequest.of(page, size); - Page notifications; - if (unreadOnly) { - notifications = notificationService.getUnreadNotifications(userDetails.getUserId(), pageable); - } else { - notifications = notificationService.getUserNotifications(userDetails.getUserId(), pageable); - } + Page notifications = notificationService.getNotifications( + userDetails.getUserId(), pageable, unreadOnly + ); long unreadCount = notificationService.getUnreadCount(userDetails.getUserId()); @@ -221,14 +106,4 @@ public ResponseEntity> markAllAsRead( return ResponseEntity.ok(RsData.success("전체 알림 읽음 처리 성공")); } - - // ==================== 헬퍼 메서드 ==================== - - private boolean isValidTargetType(String targetType) { - return targetType != null && - (targetType.equals("USER") || - targetType.equals("ROOM") || - targetType.equals("COMMUNITY") || - targetType.equals("SYSTEM")); - } } \ No newline at end of file diff --git a/src/main/java/com/back/domain/notification/dto/NotificationCreateRequest.java b/src/main/java/com/back/domain/notification/dto/NotificationCreateRequest.java deleted file mode 100644 index cef2dd3f..00000000 --- a/src/main/java/com/back/domain/notification/dto/NotificationCreateRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.back.domain.notification.dto; - -public record NotificationCreateRequest( - String targetType, - Long targetId, - Long actorId, - String title, - String message, - String redirectUrl -) {} \ No newline at end of file diff --git a/src/main/java/com/back/domain/notification/dto/NotificationListResponse.java b/src/main/java/com/back/domain/notification/dto/NotificationListResponse.java index 180c1b6b..65876b66 100644 --- a/src/main/java/com/back/domain/notification/dto/NotificationListResponse.java +++ b/src/main/java/com/back/domain/notification/dto/NotificationListResponse.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import java.util.List; +import java.util.Set; /** * 알림 목록 응답 DTO @@ -27,9 +28,13 @@ public static NotificationListResponse from( long unreadCount, NotificationService notificationService) { + // 현재 페이지의 알림들에 대해 읽음 처리된 ID 목록을 한 번에 조회 + Set readNotificationIds = notificationService.getReadNotificationIds(userId, notifications.getContent()); + List items = notifications.getContent().stream() .map(notification -> { - boolean isRead = notificationService.isNotificationRead(notification.getId(), userId); + // DB 조회가 아닌 메모리에서 읽음 여부를 빠르게 확인 + boolean isRead = readNotificationIds.contains(notification.getId()); return NotificationItemDto.from(notification, isRead); }) .toList(); diff --git a/src/main/java/com/back/domain/notification/event/community/CommunityNotificationEventListener.java b/src/main/java/com/back/domain/notification/event/community/CommunityNotificationEventListener.java index 3f35cd1b..5f77d9a4 100644 --- a/src/main/java/com/back/domain/notification/event/community/CommunityNotificationEventListener.java +++ b/src/main/java/com/back/domain/notification/event/community/CommunityNotificationEventListener.java @@ -2,10 +2,6 @@ import com.back.domain.notification.entity.NotificationSettingType; import com.back.domain.notification.service.NotificationService; -import com.back.domain.user.entity.User; -import com.back.domain.user.repository.UserRepository; -import com.back.global.exception.CustomException; -import com.back.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -18,7 +14,6 @@ public class CommunityNotificationEventListener { private final NotificationService notificationService; - private final UserRepository userRepository; // 댓글 작성 시 - 게시글 작성자에게 알림 @EventListener @@ -28,15 +23,9 @@ public void handleCommentCreated(CommentCreatedEvent event) { event.getPostId(), event.getCommentId(), event.getActorId()); try { - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - User receiver = userRepository.findById(event.getReceiverId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createCommunityNotification( - receiver, - actor, + event.getReceiverId(), + event.getActorId(), event.getTitle(), event.getContent(), "/posts/" + event.getPostId(), @@ -58,15 +47,9 @@ public void handleReplyCreated(ReplyCreatedEvent event) { event.getParentCommentId(), event.getReplyId(), event.getActorId()); try { - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - User receiver = userRepository.findById(event.getReceiverId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createCommunityNotification( - receiver, - actor, + event.getReceiverId(), + event.getActorId(), event.getTitle(), event.getContent(), "/posts/" + event.getPostId() + "#comment-" + event.getParentCommentId(), @@ -88,15 +71,9 @@ public void handlePostLiked(PostLikedEvent event) { event.getPostId(), event.getActorId()); try { - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - User receiver = userRepository.findById(event.getReceiverId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createCommunityNotification( - receiver, - actor, + event.getReceiverId(), + event.getActorId(), event.getTitle(), event.getContent(), "/posts/" + event.getPostId(), @@ -118,15 +95,9 @@ public void handleCommentLiked(CommentLikedEvent event) { event.getCommentId(), event.getActorId()); try { - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - User receiver = userRepository.findById(event.getReceiverId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createCommunityNotification( - receiver, - actor, + event.getReceiverId(), + event.getActorId(), event.getTitle(), event.getContent(), "/posts/" + event.getPostId() + "#comment-" + event.getCommentId(), diff --git a/src/main/java/com/back/domain/notification/event/study/StudyNotificationEventListener.java b/src/main/java/com/back/domain/notification/event/study/StudyNotificationEventListener.java index 2c780bd4..7c6b846e 100644 --- a/src/main/java/com/back/domain/notification/event/study/StudyNotificationEventListener.java +++ b/src/main/java/com/back/domain/notification/event/study/StudyNotificationEventListener.java @@ -18,7 +18,6 @@ public class StudyNotificationEventListener { private final NotificationService notificationService; - private final UserRepository userRepository; // 학습 기록 등록 시 - 본인에게 알림 @EventListener @@ -28,11 +27,8 @@ public void handleStudyRecordCreated(StudyRecordCreatedEvent event) { event.getUserId(), event.getDuration()); try { - User user = userRepository.findById(event.getUserId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createSelfNotification( - user, + event.getUserId(), event.getTitle(), event.getContent(), "/study/records/" + event.getStudyRecordId(), @@ -55,11 +51,8 @@ public void handleDailyGoalAchieved(DailyGoalAchievedEvent event) { event.getCompletedPlans(), event.getTotalPlans()); try { - User user = userRepository.findById(event.getUserId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createSelfNotification( - user, + event.getUserId(), event.getTitle(), event.getContent(), "/study/plans?date=" + event.getAchievedDate(), diff --git a/src/main/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListener.java b/src/main/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListener.java index 361ef042..c99544c8 100644 --- a/src/main/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListener.java +++ b/src/main/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListener.java @@ -21,9 +21,6 @@ public class StudyRoomNotificationEventListener { private final NotificationService notificationService; - private final RoomRepository roomRepository; - private final UserRepository userRepository; - private final RoomMemberRepository roomMemberRepository; // 스터디룸 공지사항 등록 시 - 전체 멤버에게 알림 @EventListener @@ -33,18 +30,9 @@ public void handleNoticeCreated(StudyRoomNoticeCreatedEvent event) { event.getStudyRoomId(), event.getActorId()); try { - // Room 조회 - Room room = roomRepository.findById(event.getStudyRoomId()) - .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - - // Actor (공지 작성자) 조회 - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 스터디룸 알림 생성 notificationService.createRoomNotification( - room, - actor, + event.getStudyRoomId(), + event.getActorId(), event.getTitle(), event.getNoticeTitle(), "/rooms/" + event.getStudyRoomId() + "/notices", @@ -67,18 +55,9 @@ public void handleMemberRoleChanged(MemberRoleChangedEvent event) { event.getStudyRoomId(), event.getTargetUserId(), event.getNewRole()); try { - // 수신자 조회 - User receiver = userRepository.findById(event.getTargetUserId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 발신자 조회 - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - // 개인 알림 생성 notificationService.createPersonalNotification( - receiver, - actor, + event.getTargetUserId(), + event.getActorId(), event.getTitle(), event.getContent(), "/rooms/" + event.getStudyRoomId(), @@ -100,15 +79,9 @@ public void handleMemberKicked(MemberKickedEvent event) { event.getStudyRoomId(), event.getTargetUserId()); try { - User receiver = userRepository.findById(event.getTargetUserId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createPersonalNotification( - receiver, - actor, + event.getTargetUserId(), + event.getActorId(), event.getTitle(), event.getContent(), "/rooms", @@ -130,15 +103,9 @@ public void handleOwnerTransferred(OwnerTransferredEvent event) { event.getStudyRoomId(), event.getNewOwnerId()); try { - User receiver = userRepository.findById(event.getNewOwnerId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - User actor = userRepository.findById(event.getActorId()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - notificationService.createPersonalNotification( - receiver, - actor, + event.getNewOwnerId(), + event.getActorId(), event.getTitle(), event.getContent(), "/rooms/" + event.getStudyRoomId(), diff --git a/src/main/java/com/back/domain/notification/repository/NotificationReadRepository.java b/src/main/java/com/back/domain/notification/repository/NotificationReadRepository.java index 53a5cc8d..d1d73205 100644 --- a/src/main/java/com/back/domain/notification/repository/NotificationReadRepository.java +++ b/src/main/java/com/back/domain/notification/repository/NotificationReadRepository.java @@ -5,7 +5,7 @@ import java.util.Optional; -public interface NotificationReadRepository extends JpaRepository { +public interface NotificationReadRepository extends JpaRepository, NotificationReadRepositoryCustom { // 특정 유저가 특정 알림을 읽었는지 확인 boolean existsByNotificationIdAndUserId(Long notificationId, Long userId); diff --git a/src/main/java/com/back/domain/notification/repository/NotificationReadRepositoryCustom.java b/src/main/java/com/back/domain/notification/repository/NotificationReadRepositoryCustom.java new file mode 100644 index 00000000..372c8bef --- /dev/null +++ b/src/main/java/com/back/domain/notification/repository/NotificationReadRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.back.domain.notification.repository; + +import java.util.List; +import java.util.Set; + +public interface NotificationReadRepositoryCustom { + + // 특정 유저가 읽은 알림 ID 목록을 한 번에 조회 + Set findReadNotificationIds(Long userId, List notificationIds); +} diff --git a/src/main/java/com/back/domain/notification/repository/NotificationReadRepositoryImpl.java b/src/main/java/com/back/domain/notification/repository/NotificationReadRepositoryImpl.java new file mode 100644 index 00000000..d0686024 --- /dev/null +++ b/src/main/java/com/back/domain/notification/repository/NotificationReadRepositoryImpl.java @@ -0,0 +1,35 @@ +package com.back.domain.notification.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.back.domain.notification.entity.QNotificationRead.notificationRead; + +@RequiredArgsConstructor +public class NotificationReadRepositoryImpl implements NotificationReadRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Set findReadNotificationIds(Long userId, List notificationIds) { + + if (notificationIds == null || notificationIds.isEmpty()) { + return new HashSet<>(); + } + + List readIds = queryFactory + .select(notificationRead.notification.id) + .from(notificationRead) + .where( + notificationRead.user.id.eq(userId), + notificationRead.notification.id.in(notificationIds) + ) + .fetch(); + + return new HashSet<>(readIds); + } +} \ No newline at end of file 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 91fb50b5..5a45f28a 100644 --- a/src/main/java/com/back/domain/notification/service/NotificationService.java +++ b/src/main/java/com/back/domain/notification/service/NotificationService.java @@ -7,7 +7,9 @@ import com.back.domain.notification.repository.NotificationReadRepository; import com.back.domain.notification.repository.NotificationRepository; import com.back.domain.studyroom.entity.Room; +import com.back.domain.studyroom.repository.RoomRepository; import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; @@ -17,7 +19,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; import java.util.List; +import java.util.Set; @Slf4j @Service @@ -28,79 +32,86 @@ public class NotificationService { private final NotificationReadRepository notificationReadRepository; private final NotificationWebSocketService webSocketService; private final NotificationSettingService notificationSettingService; + private final UserRepository userRepository; + private final RoomRepository roomRepository; // ==================== 알림 생성 및 전송 ==================== // 개인 알림 생성 및 전송 @Transactional public Notification createPersonalNotification( - User receiver, - User actor, + Long receiverId, + Long actorId, String title, String content, String targetUrl, NotificationSettingType settingType) { + User receiver = userRepository.findById(receiverId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + User actor = userRepository.findById(actorId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 자기 자신에게 알림 방지 validateActorAndReceiver(receiver, actor); // DB에 알림 저장 Notification notification = Notification.createPersonalNotification( receiver, actor, title, content, targetUrl); - notificationRepository.save(notification); + Notification savedNotification = notificationRepository.save(notification); - // 알림 설정 체크 후 전송 if (shouldSendNotification(receiver.getId(), settingType)) { - NotificationWebSocketDto dto = NotificationWebSocketDto.from(notification); + NotificationWebSocketDto dto = NotificationWebSocketDto.from(savedNotification); // ID가 있는 객체 사용 webSocketService.sendNotificationToUser(receiver.getId(), dto); - log.info("개인 알림 전송 - 수신자 ID: {}, 발신자 ID: {}, 알림 ID: {}, 설정 타입: {}", - receiver.getId(), actor.getId(), notification.getId(), settingType); + + log.info("개인 알림 전송 - 수신자 ID: {}, ... 알림 ID: {}", receiver.getId(), savedNotification.getId()); } else { - log.info("개인 알림 저장만 완료 (전송 생략) - 수신자 ID: {}, 알림 ID: {}, 설정 타입: {}", - receiver.getId(), notification.getId(), settingType); + log.info("개인 알림 저장만 완료 - ... 알림 ID: {}", receiver.getId(), savedNotification.getId()); } - return notification; + return savedNotification; } // 개인 알림 생성 및 전송 @Transactional public Notification createSelfNotification( - User user, + Long userId, String title, String content, String targetUrl, NotificationSettingType settingType) { + User user = userRepository.findById(userId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // DB에 알림 저장 Notification notification = Notification.createPersonalNotification( user, user, title, content, targetUrl); - notificationRepository.save(notification); + Notification savedNotification = notificationRepository.save(notification); // 알림 설정 체크 후 전송 if (shouldSendNotification(user.getId(), settingType)) { - NotificationWebSocketDto dto = NotificationWebSocketDto.from(notification); + NotificationWebSocketDto dto = NotificationWebSocketDto.from(savedNotification); webSocketService.sendNotificationToUser(user.getId(), dto); - log.info("자기 자신 알림 전송 - 사용자 ID: {}, 알림 ID: {}, 설정 타입: {}", - user.getId(), notification.getId(), settingType); + + log.info("자기 자신 알림 전송 - ... 알림 ID: {}", user.getId(), savedNotification.getId()); } else { - log.info("자기 자신 알림 저장만 완료 (전송 생략) - 사용자 ID: {}, 알림 ID: {}, 설정 타입: {}", - user.getId(), notification.getId(), settingType); + log.info("자기 자신 알림 저장만 완료 - ... 알림 ID: {}", user.getId(), savedNotification.getId()); } - return notification; + return savedNotification; } // 스터디룸 알림 생성 및 전송 @Transactional public Notification createRoomNotification( - Room room, - User actor, + Long roomId, + Long actorId, String title, String content, String targetUrl, NotificationSettingType settingType) { + Room room = roomRepository.findById(roomId).orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); + User actor = userRepository.findById(actorId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + Notification notification = Notification.createRoomNotification( room, actor, title, content, targetUrl); notificationRepository.save(notification); @@ -132,13 +143,16 @@ public Notification createSystemNotification(String title, String content, Strin // 커뮤니티 알림 생성 및 전송 @Transactional public Notification createCommunityNotification( - User receiver, - User actor, + Long receiverId, + Long actorId, String title, String content, String targetUrl, NotificationSettingType settingType) { + User receiver = userRepository.findById(receiverId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + User actor = userRepository.findById(actorId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + // 자기 자신에게 알림 방지 validateActorAndReceiver(receiver, actor); @@ -162,16 +176,14 @@ public Notification createCommunityNotification( // ==================== 알림 조회 ==================== - // 유저의 모든 알림 조회 - @Transactional(readOnly = true) - public Page getUserNotifications(Long userId, Pageable pageable) { - return notificationRepository.findByUserIdOrSystemType(userId, pageable); - } - - // 유저의 읽지 않은 알림 조회 + // 유저의 알림 목록 조회 @Transactional(readOnly = true) - public Page getUnreadNotifications(Long userId, Pageable pageable) { - return notificationRepository.findUnreadByUserId(userId, pageable); + public Page getNotifications(Long userId, Pageable pageable, boolean unreadOnly) { + if (unreadOnly) { + return notificationRepository.findUnreadByUserId(userId, pageable); + } else { + return notificationRepository.findByUserIdOrSystemType(userId, pageable); + } } // 유저의 읽지 않은 알림 개수 조회 @@ -193,6 +205,20 @@ public boolean isNotificationRead(Long notificationId, Long userId) { return notificationReadRepository.existsByNotificationIdAndUserId(notificationId, userId); } + // 여러 알림 중 유저가 읽은 알림 ID 목록 조회 + @Transactional(readOnly = true) + public Set getReadNotificationIds(Long userId, List notifications) { + if (notifications == null || notifications.isEmpty()) { + return Collections.emptySet(); + } + + List notificationIds = notifications.stream() + .map(Notification::getId) + .toList(); + + return notificationReadRepository.findReadNotificationIds(userId, notificationIds); + } + // ==================== 알림 읽음 처리 ==================== // 알림 읽음 처리 diff --git a/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java b/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java index 63ab3dc7..6a8e60e3 100644 --- a/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java +++ b/src/test/java/com/back/domain/notification/controller/NotificationControllerTest.java @@ -1,6 +1,5 @@ package com.back.domain.notification.controller; -import com.back.domain.notification.dto.NotificationCreateRequest; import com.back.domain.notification.entity.Notification; import com.back.domain.notification.service.NotificationService; import com.back.domain.studyroom.repository.RoomRepository; @@ -21,17 +20,20 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; @@ -104,33 +106,10 @@ void setUp() { )); } - @Nested - @DisplayName("알림 전송") - class CreateNotificationTest { - - @Test - @DisplayName("개인 알림 생성 성공") - void t1() throws Exception { - // given - NotificationCreateRequest request = new NotificationCreateRequest( - "USER", 1L, 2L, "개인 알림", "내용", "/target" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(2L)).willReturn(Optional.of(actor)); - given(notificationService.createPersonalNotification(any(), any(), any(), any(), any(), any())) - .willReturn(notification); - - // when - ResultActions result = mockMvc.perform(post("/api/notifications") - .header("Authorization", "Bearer faketoken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - - // then - result.andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)); - } + // 테스트용 Authentication 객체 생성 헬퍼 + private Authentication createMockAuthentication(Long userId) { + CustomUserDetails userDetails = CustomUserDetails.builder().userId(userId).build(); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } @Nested @@ -141,34 +120,57 @@ class GetNotificationsTest { @DisplayName("알림 목록 조회 성공") void t1() throws Exception { // given - Page notificationPage = new PageImpl<>(List.of(notification)); + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 20); + + Notification notification1 = Notification.createSystemNotification("읽은 알림", "", ""); + ReflectionTestUtils.setField(notification1, "id", 100L); + Notification notification2 = Notification.createSystemNotification("읽지 않은 알림", "", ""); + ReflectionTestUtils.setField(notification2, "id", 101L); - given(notificationService.getUserNotifications(anyLong(), any(Pageable.class))) + Page notificationPage = new PageImpl<>(List.of(notification1, notification2)); + + given(notificationService.getNotifications(eq(userId), any(Pageable.class), eq(false))) .willReturn(notificationPage); - given(notificationService.getUnreadCount(anyLong())).willReturn(3L); - given(notificationService.isNotificationRead(anyLong(), anyLong())).willReturn(false); + + Set readIds = Set.of(100L); + given(notificationService.getReadNotificationIds(eq(userId), eq(notificationPage.getContent()))) + .willReturn(readIds); + + given(notificationService.getUnreadCount(userId)).willReturn(1L); // when ResultActions result = mockMvc.perform(get("/api/notifications") .header("Authorization", "Bearer faketoken") .param("page", "0") - .param("size", "20")); + .param("size", "20") + .param("unreadOnly", "false")); // unreadOnly=false 명시 // then result.andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)); + .andExpect(jsonPath("$.data.content.length()").value(2)) + .andExpect(jsonPath("$.data.unreadCount").value(1)) + .andExpect(jsonPath("$.data.content[0].isRead").value(true)) + .andExpect(jsonPath("$.data.content[1].isRead").value(false)); + + verify(notificationService).getNotifications(eq(userId), any(Pageable.class), eq(false)); } @Test @DisplayName("읽지 않은 알림만 조회") void t2() throws Exception { // given - Page notificationPage = new PageImpl<>(List.of(notification)); + Long userId = 1L; + Notification unreadNotification = Notification.createSystemNotification("읽지 않은 알림", "", ""); + ReflectionTestUtils.setField(unreadNotification, "id", 102L); + + Page notificationPage = new PageImpl<>(List.of(unreadNotification)); - given(notificationService.getUnreadNotifications(anyLong(), any(Pageable.class))) + given(notificationService.getNotifications(eq(userId), any(Pageable.class), eq(true))) .willReturn(notificationPage); - given(notificationService.getUnreadCount(anyLong())).willReturn(1L); - given(notificationService.isNotificationRead(anyLong(), anyLong())).willReturn(false); + + given(notificationService.getReadNotificationIds(eq(userId), anyList())).willReturn(Set.of()); + given(notificationService.getUnreadCount(userId)).willReturn(1L); // when ResultActions result = mockMvc.perform(get("/api/notifications") @@ -176,8 +178,10 @@ void t2() throws Exception { .param("unreadOnly", "true")); // then - result.andExpect(status().isOk()); - verify(notificationService).getUnreadNotifications(anyLong(), any(Pageable.class)); + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].isRead").value(false)); + + verify(notificationService).getNotifications(eq(userId), any(Pageable.class), eq(true)); } } diff --git a/src/test/java/com/back/domain/notification/event/community/CommunityNotificationEventListenerTest.java b/src/test/java/com/back/domain/notification/event/community/CommunityNotificationEventListenerTest.java index 9f5a3a7c..7084ef43 100644 --- a/src/test/java/com/back/domain/notification/event/community/CommunityNotificationEventListenerTest.java +++ b/src/test/java/com/back/domain/notification/event/community/CommunityNotificationEventListenerTest.java @@ -2,9 +2,6 @@ import com.back.domain.notification.entity.NotificationSettingType; import com.back.domain.notification.service.NotificationService; -import com.back.domain.user.entity.User; -import com.back.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,8 +9,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Optional; - import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; @@ -25,56 +20,26 @@ class CommunityNotificationEventListenerTest { @Mock private NotificationService notificationService; - @Mock - private UserRepository userRepository; - @InjectMocks private CommunityNotificationEventListener listener; - private User actor; - private User receiver; - - @BeforeEach - void setUp() { - actor = User.builder() - .id(1L) - .username("actor") - .email("actor@test.com") - .build(); - - receiver = User.builder() - .id(2L) - .username("receiver") - .email("receiver@test.com") - .build(); - } - // ====================== 댓글 작성 이벤트 ====================== @Test @DisplayName("댓글 작성 이벤트 수신 - 알림 생성 성공") void t1() { // given - CommentCreatedEvent event = new CommentCreatedEvent( - 1L, // actorId (댓글 작성자) - 2L, // receiverId (게시글 작성자) - 100L, // postId - 200L, // commentId - "댓글 내용" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); + CommentCreatedEvent event = new CommentCreatedEvent(1L, 2L, 100L, 200L, "댓글"); // when listener.handleCommentCreated(event); // then verify(notificationService).createCommunityNotification( - eq(receiver), - eq(actor), - anyString(), // title - anyString(), // content + eq(2L), // receiverId + eq(1L), // actorId + anyString(), + anyString(), eq("/posts/100"), eq(NotificationSettingType.POST_COMMENT) ); @@ -84,22 +49,19 @@ void t1() { @DisplayName("댓글 작성 이벤트 - 작성자(actor) 없음") void t2() { // given - CommentCreatedEvent event = new CommentCreatedEvent( - 999L, // 존재하지 않는 actorId - 2L, - 100L, - 200L, - "댓글 내용" - ); - - given(userRepository.findById(999L)).willReturn(Optional.empty()); + ReplyCreatedEvent event = new ReplyCreatedEvent(1L, 2L, 100L, 200L, 300L, "대댓글"); // when - listener.handleCommentCreated(event); + listener.handleReplyCreated(event); // then - verify(notificationService, never()).createCommunityNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) + verify(notificationService).createCommunityNotification( + eq(2L), // receiverId + eq(1L), // actorId + anyString(), + anyString(), + eq("/posts/100#comment-200"), + eq(NotificationSettingType.POST_COMMENT) ); } @@ -107,23 +69,19 @@ void t2() { @DisplayName("댓글 작성 이벤트 - 수신자(receiver) 없음") void t3() { // given - CommentCreatedEvent event = new CommentCreatedEvent( - 1L, - 999L, // 존재하지 않는 receiverId - 100L, - 200L, - "댓글 내용" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(999L)).willReturn(Optional.empty()); + PostLikedEvent event = new PostLikedEvent(1L, 2L, 100L, "제목"); // when - listener.handleCommentCreated(event); + listener.handlePostLiked(event); // then - verify(notificationService, never()).createCommunityNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) + verify(notificationService).createCommunityNotification( + eq(2L), // receiverId + eq(1L), // actorId + anyString(), + anyString(), + eq("/posts/100"), + eq(NotificationSettingType.POST_LIKE) ); } @@ -133,29 +91,19 @@ void t3() { @DisplayName("대댓글 작성 이벤트 수신 - 알림 생성 성공") void t4() { // given - ReplyCreatedEvent event = new ReplyCreatedEvent( - 1L, // actorId (대댓글 작성자) - 2L, // receiverId (댓글 작성자) - 100L, // postId - 200L, // parentCommentId - 300L, // replyId - "대댓글 내용" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); + CommentLikedEvent event = new CommentLikedEvent(1L, 2L, 100L, 200L, "댓글"); // when - listener.handleReplyCreated(event); + listener.handleCommentLiked(event); // then verify(notificationService).createCommunityNotification( - eq(receiver), - eq(actor), + eq(2L), // receiverId + eq(1L), // actorId anyString(), anyString(), eq("/posts/100#comment-200"), - eq(NotificationSettingType.POST_COMMENT) + eq(NotificationSettingType.POST_LIKE) ); } @@ -163,24 +111,12 @@ void t4() { @DisplayName("대댓글 작성 이벤트 - 작성자(actor) 없음") void t5() { // given - ReplyCreatedEvent event = new ReplyCreatedEvent( - 999L, // 존재하지 않는 actorId - 2L, - 100L, - 200L, - 300L, - "대댓글 내용" - ); + CommentCreatedEvent event = new CommentCreatedEvent(1L, 2L, 100L, 200L, "댓글"); + willThrow(new RuntimeException("DB 오류")).given(notificationService).createCommunityNotification(anyLong(), anyLong(), any(), any(), any(), any()); - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleReplyCreated(event); - - // then - verify(notificationService, never()).createCommunityNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); + // when & then + assertThatCode(() -> listener.handleCommentCreated(event)) + .doesNotThrowAnyException(); } // ====================== 게시글 좋아요 이벤트 ====================== @@ -189,23 +125,15 @@ void t5() { @DisplayName("게시글 좋아요 이벤트 수신 - 알림 생성 성공") void t6() { // given - PostLikedEvent event = new PostLikedEvent( - 1L, // actorId (좋아요 누른 사람) - 2L, // receiverId (게시글 작성자) - 100L, // postId - "게시글 제목" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); + PostLikedEvent event = new PostLikedEvent(1L, 2L, 100L, "게시글 제목"); // when listener.handlePostLiked(event); // then verify(notificationService).createCommunityNotification( - eq(receiver), - eq(actor), + eq(2L), // receiverId + eq(1L), // actorId anyString(), anyString(), eq("/posts/100"), @@ -213,53 +141,21 @@ void t6() { ); } - @Test - @DisplayName("게시글 좋아요 이벤트 - 수신자 없음") - void t7() { - // given - PostLikedEvent event = new PostLikedEvent( - 1L, - 999L, // 존재하지 않는 receiverId - 100L, - "게시글 제목" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handlePostLiked(event); - - // then - verify(notificationService, never()).createCommunityNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - // ====================== 댓글 좋아요 이벤트 ====================== @Test @DisplayName("댓글 좋아요 이벤트 수신 - 알림 생성 성공") void t8() { // given - CommentLikedEvent event = new CommentLikedEvent( - 1L, // actorId - 2L, // receiverId - 100L, // postId - 200L, // commentId, - "댓글 내용" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); + CommentLikedEvent event = new CommentLikedEvent(1L, 2L, 100L, 200L, "댓글 내용"); // when listener.handleCommentLiked(event); // then verify(notificationService).createCommunityNotification( - eq(receiver), - eq(actor), + eq(2L), // receiverId + eq(1L), // actorId anyString(), anyString(), eq("/posts/100#comment-200"), @@ -279,16 +175,14 @@ void t9() { "댓글 내용" ); - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - // when listener.handleCommentLiked(event); // then - // NotificationService에서 자기 자신 알림 필터링 처리 + // 서비스에 동일한 ID가 전달되는지만 확인하면 됨 verify(notificationService).createCommunityNotification( - eq(actor), // receiver - eq(actor), // actor + eq(1L), // receiverId + eq(1L), // actorId anyString(), anyString(), anyString(), @@ -302,24 +196,17 @@ void t9() { @DisplayName("알림 생성 중 예외 발생 - 로그만 출력하고 예외 전파 안함") void t10() { // given - CommentCreatedEvent event = new CommentCreatedEvent( - 1L, 2L, 100L, 200L, "댓글 내용" - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); + CommentCreatedEvent event = new CommentCreatedEvent(1L, 2L, 100L, 200L, "댓글 내용"); - willThrow(new RuntimeException("알림 생성 실패")) - .given(notificationService).createCommunityNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); + willThrow(new RuntimeException("DB 오류")) + .given(notificationService) + .createCommunityNotification(anyLong(), anyLong(), any(), any(), any(), any()); - // when & then - 예외가 전파되지 않아야 함 + // when & then assertThatCode(() -> listener.handleCommentCreated(event)) .doesNotThrowAnyException(); - verify(notificationService).createCommunityNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); + // 서비스 메서드가 호출된 것 자체는 검증 + verify(notificationService).createCommunityNotification(anyLong(), anyLong(), any(), any(), any(), any()); } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/notification/event/study/StudyNotificationEventListenerTest.java b/src/test/java/com/back/domain/notification/event/study/StudyNotificationEventListenerTest.java index e667d637..242b070a 100644 --- a/src/test/java/com/back/domain/notification/event/study/StudyNotificationEventListenerTest.java +++ b/src/test/java/com/back/domain/notification/event/study/StudyNotificationEventListenerTest.java @@ -2,9 +2,6 @@ import com.back.domain.notification.entity.NotificationSettingType; import com.back.domain.notification.service.NotificationService; -import com.back.domain.user.entity.User; -import com.back.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,7 +10,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; -import java.util.Optional; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -26,118 +22,21 @@ class StudyNotificationEventListenerTest { @Mock private NotificationService notificationService; - @Mock - private UserRepository userRepository; - @InjectMocks private StudyNotificationEventListener listener; - private User testUser; - - @BeforeEach - void setUp() { - testUser = User.builder() - .id(1L) - .username("testuser") - .email("test@test.com") - .build(); - } - - // ====================== 학습 기록 생성 이벤트 ====================== - @Test - @DisplayName("학습 기록 생성 이벤트 수신 - 알림 생성 성공") + @DisplayName("학습 기록 생성 이벤트") void t1() { // given - StudyRecordCreatedEvent event = new StudyRecordCreatedEvent( - 1L, // userId - 100L, // studyRecordId - 50L, // studyPlanId - 3600L // duration (1시간) - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); + StudyRecordCreatedEvent event = new StudyRecordCreatedEvent(1L, 100L, 200L, 3600L); // when listener.handleStudyRecordCreated(event); // then verify(notificationService).createSelfNotification( - eq(testUser), // 본인 - anyString(), // title - anyString(), // content - eq("/study/records/100"), - eq(NotificationSettingType.SYSTEM) - ); - } - - @Test - @DisplayName("학습 기록 생성 이벤트 - 사용자 없음") - void t2() { - // given - StudyRecordCreatedEvent event = new StudyRecordCreatedEvent( - 999L, // 존재하지 않는 userId - 100L, - 50L, - 3600L - ); - - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleStudyRecordCreated(event); - - // then - verify(notificationService, never()).createSelfNotification( - any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - @Test - @DisplayName("학습 기록 생성 이벤트 - 짧은 학습 시간") - void t3() { - // given - StudyRecordCreatedEvent event = new StudyRecordCreatedEvent( - 1L, - 100L, - 50L, - 300L // 5분 - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - // when - listener.handleStudyRecordCreated(event); - - // then - verify(notificationService).createSelfNotification( - eq(testUser), - anyString(), - anyString(), - eq("/study/records/100"), - eq(NotificationSettingType.SYSTEM) - ); - } - - @Test - @DisplayName("학습 기록 생성 이벤트 - 긴 학습 시간") - void t4() { - // given - StudyRecordCreatedEvent event = new StudyRecordCreatedEvent( - 1L, - 100L, - 50L, - 14400L // 4시간 - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - // when - listener.handleStudyRecordCreated(event); - - // then - verify(notificationService).createSelfNotification( - eq(testUser), + eq(1L), // userId anyString(), anyString(), eq("/study/records/100"), @@ -145,105 +44,19 @@ void t4() { ); } - // ====================== 일일 목표 달성 이벤트 ====================== - - @Test - @DisplayName("일일 목표 달성 이벤트 수신 - 알림 생성 성공") - void t5() { - // given - LocalDate today = LocalDate.now(); - DailyGoalAchievedEvent event = new DailyGoalAchievedEvent( - 1L, // userId - today, // achievedDate - 5, // completedPlans - 5 // totalPlans - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - // when - listener.handleDailyGoalAchieved(event); - - // then - verify(notificationService).createSelfNotification( - eq(testUser), // 본인 - anyString(), // title - anyString(), // content - eq("/study/plans?date=" + today), - eq(NotificationSettingType.SYSTEM) - ); - } - @Test - @DisplayName("일일 목표 달성 이벤트 - 사용자 없음") - void t6() { - // given - LocalDate today = LocalDate.now(); - DailyGoalAchievedEvent event = new DailyGoalAchievedEvent( - 999L, // 존재하지 않는 userId - today, - 5, - 5 - ); - - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleDailyGoalAchieved(event); - - // then - verify(notificationService, never()).createSelfNotification( - any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - @Test - @DisplayName("일일 목표 달성 이벤트 - 부분 달성") - void t7() { - // given - LocalDate today = LocalDate.now(); - DailyGoalAchievedEvent event = new DailyGoalAchievedEvent( - 1L, - today, - 3, // completedPlans - 5 // totalPlans - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - // when - listener.handleDailyGoalAchieved(event); - - // then - verify(notificationService).createSelfNotification( - eq(testUser), - anyString(), - anyString(), - eq("/study/plans?date=" + today), - eq(NotificationSettingType.SYSTEM) - ); - } - - @Test - @DisplayName("일일 목표 달성 이벤트 - 초과 달성") - void t8() { + @DisplayName("일일 목표 달성 이벤트") + void t2() { // given LocalDate today = LocalDate.now(); - DailyGoalAchievedEvent event = new DailyGoalAchievedEvent( - 1L, - today, - 7, // completedPlans - 5 // totalPlans (목표보다 많이 달성) - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); + DailyGoalAchievedEvent event = new DailyGoalAchievedEvent(1L, today, 5, 5); // when listener.handleDailyGoalAchieved(event); // then verify(notificationService).createSelfNotification( - eq(testUser), + eq(1L), // userId anyString(), anyString(), eq("/study/plans?date=" + today), @@ -252,80 +65,19 @@ void t8() { } @Test - @DisplayName("일일 목표 달성 이벤트 - 과거 날짜") - void t9() { - // given - LocalDate yesterday = LocalDate.now().minusDays(1); - DailyGoalAchievedEvent event = new DailyGoalAchievedEvent( - 1L, - yesterday, - 5, - 5 - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - // when - listener.handleDailyGoalAchieved(event); - - // then - verify(notificationService).createSelfNotification( - eq(testUser), - anyString(), - anyString(), - eq("/study/plans?date=" + yesterday), - eq(NotificationSettingType.SYSTEM) - ); - } - - // ====================== 예외 처리 테스트 ====================== - - @Test - @DisplayName("학습 기록 알림 생성 중 예외 발생 - 로그만 출력하고 예외 전파 안함") - void t10() { + @DisplayName("알림 생성 중 예외 발생 - 로그만 출력하고 예외 전파 안함") + void t3() { // given - StudyRecordCreatedEvent event = new StudyRecordCreatedEvent( - 1L, 100L, 3600L, 50L - ); + StudyRecordCreatedEvent event = new StudyRecordCreatedEvent(1L, 100L, 200L, 3600L); + willThrow(new RuntimeException("DB 오류")) + .given(notificationService) + .createSelfNotification(anyLong(), anyString(), anyString(), anyString(), any()); - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - willThrow(new RuntimeException("알림 생성 실패")) - .given(notificationService).createSelfNotification( - any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - - // when & then - 예외가 전파되지 않아야 함 + // when & then assertThatCode(() -> listener.handleStudyRecordCreated(event)) .doesNotThrowAnyException(); - verify(notificationService).createSelfNotification( - any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - @Test - @DisplayName("일일 목표 달성 알림 생성 중 예외 발생 - 로그만 출력하고 예외 전파 안함") - void t11() { - // given - LocalDate today = LocalDate.now(); - DailyGoalAchievedEvent event = new DailyGoalAchievedEvent( - 1L, today, 5, 5 - ); - - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - - willThrow(new RuntimeException("알림 생성 실패")) - .given(notificationService).createSelfNotification( - any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - - // when & then - 예외가 전파되지 않아야 함 - assertThatCode(() -> listener.handleDailyGoalAchieved(event)) - .doesNotThrowAnyException(); - - verify(notificationService).createSelfNotification( - any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); + // 서비스 메서드가 호출된 것 자체는 검증 + verify(notificationService).createSelfNotification(anyLong(), anyString(), anyString(), anyString(), any()); } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListenerTest.java b/src/test/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListenerTest.java index 75f36eeb..ed1b0b64 100644 --- a/src/test/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListenerTest.java +++ b/src/test/java/com/back/domain/notification/event/studyroom/StudyRoomNotificationEventListenerTest.java @@ -2,12 +2,6 @@ import com.back.domain.notification.entity.NotificationSettingType; import com.back.domain.notification.service.NotificationService; -import com.back.domain.studyroom.entity.Room; -import com.back.domain.studyroom.repository.RoomMemberRepository; -import com.back.domain.studyroom.repository.RoomRepository; -import com.back.domain.user.entity.User; -import com.back.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,8 +9,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Optional; - import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; @@ -28,147 +20,42 @@ class StudyRoomNotificationEventListenerTest { @Mock private NotificationService notificationService; - @Mock - private RoomRepository roomRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private RoomMemberRepository roomMemberRepository; - @InjectMocks private StudyRoomNotificationEventListener listener; - private User actor; - private User receiver; - private Room room; - - @BeforeEach - void setUp() { - actor = User.builder() - .id(1L) - .username("actor") - .email("actor@test.com") - .build(); - - receiver = User.builder() - .id(2L) - .username("receiver") - .email("receiver@test.com") - .build(); - - room = Room.create( - "테스트 방", - "설명", - false, - null, - 10, - actor, - null, - true, // useWebRTC - null // thumbnailUrl - ); - } - - // ====================== 스터디룸 공지사항 이벤트 ====================== - @Test - @DisplayName("스터디룸 공지사항 생성 이벤트 수신 - 알림 생성 성공") + @DisplayName("스터디룸 공지사항 생성 이벤트") void t1() { // given - StudyRoomNoticeCreatedEvent event = new StudyRoomNoticeCreatedEvent( - 1L, // actorId - 100L, // roomId - "공지사항 제목입니다", - "공지사항 내용입니다" - ); - - given(roomRepository.findById(100L)).willReturn(Optional.of(room)); - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); + StudyRoomNoticeCreatedEvent event = new StudyRoomNoticeCreatedEvent(1L, 10L, "공지 제목", "내용"); // when listener.handleNoticeCreated(event); // then verify(notificationService).createRoomNotification( - eq(room), - eq(actor), - anyString(), // title - eq("공지사항 제목입니다"), // content (공지 제목) - eq("/rooms/100/notices"), + eq(10L), // roomId + eq(1L), // actorId + anyString(), + anyString(), + anyString(), eq(NotificationSettingType.ROOM_NOTICE) ); } @Test - @DisplayName("스터디룸 공지사항 이벤트 - 방 없음") + @DisplayName("멤버 권한 변경 이벤트") void t2() { // given - StudyRoomNoticeCreatedEvent event = new StudyRoomNoticeCreatedEvent( - 1L, - 999L, // 존재하지 않는 roomId - "공지사항 제목", - "공지사항 내용입니다" - ); - - given(roomRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleNoticeCreated(event); - - // then - verify(notificationService, never()).createRoomNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - @Test - @DisplayName("스터디룸 공지사항 이벤트 - 작성자 없음") - void t3() { - // given - StudyRoomNoticeCreatedEvent event = new StudyRoomNoticeCreatedEvent( - 999L, // 존재하지 않는 actorId - 100L, - "공지사항 제목", - "공지사항 내용입니다" - ); - - given(roomRepository.findById(100L)).willReturn(Optional.of(room)); - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleNoticeCreated(event); - - // then - verify(notificationService, never()).createRoomNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - // ====================== 멤버 권한 변경 이벤트 ====================== - - @Test - @DisplayName("멤버 권한 변경 이벤트 수신 - 알림 생성 성공") - void t4() { - // given - MemberRoleChangedEvent event = new MemberRoleChangedEvent( - 1L, // actorId (권한 변경한 사람) - 100L, // roomId - 2L, // targetUserId (권한 변경된 사람) - "MANAGER" // newRole - ); - - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); + MemberRoleChangedEvent event = new MemberRoleChangedEvent(1L, 100L, 2L, "MANAGER"); // when listener.handleMemberRoleChanged(event); // then verify(notificationService).createPersonalNotification( - eq(receiver), // 권한 변경된 사람이 수신자 - eq(actor), // 권한 변경한 사람이 actor + eq(2L), // receiverId (targetUserId) + eq(1L), // actorId anyString(), anyString(), eq("/rooms/100"), @@ -177,125 +64,38 @@ void t4() { } @Test - @DisplayName("멤버 권한 변경 이벤트 - 수신자 없음") - void t5() { - // given - MemberRoleChangedEvent event = new MemberRoleChangedEvent( - 1L, - 100L, - 999L, // 존재하지 않는 targetUserId - "MANAGER" - ); - - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleMemberRoleChanged(event); - - // then - verify(notificationService, never()).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - // ====================== 멤버 추방 이벤트 ====================== - - @Test - @DisplayName("멤버 추방 이벤트 수신 - 알림 생성 성공") - void t6() { + @DisplayName("멤버 추방 이벤트") + void t3() { // given - MemberKickedEvent event = new MemberKickedEvent( - 1L, // actorId (추방한 사람) - 100L, // roomId - 2L, // kickedUserId (추방당한 사람) - "테스트 방" - ); - - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); + MemberKickedEvent event = new MemberKickedEvent(1L, 100L, 2L, "테스트 방"); // when listener.handleMemberKicked(event); // then verify(notificationService).createPersonalNotification( - eq(receiver), // 추방당한 사람이 수신자 - eq(actor), // 추방한 사람이 actor + eq(2L), // receiverId (targetUserId) + eq(1L), // actorId anyString(), anyString(), - eq("/rooms"), // 방 목록으로 이동 + eq("/rooms"), eq(NotificationSettingType.ROOM_JOIN) ); } @Test - @DisplayName("멤버 추방 이벤트 - 추방당한 사람 없음") - void t7() { - // given - MemberKickedEvent event = new MemberKickedEvent( - 1L, - 100L, - 999L, // 존재하지 않는 kickedUserId - "테스트 방" - ); - - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleMemberKicked(event); - - // then - verify(notificationService, never()).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - @Test - @DisplayName("멤버 추방 이벤트 - 추방한 사람 없음") - void t8() { - // given - MemberKickedEvent event = new MemberKickedEvent( - 999L, // 존재하지 않는 actorId - 100L, - 2L, - "테스트 방" - ); - - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleMemberKicked(event); - - // then - verify(notificationService, never()).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - // ====================== 방장 위임 이벤트 ====================== - - @Test - @DisplayName("방장 위임 이벤트 수신 - 알림 생성 성공") - void t9() { + @DisplayName("방장 위임 이벤트") + void t4() { // given - OwnerTransferredEvent event = new OwnerTransferredEvent( - 1L, // actorId (이전 방장) - 100L, // roomId - 2L, // newOwnerId (새 방장) - "테스트 방" - ); - - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); + OwnerTransferredEvent event = new OwnerTransferredEvent(1L, 100L, 2L, "테스트 방"); // when listener.handleOwnerTransferred(event); // then verify(notificationService).createPersonalNotification( - eq(receiver), // 새 방장이 수신자 - eq(actor), // 이전 방장이 actor + eq(2L), // receiverId (newOwnerId) + eq(1L), // actorId anyString(), anyString(), eq("/rooms/100"), @@ -303,75 +103,19 @@ void t9() { ); } - @Test - @DisplayName("방장 위임 이벤트 - 새 방장 없음") - void t10() { - // given - OwnerTransferredEvent event = new OwnerTransferredEvent( - 1L, - 100L, - 999L, // 존재하지 않는 newOwnerId - "테스트 방" - ); - - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleOwnerTransferred(event); - - // then - verify(notificationService, never()).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - @Test - @DisplayName("방장 위임 이벤트 - 이전 방장 없음") - void t11() { - // given - OwnerTransferredEvent event = new OwnerTransferredEvent( - 999L, // 존재하지 않는 actorId - 100L, - 2L, - "테스트 방" - ); - - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - // when - listener.handleOwnerTransferred(event); - - // then - verify(notificationService, never()).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - } - - // ====================== 예외 처리 테스트 ====================== - @Test @DisplayName("알림 생성 중 예외 발생 - 로그만 출력하고 예외 전파 안함") - void t12() { + void t5() { // given - MemberRoleChangedEvent event = new MemberRoleChangedEvent( - 1L, 100L, 2L, "MANAGER" - ); - - given(userRepository.findById(2L)).willReturn(Optional.of(receiver)); - given(userRepository.findById(1L)).willReturn(Optional.of(actor)); + MemberRoleChangedEvent event = new MemberRoleChangedEvent(1L, 100L, 2L, "MANAGER"); + willThrow(new RuntimeException("DB 오류")) + .given(notificationService) + .createPersonalNotification(anyLong(), anyLong(), any(), any(), any(), any()); - willThrow(new RuntimeException("알림 생성 실패")) - .given(notificationService).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); - - // when & then - 예외가 전파되지 않아야 함 + // when & then assertThatCode(() -> listener.handleMemberRoleChanged(event)) .doesNotThrowAnyException(); - verify(notificationService).createPersonalNotification( - any(), any(), anyString(), anyString(), anyString(), any(NotificationSettingType.class) - ); + verify(notificationService).createPersonalNotification(anyLong(), anyLong(), any(), any(), any(), any()); } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/notification/repository/NotificationRepositoryTest.java b/src/test/java/com/back/domain/notification/repository/NotificationRepositoryTest.java index 333fe7fa..0786b7ac 100644 --- a/src/test/java/com/back/domain/notification/repository/NotificationRepositoryTest.java +++ b/src/test/java/com/back/domain/notification/repository/NotificationRepositoryTest.java @@ -24,6 +24,7 @@ import org.springframework.data.domain.Pageable; import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -583,4 +584,52 @@ void t3() throws InterruptedException { assertThat(result.get(1).getTitle()).isEqualTo("오래된"); } } + + @Nested + @DisplayName("findReadNotificationIds 테스트") + class FindReadNotificationIdsTest { + + @Test + @DisplayName("성공 - 주어진 알림 ID 목록 중 사용자가 읽은 알림 ID만 반환") + void t1() { + // given + // 3개의 알림 생성 + Notification n1 = Notification.createPersonalNotification(user1, actor, "알림1", "", ""); + Notification n2 = Notification.createPersonalNotification(user1, actor, "알림2", "", ""); + Notification n3 = Notification.createPersonalNotification(user1, actor, "알림3", "", ""); + notificationRepository.saveAll(List.of(n1, n2, n3)); + + // user1이 알림1과 알림3을 읽음 처리 + notificationReadRepository.save(NotificationRead.create(n1, user1)); + notificationReadRepository.save(NotificationRead.create(n3, user1)); + + // user2는 알림1만 읽음 (user1의 결과에 영향을 주지 않는지 확인) + notificationReadRepository.save(NotificationRead.create(n1, user2)); + + List allNotificationIds = List.of(n1.getId(), n2.getId(), n3.getId()); + + // when + Set readIdsForUser1 = notificationReadRepository.findReadNotificationIds(user1.getId(), allNotificationIds); + + // then + assertThat(readIdsForUser1).hasSize(2); + assertThat(readIdsForUser1).containsExactlyInAnyOrder(n1.getId(), n3.getId()); + } + + @Test + @DisplayName("성공 - 읽은 알림이 없으면 빈 Set 반환") + void t2() { + // given + Notification n1 = Notification.createPersonalNotification(user1, actor, "알림1", "", ""); + notificationRepository.save(n1); + + List notificationIds = List.of(n1.getId()); + + // when + Set readIds = notificationReadRepository.findReadNotificationIds(user1.getId(), notificationIds); + + // then + assertThat(readIds).isEmpty(); + } + } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/notification/service/NotificationServiceTest.java b/src/test/java/com/back/domain/notification/service/NotificationServiceTest.java index 2e8e8377..995fa4ba 100644 --- a/src/test/java/com/back/domain/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/back/domain/notification/service/NotificationServiceTest.java @@ -8,7 +8,9 @@ import com.back.domain.notification.repository.NotificationReadRepository; import com.back.domain.notification.repository.NotificationRepository; import com.back.domain.studyroom.entity.Room; +import com.back.domain.studyroom.repository.RoomRepository; import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; @@ -34,6 +36,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; @ExtendWith(MockitoExtension.class) class NotificationServiceTest { @@ -50,6 +53,12 @@ class NotificationServiceTest { @Mock private NotificationSettingService notificationSettingService; + @Mock + private UserRepository userRepository; + + @Mock + private RoomRepository roomRepository; + @InjectMocks private NotificationService notificationService; @@ -95,21 +104,19 @@ class CreateNotificationTest { @DisplayName("개인 알림을 생성하고 WebSocket으로 전송") void t1() { // given - given(notificationRepository.save(any(Notification.class))) - .willReturn(notification); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); + given(userRepository.findById(actor.getId())).willReturn(Optional.of(actor)); + given(notificationRepository.save(any(Notification.class))).willReturn(notification); // when - Notification result = notificationService.createPersonalNotification( - user, actor, "테스트 알림", "내용", "/target", + notificationService.createPersonalNotification( + user.getId(), actor.getId(), "테스트 알림", "내용", "/target", NotificationSettingType.SYSTEM ); // then - assertThat(result).isNotNull(); - assertThat(result.getType()).isEqualTo(NotificationType.PERSONAL); - assertThat(result.getReceiver()).isEqualTo(user); - assertThat(result.getActor()).isEqualTo(actor); - + verify(userRepository).findById(user.getId()); + verify(userRepository).findById(actor.getId()); verify(notificationRepository).save(any(Notification.class)); verify(webSocketService).sendNotificationToUser( eq(user.getId()), @@ -121,29 +128,21 @@ void t1() { @DisplayName("스터디룸 알림 생성 - 룸 멤버들에게 전송") void t2() { // given - Notification roomNotification = Notification.createRoomNotification( - room, actor, "룸 알림", "내용", "/room" - ); - given(notificationRepository.save(any(Notification.class))) - .willReturn(roomNotification); + Notification roomNotification = Notification.createRoomNotification(room, actor, "룸", "", ""); + given(roomRepository.findById(room.getId())).willReturn(Optional.of(room)); + given(userRepository.findById(actor.getId())).willReturn(Optional.of(actor)); + given(notificationRepository.save(any(Notification.class))).willReturn(roomNotification); // when Notification result = notificationService.createRoomNotification( - room, actor, "룸 알림", "내용", "/room", - NotificationSettingType.ROOM_NOTICE + room.getId(), actor.getId(), "룸", "", "", NotificationSettingType.ROOM_NOTICE ); // then assertThat(result).isNotNull(); - assertThat(result.getType()).isEqualTo(NotificationType.ROOM); - assertThat(result.getRoom()).isEqualTo(room); - assertThat(result.getActor()).isEqualTo(actor); - - verify(notificationRepository).save(any(Notification.class)); - verify(webSocketService).sendNotificationToRoom( - eq(room.getId()), - any(NotificationWebSocketDto.class) - ); + verify(roomRepository).findById(room.getId()); + verify(userRepository).findById(actor.getId()); + verify(webSocketService).sendNotificationToRoom(eq(room.getId()), any()); } @Test @@ -177,49 +176,39 @@ void t3() { @DisplayName("커뮤니티 알림 생성 - WebSocket으로 전송") void t4() { // given - Notification communityNotification = Notification.createCommunityNotification( - user, actor, "커뮤니티 알림", "내용", "/community" - ); - given(notificationRepository.save(any(Notification.class))) - .willReturn(communityNotification); - given(notificationSettingService.isNotificationEnabled(user.getId(), NotificationSettingType.POST_COMMENT)) - .willReturn(true); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); + given(userRepository.findById(actor.getId())).willReturn(Optional.of(actor)); + given(notificationRepository.save(any(Notification.class))).willReturn(notification); + given(notificationSettingService.isNotificationEnabled(anyLong(), any())).willReturn(true); // when Notification result = notificationService.createCommunityNotification( - user, actor, "커뮤니티 알림", "내용", "/community", - NotificationSettingType.POST_COMMENT + user.getId(), actor.getId(), "커뮤니티", "", "", NotificationSettingType.POST_COMMENT ); // then assertThat(result).isNotNull(); - assertThat(result.getType()).isEqualTo(NotificationType.COMMUNITY); - assertThat(result.getReceiver()).isEqualTo(user); - assertThat(result.getActor()).isEqualTo(actor); - - verify(notificationRepository).save(any(Notification.class)); - verify(notificationSettingService).isNotificationEnabled(user.getId(), NotificationSettingType.POST_COMMENT); - verify(webSocketService).sendNotificationToUser( - eq(user.getId()), - any(NotificationWebSocketDto.class) - ); + verify(userRepository).findById(user.getId()); + verify(userRepository).findById(actor.getId()); + verify(webSocketService).sendNotificationToUser(eq(user.getId()), any()); } @Test @DisplayName("자기 자신에게 개인 알림 전송 시 예외 발생") void t5() { // given - User sameUser = user; + Long sameUserId = 1L; + given(userRepository.findById(sameUserId)).willReturn(Optional.of(user)); // when & then assertThatThrownBy(() -> notificationService.createPersonalNotification( - sameUser, sameUser, "title", "content", "/url", - NotificationSettingType.SYSTEM + sameUserId, sameUserId, "title", "content", "/url", NotificationSettingType.SYSTEM ) ).isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOTIFICATION_FORBIDDEN); + verify(userRepository, times(2)).findById(sameUserId); verify(notificationRepository, never()).save(any(Notification.class)); } @@ -227,17 +216,18 @@ void t5() { @DisplayName("자기 자신에게 커뮤니티 알림 전송 시 예외 발생") void t6() { // given - User sameUser = user; + Long sameUserId = 1L; + given(userRepository.findById(sameUserId)).willReturn(Optional.of(user)); // when & then assertThatThrownBy(() -> notificationService.createCommunityNotification( - sameUser, sameUser, "title", "content", "/url", - NotificationSettingType.POST_COMMENT + sameUserId, sameUserId, "title", "content", "/url", NotificationSettingType.POST_COMMENT ) ).isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOTIFICATION_FORBIDDEN); + verify(userRepository, times(2)).findById(sameUserId); verify(notificationRepository, never()).save(any(Notification.class)); } @@ -245,21 +235,25 @@ void t6() { @DisplayName("알림 설정이 비활성화된 경우 WebSocket 전송 생략") void t7() { // given - given(notificationRepository.save(any(Notification.class))) - .willReturn(notification); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); + given(userRepository.findById(actor.getId())).willReturn(Optional.of(actor)); + given(notificationRepository.save(any(Notification.class))).willReturn(notification); + + // 사용자의 알림 설정이 false를 반환하도록 설정 given(notificationSettingService.isNotificationEnabled(user.getId(), NotificationSettingType.POST_COMMENT)) .willReturn(false); // when Notification result = notificationService.createCommunityNotification( - user, actor, "커뮤니티 알림", "내용", "/community", + user.getId(), actor.getId(), "커뮤니티 알림", "내용", "/community", NotificationSettingType.POST_COMMENT ); // then - assertThat(result).isNotNull(); + assertThat(result).isNotNull(); // 알림 자체는 DB에 저장되어야 함 verify(notificationRepository).save(any(Notification.class)); - verify(notificationSettingService).isNotificationEnabled(user.getId(), NotificationSettingType.POST_COMMENT); + + // WebSocket 전송은 호출되지 않아야 함 verify(webSocketService, never()).sendNotificationToUser(anyLong(), any()); } @@ -267,17 +261,17 @@ void t7() { @DisplayName("자기 자신 알림(createSelfNotification) 생성 성공") void t8() { // given - given(notificationRepository.save(any(Notification.class))) - .willReturn(notification); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); + given(notificationRepository.save(any(Notification.class))).willReturn(notification); // when - Notification result = notificationService.createSelfNotification( - user, "학습 기록", "1시간 공부 완료", "/study", + notificationService.createSelfNotification( + user.getId(), "학습 기록", "1시간 공부 완료", "/study", NotificationSettingType.SYSTEM ); // then - assertThat(result).isNotNull(); + verify(userRepository).findById(user.getId()); verify(notificationRepository).save(any(Notification.class)); verify(webSocketService).sendNotificationToUser( eq(user.getId()), @@ -296,18 +290,21 @@ void t1() { // given Pageable pageable = PageRequest.of(0, 10); Page expectedPage = new PageImpl<>(List.of(notification)); + given(notificationRepository.findByUserIdOrSystemType(user.getId(), pageable)) .willReturn(expectedPage); // when - Page result = notificationService.getUserNotifications( - user.getId(), pageable + Page result = notificationService.getNotifications( + user.getId(), pageable, false ); // then assertThat(result.getContent()).hasSize(1); assertThat(result.getContent().get(0)).isEqualTo(notification); + verify(notificationRepository).findByUserIdOrSystemType(user.getId(), pageable); + verify(notificationRepository, never()).findUnreadByUserId(any(), any()); } @Test @@ -316,17 +313,20 @@ void t2() { // given Pageable pageable = PageRequest.of(0, 10); Page expectedPage = new PageImpl<>(List.of(notification)); + given(notificationRepository.findUnreadByUserId(user.getId(), pageable)) .willReturn(expectedPage); // when - Page result = notificationService.getUnreadNotifications( - user.getId(), pageable + Page result = notificationService.getNotifications( + user.getId(), pageable, true ); // then assertThat(result.getContent()).hasSize(1); + verify(notificationRepository).findUnreadByUserId(user.getId(), pageable); + verify(notificationRepository, never()).findByUserIdOrSystemType(any(), any()); } @Test @@ -501,4 +501,50 @@ void t5() { )); } } + + @Nested + @DisplayName("getReadNotificationIds 메서드 테스트") + class GetReadNotificationIdsTest { + + @Test + @DisplayName("성공 - 알림 목록을 받아 읽은 알림 ID Set 반환") + void t1() { + // given + // 테스트용 알림 객체 생성 + Notification notification1 = Notification.createSystemNotification("알림1", "내용", "/1"); + ReflectionTestUtils.setField(notification1, "id", 1L); + Notification notification2 = Notification.createSystemNotification("알림2", "내용", "/2"); + ReflectionTestUtils.setField(notification2, "id", 2L); + List notifications = List.of(notification1, notification2); + + // Repository가 ID 1만 읽었다고 응답하도록 설정 + Set expectedReadIds = Set.of(1L); + List notificationIds = List.of(1L, 2L); + + given(notificationReadRepository.findReadNotificationIds(user.getId(), notificationIds)) + .willReturn(expectedReadIds); + + // when + Set result = notificationService.getReadNotificationIds(user.getId(), notifications); + + // then + assertThat(result).isEqualTo(expectedReadIds); + verify(notificationReadRepository).findReadNotificationIds(user.getId(), notificationIds); + } + + @Test + @DisplayName("성공 - 알림 목록이 비어있으면 빈 Set 반환") + void t2() { + // given + List emptyList = List.of(); + + // when + Set result = notificationService.getReadNotificationIds(user.getId(), emptyList); + + // then + assertThat(result).isEmpty(); + // Repository가 호출되지 않아야 함 + verify(notificationReadRepository, never()).findReadNotificationIds(anyLong(), anyList()); + } + } } \ No newline at end of file