Skip to content

Commit 425b980

Browse files
committed
feature: 상품 랭킹 API 구현
- `RankingFacade` 및 `RankingService` 추가로 상품 랭킹 조회 로직 구현 - Redis 기반 랭킹 데이터 처리 및 상품 상세 정보 매핑 지원 - `RankingV1Controller`와 `RankingV1ApiSpec` 추가로 REST API 제공 - `RankingV1Dto`와 `RankingInfo` 구현으로 응답 데이터 구조화
1 parent cd878d5 commit 425b980

6 files changed

Lines changed: 173 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.loopers.application.rank;
2+
3+
import com.loopers.domain.product.Product;
4+
import com.loopers.domain.product.ProductService;
5+
import com.loopers.domain.rank.RankingService;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.stream.Collectors;
9+
import java.util.stream.IntStream;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Component;
12+
13+
@RequiredArgsConstructor
14+
@Component
15+
public class RankingFacade {
16+
17+
private final RankingService rankingService;
18+
private final ProductService productService;
19+
20+
public List<RankingInfo> getTopRankings(String date, int page, int size) {
21+
List<Long> productIds = rankingService.getTopRankingIds(date, page, size);
22+
23+
if (productIds.isEmpty()) {
24+
return List.of();
25+
}
26+
27+
List<Product> products = productService.getProducts(productIds);
28+
Map<Long, Product> productMap = products.stream()
29+
.collect(Collectors.toMap(Product::getId, p -> p));
30+
31+
return IntStream.range(0, productIds.size())
32+
.mapToObj(i -> {
33+
Long productId = productIds.get(i);
34+
Product product = productMap.get(productId);
35+
36+
int currentRank = ((page - 1) * size) + i + 1;
37+
38+
return RankingInfo.of(product, currentRank);
39+
})
40+
.toList();
41+
}
42+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.loopers.application.rank;
2+
3+
import com.loopers.domain.product.Product;
4+
5+
public record RankingInfo(
6+
Long productId,
7+
String productName,
8+
Long price,
9+
int stock,
10+
int currentRank
11+
) {
12+
13+
public static RankingInfo of(Product product, int currentRank) {
14+
return new RankingInfo(
15+
product.getId(),
16+
product.getName(),
17+
product.getPrice().getValue(),
18+
product.getStock(),
19+
currentRank
20+
);
21+
}
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.loopers.domain.rank;
2+
3+
import java.util.List;
4+
import java.util.Set;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.data.redis.core.RedisTemplate;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
@RequiredArgsConstructor
11+
public class RankingService {
12+
13+
private final RedisTemplate<String, String> redisTemplate;
14+
15+
public List<Long> getTopRankingIds(String date, int page, int size) {
16+
String key = "ranking:all:" + date;
17+
int start = (page - 1) * size;
18+
int end = start + size - 1;
19+
20+
Set<String> rankedIds = redisTemplate.opsForZSet().reverseRange(key, start, end);
21+
22+
if (rankedIds == null || rankedIds.isEmpty()) {
23+
return List.of();
24+
}
25+
26+
return rankedIds.stream()
27+
.map(Long::valueOf)
28+
.toList();
29+
}
30+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.loopers.interfaces.api.rank;
2+
3+
import com.loopers.interfaces.api.ApiResponse;
4+
import com.loopers.interfaces.api.rank.RankingV1Dto.RankingResponse;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.Parameter;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import java.util.List;
9+
10+
@Tag(name = "Ranking API", description = "실시간 상품 랭킹 관련 API 입니다.")
11+
public interface RankingV1ApiSpec {
12+
13+
@Operation(summary = "실시간 랭킹 조회", description = "특정 날짜의 인기 상품 랭킹을 조회합니다.")
14+
ApiResponse<List<RankingResponse>> getRankings(
15+
@Parameter(description = "조회 날짜 (yyyyMMdd)", example = "20251225") String date,
16+
@Parameter(description = "페이지 번호") int page,
17+
@Parameter(description = "페이지 크기") int size
18+
);
19+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.loopers.interfaces.api.rank;
2+
3+
import com.loopers.application.rank.RankingFacade;
4+
import com.loopers.application.rank.RankingInfo;
5+
import com.loopers.interfaces.api.ApiResponse;
6+
import com.loopers.interfaces.api.rank.RankingV1Dto.RankingResponse;
7+
import java.util.List;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RequiredArgsConstructor
15+
@RestController
16+
@RequestMapping("/api/v1/rankings")
17+
public class RankingV1Controller implements RankingV1ApiSpec {
18+
19+
private final RankingFacade rankingFacade;
20+
21+
@GetMapping
22+
@Override
23+
public ApiResponse<List<RankingResponse>> getRankings(
24+
@RequestParam(value = "date") String date,
25+
@RequestParam(value = "page", defaultValue = "1") int page,
26+
@RequestParam(value = "size", defaultValue = "20") int size
27+
) {
28+
List<RankingInfo> infos = rankingFacade.getTopRankings(date, page, size);
29+
List<RankingV1Dto.RankingResponse> response = infos.stream()
30+
.map(RankingV1Dto.RankingResponse::from)
31+
.toList();
32+
33+
return ApiResponse.success(response);
34+
}
35+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.loopers.interfaces.api.rank;
2+
3+
import com.loopers.application.rank.RankingInfo;
4+
5+
public record RankingV1Dto() {
6+
7+
public record RankingResponse(
8+
Long productId,
9+
String productName,
10+
Long price,
11+
int stock,
12+
int currentRank
13+
) {
14+
15+
public static RankingResponse from(RankingInfo info) {
16+
return new RankingResponse(
17+
info.productId(),
18+
info.productName(),
19+
info.price(),
20+
info.stock(),
21+
info.currentRank()
22+
);
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)