Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -30,6 +30,7 @@ public List<RankingInfo> getRankings(String date, int page, int size) {
return getRankings(date, page, size, RankingType.DAILY);
}

@Transactional(readOnly = true)
public List<RankingInfo> getRankings(String date, int page, int size, RankingType type) {
long offset = (long) (page - 1) * size;
List<RankedProduct> rankedProducts = productRankingRepository.getTopProducts(date, offset, size, type);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.loopers.domain.ranking;

import java.time.LocalDate;

public record DailyMetricSnapshot(
LocalDate date,
long viewCount,
long likesCount,
long salesCount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ public static String ofHour(LocalDateTime dateTime) {
public static String currentHour() {
return ofHour(LocalDateTime.now());
}

public static String defaultKey(RankingType type) {
return switch (type) {
case DAILY, WEEKLY, MONTHLY -> today();
case HOURLY -> currentHour();
};
}
}
15 changes: 15 additions & 0 deletions domain/src/main/java/com/loopers/domain/ranking/RankingScore.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.loopers.domain.ranking;

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

public final class RankingScore {

private static final double VIEW_WEIGHT = 1.0;
private static final double LIKE_WEIGHT = 3.0;
private static final double SALE_WEIGHT = 10.0;
private static final double DECAY_RATE = 0.85;

private RankingScore() {
}
Expand All @@ -30,4 +35,14 @@ public static double calculateDaily(long viewCount, long likesCount, long salesC
+ LIKE_WEIGHT * Math.log10(likesCount + 1)
+ SALE_WEIGHT * Math.log10(salesCount + 1);
}

public static double calculateWithDecay(List<DailyMetricSnapshot> snapshots, LocalDate baseDate) {
double total = 0.0;
for (DailyMetricSnapshot snapshot : snapshots) {
long daysAgo = ChronoUnit.DAYS.between(snapshot.date(), baseDate);
double dailyScore = calculateDaily(snapshot.viewCount(), snapshot.likesCount(), snapshot.salesCount());
total += dailyScore * Math.pow(DECAY_RATE, daysAgo);
}
return total;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
public enum RankingType {

DAILY("ranking:daily:", 2 * 24 * 60 * 60),
HOURLY("ranking:hourly:", 24 * 60 * 60);
HOURLY("ranking:hourly:", 24 * 60 * 60),
WEEKLY("ranking:weekly:", 8 * 24 * 60 * 60),
MONTHLY("ranking:monthly:", 32 * 24 * 60 * 60);

private final String keyPrefix;
private final long ttlSeconds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;

class RankingScoreTest {
Expand Down Expand Up @@ -65,4 +68,48 @@ class RankingScoreTest {
// then
assertThat(salesOnly).isGreaterThan(viewsOnly);
}

@Test
void ์ฃผ๊ฐ„_์ ์ˆ˜๋Š”_์ตœ๊ทผ_๋‚ ์งœ์—_๋†’์€_๊ฐ€์ค‘์น˜๋ฅผ_์ ์šฉํ•œ๋‹ค() {
// given
LocalDate today = LocalDate.of(2026, 4, 16);
var recentDay = new DailyMetricSnapshot(today.minusDays(1), 100, 10, 5);
var oldDay = new DailyMetricSnapshot(today.minusDays(7), 100, 10, 5);

// when
double recentScore = RankingScore.calculateWithDecay(List.of(recentDay), today);
double oldScore = RankingScore.calculateWithDecay(List.of(oldDay), today);

// then
assertThat(recentScore).isGreaterThan(oldScore);
}

@Test
void ์ฃผ๊ฐ„_์ ์ˆ˜๋Š”_์—ฌ๋Ÿฌ_๋‚ ์˜_๊ฐ์‡ _์ ์ˆ˜๋ฅผ_ํ•ฉ์‚ฐํ•œ๋‹ค() {
// given
LocalDate today = LocalDate.of(2026, 4, 16);
var day1 = new DailyMetricSnapshot(today.minusDays(1), 100, 10, 5);
var day2 = new DailyMetricSnapshot(today.minusDays(2), 200, 20, 10);

double score1 = RankingScore.calculateWithDecay(List.of(day1), today);
double score2 = RankingScore.calculateWithDecay(List.of(day2), today);

// when
double combined = RankingScore.calculateWithDecay(List.of(day1, day2), today);

// then
assertThat(combined).isEqualTo(score1 + score2);
}
Comment on lines +87 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

๋ถ€๋™์†Œ์ˆ˜์  ๋™๋“ฑ ๋น„๊ต๋Š” ํ”Œ๋ž˜ํ‚ค ์œ„ํ—˜์ด ์žˆ์–ด ์˜คํ”„์…‹ ๋น„๊ต๋กœ ์ „ํ™˜ํ•ด์•ผ ํ•œ๋‹ค.

calculateWithDecay๊ฐ€ ๋‚ด๋ถ€์—์„œ Math.pow(0.85, daysAgo)์™€ log10 ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ํ•ฉ์‚ฐ ์ˆœ์„œ์™€ ์ตœ์ ํ™”์— ๋”ฐ๋ผ combined๊ณผ score1 + score2๊ฐ€ ๋น„ํŠธ ๋‹จ์œ„๋กœ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ์šด์˜ ๊ด€์ ์—์„œ CI์—์„œ ๊ฐ„ํ—์  ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ฐฐ์น˜/๋„๋ฉ”์ธ ํšŒ๊ท€ ๊ฐ์ง€ ์‹ ๋ขฐ๋„๊ฐ€ ๋–จ์–ด์ง„๋‹ค. isCloseTo(expected, within(1e-9)) ๋กœ ์ „ํ™˜์„ ๊ถŒ๊ณ ํ•œ๋‹ค.

์ œ์•ˆ ์ˆ˜์ •์•ˆ
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
@@
-        assertThat(combined).isEqualTo(score1 + score2);
+        assertThat(combined).isCloseTo(score1 + score2, within(1e-9));

์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ์ œ์•ˆ: (1) daysAgo == 0(today ๋ณธ์ธ)์ผ ๋•Œ ๊ฐ์‡  ๊ณ„์ˆ˜ 1.0์ด ์ ์šฉ๋˜๋Š”์ง€, (2) ์œˆ๋„๋ณด๋‹ค ์˜ค๋ž˜๋œ ๋‚ ์งœ๊ฐ€ ์„ž์˜€์„ ๋•Œ์˜ ๊ธฐ๋Œ€ ๋™์ž‘(์ œ์™ธ vs ํฌํ•จ), (3) date๊ฐ€ today๋ณด๋‹ค ๋ฏธ๋ž˜์ธ ๊ฒฝ์šฐ์˜ ์ •์ฑ…์„ ๊ฐ๊ฐ ๋ช…์‹œํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฐฐ์น˜ ๊ฒฝ๊ณ„ ๋ฒ„๊ทธ๋ฅผ ์‚ฌ์ „์— ๋“œ๋Ÿฌ๋‚ธ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/src/test/java/com/loopers/domain/ranking/RankingScoreTest.java` around
lines 87 - 102, Change the equality assertion in the test
'์ฃผ๊ฐ„_์ ์ˆ˜๋Š”_์—ฌ๋Ÿฌ_๋‚ ์˜_๊ฐ์‡ _์ ์ˆ˜๋ฅผ_ํ•ฉ์‚ฐํ•œ๋‹ค' to use an offset comparison to avoid flaky
floating-point equality: replace the exact equals check on 'combined' vs 'score1
+ score2' with an assertion that they are close within a small tolerance (e.g.
isCloseTo(..., within(1e-9))). Keep the rest of the setup that calls
RankingScore.calculateWithDecay(List<DailyMetricSnapshot>, today) for 'day1' and
'day2' unchanged.


@Test
void ๋นˆ_์Šค๋ƒ…์ƒท์ด๋ฉด_์ ์ˆ˜๋Š”_0์ด๋‹ค() {
// given
LocalDate today = LocalDate.of(2026, 4, 16);

// when
double score = RankingScore.calculateWithDecay(List.of(), today);

// then
assertThat(score).isEqualTo(0.0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface ProductMetricsDailyRepository extends JpaRepository<ProductMetr
Optional<ProductMetricsDaily> findByProductIdAndDate(Long productId, LocalDate date);

List<ProductMetricsDaily> findByDate(LocalDate date);

List<ProductMetricsDaily> findByDateBetween(LocalDate startDate, LocalDate endDate);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
rg -nP -C3 'findByDateBetween\s*\('

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2883


๐Ÿ Script executed:

# Verify the exact startDate and endDate calculations in the callers
cat -n presentation/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java | sed -n '100,115p'

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 973


๐Ÿ Script executed:

# Check MonthlyRankingJobConfig for the same pattern
cat -n presentation/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java | sed -n '100,115p'

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 976


๐Ÿ Script executed:

# Find the ProductMetricsDaily entity definition to check for indexing
fd -t f 'ProductMetricsDaily\.java' -x cat -n {}

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2154


๐Ÿ Script executed:

# Check if there are any index annotations or SQL `@Index` definitions
rg -n '@Index|@Column.*index' infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDaily.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


๐Ÿ Script executed:

# Check for any streaming or pagination-based approaches in batch jobs
rg -n 'Stream|Page|Pageable' presentation/commerce-batch/src/main/java/com/loopers/batch/job/ranking/

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


๐Ÿ Script executed:

# Find WINDOW_DAYS constant values
rg -n 'WINDOW_DAYS\s*=' presentation/commerce-batch/src/main/java/com/loopers/batch/job/ranking/

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 384


๐Ÿ Script executed:

# Check for database migration scripts/schema definitions
fd -t f -e 'sql' -e 'yml' -e 'yaml' | grep -i migration

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


๐Ÿ Script executed:

# Search for any index definitions in migration or schema files
rg -n 'CREATE.*INDEX.*product_metrics_daily|product_metrics_daily.*INDEX' --type sql --type yaml

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


๐Ÿ Script executed:

# Check the complete ProductMetricsDailyRepository interface
cat -n infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepository.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 723


findByDateBetween ์„ฑ๋Šฅ๊ณผ ์ธ๋ฑ์‹ฑ ์ตœ์ ํ™”๋ฅผ ์ ๊ฒ€ํ•œ๋‹ค.

์šด์˜ ๊ด€์ ์—์„œ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋ฅผ ๊ฐœ์„ ํ•ด์•ผ ํ•œ๋‹ค.

  1. date ์ปฌ๋Ÿผ ์ธ๋ฑ์Šค ์ถ”๊ฐ€: ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ์˜ date ์ปฌ๋Ÿผ์— ๋ช…์‹œ์  ์ธ๋ฑ์Šค๊ฐ€ ์—†๋‹ค. ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ฐฐ์น˜์—์„œ BETWEEN ๋ฒ”์œ„ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ํ’€ ํ…Œ์ด๋ธ” ์Šค์บ”์ด ๋ฐœ์ƒํ•œ๋‹ค. @Index(name = "idx_product_metrics_daily_date", columnList = "date") ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์—์„œ ์ธ๋ฑ์Šค๋ฅผ ์ •์˜ํ•˜๋„๋ก ๊ถŒ๊ณ ํ•œ๋‹ค.

  2. ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ ๊ฐœ์„ : ์›”๊ฐ„ 30์ผ ๋ฐ์ดํ„ฐ๋ฅผ List<ProductMetricsDaily> ํ˜•ํƒœ๋กœ ํ•œ ๋ฒˆ์— ๋กœ๋”ฉํ•˜๋ฉด ์ƒํ’ˆ ์ˆ˜๊ฐ€ ๋งŽ์„ ๊ฒฝ์šฐ ๋ฐฐ์น˜ JVM ํž™ ์ŠคํŒŒ์ดํฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ์ถ”ํ›„ Stream<ProductMetricsDaily> ๋˜๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธฐ๋ฐ˜ ์ฝ๊ธฐ๋กœ์˜ ์ „ํ™˜์„ ๊ณ ๋ คํ•˜๋„๋ก ๊ถŒ๊ณ ํ•œ๋‹ค.

์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ์ œ์•ˆ: ๊ฒฝ๊ณ„๊ฐ’(startDate == endDate, startDate > endDate) ๋ฐ ๋นˆ ๊ฒฐ๊ณผ ์ผ€์ด์Šค๋ฅผ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ์— ํฌํ•จํ•œ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDailyRepository.java`
at line 15, ProductMetricsDailyRepository์˜ findByDateBetween ๋ฉ”์„œ๋“œ๋Š” date ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๊ฐ€
์—†์–ด ๋ฒ”์œ„์กฐํšŒ ์‹œ ํ’€ํ…Œ์ด๋ธ”์Šค์บ”์„ ์œ ๋ฐœํ•˜๋ฏ€๋กœ ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ์— `@Index`(name =
"idx_product_metrics_daily_date", columnList = "date")๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ํ•ด๋‹น ์ธ๋ฑ์Šค๋ฅผ
์ƒ์„ฑํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜๊ณ , ๋Œ€๋Ÿ‰ ๋ฐ˜ํ™˜์œผ๋กœ ์ธํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ˜„์žฌ List<ProductMetricsDaily> ๋ฐ˜ํ™˜์„
Stream<ProductMetricsDaily> ๋˜๋Š” Spring Data์˜ Pageable ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• API๋กœ ์ „ํ™˜(์˜ˆ:
Stream<ProductMetricsDaily> findByDateBetween(...) ๋˜๋Š” Page<ProductMetricsDaily>
findByDateBetween(..., Pageable))ํ•˜๋Š” ๋ฆฌํŒฉํ„ฐ๋ง์„ ์ ์šฉํ•˜๋ฉฐ, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ์— ๊ฒฝ๊ณ„๊ฐ’(startDate ==
endDate, startDate > endDate)๊ณผ ๋นˆ ๊ฒฐ๊ณผ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด ๊ฒ€์ฆํ•˜์„ธ์š”.

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

import com.loopers.domain.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Entity
@Table(name = "mv_product_rank_monthly",
uniqueConstraints = @UniqueConstraint(columnNames = {"productId", "calculatedDate"}),
indexes = @Index(name = "idx_monthly_calculated_date", columnList = "calculatedDate"))
Comment on lines +16 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# 1) ์‹ค์ œ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜/์Šคํ‚ค๋งˆ ํŒŒ์ผ์—์„œ mv_product_rank_monthly ์ •์˜ ๋ฐ ์ปฌ๋Ÿผ๋ช… ํ™•์ธ
fd -e sql -e xml -e yaml -e yml | xargs rg -nP -C2 'mv_product_rank_(weekly|monthly)' 2>/dev/null

# 2) ๋„ค์ด๋ฐ ์ „๋žต ์„ค์ • ํ™•์ธ (physical-strategy / implicit-strategy)
rg -nP 'physical-strategy|implicit-strategy|PhysicalNamingStrategy|ImplicitNamingStrategy' -g '*.yml' -g '*.yaml' -g '*.properties' -g '*.java'

# 3) ๊ธฐ์กด MV ๋˜๋Š” ์œ ์‚ฌ ์—”ํ‹ฐํ‹ฐ์—์„œ uniqueConstraints/indexes ์ปฌ๋Ÿผ๋ช…์ด ์–ด๋–ป๊ฒŒ ์“ฐ์˜€๋Š”์ง€ ์‚ฌ๋ก€ ๋น„๊ต
ast-grep --pattern '@Table(name = $_, uniqueConstraints = $$$)'
rg -nP '@UniqueConstraint\(columnNames' -g '*.java' -C1
rg -nP '@Index\([^)]*columnList' -g '*.java' -C1

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 3773


๐Ÿ Script executed:

# 1) MvProductRankMonthly ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ ์ •์˜ ๋ฐ `@Column` ์–ด๋…ธํ…Œ์ด์…˜ ํ™•์ธ
cat -n infrastructure/jpa/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthly.java

# 2) MvProductRankWeekly ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ ์ •์˜ ํ™•์ธ
cat -n infrastructure/jpa/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeekly.java

# 3) DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์—์„œ mv_product_rank_monthly ํ…Œ์ด๋ธ” ์ •์˜ ๊ฒ€์ƒ‰
find . -type f \( -name "*.sql" -o -name "*.xml" \) | xargs grep -l "mv_product_rank_monthly" 2>/dev/null | head -5

# 4) ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋‚ด์šฉ ํ™•์ธ (์ปฌ๋Ÿผ๋ช… ํŒŒ์•…)
find . -type f -name "*.sql" | xargs grep -A 10 "CREATE.*mv_product_rank_monthly" 2>/dev/null

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 4188


๐Ÿ Script executed:

# DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์œ„์น˜ ๋ฐ ํ˜•์‹ ํŒŒ์•…
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*schema*" \) | head -20

# mv_product_rank ๊ด€๋ จ ํŒŒ์ผ ๊ฒ€์ƒ‰
find . -type f -name "*.sql" | xargs grep -l "mv_product_rank" 2>/dev/null

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ํ™•์ธ
find . -type d -name "*migration*" -o -name "*flyway*" -o -name "*liquibase*" 2>/dev/null | head -10

# ProductMetricsHourly, ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ๋„ ๋™์ผ ํŒจํ„ด์ธ์ง€ ํ™•์ธ (ํ•„๋“œ๋ช…๊ณผ `@Column`)
sed -n '15,40p' infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsHourly.java
sed -n '15,40p' infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDaily.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1558


๐Ÿ Script executed:

# constraint.sql ์ „์ฒด ๋‚ด์šฉ
cat docs/sql/constraint.sql

# index.sql ์ „์ฒด ๋‚ด์šฉ
cat docs/sql/index.sql

# mv_product_rank_monthly ๊ด€๋ จ ๋ถ€๋ถ„๋งŒ ์ถ”์ถœ
grep -A 5 -B 5 "mv_product_rank_monthly" docs/sql/constraint.sql docs/sql/index.sql 2>/dev/null

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1407


๐Ÿ Script executed:

# constraint.sql, index.sql ์ „์ฒด ๋ผ์ธ ์ˆ˜ ํ™•์ธ
wc -l docs/sql/constraint.sql docs/sql/index.sql

# ๋ชจ๋“  SQL ํŒŒ์ผ์—์„œ mv_product_rank ๊ฒ€์ƒ‰
find . -type f -name "*.sql" -exec grep -l "mv_product_rank" {} \;

# ๋ชจ๋“  SQL ํŒŒ์ผ ๋ชฉ๋ก ๋ฐ ๋ผ์ธ ์ˆ˜
find . -type f -name "*.sql" | xargs wc -l | sort -n

# ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๊ด€๋ จ SQL ํŒŒ์ผ ์ฐพ๊ธฐ
find . -type f -name "*.sql" | xargs grep -l "CREATE TABLE" | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 241


๐Ÿ Script executed:

# seed.sql๋„ ํ™•์ธ (ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๊ฐ€๋Šฅ์„ฑ)
cat docs/sql/seed.sql

# ProductMetricsHourly ์ „์ฒด ์ฝ”๋“œ
cat infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsHourly.java

# ProductMetricsDaily ์ „์ฒด ์ฝ”๋“œ  
cat infrastructure/jpa/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsDaily.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 5510


๐Ÿ Script executed:

# application ์„ค์ • ํŒŒ์ผ ์ฐพ๊ธฐ
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \)

# ๋„ค์ด๋ฐ ์ „๋žต ์„ค์ • ํ™•์ธ
rg -nP 'physical-naming-strategy|implicit-naming-strategy|hibernate.*naming|ddl-auto' -g '*.yml' -g '*.yaml' -g '*.properties'

# application.yml ๋‚ด์šฉ (์žˆ์œผ๋ฉด ์ „์ฒด ํ™•์ธ)
find . -type f -name "application*.yml" -o -name "application*.yaml" | head -1 | xargs cat 2>/dev/null

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1625


๐Ÿ Script executed:

# jpa.yml ์ „์ฒด ๋‚ด์šฉ
cat infrastructure/jpa/src/main/resources/jpa.yml

# ๋„ค์ด๋ฐ ์ „๋žต ๊ด€๋ จ ๋” ๊ตฌ์ฒด์  ๊ฒ€์ƒ‰
rg -nP 'naming-strategy|PhysicalNamingStrategy|ImplicitNamingStrategy' --type java --type yml

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2277


@UniqueConstraint/indexes ์ปฌ๋Ÿผ๋ช…์ด ์‹ค์ œ DB ์ปฌ๋Ÿผ๋ช…๊ณผ ๋ถˆ์ผ์น˜ํ•œ๋‹ค.

Spring Boot ๊ธฐ๋ณธ ๋„ค์ด๋ฐ ์ „๋žต(SpringPhysicalNamingStrategy)์€ camelCase ํ•„๋“œ๋ฅผ snake_case ์ปฌ๋Ÿผ์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•œ๋‹ค. ๋ณธ ์—”ํ‹ฐํ‹ฐ์˜ productId, calculatedDate ํ•„๋“œ๋Š” ์‹ค์ œ ํ…Œ์ด๋ธ”์—์„œ product_id, calculated_date๋กœ ์ƒ์„ฑ๋˜์ง€๋งŒ, @UniqueConstraint์™€ @Index ์–ด๋…ธํ…Œ์ด์…˜์€ camelCase๋ฅผ ์ง์ ‘ ์ง€์ •ํ•˜๊ณ  ์žˆ๋‹ค. ์ด๋กœ ์ธํ•ด JPA ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ์‹ค์ œ ์Šคํ‚ค๋งˆ ๊ฐ„ ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ, unique constraint์™€ index ์„ ์–ธ์ด ์‹คํšจ์„ฑ์„ ์žƒ๊ฒŒ ๋œ๋‹ค.

ProductMetricsHourly, ProductMetricsDaily ์—”ํ‹ฐํ‹ฐ๋„ ๋™์ผํ•œ ํŒจํ„ด์ด๋ฏ€๋กœ ํ•จ๊ป˜ ์ˆ˜์ •์ด ํ•„์š”ํ•˜๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•
 `@Entity`
 `@Table`(name = "mv_product_rank_monthly",
-        uniqueConstraints = `@UniqueConstraint`(columnNames = {"productId", "calculatedDate"}),
-        indexes = `@Index`(name = "idx_monthly_calculated_date", columnList = "calculatedDate"))
+        uniqueConstraints = `@UniqueConstraint`(columnNames = {"product_id", "calculated_date"}),
+        indexes = `@Index`(name = "idx_monthly_calculated_date", columnList = "product_id", "calculated_date"))

๋˜๋Š” ๋ชจ๋“  ํ•„๋“œ์— @Column(name = "snake_case")๋ฅผ ๋ช…์‹œํ•˜์—ฌ ์ปฌ๋Ÿผ๋ช…์„ ๊ณ ์ •ํ•œ๋‹ค.

๋™์ผ ๋ฌธ์ œ๊ฐ€ ProductMetricsHourly(productId, hour)์™€ ProductMetricsDaily(productId, date)์—๋„ ์กด์žฌํ•œ๋‹ค. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ค‘๋ณต ํ‚ค ์‚ฝ์ž… ์‹œ DataIntegrityViolationException ๋ฐœ์ƒ์„ ๊ฒ€์ฆํ•˜๋„๋ก ๊ถŒ๊ณ ํ•œ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@infrastructure/jpa/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthly.java`
around lines 16 - 18, Update the JPA annotations so DB column names match the
physical naming strategy: in MvProductRankMonthly change the `@UniqueConstraint`
and `@Index` columnNames/columnList from "productId"/"calculatedDate" to the
actual snake_case names "product_id" and "calculated_date" (or alternatively add
`@Column`(name = "...") on the entity fields productId and calculatedDate to fix
the mapping); make the same fixes in ProductMetricsHourly (productId, hour ->
"product_id", "hour") and ProductMetricsDaily (productId, date -> "product_id",
"date") by aligning `@UniqueConstraint/`@Index or adding `@Column`(name=...) on
those fields; finally add an integration test that inserts duplicate keys for
each entity and asserts a DataIntegrityViolationException is thrown to validate
the unique constraints.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MvProductRankMonthly extends BaseTimeEntity {

@Column(nullable = false)
private Long productId;

@Column(nullable = false)
private long viewCount;

@Column(nullable = false)
private long likesCount;

@Column(nullable = false)
private long salesCount;

@Column(nullable = false)
private LocalDate calculatedDate;

private MvProductRankMonthly(Long productId, long viewCount, long likesCount,
long salesCount, LocalDate calculatedDate) {
this.productId = productId;
this.viewCount = viewCount;
this.likesCount = likesCount;
this.salesCount = salesCount;
this.calculatedDate = calculatedDate;
}

public static MvProductRankMonthly of(Long productId, long viewCount, long likesCount,
long salesCount, LocalDate calculatedDate) {
return new MvProductRankMonthly(productId, viewCount, likesCount, salesCount, calculatedDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.loopers.infrastructure.ranking;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

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

public interface MvProductRankMonthlyRepository extends JpaRepository<MvProductRankMonthly, Long> {

List<MvProductRankMonthly> findByCalculatedDate(LocalDate calculatedDate);

@Modifying
@Query("DELETE FROM MvProductRankMonthly m WHERE m.calculatedDate = :calculatedDate")
void deleteByCalculatedDate(LocalDate calculatedDate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.loopers.infrastructure.ranking;

import com.loopers.domain.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Entity
@Table(name = "mv_product_rank_weekly",
uniqueConstraints = @UniqueConstraint(columnNames = {"productId", "calculatedDate"}),
indexes = @Index(name = "idx_weekly_calculated_date", columnList = "calculatedDate"))
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MvProductRankWeekly extends BaseTimeEntity {

@Column(nullable = false)
private Long productId;

@Column(nullable = false)
private long viewCount;

@Column(nullable = false)
private long likesCount;

@Column(nullable = false)
private long salesCount;

@Column(nullable = false)
private LocalDate calculatedDate;

private MvProductRankWeekly(Long productId, long viewCount, long likesCount,
long salesCount, LocalDate calculatedDate) {
this.productId = productId;
this.viewCount = viewCount;
this.likesCount = likesCount;
this.salesCount = salesCount;
this.calculatedDate = calculatedDate;
}

public static MvProductRankWeekly of(Long productId, long viewCount, long likesCount,
long salesCount, LocalDate calculatedDate) {
return new MvProductRankWeekly(productId, viewCount, likesCount, salesCount, calculatedDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.loopers.infrastructure.ranking;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

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

public interface MvProductRankWeeklyRepository extends JpaRepository<MvProductRankWeekly, Long> {

List<MvProductRankWeekly> findByCalculatedDate(LocalDate calculatedDate);

@Modifying
@Query("DELETE FROM MvProductRankWeekly m WHERE m.calculatedDate = :calculatedDate")
void deleteByCalculatedDate(LocalDate calculatedDate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
api.version=1.44
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
api.version=1.44
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@ public List<RankingApiResponse> getRankings(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "DAILY") RankingType type
) {
String targetDate = date != null ? date : defaultDateKey(type);
return rankingService.getRankings(targetDate, page, size, type).stream()
if (date == null) {
date = RankingDateKey.defaultKey(type);
}
return rankingService.getRankings(date, page, size, type).stream()
.map(RankingApiResponse::from)
.toList();
}

private String defaultDateKey(RankingType type) {
return switch (type) {
case DAILY -> RankingDateKey.today();
case HOURLY -> RankingDateKey.currentHour();
};
}
}
Loading