From 4e8ab1bada2203691834379050d2a1af2516ca41 Mon Sep 17 00:00:00 2001 From: dungbik Date: Thu, 23 Oct 2025 13:54:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20=ED=8A=B9=EC=A0=95=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=EC=9D=98=20=EC=B9=B4=EB=93=9C=EC=85=8B=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupCardSetController.java | 13 +++ .../repository/CardSetRepositoryCustom.java | 8 +- .../CardSetRepositoryCustomImpl.java | 89 +++++++++++++++++-- .../cardset/service/CardSetService.java | 19 ++++ .../exception/GlobalExceptionHandler.java | 3 +- 5 files changed, 124 insertions(+), 8 deletions(-) diff --git a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java index 7e4c91f4..2af768d7 100644 --- a/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java +++ b/src/main/java/project/flipnote/cardset/controller/GroupCardSetController.java @@ -4,6 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -15,10 +16,13 @@ import lombok.RequiredArgsConstructor; import project.flipnote.cardset.controller.docs.GroupCardSetControllerDocs; import project.flipnote.cardset.model.CardSetDetailResponse; +import project.flipnote.cardset.model.CardSetSearchRequest; +import project.flipnote.cardset.model.CardSetSummaryResponse; import project.flipnote.cardset.model.CardSetUpdateRequest; import project.flipnote.cardset.model.CreateCardSetRequest; import project.flipnote.cardset.model.CreateCardSetResponse; import project.flipnote.cardset.service.CardSetService; +import project.flipnote.common.model.response.PagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; @RequiredArgsConstructor @@ -62,4 +66,13 @@ public ResponseEntity updateCardSet( return ResponseEntity.ok(res); } + @GetMapping + public ResponseEntity> getCardSets( + @PathVariable("groupId") Long groupId, + @Valid @ModelAttribute CardSetSearchRequest req + ) { + PagingResponse res = cardSetService.getCardSets(groupId, req); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java index bc8dc81a..383b8e47 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java @@ -6,7 +6,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import project.flipnote.cardset.entity.CardSet; import project.flipnote.cardset.model.CardSetInfo; import project.flipnote.group.entity.Category; @@ -19,4 +18,11 @@ Page searchByNameContainingAndCategory( ); List findAllByIdWithImageRefId(Set cardSets); + + Page searchByGroupIdAndNameContainingAndCategory( + long groupId, + 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 index 6e64f473..bbd4f56b 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Set; -import org.checkerframework.checker.units.qual.C; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -22,7 +21,6 @@ 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.CardSetInfo; @@ -91,7 +89,7 @@ public Page searchByNameContainingAndCategory( imageRef.id )) .from(cardSet) - .where(buildCardSetSearchFilterConditions(name, category)) + .where(buildCardSetSearchFilterConditions(null, name, category)) .leftJoin(imageRef) .on(imageRef.referenceType.eq(ReferenceType.CARD_SET) .and(imageRef.referenceId.eq(cardSet.id))); @@ -109,7 +107,7 @@ public Page searchByNameContainingAndCategory( Long total = queryFactory .select(cardSet.count()) .from(cardSet) - .where(buildCardSetSearchFilterConditions(name, category)) + .where(buildCardSetSearchFilterConditions(null, name, category)) .fetchOne(); return new PageImpl<>(content, pageable, total != null ? total : 0L); @@ -135,6 +133,80 @@ public List findAllByIdWithImageRefId(Set cardSets) { .fetch(); } + @Override + public Page searchByGroupIdAndNameContainingAndCategory( + long groupId, + 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 if (sortField == CardSetSortField.BOOKMARK) { + orders.add(toOrderSpecifier(cardSetMetadata.bookmarkCount, order)); + useMetadata = true; + } else { + orders.add(toOrderSpecifier(cardSet.id, order)); + hasIdSort = true; + } + } + + if (!hasIdSort) { + orders.add(cardSet.id.desc()); + } + + JPAQuery selectQuery = queryFactory + .select( + Projections.constructor( + CardSetInfo.class, + cardSet, + cardSet.group, + cardSet.name, + cardSet.category, + cardSet.hashtag, + cardSet.imageUrl, + imageRef.id + )) + .from(cardSet) + .where(buildCardSetSearchFilterConditions(groupId, name, category)) + .leftJoin(imageRef) + .on(imageRef.referenceType.eq(ReferenceType.CARD_SET) + .and(imageRef.referenceId.eq(cardSet.id))); + + if (useMetadata) { + selectQuery.leftJoin(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(groupId, name, category)) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + private OrderSpecifier toOrderSpecifier( NumberPath path, Sort.Order order @@ -150,8 +222,13 @@ private BooleanExpression categoryEquals(Category category) { return category == null ? null : cardSet.category.eq(category); } - private BooleanExpression[] buildCardSetSearchFilterConditions(String name, Category category) { - return new BooleanExpression[]{ + private BooleanExpression groupIdEquals(Long groupId) { + return groupId == null ? null : cardSet.group.id.eq(groupId); + } + + private BooleanExpression[] buildCardSetSearchFilterConditions(Long groupId, String name, Category category) { + return new BooleanExpression[] { + groupIdEquals(groupId), 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 cff000c7..db14dc84 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ -326,4 +326,23 @@ public void decrementBookmarkCount(Long cardSetId) { public void decrementBookmarkCount(List cardSetIds) { cardSetMetadataRepository.decrementBookmarkCount(cardSetIds); } + + /** + * 특정 그룹의 카드셋 목록을 페이지 단위로 조회 + * + * @param groupId 조회할 그룹의 ID + * @param req 조회 조건 및 페이징 정보를 포함한 요청 DTO + * @return 페이지 단위로 조회된 카드셋 목록 + * @author 윤정환 + */ + public PagingResponse getCardSets(long groupId, CardSetSearchRequest req) { + // TODO: Projection 튜닝 필요 + Page cardSetPage = cardSetRepository.searchByGroupIdAndNameContainingAndCategory( + groupId, req.getKeyword(), Category.from(req.getCategory()), req.getPageRequest() + ); + + Page res = cardSetPage.map(CardSetSummaryResponse::from); + + return PagingResponse.from(res); + } } diff --git a/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java b/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java index 009b970e..5eb4b02d 100644 --- a/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/project/flipnote/common/exception/GlobalExceptionHandler.java @@ -49,7 +49,8 @@ public ResponseEntity>> handleValidatio @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity handleMissingServletRequestParameter( - MissingServletRequestParameterException exception) { + MissingServletRequestParameterException exception + ) { String missingParam = exception.getParameterName(); String message = String.format("필수 파라미터 '%s'가 없습니다.", missingParam); return ResponseEntity.badRequest().body(message); From 140dd653c3a5fc4f8343e15c80b3858e0a8fe362 Mon Sep 17 00:00:00 2001 From: dungbik Date: Thu, 23 Oct 2025 14:35:25 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Refactor:=20=EC=B9=B4=EB=93=9C=EC=85=8B=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/CardSetRepositoryCustom.java | 2 +- .../CardSetRepositoryCustomImpl.java | 68 +------------------ 2 files changed, 3 insertions(+), 67 deletions(-) diff --git a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java index 383b8e47..284b4c0b 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustom.java @@ -20,7 +20,7 @@ Page searchByNameContainingAndCategory( List findAllByIdWithImageRefId(Set cardSets); Page searchByGroupIdAndNameContainingAndCategory( - long groupId, + Long groupId, 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 index bbd4f56b..e5694b91 100644 --- a/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java +++ b/src/main/java/project/flipnote/cardset/repository/CardSetRepositoryCustomImpl.java @@ -46,71 +46,7 @@ public Page searchByNameContainingAndCategory( 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 if (sortField == CardSetSortField.BOOKMARK) { - orders.add(toOrderSpecifier(cardSetMetadata.bookmarkCount, order)); - useMetadata = true; - } else { - orders.add(toOrderSpecifier(cardSet.id, order)); - hasIdSort = true; - } - } - - if (!hasIdSort) { - orders.add(cardSet.id.desc()); - } - - JPAQuery selectQuery = queryFactory - .select( - Projections.constructor( - CardSetInfo.class, - cardSet, - cardSet.group, - cardSet.name, - cardSet.category, - cardSet.hashtag, - cardSet.imageUrl, - imageRef.id - )) - .from(cardSet) - .where(buildCardSetSearchFilterConditions(null, name, category)) - .leftJoin(imageRef) - .on(imageRef.referenceType.eq(ReferenceType.CARD_SET) - .and(imageRef.referenceId.eq(cardSet.id))); - - if (useMetadata) { - selectQuery.leftJoin(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(null, name, category)) - .fetchOne(); - - return new PageImpl<>(content, pageable, total != null ? total : 0L); + return searchByGroupIdAndNameContainingAndCategory(null, name, category, pageable); } public List findAllByIdWithImageRefId(Set cardSets) { @@ -135,7 +71,7 @@ public List findAllByIdWithImageRefId(Set cardSets) { @Override public Page searchByGroupIdAndNameContainingAndCategory( - long groupId, + Long groupId, String name, Category category, Pageable pageable From 1c2654abdb6bdac306b96bfd2d55120a5053db7d Mon Sep 17 00:00:00 2001 From: dungbik Date: Thu, 23 Oct 2025 14:36:06 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Feat:=20=ED=8A=B9=EC=A0=95=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=EC=B9=B4=EB=93=9C=EC=85=8B=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=8B=9C=20=EA=B7=B8=EB=A3=B9=20=EC=A1=B4=EC=9E=AC=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cardset/model/CardSetSummaryResponse.java | 2 -- .../flipnote/cardset/service/CardSetService.java | 6 ++++-- .../flipnote/group/service/GroupService.java | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java b/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java index a3067c20..e5088567 100644 --- a/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java +++ b/src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java @@ -1,7 +1,5 @@ package project.flipnote.cardset.model; -import project.flipnote.cardset.entity.CardSet; - public record CardSetSummaryResponse( Long cardSetId, Long groupId, diff --git a/src/main/java/project/flipnote/cardset/service/CardSetService.java b/src/main/java/project/flipnote/cardset/service/CardSetService.java index db14dc84..f01909e3 100644 --- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ -34,11 +34,10 @@ import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupRepository; -import project.flipnote.image.entity.Image; +import project.flipnote.group.service.GroupService; import project.flipnote.image.entity.ImageMeta; import project.flipnote.image.entity.ImageRef; import project.flipnote.image.entity.ReferenceType; -import project.flipnote.image.exception.ImageErrorCode; import project.flipnote.image.service.ImageRefService; import project.flipnote.image.service.ImageService; import project.flipnote.user.entity.UserProfile; @@ -61,6 +60,7 @@ public class CardSetService { private final CardSetMetadataRepository cardSetMetadataRepository; private final ImageService imageService; private final ImageRefService imageRefService; + private final GroupService groupService; @Value("${image.default.cardSet}") private String defaultCardSetImage; @@ -336,6 +336,8 @@ public void decrementBookmarkCount(List cardSetIds) { * @author 윤정환 */ public PagingResponse getCardSets(long groupId, CardSetSearchRequest req) { + groupService.validateGroupExists(groupId); + // TODO: Projection 튜닝 필요 Page cardSetPage = cardSetRepository.searchByGroupIdAndNameContainingAndCategory( groupId, req.getKeyword(), Category.from(req.getCategory()), req.getPageRequest() diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index 6479921a..97e0fe84 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -38,11 +38,9 @@ import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; import project.flipnote.groupjoin.exception.GroupJoinErrorCode; -import project.flipnote.image.entity.Image; import project.flipnote.image.entity.ImageMeta; import project.flipnote.image.entity.ImageRef; import project.flipnote.image.entity.ReferenceType; -import project.flipnote.image.exception.ImageErrorCode; import project.flipnote.image.service.ImageRefService; import project.flipnote.image.service.ImageService; import project.flipnote.user.entity.UserProfile; @@ -448,6 +446,19 @@ public CursorPagingResponse findMyGroup(AuthPrinciple authPrinciple, return createGroupInfoCursorPagingResponse(req, groups); } + /** + * 지정된 그룹 ID가 존재하는지 검사합니다. + * + * @param groupId 존재 여부를 확인할 그룹의 ID + * @throws BizException 그룹이 존재하지 않을 경우 발생 + * @author 윤정환 + */ + public void validateGroupExists(Long groupId) { + if (!groupRepository.existsById(groupId)) { + throw new BizException(GroupErrorCode.GROUP_NOT_FOUND); + } + } + //리스트 조회시 response 생성 private CursorPagingResponse createGroupInfoCursorPagingResponse(GroupListRequest req, List groups) {