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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,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(),
Comment on lines +61 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Put unrated items after rated ones for ascending ratings

When sortType=rating_asc and 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 treats 0.0f as unrated by pushing it after rated rows, and there is a separate unratedOnly filter, so the ascending rating view can otherwise be filled with unrated works instead of the user's lowest-rated works.

Useful? React with 👍 / 👎.

userNovel.userNovelRating.asc(),
userNovel.createdDate.desc(),
userNovel.userNovelId.desc()
);
};
}
}
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
) {
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,4 +25,14 @@ List<UserNovel> findFilteredUserNovels(Long userId, Boolean isInterest, List<Str
Long countByUserIdAndFilters(Long userId, Boolean isInterest, List<String> readStatuses,
List<String> attractivePoints, Float novelRating, String query,
LocalDateTime updatedSince);

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);

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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) {
Expand Down Expand Up @@ -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));
}
}
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);
}
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();
}
}
Loading
Loading