Skip to content

Commit d5068dd

Browse files
committed
feat: 주간, 월간 랭킹 제공 api 구현
- PeriodType별 일간, 주간, 월간 랭킹 제공 - 일간은 Redis ZSET 으로 제공 - 주간, 월간 MV를 활용한 랭킹 제공
1 parent 136e658 commit d5068dd

14 files changed

+676
-10
lines changed

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

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import com.loopers.application.product.ProductInfo;
44
import com.loopers.domain.product.Product;
55
import com.loopers.domain.product.ProductService;
6-
import com.loopers.domain.ranking.Ranking;
7-
import com.loopers.domain.ranking.RankingService;
8-
import com.loopers.domain.ranking.RankingType;
6+
import com.loopers.domain.ranking.*;
97
import lombok.RequiredArgsConstructor;
108
import org.springframework.stereotype.Component;
119
import org.springframework.transaction.annotation.Transactional;
@@ -20,14 +18,27 @@
2018
public class RankingFacade {
2119

2220
private final RankingService rankingService;
21+
private final PeriodRankingService periodRankingService;
2322
private final ProductService productService;
2423

2524
/**
2625
* TOP N 랭킹 조회 (상품 정보 포함)
2726
*/
2827
@Transactional(readOnly = true)
29-
public List<RankingInfo> getTopRanking(RankingType rankingType, LocalDate date, int limit) {
30-
List<Ranking> entries = rankingService.getTopRanking(rankingType, date, limit);
28+
public List<RankingInfo> getTopRanking(
29+
RankingType rankingType,
30+
PeriodType periodType,
31+
LocalDate date,
32+
int limit
33+
) {
34+
// period가 null이면 DAILY로 처리
35+
PeriodType period = periodType != null ? periodType : PeriodType.DAILY;
36+
37+
List<Ranking> entries = switch (period) {
38+
case DAILY -> rankingService.getTopRanking(rankingType, LocalDate.now(), limit);
39+
case WEEKLY -> periodRankingService.getTopWeeklyRanking(rankingType, date, limit);
40+
case MONTHLY -> periodRankingService.getTopMonthlyRanking(rankingType, date, limit);
41+
};
3142

3243
if (entries.isEmpty()) {
3344
return List.of();
@@ -40,9 +51,21 @@ public List<RankingInfo> getTopRanking(RankingType rankingType, LocalDate date,
4051
* 페이지네이션 랭킹 조회
4152
*/
4253
@Transactional(readOnly = true)
43-
public List<RankingInfo> getRankingWithPaging(RankingType rankingType, LocalDate date,
44-
int page, int size) {
45-
List<Ranking> entries = rankingService.getRankingWithPaging(rankingType, date, page, size);
54+
public List<RankingInfo> getRankingWithPaging(
55+
RankingType rankingType,
56+
PeriodType periodType,
57+
LocalDate date,
58+
int page,
59+
int size
60+
) {
61+
62+
PeriodType period = periodType != null ? periodType : PeriodType.DAILY;
63+
64+
List<Ranking> entries = switch (period) {
65+
case DAILY -> rankingService.getRankingWithPaging(rankingType, LocalDate.now(), page, size);
66+
case WEEKLY -> periodRankingService.getWeeklyRankingWithPaging(rankingType, date, page, size);
67+
case MONTHLY -> periodRankingService.getMonthlyRankingWithPaging(rankingType, date, page, size);
68+
};
4669

4770
if (entries.isEmpty()) {
4871
return List.of();
@@ -55,7 +78,11 @@ public List<RankingInfo> getRankingWithPaging(RankingType rankingType, LocalDate
5578
* 특정 상품의 특정 랭킹 조회
5679
*/
5780
@Transactional(readOnly = true)
58-
public RankingInfo getProductRanking(RankingType rankingType, LocalDate date, Long productId) {
81+
public RankingInfo getProductRanking(
82+
RankingType rankingType,
83+
LocalDate date,
84+
Long productId
85+
) {
5986
Ranking entry = rankingService.getProductRanking(rankingType, date, productId);
6087

6188
if (entry == null) {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.loopers.domain.metrics;
2+
3+
import jakarta.persistence.*;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import org.hibernate.annotations.Immutable;
7+
8+
import java.time.LocalDate;
9+
import java.time.ZonedDateTime;
10+
11+
/**
12+
* 월간 상품 집계 Materialized View (읽기 전용)
13+
* commerce-collector에서 생성한 집계 데이터 조회용
14+
*/
15+
@Entity
16+
@Table(name = "mv_product_metrics_monthly")
17+
@Getter
18+
@NoArgsConstructor
19+
@Immutable // 읽기 전용
20+
public class ProductMetricsMonthly {
21+
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.IDENTITY)
24+
private Long id;
25+
26+
@Column(name = "product_id", nullable = false)
27+
private Long productId;
28+
29+
@Column(name = "year", nullable = false)
30+
private Integer year;
31+
32+
@Column(name = "month", nullable = false)
33+
private Integer month;
34+
35+
@Column(name = "period_start_date", nullable = false)
36+
private LocalDate periodStartDate;
37+
38+
@Column(name = "period_end_date", nullable = false)
39+
private LocalDate periodEndDate;
40+
41+
@Column(name = "total_like_count", nullable = false)
42+
private Long totalLikeCount;
43+
44+
@Column(name = "total_view_count", nullable = false)
45+
private Long totalViewCount;
46+
47+
@Column(name = "total_order_count", nullable = false)
48+
private Long totalOrderCount;
49+
50+
@Column(name = "aggregated_at")
51+
private ZonedDateTime aggregatedAt;
52+
53+
@Column(name = "created_at")
54+
private ZonedDateTime createdAt;
55+
56+
@Column(name = "updated_at")
57+
private ZonedDateTime updatedAt;
58+
59+
/**
60+
* 종합 점수 계산 (가중치 적용)
61+
* Score = (like * 0.2) + (view * 0.1) + (order * 0.6)
62+
*/
63+
public double calculateCompositeScore() {
64+
return (totalLikeCount * 0.2) + (totalViewCount * 0.1) + (totalOrderCount * 0.6);
65+
}
66+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.loopers.domain.metrics;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
public interface ProductMetricsMonthlyRepository {
7+
/**
8+
* 특정 년도/월의 랭킹 조회 (좋아요 기준 정렬)
9+
*/
10+
List<ProductMetricsMonthly> findByYearAndMonthOrderByLikeCountDesc(int year, int month, int limit);
11+
12+
/**
13+
* 특정 년도/월의 랭킹 조회 (조회수 기준 정렬)
14+
*/
15+
List<ProductMetricsMonthly> findByYearAndMonthOrderByViewCountDesc(int year, int month, int limit);
16+
17+
/**
18+
* 특정 년도/월의 랭킹 조회 (주문수 기준 정렬)
19+
*/
20+
List<ProductMetricsMonthly> findByYearAndMonthOrderByOrderCountDesc(int year, int month, int limit);
21+
22+
/**
23+
* 특정 년도/월의 랭킹 조회 (Score 기준 정렬)
24+
*/
25+
List<ProductMetricsMonthly> findByYearAndMonthOrderByCompositeScoreDesc(int year, int month, int limit);
26+
27+
/**
28+
* 특정 상품의 월간 랭킹 조회
29+
*/
30+
Optional<ProductMetricsMonthly> findByYearAndMonthAndProductId(int year, int month, Long productId);
31+
32+
/**
33+
* 특정 년도/월의 전체 상품 수
34+
*/
35+
long countByYearAndMonth(int year, int month);
36+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.loopers.domain.metrics;
2+
3+
import jakarta.persistence.*;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import org.hibernate.annotations.Immutable;
7+
8+
import java.time.LocalDate;
9+
import java.time.ZonedDateTime;
10+
11+
/**
12+
* 주간 상품 집계 Materialized View (읽기 전용)
13+
* commerce-collector에서 생성한 집계 데이터 조회용
14+
*/
15+
@Entity
16+
@Table(name = "mv_product_metrics_weekly")
17+
@Getter
18+
@NoArgsConstructor
19+
@Immutable // 읽기 전용
20+
public class ProductMetricsWeekly {
21+
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.IDENTITY)
24+
private Long id;
25+
26+
@Column(name = "product_id", nullable = false)
27+
private Long productId;
28+
29+
@Column(name = "year", nullable = false)
30+
private Integer year;
31+
32+
@Column(name = "week", nullable = false)
33+
private Integer week;
34+
35+
@Column(name = "period_start_date", nullable = false)
36+
private LocalDate periodStartDate;
37+
38+
@Column(name = "period_end_date", nullable = false)
39+
private LocalDate periodEndDate;
40+
41+
@Column(name = "total_like_count", nullable = false)
42+
private Long totalLikeCount;
43+
44+
@Column(name = "total_view_count", nullable = false)
45+
private Long totalViewCount;
46+
47+
@Column(name = "total_order_count", nullable = false)
48+
private Long totalOrderCount;
49+
50+
@Column(name = "aggregated_at")
51+
private ZonedDateTime aggregatedAt;
52+
53+
@Column(name = "created_at")
54+
private ZonedDateTime createdAt;
55+
56+
@Column(name = "updated_at")
57+
private ZonedDateTime updatedAt;
58+
59+
/**
60+
* 종합 점수 계산 (가중치 적용)
61+
* Score = (like * 0.2) + (view * 0.1) + (order * 0.6)
62+
*/
63+
public double calculateCompositeScore() {
64+
return (totalLikeCount * 0.2) + (totalViewCount * 0.1) + (totalOrderCount * 0.6);
65+
}
66+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.loopers.domain.metrics;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
public interface ProductMetricsWeeklyRepository {
7+
/**
8+
* 특정 년도/주차의 랭킹 조회 (좋아요 기준 정렬)
9+
*/
10+
List<ProductMetricsWeekly> findByYearAndWeekOrderByLikeCountDesc(int year, int week, int limit);
11+
12+
/**
13+
* 특정 년도/주차의 랭킹 조회 (조회수 기준 정렬)
14+
*/
15+
List<ProductMetricsWeekly> findByYearAndWeekOrderByViewCountDesc(int year, int week, int limit);
16+
17+
/**
18+
* 특정 년도/주차의 랭킹 조회 (주문수 기준 정렬)
19+
*/
20+
List<ProductMetricsWeekly> findByYearAndWeekOrderByOrderCountDesc(int year, int week, int limit);
21+
22+
/**
23+
* 특정 년도/주차의 랭킹 조회 (score 기준 정렬)
24+
*/
25+
List<ProductMetricsWeekly> findByYearAndWeekOrderByCompositeScoreDesc(int year, int week, int limit);
26+
27+
/**
28+
* 특정 상품의 주간 랭킹 조회
29+
*/
30+
Optional<ProductMetricsWeekly> findByYearAndWeekAndProductId(int year, int week, Long productId);
31+
32+
/**
33+
* 특정 년도/주차의 전체 상품 수
34+
*/
35+
long countByYearAndWeek(int year, int week);
36+
}

0 commit comments

Comments
 (0)