Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ public ResponseEntity<Page<ProductSearchResponseDto>> searchProductV2(
return ResponseEntity.ok(productService.getProductsV2(name, category, size, page));
}

// 제품 다건 조회 version 3
@GetMapping("/v3/products")
public ResponseEntity<Page<ProductSearchResponseDto>> 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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> 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, '%')) " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +32,7 @@ public class ProductService {
private final ReviewRepository reviewRepository;
private final SearchServiceV1 searchServiceV1;
private final SearchServiceV2 searchServiceV2;
private final SearchServiceV3 searchServiceV3;

// 제품 생성
@Transactional
Expand Down Expand Up @@ -116,13 +118,27 @@ public Page<ProductSearchResponseDto> getProductsV2(String productName, Category
return productsResponse;
}

// 제품 다건 조회 version 3
@Transactional(readOnly = true)
public Page<ProductSearchResponseDto> getProductsV3(String productName, Category category, int size, int page) {
int adjustedPage = Math.max(0, page - 1);
Pageable pageable = PageRequest.of(adjustedPage, size);
Page<ProductSearchResponseDto> 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<Review> findReviewList = reviewRepository.findReviewsByProductId(productId);

for(Review review : findReviewList){
for (Review review : findReviewList) {
review.delete();
}

Expand All @@ -134,4 +150,6 @@ public Product findProductByIdOrElseThrow(Long productId) {
() -> new NotFoundException("해당 제품이 존재하지 않습니다.")
);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")
Expand All @@ -33,4 +35,12 @@ public ResponseEntity<List<PopularKeywordDto>> searchPopularKeywordsV2(
) {
return ResponseEntity.ok(popularKeywordServiceV2.searchPopularKeywords(days));
}

// 실시간 인기 검색어 조회 (캐시 O)
@GetMapping("/api/v3/search/popular")
public ResponseEntity<List<PopularKeywordDto>> searchPopularKeywordsV3(
@RequestParam(defaultValue = "10") int limits
) {
return ResponseEntity.ok(popularKeywordServiceV3.searchPopularKeywords(limits));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> redisTemplate;
private static final String RANKING_KEY = "rankingPopularKeywords";

// 인기 검색어 상위 N개 조회
@Transactional(readOnly = true)
public List<PopularKeywordDto> searchPopularKeywords(int limit) {
Set<ZSetOperations.TypedTuple<String>> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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));
}
}