From fea9a32377b083d4ee209a17e7988d1040ab39d2 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 17:03:10 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-48=20:?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EA=B0=9C=EC=88=98=EB=8A=94=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/response/page/SliceResponse.java | 17 ++++++++++++++ .../adapter/in/web/ProductController.java | 23 ++++--------------- .../in/web/swagger/ProductControllerSpec.java | 8 ------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java b/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java index 2342ffc..f806d32 100644 --- a/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java +++ b/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java @@ -1,12 +1,18 @@ package site.kikihi.custom.global.response.page; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; import org.springframework.data.domain.Slice; import java.util.List; +/// null 값은 직렬화에서 제외 + @Builder +@JsonInclude(JsonInclude.Include.NON_NULL) public record SliceResponse( + + long totalCount, List content, boolean hasNext, int page, @@ -20,4 +26,15 @@ public static SliceResponse from(Slice slice) { .size(slice.getSize()) .build(); } + + public static SliceResponse from(Slice slice, long totalCount) { + return SliceResponse.builder() + .totalCount(totalCount) + .content(slice.getContent()) + .hasNext(slice.hasNext()) + .page(slice.getNumber() + 1) + .size(slice.getSize()) + .build(); + } + } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/ProductController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/ProductController.java index 42ae3d4..bc74918 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/ProductController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/ProductController.java @@ -57,6 +57,7 @@ public ApiResponse> getProductList(PageReques Sort.by(Sort.Direction.DESC, "id") ); + /// 공통 객체들 저장 Slice products; @@ -84,7 +85,10 @@ public ApiResponse> getProductList(PageReques throw new IllegalArgumentException(ErrorCode.BAD_REQUEST.getMessage()); } - return ApiResponse.ok(SliceResponse.from(products)); + /// 카테고리에 따른 전체 개수 조회 + Long totalCounts = productService.getCountProducts(category.getValue()); + + return ApiResponse.ok(SliceResponse.from(products, totalCounts)); } /** @@ -129,22 +133,5 @@ public ApiResponse> getManufacturers( return ApiResponse.ok(SliceResponse.from(responses)); } - - /** - * 전체 상품 개수 조회 API - * @param category 카테 고리 - */ - @GetMapping("/total") - public ApiResponse getTotalProducts( - @RequestParam CategoryType category - ) { - - /// 서비스 호출 - Long response = productService.getCountProducts(category.getValue()); - - /// 리턴 - return ApiResponse.ok(response); - } - } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java index 093233e..7106234 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java @@ -19,14 +19,6 @@ public interface ProductControllerSpec { - @Operation( - summary = "상품 개수 조회 API", - description = "카테고리별 상품 전체 개수를 파악합니다." - ) - ApiResponse getTotalProducts( - @RequestParam CategoryType category - ); - /** * 상품 상세 조회 API * @param id 상품 아이디 From e77d83547b859ee362d168be3b992c6946ccc44d Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 17:31:58 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-48=20:?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EA=B0=9C=EC=88=98=EA=B0=80=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EC=97=86=EC=9D=84=20=EB=95=8C=EB=8A=94=20=EB=AC=B4?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../site/kikihi/custom/global/response/page/SliceResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java b/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java index f806d32..892d307 100644 --- a/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java +++ b/src/main/java/site/kikihi/custom/global/response/page/SliceResponse.java @@ -12,7 +12,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public record SliceResponse( - long totalCount, + Long totalCount, List content, boolean hasNext, int page, @@ -20,6 +20,7 @@ public record SliceResponse( ) { public static SliceResponse from(Slice slice) { return SliceResponse.builder() + .totalCount(null) .content(slice.getContent()) .hasNext(slice.hasNext()) .page(slice.getNumber() + 1) From 9eca50a44c10803926864d5c293b91094daf29b9 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 18:01:07 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-48=20:?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EC=9E=90=EB=8F=99=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20Slice=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/SearchController.java | 55 +------- .../in/web/swagger/SearchControllerSpec.java | 31 +---- .../platform/adapter/out/BookmarkAdapter.java | 7 + .../jpa/bookmark/BookmarkJpaRepository.java | 2 + .../adapter/out/jpa/user/UserJpaEntity.java | 8 -- .../application/in/search/SearchUseCase.java | 15 +- .../out/bookmark/BookmarkPort.java | 2 + .../application/service/SearchService.java | 128 +++++++++++------- .../custom/platform/domain/user/User.java | 11 -- 9 files changed, 97 insertions(+), 162 deletions(-) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java index f8785ab..0ed36ae 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java @@ -1,13 +1,14 @@ package site.kikihi.custom.platform.adapter.in.web; +import org.springframework.data.domain.Slice; import org.springframework.security.core.annotation.AuthenticationPrincipal; import site.kikihi.custom.global.response.ApiResponse; import site.kikihi.custom.global.response.page.PageRequest; +import site.kikihi.custom.global.response.page.SliceResponse; import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.adapter.in.web.dto.response.search.SearchListResponse; import site.kikihi.custom.platform.adapter.in.web.swagger.SearchControllerSpec; import site.kikihi.custom.platform.application.in.search.SearchUseCase; -import site.kikihi.custom.platform.domain.product.Product; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import site.kikihi.custom.platform.domain.search.Search; @@ -25,7 +26,7 @@ public class SearchController implements SearchControllerSpec { /// 상품 검색 @GetMapping - public ApiResponse> searchProducts( + public ApiResponse> searchProducts( @RequestParam("keyword") String keyword, PageRequest pageRequest, @AuthenticationPrincipal PrincipalDetails principalDetails @@ -35,13 +36,10 @@ public ApiResponse> searchProducts( UUID userId = principalDetails != null ? principalDetails.getId() : null; /// 서비스 호출 - List productList = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId); - - /// DTO 수정 - List responses = ProductListResponse.from(productList); + Slice products = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId); /// 응답 - return ApiResponse.ok(responses); + return ApiResponse.ok(SliceResponse.from(products)); } /// 나의 최근 검색어 조회 @@ -84,47 +82,4 @@ public ApiResponse deleteAllSearch( return ApiResponse.deleted(); } - /// 자동 저장 기능 조회 - @GetMapping("/auto") - public ApiResponse getMyAutoSearch( - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - - /// 서비스 호출 - boolean checked = service.checkSearch(principalDetails.getId()); - - String autoSearch = checked ? "자동 저장이 활성화되었습니다." : "자동 저장이 꺼져있습니다."; - - /// 리턴 - return ApiResponse.ok(autoSearch); - - } - - /// 자동 저장 기능 켜기 - @PutMapping("/auto/on") - public ApiResponse turnOnSearch( - @AuthenticationPrincipal PrincipalDetails principalDetails - ){ - - /// 서비스 호출 - service.turnOnMySearchKeyword(principalDetails.getId()); - - /// 리턴 - return ApiResponse.updated(); - - } - - /// 자동 저장 기능 끄기 - @PutMapping("/auto/off") - public ApiResponse turnOffSearch( - @AuthenticationPrincipal PrincipalDetails principalDetails - ){ - - /// 서비스 호출 - service.turnOffMySearchKeyword(principalDetails.getId()); - - /// 리턴 - return ApiResponse.updated(); - } - } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java index 6794a94..c47fb25 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestParam; import site.kikihi.custom.global.response.ApiResponse; import site.kikihi.custom.global.response.page.PageRequest; +import site.kikihi.custom.global.response.page.SliceResponse; import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.adapter.in.web.dto.response.search.SearchListResponse; import site.kikihi.custom.security.oauth2.domain.PrincipalDetails; @@ -21,7 +22,7 @@ public interface SearchControllerSpec { summary = "검색 API", description = "키워드를 바탕으로 조회합니다." ) - ApiResponse> searchProducts( + ApiResponse> searchProducts( @Parameter(example = "하우징") @RequestParam("keyword") String keyword, PageRequest pageRequest, @@ -57,32 +58,4 @@ ApiResponse deleteAllSearch( @AuthenticationPrincipal PrincipalDetails principalDetails ); - - @Operation( - summary = "검색어 자동저장 여부 API", - description = "JWT를 바탕으로 자동저장을 확인합니다." - ) - ApiResponse getMyAutoSearch( - @AuthenticationPrincipal PrincipalDetails principalDetails - ); - - - @Operation( - summary = "검색어 자동저장 기능 ON API", - description = "JWT를 바탕으로 자동저장을 킵니다." - ) - ApiResponse turnOnSearch( - @AuthenticationPrincipal PrincipalDetails principalDetails - ); - - - - - @Operation( - summary = "검색어 자동저장 기능 OFF API", - description = "JWT를 바탕으로 자동저장을 끕니다." - ) - ApiResponse turnOffSearch( - @AuthenticationPrincipal PrincipalDetails principalDetails - ); } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/BookmarkAdapter.java b/src/main/java/site/kikihi/custom/platform/adapter/out/BookmarkAdapter.java index 1137d31..f06be2b 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/BookmarkAdapter.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/BookmarkAdapter.java @@ -67,6 +67,13 @@ public List getBookmarksByUserIdAndCategoryId(UUID userId, String cate .toList(); } + @Override + public List getBookmarksByUserId(UUID userId) { + return repository.findByUserId(userId).stream() + .map(BookmarkJpaEntity::toDomain) + .toList(); + } + /** * 북마크ID 와 유저가 존재하는지 체크 * diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/bookmark/BookmarkJpaRepository.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/bookmark/BookmarkJpaRepository.java index 28b6c02..e745242 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/bookmark/BookmarkJpaRepository.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/bookmark/BookmarkJpaRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import site.kikihi.custom.platform.domain.bookmark.Bookmark; import java.util.List; import java.util.UUID; @@ -35,4 +36,5 @@ public interface BookmarkJpaRepository extends JpaRepository findByUserId(UUID userId); } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java index 8a0a235..535744b 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java @@ -41,8 +41,6 @@ public class UserJpaEntity extends BaseTimeEntity { @Embedded private AddressJpaEntity address; - private boolean isSearch; - @PrePersist public void generateUUID() { if (this.id == null) { @@ -61,7 +59,6 @@ public static UserJpaEntity from(User user) { .role(user.getRole()) .profileImage(user.getProfileImage()) .address(AddressJpaEntity.from(user.getAddress())) - .isSearch(user.isSearch()) .build(); } @@ -76,12 +73,7 @@ public User toDomain() { .role(role) .profileImage(profileImage) .address(address.toDomain()) - .isSearch(isSearch) .build(); } - /// 엔티티 수정용 - public void updateSearch(boolean isSearch) { - this.isSearch = isSearch; - } } diff --git a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java index ab6d812..f82467d 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java @@ -1,5 +1,7 @@ package site.kikihi.custom.platform.application.in.search; +import org.springframework.data.domain.Slice; +import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.domain.product.Product; import site.kikihi.custom.platform.domain.search.Search; @@ -16,7 +18,7 @@ public interface SearchUseCase { /// 검색 - List searchProducts(String keyword, int page, int size, UUID userId); + Slice searchProducts(String keyword, int page, int size, UUID userId); /// 나의 검색에 목록 확인하기 List getMySearches(UUID userId); @@ -26,15 +28,4 @@ public interface SearchUseCase { /// 키워드 모두 삭제하기 void deleteAllKeywords(UUID userId); - - /// 나의 최근 검색어 여부 - boolean checkSearch(UUID userId); - - /// 나의 최근 검색어 저장 끄기 - void turnOffMySearchKeyword(UUID userId); - - /// 나의 최근 검색어 저장 켜기 - void turnOnMySearchKeyword(UUID userId); - - } diff --git a/src/main/java/site/kikihi/custom/platform/application/out/bookmark/BookmarkPort.java b/src/main/java/site/kikihi/custom/platform/application/out/bookmark/BookmarkPort.java index bdbd548..09496f8 100644 --- a/src/main/java/site/kikihi/custom/platform/application/out/bookmark/BookmarkPort.java +++ b/src/main/java/site/kikihi/custom/platform/application/out/bookmark/BookmarkPort.java @@ -29,6 +29,8 @@ public interface BookmarkPort { /// 유저와 카테고리 ID를 바탕으로 북마크 목록 조회 List getBookmarksByUserIdAndCategoryId(UUID userId, String category); + List getBookmarksByUserId(UUID userId); + /// 유저와 북마크 ID를 바탕으로 북마크 여부 조회 boolean checkBookmarkByUserIdAndId(UUID userId, Long bookmarkId); diff --git a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index 24a3dfc..2f292c9 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -1,11 +1,17 @@ package site.kikihi.custom.platform.application.service; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.elasticsearch.core.SearchHits; import site.kikihi.custom.global.response.ErrorCode; +import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESDocument; -import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESRepository; import site.kikihi.custom.platform.application.in.search.SearchUseCase; +import site.kikihi.custom.platform.application.out.bookmark.BookmarkPort; import site.kikihi.custom.platform.application.out.search.SearchPort; import site.kikihi.custom.platform.application.out.user.UserPort; +import site.kikihi.custom.platform.domain.bookmark.Bookmark; import site.kikihi.custom.platform.domain.product.Product; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -21,6 +27,7 @@ import java.util.List; import java.util.NoSuchElementException; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -30,18 +37,22 @@ public class SearchService implements SearchUseCase { /// 의존성 private final ElasticsearchOperations elasticsearchOperations; - private final ProductESRepository productESRepository; private final SearchPort port; /// 외부 의존성 private final UserPort userPort; + private final BookmarkPort bookmarkPort; /// 스태틱 private final Float minScore = 0.001f; /// 키워드 검색 (name, description) @Override - public List searchProducts(String keyword, int page, int size, UUID userId) { + public Slice searchProducts(String keyword, int page, int size, UUID userId) { + + /// Pageable 구성 + Pageable pageRequest = PageRequest.of(page, size); + // match 쿼리 구성 Query nameMatch = MatchQuery.of(m -> m.field("name").query(keyword))._toQuery(); Query descMatch = MatchQuery.of(m -> m.field("description").query(keyword))._toQuery(); @@ -56,8 +67,8 @@ public List searchProducts(String keyword, int page, int size, UUID use // NativeQuery NativeQuery query = NativeQuery.builder() .withQuery(boolQuery) - .withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size) - .withMinScore(minScore) // <<-- 추가됨 + .withPageable(pageRequest) + .withMinScore(minScore) .build(); /// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기 @@ -66,20 +77,37 @@ public List searchProducts(String keyword, int page, int size, UUID use /// 유저 조회 User user = getUser(userId); - /// 자동저장이 ON인 유저만 저장한다. - if (user.isSearch()) { + /// DB에 최신 검색어 저장하기 + Search search = Search.of(user.getId(), keyword); + port.saveSearch(search); - /// DB에 최신 검색어 저장하기 - Search search = Search.of(user.getId(), keyword); - port.saveSearch(search); - } } - return elasticsearchOperations.search(query, ProductESDocument.class) - .stream() + SearchHits searchHits = elasticsearchOperations.search(query, ProductESDocument.class); + + /// 결과물 출력 + List elasticProducts = searchHits.stream() .map(SearchHit::getContent) .map(ProductESDocument::toDomain) - .collect(Collectors.toList()); + .toList(); + + /// 페이징 처리 + // 현재 페이지 결과 수 + boolean hasNext = checkNext(page, size, searchHits); + + Slice products = new SliceImpl<>(elasticProducts, pageRequest, hasNext); + + return toProductListResponse(userId, products); + } + + private boolean checkNext(int page, int size, SearchHits searchHits) { + + long totalHits = searchHits.getTotalHits(); + + // 현재 페이지와 size를 이용해 현재 페이지가 마지막 페이지인지 판단 + boolean hasNext = (page + 1) * size < totalHits; + + return hasNext; } @Override @@ -129,60 +157,56 @@ public void deleteAllKeywords(UUID userId) { port.deleteALlSearch(user.getId()); } - @Override - public boolean checkSearch(UUID userId) { + /// 유저 조회 + private User getUser(UUID userId) { - /// 유저 예외 처리 - User user = getUser(userId); + return userPort.loadUserById(userId) + .orElseThrow(() -> new NoSuchElementException(ErrorCode.USER_NOT_FOUND.getMessage())); + } + + + /// 검색 기록 조회 + private Search getSearch(Long searchId) { - /// 유저의 여부 체크 - return user.isSearch(); + return port.getSearch(searchId) + .orElseThrow(() -> new NoSuchElementException(ErrorCode.SEARCH_NOT_FOUND.getMessage())); } /** - * 검색 기록을 저장하지않도록 끕니다. - * @param userId 유저 ID + * 상품 목록 조회를 진행할때, 북마크 여부를 파악하는 함수입니다. + * @param userId 유저 ID + * @param products 상품 목록 */ - @Override - public void turnOffMySearchKeyword(UUID userId) { + private Slice toProductListResponse(UUID userId, Slice products) { + /// 응답 값 + List dtoList; - /// 유저 - User user = getUser(userId); + /// 상품 목록 꺼내서 DTO 변환 + List content = products.getContent(); - /// 켜져있을때만 끌 수있게 - if (user.isSearch()) { - user.turnOffSearch(); - userPort.updateUser(user); - } - } + /// 로그인 하지 않은 유저가 확인한다면 + if (userId == null) { - @Override - public void turnOnMySearchKeyword(UUID userId) { - /// 유저 - User user = getUser(userId); + /// 하트가 전부 false 되는 로직 + dtoList = ProductListResponse.from(content); - /// 꺼져있을때만 켤 수있게 - if (!user.isSearch()) { - user.turnOnSearch(); - userPort.updateUser(user); + /// 새로운 Slice 객체로 생성 + return new SliceImpl<>(dtoList, products.getPageable(), products.hasNext()); } - } - - /// 유저 조회 - private User getUser(UUID userId) { + /// 유저가 북마크를 했는지 체크 + List bookmarks = bookmarkPort.getBookmarksByUserId(userId); - return userPort.loadUserById(userId) - .orElseThrow(() -> new NoSuchElementException(ErrorCode.USER_NOT_FOUND.getMessage())); - } + /// 북마크된 상품 ID만 추출 + Set bookmarkedProductIds = bookmarks.stream() + .map(Bookmark::getProductId) + .collect(Collectors.toSet()); + // 북마크 여부 반영하여 DTO 변환 + dtoList = ProductListResponse.from(content, bookmarkedProductIds); - /// 검색 기록 조회 - private Search getSearch(Long searchId) { - - return port.getSearch(searchId) - .orElseThrow(() -> new NoSuchElementException(ErrorCode.SEARCH_NOT_FOUND.getMessage())); + return new SliceImpl<>(dtoList, products.getPageable(), products.hasNext()); } } diff --git a/src/main/java/site/kikihi/custom/platform/domain/user/User.java b/src/main/java/site/kikihi/custom/platform/domain/user/User.java index 6873967..0ee34e3 100644 --- a/src/main/java/site/kikihi/custom/platform/domain/user/User.java +++ b/src/main/java/site/kikihi/custom/platform/domain/user/User.java @@ -24,7 +24,6 @@ public class User { private Role role; private String profileImage; private Address address; - private boolean isSearch = true; /// 정적 팩토리 메서드 public static User of(OAuth2UserInfo userInfo) { @@ -38,17 +37,7 @@ public static User of(OAuth2UserInfo userInfo) { .profileImage(userInfo.getImageUrl()) .role(Role.USER) .address(Address.of()) - .isSearch(true) .build(); } - /// 비즈니스 로직 - public void turnOnSearch() { - isSearch = true; - } - - public void turnOffSearch() { - isSearch = false; - } - } From 414cc1e27e7416c27d47cbd65879ab1132c08477 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 18:01:41 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=94=A8chore/KIKI-48=20:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=9E=90=EB=8F=99=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index b8bdf11..95ebfec 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -9,7 +9,7 @@ name: 키키하이 dev CI-CD 파이프라인 on: push: - branches: [ "develop"] + branches: [ "develop","feat/product/KIKI-48-Product"] jobs: #1. 개발 서버 CI, Build 용 From 65d5dbae07aff1a730f57bfe060ebc5cbaba13e1 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 18:03:58 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-48=20:?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=20=EA=B0=9C=EC=88=98?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/SearchController.java | 5 +++- .../application/in/search/SearchUseCase.java | 3 ++ .../application/service/SearchService.java | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java index 0ed36ae..463d2f2 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java @@ -38,8 +38,11 @@ public ApiResponse> searchProducts( /// 서비스 호출 Slice products = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId); + /// 서비스 호출(총 검색 결과 개수) + long countByKeyword = service.countByKeyword(keyword); + /// 응답 - return ApiResponse.ok(SliceResponse.from(products)); + return ApiResponse.ok(SliceResponse.from(products, countByKeyword)); } /// 나의 최근 검색어 조회 diff --git a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java index f82467d..448a2f2 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java @@ -23,6 +23,9 @@ public interface SearchUseCase { /// 나의 검색에 목록 확인하기 List getMySearches(UUID userId); + /// 키워드 검색 결과 개수 + long countByKeyword(String keyword); + /// 키워드 하나 삭제하기 void deleteMySearchKeyword(Long searchId, UUID userId); diff --git a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index 2f292c9..f2eff23 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -157,6 +157,35 @@ public void deleteAllKeywords(UUID userId) { port.deleteALlSearch(user.getId()); } + /** + * 키워드에 따른 검색으로 검색 결과가 몇 개인지 + * @param keyword 키워드 + */ + @Override + public long countByKeyword(String keyword) { + // match 쿼리 구성 + Query nameMatch = MatchQuery.of(m -> m.field("name").query(keyword))._toQuery(); + Query descMatch = MatchQuery.of(m -> m.field("description").query(keyword))._toQuery(); + + // bool 쿼리 + Query boolQuery = BoolQuery.of(b -> b + .should(nameMatch) + .should(descMatch) + .minimumShouldMatch("1") + )._toQuery(); + + // NativeQuery - 페이징 없이 전체 개수 조회용 + NativeQuery query = NativeQuery.builder() + .withQuery(boolQuery) + .withMinScore(minScore) + .build(); + + SearchHits searchHits = elasticsearchOperations.search(query, ProductESDocument.class); + + return searchHits.getTotalHits(); + } + + /// 유저 조회 private User getUser(UUID userId) { From 0c9fcad61b635381a6cbce28d5810473ee66c4d5 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 18:07:44 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-48=20:?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=EC=A0=80=EC=9E=A5=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/platform/adapter/in/web/DevAuthController.java | 1 - .../site/kikihi/custom/platform/adapter/out/UserAdapter.java | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java index 96111e9..cb91a59 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java @@ -74,7 +74,6 @@ private User createDev() { .provider(Provider.KAKAO) // 테스트용 값 (Enum) .role(Role.ADMIN) // 관리자 권한 부여 .address(Address.of()) - .isSearch(true) .build(); } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java b/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java index a1c4103..69a8464 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java @@ -33,9 +33,6 @@ public void updateUser(User user) { /// 조회 var entity = userJpaRepository.findById(user.getId()) .orElseThrow(() -> new IllegalArgumentException(ErrorCode.USER_NOT_FOUND.getMessage())); - - /// 자동저장 여부 수정 - entity.updateSearch(user.isSearch()); } @Override From 046fe1907297c928ca36abbf0e6c7052789426fc Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 18:21:49 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-48=20:?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20Pageable=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/adapter/in/web/SearchController.java | 11 ++++++++++- .../platform/application/in/search/SearchUseCase.java | 4 ++-- .../platform/application/service/SearchService.java | 10 ++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java index 463d2f2..a902077 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java @@ -1,6 +1,8 @@ package site.kikihi.custom.platform.adapter.in.web; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.security.core.annotation.AuthenticationPrincipal; import site.kikihi.custom.global.response.ApiResponse; import site.kikihi.custom.global.response.page.PageRequest; @@ -32,11 +34,18 @@ public ApiResponse> searchProducts( @AuthenticationPrincipal PrincipalDetails principalDetails ) { + /// Pageable + Pageable pageable = org.springframework.data.domain.PageRequest.of( + pageRequest.getPage() - 1, + pageRequest.getSize(), + Sort.by(Sort.Direction.DESC, "id") + ); + /// 유저가 없다면 null 저장 UUID userId = principalDetails != null ? principalDetails.getId() : null; /// 서비스 호출 - Slice products = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId); + Slice products = service.searchProducts(keyword, pageable, userId); /// 서비스 호출(총 검색 결과 개수) long countByKeyword = service.countByKeyword(keyword); diff --git a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java index 448a2f2..d080baf 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java @@ -1,8 +1,8 @@ package site.kikihi.custom.platform.application.in.search; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; -import site.kikihi.custom.platform.domain.product.Product; import site.kikihi.custom.platform.domain.search.Search; import java.util.List; @@ -18,7 +18,7 @@ public interface SearchUseCase { /// 검색 - Slice searchProducts(String keyword, int page, int size, UUID userId); + Slice searchProducts(String keyword, Pageable pageable, UUID userId); /// 나의 검색에 목록 확인하기 List getMySearches(UUID userId); diff --git a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index f2eff23..f119336 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -14,7 +14,6 @@ import site.kikihi.custom.platform.domain.bookmark.Bookmark; import site.kikihi.custom.platform.domain.product.Product; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.client.elc.NativeQuery; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; @@ -48,10 +47,9 @@ public class SearchService implements SearchUseCase { /// 키워드 검색 (name, description) @Override - public Slice searchProducts(String keyword, int page, int size, UUID userId) { + public Slice searchProducts(String keyword, Pageable pageable, UUID userId) { /// Pageable 구성 - Pageable pageRequest = PageRequest.of(page, size); // match 쿼리 구성 Query nameMatch = MatchQuery.of(m -> m.field("name").query(keyword))._toQuery(); @@ -67,7 +65,7 @@ public Slice searchProducts(String keyword, int page, int s // NativeQuery NativeQuery query = NativeQuery.builder() .withQuery(boolQuery) - .withPageable(pageRequest) + .withPageable(pageable) .withMinScore(minScore) .build(); @@ -93,9 +91,9 @@ public Slice searchProducts(String keyword, int page, int s /// 페이징 처리 // 현재 페이지 결과 수 - boolean hasNext = checkNext(page, size, searchHits); + boolean hasNext = checkNext(pageable.getPageNumber(), pageable.getPageSize(), searchHits); - Slice products = new SliceImpl<>(elasticProducts, pageRequest, hasNext); + Slice products = new SliceImpl<>(elasticProducts, pageable, hasNext); return toProductListResponse(userId, products); } From 76604f80f0f63788bb69c0ffcc4443a0b1be46b7 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Wed, 3 Sep 2025 22:03:16 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=A8=20chore/KIKI-48=20:=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index 95ebfec..b8bdf11 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -9,7 +9,7 @@ name: 키키하이 dev CI-CD 파이프라인 on: push: - branches: [ "develop","feat/product/KIKI-48-Product"] + branches: [ "develop"] jobs: #1. 개발 서버 CI, Build 용