From fcf322ae925f80a1ff764e63cd4ac5291e3cde2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=98=81?= <141599648+ljy1348@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:53:01 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[FEAT]=20=EC=9C=A0=EC=A0=80=20=EC=84=9C?= =?UTF-8?q?=EC=9E=AC=20=EC=A1=B0=ED=9A=8C=20API=20V2=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=20-=20=EC=9C=A0=EC=A0=80=20=EC=84=9C=EC=9E=AC=20v2=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC,=20=ED=95=84=ED=84=B0,=20=EC=BB=A4=EC=84=9C?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=20-=20=EC=9C=A0=EC=A0=80=EA=B0=80=20?= =?UTF-8?q?=EC=84=9C=EC=9E=AC=EC=97=90=20=EB=93=B1=EB=A1=9D=ED=95=9C=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20=20-=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=84=9C=EC=9E=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84/=EC=82=AD=EC=A0=9C=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20QueryDSL=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/UserNovelSortType.java | 67 ++++++++++ .../userNovel/UserNovelsV2GetResponse.java | 11 ++ .../exception/error/CustomFilteringError.java | 4 +- .../repository/UserNovelCustomRepository.java | 12 ++ .../UserNovelCustomRepositoryImpl.java | 125 +++++++++++++++++- .../UserNovelKeywordCustomRepository.java | 16 +++ .../UserNovelKeywordCustomRepositoryImpl.java | 53 ++++++++ .../UserNovelKeywordRepository.java | 19 +-- .../repository/cursor/CursorCodec.java | 47 +++++++ .../repository/cursor/UserNovelCursor.java | 12 ++ .../library/service/UserNovelService.java | 79 ++++++++++- .../user/controller/UserController.java | 34 +++++ 12 files changed, 459 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java create mode 100644 src/main/java/org/websoso/WSSServer/dto/userNovel/UserNovelsV2GetResponse.java create mode 100644 src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepository.java create mode 100644 src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepositoryImpl.java create mode 100644 src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java create mode 100644 src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java diff --git a/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java b/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java new file mode 100644 index 00000000..1b0a86c6 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java @@ -0,0 +1,67 @@ +package org.websoso.WSSServer.domain.common; + +import static org.websoso.WSSServer.exception.error.CustomFilteringError.SORT_CRITERIA_NOT_FOUND; +import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel; +import static org.websoso.WSSServer.novel.domain.QNovel.novel; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.CaseBuilder; +import java.util.List; +import org.websoso.WSSServer.exception.exception.CustomFilteringException; + +public enum UserNovelSortType { + CREATED_DESC("created_desc"), + CREATED_ASC("created_asc"), + TITLE("title"), + RATING_DESC("rating_desc"), + RATING_ASC("rating_asc"); + + private final String value; + + UserNovelSortType(String value) { + this.value = value; + } + + public static UserNovelSortType of(String sortType) { + if (sortType == null || sortType.isBlank()) { + return CREATED_DESC; + } + + for (UserNovelSortType value : UserNovelSortType.values()) { + if (value.value.equalsIgnoreCase(sortType)) { + return value; + } + } + + throw new CustomFilteringException(SORT_CRITERIA_NOT_FOUND, + "given user novel sort type does not exist"); + } + + public List> orderSpecifiers() { + return switch (this) { + case CREATED_DESC -> List.of(userNovel.createdDate.desc(), userNovel.userNovelId.desc()); + case CREATED_ASC -> List.of(userNovel.createdDate.asc(), userNovel.userNovelId.asc()); + case TITLE -> List.of(novel.title.asc(), userNovel.userNovelId.asc()); + case RATING_DESC -> List.of( + new CaseBuilder() + .when(userNovel.userNovelRating.eq(0.0f)) + .then(1) + .otherwise(0) + .asc(), + userNovel.userNovelRating.desc(), + userNovel.createdDate.desc(), + userNovel.userNovelId.desc() + ); + case RATING_ASC -> List.of( + new CaseBuilder() + .when(userNovel.userNovelRating.eq(0.0f)) + .then(0) + .otherwise(1) + .asc(), + userNovel.userNovelRating.asc(), + userNovel.createdDate.desc(), + userNovel.userNovelId.desc() + ); + }; + } +} diff --git a/src/main/java/org/websoso/WSSServer/dto/userNovel/UserNovelsV2GetResponse.java b/src/main/java/org/websoso/WSSServer/dto/userNovel/UserNovelsV2GetResponse.java new file mode 100644 index 00000000..2a583a04 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/dto/userNovel/UserNovelsV2GetResponse.java @@ -0,0 +1,11 @@ +package org.websoso.WSSServer.dto.userNovel; + +import java.util.List; + +public record UserNovelsV2GetResponse( + Long userNovelCount, + Boolean isLoadable, + String nextCursor, + List userNovels +) { +} diff --git a/src/main/java/org/websoso/WSSServer/exception/error/CustomFilteringError.java b/src/main/java/org/websoso/WSSServer/exception/error/CustomFilteringError.java index e71fb866..3efb14b4 100644 --- a/src/main/java/org/websoso/WSSServer/exception/error/CustomFilteringError.java +++ b/src/main/java/org/websoso/WSSServer/exception/error/CustomFilteringError.java @@ -1,5 +1,6 @@ package org.websoso.WSSServer.exception.error; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; import lombok.AllArgsConstructor; @@ -12,7 +13,8 @@ public enum CustomFilteringError implements ICustomError { SORT_CRITERIA_NOT_FOUND("Filtering-001", "해당 정렬기준을 찾을 수 없습니다.", NOT_FOUND), - FEED_GET_OPTION_NOT_FOUND("Filtering-002", "해당 소소 피드 조회 조건을 찾을 수 없습니다.", NOT_FOUND); + FEED_GET_OPTION_NOT_FOUND("Filtering-002", "해당 소소 피드 조회 조건을 찾을 수 없습니다.", NOT_FOUND), + INVALID_CURSOR("Filtering-003", "유효하지 않은 커서입니다.", BAD_REQUEST); private final String code; private final String description; diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepository.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepository.java index c1dc8b36..f9e20b7a 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepository.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepository.java @@ -4,6 +4,8 @@ import java.util.List; import org.springframework.data.domain.Pageable; import org.websoso.WSSServer.domain.Genre; +import org.websoso.WSSServer.domain.common.UserNovelSortType; +import org.websoso.WSSServer.library.repository.cursor.UserNovelCursor; import org.websoso.WSSServer.novel.domain.Novel; import org.websoso.WSSServer.library.domain.UserNovel; import org.websoso.WSSServer.dto.user.UserNovelCountGetResponse; @@ -23,4 +25,14 @@ List findFilteredUserNovels(Long userId, Boolean isInterest, List readStatuses, List attractivePoints, Float novelRating, String query, LocalDateTime updatedSince); + + List findFilteredUserNovelsV2(Long userId, Boolean isInterest, List readStatuses, + List genres, Boolean isCompleted, Float ratingMin, + Float ratingMax, Boolean unratedOnly, List attractivePoints, + List keywords, UserNovelCursor cursor, int size, + UserNovelSortType sortType); + + Long countByUserIdAndFiltersV2(Long userId, Boolean isInterest, List readStatuses, List genres, + Boolean isCompleted, Float ratingMin, Float ratingMax, Boolean unratedOnly, + List attractivePoints, List keywords); } diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java index 0347e2ba..073c9bd7 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java @@ -22,10 +22,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import org.websoso.WSSServer.domain.Genre; -import org.websoso.WSSServer.novel.domain.Novel; import org.websoso.WSSServer.library.domain.UserNovel; import org.websoso.WSSServer.domain.common.ReadStatus; +import org.websoso.WSSServer.domain.common.UserNovelSortType; import org.websoso.WSSServer.dto.user.UserNovelCountGetResponse; +import org.websoso.WSSServer.library.repository.cursor.UserNovelCursor; +import org.websoso.WSSServer.novel.domain.Novel; @Repository @RequiredArgsConstructor @@ -153,6 +155,43 @@ public Long countByUserIdAndFilters(Long userId, Boolean isInterest, List findFilteredUserNovelsV2(Long userId, Boolean isInterest, List readStatuses, + List genres, Boolean isCompleted, Float ratingMin, + Float ratingMax, Boolean unratedOnly, + List attractivePoints, List keywords, + UserNovelCursor cursor, int size, UserNovelSortType sortType) { + JPAQuery queryBuilder = jpaQueryFactory + .selectFrom(userNovel) + .distinct() + .join(userNovel.novel, novel).fetchJoin() + .where(userNovel.user.userId.eq(userId)); + + applyFiltersV2(queryBuilder, isInterest, readStatuses, genres, isCompleted, ratingMin, ratingMax, + unratedOnly, attractivePoints, keywords); + + queryBuilder.where(cursorCondition(cursor, sortType)); + queryBuilder.orderBy(sortType.orderSpecifiers().toArray(OrderSpecifier[]::new)); + + return queryBuilder.limit(size).fetch(); + } + + @Override + public Long countByUserIdAndFiltersV2(Long userId, Boolean isInterest, List readStatuses, + List genres, Boolean isCompleted, Float ratingMin, Float ratingMax, + Boolean unratedOnly, List attractivePoints, List keywords) { + JPAQuery queryBuilder = jpaQueryFactory + .select(userNovel.countDistinct()) + .from(userNovel) + .join(userNovel.novel, novel) + .where(userNovel.user.userId.eq(userId)); + + applyFiltersV2(queryBuilder, isInterest, readStatuses, genres, isCompleted, ratingMin, ratingMax, + unratedOnly, attractivePoints, keywords); + + return queryBuilder.fetchOne(); + } + private void applyFilters(JPAQuery queryBuilder, Boolean isInterest, List readStatuses, List attractivePoints, Float novelRating, String query, LocalDateTime updatedSince) { @@ -181,4 +220,88 @@ private void applyFilters(JPAQuery queryBuilder, Boolean isInterest, List Optional.ofNullable(updatedSince) .ifPresent(ts -> queryBuilder.where(userNovel.modifiedDate.gt(ts))); } + + private void applyFiltersV2(JPAQuery queryBuilder, Boolean isInterest, List readStatuses, + List genres, Boolean isCompleted, Float ratingMin, Float ratingMax, + Boolean unratedOnly, List attractivePoints, List keywords) { + applyFilters(queryBuilder, isInterest, readStatuses, attractivePoints, null, null, null); + + Optional.ofNullable(genres) + .filter(list -> !list.isEmpty()) + .ifPresent(names -> queryBuilder.where(novel.novelGenres.any().genre.genreName.in(names))); + + Optional.ofNullable(isCompleted) + .ifPresent(completed -> queryBuilder.where(novel.isCompleted.eq(completed))); + + if (Boolean.TRUE.equals(unratedOnly)) { + queryBuilder.where(userNovel.userNovelRating.eq(0.0f)); + } else { + Optional.ofNullable(ratingMin) + .ifPresent(min -> queryBuilder.where(userNovel.userNovelRating.goe(min))); + Optional.ofNullable(ratingMax) + .ifPresent(max -> queryBuilder.where(userNovel.userNovelRating.loe(max))); + } + + Optional.ofNullable(keywords) + .filter(list -> !list.isEmpty()) + .ifPresent(names -> queryBuilder.where(userNovel.userNovelKeywords.any().keyword.keywordName.in(names))); + } + + private BooleanExpression cursorCondition(UserNovelCursor cursor, UserNovelSortType sortType) { + if (cursor == null) { + return null; + } + + return switch (sortType) { + case CREATED_DESC -> createdDescCursorCondition(cursor); + case CREATED_ASC -> createdAscCursorCondition(cursor); + case TITLE -> titleCursorCondition(cursor); + case RATING_DESC -> ratingDescCursorCondition(cursor); + case RATING_ASC -> ratingAscCursorCondition(cursor); + }; + } + + private BooleanExpression createdDescCursorCondition(UserNovelCursor cursor) { + return userNovel.createdDate.lt(cursor.lastCreatedDate()) + .or(userNovel.createdDate.eq(cursor.lastCreatedDate()) + .and(userNovel.userNovelId.lt(cursor.lastUserNovelId()))); + } + + private BooleanExpression createdAscCursorCondition(UserNovelCursor cursor) { + return userNovel.createdDate.gt(cursor.lastCreatedDate()) + .or(userNovel.createdDate.eq(cursor.lastCreatedDate()) + .and(userNovel.userNovelId.gt(cursor.lastUserNovelId()))); + } + + private BooleanExpression titleCursorCondition(UserNovelCursor cursor) { + return novel.title.gt(cursor.lastTitle()) + .or(novel.title.eq(cursor.lastTitle()) + .and(userNovel.userNovelId.gt(cursor.lastUserNovelId()))); + } + + private BooleanExpression ratingDescCursorCondition(UserNovelCursor cursor) { + if (Boolean.TRUE.equals(cursor.rated())) { + return userNovel.userNovelRating.ne(0.0f) + .and(userNovel.userNovelRating.lt(cursor.lastRating()) + .or(userNovel.userNovelRating.eq(cursor.lastRating()) + .and(createdDescCursorCondition(cursor)))) + .or(userNovel.userNovelRating.eq(0.0f)); + } + + return userNovel.userNovelRating.eq(0.0f) + .and(createdDescCursorCondition(cursor)); + } + + private BooleanExpression ratingAscCursorCondition(UserNovelCursor cursor) { + if (Boolean.TRUE.equals(cursor.rated())) { + return userNovel.userNovelRating.ne(0.0f) + .and(userNovel.userNovelRating.gt(cursor.lastRating()) + .or(userNovel.userNovelRating.eq(cursor.lastRating()) + .and(createdDescCursorCondition(cursor)))); + } + + return userNovel.userNovelRating.eq(0.0f) + .and(createdDescCursorCondition(cursor)) + .or(userNovel.userNovelRating.ne(0.0f)); + } } diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepository.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepository.java new file mode 100644 index 00000000..18589fb7 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepository.java @@ -0,0 +1,16 @@ +package org.websoso.WSSServer.library.repository; + +import java.util.List; +import java.util.Set; +import org.springframework.data.domain.Pageable; +import org.websoso.WSSServer.library.domain.Keyword; +import org.websoso.WSSServer.library.domain.UserNovel; + +public interface UserNovelKeywordCustomRepository { + + List findTopKeywordsByCount(Pageable pageable); + + List findKeywordsByUserIdOrderByCountDesc(Long userId); + + void deleteByKeywordsAndUserNovel(Set keywords, UserNovel userNovel); +} diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepositoryImpl.java new file mode 100644 index 00000000..bb309d14 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepositoryImpl.java @@ -0,0 +1,53 @@ +package org.websoso.WSSServer.library.repository; + +import static org.websoso.WSSServer.library.domain.QUserNovelKeyword.userNovelKeyword; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import org.websoso.WSSServer.library.domain.Keyword; +import org.websoso.WSSServer.library.domain.UserNovel; + +@Repository +@RequiredArgsConstructor +public class UserNovelKeywordCustomRepositoryImpl implements UserNovelKeywordCustomRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findTopKeywordsByCount(Pageable pageable) { + return jpaQueryFactory + .select(userNovelKeyword.keyword) + .from(userNovelKeyword) + .groupBy(userNovelKeyword.keyword) + .orderBy(userNovelKeyword.count().desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + @Override + public List findKeywordsByUserIdOrderByCountDesc(Long userId) { + return jpaQueryFactory + .select(userNovelKeyword.keyword) + .from(userNovelKeyword) + .where(userNovelKeyword.userNovel.user.userId.eq(userId)) + .groupBy(userNovelKeyword.keyword) + .orderBy(userNovelKeyword.count().desc(), userNovelKeyword.keyword.sortOrder.asc()) + .fetch(); + } + + @Override + public void deleteByKeywordsAndUserNovel(Set keywords, UserNovel userNovel) { + jpaQueryFactory + .delete(userNovelKeyword) + .where( + userNovelKeyword.userNovel.eq(userNovel), + userNovelKeyword.keyword.in(keywords) + ) + .execute(); + } +} diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordRepository.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordRepository.java index eefa42dd..badcdd2d 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordRepository.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordRepository.java @@ -1,29 +1,14 @@ package org.websoso.WSSServer.library.repository; import java.util.List; -import java.util.Set; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.library.domain.Keyword; import org.websoso.WSSServer.novel.domain.Novel; -import org.websoso.WSSServer.library.domain.UserNovel; import org.websoso.WSSServer.library.domain.UserNovelKeyword; @Repository -public interface UserNovelKeywordRepository extends JpaRepository { +public interface UserNovelKeywordRepository extends JpaRepository, + UserNovelKeywordCustomRepository { List findAllByUserNovel_Novel(Novel novel); - - @Query("SELECT unk.keyword FROM UserNovelKeyword unk GROUP BY unk.keyword ORDER BY COUNT(unk) DESC") - List findTopKeywordsByCount(Pageable pageable); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Transactional - @Query("DELETE FROM UserNovelKeyword un WHERE un.userNovel = :userNovel AND un.keyword IN :keywords") - void deleteByKeywordsAndUserNovel(Set keywords, UserNovel userNovel); - } diff --git a/src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java b/src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java new file mode 100644 index 00000000..59b5e57d --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java @@ -0,0 +1,47 @@ +package org.websoso.WSSServer.library.repository.cursor; + +import static org.websoso.WSSServer.exception.error.CustomFilteringError.INVALID_CURSOR; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.websoso.WSSServer.exception.exception.CustomFilteringException; + +@Component +@RequiredArgsConstructor +public class CursorCodec { + + private final ObjectMapper objectMapper; + + public String encode(UserNovelCursor cursor) { + if (cursor == null) { + return null; + } + + try { + String json = objectMapper.writeValueAsString(cursor); + return Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + throw new CustomFilteringException(INVALID_CURSOR, "failed to encode user novel cursor"); + } + } + + public UserNovelCursor decode(String cursor) { + if (cursor == null || cursor.isBlank()) { + return null; + } + + try { + byte[] decoded = Base64.getUrlDecoder().decode(cursor); + return objectMapper.readValue(decoded, UserNovelCursor.class); + } catch (IllegalArgumentException | IOException e) { + throw new CustomFilteringException(INVALID_CURSOR, "given user novel cursor is invalid"); + } + } +} diff --git a/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java b/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java new file mode 100644 index 00000000..3353af3c --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java @@ -0,0 +1,12 @@ +package org.websoso.WSSServer.library.repository.cursor; + +import java.time.LocalDateTime; + +public record UserNovelCursor( + Float lastRating, + LocalDateTime lastCreatedDate, + Long lastUserNovelId, + Boolean rated, + String lastTitle +) { +} diff --git a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java index e61216cc..011d2777 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java @@ -25,6 +25,9 @@ import org.websoso.WSSServer.library.domain.UserNovelKeyword; import org.websoso.WSSServer.domain.common.Gender; import org.websoso.WSSServer.domain.common.SortCriteria; +import org.websoso.WSSServer.domain.common.UserNovelSortType; +import org.websoso.WSSServer.dto.keyword.KeywordGetResponse; +import org.websoso.WSSServer.dto.keyword.KeywordPopularGetResponse; import org.websoso.WSSServer.dto.user.UserNovelCountGetResponse; import org.websoso.WSSServer.dto.userNovel.TasteKeywordGetResponse; import org.websoso.WSSServer.dto.userNovel.UserGenrePreferenceGetResponse; @@ -33,10 +36,14 @@ import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelGetResponseLegacy; import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelsGetResponse; import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelsGetResponseLegacy; +import org.websoso.WSSServer.dto.userNovel.UserNovelsV2GetResponse; import org.websoso.WSSServer.dto.userNovel.UserTasteAttractivePointPreferencesAndKeywordsGetResponse; import org.websoso.WSSServer.exception.exception.CustomGenreException; import org.websoso.WSSServer.exception.exception.CustomUserException; import org.websoso.WSSServer.feed.repository.FeedRepository; +import org.websoso.WSSServer.library.repository.cursor.CursorCodec; +import org.websoso.WSSServer.library.repository.cursor.UserNovelCursor; +import org.websoso.WSSServer.library.repository.UserNovelKeywordRepository; import org.websoso.WSSServer.repository.GenreRepository; import org.websoso.WSSServer.library.repository.UserNovelRepository; import org.websoso.WSSServer.user.service.UserService; @@ -51,6 +58,8 @@ public class UserNovelService { private final UserService userService; private final GenreRepository genreRepository; private final FeedRepository feedRepository; + private final CursorCodec cursorCodec; + private final UserNovelKeywordRepository userNovelKeywordRepository; private static final List priorityGenreNamesOfMale = List.of( "fantasy", "modernFantasy", "wuxia", "drama", "mystery", "lightNovel", "romance", "romanceFantasy", "BL" @@ -76,7 +85,7 @@ public UserNovelAndNovelsGetResponse getUserNovelsAndNovels(User visitor, Long o throw new CustomUserException(PRIVATE_PROFILE_STATUS, "the profile status of the user is set to private"); } - boolean isOwner = visitor.getUserId().equals(ownerId); + boolean isOwner = isOwner(visitor, ownerId); boolean isAscending = sortCriteria.isOld(); List userNovels = userNovelRepository.findFilteredUserNovels(ownerId, isInterest, readStatuses, @@ -93,6 +102,64 @@ public UserNovelAndNovelsGetResponse getUserNovelsAndNovels(User visitor, Long o return new UserNovelAndNovelsGetResponse(totalCount, isLoadable, userNovelAndNovelGetResponses); } + @Transactional(readOnly = true) + public UserNovelsV2GetResponse getUserNovelsAndNovelsV2(User visitor, Long ownerId, String cursor, int size, + String sortType, Boolean isInterest, + List readStatuses, List genres, + Boolean isCompleted, Float ratingMin, Float ratingMax, + Boolean unratedOnly, List attractivePoints, + List keywords) { + User owner = userService.getUserOrException(ownerId); + + if (isProfileInaccessible(visitor, ownerId, owner)) { + throw new CustomUserException(PRIVATE_PROFILE_STATUS, "the profile status of the user is set to private"); + } + + boolean isOwner = isOwner(visitor, ownerId); + UserNovelSortType userNovelSortType = UserNovelSortType.of(sortType); + UserNovelCursor userNovelCursor = cursorCodec.decode(cursor); + + int requestSize = Math.max(size, 0); + List fetchedUserNovels = userNovelRepository.findFilteredUserNovelsV2(ownerId, isInterest, + readStatuses, genres, isCompleted, ratingMin, ratingMax, unratedOnly, attractivePoints, keywords, + userNovelCursor, requestSize + 1, userNovelSortType); + + boolean isLoadable = requestSize > 0 && fetchedUserNovels.size() > requestSize; + List userNovels = requestSize == 0 + ? List.of() + : isLoadable + ? fetchedUserNovels.subList(0, requestSize) + : fetchedUserNovels; + + Long totalCount = userNovelRepository.countByUserIdAndFiltersV2(ownerId, isInterest, readStatuses, genres, + isCompleted, ratingMin, ratingMax, unratedOnly, attractivePoints, keywords); + + List userNovelAndNovelGetResponses = buildUserNovelAndNovelGetResponses( + userNovels, ownerId, isOwner); + + String nextCursor = isLoadable && !userNovels.isEmpty() + ? cursorCodec.encode(toCursor(userNovels.get(userNovels.size() - 1))) + : null; + + return new UserNovelsV2GetResponse(totalCount, isLoadable, nextCursor, userNovelAndNovelGetResponses); + } + + @Transactional(readOnly = true) + public KeywordPopularGetResponse getUserNovelKeywordsV2(User visitor, Long ownerId) { + User owner = userService.getUserOrException(ownerId); + + if (isProfileInaccessible(visitor, ownerId, owner)) { + throw new CustomUserException(PRIVATE_PROFILE_STATUS, "the profile status of the user is set to private"); + } + + List keywords = userNovelKeywordRepository.findKeywordsByUserIdOrderByCountDesc(ownerId) + .stream() + .map(KeywordGetResponse::of) + .toList(); + + return KeywordPopularGetResponse.of(keywords); + } + @Transactional(readOnly = true) public UserNovelAndNovelsGetResponseLegacy getUserNovelsAndNovelsLegacy(User visitor, Long ownerId, String readStatus, @@ -148,6 +215,16 @@ private List buildUserNovelAndNovelGetResponses(Li .toList(); } + private UserNovelCursor toCursor(UserNovel userNovel) { + return new UserNovelCursor( + userNovel.getUserNovelRating(), + userNovel.getCreatedDate(), + userNovel.getUserNovelId(), + Float.compare(userNovel.getUserNovelRating(), 0.0f) != 0, + userNovel.getNovel().getTitle() + ); + } + private Map> getFeedsGroupedByNovel(List userNovels, Long ownerId, boolean isOwner) { List novelIds = userNovels.stream() .map(un -> un.getNovel().getNovelId()) diff --git a/src/main/java/org/websoso/WSSServer/user/controller/UserController.java b/src/main/java/org/websoso/WSSServer/user/controller/UserController.java index b1ae01f2..5f7294e1 100644 --- a/src/main/java/org/websoso/WSSServer/user/controller/UserController.java +++ b/src/main/java/org/websoso/WSSServer/user/controller/UserController.java @@ -25,6 +25,7 @@ import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.domain.common.SortCriteria; import org.websoso.WSSServer.dto.feed.UserFeedsGetResponse; +import org.websoso.WSSServer.dto.keyword.KeywordPopularGetResponse; import org.websoso.WSSServer.dto.user.PushSettingGetResponse; import org.websoso.WSSServer.dto.user.PushSettingRequest; import org.websoso.WSSServer.dto.user.EditMyInfoRequest; @@ -44,6 +45,7 @@ import org.websoso.WSSServer.dto.userNovel.UserGenrePreferencesGetResponse; import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelsGetResponse; import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelsGetResponseLegacy; +import org.websoso.WSSServer.dto.userNovel.UserNovelsV2GetResponse; import org.websoso.WSSServer.dto.userNovel.UserTasteAttractivePointPreferencesAndKeywordsGetResponse; import org.websoso.WSSServer.feed.service.FeedService; import org.websoso.WSSServer.library.service.UserNovelService; @@ -183,6 +185,38 @@ public ResponseEntity getUserNovelsAndNovels(@Aut lastUserNovelId, size, sortCriteria, updatedSince)); } + @GetMapping("/{userId}/novels/v2") + public ResponseEntity getUserNovelsAndNovelsV2( + @AuthenticationPrincipal User visitor, + @PathVariable("userId") Long userId, + @RequestParam(value = "cursor", required = false) String cursor, + @RequestParam("size") int size, + @RequestParam(value = "sortType", required = false, defaultValue = "created_desc") String sortType, + @RequestParam(value = "isInterest", required = false) Boolean isInterest, + @RequestParam(value = "readStatuses", required = false) List readStatuses, + @RequestParam(value = "genres", required = false) List genres, + @RequestParam(value = "isCompleted", required = false) Boolean isCompleted, + @RequestParam(value = "ratingMin", required = false) Float ratingMin, + @RequestParam(value = "ratingMax", required = false) Float ratingMax, + @RequestParam(value = "unratedOnly", required = false) Boolean unratedOnly, + @RequestParam(value = "attractivePoints", required = false) List attractivePoints, + @RequestParam(value = "keywords", required = false) List keywords) { + return ResponseEntity + .status(OK) + .body(userNovelService.getUserNovelsAndNovelsV2(visitor, userId, cursor, size, sortType, isInterest, + readStatuses, genres, isCompleted, ratingMin, ratingMax, unratedOnly, attractivePoints, + keywords)); + } + + @GetMapping("/{userId}/novels/keywords") + public ResponseEntity getUserNovelKeywordsV2( + @AuthenticationPrincipal User visitor, + @PathVariable("userId") Long userId) { + return ResponseEntity + .status(OK) + .body(userNovelService.getUserNovelKeywordsV2(visitor, userId)); + } + @GetMapping("/{userId}/novels/legacy") public ResponseEntity getUserNovelsAndNovelsOld( @AuthenticationPrincipal User visitor, From 800dd640ddb333fd485716c977cf2fafc5b35d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=98=81?= <141599648+ljy1348@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:43:04 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[FEAT]=20=EC=9C=A0=EC=A0=80=20=EC=84=9C?= =?UTF-8?q?=EC=9E=AC=20=EC=A1=B0=ED=9A=8C=20API=20V2=20-=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=80=20=EB=82=A0=EC=A7=9C=EC=88=9C=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/UserNovelSortType.java | 5 ++--- .../UserNovelCustomRepositoryImpl.java | 16 +++++++++++----- .../repository/cursor/UserNovelCursor.java | 3 ++- .../library/service/UserNovelService.java | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java b/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java index 1b0a86c6..46df150e 100644 --- a/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java +++ b/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java @@ -2,7 +2,6 @@ import static org.websoso.WSSServer.exception.error.CustomFilteringError.SORT_CRITERIA_NOT_FOUND; import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel; -import static org.websoso.WSSServer.novel.domain.QNovel.novel; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.CaseBuilder; @@ -12,7 +11,7 @@ public enum UserNovelSortType { CREATED_DESC("created_desc"), CREATED_ASC("created_asc"), - TITLE("title"), + READ_DATE("read_date"), RATING_DESC("rating_desc"), RATING_ASC("rating_asc"); @@ -41,7 +40,7 @@ public List> orderSpecifiers() { return switch (this) { case CREATED_DESC -> List.of(userNovel.createdDate.desc(), userNovel.userNovelId.desc()); case CREATED_ASC -> List.of(userNovel.createdDate.asc(), userNovel.userNovelId.asc()); - case TITLE -> List.of(novel.title.asc(), userNovel.userNovelId.asc()); + case READ_DATE -> List.of(userNovel.startDate.desc().nullsLast(), userNovel.userNovelId.desc()); case RATING_DESC -> List.of( new CaseBuilder() .when(userNovel.userNovelRating.eq(0.0f)) diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java index 073c9bd7..aea37010 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java @@ -255,7 +255,7 @@ private BooleanExpression cursorCondition(UserNovelCursor cursor, UserNovelSortT return switch (sortType) { case CREATED_DESC -> createdDescCursorCondition(cursor); case CREATED_ASC -> createdAscCursorCondition(cursor); - case TITLE -> titleCursorCondition(cursor); + case READ_DATE -> readDateCursorCondition(cursor); case RATING_DESC -> ratingDescCursorCondition(cursor); case RATING_ASC -> ratingAscCursorCondition(cursor); }; @@ -273,10 +273,16 @@ private BooleanExpression createdAscCursorCondition(UserNovelCursor cursor) { .and(userNovel.userNovelId.gt(cursor.lastUserNovelId()))); } - private BooleanExpression titleCursorCondition(UserNovelCursor cursor) { - return novel.title.gt(cursor.lastTitle()) - .or(novel.title.eq(cursor.lastTitle()) - .and(userNovel.userNovelId.gt(cursor.lastUserNovelId()))); + private BooleanExpression readDateCursorCondition(UserNovelCursor cursor) { + if (cursor.lastStartDate() == null) { + return userNovel.startDate.isNull() + .and(userNovel.userNovelId.lt(cursor.lastUserNovelId())); + } + + return userNovel.startDate.lt(cursor.lastStartDate()) + .or(userNovel.startDate.eq(cursor.lastStartDate()) + .and(userNovel.userNovelId.lt(cursor.lastUserNovelId()))) + .or(userNovel.startDate.isNull()); } private BooleanExpression ratingDescCursorCondition(UserNovelCursor cursor) { diff --git a/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java b/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java index 3353af3c..ab5ada98 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java @@ -1,12 +1,13 @@ package org.websoso.WSSServer.library.repository.cursor; import java.time.LocalDateTime; +import java.time.LocalDate; public record UserNovelCursor( Float lastRating, LocalDateTime lastCreatedDate, Long lastUserNovelId, Boolean rated, - String lastTitle + LocalDate lastStartDate ) { } diff --git a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java index 011d2777..f787db3d 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java @@ -221,7 +221,7 @@ private UserNovelCursor toCursor(UserNovel userNovel) { userNovel.getCreatedDate(), userNovel.getUserNovelId(), Float.compare(userNovel.getUserNovelRating(), 0.0f) != 0, - userNovel.getNovel().getTitle() + userNovel.getStartDate() ); } From c9393aa438d5e6484b6e1ed45e05cf4fa9a0f351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=98=81?= <141599648+ljy1348@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:28:24 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[REFACTOR]=20CursorCodec.java=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=A5=BC=20repository=20->=20util=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../library/{repository/cursor => util}/CursorCodec.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/main/java/org/websoso/WSSServer/library/{repository/cursor => util}/CursorCodec.java (93%) diff --git a/src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java b/src/main/java/org/websoso/WSSServer/library/util/CursorCodec.java similarity index 93% rename from src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java rename to src/main/java/org/websoso/WSSServer/library/util/CursorCodec.java index 59b5e57d..e07e4ab2 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/cursor/CursorCodec.java +++ b/src/main/java/org/websoso/WSSServer/library/util/CursorCodec.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.library.repository.cursor; +package org.websoso.WSSServer.library.util; import static org.websoso.WSSServer.exception.error.CustomFilteringError.INVALID_CURSOR; @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.websoso.WSSServer.exception.exception.CustomFilteringException; +import org.websoso.WSSServer.library.repository.cursor.UserNovelCursor; @Component @RequiredArgsConstructor From 4ede3ec88ffa2a1585618f832b10c68621cb75aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A3=BC=EC=98=81?= <141599648+ljy1348@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:28:43 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[FEAT]=20=EC=84=9C=EC=9E=AC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20V2=EC=97=90=20TITLE=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/UserNovelSortType.java | 6 +++++ .../UserNovelCustomRepositoryImpl.java | 23 +++++++++++++++++++ .../repository/cursor/UserNovelCursor.java | 3 ++- .../library/service/UserNovelService.java | 5 ++-- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java b/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java index 46df150e..bd219333 100644 --- a/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java +++ b/src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java @@ -2,6 +2,7 @@ import static org.websoso.WSSServer.exception.error.CustomFilteringError.SORT_CRITERIA_NOT_FOUND; import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel; +import static org.websoso.WSSServer.novel.domain.QNovel.novel; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.CaseBuilder; @@ -11,6 +12,9 @@ public enum UserNovelSortType { CREATED_DESC("created_desc"), CREATED_ASC("created_asc"), + TITLE("title"), + TITLE_ASC("title_asc"), + TITLE_DESC("title_desc"), READ_DATE("read_date"), RATING_DESC("rating_desc"), RATING_ASC("rating_asc"); @@ -40,6 +44,8 @@ public List> orderSpecifiers() { return switch (this) { case CREATED_DESC -> List.of(userNovel.createdDate.desc(), userNovel.userNovelId.desc()); case CREATED_ASC -> List.of(userNovel.createdDate.asc(), userNovel.userNovelId.asc()); + case TITLE, TITLE_ASC -> List.of(novel.title.asc(), userNovel.userNovelId.asc()); + case TITLE_DESC -> List.of(novel.title.desc(), userNovel.userNovelId.desc()); case READ_DATE -> List.of(userNovel.startDate.desc().nullsLast(), userNovel.userNovelId.desc()); case RATING_DESC -> List.of( new CaseBuilder() diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java index aea37010..d30f446b 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelCustomRepositoryImpl.java @@ -221,6 +221,7 @@ private void applyFilters(JPAQuery queryBuilder, Boolean isInterest, List .ifPresent(ts -> queryBuilder.where(userNovel.modifiedDate.gt(ts))); } + // 전달받은 QueryDSL 쿼리 객체에 필터 조건을 누적해서 적용한다. private void applyFiltersV2(JPAQuery queryBuilder, Boolean isInterest, List readStatuses, List genres, Boolean isCompleted, Float ratingMin, Float ratingMax, Boolean unratedOnly, List attractivePoints, List keywords) { @@ -255,6 +256,8 @@ private BooleanExpression cursorCondition(UserNovelCursor cursor, UserNovelSortT return switch (sortType) { case CREATED_DESC -> createdDescCursorCondition(cursor); case CREATED_ASC -> createdAscCursorCondition(cursor); + case TITLE, TITLE_ASC -> titleAscCursorCondition(cursor); + case TITLE_DESC -> titleDescCursorCondition(cursor); case READ_DATE -> readDateCursorCondition(cursor); case RATING_DESC -> ratingDescCursorCondition(cursor); case RATING_ASC -> ratingAscCursorCondition(cursor); @@ -273,6 +276,26 @@ private BooleanExpression createdAscCursorCondition(UserNovelCursor cursor) { .and(userNovel.userNovelId.gt(cursor.lastUserNovelId()))); } + private BooleanExpression titleAscCursorCondition(UserNovelCursor cursor) { + if (cursor.lastTitle() == null) { + return null; + } + + return novel.title.gt(cursor.lastTitle()) + .or(novel.title.eq(cursor.lastTitle()) + .and(userNovel.userNovelId.gt(cursor.lastUserNovelId()))); + } + + private BooleanExpression titleDescCursorCondition(UserNovelCursor cursor) { + if (cursor.lastTitle() == null) { + return null; + } + + return novel.title.lt(cursor.lastTitle()) + .or(novel.title.eq(cursor.lastTitle()) + .and(userNovel.userNovelId.lt(cursor.lastUserNovelId()))); + } + private BooleanExpression readDateCursorCondition(UserNovelCursor cursor) { if (cursor.lastStartDate() == null) { return userNovel.startDate.isNull() diff --git a/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java b/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java index ab5ada98..808a6535 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/cursor/UserNovelCursor.java @@ -8,6 +8,7 @@ public record UserNovelCursor( LocalDateTime lastCreatedDate, Long lastUserNovelId, Boolean rated, - LocalDate lastStartDate + LocalDate lastStartDate, + String lastTitle ) { } diff --git a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java index f5081926..650c1f69 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java @@ -41,7 +41,7 @@ import org.websoso.WSSServer.exception.exception.CustomGenreException; import org.websoso.WSSServer.exception.exception.CustomUserException; import org.websoso.WSSServer.feed.feed.repository.FeedRepository; -import org.websoso.WSSServer.library.repository.cursor.CursorCodec; +import org.websoso.WSSServer.library.util.CursorCodec; import org.websoso.WSSServer.library.repository.cursor.UserNovelCursor; import org.websoso.WSSServer.library.repository.UserNovelKeywordRepository; import org.websoso.WSSServer.repository.GenreRepository; @@ -221,7 +221,8 @@ private UserNovelCursor toCursor(UserNovel userNovel) { userNovel.getCreatedDate(), userNovel.getUserNovelId(), Float.compare(userNovel.getUserNovelRating(), 0.0f) != 0, - userNovel.getStartDate() + userNovel.getStartDate(), + userNovel.getNovel().getTitle() ); }