Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
514452c
feat: carry over λ³€μˆ˜ μΆ”κ°€
rnqhstmd Dec 26, 2025
2b4e367
feat: μŠ€μΌ€μ₯΄λŸ¬ ν™œμ„± μ„€μ • μΆ”κ°€
rnqhstmd Dec 26, 2025
544c506
feat: ProductDetailInfo에 rank ν•„λ“œ μΆ”κ°€ 및 κ΄€λ ¨ λ©”μ„œλ“œ μ˜€λ²„λ‘œλ“œ
rnqhstmd Dec 26, 2025
2cc76d3
feat: ProductDetailInfo에 λž­ν‚Ή 정보 μΆ”κ°€
rnqhstmd Dec 26, 2025
86642b4
feat: ProductMetricsFacade에 λž­ν‚Ή 점수 μ—…λ°μ΄νŠΈ 둜직 μΆ”κ°€
rnqhstmd Dec 26, 2025
3728d07
feat: RankingFacade 클래슀 μΆ”κ°€ 및 이벀트 처리 λ©”μ„œλ“œ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
3b834f1
feat: RankingKey 클래슀 μΆ”κ°€ 및 λž­ν‚Ή ν‚€ 생성 λ©”μ„œλ“œ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
47901f0
feat: RankingScheduler 클래슀 μΆ”κ°€ 및 맀일 λž­ν‚Ή μ€€λΉ„ 둜직 κ΅¬ν˜„
rnqhstmd Dec 26, 2025
3a72f74
feat: RankingService 클래슀 μΆ”κ°€ 및 λž­ν‚Ή 점수 관리 둜직 κ΅¬ν˜„
rnqhstmd Dec 26, 2025
a1c1536
feat: RankingWeight 클래슀 μΆ”κ°€ 및 λž­ν‚Ή κ°€μ€‘μΉ˜ 관리 둜직 κ΅¬ν˜„
rnqhstmd Dec 26, 2025
a7c749a
feat: ClockConfig 클래슀 μΆ”κ°€ 및 μ‹œμŠ€ν…œ κΈ°λ³Έ μ‹œκ°„ μ„€μ • 둜직 κ΅¬ν˜„
rnqhstmd Dec 26, 2025
0ce4a25
feat: μƒν’ˆ 쑰회 API κ΅¬ν˜„ 및 DTO 클래슀 μΆ”κ°€
rnqhstmd Dec 26, 2025
4291ab4
feat: λž­ν‚Ή κ°€μ€‘μΉ˜ μ„€μ • 관리 API κ΅¬ν˜„
rnqhstmd Dec 26, 2025
1855152
feat: RankingCommand 클래슀 μΆ”κ°€ 및 μœ νš¨μ„± 검증 둜직 κ΅¬ν˜„
rnqhstmd Dec 26, 2025
ae41a7a
feat: μƒν’ˆ λž­ν‚Ή API κ΅¬ν˜„ 및 DTO 클래슀 μΆ”κ°€
rnqhstmd Dec 26, 2025
6c51a58
feat: RankingEntry 클래슀 μΆ”κ°€
rnqhstmd Dec 26, 2025
d9f23f0
feat: RankingFacade 클래슀 μΆ”κ°€ 및 λž­ν‚Ή 쑰회 κΈ°λŠ₯ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
695fe6e
feat: RankingInfo 클래슀 μΆ”κ°€ 및 λž­ν‚Ή 정보 ν‘œν˜„μ„ μœ„ν•œ λ ˆμ½”λ“œ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
c5ff2ee
feat: RankingKey 클래슀 μΆ”κ°€ 및 Redis ZSET ν‚€ 생성 μœ ν‹Έλ¦¬ν‹° κ΅¬ν˜„
rnqhstmd Dec 26, 2025
ee72acc
feat: RankingPageInfo 클래슀 μΆ”κ°€ 및 νŽ˜μ΄μ§€ 정보 처리 μœ ν‹Έλ¦¬ν‹° κ΅¬ν˜„
rnqhstmd Dec 26, 2025
3a33ab2
feat: RankingService 클래슀 μΆ”κ°€ 및 λž­ν‚Ή 쑰회 κΈ°λŠ₯ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
81c7deb
test: ProductMetricsConsumerTest에 쀑볡 이벀트 및 λ©±λ“±μ„± 처리 ν…ŒμŠ€νŠΈ μΆ”κ°€
rnqhstmd Dec 26, 2025
43d49a1
test: RankingIntegrationTest 클래슀 μΆ”κ°€ 및 λž­ν‚Ή κ΄€λ ¨ κΈ°λŠ₯ 톡합 ν…ŒμŠ€νŠΈ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
205e26f
test: RankingSchedulerTest 클래슀 μΆ”κ°€ 및 λž­ν‚Ή μŠ€μΌ€μ€„λŸ¬ κΈ°λŠ₯ ν…ŒμŠ€νŠΈ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
ff536c6
test: RankingServiceTest 클래슀 μΆ”κ°€ 및 λž­ν‚Ή μ„œλΉ„μŠ€ κΈ°λŠ₯ ν…ŒμŠ€νŠΈ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
f5a6e25
test: RankingServiceTest 클래슀 μΆ”κ°€ 및 쑰회, μ’‹μ•„μš”, μ£Όλ¬Έ 점수 증가 κΈ°λŠ₯ ν…ŒμŠ€νŠΈ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
b99c7b9
test: RankingV1ApiE2ETest 클래슀 μΆ”κ°€ 및 λž­ν‚Ή API κΈ°λŠ₯ ν…ŒμŠ€νŠΈ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
fd11c00
test: RankingWeightTest 클래슀 μΆ”κ°€ 및 κ°€μ€‘μΉ˜ κ΄€λ ¨ κΈ°λŠ₯ ν…ŒμŠ€νŠΈ κ΅¬ν˜„
rnqhstmd Dec 26, 2025
328b783
feat: validation import μΆ”κ°€
rnqhstmd Dec 29, 2025
e3b1b4e
test: displayname λ³€κ²½
rnqhstmd Dec 29, 2025
2f2917a
feat: μœ νš¨μ„± 검증 μΆ”κ°€
rnqhstmd Dec 29, 2025
151393f
refactor: μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” λ©”μ„œλ“œ 제거
rnqhstmd Dec 29, 2025
ea4e6f0
test: assertTrue μˆ˜μ •
rnqhstmd Dec 29, 2025
2699b7d
feat: κ°€μ€‘μΉ˜ 일괄 μ—…λ°μ΄νŠΈ μ‹œ μ—λŸ¬ 핸듀링 μΆ”κ°€
rnqhstmd Dec 29, 2025
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 @@ -11,7 +11,8 @@ public record ProductDetailInfo(
Integer stock,
Long brandId,
String brandName,
Long likeCount
Long likeCount,
Long rank
) implements Serializable {
public static ProductDetailInfo of(Product product, Long likeCount) {
return new ProductDetailInfo(
Expand All @@ -21,7 +22,34 @@ public static ProductDetailInfo of(Product product, Long likeCount) {
product.getStockValue(),
product.getBrand().getId(),
product.getBrand().getName(),
likeCount
likeCount,
null
);
}

public static ProductDetailInfo of(Product product, Long likeCount, Long rank) {
return new ProductDetailInfo(
product.getId(),
product.getName(),
product.getPriceValue(),
product.getStockValue(),
product.getBrand().getId(),
product.getBrand().getName(),
likeCount,
rank
);
}

public ProductDetailInfo withRank(Long rank) {
return new ProductDetailInfo(
this.productId,
this.productName,
this.price,
this.stock,
this.brandId,
this.brandName,
this.likeCount,
rank
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.loopers.application.product;

import com.loopers.application.ranking.RankingFacade;
import com.loopers.domain.product.ProductSearchCondition;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserActionEvent;
Expand All @@ -18,11 +19,15 @@ public class ProductFacade {
private final ProductCacheService productCacheService;
private final UserService userService;
private final ApplicationEventPublisher eventPublisher;
private final RankingFacade rankingFacade;

@Transactional(readOnly = true)
public ProductDetailInfo getProductDetail(Long productId, String loginId) {
ProductDetailInfo productDetail = productCacheService.getProductDetailWithCache(productId);

Long rank = rankingFacade.getProductRankToday(productId);
productDetail = productDetail.withRank(rank);

// μœ μ € 행동 λ‘œκΉ…
if (loginId != null) {
try {
Expand All @@ -40,7 +45,11 @@ public ProductDetailInfo getProductDetail(Long productId, String loginId) {

@Transactional(readOnly = true)
public ProductDetailInfo getProductDetail(Long productId) {
return productCacheService.getProductDetailWithCache(productId);
ProductDetailInfo productDetail = productCacheService.getProductDetailWithCache(productId);

// λž­ν‚Ή 정보 μΆ”κ°€
Long rank = rankingFacade.getProductRankToday(productId);
return productDetail.withRank(rank);
}

public ProductListInfo getProducts(ProductGetListCommand command) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.loopers.application.ranking;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public record RankingCommand(
LocalDate date,
int page,
int size
) {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final int MAX_PAGE_SIZE = 100;
private static final int DEFAULT_PAGE_SIZE = 20;

public RankingCommand {
// μœ νš¨μ„± 검증
if (page < 0) {
page = 0;
}
if (size <= 0 || size > MAX_PAGE_SIZE) {
size = DEFAULT_PAGE_SIZE;
}
if (date == null) {
date = LocalDate.now();
}
}

public static RankingCommand of(String dateString, int page, int size) {
LocalDate date = parseDate(dateString);
return new RankingCommand(date, page, size);
}

public static RankingCommand today(int page, int size) {
return new RankingCommand(LocalDate.now(), page, size);
}

private static LocalDate parseDate(String dateString) {
if (dateString == null || dateString.isBlank()) {
return LocalDate.now();
}
try {
return LocalDate.parse(dateString, DATE_FORMATTER);
} catch (DateTimeParseException e) {
return LocalDate.now();
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.loopers.application.ranking;

import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.ranking.RankingEntry;
import com.loopers.domain.ranking.RankingInfo;
import com.loopers.domain.ranking.RankingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.Clock;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class RankingFacade {

private final RankingService rankingService;
private final ProductRepository productRepository;
private final Clock clock;

/**
* λž­ν‚Ή νŽ˜μ΄μ§€ 쑰회
*/
@Transactional(readOnly = true)
public RankingPageInfo getRankingPage(RankingCommand command) {
List<RankingEntry> entries = rankingService.getRankingPage(
command.date(),
command.page(),
command.size()
);

if (entries.isEmpty()) {
return RankingPageInfo.empty(command.date(), command.page(), command.size());
}

// μƒν’ˆ 정보 쑰회
List<Long> productIds = entries.stream()
.map(RankingEntry::productId)
.collect(Collectors.toList());

Map<Long, Product> productMap = productRepository.findAllByIds(productIds).stream()
.collect(Collectors.toMap(Product::getId, p -> p));

// λž­ν‚Ή 정보 μ‘°ν•©
List<RankingInfo> rankings = new ArrayList<>();
long startRank = (long) command.page() * command.size() + 1;

for (int i = 0; i < entries.size(); i++) {
RankingEntry entry = entries.get(i);
Product product = productMap.get(entry.productId());

if (product != null) {
rankings.add(RankingInfo.of(
product.getId(),
product.getName(),
product.getPriceValue(),
product.getBrand().getName(),
startRank + i,
entry.score()
));
}
}

Long totalCount = rankingService.getRankingSize(command.date());

return RankingPageInfo.of(
rankings,
command.date(),
command.page(),
command.size(),
totalCount
);
}

/**
* Top-N λž­ν‚Ή 쑰회
*/
@Transactional(readOnly = true)
public List<RankingInfo> getTopN(LocalDate date, int n) {
List<RankingEntry> entries = rankingService.getTopNWithScores(date, n);

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

List<Long> productIds = entries.stream()
.map(RankingEntry::productId)
.collect(Collectors.toList());

Map<Long, Product> productMap = productRepository.findAllByIds(productIds).stream()
.collect(Collectors.toMap(Product::getId, p -> p));

List<RankingInfo> rankings = new ArrayList<>();
for (int i = 0; i < entries.size(); i++) {
RankingEntry entry = entries.get(i);
Product product = productMap.get(entry.productId());

if (product != null) {
rankings.add(RankingInfo.of(
product.getId(),
product.getName(),
product.getPriceValue(),
product.getBrand().getName(),
(long) (i + 1),
entry.score()
));
}
}

return rankings;
}

/**
* νŠΉμ • μƒν’ˆμ˜ μˆœμœ„ 쑰회
*/
public Long getProductRank(Long productId, LocalDate date) {
return rankingService.getRank(productId, date);
}

/**
* νŠΉμ • μƒν’ˆμ˜ μˆœμœ„ 쑰회 (였늘 κΈ°μ€€)
*/
public Long getProductRankToday(Long productId) {
return rankingService.getRank(productId, LocalDate.now(clock));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.loopers.application.ranking;

import com.loopers.domain.ranking.RankingInfo;

import java.time.LocalDate;
import java.util.List;

public record RankingPageInfo(
List<RankingInfo> rankings,
LocalDate date,
int page,
int size,
Long totalCount,
int totalPages
) {
public static RankingPageInfo of(
List<RankingInfo> rankings,
LocalDate date,
int page,
int size,
Long totalCount
) {
int totalPages = (int) Math.ceil((double) totalCount / size);
return new RankingPageInfo(rankings, date, page, size, totalCount, totalPages);
}

public static RankingPageInfo empty(LocalDate date, int page, int size) {
return new RankingPageInfo(List.of(), date, page, size, 0L, 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.loopers.domain.ranking;

public record RankingEntry(
Long productId,
Double score
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.loopers.domain.ranking;

public record RankingInfo(
Long productId,
String productName,
Long price,
String brandName,
Long rank,
Double score
) {
public static RankingInfo of(
Long productId,
String productName,
Long price,
String brandName,
Long rank,
Double score
) {
return new RankingInfo(productId, productName, price, brandName, rank, score);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.loopers.domain.ranking;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
* Redis ZSET ν‚€ 생성 μœ ν‹Έλ¦¬ν‹°
*/
public class RankingKey {

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

private RankingKey() {
}

public static String daily(LocalDate date) {
return KEY_PREFIX + date.format(DATE_FORMATTER);
}
}
Loading