From f6ed47c8f3a06c064e39e97027abeae27b5addc0 Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Tue, 10 Feb 2026 03:58:47 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(#333):=20=EB=B8=8C=EB=9E=9C=EB=93=9C?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BrandController.java | 21 ++++ .../presentation/swagger/BrandSwagger.java | 27 +++++- .../application/service/MatchService.java | 10 ++ .../application/service/MatchServiceImpl.java | 96 +++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/RealMatch/brand/presentation/controller/BrandController.java b/src/main/java/com/example/RealMatch/brand/presentation/controller/BrandController.java index 9400b707..c216c1c9 100644 --- a/src/main/java/com/example/RealMatch/brand/presentation/controller/BrandController.java +++ b/src/main/java/com/example/RealMatch/brand/presentation/controller/BrandController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.example.RealMatch.brand.application.service.BrandService; @@ -34,6 +35,10 @@ import com.example.RealMatch.global.config.jwt.CustomUserDetails; import com.example.RealMatch.global.presentation.CustomResponse; import com.example.RealMatch.global.presentation.code.GeneralSuccessCode; +import com.example.RealMatch.match.application.service.MatchService; +import com.example.RealMatch.match.domain.entity.enums.BrandSortType; +import com.example.RealMatch.match.domain.entity.enums.CategoryType; +import com.example.RealMatch.match.presentation.dto.response.MatchBrandResponseDto; import lombok.RequiredArgsConstructor; @@ -43,6 +48,7 @@ public class BrandController implements BrandSwagger { private final BrandService brandService; + private final MatchService matchService; // ******** // // 브랜드 조회 // @@ -58,6 +64,21 @@ public CustomResponse> getBrandDetail( return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, Collections.singletonList(result)); } + @Override + @GetMapping("/search") + public CustomResponse searchBrands( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(required = false) String title, + @RequestParam(defaultValue = "MATCH_SCORE") BrandSortType sortBy, + @RequestParam(defaultValue = "ALL") CategoryType category, + @RequestParam(required = false) List tags, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + String userId = String.valueOf(userDetails.getUserId()); + MatchBrandResponseDto result = matchService.searchMatchingBrands(userId, title, sortBy, category, tags, page, size); + return CustomResponse.ok(result); + } + @Override @PostMapping("/{brandId}/like") public CustomResponse> likeBrand( diff --git a/src/main/java/com/example/RealMatch/brand/presentation/swagger/BrandSwagger.java b/src/main/java/com/example/RealMatch/brand/presentation/swagger/BrandSwagger.java index f3fba371..4d717b91 100644 --- a/src/main/java/com/example/RealMatch/brand/presentation/swagger/BrandSwagger.java +++ b/src/main/java/com/example/RealMatch/brand/presentation/swagger/BrandSwagger.java @@ -23,6 +23,9 @@ import com.example.RealMatch.brand.presentation.dto.response.SponsorProductListResponseDto; import com.example.RealMatch.global.config.jwt.CustomUserDetails; import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.match.domain.entity.enums.BrandSortType; +import com.example.RealMatch.match.domain.entity.enums.CategoryType; +import com.example.RealMatch.match.presentation.dto.response.MatchBrandResponseDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -88,6 +91,28 @@ CustomResponse> getBrandDetail( @Parameter(hidden = true) CustomUserDetails principal ); + @Operation(summary = "브랜드 검색 by 이예림", + description = """ + JWT 토큰의 사용자 ID를 기반으로 매칭률이 높은 브랜드 목록을 검색합니다. + **검색**: title을 입력하면 브랜드명(title)만 검색합니다. + 정렬 옵션: MATCH_SCORE(매칭률 순), POPULARITY(인기순), NEWEST(신규순) + 카테고리 필터: ALL(전체), FASHION(패션), BEAUTY(뷰티) + 태그 필터: 뷰티/패션 관련 태그로 필터링 + 페이지네이션: page(0부터 시작), size(기본 20) + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "브랜드 검색 성공") + }) + CustomResponse searchBrands( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, + @Parameter(description = "브랜드명 검색어 (브랜드명 title만 검색)") String title, + @Parameter(description = "정렬 기준 (MATCH_SCORE, POPULARITY, NEWEST)") BrandSortType sortBy, + @Parameter(description = "카테고리 필터 (ALL, FASHION, BEAUTY)") CategoryType category, + @Parameter(description = "태그 필터 (예: 스킨케어, 미니멀)") List tags, + @Parameter(description = "페이지 번호 (0부터 시작)") int page, + @Parameter(description = "페이지 크기 (기본 20)") int size + ); + @Operation(summary = "브랜드 좋아요 토글 by 이예림", description = "브랜드 ID로 좋아요를 추가하거나 취소합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "토글 성공"), @@ -177,7 +202,7 @@ ResponseEntity getBrandIdByUserId( @Parameter(description = "조회할 유저의 ID", required = true) @PathVariable Long userId ); - @Operation(summary = "브랜드 개별 요약 조회 API", description = "사진 클릭 시 하단에 노출되는 브랜드의 기본 정보(이미지, 이름, 태그, 매칭률)를 조회합니다.") + @Operation(summary = "브랜드 개별 요약 조회 API by 이예림", description = "사진 클릭 시 하단에 노출되는 브랜드의 기본 정보(이미지, 이름, 태그, 매칭률)를 조회합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = BrandSimpleDetailResponse.class))), diff --git a/src/main/java/com/example/RealMatch/match/application/service/MatchService.java b/src/main/java/com/example/RealMatch/match/application/service/MatchService.java index 2a602ab9..87c073ba 100644 --- a/src/main/java/com/example/RealMatch/match/application/service/MatchService.java +++ b/src/main/java/com/example/RealMatch/match/application/service/MatchService.java @@ -16,6 +16,16 @@ public interface MatchService { MatchBrandResponseDto getMatchingBrands(String userId, BrandSortType sortBy, CategoryType category, List tags); + MatchBrandResponseDto searchMatchingBrands( + String userId, + String title, + BrandSortType sortBy, + CategoryType category, + List tags, + int page, + int size + ); + MatchCampaignResponseDto getMatchingCampaigns( String userId, String keyword, diff --git a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java index 68841652..fcacd73f 100644 --- a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java +++ b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java @@ -17,6 +17,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import com.example.RealMatch.brand.domain.entity.Brand; import com.example.RealMatch.brand.domain.entity.BrandDescribeTag; @@ -483,6 +484,7 @@ public MatchBrandResponseDto getMatchingBrands(String userId, BrandSortType sort List matchedBrands = brandHistories.stream() .filter(history -> filterBrandByCategory(history.getBrand(), category)) + .filter(history -> filterBrandByTags(history.getBrand().getId(), tags, brandDescribeTagMap)) .sorted(getBrandHistoryComparator(sortBy, brandLikeCountMap)) .limit(TOP_MATCH_COUNT) .map(history -> toMatchBrandDtoFromHistory(history, likedBrandIds, recruitingBrandIds, brandDescribeTagMap)) @@ -494,6 +496,69 @@ public MatchBrandResponseDto getMatchingBrands(String userId, BrandSortType sort .build(); } + @Override + public MatchBrandResponseDto searchMatchingBrands( + String userId, + String title, + BrandSortType sortBy, + CategoryType category, + List tags, + int page, + int size + ) { + Long userIdLong = Long.parseLong(userId); + + List brandHistories = matchBrandHistoryRepository.findByUserIdAndIsDeprecatedFalse(userIdLong); + + if (brandHistories.isEmpty()) { + LOG.warn("No match brand history found in DB. userId={}", userId); + return MatchBrandResponseDto.builder() + .count(0) + .brands(List.of()) + .build(); + } + + Set likedBrandIds = brandLikeRepository.findByUserId(userIdLong).stream() + .map(like -> like.getBrand().getId()) + .collect(Collectors.toSet()); + + Set recruitingBrandIds = getRecruitingBrandIds(); + Map brandLikeCountMap = getBrandLikeCountMap(); + + // BrandDescribeTag 조회하여 Map으로 변환 + List brandIds = brandHistories.stream() + .map(h -> h.getBrand().getId()) + .toList(); + Map> brandDescribeTagMap = brandIds.stream() + .collect(Collectors.toMap( + brandId -> brandId, + brandId -> brandDescribeTagRepository.findAllByBrandId(brandId).stream() + .map(BrandDescribeTag::getBrandDescribeTag) + .toList() + )); + + List filteredBrands = brandHistories.stream() + .filter(history -> filterBrandByCategory(history.getBrand(), category)) + .filter(history -> filterBrandByTitle(history.getBrand(), title)) + .filter(history -> filterBrandByTags(history.getBrand().getId(), tags, brandDescribeTagMap)) + .sorted(getBrandHistoryComparator(sortBy, brandLikeCountMap)) + .map(history -> toMatchBrandDtoFromHistory(history, likedBrandIds, recruitingBrandIds, brandDescribeTagMap)) + .toList(); + + int total = filteredBrands.size(); + int safePage = Math.max(page, 0); + int safeSize = size <= 0 ? 20 : size; + int fromIndex = safePage * safeSize; + int toIndex = Math.min(fromIndex + safeSize, total); + List pagedBrands = + fromIndex >= total ? List.of() : filteredBrands.subList(fromIndex, toIndex); + + return MatchBrandResponseDto.builder() + .count(total) + .brands(pagedBrands) + .build(); + } + @Override public MatchCampaignResponseDto getMatchingCampaigns( String userId, @@ -607,6 +672,37 @@ private Comparator getBrandHistoryComparator(BrandSortType so }; } + private boolean filterBrandByTitle(Brand brand, String title) { + if (!StringUtils.hasText(title)) { + return true; + } + String brandName = brand != null ? brand.getBrandName() : null; + if (!StringUtils.hasText(brandName)) { + return false; + } + return brandName.toLowerCase().contains(title.trim().toLowerCase()); + } + + private boolean filterBrandByTags(Long brandId, List tags, Map> brandDescribeTagMap) { + if (tags == null || tags.isEmpty()) { + return true; + } + List brandTags = brandDescribeTagMap.getOrDefault(brandId, List.of()); + if (brandTags.isEmpty()) { + return false; + } + + List normalizedBrandTags = brandTags.stream() + .filter(StringUtils::hasText) + .map(tag -> tag.trim().toLowerCase()) + .toList(); + + return tags.stream() + .filter(StringUtils::hasText) + .map(tag -> tag.trim().toLowerCase()) + .anyMatch(normalizedBrandTags::contains); + } + private MatchBrandResponseDto.BrandDto toMatchBrandDtoFromHistory( MatchBrandHistory history, Set likedBrandIds, Set recruitingBrandIds, Map> brandDescribeTagMap) { From 3f23ec55e1e80e97445ba5591a73854181993edd Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Tue, 10 Feb 2026 10:51:07 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor(#333):=20N+1=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B8=8C=EB=9E=9C=EB=93=9C=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=ED=83=9C=EA=B7=B8=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BrandDescribeTagRepository.java | 2 ++ .../application/service/MatchServiceImpl.java | 20 ++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/RealMatch/brand/domain/repository/BrandDescribeTagRepository.java b/src/main/java/com/example/RealMatch/brand/domain/repository/BrandDescribeTagRepository.java index 4b7b2abf..bb94e027 100644 --- a/src/main/java/com/example/RealMatch/brand/domain/repository/BrandDescribeTagRepository.java +++ b/src/main/java/com/example/RealMatch/brand/domain/repository/BrandDescribeTagRepository.java @@ -11,4 +11,6 @@ public interface BrandDescribeTagRepository extends JpaRepository { List findAllByBrandId(Long brandId); + + List findAllByBrandIdIn(List brandIds); } diff --git a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java index fcacd73f..5804906b 100644 --- a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java +++ b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java @@ -474,12 +474,10 @@ public MatchBrandResponseDto getMatchingBrands(String userId, BrandSortType sort List brandIds = brandHistories.stream() .map(h -> h.getBrand().getId()) .toList(); - Map> brandDescribeTagMap = brandIds.stream() - .collect(Collectors.toMap( - brandId -> brandId, - brandId -> brandDescribeTagRepository.findAllByBrandId(brandId).stream() - .map(BrandDescribeTag::getBrandDescribeTag) - .toList() + Map> brandDescribeTagMap = brandDescribeTagRepository.findAllByBrandIdIn(brandIds).stream() + .collect(Collectors.groupingBy( + tag -> tag.getBrand().getId(), + Collectors.mapping(BrandDescribeTag::getBrandDescribeTag, Collectors.toList()) )); List matchedBrands = brandHistories.stream() @@ -529,12 +527,10 @@ public MatchBrandResponseDto searchMatchingBrands( List brandIds = brandHistories.stream() .map(h -> h.getBrand().getId()) .toList(); - Map> brandDescribeTagMap = brandIds.stream() - .collect(Collectors.toMap( - brandId -> brandId, - brandId -> brandDescribeTagRepository.findAllByBrandId(brandId).stream() - .map(BrandDescribeTag::getBrandDescribeTag) - .toList() + Map> brandDescribeTagMap = brandDescribeTagRepository.findAllByBrandIdIn(brandIds).stream() + .collect(Collectors.groupingBy( + tag -> tag.getBrand().getId(), + Collectors.mapping(BrandDescribeTag::getBrandDescribeTag, Collectors.toList()) )); List filteredBrands = brandHistories.stream() From 9de60a837ea57cc33d66416d5b2563f5dd4de212 Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Tue, 10 Feb 2026 10:56:58 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor(#333):=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=83=89=EC=9D=84=20DB=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=ED=95=84=ED=84=B0/=EC=A0=95=EB=A0=AC/=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/MatchServiceImpl.java | 32 ++-- .../MatchBrandHistoryRepository.java | 2 +- .../MatchBrandHistoryRepositoryCustom.java | 29 ++++ ...MatchBrandHistoryRepositoryCustomImpl.java | 145 ++++++++++++++++++ 4 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustom.java create mode 100644 src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java diff --git a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java index 5804906b..bce94370 100644 --- a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java +++ b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java @@ -14,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -506,9 +507,14 @@ public MatchBrandResponseDto searchMatchingBrands( ) { Long userIdLong = Long.parseLong(userId); - List brandHistories = matchBrandHistoryRepository.findByUserIdAndIsDeprecatedFalse(userIdLong); + int safePage = Math.max(page, 0); + int safeSize = size <= 0 ? 20 : size; + PageRequest pageable = PageRequest.of(safePage, safeSize); - if (brandHistories.isEmpty()) { + Page historyPage = matchBrandHistoryRepository + .searchBrands(userIdLong, title, category, sortBy, tags, pageable); + + if (historyPage.isEmpty()) { LOG.warn("No match brand history found in DB. userId={}", userId); return MatchBrandResponseDto.builder() .count(0) @@ -521,36 +527,22 @@ public MatchBrandResponseDto searchMatchingBrands( .collect(Collectors.toSet()); Set recruitingBrandIds = getRecruitingBrandIds(); - Map brandLikeCountMap = getBrandLikeCountMap(); - // BrandDescribeTag 조회하여 Map으로 변환 - List brandIds = brandHistories.stream() + List pageBrandIds = historyPage.getContent().stream() .map(h -> h.getBrand().getId()) .toList(); - Map> brandDescribeTagMap = brandDescribeTagRepository.findAllByBrandIdIn(brandIds).stream() + Map> brandDescribeTagMap = brandDescribeTagRepository.findAllByBrandIdIn(pageBrandIds).stream() .collect(Collectors.groupingBy( tag -> tag.getBrand().getId(), Collectors.mapping(BrandDescribeTag::getBrandDescribeTag, Collectors.toList()) )); - List filteredBrands = brandHistories.stream() - .filter(history -> filterBrandByCategory(history.getBrand(), category)) - .filter(history -> filterBrandByTitle(history.getBrand(), title)) - .filter(history -> filterBrandByTags(history.getBrand().getId(), tags, brandDescribeTagMap)) - .sorted(getBrandHistoryComparator(sortBy, brandLikeCountMap)) + List pagedBrands = historyPage.getContent().stream() .map(history -> toMatchBrandDtoFromHistory(history, likedBrandIds, recruitingBrandIds, brandDescribeTagMap)) .toList(); - int total = filteredBrands.size(); - int safePage = Math.max(page, 0); - int safeSize = size <= 0 ? 20 : size; - int fromIndex = safePage * safeSize; - int toIndex = Math.min(fromIndex + safeSize, total); - List pagedBrands = - fromIndex >= total ? List.of() : filteredBrands.subList(fromIndex, toIndex); - return MatchBrandResponseDto.builder() - .count(total) + .count((int) historyPage.getTotalElements()) .brands(pagedBrands) .build(); } diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java index 2036a5e7..6faef45b 100644 --- a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java +++ b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java @@ -10,7 +10,7 @@ import com.example.RealMatch.match.domain.entity.MatchBrandHistory; -public interface MatchBrandHistoryRepository extends JpaRepository { +public interface MatchBrandHistoryRepository extends JpaRepository, MatchBrandHistoryRepositoryCustom { List findByUserId(Long userId); diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustom.java b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustom.java new file mode 100644 index 00000000..377c98a6 --- /dev/null +++ b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustom.java @@ -0,0 +1,29 @@ +package com.example.RealMatch.match.domain.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.example.RealMatch.match.domain.entity.MatchBrandHistory; +import com.example.RealMatch.match.domain.entity.enums.BrandSortType; +import com.example.RealMatch.match.domain.entity.enums.CategoryType; + +public interface MatchBrandHistoryRepositoryCustom { + + Page searchBrands( + Long userId, + String title, + CategoryType category, + BrandSortType sortBy, + List tags, + Pageable pageable + ); + + long countSearchBrands( + Long userId, + String title, + CategoryType category, + List tags + ); +} diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java new file mode 100644 index 00000000..bb2babc4 --- /dev/null +++ b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java @@ -0,0 +1,145 @@ +package com.example.RealMatch.match.domain.repository; + +import static com.example.RealMatch.brand.domain.entity.QBrand.brand; +import static com.example.RealMatch.brand.domain.entity.QBrandDescribeTag.brandDescribeTag; +import static com.example.RealMatch.match.domain.entity.QMatchBrandHistory.matchBrandHistory; + +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.stereotype.Repository; +import org.springframework.util.StringUtils; + +import com.example.RealMatch.brand.domain.entity.QBrandLike; +import com.example.RealMatch.match.domain.entity.MatchBrandHistory; +import com.example.RealMatch.match.domain.entity.enums.BrandSortType; +import com.example.RealMatch.match.domain.entity.enums.CategoryType; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class MatchBrandHistoryRepositoryCustomImpl implements MatchBrandHistoryRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page searchBrands( + Long userId, + String title, + CategoryType category, + BrandSortType sortBy, + List tags, + Pageable pageable + ) { + BooleanBuilder whereClause = buildWhereClause(userId, title, category, tags); + List> orderSpecifiers = buildOrderSpecifiers(sortBy); + + List content = queryFactory + .selectFrom(matchBrandHistory) + .join(matchBrandHistory.brand, brand).fetchJoin() + .where(whereClause) + .orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = countSearchBrands(userId, title, category, tags); + + return new PageImpl<>(content, pageable, total); + } + + @Override + public long countSearchBrands(Long userId, String title, CategoryType category, List tags) { + BooleanBuilder whereClause = buildWhereClause(userId, title, category, tags); + + Long count = queryFactory + .select(matchBrandHistory.count()) + .from(matchBrandHistory) + .join(matchBrandHistory.brand, brand) + .where(whereClause) + .fetchOne(); + + return count != null ? count : 0L; + } + + // *********** // + // 검색 조건 빌더 // + // *********** // + private BooleanBuilder buildWhereClause(Long userId, String title, CategoryType category, List tags) { + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(matchBrandHistory.user.id.eq(userId)); + builder.and(matchBrandHistory.isDeprecated.eq(false)); + + if (StringUtils.hasText(title)) { + builder.and(brand.brandName.containsIgnoreCase(title.trim())); + } + + if (category != null && category != CategoryType.ALL) { + builder.and(brand.industryType.stringValue().equalsIgnoreCase(category.name())); + } + + if (tags != null && !tags.isEmpty()) { + List normalizedTags = tags.stream() + .filter(StringUtils::hasText) + .map(tag -> tag.trim().toLowerCase()) + .toList(); + + if (!normalizedTags.isEmpty()) { + StringExpression tagLower = brandDescribeTag.brandDescribeTag.lower(); + builder.and( + JPAExpressions + .selectOne() + .from(brandDescribeTag) + .where( + brandDescribeTag.brand.id.eq(brand.id), + tagLower.in(normalizedTags) + ) + .exists() + ); + } + } + + return builder; + } + + // *********** // + // 정렬 조건 빌더 // + // *********** // + private com.querydsl.core.types.Expression likeCountSubquery() { + QBrandLike likeSub = new QBrandLike("bl"); + return JPAExpressions + .select(likeSub.id.count()) + .from(likeSub) + .where(likeSub.brand.id.eq(brand.id)); + } + + private OrderSpecifier likeCountDesc() { + return new OrderSpecifier<>(Order.DESC, likeCountSubquery(), OrderSpecifier.NullHandling.NullsLast); + } + + private List> buildOrderSpecifiers(BrandSortType sortBy) { + if (sortBy == null) { + sortBy = BrandSortType.MATCH_SCORE; + } + + OrderSpecifier matchingRatioDesc = matchBrandHistory.matchingRatio.desc().nullsLast(); + OrderSpecifier brandIdDesc = brand.id.desc(); + OrderSpecifier popularityDesc = likeCountDesc(); + + return switch (sortBy) { + case POPULARITY -> List.of(popularityDesc, matchingRatioDesc, brandIdDesc); + case NEWEST -> List.of(brandIdDesc); + default -> List.of(matchingRatioDesc, popularityDesc, brandIdDesc); + }; + } +} From c6e17d9273148d3529ddea152a38f44f4381ee8d Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Tue, 10 Feb 2026 11:04:23 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(#333):=20=EB=B9=8C=EB=93=9C=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/application/service/MatchServiceImpl.java | 1 - .../MatchBrandHistoryRepositoryCustomImpl.java | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java index bce94370..fc5aa60d 100644 --- a/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java +++ b/src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java @@ -14,7 +14,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java index bb2babc4..a3a64387 100644 --- a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java +++ b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepositoryCustomImpl.java @@ -1,7 +1,6 @@ package com.example.RealMatch.match.domain.repository; import static com.example.RealMatch.brand.domain.entity.QBrand.brand; -import static com.example.RealMatch.brand.domain.entity.QBrandDescribeTag.brandDescribeTag; import static com.example.RealMatch.match.domain.entity.QMatchBrandHistory.matchBrandHistory; import java.util.List; @@ -12,6 +11,7 @@ import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; +import com.example.RealMatch.brand.domain.entity.QBrandDescribeTag; import com.example.RealMatch.brand.domain.entity.QBrandLike; import com.example.RealMatch.match.domain.entity.MatchBrandHistory; import com.example.RealMatch.match.domain.entity.enums.BrandSortType; @@ -29,6 +29,8 @@ @RequiredArgsConstructor public class MatchBrandHistoryRepositoryCustomImpl implements MatchBrandHistoryRepositoryCustom { + private static final QBrandDescribeTag BRAND_DESCRIBE_TAG = QBrandDescribeTag.brandDescribeTag1; + private final JPAQueryFactory queryFactory; @Override @@ -95,13 +97,13 @@ private BooleanBuilder buildWhereClause(Long userId, String title, CategoryType .toList(); if (!normalizedTags.isEmpty()) { - StringExpression tagLower = brandDescribeTag.brandDescribeTag.lower(); + StringExpression tagLower = BRAND_DESCRIBE_TAG.brandDescribeTag.lower(); builder.and( JPAExpressions .selectOne() - .from(brandDescribeTag) + .from(BRAND_DESCRIBE_TAG) .where( - brandDescribeTag.brand.id.eq(brand.id), + BRAND_DESCRIBE_TAG.brand.id.eq(brand.id), tagLower.in(normalizedTags) ) .exists()