Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
7fe86f3
feat: commerce-batch ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒ์„ฑ
yeonsu00 Dec 30, 2025
3219edb
feat: ๊ฐ ์ƒํ’ˆ๋ณ„, ๋‚ ์งœ๋ณ„๋กœ likeCount, viewCount, salesCount ์ง‘๊ณ„ ๋  ์ˆ˜ ์žˆ๋„๋ก ๋กœ์ง ์ˆ˜์ •
yeonsu00 Dec 30, 2025
a8d0651
feat: ProductMetrics ๋„๋ฉ”์ธ ์ถ”๊ฐ€
yeonsu00 Jan 1, 2026
6db0d0b
feat: ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ๋ฐฐ์น˜ ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ ์ถ”๊ฐ€
yeonsu00 Jan 1, 2026
5ef9c2c
feat: ์›”๊ฐ„ ๋ฐ ์ฃผ๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ๋„๋ฉ”์ธ ๋ฐ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ถ”๊ฐ€
yeonsu00 Jan 1, 2026
9206eb2
feat: ์›”๊ฐ„ ๋ฐ ์ฃผ๊ฐ„ ๋žญํ‚น ์„œ๋น„์Šค ์ถ”๊ฐ€
yeonsu00 Jan 1, 2026
8ce3935
feat: ๋žญํ‚น ๋ฐฐ์น˜ ์ž‘์—…์„ ์œ„ํ•œ Config, Reader, Processor, Writer ์ถ”๊ฐ€
yeonsu00 Jan 1, 2026
4da5b86
feat: ๋žญํ‚น ์กฐํšŒ API ๊ฐœ์„  - ์ผ๊ฐ„, ์ฃผ๊ฐ„, ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ์ง€์›
yeonsu00 Jan 1, 2026
6ce8ba3
feat: Ranking API E2E ํ…Œ์ŠคํŠธ ์ˆ˜์ •
yeonsu00 Jan 1, 2026
bdbd9a9
feat: ์›”๊ฐ„ ๋ฐ ์ฃผ๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ๋กœ์ง ๊ตฌํ˜„
yeonsu00 Jan 1, 2026
8fbbdcd
test: RankingServiceIntegrationTest ์ฃผ๊ฐ„ ๋ฐ ์›”๊ฐ„ ๋žญํ‚น ์กฐํšŒ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
yeonsu00 Jan 1, 2026
af65e9c
Merge pull request #38 from yeonsu00/feature/week10-ranking-batch
yeonsu00 Jan 1, 2026
de4d9cb
Merge pull request #39 from yeonsu00/round-10
yeonsu00 Jan 1, 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
Expand Up @@ -4,8 +4,15 @@

public class RankingCommand {

public record GetDailyRankingCommand(
public enum RankingType {
DAILY,
WEEKLY,
MONTHLY
}

public record GetRankingCommand(
LocalDate date,
RankingType type,
int page,
int size
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.loopers.domain.product.ProductService;
import com.loopers.domain.ranking.Ranking;
import com.loopers.domain.ranking.RankingService;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -23,12 +25,18 @@ public class RankingFacade {
private final ProductService productService;
private final BrandService brandService;

public RankingInfo getDailyRanking(RankingCommand.GetDailyRankingCommand command) {
List<Ranking> rankings = rankingService.getRanking(
command.date(),
command.page(),
command.size()
);
public RankingInfo getRanking(RankingCommand.GetRankingCommand command) {
List<Ranking> rankings;

if (command.type() == RankingCommand.RankingType.DAILY) {
rankings = rankingService.getDailyRanking(command.date(), command.page(), command.size());
} else if (command.type() == RankingCommand.RankingType.WEEKLY) {
rankings = rankingService.getWeeklyRanking(command.date(), command.page(), command.size());
} else if (command.type() == RankingCommand.RankingType.MONTHLY) {
rankings = rankingService.getMonthlyRanking(command.date(), command.page(), command.size());
} else {
throw new CoreException(ErrorType.BAD_REQUEST, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋žญํ‚น ํƒ€์ž…์ž…๋‹ˆ๋‹ค: " + command.type());
}

if (rankings.isEmpty()) {
return new RankingInfo(List.of());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.loopers.domain.ranking;

import com.loopers.domain.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;

@Entity
@Table(
name = "mv_product_rank_monthly",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"product_id", "period_start_date", "period_end_date"})
}
)
@Getter
public class MvProductRankMonthly extends BaseEntity {

@Column(name = "product_id", nullable = false)
private Long productId;

@Column(nullable = true)
private Integer ranking;

@Column(nullable = false)
private Double score;

@Column(name = "period_start_date", nullable = false)
private LocalDate periodStartDate;

@Column(name = "period_end_date", nullable = false)
private LocalDate periodEndDate;

@Column(name = "like_count", nullable = false)
private Long likeCount;

@Column(name = "view_count", nullable = false)
private Long viewCount;

@Column(name = "sales_count", nullable = false)
private Long salesCount;

@Builder
private MvProductRankMonthly(Long productId, Integer ranking, Double score, LocalDate periodStartDate,
LocalDate periodEndDate, Long likeCount, Long viewCount, Long salesCount) {
this.productId = productId;
this.ranking = ranking;
this.score = score;
this.periodStartDate = periodStartDate;
this.periodEndDate = periodEndDate;
this.likeCount = likeCount;
this.viewCount = viewCount;
this.salesCount = salesCount;
}

public MvProductRankMonthly() {
}

public static MvProductRankMonthly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate,
LocalDate periodEndDate, Long likeCount, Long viewCount, Long salesCount) {
return MvProductRankMonthly.builder()
.productId(productId)
.ranking(ranking)
.score(score)
.periodStartDate(periodStartDate)
.periodEndDate(periodEndDate)
.likeCount(likeCount)
.viewCount(viewCount)
.salesCount(salesCount)
.build();
}
}
Comment on lines +13 to +76
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

์—”ํ‹ฐํ‹ฐ์— ํ•„์š”ํ•œ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

MonthlyRankingService์—์„œ existing.updateMetrics(score, likeCount, viewCount, salesCount)์™€ rank.updateRanking(i + 1)๋ฅผ ํ˜ธ์ถœํ•˜๋Š”๋ฐ, ์ด ์—”ํ‹ฐํ‹ฐ์—๋Š” ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋“ค์ด ์—†์Šต๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋“ค์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •
     public MvProductRankMonthly() {
     }
 
+    public void updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount) {
+        this.score = score;
+        this.likeCount = likeCount;
+        this.viewCount = viewCount;
+        this.salesCount = salesCount;
+    }
+
+    public void updateRanking(Integer ranking) {
+        this.ranking = ranking;
+    }
+
     public static MvProductRankMonthly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate,
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java
around lines 13 to 76, the entity is missing the updateMetrics(...) and
updateRanking(...) methods used by MonthlyRankingService; add a public method
updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount)
that updates the score, likeCount, viewCount and salesCount fields (and
optionally validates/null-checks inputs) and a public method
updateRanking(Integer ranking) that sets the ranking field; keep methods simple
mutators (void) and place them alongside the existing constructors/builders so
the service calls compile.


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

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

public interface MvProductRankMonthlyRepository {
List<MvProductRankMonthly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, int page, int size);
}

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

import com.loopers.domain.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;

@Entity
@Table(
name = "mv_product_rank_weekly",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"product_id", "period_start_date", "period_end_date"})
}
)
@Getter
public class MvProductRankWeekly extends BaseEntity {

@Column(name = "product_id", nullable = false)
private Long productId;

@Column(nullable = true)
private Integer ranking;

@Column(nullable = false)
private Double score;

@Column(name = "period_start_date", nullable = false)
private LocalDate periodStartDate;

@Column(name = "period_end_date", nullable = false)
private LocalDate periodEndDate;

@Column(name = "like_count", nullable = false)
private Long likeCount;

@Column(name = "view_count", nullable = false)
private Long viewCount;

@Column(name = "sales_count", nullable = false)
private Long salesCount;

@Builder
private MvProductRankWeekly(Long productId, Integer ranking, Double score, LocalDate periodStartDate, LocalDate periodEndDate,
Long likeCount, Long viewCount, Long salesCount) {
this.productId = productId;
this.ranking = ranking;
this.score = score;
this.periodStartDate = periodStartDate;
this.periodEndDate = periodEndDate;
this.likeCount = likeCount;
this.viewCount = viewCount;
this.salesCount = salesCount;
}
Comment on lines +47 to +58
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

๋ˆ„๋ฝ๋œ ๋ฉ”์„œ๋“œ: updateMetrics ๋ฐ updateRanking

WeeklyRankingService์—์„œ existing.updateMetrics(...) ๋ฐ rank.updateRanking(...)์„ ํ˜ธ์ถœํ•˜์ง€๋งŒ, ์ด ์—”ํ‹ฐํ‹ฐ์— ํ•ด๋‹น ๋ฉ”์„œ๋“œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ •์•ˆ
     public MvProductRankWeekly() {
     }

+    public void updateMetrics(Double score, Long likeCount, Long viewCount, Long salesCount) {
+        this.score = score;
+        this.likeCount = likeCount;
+        this.viewCount = viewCount;
+        this.salesCount = salesCount;
+    }
+
+    public void updateRanking(Integer ranking) {
+        this.ranking = ranking;
+    }
+
     public static MvProductRankWeekly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate,

Committable suggestion skipped: line range outside the PR's diff.


public MvProductRankWeekly() {
}

public static MvProductRankWeekly create(Long productId, Integer ranking, Double score, LocalDate periodStartDate,
LocalDate periodEndDate, Long likeCount, Long viewCount, Long salesCount) {
return MvProductRankWeekly.builder()
.productId(productId)
.ranking(ranking)
.score(score)
.periodStartDate(periodStartDate)
.periodEndDate(periodEndDate)
.likeCount(likeCount)
.viewCount(viewCount)
.salesCount(salesCount)
.build();
}
}

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

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

public interface MvProductRankWeeklyRepository {
List<MvProductRankWeekly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, int page, int size);
}

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

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.TemporalAdjusters;

public record RankingPeriod(
LocalDate startDate,
LocalDate endDate
) {
public static RankingPeriod ofWeek(LocalDate date) {
LocalDate weekStart = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
LocalDate weekEnd = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
return new RankingPeriod(weekStart, weekEnd);
}

public static RankingPeriod ofMonth(LocalDate date) {
YearMonth yearMonth = YearMonth.from(date);
LocalDate monthStart = yearMonth.atDay(1);
LocalDate monthEnd = yearMonth.atEndOfMonth();
return new RankingPeriod(monthStart, monthEnd);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
public class RankingService {

private final RankingCacheService rankingCacheService;
private final MvProductRankWeeklyRepository mvProductRankWeeklyRepository;
private final MvProductRankMonthlyRepository mvProductRankMonthlyRepository;

public List<Ranking> getRanking(LocalDate date, int page, int size) {
public List<Ranking> getDailyRanking(LocalDate date, int page, int size) {
long start = (long) (page - 1) * size;
long end = start + size - 1;

Expand All @@ -33,5 +35,53 @@ public List<Ranking> getRanking(LocalDate date, int page, int size) {
})
.toList();
}

public List<Ranking> getWeeklyRanking(LocalDate date, int page, int size) {
RankingPeriod period = RankingPeriod.ofWeek(date);
List<MvProductRankWeekly> weeklyRanks = mvProductRankWeeklyRepository
.findByPeriodOrderByRankingAsc(
period.startDate(),
period.endDate(),
page,
size
);

if (weeklyRanks.isEmpty()) {
return new ArrayList<>();
}

return weeklyRanks.stream()
.filter(rank -> rank.getRanking() != null)
.map(rank -> new Ranking(
rank.getProductId(),
rank.getRanking().longValue(),
rank.getScore()
))
.toList();
}

public List<Ranking> getMonthlyRanking(LocalDate date, int page, int size) {
RankingPeriod period = RankingPeriod.ofMonth(date);
List<MvProductRankMonthly> monthlyRanks = mvProductRankMonthlyRepository
.findByPeriodOrderByRankingAsc(
period.startDate(),
period.endDate(),
page,
size
);

if (monthlyRanks.isEmpty()) {
return new ArrayList<>();
}

return monthlyRanks.stream()
.filter(rank -> rank.getRanking() != null)
.map(rank -> new Ranking(
rank.getProductId(),
rank.getRanking().longValue(),
rank.getScore()
))
.toList();
}
}

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

import com.loopers.domain.ranking.MvProductRankMonthly;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

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

public interface MvProductRankMonthlyJpaRepository extends JpaRepository<MvProductRankMonthly, Long> {

List<MvProductRankMonthly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc(
LocalDate periodStartDate,
LocalDate periodEndDate,
Pageable pageable
);

}
Comment on lines +10 to +18
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Find the Weekly repository interface
fd -t f "MvProductRankWeeklyJpaRepository" --type=file

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 299


๐Ÿ Script executed:

# Check the Monthly repository file mentioned in the review
cat "apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java"

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 660


๐Ÿ Script executed:

# Search for all methods in the Weekly repository
rg "findByProductId" --type=java -A 5 | head -50

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 7308


๐Ÿ Script executed:

# Check for usage of Monthly repository's methods
rg "MvProductRankMonthlyJpaRepository" --type=java -A 3

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 3870


๐Ÿ Script executed:

cat "apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyRepositoryImpl.java"

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1087


findByProductIdAndPeriodStartDateAndPeriodEndDate ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ํ•„์š”

์ด Monthly ์ €์žฅ์†Œ์—์„œ findByProductIdAndPeriodStartDateAndPeriodEndDate ๋ฉ”์„œ๋“œ๊ฐ€ ๋ˆ„๋ฝ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Weekly ์ €์žฅ์†Œ์—๋Š” ์žˆ์œผ๋ฉฐ, ๋™์ผํ•œ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ batch ๋ชจ๋“ˆ์˜ MvProductRankMonthlyJpaRepository์—๋„ ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“ˆ ๊ฐ„ ์ธํ„ฐํŽ˜์ด์Šค ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”:

Optional<MvProductRankMonthly> findByProductIdAndPeriodStartDateAndPeriodEndDate(
        Long productId,
        LocalDate periodStartDate,
        LocalDate periodEndDate
);
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java
around lines 10 to 18, the repository is missing the method
findByProductIdAndPeriodStartDateAndPeriodEndDate required for consistency with
the weekly repository and the batch module; add the following method signature
to the interface: Optional<MvProductRankMonthly>
findByProductIdAndPeriodStartDateAndPeriodEndDate(Long productId, LocalDate
periodStartDate, LocalDate periodEndDate); ensuring imports for Optional and
LocalDate are available and the method follows the existing repository style.


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

import com.loopers.domain.ranking.MvProductRankMonthly;
import com.loopers.domain.ranking.MvProductRankMonthlyRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

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

@Repository
@RequiredArgsConstructor
public class MvProductRankMonthlyRepositoryImpl implements MvProductRankMonthlyRepository {

private final MvProductRankMonthlyJpaRepository mvProductRankMonthlyJpaRepository;

@Override
public List<MvProductRankMonthly> findByPeriodOrderByRankingAsc(LocalDate periodStartDate, LocalDate periodEndDate, int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
return mvProductRankMonthlyJpaRepository.findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc(periodStartDate, periodEndDate, pageable);
}
}

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

import com.loopers.domain.ranking.MvProductRankWeekly;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

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

public interface MvProductRankWeeklyJpaRepository extends JpaRepository<MvProductRankWeekly, Long> {

List<MvProductRankWeekly> findByPeriodStartDateAndPeriodEndDateOrderByRankingAsc(
LocalDate periodStartDate,
LocalDate periodEndDate,
Pageable pageable
);
}

Loading