diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index b510d58..54c28ed 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -74,6 +74,17 @@ public ResponseEntity> searchProductV2( return ResponseEntity.ok(productService.getProductsV2(name, category, size, page)); } + // 제품 다건 조회 version 3 + @GetMapping("/v3/products") + public ResponseEntity> searchProductV3( + @RequestParam(required = false) String name, + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "1") int page + ) { + return ResponseEntity.ok(productService.getProductsV3(name, category, size, page)); + } + // 제품 삭제 @Secured("ROLE_ADMIN") @DeleteMapping("/v1/products/{productId}") diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index f018fd1..6158523 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -19,7 +19,7 @@ public interface ProductRepository extends JpaRepository { Optional findById(@Param("productId") Long productId); @Query("SELECT new com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto(p.name, p.category, p.price, AVG(r.score)) " + - "FROM Product p JOIN p.reviews r " + + "FROM Product p LEFT JOIN p.reviews r " + "WHERE p.saleState = 'FOR_SALE' " + "AND (:category IS NULL OR p.category = :category) " + "AND (:name IS NULL OR p.name LIKE CONCAT('%', :name, '%')) " + diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index e1ecf5d..9d4e986 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -11,6 +11,7 @@ import com.example.eightyage.domain.review.repository.ReviewRepository; import com.example.eightyage.domain.search.service.v1.SearchServiceV1; import com.example.eightyage.domain.search.service.v2.SearchServiceV2; +import com.example.eightyage.domain.search.service.v3.SearchServiceV3; import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -31,6 +32,7 @@ public class ProductService { private final ReviewRepository reviewRepository; private final SearchServiceV1 searchServiceV1; private final SearchServiceV2 searchServiceV2; + private final SearchServiceV3 searchServiceV3; // 제품 생성 @Transactional @@ -116,13 +118,27 @@ public Page getProductsV2(String productName, Category return productsResponse; } + // 제품 다건 조회 version 3 + @Transactional(readOnly = true) + public Page getProductsV3(String productName, Category category, int size, int page) { + int adjustedPage = Math.max(0, page - 1); + Pageable pageable = PageRequest.of(adjustedPage, size); + Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); + + if(StringUtils.hasText(productName) && !productsResponse.isEmpty()){ + searchServiceV3.saveSearchLog(productName); + searchServiceV3.increaseSortedKeywordRank(productName); + } + return productsResponse; + } + // 제품 삭제 @Transactional public void deleteProduct(Long productId) { Product findProduct = findProductByIdOrElseThrow(productId); List findReviewList = reviewRepository.findReviewsByProductId(productId); - for(Review review : findReviewList){ + for (Review review : findReviewList) { review.delete(); } @@ -134,4 +150,6 @@ public Product findProductByIdOrElseThrow(Long productId) { () -> new NotFoundException("해당 제품이 존재하지 않습니다.") ); } + + } diff --git a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java index 3bfbcc2..6ef8d33 100644 --- a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java +++ b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java @@ -3,6 +3,7 @@ import com.example.eightyage.domain.search.dto.PopularKeywordDto; import com.example.eightyage.domain.search.service.v1.PopularKeywordServiceV1; import com.example.eightyage.domain.search.service.v2.PopularKeywordServiceV2; +import com.example.eightyage.domain.search.service.v3.PopularKeywordServiceV3; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,6 +18,7 @@ public class SearchController { private final PopularKeywordServiceV1 popularKeywordServiceV1; private final PopularKeywordServiceV2 popularKeywordServiceV2; + private final PopularKeywordServiceV3 popularKeywordServiceV3; // 인기 검색어 조회 (캐시 X) @GetMapping("/api/v1/search/popular") @@ -33,4 +35,12 @@ public ResponseEntity> searchPopularKeywordsV2( ) { return ResponseEntity.ok(popularKeywordServiceV2.searchPopularKeywords(days)); } + + // 실시간 인기 검색어 조회 (캐시 O) + @GetMapping("/api/v3/search/popular") + public ResponseEntity> searchPopularKeywordsV3( + @RequestParam(defaultValue = "10") int limits + ) { + return ResponseEntity.ok(popularKeywordServiceV3.searchPopularKeywords(limits)); + } } diff --git a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java index 5f7a39f..89eac11 100644 --- a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java +++ b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java @@ -14,4 +14,7 @@ public class PopularKeywordDto { private String keyword; private Long count; + public static PopularKeywordDto of(String keyword, Long score) { + return new PopularKeywordDto(keyword, score); + } } diff --git a/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java b/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java similarity index 97% rename from src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java rename to src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java index f6d3832..b7aa890 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.search.service; +package com.example.eightyage.domain.search.service.v2; import com.example.eightyage.domain.search.entity.KeywordCount; import com.example.eightyage.domain.search.repository.KeywordCountRepository; diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index 8b9b721..c2928a6 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -47,6 +47,7 @@ public void increaseKeywordCount(String keyword) { updateKeywordSet(keyword); } + // 캐시에 키워드 추가 private void updateKeywordSet(String keyword) { Cache keySetCache = cacheManager.getCache(KEYWORD_KEY_SET); if (keySetCache != null) { diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java new file mode 100644 index 0000000..843a561 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java @@ -0,0 +1,34 @@ +package com.example.eightyage.domain.search.service.v3; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PopularKeywordServiceV3 { + + private final RedisTemplate redisTemplate; + private static final String RANKING_KEY = "rankingPopularKeywords"; + + // 인기 검색어 상위 N개 조회 + @Transactional(readOnly = true) + public List searchPopularKeywords(int limit) { + Set> keywordSet = + redisTemplate.opsForZSet().reverseRangeWithScores(RANKING_KEY, 0, limit - 1); + + if (keywordSet == null) { + return List.of(); + } + return keywordSet.stream().map(tuple -> PopularKeywordDto.of(tuple.getValue(), Objects.requireNonNull(tuple.getScore()).longValue())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java new file mode 100644 index 0000000..13e04aa --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java @@ -0,0 +1,36 @@ +package com.example.eightyage.domain.search.service.v3; + +import com.example.eightyage.domain.search.entity.SearchLog; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.Duration; + + +@Service +@RequiredArgsConstructor +public class SearchServiceV3 { + + private final SearchLogRepository searchLogRepository; + private final RedisTemplate redisTemplate; + private static final String RANKING_KEY = "rankingPopularKeywords"; + + // 검색 키워드를 로그에 저장 + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveSearchLog(String keyword) { + if (StringUtils.hasText(keyword)) { + searchLogRepository.save(SearchLog.of(keyword)); + } + } + + // 검색어 점수 증가 + public void increaseSortedKeywordRank(String productName) { + redisTemplate.opsForZSet().incrementScore(RANKING_KEY, productName, 1); + redisTemplate.expire(RANKING_KEY, Duration.ofMinutes(1)); + } +}