Skip to content

Commit

Permalink
fix: ✏️ 확인 알림과 미확인 알림 조회 API 분리 (#161)
Browse files Browse the repository at this point in the history
* rename: 미확인 알림 조회 api 메서드 명 수정

* rename: 수신한 알림 무한 스크롤 조회 api 메서드명 수정

* feat: 미확인 알림 조회 api 추가

* fix: 미확인 알림 조회 controller http method 및 경로 설정

* feat: 미확인 알림 조회 service 구현

* feat: notification list -> notification dto info list 변환 mapper 구현

* feat: 미확인 알림 조회 usecase 구현

* fix: 확인한 알림 조회 쿼리에서 is_read is null 조건 추가

* fix: 미확인 알림 조회 시, query join 되는 문제 해결

* docs: swagger operation 요약 보다 명시적으로 수정

* test: 읽음 알림 조회 repository 단위 테스트에서 read_at is not null 조건에 추가됨에 따른 테스트 수정
  • Loading branch information
psychology50 authored Oct 8, 2024
1 parent 8668176 commit d1ae456
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

@Tag(name = "[알림 API]")
public interface NotificationApi {
@Operation(summary = "수신한 알림 목록 무한 스크롤 조회")
@Operation(summary = "수신한 알림 중 확인한 알림 목록 무한 스크롤 조회")
@Parameters({
@Parameter(
in = ParameterIn.QUERY,
Expand Down Expand Up @@ -56,14 +56,18 @@ public interface NotificationApi {
)
), @Parameter(name = "pageable", hidden = true)})
@ApiResponse(responseCode = "200", description = "알림 목록 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "notifications", schema = @Schema(implementation = NotificationDto.SliceRes.class))))
ResponseEntity<?> getNotifications(
ResponseEntity<?> getReadNotifications(
@PageableDefault(page = 0, size = 30) @SortDefault(sort = "notification.createdAt", direction = Sort.Direction.DESC) Pageable pageable,
@AuthenticationPrincipal SecurityUserDetails user
);

@Operation(summary = "수신한 알림 중 미확인 알림 목록 조회")
@ApiResponse(responseCode = "200", description = "미확인 알림 목록 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "notifications", array = @ArraySchema(schema = @Schema(implementation = NotificationDto.Info.class)))))
ResponseEntity<?> getUnreadNotifications(@AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "수신한 알림 중 미확인 알림 존재 여부 조회")
@ApiResponse(responseCode = "200", description = "미확인 알림 존재 여부 조회 성공", content = @Content(schemaProperties = @SchemaProperty(name = "hasUnread", schema = @Schema(type = "boolean"))))
ResponseEntity<?> getUnreadNotifications(@AuthenticationPrincipal SecurityUserDetails user);
ResponseEntity<?> getHasUnreadNotification(@AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "수신한 알림 읽음 처리", description = "사용자가 수신한 알림을 읽음처리 합니다. 단, 읽음 처리할 알림의 pk는 사용자가 receiver여야 하며, 미확인 알림만 포함되어 있어야 합니다.")
@ApiResponses({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,24 @@ public class NotificationController implements NotificationApi {
@Override
@GetMapping("")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> getNotifications(
public ResponseEntity<?> getReadNotifications(
@PageableDefault(page = 0, size = 30) @SortDefault(sort = "notification.createdAt", direction = Sort.Direction.DESC) Pageable pageable,
@AuthenticationPrincipal SecurityUserDetails user
) {
return ResponseEntity.ok(SuccessResponse.from(NOTIFICATIONS, notificationUseCase.getNotifications(user.getUserId(), pageable)));
return ResponseEntity.ok(SuccessResponse.from(NOTIFICATIONS, notificationUseCase.getReadNotifications(user.getUserId(), pageable)));
}

@Override
@GetMapping("/unread")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> getUnreadNotifications(@AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from(NOTIFICATIONS, notificationUseCase.getUnreadNotifications(user.getUserId())));
}

@Override
@GetMapping("/unread/exist")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> getHasUnreadNotification(@AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from(HAS_UNREAD, notificationUseCase.hasUnreadNotification(user.getUserId())));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public record ReadReq(
) {
}

@Schema(title = "푸시 알림 리스트 응답")
public record ListRes(
@Schema(description = "푸시 알림 리스트")
List<Info> notifications
) {
public static ListRes from(List<Info> notifications) {
return new ListRes(notifications);
}
}

@Schema(title = "푸시 알림 슬라이스 응답")
public record SliceRes(
@Schema(description = "푸시 알림 리스트")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,22 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

import java.util.Comparator;
import java.util.List;

@Slf4j
@Mapper
public class NotificationMapper {
/**
* Notification 타입을 NotificationDto.Info 타입으로 변환한다.
*/
public static List<NotificationDto.Info> toInfoList(List<Notification> notifications) {
return notifications.stream()
.map(NotificationDto.Info::from)
.sorted(Comparator.comparing(NotificationDto.Info::id).reversed())
.toList();
}

/**
* Slice<Notification> 타입을 무한 스크롤 응답 형태로 변환한다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
Expand All @@ -20,6 +22,11 @@ public Slice<Notification> getNotifications(Long userId, Pageable pageable) {
return notificationService.readNotificationsSlice(userId, pageable);
}

@Transactional(readOnly = true)
public List<Notification> getUnreadNotifications(Long userId) {
return notificationService.readUnreadNotifications(userId);
}

@Transactional(readOnly = true)
public boolean isExistsUnreadNotification(Long userId) {
return notificationService.isExistsUnreadNotification(userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ public class NotificationUseCase {
private final NotificationSearchService notificationSearchService;
private final NotificationSaveService notificationSaveService;

public NotificationDto.SliceRes getNotifications(Long userId, Pageable pageable) {
public NotificationDto.SliceRes getReadNotifications(Long userId, Pageable pageable) {
Slice<Notification> notifications = notificationSearchService.getNotifications(userId, pageable);

return NotificationMapper.toSliceRes(notifications, pageable);
}

public List<NotificationDto.Info> getUnreadNotifications(Long userId) {
List<Notification> notifications = notificationSearchService.getUnreadNotifications(userId);

return NotificationMapper.toInfoList(notifications);
}

public boolean hasUnreadNotification(Long userId) {
return notificationSearchService.isExistsUnreadNotification(userId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void getNotificationsWithDefaultParameters() throws Exception {
Notification notification = NotificationFixture.ANNOUNCEMENT_DAILY_SPENDING.toEntity(UserFixture.GENERAL_USER.toUser());
NotificationDto.Info info = NotificationDto.Info.from(notification);

given(notificationUseCase.getNotifications(eq(1L), any())).willReturn(NotificationDto.SliceRes.from(List.of(info), pa, numberOfElements, false));
given(notificationUseCase.getReadNotifications(eq(1L), any())).willReturn(NotificationDto.SliceRes.from(List.of(info), pa, numberOfElements, false));

// when
ResultActions result = performGetNotifications(page);
Expand All @@ -82,7 +82,7 @@ void getNotificationsWithInfiniteScroll() throws Exception {
NotificationDto.Info info = NotificationDto.Info.from(notification);
NotificationDto.SliceRes sliceRes = NotificationDto.SliceRes.from(List.of(info), pa, numberOfElements, false);

given(notificationUseCase.getNotifications(eq(1L), any())).willReturn(sliceRes);
given(notificationUseCase.getReadNotifications(eq(1L), any())).willReturn(sliceRes);

// when
ResultActions result = performGetNotifications(page);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class NotificationService {

@Transactional(readOnly = true)
public Slice<Notification> readNotificationsSlice(Long userId, Pageable pageable) {
Predicate predicate = notification.receiver.id.eq(userId);
Predicate predicate = notification.receiver.id.eq(userId).and(notification.readAt.isNotNull());

QueryHandler queryHandler = query -> query
.offset(pageable.getOffset())
Expand All @@ -37,6 +37,13 @@ public Slice<Notification> readNotificationsSlice(Long userId, Pageable pageable
return SliceUtil.toSlice(notificationRepository.findList(predicate, queryHandler, sort), pageable);
}

@Transactional(readOnly = true)
public List<Notification> readUnreadNotifications(Long userId) {
Predicate predicate = notification.receiver.id.eq(userId).and(notification.readAt.isNull());

return notificationRepository.findList(predicate, null, null);
}

@Transactional(readOnly = true)
public boolean isExistsUnreadNotification(Long userId) {
return notificationRepository.existsUnreadNotification(userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
Expand Down Expand Up @@ -64,6 +65,7 @@ public void readNotificationsSliceSorted() {
List<Notification> notifications = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Notification notification = new Notification.Builder(NoticeType.ANNOUNCEMENT, Announcement.DAILY_SPENDING, user).build();
ReflectionTestUtils.setField(notification, "readAt", LocalDateTime.now());
notifications.add(notification);
}
bulkInsertNotifications(notifications);
Expand Down Expand Up @@ -97,8 +99,8 @@ private User createUser(String name) {

private void bulkInsertNotifications(List<Notification> notifications) {
String sql = String.format("""
INSERT INTO `%s` (type, announcement, created_at, updated_at, receiver, receiver_name)
VALUES (:type, :announcement, :createdAt, :updatedAt, :receiver, :receiverName);
INSERT INTO `%s` (type, announcement, created_at, updated_at, receiver, receiver_name, read_at)
VALUES (:type, :announcement, :createdAt, :updatedAt, :receiver, :receiverName, :readAt);
""", "notification");

LocalDateTime date = LocalDateTime.now();
Expand All @@ -112,7 +114,8 @@ private void bulkInsertNotifications(List<Notification> notifications) {
.addValue("createdAt", date)
.addValue("updatedAt", date)
.addValue("receiver", notification.getReceiver().getId())
.addValue("receiverName", notification.getReceiverName());
.addValue("receiverName", notification.getReceiverName())
.addValue("readAt", notification.getReadAt());
date = date.minusDays(1);
}

Expand Down

0 comments on commit d1ae456

Please sign in to comment.