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
Expand Up @@ -6,6 +6,11 @@

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.DateExpression;
import com.querydsl.core.types.dsl.DateTimeExpression;
import com.querydsl.core.types.dsl.Expressions;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.websoso.WSSServer.exception.exception.CustomFilteringException;

Expand Down Expand Up @@ -40,13 +45,18 @@ public static UserNovelSortType of(String sortType) {
"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 READ_DATE -> List.of(
readDateExpression().desc().nullsLast(),
undatedCreatedDateExpression().desc().nullsLast(),
userNovel.userNovelId.desc()
);
case RATING_DESC -> List.of(
new CaseBuilder()
.when(userNovel.userNovelRating.eq(0.0f))
Expand All @@ -69,4 +79,20 @@ public List<OrderSpecifier<?>> orderSpecifiers() {
);
};
}

// 독서 상태에 따라 날짜순 정렬에 사용할 날짜 표현식을 생성한다.
public static DateExpression<LocalDate> readDateExpression() {
return new CaseBuilder()
.when(userNovel.status.eq(ReadStatus.WATCHING))
.then(userNovel.startDate)
.otherwise(userNovel.endDate);
}

// 독서 날짜가 없는 항목에만 등록일 정렬 값을 부여한다.
private static DateTimeExpression<LocalDateTime> undatedCreatedDateExpression() {
return new CaseBuilder()
.when(readDateExpression().isNull())
.then(userNovel.createdDate)
.otherwise(Expressions.nullExpression(LocalDateTime.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public static UserNovel create(ReadStatus status, Float userNovelRating, LocalDa
return new UserNovel(status, userNovelRating, startDate, endDate, user, novel);
}

// 상태별 날짜순 정렬에 사용할 독서 날짜를 반환한다.
public LocalDate getReadDate() {
return status == ReadStatus.WATCHING ? startDate : endDate;
}

/**
* 관심 상태 지정
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.websoso.WSSServer.domain.common.ReadStatus.QUIT;
import static org.websoso.WSSServer.domain.common.ReadStatus.WATCHED;
import static org.websoso.WSSServer.domain.common.ReadStatus.WATCHING;
import static org.websoso.WSSServer.domain.common.UserNovelSortType.readDateExpression;
import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel;
import static org.websoso.WSSServer.novel.domain.QNovel.novel;
import static org.websoso.WSSServer.novel.domain.QNovelGenre.novelGenre;
Expand All @@ -11,6 +12,7 @@
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.DateExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.LockModeType;
Expand Down Expand Up @@ -328,16 +330,19 @@ private BooleanExpression titleDescCursorCondition(UserNovelCursor cursor) {
.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()));
DateExpression<LocalDate> representativeDate = readDateExpression();

if (cursor.lastRepresentativeDate() == null) {
return representativeDate.isNull()
.and(createdDescCursorCondition(cursor));
}

return userNovel.startDate.lt(cursor.lastStartDate())
.or(userNovel.startDate.eq(cursor.lastStartDate())
return representativeDate.lt(cursor.lastRepresentativeDate())
.or(representativeDate.eq(cursor.lastRepresentativeDate())
.and(userNovel.userNovelId.lt(cursor.lastUserNovelId())))
.or(userNovel.startDate.isNull());
.or(representativeDate.isNull());
}

private BooleanExpression ratingDescCursorCondition(UserNovelCursor cursor) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.websoso.WSSServer.library.repository.cursor;

import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.LocalDateTime;

/**
* 사용자 서재 정렬별 다음 페이지 조회 기준값을 저장한다.
*/
public record UserNovelCursor(
Float lastRating,
LocalDateTime lastCreatedDate,
Long lastUserNovelId,
Boolean rated,
LocalDate lastStartDate,
LocalDate lastRepresentativeDate,
String lastTitle
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,14 @@ private List<UserNovelAndNovelGetResponse> 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.getStartDate(),
userNovel.getReadDate(),
userNovel.getNovel().getTitle()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.websoso.WSSServer.domain.common;

import static org.assertj.core.api.Assertions.assertThat;
import static org.websoso.WSSServer.domain.common.UserNovelSortType.READ_DATE;
import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel;

import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.OrderSpecifier.NullHandling;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
* 사용자 서재 정렬 유형의 QueryDSL 정렬 조건을 검증한다.
*/
class UserNovelSortTypeTest {

// 날짜순 정렬이 상태별 독서 날짜와 날짜 없는 항목의 등록일을 사용하는지 검증한다.
@Test
@DisplayName("날짜순 정렬은 상태별 독서 날짜를 기준으로 최신순 정렬한다")
void orderByReadDate() {
List<OrderSpecifier<?>> orderSpecifiers = READ_DATE.orderSpecifiers();

assertThat(orderSpecifiers).hasSize(3);
assertThat(orderSpecifiers.get(0).getOrder()).isEqualTo(Order.DESC);
assertThat(orderSpecifiers.get(0).getNullHandling()).isEqualTo(NullHandling.NullsLast);
assertThat(orderSpecifiers.get(0).getTarget().toString())
.contains("status", "WATCHING", "startDate", "endDate");
assertThat(orderSpecifiers.get(1).getOrder()).isEqualTo(Order.DESC);
assertThat(orderSpecifiers.get(1).getNullHandling()).isEqualTo(NullHandling.NullsLast);
assertThat(orderSpecifiers.get(1).getTarget().toString())
.contains("startDate", "endDate", "createdDate");
assertThat(orderSpecifiers.get(2).getTarget()).isEqualTo(userNovel.userNovelId);
assertThat(orderSpecifiers.get(2).getOrder()).isEqualTo(Order.DESC);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.websoso.WSSServer.library.domain;

import static org.assertj.core.api.Assertions.assertThat;
import static org.websoso.WSSServer.domain.common.ReadStatus.QUIT;
import static org.websoso.WSSServer.domain.common.ReadStatus.WATCHED;
import static org.websoso.WSSServer.domain.common.ReadStatus.WATCHING;

import java.time.LocalDate;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.websoso.WSSServer.domain.common.ReadStatus;

/**
* 사용자 서재 도메인의 상태별 독서 날짜 정책을 검증한다.
*/
class UserNovelTest {

private static final LocalDate START_DATE = LocalDate.of(2026, 1, 1);
private static final LocalDate END_DATE = LocalDate.of(2026, 2, 1);

// 읽는 중은 시작일, 완독과 하차는 마지막 날짜를 반환하는지 검증한다.
@ParameterizedTest
@MethodSource("readDateCases")
@DisplayName("독서 상태에 맞는 날짜순 정렬 날짜를 반환한다")
void getReadDateByStatus(ReadStatus status, LocalDate expected) {
UserNovel userNovel = UserNovel.create(status, 0.0f, START_DATE, END_DATE, null, null);

assertThat(userNovel.getReadDate()).isEqualTo(expected);
}

// 상태별 기대 독서 날짜 테스트 데이터를 제공한다.
private static Stream<Arguments> readDateCases() {
return Stream.of(
Arguments.of(WATCHING, START_DATE),
Arguments.of(WATCHED, END_DATE),
Arguments.of(QUIT, END_DATE)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.websoso.WSSServer.library.repository;

import static org.assertj.core.api.Assertions.assertThat;

import com.querydsl.core.types.dsl.BooleanExpression;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.websoso.WSSServer.library.repository.cursor.UserNovelCursor;

/**
* 사용자 서재의 날짜순 커서 조건을 검증한다.
*/
class UserNovelCustomRepositoryImplTest {

private static final LocalDateTime CREATED_DATE = LocalDateTime.of(2026, 1, 1, 0, 0);
private final UserNovelCustomRepositoryImpl repository = new UserNovelCustomRepositoryImpl(null);

// 날짜가 없는 커서 이후 항목을 등록일과 식별자 내림차순으로 조회하는지 검증한다.
@Test
@DisplayName("날짜가 없는 항목은 등록 최신순 커서를 적용한다")
void createReadDateCursorConditionWithoutReadDate() {
UserNovelCursor cursor = createCursor(null);

BooleanExpression condition = ReflectionTestUtils.invokeMethod(
repository, "readDateCursorCondition", cursor);

assertThat(condition).isNotNull();
assertThat(condition.toString())
.contains("startDate", "endDate", "createdDate", "userNovelId");
}

// 날짜가 있는 커서 이후 항목을 독서 날짜와 식별자 내림차순으로 조회하는지 검증한다.
@Test
@DisplayName("날짜가 있는 항목은 상태별 독서 날짜 커서를 적용한다")
void createReadDateCursorConditionWithReadDate() {
UserNovelCursor cursor = createCursor(LocalDate.of(2026, 1, 1));

BooleanExpression condition = ReflectionTestUtils.invokeMethod(
repository, "readDateCursorCondition", cursor);

assertThat(condition).isNotNull();
assertThat(condition.toString())
.contains("status", "WATCHING", "startDate", "endDate", "userNovelId")
.doesNotContain("createdDate");
}

// 날짜순 커서 조건 테스트에 필요한 커서를 생성한다.
private UserNovelCursor createCursor(LocalDate readDate) {
return new UserNovelCursor(0.0f, CREATED_DATE, 1L, false, readDate, "제목");
}
}
Loading