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 @@ -7,6 +7,7 @@
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import java.math.BigDecimal;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
Expand Down Expand Up @@ -72,6 +73,21 @@ public Page<ProductInfo> getProductsByBrand(Long brandId, Pageable pageable) {
});
}

/**
* ์—ฌ๋Ÿฌ ์ƒํ’ˆ ID๋กœ ์กฐํšŒ (๋žญํ‚น์šฉ)
*/
@Transactional(readOnly = true)
public List<ProductInfo> findByIds(List<Long> ids) {
List<Product> products = productRepository.findByIdIn(ids);
// Brand ๋กœ๋”ฉ ๋ฐ DTO ๋ณ€ํ™˜
return products.stream()
.map(product -> {
product.getBrand().getName(); // Brand ๋กœ๋”ฉ
return ProductInfo.from(product);
})
.toList();
}
Comment on lines +76 to +89
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๋ฐ˜ํ™˜ ์ˆœ์„œ๊ฐ€ ์ž…๋ ฅ ID ์ˆœ์„œ์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ

findByIdIn์€ ์ž…๋ ฅ๋œ ID ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋žญํ‚น API์—์„œ ์ƒ์œ„ ๋žญํ‚น ์ˆœ์„œ๋Œ€๋กœ ์ƒํ’ˆ์„ ์กฐํšŒํ•  ๋•Œ, ๋ฐ˜ํ™˜๋œ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋žญํ‚น ์ˆœ์„œ์™€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆœ์„œ ๋ณด์žฅ์„ ์œ„ํ•œ ์ˆ˜์ • ์ œ์•ˆ
 @Transactional(readOnly = true)
 public List<ProductInfo> findByIds(List<Long> ids) {
+    if (ids.isEmpty()) {
+        return List.of();
+    }
     List<Product> products = productRepository.findByIdIn(ids);
+    // ID ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌํ•˜์—ฌ ๋žญํ‚น ์ˆœ์„œ ๋ณด์žฅ
+    Map<Long, Product> productMap = products.stream()
+        .collect(Collectors.toMap(Product::getId, Function.identity()));
+    
-    // Brand ๋กœ๋”ฉ ๋ฐ DTO ๋ณ€ํ™˜
-    return products.stream()
-        .map(product -> {
-            product.getBrand().getName();  // Brand ๋กœ๋”ฉ
-            return ProductInfo.from(product);
-        })
-        .toList();
+    return ids.stream()
+        .map(productMap::get)
+        .filter(Objects::nonNull)
+        .map(product -> {
+            product.getBrand().getName();  // Brand ๋กœ๋”ฉ
+            return ProductInfo.from(product);
+        })
+        .toList();
 }
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* ์—ฌ๋Ÿฌ ์ƒํ’ˆ ID๋กœ ์กฐํšŒ (๋žญํ‚น์šฉ)
*/
@Transactional(readOnly = true)
public List<ProductInfo> findByIds(List<Long> ids) {
List<Product> products = productRepository.findByIdIn(ids);
// Brand ๋กœ๋”ฉ ๋ฐ DTO ๋ณ€ํ™˜
return products.stream()
.map(product -> {
product.getBrand().getName(); // Brand ๋กœ๋”ฉ
return ProductInfo.from(product);
})
.toList();
}
/**
* ์—ฌ๋Ÿฌ ์ƒํ’ˆ ID๋กœ ์กฐํšŒ (๋žญํ‚น์šฉ)
*/
@Transactional(readOnly = true)
public List<ProductInfo> findByIds(List<Long> ids) {
if (ids.isEmpty()) {
return List.of();
}
List<Product> products = productRepository.findByIdIn(ids);
// ID ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌํ•˜์—ฌ ๋žญํ‚น ์ˆœ์„œ ๋ณด์žฅ
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));
return ids.stream()
.map(productMap::get)
.filter(Objects::nonNull)
.map(product -> {
product.getBrand().getName(); // Brand ๋กœ๋”ฉ
return ProductInfo.from(product);
})
.toList();
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java
around lines 76 to 89, the current findByIds(List<Long> ids) uses
productRepository.findByIdIn(ids) which does not preserve the input ID order;
change the method to build a Map<Long,Product> from the repository result (keyed
by product.getId()), then iterate the original ids list to produce the returned
List<ProductInfo> in the same order (skipping or handling missing ids as
appropriate); ensure you still trigger brand loading for each Product before
converting to ProductInfo (or update the repo to use a fetch-join so brand is
preloaded) so the DTO conversion remains correct.


@Transactional
@CacheEvict(value = "product", key = "#id")
public Product updateProduct(Long id, String name, BigDecimal price, Integer stock,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.loopers.application.ranking;

import com.loopers.application.product.ProductInfo;
import com.loopers.application.product.ProductService;
import com.loopers.application.ranking.RankingService.RankingItem;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* ๋žญํ‚น + ์ƒํ’ˆ ์ •๋ณด ์กฐํ•ฉ Facade
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RankingFacade {

private final RankingService rankingService;
private final ProductService productService;

/**
* ์ผ๊ฐ„ ๋žญํ‚น ํŽ˜์ด์ง€ ์กฐํšŒ (์ƒํ’ˆ ์ •๋ณด ํฌํ•จ)
*/
public List<RankingProductInfo> getDailyRanking(String date, int page, int size) {
// 1. Redis ZSET์—์„œ ๋žญํ‚น ์กฐํšŒ
List<RankingItem> rankingItems = rankingService.getDailyRanking(date, page, size);

if (rankingItems.isEmpty()) {
return List.of();
}

// 2. Product ์ •๋ณด ์กฐํšŒ (Batch)
List<Long> productIds = rankingItems.stream()
.map(RankingItem::getProductId)
.toList();

Map<Long, ProductInfo> productMap = productService.findByIds(productIds).stream()
.collect(Collectors.toMap(ProductInfo::id, p -> p));

// 3. ๋žญํ‚น + ์ƒํ’ˆ ์ •๋ณด ์กฐํ•ฉ
List<RankingProductInfo> results = new ArrayList<>();
for (RankingItem item : rankingItems) {
ProductInfo product = productMap.get(item.getProductId());

if (product == null) {
log.warn("๋žญํ‚น์— ์žˆ์ง€๋งŒ ์ƒํ’ˆ ์ •๋ณด ์—†์Œ - productId: {}", item.getProductId());
continue;
}

results.add(RankingProductInfo.builder()
.rank(item.getRank())
.score(item.getScore())
.productId(product.id())
.productName(product.name())
.brandName(product.brand().name())
.price(product.price())
.stock(product.stock())
.likeCount(product.likeCount())
.build());
}

return results;
}

/**
* ์˜ค๋Š˜ ๋žญํ‚น ํŽ˜์ด์ง€ ์กฐํšŒ
*/
public List<RankingProductInfo> getTodayRanking(int page, int size) {
String today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
return getDailyRanking(today, page, size);
}

/**
* ๋žญํ‚น + ์ƒํ’ˆ ์ •๋ณด DTO
*/
@lombok.Getter
@lombok.Builder
public static class RankingProductInfo {
private int rank; // ์ˆœ์œ„
private Double score; // ์ ์ˆ˜
private Long productId; // ์ƒํ’ˆ ID
private String productName; // ์ƒํ’ˆ๋ช…
private String brandName; // ๋ธŒ๋žœ๋“œ๋ช…
private BigDecimal price; // ๊ฐ€๊ฒฉ
private Integer stock; // ์žฌ๊ณ 
private Long likeCount; // ์ข‹์•„์š” ์ˆ˜
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.loopers.application.ranking;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.stereotype.Service;

/**
* ๋žญํ‚น ์กฐํšŒ ์„œ๋น„์Šค (Redis ZSET ์กฐํšŒ)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RankingService {

private final RedisTemplate<String, String> redisTemplate;

private static final String DAILY_PREFIX = "ranking:all:";
private static final String HOURLY_PREFIX = "ranking:realtime:";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

/**
* ์ผ๊ฐ„ ๋žญํ‚น Top-N ์กฐํšŒ
*
* @param date ์กฐํšŒ ๋‚ ์งœ (yyyyMMdd)
* @param page ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (1๋ถ€ํ„ฐ ์‹œ์ž‘)
* @param size ํŽ˜์ด์ง€ ํฌ๊ธฐ
* @return ๋žญํ‚น ์•„์ดํ…œ ๋ชฉ๋ก
*/
public List<RankingItem> getDailyRanking(String date, int page, int size) {
String key = DAILY_PREFIX + date;
return getRanking(key, page, size);
}
Comment on lines +36 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Search for RankingKey class definition
find . -name "RankingKey.java" -type f

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 169


๐Ÿ Script executed:

#!/bin/bash
# Search for DAILY_PREFIX usage
rg -n "DAILY_PREFIX" --type=java -B 2 -A 2

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 3855


๐Ÿ Script executed:

#!/bin/bash
# Search for dailyToday method and RankingKey usage
rg -n "dailyToday\|RankingKey" --type=java -B 1 -A 1

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

#!/bin/bash
# Search for RankingAggregator class
find . -name "RankingAggregator.java" -type f

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 181


๐Ÿ Script executed:

#!/bin/bash
# Search for ranking:all: pattern
rg -n "ranking:all:" --type=java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 502


๐Ÿ Script executed:

#!/bin/bash
# Read full RankingKey class
cat apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1864


๐Ÿ Script executed:

#!/bin/bash
# Read RankingAggregator class to see how it uses keys
cat apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingAggregator.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 4303


๐Ÿ Script executed:

#!/bin/bash
# Also check RankingService for complete context
cat apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 3826


RankingService๋ฅผ RankingKey๋ฅผ ํ†ตํ•ด ์ผ๊ด€๋˜๊ฒŒ ๋ฆฌํŒฉํ† ๋ง ํ•„์š”

RankingService๊ฐ€ DAILY_PREFIX๋ฅผ ์ค‘๋ณต ์ •์˜ํ•˜๊ณ  ํ‚ค๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ˜๋ฉด, RankingAggregator๋Š” RankingKey.dailyToday()๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋” ์‹ฌ๊ฐํ•˜๊ฒŒ๋Š” RankingService.getDailyRanking(String date, ...)์ด ๋‚ ์งœ ํ˜•์‹ ๊ฒ€์ฆ ์—†์ด ์ง์ ‘ ๋ฌธ์ž์—ด์„ ์—ฐ๊ฒฐํ•˜๋ฏ€๋กœ, ์ž˜๋ชป๋œ ํ˜•์‹์˜ date๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด RankingAggregator๊ฐ€ ์ƒ์„ฑํ•œ ํ‚ค์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

RankingKey.daily(String dateString) ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋‚ ์งœ ํŒŒ์‹ฑ๊ณผ ํ‚ค ์ƒ์„ฑ์„ ์ค‘์•™ํ™”ํ•˜์„ธ์š” (๋ผ์ธ 36-39, 56-59, 76-77).

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java
around lines 36-39, 56-59 and 76-77, the service builds daily ranking keys by
concatenating DAILY_PREFIX + date (and repeats similar manual key construction),
which bypasses date validation and causes mismatches with RankingAggregator;
replace those manual constructions to call RankingKey.daily(date) (or
RankingKey.dailyToday() where appropriate) so key creation and date
parsing/validation are centralized in RankingKey, updating all uses in the
listed lines to accept/propagate the RankingKey result instead of assembling raw
strings.


/**
* ์˜ค๋Š˜ ์ผ๊ฐ„ ๋žญํ‚น Top-N ์กฐํšŒ
*/
public List<RankingItem> getTodayRanking(int page, int size) {
String today = LocalDate.now().format(DATE_FORMATTER);
return getDailyRanking(today, page, size);
}

/**
* ํŠน์ • ์ƒํ’ˆ์˜ ์ผ๊ฐ„ ๋žญํ‚น ์ˆœ์œ„ ์กฐํšŒ
*
* @param date ์กฐํšŒ ๋‚ ์งœ
* @param productId ์ƒํ’ˆ ID
* @return ์ˆœ์œ„ (1๋ถ€ํ„ฐ ์‹œ์ž‘, ์—†์œผ๋ฉด null)
*/
public Long getProductRank(String date, Long productId) {
String key = DAILY_PREFIX + date;
String member = productId.toString();

Long rank = redisTemplate.opsForZSet().reverseRank(key, member);
return rank != null ? rank + 1 : null; // 0-based โ†’ 1-based
}

/**
* ์˜ค๋Š˜ ํŠน์ • ์ƒํ’ˆ์˜ ๋žญํ‚น ์ˆœ์œ„ ์กฐํšŒ
*/
public Long getProductRankToday(Long productId) {
String today = LocalDate.now().format(DATE_FORMATTER);
return getProductRank(today, productId);
}

/**
* ํŠน์ • ์ƒํ’ˆ์˜ ์ ์ˆ˜ ์กฐํšŒ
*/
public Double getProductScore(String date, Long productId) {
String key = DAILY_PREFIX + date;
String member = productId.toString();

return redisTemplate.opsForZSet().score(key, member);
}

/**
* ZSET์—์„œ ๋žญํ‚น ์กฐํšŒ (๋‚ด๋ถ€ ๊ณตํ†ต ๋กœ์ง)
*/
private List<RankingItem> getRanking(String key, int page, int size) {
// ํŽ˜์ด์ง€ ๊ณ„์‚ฐ (1-based โ†’ 0-based)
int start = (page - 1) * size;
int end = start + size - 1;

// ZREVRANGE๋กœ ์ ์ˆ˜ ๋†’์€ ์ˆœ์œผ๋กœ ์กฐํšŒ
Set<TypedTuple<String>> results = redisTemplate.opsForZSet()
.reverseRangeWithScores(key, start, end);

if (results == null || results.isEmpty()) {
log.debug("๋žญํ‚น ์กฐํšŒ ๊ฒฐ๊ณผ ์—†์Œ - key: {}, page: {}, size: {}", key, page, size);
return List.of();
}

List<RankingItem> items = new ArrayList<>();
int rank = start + 1; // ์ˆœ์œ„๋Š” 1๋ถ€ํ„ฐ ์‹œ์ž‘

for (TypedTuple<String> tuple : results) {
Long productId = Long.parseLong(tuple.getValue());
Double score = tuple.getScore();

items.add(RankingItem.builder()
.rank(rank++)
.productId(productId)
.score(score)
.build());
}

log.info("๋žญํ‚น ์กฐํšŒ ์™„๋ฃŒ - key: {}, page: {}, size: {}, count: {}",
key, page, size, items.size());

return items;
}

/**
* ๋žญํ‚น ์•„์ดํ…œ DTO
*/
@lombok.Getter
@lombok.Builder
public static class RankingItem {
private int rank; // ์ˆœ์œ„ (1๋ถ€ํ„ฐ ์‹œ์ž‘)
private Long productId; // ์ƒํ’ˆ ID
private Double score; // ์ ์ˆ˜
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.loopers.application.product.ProductInfo;
import com.loopers.application.product.ProductService;
import com.loopers.application.ranking.RankingService;
import com.loopers.application.useraction.UserActionService;
import com.loopers.domain.product.Product;
import com.loopers.interfaces.api.ApiResponse;
Expand All @@ -25,6 +26,7 @@ public class ProductV1Controller implements ProductV1ApiSpec {

private final ProductService productService;
private final UserActionService userActionService;
private final RankingService rankingService;

@PostMapping
@Override
Expand Down Expand Up @@ -55,7 +57,11 @@ public ApiResponse<ProductV1Dto.ProductResponse> getProduct(
}

ProductInfo productInfo = productService.getProduct(productId);
ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(productInfo);

// ์˜ค๋Š˜์˜ ๋žญํ‚น ์ˆœ์œ„ ์กฐํšŒ
Long rank = rankingService.getProductRankToday(productId);

ProductV1Dto.ProductResponse response = ProductV1Dto.ProductResponse.from(productInfo, rank);
Comment on lines +60 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Search for RankingService interface or class definition
find . -type f -name "*RankingService*" | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 173


๐Ÿ Script executed:

#!/bin/bash
# Search for getProductRankToday method implementation
rg "getProductRankToday" -A 10 --type java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 2747


๐Ÿ Script executed:

#!/bin/bash
# Get context around lines 60-64 in ProductV1Controller
find . -type f -name "ProductV1Controller.java" -exec head -100 {} \; | tail -50

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1871


๐Ÿ Script executed:

#!/bin/bash
# Check for exception handling patterns in ProductV1Controller
rg "throws|catch|try" . --type java -B 2 -A 2 | grep -A 5 -B 5 "getProductRankToday\|rankingService"

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

#!/bin/bash
# Read the full RankingService implementation
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingService.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 4729


๐Ÿ Script executed:

#!/bin/bash
# Check if there are any tests showing expected behavior
find . -type f -name "*RankingServiceTest*" -o -name "*ProductV1ControllerTest*" | xargs grep -l "getProductRankToday" 2>/dev/null | head -5

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


Redis ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

rankingService.getProductRankToday(productId)๋Š” Redis ์—ฐ๊ฒฐ ์‹คํŒจ ๋“ฑ์˜ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํ’ˆ ์กฐํšŒ API ์ „์ฒด๊ฐ€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, try-catch๋กœ ๊ฐ์‹ธ์„œ ๋žญํ‚น ์กฐํšŒ ์‹คํŒจ ์‹œ์—๋„ ์ƒํ’ˆ ์ •๋ณด๋Š” ๋ฐ˜ํ™˜ํ•˜๊ณ  rank๋ฅผ null๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
around lines 60 to 64, the call to rankingService.getProductRankToday(productId)
can throw (e.g., Redis connection) and currently will bubble up and fail the
whole API; wrap the ranking lookup in a try-catch, catch Exception (or the
specific Redis-related exception), on error set rank = null, log the exception
with a clear warning message and context (productId), and proceed to build and
return ProductV1Dto.ProductResponse.from(productInfo, rank) so the product data
is returned even when ranking fails.

return ApiResponse.success(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public record ProductResponse(
Integer stock,
String description,
BrandResponse brand,
Long likeCount
Long likeCount,
Long rank // ์˜ค๋Š˜์˜ ๋žญํ‚น ์ˆœ์œ„ (์—†์œผ๋ฉด null)
) {
public static ProductResponse from(ProductInfo info) {
return new ProductResponse(
Expand All @@ -54,7 +55,21 @@ public static ProductResponse from(ProductInfo info) {
info.stock(),
info.description(),
BrandResponse.from(info.brand()),
info.likeCount()
info.likeCount(),
null // ๊ธฐ๋ณธ๊ฐ’ null
);
}

public static ProductResponse from(ProductInfo info, Long rank) {
return new ProductResponse(
info.id(),
info.name(),
info.price(),
info.stock(),
info.description(),
BrandResponse.from(info.brand()),
info.likeCount(),
rank
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.loopers.interfaces.api.ranking;

import com.loopers.application.ranking.RankingFacade;
import com.loopers.application.ranking.RankingFacade.RankingProductInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* ๋žญํ‚น API
*/
@Tag(name = "Ranking", description = "์ƒํ’ˆ ๋žญํ‚น API")
@RestController
@RequestMapping("/api/v1/rankings")
@RequiredArgsConstructor
public class RankingApi {

private final RankingFacade rankingFacade;

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

@Operation(
summary = "์ผ๊ฐ„ ๋žญํ‚น ์กฐํšŒ",
description = "ํŠน์ • ๋‚ ์งœ์˜ ์ƒํ’ˆ ๋žญํ‚น์„ ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค."
)
@GetMapping
public ResponseEntity<RankingResponse> getRankings(
@Parameter(description = "์กฐํšŒ ๋‚ ์งœ (yyyyMMdd), ๋ฏธ์ž…๋ ฅ ์‹œ ์˜ค๋Š˜", example = "20250123")
@RequestParam(required = false) String date,

@Parameter(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (1๋ถ€ํ„ฐ ์‹œ์ž‘)", example = "1")
@RequestParam(defaultValue = "1") int page,

@Parameter(description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "20")
@RequestParam(defaultValue = "20") int size
) {
// ๋‚ ์งœ ๊ฒ€์ฆ
String targetDate = validateAndGetDate(date);

// ๋žญํ‚น ์กฐํšŒ
List<RankingProductInfo> rankings = rankingFacade.getDailyRanking(targetDate, page, size);

return ResponseEntity.ok(new RankingResponse(
targetDate,
page,
size,
rankings
));
}

/**
* ๋‚ ์งœ ๊ฒ€์ฆ ๋ฐ ๋ณ€ํ™˜
*/
private String validateAndGetDate(String date) {
if (date == null || date.isBlank()) {
return LocalDate.now().format(DATE_FORMATTER);
}

try {
LocalDate.parse(date, DATE_FORMATTER);
return date;
} catch (Exception e) {
throw new IllegalArgumentException("๋‚ ์งœ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (yyyyMMdd)");
}
}
}
Loading