diff --git a/src/main/java/UMC/news/newsIntelligent/domain/notification/controller/NotificationController.java b/src/main/java/UMC/news/newsIntelligent/domain/notification/controller/NotificationController.java
new file mode 100644
index 0000000..52ccde3
--- /dev/null
+++ b/src/main/java/UMC/news/newsIntelligent/domain/notification/controller/NotificationController.java
@@ -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 = "
구독한 토픽, 읽은 토픽에 대한 홈화면 알림 목록입니다."
+ + "
커서 페이징 처리하였습니다.")
+ @GetMapping
+ public CustomResponse 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);
+ }
+}
diff --git a/src/main/java/UMC/news/newsIntelligent/domain/notification/dto/NotificationRequest.java b/src/main/java/UMC/news/newsIntelligent/domain/notification/dto/NotificationRequest.java
new file mode 100644
index 0000000..89012d3
--- /dev/null
+++ b/src/main/java/UMC/news/newsIntelligent/domain/notification/dto/NotificationRequest.java
@@ -0,0 +1,5 @@
+package UMC.news.newsIntelligent.domain.notification.dto;
+
+public class NotificationRequest {
+
+}
diff --git a/src/main/java/UMC/news/newsIntelligent/domain/notification/dto/NotificationResponse.java b/src/main/java/UMC/news/newsIntelligent/domain/notification/dto/NotificationResponse.java
new file mode 100644
index 0000000..26c64fa
--- /dev/null
+++ b/src/main/java/UMC/news/newsIntelligent/domain/notification/dto/NotificationResponse.java
@@ -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 notifications;
+ private String nextCursor;
+ private boolean hasNext;
+
+ // dto 변환 메서드
+ public static NotificationCursorDto of(List items, String nextCursor, boolean hasNext) {
+ List 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();
+ }
+ }
+}
diff --git a/src/main/java/UMC/news/newsIntelligent/domain/notification/repository/NotificationRepository.java b/src/main/java/UMC/news/newsIntelligent/domain/notification/repository/NotificationRepository.java
new file mode 100644
index 0000000..2e753f0
--- /dev/null
+++ b/src/main/java/UMC/news/newsIntelligent/domain/notification/repository/NotificationRepository.java
@@ -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 {
+ List 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 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 findByMemberIdAfterCursor(
+ @Param("memberId") Long memberId,
+ @Param("createdAt")LocalDateTime createdAt,
+ @Param("id") Long id,
+ @Param("limit") int limit
+ );
+}
\ No newline at end of file
diff --git a/src/main/java/UMC/news/newsIntelligent/domain/notification/service/NotificationService.java b/src/main/java/UMC/news/newsIntelligent/domain/notification/service/NotificationService.java
new file mode 100644
index 0000000..2a653d4
--- /dev/null
+++ b/src/main/java/UMC/news/newsIntelligent/domain/notification/service/NotificationService.java
@@ -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);
+}
diff --git a/src/main/java/UMC/news/newsIntelligent/domain/notification/service/NotificationServiceImpl.java b/src/main/java/UMC/news/newsIntelligent/domain/notification/service/NotificationServiceImpl.java
new file mode 100644
index 0000000..d597c55
--- /dev/null
+++ b/src/main/java/UMC/news/newsIntelligent/domain/notification/service/NotificationServiceImpl.java
@@ -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 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 page = hasNext
+ ? items.subList(0, size)
+ : items;
+
+ return NotificationResponse.NotificationCursorDto.of(page, nextCursor, hasNext);
+ }
+}
diff --git a/src/main/java/UMC/news/newsIntelligent/global/apiPayload/code/error/GeneralErrorCode.java b/src/main/java/UMC/news/newsIntelligent/global/apiPayload/code/error/GeneralErrorCode.java
index 2b9600a..68aa888 100644
--- a/src/main/java/UMC/news/newsIntelligent/global/apiPayload/code/error/GeneralErrorCode.java
+++ b/src/main/java/UMC/news/newsIntelligent/global/apiPayload/code/error/GeneralErrorCode.java
@@ -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", "커서가 유효하지 않습니다.")
;
// 필요한 필드값 선언