Skip to content

Commit d824c55

Browse files
committed
feat: MV 기반 주간/월간 랭킹 조회 추가
1 parent 072f563 commit d824c55

9 files changed

Lines changed: 411 additions & 7 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.loopers.application.ranking;
2+
3+
public record ProductRankSnapshot(
4+
long rank,
5+
Long productId,
6+
double score
7+
) {
8+
}

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,30 @@ public class RankingFacade {
2121
private final RankingService rankingService;
2222
private final ProductService productService;
2323
private final BrandService brandService;
24+
private final RankingMaterializedViewService rankingMaterializedViewService;
2425

2526
@Transactional(readOnly = true)
26-
public List<RankingInfo> getRankingItems(String date, int page, int size) {
27-
return rankingService.getRankingRows(date, page, size)
27+
public List<RankingInfo> getRankingItems(String date, RankingPeriod period, int page, int size) {
28+
if (period.isDaily()) {
29+
return rankingService.getRankingRows(date, page, size)
30+
.stream()
31+
.map(this::toDto)
32+
.filter(Objects::nonNull)
33+
.collect(Collectors.toList());
34+
}
35+
return rankingMaterializedViewService.getRankings(period, period.resolveKey(date), page, size)
2836
.stream()
29-
.map(this::toDto)
37+
.map(this::toSnapshotDto)
3038
.filter(Objects::nonNull)
3139
.collect(Collectors.toList());
3240
}
3341

3442
@Transactional(readOnly = true)
35-
public long count(String date) {
36-
return rankingService.count(date);
43+
public long count(String date, RankingPeriod period) {
44+
if (period.isDaily()) {
45+
return rankingService.count(date);
46+
}
47+
return rankingMaterializedViewService.count(period, period.resolveKey(date));
3748
}
3849

3950
@Transactional(readOnly = true)
@@ -53,8 +64,19 @@ private RankingInfo toDto(RankingRow row) {
5364
return null;
5465
}
5566

56-
Product product = productService.getProduct(row.productId());
67+
return createRankingInfo(row.productId(), row.rank(), row.score());
68+
}
69+
70+
private RankingInfo toSnapshotDto(ProductRankSnapshot snapshot) {
71+
if (snapshot.productId() == null) {
72+
return null;
73+
}
74+
return createRankingInfo(snapshot.productId(), snapshot.rank(), snapshot.score());
75+
}
76+
77+
private RankingInfo createRankingInfo(Long productId, long rank, double score) {
78+
Product product = productService.getProduct(productId);
5779
ProductInfo productInfo = ProductInfo.of(product, brandService.getBrand(product.getBrandId()));
58-
return new RankingInfo(row.rank(), row.score(), productInfo);
80+
return new RankingInfo(rank, score, productInfo);
5981
}
6082
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.loopers.application.ranking;
2+
3+
import com.loopers.domain.ranking.MvProductRankMonthly;
4+
import com.loopers.domain.ranking.MvProductRankWeekly;
5+
import com.loopers.infrastructure.ranking.MvProductRankMonthlyJpaRepository;
6+
import com.loopers.infrastructure.ranking.MvProductRankWeeklyJpaRepository;
7+
import java.util.List;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class RankingMaterializedViewService {
14+
15+
private final MvProductRankWeeklyJpaRepository weeklyRepository;
16+
private final MvProductRankMonthlyJpaRepository monthlyRepository;
17+
18+
public List<ProductRankSnapshot> getRankings(RankingPeriod period, String periodKey, int page, int size) {
19+
List<ProductRankSnapshot> snapshots = fetch(period, periodKey);
20+
int safeSize = Math.max(size, 1);
21+
long skip = (long) Math.max(page, 0) * safeSize;
22+
return snapshots.stream()
23+
.skip(skip)
24+
.limit(safeSize)
25+
.toList();
26+
}
27+
28+
public long count(RankingPeriod period, String periodKey) {
29+
return fetch(period, periodKey).size();
30+
}
31+
32+
private List<ProductRankSnapshot> fetch(RankingPeriod period, String periodKey) {
33+
if (period == RankingPeriod.WEEKLY) {
34+
return weeklyRepository.findByIdPeriodKeyOrderByRankAsc(periodKey).stream()
35+
.map(this::fromWeekly)
36+
.toList();
37+
}
38+
if (period == RankingPeriod.MONTHLY) {
39+
return monthlyRepository.findByIdPeriodKeyOrderByRankAsc(periodKey).stream()
40+
.map(this::fromMonthly)
41+
.toList();
42+
}
43+
throw new IllegalArgumentException("Unsupported period for MV: " + period);
44+
}
45+
46+
private ProductRankSnapshot fromWeekly(MvProductRankWeekly entity) {
47+
return new ProductRankSnapshot(entity.getRank(), entity.getProductId(), entity.getScore());
48+
}
49+
50+
private ProductRankSnapshot fromMonthly(MvProductRankMonthly entity) {
51+
return new ProductRankSnapshot(entity.getRank(), entity.getProductId(), entity.getScore());
52+
}
53+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.loopers.application.ranking;
2+
3+
import java.time.DayOfWeek;
4+
import java.time.LocalDate;
5+
import java.time.ZoneId;
6+
import java.time.format.DateTimeFormatter;
7+
import java.time.temporal.TemporalAdjusters;
8+
import java.time.temporal.WeekFields;
9+
import java.util.Arrays;
10+
import java.util.Locale;
11+
12+
public enum RankingPeriod {
13+
DAILY("daily") {
14+
@Override
15+
public LocalDate resolveStartDate(String date) {
16+
return parse(date);
17+
}
18+
19+
@Override
20+
public String resolveKey(String date) {
21+
return parse(date).format(FORMATTER);
22+
}
23+
},
24+
WEEKLY("weekly") {
25+
@Override
26+
public LocalDate resolveStartDate(String date) {
27+
LocalDate target = parse(date);
28+
return target.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
29+
}
30+
31+
@Override
32+
public String resolveKey(String date) {
33+
return toYearMonthWeek(resolveStartDate(date));
34+
}
35+
},
36+
MONTHLY("monthly") {
37+
@Override
38+
public LocalDate resolveStartDate(String date) {
39+
LocalDate target = parse(date);
40+
return target.withDayOfMonth(1);
41+
}
42+
43+
@Override
44+
public String resolveKey(String date) {
45+
return toYearMonth(resolveStartDate(date));
46+
}
47+
};
48+
49+
private static final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul");
50+
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
51+
52+
private final String value;
53+
54+
RankingPeriod(String value) {
55+
this.value = value;
56+
}
57+
58+
public static RankingPeriod from(String value) {
59+
if (value == null) {
60+
return DAILY;
61+
}
62+
return Arrays.stream(values())
63+
.filter(period -> period.value.equalsIgnoreCase(value))
64+
.findFirst()
65+
.orElse(DAILY);
66+
}
67+
68+
public abstract LocalDate resolveStartDate(String date);
69+
70+
public abstract String resolveKey(String date);
71+
72+
public boolean isDaily() {
73+
return this == DAILY;
74+
}
75+
76+
private static LocalDate parse(String date) {
77+
if (date == null || date.isBlank()) {
78+
return LocalDate.now(ZONE_ID);
79+
}
80+
return LocalDate.parse(date, FORMATTER);
81+
}
82+
83+
private static String toYearMonthWeek(LocalDate target) {
84+
WeekFields weekFields = WeekFields.of(Locale.KOREA);
85+
int weekBasedYear = target.get(weekFields.weekBasedYear());
86+
int week = target.get(weekFields.weekOfWeekBasedYear());
87+
return String.format("%04d-W%02d", weekBasedYear, week);
88+
}
89+
90+
private static String toYearMonth(LocalDate target) {
91+
return String.format("%04d-%02d", target.getYear(), target.getMonthValue());
92+
}
93+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.loopers.domain.ranking;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.EmbeddedId;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.Table;
7+
import java.time.LocalDateTime;
8+
import lombok.AccessLevel;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Getter
13+
@Entity
14+
@Table(name = "mv_product_rank_monthly")
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
public class MvProductRankMonthly {
17+
18+
@EmbeddedId
19+
private ProductRankId id;
20+
21+
@Column(name = "like_count", nullable = false)
22+
private long likeCount;
23+
24+
@Column(name = "sales_count", nullable = false)
25+
private long salesCount;
26+
27+
@Column(name = "score", nullable = false)
28+
private double score;
29+
30+
@Column(name = "rank", nullable = false)
31+
private int rank;
32+
33+
@Column(name = "aggregated_at", nullable = false)
34+
private LocalDateTime aggregatedAt;
35+
36+
private MvProductRankMonthly(
37+
ProductRankId id,
38+
long likeCount,
39+
long salesCount,
40+
double score,
41+
int rank,
42+
LocalDateTime aggregatedAt
43+
) {
44+
this.id = id;
45+
this.likeCount = likeCount;
46+
this.salesCount = salesCount;
47+
this.score = score;
48+
this.rank = rank;
49+
this.aggregatedAt = aggregatedAt;
50+
}
51+
52+
public static MvProductRankMonthly create(
53+
String yearMonth,
54+
Long productId,
55+
long likeCount,
56+
long salesCount,
57+
double score,
58+
int rank,
59+
LocalDateTime aggregatedAt
60+
) {
61+
return new MvProductRankMonthly(
62+
ProductRankId.of(yearMonth, productId),
63+
likeCount,
64+
salesCount,
65+
score,
66+
rank,
67+
aggregatedAt
68+
);
69+
}
70+
71+
public String getPeriodKey() {
72+
return id != null ? id.getPeriodKey() : null;
73+
}
74+
75+
public Long getProductId() {
76+
return id != null ? id.getProductId() : null;
77+
}
78+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.loopers.domain.ranking;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.EmbeddedId;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.Table;
7+
import java.time.LocalDateTime;
8+
import lombok.AccessLevel;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Getter
13+
@Entity
14+
@Table(name = "mv_product_rank_weekly")
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
public class MvProductRankWeekly {
17+
18+
@EmbeddedId
19+
private ProductRankId id;
20+
21+
@Column(name = "like_count", nullable = false)
22+
private long likeCount;
23+
24+
@Column(name = "sales_count", nullable = false)
25+
private long salesCount;
26+
27+
@Column(name = "score", nullable = false)
28+
private double score;
29+
30+
@Column(name = "rank", nullable = false)
31+
private int rank;
32+
33+
@Column(name = "aggregated_at", nullable = false)
34+
private LocalDateTime aggregatedAt;
35+
36+
private MvProductRankWeekly(
37+
ProductRankId id,
38+
long likeCount,
39+
long salesCount,
40+
double score,
41+
int rank,
42+
LocalDateTime aggregatedAt
43+
) {
44+
this.id = id;
45+
this.likeCount = likeCount;
46+
this.salesCount = salesCount;
47+
this.score = score;
48+
this.rank = rank;
49+
this.aggregatedAt = aggregatedAt;
50+
}
51+
52+
public static MvProductRankWeekly create(
53+
String yearMonthWeek,
54+
Long productId,
55+
long likeCount,
56+
long salesCount,
57+
double score,
58+
int rank,
59+
LocalDateTime aggregatedAt
60+
) {
61+
return new MvProductRankWeekly(
62+
ProductRankId.of(yearMonthWeek, productId),
63+
likeCount,
64+
salesCount,
65+
score,
66+
rank,
67+
aggregatedAt
68+
);
69+
}
70+
71+
public String getPeriodKey() {
72+
return id != null ? id.getPeriodKey() : null;
73+
}
74+
75+
public Long getProductId() {
76+
return id != null ? id.getProductId() : null;
77+
}
78+
}

0 commit comments

Comments
 (0)