Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
49c7279
setting: springdoc ์„ค์ • ์ถ”
rnqhstmd Jan 2, 2026
12a0760
feat: RankingPeriod enum ์ถ”๊ฐ€ ๋ฐ command ์ ์šฉ
rnqhstmd Jan 2, 2026
c0005ac
feat: ๋žญํ‚น ์กฐํšŒ ๊ธฐ๋Šฅ์— ๊ธฐ๊ฐ„๋ณ„ ์ง€์› ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
15aece1
feat: RankingPageInfo์— RankingPeriod ์ถ”๊ฐ€ ๋ฐ ๋ฉ”์„œ๋“œ ์ˆ˜์ •
rnqhstmd Jan 2, 2026
4849e9e
feat: ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
d2c7ce7
feat: ๋žญํ‚น ์กฐํšŒ API์— ๊ธฐ๊ฐ„๋ณ„ ์ง€์› ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
0ebf4be
feat: commerce-batch ๋ชจ๋“ˆ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
962d737
feat: ์ดˆ๊ธฐ commerce-batch ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ • ๋ฐ ๋ฐฐ์น˜ ์ž‘์—… ๊ตฌ์„ฑ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
5884808
feat: ์ˆ˜๋™ ๋ฐฐ์น˜ ์ปจํŠธ๋กค๋Ÿฌ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
aaf8cd8
feat: MonthlyRanking ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€ ๋ฐ ์›”๋ณ„ ๋žญํ‚น ์ €์žฅ ๊ตฌ์กฐ ์ •์˜
rnqhstmd Jan 2, 2026
904ef24
feat: MonthlyRanking ์—”ํ‹ฐํ‹ฐ ์ถ”๊ฐ€ ๋ฐ ์›”๋ณ„ ๋žญํ‚น ์ •๋ณด ์ •์˜
rnqhstmd Jan 2, 2026
4563be8
feat: ์›”๋ณ„/์ฃผ๊ฐ„๋ณ„ job config ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
170808d
feat: MonthlyRankingJpaRepository ๋ฐ WeeklyRankingJpaRepository์— ์‚ญ์ œ ๋ฉ”์„œโ€ฆ
rnqhstmd Jan 2, 2026
e27d520
feat: ์›”๋ณ„/์ฃผ๊ฐ„๋ณ„ ๋žญํ‚น ํ”„๋กœ์„ธ์„œ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
ebc3c17
feat: ์›”๊ฐ„ ๋žญํ‚น ๋ฆฌ๋” ๋ฐ ์ฃผ๊ฐ„ ๋žญํ‚น ๋ฆฌ๋” ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
b7c5d5f
feat: ์›”๊ฐ„ ๋ฐ ์ฃผ๊ฐ„ ๋žญํ‚น ์ž‘์„ฑ๊ธฐ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
897a2f0
feat: ์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ์Šค ์—”ํ‹ฐํ‹ฐ ๋ฐ ๋ณตํ•ฉ ํ‚ค ํด๋ž˜์Šค ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
ff76dfb
feat: Redis์—์„œ ๋žญํ‚น ๊ฐ€์ค‘์น˜๋ฅผ ์ฝ์–ด์˜ค๋Š” ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
0d50304
feat: ์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ์Šค ์š”์•ฝ ๋ฐ ๋žญํฌ๋œ ์ œํ’ˆ ํด๋ž˜์Šค ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
52dbf4b
feat: ์ฃผ๊ฐ„ ๋žญํ‚น ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ธ๋ฑ์Šค ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
ee7df8b
feat: YearMonthAttributeConverter ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
f74bed0
feat: RankingRepository ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
7d1d343
feat: MonthlyRankingJpaRepository์— @Transactional ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€
rnqhstmd Jan 2, 2026
93b4a67
feat: ์ฃผ๊ฐ„ ๋žญํ‚น ํ”„๋กœ์„ธ์„œ์—์„œ ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง ๊ฐœ์„  ๋ฐ ๋ฉ”ํŠธ๋ฆญ์Šค ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์ˆ˜์ •
rnqhstmd Jan 2, 2026
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
@@ -1,49 +1,40 @@
package com.loopers.application.ranking;

import com.loopers.domain.ranking.RankingPeriod;

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

public record RankingCommand(
LocalDate date,
RankingPeriod period,
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 date, String period, int page, int size) {
LocalDate parsedDate = (date == null || date.isBlank())
? LocalDate.now()
: LocalDate.parse(date, DATE_FORMATTER);

RankingPeriod rankingPeriod = parsePeriod(period);

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

public static RankingCommand today(int page, int size) {
return new RankingCommand(LocalDate.now(), page, size);
public static RankingCommand of(String date, int page, int size) {
return of(date, "daily", page, size);
}

private static LocalDate parseDate(String dateString) {
if (dateString == null || dateString.isBlank()) {
return LocalDate.now();
private static RankingPeriod parsePeriod(String period) {
if (period == null || period.isBlank()) {
return RankingPeriod.DAILY;
}
try {
return LocalDate.parse(dateString, DATE_FORMATTER);
} catch (DateTimeParseException e) {
return LocalDate.now();
return RankingPeriod.valueOf(period.toUpperCase());
} catch (IllegalArgumentException e) {
return RankingPeriod.DAILY;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.ranking.RankingEntry;
import com.loopers.domain.ranking.RankingInfo;
import com.loopers.domain.ranking.RankingPeriod;
import com.loopers.domain.ranking.RankingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -26,30 +27,37 @@ public class RankingFacade {
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()
);
List<RankingEntry> entries;
Long totalCount;

switch (command.period()) {
case WEEKLY -> {
entries = rankingService.getWeeklyRankingPage(command.date(), command.page(), command.size());
totalCount = rankingService.getWeeklyRankingSize(command.date());
}
case MONTHLY -> {
entries = rankingService.getMonthlyRankingPage(command.date(), command.page(), command.size());
totalCount = rankingService.getMonthlyRankingSize(command.date());
}
default -> {
entries = rankingService.getRankingPage(command.date(), command.page(), command.size());
totalCount = rankingService.getRankingSize(command.date());
}
}

if (entries.isEmpty()) {
return RankingPageInfo.empty(command.date(), command.page(), command.size());
return RankingPageInfo.empty(command.date(), command.period(), 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;

Expand All @@ -69,23 +77,23 @@ public RankingPageInfo getRankingPage(RankingCommand command) {
}
}

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

return RankingPageInfo.of(
rankings,
command.date(),
command.period(),
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);
public List<RankingInfo> getTopN(LocalDate date, RankingPeriod period, int n) {
List<RankingEntry> entries = switch (period) {
case WEEKLY -> rankingService.getWeeklyTopN(date, n);
case MONTHLY -> rankingService.getMonthlyTopN(date, n);
default -> rankingService.getTopNWithScores(date, n);
};

if (entries.isEmpty()) {
return List.of();
Expand Down Expand Up @@ -118,16 +126,23 @@ public List<RankingInfo> getTopN(LocalDate date, int n) {
return rankings;
}

/**
* ํŠน์ • ์ƒํ’ˆ์˜ ์ˆœ์œ„ ์กฐํšŒ
*/
@Transactional(readOnly = true)
public List<RankingInfo> getTopN(LocalDate date, int n) {
return getTopN(date, RankingPeriod.DAILY, n);
}

public Long getProductRank(Long productId, LocalDate date, RankingPeriod period) {
return switch (period) {
case WEEKLY -> rankingService.getWeeklyRank(productId, date);
case MONTHLY -> rankingService.getMonthlyRank(productId, date);
default -> rankingService.getRank(productId, date);
};
}

public Long getProductRank(Long productId, LocalDate date) {
return rankingService.getRank(productId, date);
}

/**
* ํŠน์ • ์ƒํ’ˆ์˜ ์ˆœ์œ„ ์กฐํšŒ (์˜ค๋Š˜ ๊ธฐ์ค€)
*/
public Long getProductRankToday(Long productId) {
return rankingService.getRank(productId, LocalDate.now(clock));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
package com.loopers.application.ranking;

import com.loopers.domain.ranking.RankingInfo;
import com.loopers.domain.ranking.RankingPeriod;

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

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

public static RankingPageInfo empty(LocalDate date, RankingPeriod period, int page, int size) {
return new RankingPageInfo(List.of(), date, period, page, size, 0L, 0);
}

public static RankingPageInfo of(List<RankingInfo> rankings, LocalDate date,
int page, int size, Long totalCount) {
return of(rankings, date, RankingPeriod.DAILY, page, size, totalCount);
}

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

import com.loopers.infrastructure.converter.YearMonthAttributeConverter;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.YearMonth;

@Entity
@Getter
@Table(name = "mv_product_rank_monthly")
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public class MonthlyRanking {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "monthly_rank")
private int rank;

private Long productId;

private double score;

@Convert(converter = YearMonthAttributeConverter.class)
@Column(name = "month_period")
private YearMonth monthPeriod;
}
Comment on lines +10 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ› ๏ธ Refactor suggestion | ๐ŸŸ  Major

์ธ์Šคํ„ด์Šค ์ƒ์„ฑ์„ ์œ„ํ•œ ๋นŒ๋” ๋˜๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”.

ํ˜„์žฌ protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๋งŒ ์ œ๊ณต๋˜์–ด ์žˆ์–ด, ๋ฐฐ์น˜ ์ž‘์—…์—์„œ MonthlyRanking ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ๊ณต๊ฐœ ์ƒ์„ฑ์ž, ๋นŒ๋” ํŒจํ„ด, ๋˜๋Š” ์ •์  ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋นŒ๋” ํŒจํ„ด ์ถ”๊ฐ€ ์ œ์•ˆ
+import lombok.Builder;
+
 @Entity
 @Getter
+@Builder
 @Table(name = "mv_product_rank_monthly")
 @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
+@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
 public class MonthlyRanking {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MonthlyRanking.java
around lines 10 to 30, the entity only exposes a protected no-arg constructor
which makes creating instances from batch code difficult; add a public
constructor or a static factory method and/or a Lombok @Builder to allow easy,
explicit instance creation with required fields (id optional) โ€” implement a
public constructor or static of(...) that accepts rank, productId, score,
monthPeriod (and optionally id), or annotate the class with @Builder and add a
public all-args constructor so callers can construct MonthlyRanking instances in
batch jobs.

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

public enum RankingPeriod {
DAILY,
WEEKLY,
MONTHLY
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.domain.ranking;

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

public interface RankingRepository {

// Weekly
List<WeeklyRanking> findWeeklyByDateOrderByRank(LocalDate weekStart, LocalDate weekEnd, int limit, int offset);

Optional<WeeklyRanking> findWeeklyByProductIdAndDate(Long productId, LocalDate weekStart, LocalDate weekEnd);

long countWeeklyByDate(LocalDate weekStart, LocalDate weekEnd);

// Monthly
List<MonthlyRanking> findMonthlyByPeriodOrderByRank(YearMonth period, int limit, int offset);

Optional<MonthlyRanking> findMonthlyByProductIdAndPeriod(Long productId, YearMonth period);

long countMonthlyByPeriod(YearMonth period);
}
Loading