diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 0628d8a6d..e9d19073d 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -222,7 +222,13 @@ public enum ErrorCode implements ResponseCode { FCM_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, 200000, "존재하지 않는 FCM TOKEN 입니다."), FCM_TOKEN_ENABLED_STATE_ALREADY(HttpStatus.BAD_REQUEST, 200001, "요청한 상태로 이미 푸쉬 알림 여부가 설정되어 있습니다."), FCM_TOKEN_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 200002, "토큰을 소유하고 있는 계정이 아닙니다."), - FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 알림 전송에 실패했습니다.") + FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 알림 전송에 실패했습니다."), + + + /** + * 205000 : notification error + */ + INVALID_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, 205000, "유효하지 않은 알림 타입입니다."), ; diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 223bdbef9..81c2e1f04 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -373,6 +373,9 @@ public enum SwaggerResponseDescription { NOTIFICATION_GET_ENABLE_STATE(new LinkedHashSet<>(Set.of( FCM_TOKEN_NOT_FOUND ))), + NOTIFICATION_SHOW(new LinkedHashSet<>(Set.of( + INVALID_NOTIFICATION_TYPE + ))), ; private final Set errorCodeList; diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java index b6d3772f5..f9f572237 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java @@ -11,9 +11,9 @@ import konkuk.thip.notification.adapter.in.web.request.FcmTokenEnableStateChangeRequest; import konkuk.thip.notification.adapter.in.web.request.FcmTokenRegisterRequest; import konkuk.thip.notification.adapter.in.web.response.FcmTokenEnableStateChangeResponse; -import konkuk.thip.notification.application.port.in.FcmDeleteUseCase; -import konkuk.thip.notification.application.port.in.FcmEnableStateChangeUseCase; -import konkuk.thip.notification.application.port.in.FcmRegisterUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmDeleteUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmEnableStateChangeUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmRegisterUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java index b6afd1e39..a8ace6dab 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java @@ -7,13 +7,17 @@ import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.notification.adapter.in.web.response.NotificationShowEnableStateResponse; +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; import konkuk.thip.notification.application.port.in.NotificationShowEnableStateUseCase; +import konkuk.thip.notification.application.port.in.NotificationShowUseCase; +import konkuk.thip.notification.application.port.in.dto.NotificationType; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import static konkuk.thip.common.swagger.SwaggerResponseDescription.NOTIFICATION_GET_ENABLE_STATE; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.NOTIFICATION_SHOW; @Tag(name = "Notification Query API", description = "알림 조회 관련 API") @RestController @@ -21,6 +25,7 @@ public class NotificationQueryController { private final NotificationShowEnableStateUseCase notificationShowEnableStateUseCase; + private final NotificationShowUseCase notificationShowUseCase; @Operation( summary = "사용자 푸시알림 수신여부 조회 (마이페이지 -> 알림설정)", @@ -35,4 +40,20 @@ public BaseResponse showNotificationEnableS return BaseResponse.ok( NotificationShowEnableStateResponse.of(notificationShowEnableStateUseCase.getNotificationShowEnableState(userId,deviceId))); } + + @Operation( + summary = "유저의 알림 조회", + description = "유저의 알림 목록을 조회합니다. 최신순으로 정렬합니다." + ) + @ExceptionDescription(NOTIFICATION_SHOW) + @GetMapping("/notifications") + public BaseResponse showNotifications( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") + @RequestParam(value = "cursor", required = false) final String cursor, + @Parameter(description = "알림 타입. 해당 파라미터 값이 null인 경우에는 알림 타입을 구분하지 않고 조회합니다.", example = "feed or room") + @RequestParam(value = "type", required = false, defaultValue = "feedAndRoom") final String type + ) { + return BaseResponse.ok(notificationShowUseCase.showNotifications(userId, cursor, NotificationType.from(type))); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java new file mode 100644 index 000000000..d810e8dd1 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationShowResponse.java @@ -0,0 +1,18 @@ +package konkuk.thip.notification.adapter.in.web.response; + +import java.util.List; + +public record NotificationShowResponse( + List notifications, + String nextCursor, + boolean isLast +) { + public record NotificationOfUser( + Long notificationId, + String title, + String content, + boolean isChecked, + String notificationType, + String postDate + ) {} +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java index b28d749e5..3e8a1f549 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.notification.domain.value.NotificationCategory; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; @@ -23,9 +24,13 @@ public class NotificationJpaEntity extends BaseJpaEntity { @Column(length = 200, nullable = false) private String content; - @Column(name = "is_checked",nullable = false) + @Column(name = "is_checked", nullable = false) private boolean isChecked; + @Enumerated(EnumType.STRING) + @Column(name = "notification_category", length = 16, nullable = false) + private NotificationCategory notificationCategory; + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; diff --git a/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java b/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java index aff2462d0..dd65e5db3 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/mapper/NotificationMapper.java @@ -13,6 +13,7 @@ public NotificationJpaEntity toJpaEntity(Notification notification, UserJpaEntit .title(notification.getTitle()) .content(notification.getContent()) .isChecked(notification.isChecked()) + .notificationCategory(notification.getNotificationCategory()) .userJpaEntity(userJpaEntity) .build(); } @@ -23,6 +24,7 @@ public Notification toDomainEntity(NotificationJpaEntity notificationJpaEntity) .title(notificationJpaEntity.getTitle()) .content(notificationJpaEntity.getContent()) .isChecked(notificationJpaEntity.isChecked()) + .notificationCategory(notificationJpaEntity.getNotificationCategory()) .targetUserId(notificationJpaEntity.getUserJpaEntity().getUserId()) .createdAt(notificationJpaEntity.getCreatedAt()) .modifiedAt(notificationJpaEntity.getModifiedAt()) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java index 469395bc3..289428136 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java @@ -1,15 +1,56 @@ package konkuk.thip.notification.adapter.out.persistence; +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.notification.adapter.out.mapper.NotificationMapper; +import konkuk.thip.notification.adapter.out.persistence.function.PrimaryKeyNotificationQueryFunction; import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.notification.application.port.out.NotificationQueryPort; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository @RequiredArgsConstructor -public class NotificationQueryPersistenceAdapter { +public class NotificationQueryPersistenceAdapter implements NotificationQueryPort { - private final NotificationJpaRepository jpaRepository; + private final NotificationJpaRepository notificationJpaRepository; private final NotificationMapper notificationMapper; + @Override + public CursorBasedList findFeedNotificationsByUserId(Long userId, Cursor cursor) { + return findNotificationsByPrimaryKeyCursor(cursor, ((lastNotificationId, pageSize) -> + notificationJpaRepository.findFeedNotificationsOrderByCreatedAtDesc(userId, lastNotificationId, pageSize) + )); + } + + @Override + public CursorBasedList findRoomNotificationsByUserId(Long userId, Cursor cursor) { + return findNotificationsByPrimaryKeyCursor(cursor, ((lastNotificationId, pageSize) -> + notificationJpaRepository.findRoomNotificationsOrderByCreatedAtDesc(userId, lastNotificationId, pageSize) + )); + } + + @Override + public CursorBasedList findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor) { + return findNotificationsByPrimaryKeyCursor(cursor, ((lastNotificationId, pageSize) -> + notificationJpaRepository.findFeedAndRoomNotificationsOrderByCreatedAtDesc(userId, lastNotificationId, pageSize) + )); + } + + private CursorBasedList findNotificationsByPrimaryKeyCursor(Cursor cursor, PrimaryKeyNotificationQueryFunction queryFunction) { + Long lastNotificationId = cursor.isFirstRequest() ? null : cursor.getLong(0); + int pageSize = cursor.getPageSize(); + + List dtos = queryFunction.apply(lastNotificationId, pageSize); + + return CursorBasedList.of(dtos, pageSize, dto -> { + Cursor nextCursor = new Cursor(List.of( + dto.notificationId().toString() + )); + return nextCursor.toEncodedString(); + }); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java new file mode 100644 index 000000000..9783cdcee --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/function/PrimaryKeyNotificationQueryFunction.java @@ -0,0 +1,11 @@ +package konkuk.thip.notification.adapter.out.persistence.function; + +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; + +import java.util.List; + +@FunctionalInterface +public interface PrimaryKeyNotificationQueryFunction { + + List apply(Long lastNotificationId, int pageSize); +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java index c56b45e08..5fc891d93 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationJpaRepository.java @@ -3,5 +3,5 @@ import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface NotificationJpaRepository extends JpaRepository { +public interface NotificationJpaRepository extends JpaRepository, NotificationQueryRepository { } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java new file mode 100644 index 000000000..ccf540952 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java @@ -0,0 +1,14 @@ +package konkuk.thip.notification.adapter.out.persistence.repository; + +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; + +import java.util.List; + +public interface NotificationQueryRepository { + + List findFeedNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); + + List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); + + List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java new file mode 100644 index 000000000..c0fd7b310 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java @@ -0,0 +1,72 @@ +package konkuk.thip.notification.adapter.out.persistence.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.notification.adapter.out.jpa.QNotificationJpaEntity; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; +import konkuk.thip.notification.application.port.out.dto.QNotificationQueryDto; +import konkuk.thip.notification.domain.value.NotificationCategory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class NotificationQueryRepositoryImpl implements NotificationQueryRepository { + + private final JPAQueryFactory queryFactory; + + private final QNotificationJpaEntity notification = QNotificationJpaEntity.notificationJpaEntity; + + @Override + public List findFeedNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { + var where = notification.userJpaEntity.userId.eq(userId) + .and(notification.notificationCategory.eq(NotificationCategory.FEED)); + + where = applyCursor(lastNotificationId, where, notification); + + return getNotificationQueryDtos(pageSize, notification, where); + } + + @Override + public List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { + var where = notification.userJpaEntity.userId.eq(userId) + .and(notification.notificationCategory.eq(NotificationCategory.ROOM)); + where = applyCursor(lastNotificationId, where, notification); + + return getNotificationQueryDtos(pageSize, notification, where); + } + + @Override + public List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize) { + var where = notification.userJpaEntity.userId.eq(userId) + .and(notification.notificationCategory.in(NotificationCategory.FEED, NotificationCategory.ROOM)); + where = applyCursor(lastNotificationId, where, notification); + + return getNotificationQueryDtos(pageSize, notification, where); + } + + private static BooleanExpression applyCursor(Long lastNotificationId, BooleanExpression where, QNotificationJpaEntity notification) { + if (lastNotificationId != null) { + where = where.and(notification.notificationId.lt(lastNotificationId)); + } + return where; + } + + private List getNotificationQueryDtos(int pageSize, QNotificationJpaEntity notification, BooleanExpression where) { + return queryFactory.select(new QNotificationQueryDto( + notification.notificationId, + notification.title, + notification.content, + notification.isChecked, + notification.notificationCategory, + notification.createdAt + )) + .from(notification) + .where(where) + .orderBy(notification.notificationId.desc()) // PK 기준 내림차순 (= 최신순) + .limit(pageSize + 1) + .fetch(); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java b/src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java new file mode 100644 index 000000000..1ccf2203e --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/mapper/NotificationQueryMapper.java @@ -0,0 +1,29 @@ +package konkuk.thip.notification.application.mapper; + +import konkuk.thip.common.util.DateUtil; +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +import java.util.List; + +@Mapper( + componentModel = "spring", + imports = DateUtil.class, + unmappedTargetPolicy = ReportingPolicy.IGNORE // 명시적으로 매핑하지 않은 필드를 무시하도록 설정 +) +public interface NotificationQueryMapper { + + // 단건 매핑 + @Mapping(target = "notificationType", + expression = "java(dto.notificationCategory().getDisplay())") + @Mapping(target = "postDate", + expression = "java(DateUtil.formatBeforeTime(dto.createdAt()))") + NotificationShowResponse.NotificationOfUser toNotificationOfUser(NotificationQueryDto dto); + + // 컬렉션 매핑 + List toNotificationOfUsers(List dtos); + +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java new file mode 100644 index 000000000..4841480c3 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/NotificationShowUseCase.java @@ -0,0 +1,9 @@ +package konkuk.thip.notification.application.port.in; + +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.application.port.in.dto.NotificationType; + +public interface NotificationShowUseCase { + + NotificationShowResponse showNotifications(Long userId, String cursorStr, NotificationType notificationType); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java new file mode 100644 index 000000000..f825b16e6 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/NotificationType.java @@ -0,0 +1,28 @@ +package konkuk.thip.notification.application.port.in.dto; + +import konkuk.thip.common.exception.InvalidStateException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_NOTIFICATION_TYPE; + +@Getter +@RequiredArgsConstructor +public enum NotificationType { + FEED("feed"), + ROOM("room"), + FEED_AND_ROOM("feedAndRoom"); + + private final String type; + + public static NotificationType from(String type) { + return Arrays.stream(NotificationType.values()) + .filter(param -> param.getType().equals(type)) + .findFirst() + .orElseThrow( + () -> new InvalidStateException(INVALID_NOTIFICATION_TYPE) + ); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmDeleteUseCase.java similarity index 74% rename from src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java rename to src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmDeleteUseCase.java index b6335003b..10f3e7e3b 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmDeleteUseCase.java @@ -1,4 +1,4 @@ -package konkuk.thip.notification.application.port.in; +package konkuk.thip.notification.application.port.in.fcm; import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmEnableStateChangeUseCase.java similarity index 77% rename from src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java rename to src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmEnableStateChangeUseCase.java index 14ba4c395..ed19dee18 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmEnableStateChangeUseCase.java @@ -1,4 +1,4 @@ -package konkuk.thip.notification.application.port.in; +package konkuk.thip.notification.application.port.in.fcm; import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmRegisterUseCase.java similarity index 75% rename from src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java rename to src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmRegisterUseCase.java index ff9b096ed..870a4b529 100644 --- a/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java +++ b/src/main/java/konkuk/thip/notification/application/port/in/fcm/FcmRegisterUseCase.java @@ -1,4 +1,4 @@ -package konkuk.thip.notification.application.port.in; +package konkuk.thip.notification.application.port.in.fcm; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; diff --git a/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java new file mode 100644 index 000000000..e1af77781 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java @@ -0,0 +1,14 @@ +package konkuk.thip.notification.application.port.out; + +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; + +public interface NotificationQueryPort { + + CursorBasedList findFeedNotificationsByUserId(Long userId, Cursor cursor); + + CursorBasedList findRoomNotificationsByUserId(Long userId, Cursor cursor); + + CursorBasedList findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.java b/src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.java new file mode 100644 index 000000000..a08f14af0 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/out/dto/NotificationQueryDto.java @@ -0,0 +1,28 @@ +package konkuk.thip.notification.application.port.out.dto; + +import com.querydsl.core.annotations.QueryProjection; +import konkuk.thip.notification.domain.value.NotificationCategory; +import lombok.Builder; +import org.springframework.util.Assert; + +import java.time.LocalDateTime; + +@Builder +public record NotificationQueryDto( + Long notificationId, + String title, + String content, + boolean isChecked, + NotificationCategory notificationCategory, + LocalDateTime createdAt +) { + @QueryProjection + public NotificationQueryDto { + Assert.notNull(notificationId, "NotificationId must not be null"); + Assert.notNull(title, "Title must not be null"); + Assert.notNull(content, "Content must not be null"); + Assert.notNull(isChecked, "isChecked must not be null"); + Assert.notNull(notificationCategory, "NotificationCategory must not be null"); + Assert.notNull(createdAt, "CreatedAt must not be null"); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java new file mode 100644 index 000000000..27e98e14a --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationShowService.java @@ -0,0 +1,42 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; +import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.application.mapper.NotificationQueryMapper; +import konkuk.thip.notification.application.port.in.NotificationShowUseCase; +import konkuk.thip.notification.application.port.in.dto.NotificationType; +import konkuk.thip.notification.application.port.out.NotificationQueryPort; +import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NotificationShowService implements NotificationShowUseCase { + + private final static int PAGE_SIZE = 10; + + private final NotificationQueryPort notificationQueryPort; + private final NotificationQueryMapper notificationQueryMapper; + + @Override + @Transactional(readOnly = true) + public NotificationShowResponse showNotifications(Long userId, String cursorStr, NotificationType notificationType) { + // 1. Cursor 생성 + Cursor cursor = Cursor.from(cursorStr, PAGE_SIZE); + + // 2. 커서 기반 조회 + CursorBasedList result = switch (notificationType) { + case FEED -> notificationQueryPort.findFeedNotificationsByUserId(userId, cursor); + case ROOM -> notificationQueryPort.findRoomNotificationsByUserId(userId, cursor); + case FEED_AND_ROOM -> notificationQueryPort.findFeedAndRoomNotificationsByUserId(userId, cursor); + }; + + // 3. dto -> response 매핑 + var responses = notificationQueryMapper.toNotificationOfUsers(result.contents()); + + return new NotificationShowResponse(responses, result.nextCursor(), !result.hasNext()); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java index bff1959b9..e5bf17417 100644 --- a/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationSyncExecutor.java @@ -4,6 +4,7 @@ import konkuk.thip.notification.application.port.out.NotificationCommandPort; import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.Notification; +import konkuk.thip.notification.domain.value.NotificationCategory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,9 +23,10 @@ public void execute( ) { String title = template.title(args); String content = template.content(args); + NotificationCategory notificationCategory = template.notificationCategory(args); // 1. DB 저장 - saveNotification(title, content, targetUserId); + saveNotification(title, content, notificationCategory, targetUserId); // 2. 이벤트 퍼블리시 try { @@ -37,8 +39,8 @@ public void execute( } } - private void saveNotification(String title, String content, Long targetUserId) { - Notification notification = Notification.withoutId(title, content, targetUserId); + private void saveNotification(String title, String content, NotificationCategory category, Long targetUserId) { + Notification notification = Notification.withoutId(title, content, category, targetUserId); notificationCommandPort.save(notification); } } diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmDeleteService.java similarity index 86% rename from src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java rename to src/main/java/konkuk/thip/notification/application/service/fcm/FcmDeleteService.java index b08f5ab45..b1de324ee 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java +++ b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmDeleteService.java @@ -1,6 +1,6 @@ -package konkuk.thip.notification.application.service; +package konkuk.thip.notification.application.service.fcm; -import konkuk.thip.notification.application.port.in.FcmDeleteUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmDeleteUseCase; import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmEnableStateChangeService.java similarity index 86% rename from src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java rename to src/main/java/konkuk/thip/notification/application/service/fcm/FcmEnableStateChangeService.java index abae5af41..710a6f4a7 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java +++ b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmEnableStateChangeService.java @@ -1,6 +1,6 @@ -package konkuk.thip.notification.application.service; +package konkuk.thip.notification.application.service.fcm; -import konkuk.thip.notification.application.port.in.FcmEnableStateChangeUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmEnableStateChangeUseCase; import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmRegisterService.java similarity index 92% rename from src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java rename to src/main/java/konkuk/thip/notification/application/service/fcm/FcmRegisterService.java index 71f8835d2..48b56ba0d 100644 --- a/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java +++ b/src/main/java/konkuk/thip/notification/application/service/fcm/FcmRegisterService.java @@ -1,6 +1,6 @@ -package konkuk.thip.notification.application.service; +package konkuk.thip.notification.application.service.fcm; -import konkuk.thip.notification.application.port.in.FcmRegisterUseCase; +import konkuk.thip.notification.application.port.in.fcm.FcmRegisterUseCase; import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; import konkuk.thip.notification.domain.FcmToken; diff --git a/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java index 8d8e22f95..30165b1a9 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/NotificationTemplate.java @@ -1,8 +1,12 @@ package konkuk.thip.notification.application.service.template; +import konkuk.thip.notification.domain.value.NotificationCategory; + public interface NotificationTemplate { String title(T args); String content(T args); + + NotificationCategory notificationCategory(T args); } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java index 4c2358115..a28825ff2 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentLikedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java index e58391abe..f3cf02dfd 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedCommentedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 글에 댓글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java index 15b608c3a..bb52e6a68 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedLikedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 글에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java index 37a8de487..5a6c8c143 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FeedRepliedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java index f9e8003f4..1fcdb2b8d 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FollowedTemplate.java @@ -18,5 +18,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 나를 띱했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java index af18247fa..61c4130d1 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/feed/FolloweeNewPostTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 새로운 글을 작성했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.FEED; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java index 4ea611682..6d6cdb605 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomActivityStartedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String roomTitle) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java index 09e09c86c..252b3795d 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomCommentLikedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java index 5ab0096b1..049e33eeb 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomJoinToHostTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 모임에 참여했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String roomTitle, String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java index 860c30c9c..4870d791b 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentRepliedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 댓글에 답글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java index c10510b20..53e475cd9 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostCommentedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 독서기록에 댓글을 달았어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java index 37f3133b7..3eebe7a05 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomPostLikedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 내 독서기록에 좋아요를 눌렀어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java index a1180550e..dd32368f8 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecordCreatedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "@" + args.actorUsername() + " 님이 새로운 독서 기록을 작성했어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String roomTitle, String actorUsername) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java index 600f3c778..85951e30a 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomRecruitClosedEarlyTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "모임방 활동이 시작되었어요. 모임방에서 독서 기록을 시작해보세요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String roomTitle) {} } diff --git a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java index 2384361eb..837b3979f 100644 --- a/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java +++ b/src/main/java/konkuk/thip/notification/application/service/template/room/RoomVoteStartedTemplate.java @@ -16,5 +16,10 @@ public String content(Args args) { return "새로운 투표가 시작되었어요!"; } + @Override + public NotificationCategory notificationCategory(Args args) { + return NotificationCategory.ROOM; + } + public record Args(String roomTitle) {} } diff --git a/src/main/java/konkuk/thip/notification/domain/Notification.java b/src/main/java/konkuk/thip/notification/domain/Notification.java index 6c35bd815..412342eaf 100644 --- a/src/main/java/konkuk/thip/notification/domain/Notification.java +++ b/src/main/java/konkuk/thip/notification/domain/Notification.java @@ -1,6 +1,7 @@ package konkuk.thip.notification.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.notification.domain.value.NotificationCategory; import lombok.Getter; import lombok.experimental.SuperBuilder; @@ -16,13 +17,16 @@ public class Notification extends BaseDomainEntity { private boolean isChecked; + private NotificationCategory notificationCategory; + private Long targetUserId; - public static Notification withoutId (String title, String content, Long targetUserId) { + public static Notification withoutId (String title, String content, NotificationCategory notificationCategory, Long targetUserId) { return Notification.builder() .title(title) .content(content) .isChecked(false) + .notificationCategory(notificationCategory) .targetUserId(targetUserId) .build(); } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index da9c13f2f..44dc0f9eb 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -10,6 +10,7 @@ import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; import konkuk.thip.room.adapter.in.web.response.*; import konkuk.thip.room.application.port.in.*; +import konkuk.thip.room.application.port.in.dto.MyRoomType; import konkuk.thip.room.application.port.in.dto.RoomGetHomeJoinedListQuery; import konkuk.thip.room.application.port.in.dto.RoomSearchQuery; import lombok.RequiredArgsConstructor; @@ -132,7 +133,7 @@ public BaseResponse getMyRooms( @RequestParam(value = "type", required = false, defaultValue = "playingAndRecruiting") final String type, @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor) { - return BaseResponse.ok(roomShowMineUseCase.getMyRooms(userId, type, cursor)); + return BaseResponse.ok(roomShowMineUseCase.getMyRooms(userId, MyRoomType.from(type), cursor)); } @Operation( diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java index 4a7bbffed..c8221d017 100644 --- a/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java @@ -1,8 +1,9 @@ package konkuk.thip.room.application.port.in; import konkuk.thip.room.adapter.in.web.response.RoomShowMineResponse; +import konkuk.thip.room.application.port.in.dto.MyRoomType; public interface RoomShowMineUseCase { - RoomShowMineResponse getMyRooms(Long userId, String type, String cursor); + RoomShowMineResponse getMyRooms(Long userId, MyRoomType myRoomType, String cursor); } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java b/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java index d53f30ec5..e99671939 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java @@ -25,12 +25,11 @@ public class RoomShowMineService implements RoomShowMineUseCase { @Override @Transactional(readOnly = true) - public RoomShowMineResponse getMyRooms(Long userId, String type, String cursor) { + public RoomShowMineResponse getMyRooms(Long userId, MyRoomType myRoomType, String cursor) { // 1. cursor 생성 Cursor nextCursor = Cursor.from(cursor, PAGE_SIZE); - // 2. type 검증 및 커서 기반 조회 - MyRoomType myRoomType = MyRoomType.from(type); + // 2. 커서 기반 조회 CursorBasedList result = switch (myRoomType) { case RECRUITING -> roomQueryPort .findRecruitingRoomsUserParticipated(userId, nextCursor); diff --git a/src/main/resources/db/migration/V250916__Add_notification_category.sql b/src/main/resources/db/migration/V250916__Add_notification_category.sql new file mode 100644 index 000000000..f96e0b8b2 --- /dev/null +++ b/src/main/resources/db/migration/V250916__Add_notification_category.sql @@ -0,0 +1,14 @@ +-- 1단계: NULL 허용으로 notification_category 컬럼 추가 +ALTER TABLE notifications + ADD COLUMN notification_category VARCHAR(16) + COMMENT '알림 카테고리: FEED or ROOM'; + +-- 2단계: 기존 데이터 update +UPDATE notifications +SET notification_category = 'FEED' +WHERE notification_category IS NULL; + +-- 3단계: NOT NULL 제약 추가 +ALTER TABLE notifications + MODIFY COLUMN notification_category VARCHAR(16) NOT NULL + COMMENT '알림 카테고리: FEED/ROOM'; \ No newline at end of file diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 959aeb160..86a2f75f1 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -9,6 +9,8 @@ import konkuk.thip.feed.domain.value.Tag; import konkuk.thip.feed.domain.value.TagList; import konkuk.thip.feed.domain.value.ContentList; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.domain.value.NotificationCategory; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; import konkuk.thip.post.domain.PostType; @@ -371,4 +373,13 @@ public static RecentSearchJpaEntity createRecentSearch(UserJpaEntity userJpaEnti .build(); } + public static NotificationJpaEntity createNotification(UserJpaEntity user, String title, NotificationCategory category) { + return NotificationJpaEntity.builder() + .title(title) + .content("알림 내용") + .isChecked(false) + .notificationCategory(category) + .userJpaEntity(user) + .build(); + } } diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java new file mode 100644 index 000000000..06ac5bc83 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationShowApiTest.java @@ -0,0 +1,285 @@ +package konkuk.thip.notification.adapter.in.web; + +import com.jayway.jsonpath.JsonPath; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.hamcrest.Matchers.*; + + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 알림센터 조회 api 통합 테스트") +@Transactional +class NotificationShowApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + + @Test + @DisplayName("type == feed : 유저의 피드 알림을 최신순으로 반환한다.") + void show_feed_notifications() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity feedN_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_1", NotificationCategory.FEED)); + NotificationJpaEntity feedN_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_2", NotificationCategory.FEED)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), feedN_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), feedN_2.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "feed")); // type == feed + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(2))) + .andExpect(jsonPath("$.data.isLast").value(true)) + // 정렬 순서 : 최신순 (feedN_2 -> feedN_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(feedN_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("피드알림_2")) + .andExpect(jsonPath("$.data.notifications[0].notificationType").value(NotificationCategory.FEED.getDisplay())) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(feedN_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("피드알림_1")) + .andExpect(jsonPath("$.data.notifications[1].notificationType").value(NotificationCategory.FEED.getDisplay())); + } + + @Test + @DisplayName("type == room : 유저의 모임 알림을 최신순으로 반환한다.") + void show_room_notifications() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity roomN_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "모임알림_1", NotificationCategory.ROOM)); + NotificationJpaEntity roomN_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "모임알림_2", NotificationCategory.ROOM)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), roomN_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), roomN_2.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "room")); // type == room + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(2))) + .andExpect(jsonPath("$.data.isLast").value(true)) + // 정렬 순서 : 최신순 (roomN_2 -> roomN_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(roomN_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("모임알림_2")) + .andExpect(jsonPath("$.data.notifications[0].notificationType").value(NotificationCategory.ROOM.getDisplay())) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(roomN_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("모임알림_1")) + .andExpect(jsonPath("$.data.notifications[1].notificationType").value(NotificationCategory.ROOM.getDisplay())); + } + + @Test + @DisplayName("type == null : type이 null 일 경우, 유저의 피드 & 모임 알림을 최신순으로 반환한다.") + void show_feed_and_room_notifications() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity n_feed_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_피드_1", NotificationCategory.FEED)); + NotificationJpaEntity n_feed_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_피드_2", NotificationCategory.FEED)); + NotificationJpaEntity n_room_3 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_모임_3", NotificationCategory.ROOM)); + NotificationJpaEntity n_room_4 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림_모임_4", NotificationCategory.ROOM)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), n_feed_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(20), n_feed_2.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), n_room_3.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(5), n_room_4.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId())); // type == null + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(4))) + .andExpect(jsonPath("$.data.isLast").value(true)) + // 정렬 순서 : 최신순 (n_room_4 -> n_room_3 -> n_feed_2 -> n_feed_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(n_room_4.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("알림_모임_4")) + .andExpect(jsonPath("$.data.notifications[0].notificationType").value(NotificationCategory.ROOM.getDisplay())) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(n_room_3.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("알림_모임_3")) + .andExpect(jsonPath("$.data.notifications[1].notificationType").value(NotificationCategory.ROOM.getDisplay())) + .andExpect(jsonPath("$.data.notifications[2].notificationId").value(n_feed_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[2].title").value("알림_피드_2")) + .andExpect(jsonPath("$.data.notifications[2].notificationType").value(NotificationCategory.FEED.getDisplay())) + .andExpect(jsonPath("$.data.notifications[3].notificationId").value(n_feed_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[3].title").value("알림_피드_1")) + .andExpect(jsonPath("$.data.notifications[3].notificationType").value(NotificationCategory.FEED.getDisplay())); + } + + @Test + @DisplayName("유저의 알림을 최신순으로 최대 10개 반환한다. 다음 페이지에 해당하는 데이터가 있을 경우, 다음 페이지의 cursor 값을 반환한다.") + void show_notifications_paging() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + NotificationJpaEntity feedN_1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_1", NotificationCategory.FEED)); + NotificationJpaEntity feedN_2 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_2", NotificationCategory.FEED)); + NotificationJpaEntity feedN_3 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_3", NotificationCategory.FEED)); + NotificationJpaEntity feedN_4 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_4", NotificationCategory.FEED)); + NotificationJpaEntity feedN_5 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_5", NotificationCategory.FEED)); + NotificationJpaEntity feedN_6 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_6", NotificationCategory.FEED)); + NotificationJpaEntity feedN_7 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_7", NotificationCategory.FEED)); + NotificationJpaEntity feedN_8 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_8", NotificationCategory.FEED)); + NotificationJpaEntity feedN_9 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_9", NotificationCategory.FEED)); + NotificationJpaEntity feedN_10 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_10", NotificationCategory.FEED)); + NotificationJpaEntity feedN_11 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_11", NotificationCategory.FEED)); + NotificationJpaEntity feedN_12 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "피드알림_12", NotificationCategory.FEED)); + + LocalDateTime now = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(50), feedN_1.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(45), feedN_2.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(40), feedN_3.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(35), feedN_4.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(30), feedN_5.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(25), feedN_6.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(20), feedN_7.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(15), feedN_8.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(10), feedN_9.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(5), feedN_10.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(3), feedN_11.getNotificationId() + ); + jdbcTemplate.update( + "UPDATE notifications SET created_at = ? WHERE notification_id = ?", + now.minusMinutes(1), feedN_12.getNotificationId() + ); + + //when //then + MvcResult firstResult = mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "feed")) // type == feed + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(10))) // 10개 조회 + .andExpect(jsonPath("$.data.isLast").value(false)) // 다음 페이지 존재 + .andExpect(jsonPath("$.data.nextCursor").isNotEmpty()) // nextCursor 존재 + // 정렬 순서 : 최신순 (feedN_12 -> feedN_3) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(feedN_12.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("피드알림_12")) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(feedN_11.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("피드알림_11")) + .andExpect(jsonPath("$.data.notifications[2].notificationId").value(feedN_10.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[2].title").value("피드알림_10")) + .andExpect(jsonPath("$.data.notifications[3].notificationId").value(feedN_9.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[3].title").value("피드알림_9")) + .andExpect(jsonPath("$.data.notifications[4].notificationId").value(feedN_8.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[4].title").value("피드알림_8")) + .andExpect(jsonPath("$.data.notifications[5].notificationId").value(feedN_7.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[5].title").value("피드알림_7")) + .andExpect(jsonPath("$.data.notifications[6].notificationId").value(feedN_6.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[6].title").value("피드알림_6")) + .andExpect(jsonPath("$.data.notifications[7].notificationId").value(feedN_5.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[7].title").value("피드알림_5")) + .andExpect(jsonPath("$.data.notifications[8].notificationId").value(feedN_4.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[8].title").value("피드알림_4")) + .andExpect(jsonPath("$.data.notifications[9].notificationId").value(feedN_3.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[9].title").value("피드알림_3")) + .andReturn(); + + String responseBody = firstResult.getResponse().getContentAsString(); + String nextCursor = JsonPath.read(responseBody, "$.data.nextCursor"); + + mockMvc.perform(get("/notifications") + .requestAttr("userId", user.getUserId()) + .param("type", "feed") // type == feed + .param("cursor", nextCursor)) // 두번째 페이지 조회 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.notifications", hasSize(2))) // 2개 조회 + .andExpect(jsonPath("$.data.isLast").value(true)) // 마지막 페이지 + // 정렬 순서 : 최신순 (feedN_2 -> feedN_1) + .andExpect(jsonPath("$.data.notifications[0].notificationId").value(feedN_2.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[0].title").value("피드알림_2")) + .andExpect(jsonPath("$.data.notifications[1].notificationId").value(feedN_1.getNotificationId())) + .andExpect(jsonPath("$.data.notifications[1].title").value("피드알림_1")); + } +} diff --git a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java index d988c278d..8db11367a 100644 --- a/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java +++ b/src/test/java/konkuk/thip/notification/application/service/NotificationSyncExecutorTest.java @@ -3,6 +3,7 @@ import konkuk.thip.notification.application.port.out.NotificationCommandPort; import konkuk.thip.notification.application.service.template.NotificationTemplate; import konkuk.thip.notification.domain.Notification; +import konkuk.thip.notification.domain.value.NotificationCategory; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -27,6 +28,8 @@ void execute_publish_failure_does_not_throw() { public String title(String args) { return "테스트제목"; } @Override public String content(String args) { return "테스트내용"; } + @Override + public NotificationCategory notificationCategory(String args) { return NotificationCategory.FEED; } }; // publish 호출 시 강제로 예외를 던지는 invoker