Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8181ea4
docs: 과제 문서, 학습 로드맵 추가
SukheeChoi Apr 14, 2026
f0d3df0
docs: 배치 어플리케이션 분석 보고서 — MV Score 전략 확정
Apr 15, 2026
9c1764c
docs: 배치 코드 참고 스니펫 — 구현 시 패턴 레퍼런스
Apr 16, 2026
75de902
docs: 배치 코드 참고 스니펫 보강 + MV 랭킹 시스템 설계 문서
Apr 16, 2026
f6de522
docs: MV 랭킹 설계 — 슬라이딩 윈도우 확정, 지수 감쇠 분석, 요구사항 대조
Apr 16, 2026
65183a8
docs: 테크니컬 라이팅 소재 모음 — Score 전략 트레이드오프 5개 주제
Apr 16, 2026
739d492
docs: 테크니컬 라이팅 소재 — 실운영 관점으로 판단 근거 재작성
Apr 16, 2026
b1f4b16
docs: 블로그 소재 — Score/TOP-N을 SQL에서 처리하는 판단 근거
Apr 16, 2026
458c4de
docs: 블로그 소재 — Tasklet 전환 시 트레이드오프 분석
Apr 16, 2026
6b5eb12
docs: 블로그 소재 — Chunk vs Tasklet 본질 분석, 출제 의도 해석
Apr 16, 2026
016aff4
docs: Chunk vs Tasklet 트레이드오프 전면 재작성 + 설계 문서 갱신
Apr 16, 2026
3753d9c
docs: 블로그 소재 — Chunk/Tasklet 코드 예시 + 배치 운영 기능 분석
Apr 16, 2026
1c3b219
docs: Best Practice 대조 분석 + 설계 문서 운영 기능 반영
Apr 16, 2026
39fcaec
docs: CursorReader vs PagingReader 트레이드오프 분석
Apr 16, 2026
05635d6
docs: Partitioning 기반 대규모 집계 설계 확정
Apr 16, 2026
5e30b71
docs: 멱등성 시나리오 + Job Instance 동일성 설계 확정
Apr 16, 2026
ac79004
docs: 블로그 소재 전체에 '이 고민이 시작된 맥락' 도입부 추가
Apr 16, 2026
c814ce7
docs: Redis fallback 제거, MV 단일 소스 원칙 확정
Apr 16, 2026
4c9fbef
docs: 전체 재계산 vs 증분 계산 트레이드오프 + 전일 MV fallback 설계
Apr 16, 2026
1dfb514
feat: MV 랭킹 배치 Job 구현 — Partitioning + Map-Reduce 3-Step 구조
Apr 16, 2026
1babb5b
feat: MV 기반 주간/월간 랭킹 API 확장 — 단일 소스 + 전일 fallback
Apr 16, 2026
ea6d7d5
test: MV 랭킹 Job 통합 테스트 — 정상/멱등성/엣지케이스/취소 반영
Apr 16, 2026
fa7d840
docs: Phase 상태 갱신 + 다른 세션용 실행 가이드/PR/블로그 구조 추가
Apr 16, 2026
00bfc99
test: MV 랭킹 Job E2E 테스트 보강 및 실환경 검증 문서화
SukheeChoi Apr 16, 2026
00e5457
fix: Partitioner를 실제 행 수 기반 분할로 변경 — 데이터 skew 대응
Apr 17, 2026
675cf8b
docs: PR 초안 수정 + 10만 건 대규모 테스트 프롬프트 + 블로그 참고자료 정리
Apr 17, 2026
29fcf39
docs: Round 10 테크니컬 라이팅 기획 + 블로그 초안 작성
SukheeChoi Apr 16, 2026
2777e4a
test: 10만 상품 × 30일 메트릭 대규모 배치 성능 테스트 추가
SukheeChoi Apr 17, 2026
b2afd2f
docs: 배치 앱 분석 문서 production 브랜치 기준으로 갱신
Apr 17, 2026
1df0403
refactor: ProductRankingMvJobConfig 코드 정리
Apr 17, 2026
a352a78
docs: Partitioning 벤치마크 프롬프트 + PR 테스트 시나리오 갱신
Apr 17, 2026
94a1110
docs: PR 초안 양식 재구성 — Summary/Context/Design/Flow 구조
Apr 17, 2026
7ad094d
docs: PR 선택지 순서 변경 + Score 계산 방식 설명 보강
Apr 17, 2026
b65e020
test: Partitioning 벤치마크 추가 — gridSize=1(3,740ms) vs gridSize=4(1,763m…
SukheeChoi Apr 17, 2026
2565bc0
docs: 블로그 성능 섹션을 10만 건 대규모 테스트 기준으로 갱신
SukheeChoi Apr 17, 2026
4fd5255
docs: 성능 비교표 규모 네이밍 정리 — 소규모/대규모 2단계로 간소화
SukheeChoi Apr 17, 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 @@ -3,12 +3,15 @@
import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.domain.product.ProductWithBrand;
import com.loopers.domain.ranking.MvProductRank;
import com.loopers.domain.ranking.MvProductRankRepository;
import com.loopers.infrastructure.ranking.RankingRedisRepository;
import com.loopers.interfaces.api.ranking.RankingDto;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -31,22 +34,78 @@ public class RankingFacade {
private static final int MAX_RANKING_SIZE = 100;

private static final String DAILY_ZSET_PREFIX = "ranking:all:";
private static final String WEEKLY_ZSET_PREFIX = "ranking:weekly:";
private static final String MONTHLY_ZSET_PREFIX = "ranking:monthly:";

private final RankingRedisRepository rankingRedisRepository;
private final MvProductRankRepository mvProductRankRepository;
private final ProductRepository productRepository;
private final RankingProperties properties;

public RankingDto.PagedRankingResponse getRankings(String scope, String date, int page, int size, Long memberId) {
String resolvedDate = (date != null) ? date : LocalDate.now(KST).format(DATE_FORMATTER);
String prefix = resolveZsetPrefix(scope, memberId);

return switch (scope) {
case "weekly", "monthly" -> getFromMv(scope, resolvedDate, page, size);
default -> getFromRedis(scope, resolvedDate, page, size, memberId);
};
}
Comment on lines 43 to +50
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

scope가 null로 들어올 때 NPE가 발생한다.

switch (scope)에서 scope가 null이면 NullPointerException이 발생해 500으로 떨어진다. 컨트롤러 계층에서 필터링되고 있더라도, 이 Facade는 memberId/date에 대해서는 null을 방어적으로 처리하는 반면 scope만 방어가 없어 일관성이 떨어진다.

🛡️ 제안 수정
-        return switch (scope) {
+        String resolvedScope = scope == null ? "" : scope;
+        return switch (resolvedScope) {
             case "weekly", "monthly" -> getFromMv(scope, resolvedDate, page, size);
             default -> getFromRedis(scope, resolvedDate, page, size, memberId);
         };

혹은 ScopeType enum을 도입하여 상위 계층에서 검증하도록 통일하는 편이 낫다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java`
around lines 43 - 50, getRankings currently does a switch(scope) which throws
NPE when scope is null; guard against null before the switch in
RankingFacade.getRankings by resolving scope to a safe value or validating it
(e.g., if scope == null -> use a default like "default" or call the same
fallback path as the switch default) and then perform the switch, or explicitly
throw a clear IllegalArgumentException; alternatively consider introducing a
ScopeType enum and require/convert scope to that enum before branching so the
null/invalid case is handled consistently with memberId/date checks and routed
to getFromRedis/getFromMv as appropriate.


private RankingDto.PagedRankingResponse getFromMv(String scope, String date, int page, int size) {
// 1. 당일 MV 조회
List<MvProductRank> mvResults = mvProductRankRepository.findByPeriodKeyAndScope(
date, scope, PageRequest.of(page, size));
long totalElements = mvProductRankRepository.countByPeriodKeyAndScope(date, scope);

// 2. 당일 데이터 없으면 전일 fallback
if (mvResults.isEmpty()) {
String previousDate = LocalDate.parse(date, DATE_FORMATTER)
.minusDays(1).format(DATE_FORMATTER);
mvResults = mvProductRankRepository.findByPeriodKeyAndScope(
previousDate, scope, PageRequest.of(page, size));
totalElements = mvProductRankRepository.countByPeriodKeyAndScope(previousDate, scope);

if (!mvResults.isEmpty()) {
log.info("MV 전일 fallback 적용: scope={}, date={} → {}", scope, date, previousDate);
}
}

// 3. 전일도 없으면 빈 결과
if (mvResults.isEmpty()) {
return new RankingDto.PagedRankingResponse(List.of(), 0, 0, page, size);
}
Comment on lines +52 to +74
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 | 🔴 Critical

Fallback 트리거 조건이 잘못되어 정상 데이터가 전일 데이터로 덮어써질 수 있다.

mvResults.isEmpty()는 "해당 period에 데이터가 없음"과 "요청한 page가 범위 밖"을 구분하지 못한다. 예를 들어 오늘 MV에 100건이 존재하고 클라이언트가 page=5, size=50으로 요청하면, findByPeriodKeyOrderByRankingAsc는 오프셋이 총건수를 초과하여 빈 리스트를 반환한다. 이 상황에서 현재 구현은 "오늘 데이터가 없다"고 오판하여 전일(previousDate) 결과로 mvResultstotalElements를 모두 덮어써, 오늘 랭킹이 멀쩡히 존재함에도 API 응답이 전일 데이터를 섞어 내보낸다. 운영에서 MV 전일 fallback 적용 로그만 남으므로 원인 추적도 어렵다.

Fallback 트리거는 반드시 count 기준으로 판단해야 하며, 호출 순서도 count 먼저 조회하도록 정리하는 것이 낫다.

🐛 제안 수정
     private RankingDto.PagedRankingResponse getFromMv(String scope, String date, int page, int size) {
-        // 1. 당일 MV 조회
-        List<MvProductRank> mvResults = mvProductRankRepository.findByPeriodKeyAndScope(
-            date, scope, PageRequest.of(page, size));
-        long totalElements = mvProductRankRepository.countByPeriodKeyAndScope(date, scope);
-
-        // 2. 당일 데이터 없으면 전일 fallback
-        if (mvResults.isEmpty()) {
-            String previousDate = LocalDate.parse(date, DATE_FORMATTER)
-                .minusDays(1).format(DATE_FORMATTER);
-            mvResults = mvProductRankRepository.findByPeriodKeyAndScope(
-                previousDate, scope, PageRequest.of(page, size));
-            totalElements = mvProductRankRepository.countByPeriodKeyAndScope(previousDate, scope);
-
-            if (!mvResults.isEmpty()) {
-                log.info("MV 전일 fallback 적용: scope={}, date={} → {}", scope, date, previousDate);
-            }
-        }
-
-        // 3. 전일도 없으면 빈 결과
-        if (mvResults.isEmpty()) {
-            return new RankingDto.PagedRankingResponse(List.of(), 0, 0, page, size);
-        }
+        // 1. 당일 총건수로 존재 여부 판단 (빈 페이지 ≠ 데이터 없음)
+        String effectiveDate = date;
+        long totalElements = mvProductRankRepository.countByPeriodKeyAndScope(effectiveDate, scope);
+
+        // 2. 당일 데이터 자체가 없을 때만 전일 fallback
+        if (totalElements == 0) {
+            String previousDate = LocalDate.parse(date, DATE_FORMATTER)
+                .minusDays(1).format(DATE_FORMATTER);
+            long prevTotal = mvProductRankRepository.countByPeriodKeyAndScope(previousDate, scope);
+            if (prevTotal == 0) {
+                return new RankingDto.PagedRankingResponse(List.of(), 0, 0, page, size);
+            }
+            log.info("MV 전일 fallback 적용: scope={}, date={} → {}", scope, date, previousDate);
+            effectiveDate = previousDate;
+            totalElements = prevTotal;
+        }
+
+        List<MvProductRank> mvResults = mvProductRankRepository.findByPeriodKeyAndScope(
+            effectiveDate, scope, PageRequest.of(page, size));

추가 테스트로 다음 케이스를 권한다.

  • 당일 MV 100건 존재 + page=5, size=50 요청 시 전일 fallback이 트리거되지 않고 빈 data/정상 totalElements를 반환
  • 당일 MV 0건 + 전일 MV 100건 + page=0 요청 시 전일 데이터로 fallback되고 로그가 남음
  • 당일/전일 모두 0건일 때 빈 응답
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java`
around lines 52 - 74, The fallback currently uses mvResults.isEmpty() which
conflates "no data for period" with "page out of range" and can wrongly replace
today's data with yesterday's; in getFromMv, first call
mvProductRankRepository.countByPeriodKeyAndScope(date, scope) to get
totalElements and only if totalElements == 0 perform the previousDate fallback
(recompute previousDate, set totalElements =
countByPeriodKeyAndScope(previousDate, scope) and log), then call
mvProductRankRepository.findByPeriodKeyAndScope(...) to load mvResults using
PageRequest.of(page, size); ensure you reference and update totalElements and
mvResults only when count indicates zero so that page-out-of-range cases return
empty results for the existing period instead of falling back.


totalElements = Math.min(totalElements, MAX_RANKING_SIZE);
int totalPages = (int) Math.ceil((double) totalElements / size);

// 4. Product 상세 조합
List<Long> productIds = mvResults.stream()
.map(MvProductRank::getProductId).toList();

Map<Long, ProductWithBrand> productMap = productRepository.findAllByIds(productIds).stream()
.collect(Collectors.toMap(pwb -> pwb.product().getId(), pwb -> pwb));

List<RankingDto.RankingResponse> data = new ArrayList<>();
for (MvProductRank mv : mvResults) {
ProductWithBrand pwb = productMap.get(mv.getProductId());
if (pwb != null) {
Product product = pwb.product();
data.add(new RankingDto.RankingResponse(
mv.getProductId(), product.getName(), pwb.brandName(),
product.getPrice().getValue(), mv.getRanking(), mv.getScore()
));
}
}

return new RankingDto.PagedRankingResponse(data, totalElements, totalPages, page, size);
}

private RankingDto.PagedRankingResponse getFromRedis(String scope, String date, int page, int size, Long memberId) {
String prefix = resolveDailyPrefix(memberId);

long totalElements;
List<RankingRedisRepository.RankingEntry> entries;

try {
long rawTotal = rankingRedisRepository.getTotalCount(prefix, resolvedDate);
long rawTotal = rankingRedisRepository.getTotalCount(prefix, date);
totalElements = Math.min(rawTotal, MAX_RANKING_SIZE);

long start = (long) page * size;
Expand All @@ -57,15 +116,14 @@ public RankingDto.PagedRankingResponse getRankings(String scope, String date, in
}

long end = Math.min(start + size - 1, totalElements - 1);
entries = rankingRedisRepository.getTopN(prefix, resolvedDate, start, end);
entries = rankingRedisRepository.getTopN(prefix, date, start, end);
} catch (Exception e) {
log.error("랭킹 Redis 조회 실패", e);
throw new CoreException(ErrorType.INTERNAL_ERROR, "랭킹 서비스를 일시적으로 이용할 수 없습니다.");
}

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

Map<Long, ProductWithBrand> productMap = productRepository.findAllByIds(productIds).stream()
.collect(Collectors.toMap(pwb -> pwb.product().getId(), pwb -> pwb));
Expand All @@ -88,24 +146,15 @@ public RankingDto.PagedRankingResponse getRankings(String scope, String date, in
return new RankingDto.PagedRankingResponse(data, totalElements, totalPages, page, size);
}

private String resolveZsetPrefix(String scope, Long memberId) {
// A/B 테스트는 daily에만 적용
if ("daily".equals(scope) || scope == null) {
RankingProperties.Experiment experiment = properties.experiment();
if (experiment.enabled() && !experiment.variants().isEmpty() && memberId != null) {
List<String> variantKeys = new ArrayList<>(experiment.variants().keySet());
int variantIndex = (int) (Math.abs(memberId) % variantKeys.size());
String selectedKey = variantKeys.get(variantIndex);
RankingProperties.Variant variant = experiment.variants().get(selectedKey);
return variant.zsetPrefix();
}
return DAILY_ZSET_PREFIX;
private String resolveDailyPrefix(Long memberId) {
RankingProperties.Experiment experiment = properties.experiment();
if (experiment.enabled() && !experiment.variants().isEmpty() && memberId != null) {
List<String> variantKeys = new ArrayList<>(experiment.variants().keySet());
int variantIndex = (int) (Math.abs(memberId) % variantKeys.size());
String selectedKey = variantKeys.get(variantIndex);
RankingProperties.Variant variant = experiment.variants().get(selectedKey);
return variant.zsetPrefix();
}

return switch (scope) {
case "weekly" -> WEEKLY_ZSET_PREFIX;
case "monthly" -> MONTHLY_ZSET_PREFIX;
default -> DAILY_ZSET_PREFIX;
};
return DAILY_ZSET_PREFIX;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.loopers.domain.ranking;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@MappedSuperclass
public abstract class MvProductRank {

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

@Column(nullable = false)
private Long productId;

@Column(nullable = false)
private Integer ranking;

@Column(nullable = false)
private Double score;

@Column(nullable = false)
private Long viewCount;

@Column(nullable = false)
private Long likeCount;

@Column(nullable = false)
private Long salesCount;

@Column(nullable = false)
private Long salesAmount;

@Column(nullable = false, length = 8)
private String periodKey;

@Column(nullable = false)
private LocalDateTime createdAt;
Comment on lines +43 to +44
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
# MvProductRank(Weekly|Monthly) 가 JPA save/persist 경로로 쓰이는지 확인
rg -nP --type=java -C3 '\b(MvProductRankWeekly|MvProductRankMonthly)\b'
rg -nP --type=java -C2 '\b(save|persist)\s*\(' -g '**/ranking/**'

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

Length of output: 5490


🏁 Script executed:

cat apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRank.java

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

Length of output: 1026


🏁 Script executed:

# 배치 Job 구현 찾기
fd -e java -path '**/ranking/**' | head -20
rg -n '@PrePersist|@CreationTimestamp' -t java -g '**/ranking/**'

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

Length of output: 253


🏁 Script executed:

# 전체 저장소에서 save/persist 호출 찾기
rg -n '\.(save|saveAll|persist)\s*\(' -t java --max-count 50

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

Length of output: 40751


🏁 Script executed:

# 랭킹 관련 배치 Job 또는 JDBC 직접 INSERT 찾기
fd -e java -path '**/ranking/**' -o -path '**/batch/**' -o -path '**/job/**' | head -30
rg -n 'MvProductRank|MERGE|INSERT' -A5 -B5 --type=java | head -100

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

Length of output: 13939


🏁 Script executed:

# DB 마이그레이션 파일에서 mv_product_rank 스키마 확인
fd -e sql -o -e yaml -o -e xml | xargs grep -l 'mv_product_rank' 2>/dev/null | head -5

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

Length of output: 201


JPA 저장 경로 추가 시 createdAt 자동 설정 부재로 인한 제약 위반 위험 존재.

현재는 배치 Job이 JDBC 기반 INSERT로 createdAt 값을 명시적으로 채우고 있어 문제가 발생하지 않으나, JpaRepository 인터페이스 존재로 인해 향후 누군가 save() 메서드로 이 엔티티를 영속화하면 createdAt이 NULL이 되어 ConstraintViolationException이 발생한다. 도메인 모델 자체에 자동 설정 메커니즘을 추가하여 방어해야 한다.

  • 왜 문제인가: JPA 저장 경로가 추가되는 순간 즉시 저장 실패가 발생하며, DB 스키마의 DEFAULT CURRENT_TIMESTAMP도 하이버네이트의 INSERT 시 NULL 전달로 무효화된다.
  • 수정안: @PrePersist로 기본값을 채우거나, 하이버네이트의 @CreationTimestamp를 부여한다.
♻️ 제안
-    `@Column`(nullable = false)
-    private LocalDateTime createdAt;
+    `@Column`(nullable = false, updatable = false)
+    private LocalDateTime createdAt;
+
+    `@PrePersist`
+    protected void onCreate() {
+        if (createdAt == null) {
+            createdAt = LocalDateTime.now();
+        }
+    }
  • 추가 테스트: JPA save() 경로로 엔티티를 저장하는 리포지토리 단위 테스트를 추가해 createdAt 자동 설정 여부를 검증한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRank.java`
around lines 43 - 44, MvProductRank currently declares private LocalDateTime
createdAt with `@Column`(nullable = false) but has no JPA-side auto-population, so
saving via JpaRepository will set NULL and violate the constraint; update the
entity to auto-populate createdAt (either add a `@PrePersist` method in
MvProductRank that sets createdAt = LocalDateTime.now() if null, or annotate the
field with Hibernate's `@CreationTimestamp`) and add a repository unit test that
saves MvProductRank via save() to assert createdAt is non-null after persist.

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

import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "mv_product_rank_monthly")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MvProductRankMonthly extends MvProductRank {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.loopers.domain.ranking;

import org.springframework.data.domain.Pageable;

import java.util.List;

public interface MvProductRankRepository {

List<MvProductRank> findByPeriodKeyAndScope(String periodKey, String scope, Pageable pageable);

long countByPeriodKeyAndScope(String periodKey, String scope);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.loopers.domain.ranking;

import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "mv_product_rank_weekly")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MvProductRankWeekly extends MvProductRank {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.loopers.infrastructure.ranking;

import com.loopers.domain.ranking.*;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MvProductRankJpaRepository implements MvProductRankRepository {

private final MvProductRankWeeklySpringDataRepository weeklyRepository;
private final MvProductRankMonthlySpringDataRepository monthlyRepository;

@Override
public List<MvProductRank> findByPeriodKeyAndScope(String periodKey, String scope, Pageable pageable) {
return switch (scope) {
case "weekly" -> weeklyRepository.findByPeriodKeyOrderByRankingAsc(periodKey, pageable)
.stream().map(r -> (MvProductRank) r).toList();
case "monthly" -> monthlyRepository.findByPeriodKeyOrderByRankingAsc(periodKey, pageable)
.stream().map(r -> (MvProductRank) r).toList();
default -> throw new IllegalArgumentException("Invalid scope: " + scope);
};
}

@Override
public long countByPeriodKeyAndScope(String periodKey, String scope) {
return switch (scope) {
case "weekly" -> weeklyRepository.countByPeriodKey(periodKey);
case "monthly" -> monthlyRepository.countByPeriodKey(periodKey);
default -> throw new IllegalArgumentException("Invalid scope: " + scope);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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.util.List;

public interface MvProductRankMonthlySpringDataRepository extends JpaRepository<MvProductRankMonthly, Long> {

List<MvProductRankMonthly> findByPeriodKeyOrderByRankingAsc(String periodKey, Pageable pageable);

long countByPeriodKey(String periodKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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.util.List;

public interface MvProductRankWeeklySpringDataRepository extends JpaRepository<MvProductRankWeekly, Long> {

List<MvProductRankWeekly> findByPeriodKeyOrderByRankingAsc(String periodKey, Pageable pageable);

long countByPeriodKey(String periodKey);
}
Loading