diff --git a/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java b/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java index 5833b52a..84ac8c63 100644 --- a/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java +++ b/src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java @@ -13,23 +13,23 @@ @Setter public class CardSetSearchRequest extends PagingRequest { - private static final Set ALLOWED_SORT_FIELDS = Set.of("id"); + private static final Set ALLOWED_SORT_FIELDS = CardSetSortField.getFieldNames(); private String keyword; private String category; @Override public PageRequest getPageRequest() { - String sortBy = this.getSortBy(); - String effectiveSortBy = (sortBy != null && ALLOWED_SORT_FIELDS.contains(sortBy)) ? sortBy : "id"; - - Sort.Direction direction; - try { - direction = Sort.Direction.fromString(this.getOrder()); - } catch (IllegalArgumentException e) { - direction = Sort.Direction.DESC; + return PageRequest.of(getPage() - 1, getSize(), Sort.by(getOrder(), getSortBy())); + } + + @Override + public String getSortBy() { + String sortBy = super.getSortBy(); + if (sortBy != null && ALLOWED_SORT_FIELDS.contains(sortBy)) { + return sortBy; } - return PageRequest.of(getPage() - 1, getSize(), Sort.by(direction, effectiveSortBy)); + return "ID"; } } diff --git a/src/main/java/project/flipnote/cardset/model/CardSetSortField.java b/src/main/java/project/flipnote/cardset/model/CardSetSortField.java new file mode 100644 index 00000000..0282178c --- /dev/null +++ b/src/main/java/project/flipnote/cardset/model/CardSetSortField.java @@ -0,0 +1,15 @@ +package project.flipnote.cardset.model; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +public enum CardSetSortField { + ID, LIKE; + + public static Set getFieldNames() { + return Arrays.stream(values()) + .map(CardSetSortField::name) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java index 91f10e06..59affad9 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepository.java @@ -3,31 +3,15 @@ import java.util.Optional; import java.util.Set; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import project.flipnote.cardset.entity.CardSet; -import project.flipnote.group.entity.Category; @Repository -public interface CardSetRepository extends JpaRepository { - - @Query(""" - SELECT c FROM CardSet c - WHERE (:name IS NULL OR c.name LIKE CONCAT('%', :name, '%')) - AND (:category IS NULL OR c.category = :category) - AND c.publicVisible = TRUE - """) - Page findByNameContainingAndCategory( - @Param("name") String name, - @Param("category") Category category, - Pageable pageable - ); - +public interface CardSetRepository extends JpaRepository, CardSetRepositoryCustom { Optional findByIdAndGroup_Id(Long id, Long groupId); @Query(""" diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java new file mode 100644 index 00000000..df2c9688 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java @@ -0,0 +1,16 @@ +package project.flipnote.cardset.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import project.flipnote.cardset.entity.CardSet; +import project.flipnote.group.entity.Category; + +public interface CardSetRepositoryCustom { + + Page searchByNameContainingAndCategory( + String name, + Category category, + Pageable pageable + ); +} diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java new file mode 100644 index 00000000..5f950ea2 --- /dev/null +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java @@ -0,0 +1,117 @@ +package project.flipnote.cardset.repository; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.cardset.entity.CardSet; +import project.flipnote.cardset.entity.QCardSet; +import project.flipnote.cardset.entity.QCardSetMetadata; +import project.flipnote.cardset.model.CardSetSortField; +import project.flipnote.group.entity.Category; + +@Slf4j +@RequiredArgsConstructor +@Repository +public class CardSetRepositoryCustomImpl implements CardSetRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + private final QCardSet cardSet = QCardSet.cardSet; + private final QCardSetMetadata cardSetMetadata = QCardSetMetadata.cardSetMetadata; + + @Override + public Page searchByNameContainingAndCategory( + String name, + Category category, + Pageable pageable + ) { + List> orders = new ArrayList<>(); + + boolean useMetadata = false; + boolean hasIdSort = false; + for (Sort.Order order : pageable.getSort()) { + CardSetSortField sortField = null; + try { + sortField = CardSetSortField.valueOf(order.getProperty()); + } catch (IllegalArgumentException iae) { + log.warn( + "Unknown sort property: {}. Valid values are {}", + order.getProperty(), Arrays.toString(CardSetSortField.values()), iae + ); + } + if (sortField == CardSetSortField.LIKE) { + orders.add(toOrderSpecifier(cardSetMetadata.likeCount, order)); + useMetadata = true; + } else { + orders.add(toOrderSpecifier(cardSet.id, order)); + hasIdSort = true; + } + } + + if (!hasIdSort) { + orders.add(cardSet.id.desc()); + } + + JPAQuery selectQuery = queryFactory + .select(cardSet) + .from(cardSet) + .where(buildCardSetSearchFilterConditions(name, category)); + + if (useMetadata) { + selectQuery.join(cardSetMetadata).on(cardSet.id.eq(cardSetMetadata.id)); + } + + List content = selectQuery + .orderBy(orders.toArray(new OrderSpecifier[0])) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(cardSet.count()) + .from(cardSet) + .where(buildCardSetSearchFilterConditions(name, category)) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + + private OrderSpecifier toOrderSpecifier( + NumberPath path, + Sort.Order order + ) { + return order.isAscending() ? path.asc() : path.desc(); + } + + private BooleanExpression nameContains(String name) { + return StringUtils.hasText(name) ? cardSet.name.contains(name) : null; + } + + private BooleanExpression categoryEquals(Category category) { + return category == null ? null : cardSet.category.eq(category); + } + + private BooleanExpression[] buildCardSetSearchFilterConditions(String name, Category category) { + return new BooleanExpression[]{ + nameContains(name), + categoryEquals(category), + cardSet.publicVisible.isTrue() + }; + } +} diff --git a/src/main/java/project/flipnote/cardset/service/CardSetService.java b/src/main/java/project/flipnote/cardset/service/CardSetService.java index cb9dc43c..5cfee2a6 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ -120,10 +120,9 @@ public CreateCardSetResponse createCardSet(Long groupId, AuthPrinciple authPrinc * @author 윤정환 */ public PagingResponse getCardSets(CardSetSearchRequest req) { - - // TODO: Projection 및 카운트 쿼리 튜닝 필요, 좋아요 수 및 즐겨찾기 수 등 다양한 정렬 조건 추가 필요 - Page cardSetPage = cardSetRepository.findByNameContainingAndCategory( - req.getKeyword(), Category.from(req.getCategory()), req.getPageRequest() + // TODO: Projection 튜닝 필요 + Page cardSetPage = cardSetRepository.searchByNameContainingAndCategory( + req.getKeyword(), Category.from(req.getCategory()), req.getPageRequest() ); Page res = cardSetPage.map(CardSetSummaryResponse::from); diff --git a/src/main/java/project/flipnote/common/model/request/PagingRequest.java b/src/main/java/project/flipnote/common/model/request/PagingRequest.java index bc53bbee..68a11046 100644 --- a/src/main/java/project/flipnote/common/model/request/PagingRequest.java +++ b/src/main/java/project/flipnote/common/model/request/PagingRequest.java @@ -29,14 +29,22 @@ public PageRequest getPageRequest() { if (sortBy == null || sortBy.isEmpty()) { return PageRequest.of(page - 1, size); } else { - Sort.Direction direction; - try { - direction = Sort.Direction.fromString(order); - } catch (IllegalArgumentException e) { - direction = Sort.Direction.DESC; - } - - return PageRequest.of(page - 1, size, Sort.by(direction, sortBy)); + return PageRequest.of(page - 1, size, Sort.by(getOrder(), sortBy)); } } + + public Sort.Direction getOrder() { + Sort.Direction direction; + try { + direction = Sort.Direction.fromString(order); + } catch (IllegalArgumentException e) { + direction = Sort.Direction.DESC; + } + + return direction; + } + + public String getSortBy() { + return sortBy != null ? sortBy.toUpperCase() : null; + } }