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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package UMC.news.newsIntelligent.domain.notification.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import UMC.news.newsIntelligent.domain.notification.dto.NotificationResponse;
import UMC.news.newsIntelligent.domain.notification.service.NotificationService;
import UMC.news.newsIntelligent.global.apiPayload.CustomResponse;
import UMC.news.newsIntelligent.global.apiPayload.code.success.GeneralSuccessCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/notification")
@RequiredArgsConstructor
@Tag(name = "사용자 알림 관련 API")
public class NotificationController {

private final NotificationService notificationService;

@Operation(summary = "알림 목록 조회 API",
description = "<p>구독한 토픽, 읽은 토픽에 대한 홈화면 알림 목록입니다."
+ "<p>커서 페이징 처리하였습니다.")
@GetMapping
public CustomResponse<NotificationResponse.NotificationCursorDto> getNotifications(
@RequestParam(required = false) String cursor,
@RequestParam(defaultValue = "10") int size
) {
Long memberId = 0L;
NotificationResponse.NotificationCursorDto body =
notificationService.getNotifications(memberId, cursor, size);

return CustomResponse.onSuccess(GeneralSuccessCode.OK, body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package UMC.news.newsIntelligent.domain.notification.dto;

public class NotificationRequest {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package UMC.news.newsIntelligent.domain.notification.dto;

import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;

import UMC.news.newsIntelligent.domain.notification.Notification;
import UMC.news.newsIntelligent.domain.notification.NotificationType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class NotificationResponse {

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class NotificationCursorDto {
private List<NotificationDto> notifications;
private String nextCursor;
private boolean hasNext;

// dto 변환 메서드
public static NotificationCursorDto of(List<Notification> items, String nextCursor, boolean hasNext) {
List<NotificationDto> dtos = items.stream()
.map(NotificationDto::of).collect(Collectors.toList());
return NotificationCursorDto.builder()
.notifications(dtos)
.nextCursor(nextCursor)
.hasNext(hasNext)
.build();
}
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class NotificationDto {
private NotificationType type;
private String content;
private Boolean isChecked;
private String createdAt;

// '2011-12-03T10:15:30+01:00' 형식의 포매터
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;

// Notification entity -> Dto
public static NotificationDto of(Notification notification) {

return NotificationDto.builder()
.content(notification.getContent())
.type(notification.getNotificationType())
.isChecked(notification.getIsChecked())
.createdAt(notification.getCreatedAt().format(FORMATTER))
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package UMC.news.newsIntelligent.domain.notification.repository;

import java.awt.print.Pageable;
import java.time.LocalDateTime;
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import UMC.news.newsIntelligent.domain.notification.Notification;

public interface NotificationRepository extends JpaRepository<Notification, Long> {
List<Notification> findByMemberIdOrderByCreatedAtAsc(Long memberId);

/**
* 초기 조회: 최신순으로 size+1 개 가져온다.
*/
@Query("""
SELECT n
FROM Notification n
WHERE n.member.id = :memberId
ORDER BY n.createdAt DESC, n.id DESC
""")
List<Notification> findByMemberIdBeforeCursor(
@Param("memberId") Long memberId,
@Param("limit") int limit);

/**
* 커서 다음 조회:
* createdAt < cursor.createdAt
* (createdAt = cursor.createdAt AND id < cursor.id)
*/
@Query("""
SELECT n
FROM Notification n
WHERE n.member.id = :memberId
AND (n.createdAt < :createdAt
OR (n.createdAt = :createdAt AND n.id < :id))
ORDER BY n.createdAt DESC, n.id DESC
""")
List<Notification> findByMemberIdAfterCursor(
@Param("memberId") Long memberId,
@Param("createdAt")LocalDateTime createdAt,
@Param("id") Long id,
@Param("limit") int limit
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package UMC.news.newsIntelligent.domain.notification.service;

import UMC.news.newsIntelligent.domain.notification.dto.NotificationResponse;

public interface NotificationService {
NotificationResponse.NotificationCursorDto getNotifications(Long memberId, String cursor, int size);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package UMC.news.newsIntelligent.domain.notification.service;

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import UMC.news.newsIntelligent.domain.notification.Notification;
import UMC.news.newsIntelligent.domain.notification.dto.NotificationResponse;
import UMC.news.newsIntelligent.domain.notification.repository.NotificationRepository;
import UMC.news.newsIntelligent.global.apiPayload.code.error.GeneralErrorCode;
import UMC.news.newsIntelligent.global.apiPayload.exception.CustomException;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional
public class NotificationServiceImpl implements NotificationService {

private final NotificationRepository notificationRepository;

@Override
public NotificationResponse.NotificationCursorDto getNotifications(
Long memberId, String cursor, int size
) {
// 커서 디코딩
LocalDateTime createdAt = null;
Long id = null;
if(cursor != null && !cursor.isBlank()) {
try {
String decoded = new String(Base64.getUrlDecoder().decode(cursor));
String[] parts = decoded.split("_", 2);
createdAt = LocalDateTime.parse(parts[0]);
id = Long.parseLong(parts[1]);

} catch (IllegalArgumentException | DateTimeException e) {
throw new CustomException(GeneralErrorCode.CURSOR_INVALID);
}
}
// size+1 만큼 조회
int limit = size +1;
List<Notification> items = (createdAt == null)
? notificationRepository.findByMemberIdBeforeCursor(memberId, limit)
: notificationRepository.findByMemberIdAfterCursor(memberId, createdAt, id, limit);

boolean hasNext = items.size() > size;
String nextCursor = null;
if (hasNext) {
Notification pivot = items.get(size);
String raw = pivot.getCreatedAt().toString() + "_" + pivot.getId();
nextCursor = Base64.getUrlEncoder().encodeToString(raw.getBytes());
}
List<Notification> page = hasNext
? items.subList(0, size)
: items;

return NotificationResponse.NotificationCursorDto.of(page, nextCursor, hasNext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public enum GeneralErrorCode implements BaseErrorCode{
INTERNAL_SERVER_ERROR_500(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 내부 오류가 발생했습니다"),

// 유효성 검사
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "VALID400_0", "잘못된 파라미터 입니다.")
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "VALID400_0", "잘못된 파라미터 입니다."),
// 커서 에러
CURSOR_INVALID(HttpStatus.BAD_REQUEST, "CURSOR400", "커서가 유효하지 않습니다.")
;

// 필요한 필드값 선언
Expand Down