Skip to content

Commit 7d62f8e

Browse files
committed
feat: 랭킹 조회 API 구현
1 parent 1c0effe commit 7d62f8e

8 files changed

Lines changed: 363 additions & 43 deletions

File tree

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,25 @@
33
import com.loopers.application.product.ProductInfo;
44
import com.loopers.application.product.ProductService;
55
import com.loopers.application.ranking.RankingService.RankingItem;
6+
import com.loopers.domain.rank.MonthlyProductRank;
7+
import com.loopers.domain.rank.WeeklyProductRank;
8+
import com.loopers.infrastructure.rank.MonthlyRankJpaRepository;
9+
import com.loopers.infrastructure.rank.WeeklyRankJpaRepository;
610
import java.math.BigDecimal;
711
import java.util.ArrayList;
812
import java.util.List;
913
import java.util.Map;
1014
import java.util.stream.Collectors;
1115
import lombok.RequiredArgsConstructor;
1216
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.data.domain.PageRequest;
1318
import org.springframework.stereotype.Service;
1419

1520
/**
16-
* 랭킹 + 상품 정보 조합 Facade
21+
* Facade for combining ranking data with product information.
22+
*
23+
* <p>This service provides unified access to daily, weekly, and monthly rankings
24+
* by merging ranking data with detailed product information.
1725
*/
1826
@Slf4j
1927
@Service
@@ -22,33 +30,117 @@ public class RankingFacade {
2230

2331
private final RankingService rankingService;
2432
private final ProductService productService;
33+
private final WeeklyRankJpaRepository weeklyRankJpaRepository;
34+
private final MonthlyRankJpaRepository monthlyRankJpaRepository;
2535

2636
/**
27-
* 일간 랭킹 페이지 조회 (상품 정보 포함)
37+
* Retrieves daily rankings with product information.
38+
*
39+
* @param date the target date in yyyyMMdd format
40+
* @param page the page number (1-based)
41+
* @param size the page size
42+
* @return list of rankings with product details
2843
*/
2944
public List<RankingProductInfo> getDailyRanking(String date, int page, int size) {
30-
// 1. Redis ZSET에서 랭킹 조회
3145
List<RankingItem> rankingItems = rankingService.getDailyRanking(date, page, size);
3246

3347
if (rankingItems.isEmpty()) {
3448
return List.of();
3549
}
3650

37-
// 2. Product 정보 조회 (Batch)
51+
return combineWithProductInfo(rankingItems);
52+
}
53+
54+
/**
55+
* Retrieves today's rankings.
56+
*
57+
* @param page the page number (1-based)
58+
* @param size the page size
59+
* @return list of rankings with product details
60+
*/
61+
public List<RankingProductInfo> getTodayRanking(int page, int size) {
62+
String today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
63+
return getDailyRanking(today, page, size);
64+
}
65+
66+
/**
67+
* Retrieves weekly rankings with product information.
68+
*
69+
* @param yearWeek the target week in ISO format (e.g., "2025-W01")
70+
* @param page the page number (1-based)
71+
* @param size the page size
72+
* @return list of rankings with product details
73+
*/
74+
public List<RankingProductInfo> getWeeklyRanking(String yearWeek, int page, int size) {
75+
PageRequest pageRequest = PageRequest.of(page - 1, size);
76+
List<WeeklyProductRank> weeklyRanks = weeklyRankJpaRepository.findByYearWeekOrderByRankPositionAsc(
77+
yearWeek, pageRequest
78+
);
79+
80+
if (weeklyRanks.isEmpty()) {
81+
return List.of();
82+
}
83+
84+
return combineWithProductInfo(
85+
weeklyRanks.stream()
86+
.map(rank -> new RankingItem(
87+
rank.getRankPosition(),
88+
rank.getProductId(),
89+
rank.getTotalScore()
90+
))
91+
.toList()
92+
);
93+
}
94+
95+
/**
96+
* Retrieves monthly rankings with product information.
97+
*
98+
* @param yearMonth the target month (e.g., "2025-01")
99+
* @param page the page number (1-based)
100+
* @param size the page size
101+
* @return list of rankings with product details
102+
*/
103+
public List<RankingProductInfo> getMonthlyRanking(String yearMonth, int page, int size) {
104+
PageRequest pageRequest = PageRequest.of(page - 1, size);
105+
List<MonthlyProductRank> monthlyRanks = monthlyRankJpaRepository.findByYearMonthOrderByRankPositionAsc(
106+
yearMonth, pageRequest
107+
);
108+
109+
if (monthlyRanks.isEmpty()) {
110+
return List.of();
111+
}
112+
113+
return combineWithProductInfo(
114+
monthlyRanks.stream()
115+
.map(rank -> new RankingItem(
116+
rank.getRankPosition(),
117+
rank.getProductId(),
118+
rank.getTotalScore()
119+
))
120+
.toList()
121+
);
122+
}
123+
124+
/**
125+
* Combines ranking items with product information.
126+
*
127+
* @param rankingItems the list of ranking items
128+
* @return list of rankings with product details
129+
*/
130+
private List<RankingProductInfo> combineWithProductInfo(List<RankingItem> rankingItems) {
38131
List<Long> productIds = rankingItems.stream()
39132
.map(RankingItem::getProductId)
40133
.toList();
41134

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

45-
// 3. 랭킹 + 상품 정보 조합
46138
List<RankingProductInfo> results = new ArrayList<>();
47139
for (RankingItem item : rankingItems) {
48140
ProductInfo product = productMap.get(item.getProductId());
49141

50142
if (product == null) {
51-
log.warn("랭킹에 있지만 상품 정보 없음 - productId: {}", item.getProductId());
143+
log.warn("Product not found for ranking: productId={}", item.getProductId());
52144
continue;
53145
}
54146

@@ -68,26 +160,18 @@ public List<RankingProductInfo> getDailyRanking(String date, int page, int size)
68160
}
69161

70162
/**
71-
* 오늘 랭킹 페이지 조회
72-
*/
73-
public List<RankingProductInfo> getTodayRanking(int page, int size) {
74-
String today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
75-
return getDailyRanking(today, page, size);
76-
}
77-
78-
/**
79-
* 랭킹 + 상품 정보 DTO
163+
* DTO for ranking combined with product information.
80164
*/
81165
@lombok.Getter
82166
@lombok.Builder
83167
public static class RankingProductInfo {
84-
private int rank; // 순위
85-
private Double score; // 점수
86-
private Long productId; // 상품 ID
87-
private String productName; // 상품명
88-
private String brandName; // 브랜드명
89-
private BigDecimal price; // 가격
90-
private Integer stock; // 재고
91-
private Long likeCount; // 좋아요 수
168+
private int rank;
169+
private Double score;
170+
private Long productId;
171+
private String productName;
172+
private String brandName;
173+
private BigDecimal price;
174+
private Integer stock;
175+
private Long likeCount;
92176
}
93177
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.loopers.domain.rank;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import java.math.BigDecimal;
10+
import java.time.LocalDateTime;
11+
import lombok.AccessLevel;
12+
import lombok.Getter;
13+
import lombok.NoArgsConstructor;
14+
15+
/**
16+
* Monthly product ranking entity for materialized view.
17+
*
18+
* <p>This table stores pre-aggregated monthly ranking data for performance optimization.
19+
*/
20+
@Entity
21+
@Table(name = "mv_product_rank_monthly")
22+
@Getter
23+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
24+
public class MonthlyProductRank {
25+
26+
@Id
27+
@GeneratedValue(strategy = GenerationType.IDENTITY)
28+
private Long id;
29+
30+
@Column(name = "product_id", nullable = false)
31+
private Long productId;
32+
33+
@Column(name = "`year_month`", nullable = false, length = 7)
34+
private String yearMonth;
35+
36+
@Column(name = "rank_position", nullable = false)
37+
private Integer rankPosition;
38+
39+
@Column(name = "total_score", nullable = false)
40+
private Double totalScore;
41+
42+
@Column(name = "like_count", nullable = false)
43+
private Integer likeCount;
44+
45+
@Column(name = "view_count", nullable = false)
46+
private Integer viewCount;
47+
48+
@Column(name = "order_count", nullable = false)
49+
private Integer orderCount;
50+
51+
@Column(name = "sales_amount", nullable = false, precision = 15, scale = 2)
52+
private BigDecimal salesAmount;
53+
54+
@Column(name = "created_at", nullable = false, updatable = false)
55+
private LocalDateTime createdAt;
56+
57+
@Column(name = "updated_at", nullable = false)
58+
private LocalDateTime updatedAt;
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.loopers.domain.rank;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import java.math.BigDecimal;
10+
import java.time.LocalDateTime;
11+
import lombok.AccessLevel;
12+
import lombok.Getter;
13+
import lombok.NoArgsConstructor;
14+
15+
/**
16+
* Weekly product ranking entity for materialized view.
17+
*
18+
* <p>This table stores pre-aggregated weekly ranking data for performance optimization.
19+
*/
20+
@Entity
21+
@Table(name = "mv_product_rank_weekly")
22+
@Getter
23+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
24+
public class WeeklyProductRank {
25+
26+
@Id
27+
@GeneratedValue(strategy = GenerationType.IDENTITY)
28+
private Long id;
29+
30+
@Column(name = "product_id", nullable = false)
31+
private Long productId;
32+
33+
@Column(name = "year_week", nullable = false, length = 10)
34+
private String yearWeek;
35+
36+
@Column(name = "rank_position", nullable = false)
37+
private Integer rankPosition;
38+
39+
@Column(name = "total_score", nullable = false)
40+
private Double totalScore;
41+
42+
@Column(name = "like_count", nullable = false)
43+
private Integer likeCount;
44+
45+
@Column(name = "view_count", nullable = false)
46+
private Integer viewCount;
47+
48+
@Column(name = "order_count", nullable = false)
49+
private Integer orderCount;
50+
51+
@Column(name = "sales_amount", nullable = false, precision = 15, scale = 2)
52+
private BigDecimal salesAmount;
53+
54+
@Column(name = "created_at", nullable = false, updatable = false)
55+
private LocalDateTime createdAt;
56+
57+
@Column(name = "updated_at", nullable = false)
58+
private LocalDateTime updatedAt;
59+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.loopers.infrastructure.rank;
2+
3+
import com.loopers.domain.rank.MonthlyProductRank;
4+
import java.util.List;
5+
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
/**
9+
* JPA Repository for MonthlyProductRank entity.
10+
*/
11+
public interface MonthlyRankJpaRepository extends JpaRepository<MonthlyProductRank, Long> {
12+
13+
/**
14+
* Finds monthly rankings for a specific month with pagination.
15+
*
16+
* @param yearMonth the year-month (e.g., "2025-01")
17+
* @param pageable pagination parameters
18+
* @return list of monthly rankings
19+
*/
20+
List<MonthlyProductRank> findByYearMonthOrderByRankPositionAsc(String yearMonth, Pageable pageable);
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.loopers.infrastructure.rank;
2+
3+
import com.loopers.domain.rank.WeeklyProductRank;
4+
import java.util.List;
5+
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
/**
9+
* JPA Repository for WeeklyProductRank entity.
10+
*/
11+
public interface WeeklyRankJpaRepository extends JpaRepository<WeeklyProductRank, Long> {
12+
13+
/**
14+
* Finds weekly rankings for a specific week with pagination.
15+
*
16+
* @param yearWeek the year-week in ISO format (e.g., "2025-W01")
17+
* @param pageable pagination parameters
18+
* @return list of weekly rankings
19+
*/
20+
List<WeeklyProductRank> findByYearWeekOrderByRankPositionAsc(String yearWeek, Pageable pageable);
21+
}

0 commit comments

Comments
 (0)