-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] 사용자 서재 조회 API V2 추가 #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
fcf322a
[FEAT] 유저 서재 조회 API V2 추가
ljy1348 800dd64
[FEAT] 유저 서재 조회 API V2 - 읽은 날짜순 정렬 추가
ljy1348 b5ed1da
merge develop into feat/#537
GiJungPark c9393aa
[REFACTOR] CursorCodec.java 위치를 repository -> util로 변경
ljy1348 4ede3ec
[FEAT] 서재 조회 API V2에 TITLE 정렬 추가
ljy1348 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
72 changes: 72 additions & 0 deletions
72
src/main/java/org/websoso/WSSServer/domain/common/UserNovelSortType.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| 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"), | ||
| TITLE_ASC("title_asc"), | ||
| TITLE_DESC("title_desc"), | ||
| READ_DATE("read_date"), | ||
| 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<OrderSpecifier<?>> 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() | ||
| .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() | ||
| ); | ||
| }; | ||
| } | ||
| } | ||
11 changes: 11 additions & 0 deletions
11
src/main/java/org/websoso/WSSServer/dto/userNovel/UserNovelsV2GetResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package org.websoso.WSSServer.dto.userNovel; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record UserNovelsV2GetResponse( | ||
| Long userNovelCount, | ||
| Boolean isLoadable, | ||
| String nextCursor, | ||
| List<UserNovelAndNovelGetResponse> userNovels | ||
| ) { | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<String | |
| return queryBuilder.fetchOne(); | ||
| } | ||
|
|
||
| @Override | ||
| public List<UserNovel> findFilteredUserNovelsV2(Long userId, Boolean isInterest, List<String> readStatuses, | ||
| List<String> genres, Boolean isCompleted, Float ratingMin, | ||
| Float ratingMax, Boolean unratedOnly, | ||
| List<String> attractivePoints, List<String> keywords, | ||
| UserNovelCursor cursor, int size, UserNovelSortType sortType) { | ||
| JPAQuery<UserNovel> queryBuilder = jpaQueryFactory | ||
| .selectFrom(userNovel) | ||
| .distinct() | ||
| .join(userNovel.novel, novel).fetchJoin() | ||
| .where(userNovel.user.userId.eq(userId)); | ||
|
|
||
| applyFiltersV2(queryBuilder, isInterest, readStatuses, genres, isCompleted, ratingMin, ratingMax, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p4; |
||
| 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<String> readStatuses, | ||
| List<String> genres, Boolean isCompleted, Float ratingMin, Float ratingMax, | ||
| Boolean unratedOnly, List<String> attractivePoints, List<String> keywords) { | ||
| JPAQuery<Long> 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 <T> void applyFilters(JPAQuery<T> queryBuilder, Boolean isInterest, List<String> readStatuses, | ||
| List<String> attractivePoints, Float novelRating, String query, | ||
| LocalDateTime updatedSince) { | ||
|
|
@@ -181,4 +220,117 @@ private <T> void applyFilters(JPAQuery<T> queryBuilder, Boolean isInterest, List | |
| Optional.ofNullable(updatedSince) | ||
| .ifPresent(ts -> queryBuilder.where(userNovel.modifiedDate.gt(ts))); | ||
| } | ||
|
|
||
| // 전달받은 QueryDSL 쿼리 객체에 필터 조건을 누적해서 적용한다. | ||
| private <T> void applyFiltersV2(JPAQuery<T> queryBuilder, Boolean isInterest, List<String> readStatuses, | ||
| List<String> genres, Boolean isCompleted, Float ratingMin, Float ratingMax, | ||
| Boolean unratedOnly, List<String> attractivePoints, List<String> 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, TITLE_ASC -> titleAscCursorCondition(cursor); | ||
| case TITLE_DESC -> titleDescCursorCondition(cursor); | ||
| case READ_DATE -> readDateCursorCondition(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 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() | ||
| .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) { | ||
| 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)); | ||
| } | ||
| } | ||
16 changes: 16 additions & 0 deletions
16
src/main/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Keyword> findTopKeywordsByCount(Pageable pageable); | ||
|
|
||
| List<Keyword> findKeywordsByUserIdOrderByCountDesc(Long userId); | ||
|
|
||
| void deleteByKeywordsAndUserNovel(Set<Keyword> keywords, UserNovel userNovel); | ||
| } |
53 changes: 53 additions & 0 deletions
53
...n/java/org/websoso/WSSServer/library/repository/UserNovelKeywordCustomRepositoryImpl.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Keyword> 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<Keyword> 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<Keyword> keywords, UserNovel userNovel) { | ||
| jpaQueryFactory | ||
| .delete(userNovelKeyword) | ||
| .where( | ||
| userNovelKeyword.userNovel.eq(userNovel), | ||
| userNovelKeyword.keyword.in(keywords) | ||
| ) | ||
| .execute(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
sortType=rating_ascand the library contains interest-only/unrated entries (userNovelRating == 0.0f), this case expression sorts those unrated rows before actual low ratings such as 0.5 or 1.0. The descending sort already treats0.0fas unrated by pushing it after rated rows, and there is a separateunratedOnlyfilter, so the ascending rating view can otherwise be filled with unrated works instead of the user's lowest-rated works.Useful? React with 👍 / 👎.