Skip to content

[round-10] 주간/월간 랭킹 시스템 구현#11

Merged
ImSejin merged 9 commits into
masterfrom
round-10
Sep 28, 2025
Merged

[round-10] 주간/월간 랭킹 시스템 구현#11
ImSejin merged 9 commits into
masterfrom
round-10

Conversation

@ImSejin

@ImSejin ImSejin commented Sep 19, 2025

Copy link
Copy Markdown
Member

📌 Summary

일간/주간/월간 랭킹 영속화

  1. 실시간 랭킹이 레디스에 저장
  2. 매일 00:00:05에 전날 실시간 랭킹을 RDB에 영속화
  3. 매주 월요일 00:00:05에 지난 주 랭킹을 RDB에 영속화
  4. 매월 1일 00:00:05에 전월 랭킹을 RDB에 영속화

주간/월간 랭킹 API 구현

Redis에서 조회한 일간 랭킹과 달리 RDB에서 주간/월간 랭킹을 조회했습니다.

💬 Review Points

일간 랭킹 테이블 대신 실시간 랭킹의 TTL을 한 달 이상으로 한다면?

맨 처음에 실시간으로 집계되는 일간 랭킹이 레디스에 있으니 이를 활용하자고 생각했습니다.
하지만 바로 문제가 생겼습니다. TTL이 3일로 지정되어 있었기 때문입니다.

TTL을 한 달 이상으로 설정하면 해결은 되겠으나, 메모리를 상당히 많이 차지할 거 같습니다.
분기별/연간 랭킹 요구사항이 들어온다면 TTL을 또 늘려야 하니 비효율적일 거 같다고 생각합니다.

그럼에도 불구하고, 실무에서 레디스에 집계되는 실시간 랭킹의 TTL을 배치를 위해 길게 잡는 경우가 있을까요?

✅ Checklist

🧱 Spring Batch

  • Spring Batch Job 을 작성하고, 파라미터 기반으로 동작시킬 수 있다.
  • Chunk Oriented Processing (Reader/Processor/Writer or Tasklet) 기반의 배치 처리를 구현했다.
  • 집계 결과를 저장할 Materialized View 의 구조를 설계하고 올바르게 적재했다.

🧩 Ranking API

  • API 가 일간, 주간, 월간 랭킹을 제공하며 조회해야 하는 형태에 따라 적절한 데이터를 기반으로 랭킹을 제공한다.

Summary by CodeRabbit

  • New Features

    • Introduced daily, weekly, and monthly ranking endpoints (/api/v1/rankings/daily|weekly|monthly) with respective parameters (date, yearWeek, yearMonth). Unified response items and pagination.
    • Added automated aggregation jobs: daily updates, with weekly (Mondays) and monthly (1st) rollups for fresher rankings.
  • Documentation

    • Updated README to include the new commerce-batch app.
    • Revised HTTP examples to demonstrate the new ranking endpoints and headers.

@ImSejin ImSejin self-assigned this Sep 19, 2025
@coderabbitai

coderabbitai Bot commented Sep 19, 2025

Copy link
Copy Markdown

Walkthrough

Adds weekly and monthly ranking support alongside daily across commerce-api, updates DTOs, services, repositories, and controllers; introduces YearWeek handling and formatter. Adds commerce-batch application with entities, repositories, services, tasklets, scheduler, and a split-flow batch job aggregating daily to weekly/monthly. Documentation and HTTP examples updated. Build files add threeten-extra.

Changes

Cohort / File(s) Summary
Docs
README.md, http/commerce-api/rank.http
Update project tree with commerce-batch; replace single ranking endpoint with daily/weekly/monthly examples and headers.
Build and dependency management
build.gradle.kts, apps/commerce-api/build.gradle.kts, apps/commerce-batch/build.gradle.kts
Add org.threeten:threeten-extra to dependency management and module builds; add batch, QueryDSL, and module/support deps for commerce-batch.
Web config and formatting
apps/commerce-api/src/main/java/com/loopers/config/web/WebConfig.java, apps/commerce-api/src/main/java/com/loopers/config/web/converter/YearWeekFormatter.java
Register YearWeek formatter in Spring MVC for binding; implement Formatter.
API Controller & Spec
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java, .../RankingV1ApiSpec.java
Rename daily endpoint and add weekly/monthly endpoints; adjust request/response and return types accordingly.
API Request/Response DTOs
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java, .../RankingResponse.java
Split SearchRankings into SearchDaily and add SearchWeekly/SearchMonthly; validations added; centralize Item mapping.
Application Facade & IO Models
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java, .../RankingInput.java, .../RankingOutput.java
Replace SearchRankings with daily; add weekly/monthly inputs/outputs; use shared Item.from; map product details; handle empty results per period.
Domain Commands/Results (API)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingCommand.java, .../RankingResult.java
Rename SearchRanks→SearchDaily; add weekly/monthly records with factories and per-item records; remove pageable() method.
Domain Service (API)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java
Replace searchRanks with searchDaily; add searchWeekly and searchMonthly; build Pageable and delegate to repository per period.
Domain Entities (API)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java, .../ProductRankingMonthly.java
Add JPA entities for weekly and monthly rankings with constraints/indexes and YearWeek/YearMonth fields.
Repositories (API)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingRepository.java, .../infrastructure/ranking/RankingRepositoryImpl.java, .../ProductRankingWeeklyJpaRepository.java, .../ProductRankingMonthlyJpaRepository.java
Extend searchRanks overloads for YearWeek/YearMonth; implement weekly/monthly queries via new JPA repositories and map to query results.
Test utility
apps/commerce-api/src/test/java/com/loopers/test/assertion/ConcurrentAssertion.java
Handle InterruptedException separately by re-interrupting and recording error.
Batch app bootstrap & config
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java, .../resources/application.yml
New Spring Boot batch app with async, scheduling, batch enabled; profiles and executor config; timezone setup.
Batch domain entities
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingDaily.java, .../ProductRankingWeekly.java, .../ProductRankingMonthly.java
Add daily/weekly/monthly ranking entities with builders and validations; map to corresponding tables with constraints/indexes.
Batch domain commands/results/service
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingCommand.java, .../RankingResult.java, .../RankingService.java
Define commands for get/aggregate operations; result mapper for daily; service to get daily and aggregate daily/weekly/monthly.
Batch repositories (infra)
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingDailyJpaRepository.java, .../ProductRankingWeeklyJpaRepository.java, .../ProductRankingMonthlyJpaRepository.java, .../RankingRepositoryImpl.java
Add JPA repos with upsert-style merge queries; implement repository bridging findDaily and merge for all periods.
Batch job: tasklets, scheduler, job config, listener
apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java, .../WeeklyRankingAggregationTasklet.java, .../MonthlyRankingAggregationTasklet.java, .../ProductRankingJobConfig.java, .../interfaces/scheduler/ranking/RankingScheduler.java, .../support/CommonJobListener.java
Tasklets to aggregate daily from Redis and roll-up weekly/monthly; job config with deciders and split flows; scheduler triggers job daily; listener logs start/end.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant API as RankingV1Controller
  participant Fac as RankingFacade
  participant Svc as RankingService (API)
  participant Repo as RankingRepository
  participant Prod as ProductService

  rect rgb(235,245,255)
  note over C,API: Daily/Weekly/Monthly search
  C->>API: GET /api/v1/rankings/{daily|weekly|monthly}
  API->>Fac: search{Daily|Weekly|Monthly}(Request)
  Fac->>Svc: search{Daily|Weekly|Monthly}(Command)
  Svc->>Repo: searchRanks(date|yearWeek|yearMonth, pageable)
  Repo-->>Svc: Page<SearchRanks>
  Svc-->>Fac: RankingResult.{Daily|Weekly|Monthly}
  Fac->>Prod: getProductDetail(productId...) loop
  Prod-->>Fac: ProductDetail list
  Fac-->>API: RankingOutput.{Daily|Weekly|Monthly}
  API-->>C: ApiResponse<RankingResponse.{Daily|Weekly|Monthly}>
  end
Loading
sequenceDiagram
  autonumber
  participant S as Scheduler
  participant JL as JobLauncher
  participant J as product_ranking_job
  participant D as Daily Tasklet
  participant WD as Weekly Decider
  participant WS as Weekly Tasklet
  participant MD as Monthly Decider
  participant MS as Monthly Tasklet
  participant BS as RankingService (Batch)
  participant BR as RankingRepository (Batch)
  participant R as Redis

  S->>JL: run(J, params: date=yesterday)
  JL->>J: start
  J->>D: execute
  D->>R: ZREVRANGE metric.product.all:<date> WITHSCORES
  R-->>D: top items
  D->>BS: aggregateDaily(entries)
  BS->>BR: merge daily rows
  BR-->>BS: ok
  J->>WD: decide(date)
  WD-->>J: RUN or SKIP
  alt Weekly RUN
    J->>WS: execute
    WS->>BS: getDaily(start..end)
    BS->>BR: findDailyRanking
    BR-->>BS: list
    WS->>BS: aggregateWeekly(productIds)
  end
  J->>MD: decide(date)
  MD-->>J: RUN or SKIP
  alt Monthly RUN
    J->>MS: execute
    MS->>BS: getDaily(start..end)
    BS->>BR: findDailyRanking
    BR-->>BS: list
    MS->>BS: aggregateMonthly(productIds)
  end
  J-->>JL: completed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

A rabbit taps the weekly beat,
Then counts the months with tidy feet.
Daily hops from Redis logs,
To leaderboards and batchy jogs.
Deciders nod, the rankings grow—
Three tempos now, one burrow’s flow. 🐇📈

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "[round-10] 주간/월간 랭킹 시스템 구현" concisely and accurately describes the main change: implementing weekly and monthly ranking functionality. It matches the changeset which adds batch jobs, new ranking entities/repositories, formatters, and API endpoints for daily/weekly/monthly rankings. The title is specific, short, and appropriate for repository history.
Description Check ✅ Passed The PR description follows the repository template: it provides a clear Summary describing daily/weekly/monthly persistence and API behavior, includes a Review Points section discussing TTL tradeoffs, and contains a populated Checklist marking implemented batch and API work. The optional References section is not present but is not required for a passing description. Overall the description is sufficiently detailed for reviewers to understand scope and design decisions.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch round-10

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ImSejin ImSejin marked this pull request as ready for review September 19, 2025 07:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)

34-39: Consider bulk fetching product details for performance

The current implementation makes N individual calls to getProductDetail for N products. This could cause performance issues with database round trips.

Would you like me to suggest an implementation that fetches all product details in a single query? This would significantly improve performance by reducing database round trips from N to 1.

Also applies to: 55-60, 76-81

🧹 Nitpick comments (19)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingDaily.java (1)

58-82: Consider consistency in validation patterns across ranking entities.

The validation logic here differs slightly from the weekly/monthly counterparts in the relevant code snippets:

  1. Score field: This entity includes a score field with validation (score <= 0.0), but ProductRankingWeekly and ProductRankingMonthly don't have score fields
  2. Date handling: This entity uses LocalDate directly, while weekly/monthly entities derive their temporal fields from the standardDate parameter

Consider documenting why the daily ranking includes score while weekly/monthly do not, and whether this architectural difference is intentional for the aggregation logic.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingMonthlyJpaRepository.java (1)

4-4: Remove unused import.

The import ProductRankingWeekly is not used in this monthly repository interface.

Apply this diff to remove the unused import:

-import com.loopers.domain.ranking.ProductRankingWeekly;
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1)

32-33: Update Javadoc comment to reflect monthly rather than weekly

The Javadoc comment says "한 해의 주차" (week of the year) but this field represents YearMonth for monthly rankings.

Apply this diff to fix the comment:

-     * 한 해의 주차
+     * 연월
apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java (2)

39-40: Consider using a more descriptive Redis key prefix.

The hardcoded key prefix "metric.product.all:" could benefit from being externalized to configuration or at least documented with a comment explaining the key structure.

Consider extracting this to a constant:

+    private static final String REDIS_KEY_PREFIX = "metric.product.all:";
+
     @Override
     public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
         String day = date.format(DateTimeFormatter.BASIC_ISO_DATE);
-        String all = "metric.product.all:" + day;
+        String all = REDIS_KEY_PREFIX + day;

44-44: Consider parameterizing the top 100 limit.

The hardcoded limit of 99 (top 100 items) should be configurable to allow flexibility in ranking size requirements.

Consider adding a configurable property:

+    @Value("${ranking.daily.limit:99}")
+    private int rankingLimit;
+
     // Top 100
     ZSetOperations<String, String> zSet = stringRedisTemplate.opsForZSet();
-    Set<ZSetOperations.TypedTuple<String>> tuples = zSet.reverseRangeWithScores(all, 0, 99);
+    Set<ZSetOperations.TypedTuple<String>> tuples = zSet.reverseRangeWithScores(all, 0, rankingLimit);
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingRepository.java (1)

8-8: Consider adding pagination support for daily ranking queries.

The findDailyRanking method returns a List<ProductRankingDaily> without pagination, which could become problematic with large datasets over extended date ranges.

Consider adding a paginated version:

 public interface RankingRepository {
 
     List<ProductRankingDaily> findDailyRanking(LocalDate startDate, LocalDate endDate);
+    
+    Page<ProductRankingDaily> findDailyRanking(LocalDate startDate, LocalDate endDate, Pageable pageable);
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)

20-24: Code duplication with CommerceStreamerApplication.

The timezone initialization logic is duplicated from CommerceStreamerApplication.java. Consider extracting this to a common configuration component.

Consider creating a shared configuration class:

@Component
public class TimezoneConfiguration {
    
    @PostConstruct
    public void initializeTimezone() {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
    }
}

Then import this component in both applications instead of duplicating the logic.

apps/commerce-batch/src/main/resources/application.yml (1)

17-22: Consider adjusting the task executor pool configuration for batch processing.

The current thread pool configuration (core-size: 8, max-size: 16, queue-capacity: 8) has a very small queue capacity relative to the pool sizes. This could lead to task rejections under moderate load since the queue will fill quickly before new threads are spawned.

For batch processing workloads, consider a larger queue capacity or using an unbounded queue if memory permits:

  task:
    execution:
      pool:
        core-size: 8
        max-size: 16
-       queue-capacity: 8
+       queue-capacity: 100  # Or consider Integer.MAX_VALUE for unbounded
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1)

30-30: Fix incorrect Javadoc comment.

The comment "한 해의 주차" (week of the year) is incorrect for a monthly field. It should describe months, not weeks.

    /**
-    * 한 해의 주차
+    * 연월
     */
apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java (2)

65-66: Consider limiting the number of products for performance

The comment mentions "Top 100" but the code doesn't enforce this limit. If there are thousands of products, this could cause memory/performance issues.

Apply this diff to limit to top 100:

         List<Long> productIds = productScores.entrySet()
                 .stream()
                 .sorted(Comparator
                         .<Map.Entry<Long, Double>>comparingDouble(Map.Entry::getValue).reversed()
                         .thenComparing(Map.Entry::getKey))
                 .map(Map.Entry::getKey)
+                .limit(100)
                 .toList();

43-43: Verify getDaily handles month-long ranges efficiently

  • Found: getDaily -> rankingRepository.findDailyRanking(start,end) -> ProductRankingDailyJpaRepository.findByDateBetween(start,end) which returns List; MonthlyRankingAggregationTasklet then aggregates in-memory. (files: apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingService.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java, apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingDailyJpaRepository.java, apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java)

  • Risk & fix: date is indexed but the current flow loads all rows into memory — for large volumes switch to DB-side aggregation (repo method that SELECTs product_id, SUM(score) GROUP BY product_id ORDER BY SUM DESC LIMIT 100) or use streaming/pagination to avoid high memory/CPU.

apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (1)

82-82: Unnecessary Long.valueOf() for Integer to Long conversion

Using Long.valueOf(item.getRank()) is verbose. Consider using auto-boxing or a direct cast if getRank() returns an int/Integer.

Apply this diff:

-                        Long.valueOf(item.getRank())
+                        item.getRank().longValue()

Or if getRank() returns a primitive int:

-                        Long.valueOf(item.getRank())
+                        (long) item.getRank()

Also applies to: 97-97

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)

23-84: Code duplication across three ranking methods

The three methods (searchDaily, searchWeekly, searchMonthly) have identical logic patterns. This violates DRY principle and makes maintenance harder.

Consider extracting common logic into a generic method:

private <TCommand, TResult, TOutput> TOutput searchRanking(
        TCommand command,
        Function<TCommand, TResult> searchFunction,
        Function<TResult, Boolean> isEmpty,
        Function<TResult, TOutput> emptyMapper,
        Function<TResult, List<Long>> getProductIds,
        BiFunction<TResult, List<ProductResult.GetProductDetail>, TOutput> mapper
) {
    TResult ranks = searchFunction.apply(command);
    
    if (isEmpty.apply(ranks)) {
        return emptyMapper.apply(ranks);
    }
    
    List<ProductResult.GetProductDetail> details = getProductIds.apply(ranks)
            .stream()
            .map(productId -> productService.getProductDetail(productId)
                    .orElseThrow(() -> new BusinessException(CommonErrorType.NOT_FOUND))
            )
            .toList();
    
    return mapper.apply(ranks, details);
}

However, given the specific types involved, a simpler approach might be to create a base method with common logic.

apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java (1)

66-67: Top 100 comment is misleading - no limit is enforced.

The comment says "Top 100" but the code passes all productIds without limiting to 100 items.

Either enforce the limit or update the comment:

         // Top 100
-        rankingService.aggregateWeekly(new RankingCommand.AggregateWeekly(date, productIds));
+        List<Long> top100ProductIds = productIds.size() > 100 ? productIds.subList(0, 100) : productIds;
+        rankingService.aggregateWeekly(new RankingCommand.AggregateWeekly(date, top100ProductIds));
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingService.java (1)

24-41: Consider batch processing for better performance.

The daily aggregation iterates and persists rankings one by one, which could be inefficient for large datasets. Consider batch inserts for better performance.

     @Transactional
     public void aggregateDaily(RankingCommand.AggregateDaily command) {
         List<Map.Entry<Long, Double>> entries = command.entries();
         if (CollectionUtils.isEmpty(entries)) {
             return;
         }

+        List<ProductRankingDaily> rankings = new ArrayList<>();
         for (int i = 0; i < entries.size(); i++) {
             ProductRankingDaily ranking = ProductRankingDaily.builder()
                     .date(command.date())
                     .productId(entries.get(i).getKey())
                     .rank(i + 1)
                     .score(entries.get(i).getValue())
                     .build();
-
-            rankingRepository.merge(ranking);
+            rankings.add(ranking);
         }
+        rankingRepository.mergeAll(rankings); // Implement batch merge in repository
     }

The same optimization applies to aggregateWeekly and aggregateMonthly methods.

apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1)

71-72: Document or change week calculation (ISO-8601 vs calendar-year)

File: apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java lines 71–72

        LocalDate dateAtMonday = standardDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
        this.yearWeek = YearWeek.from(dateAtMonday);

Verified: anchoring to the previous-or-same Monday then YearWeek.from(...) follows ISO-8601 week-based-year — e.g., 2024-12-30..2025-01-01 produce YearWeek 2025-W01.

  • If ISO-8601 semantics are intended: add a comment and unit tests covering year-boundary dates.
  • If you expect the week to belong to the date's calendar year: change the calculation (e.g., derive week/year via WeekFields.of(Locale) or compute week-number within standardDate.getYear()) and add tests.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java (1)

34-77: Consider extracting common response structure to reduce duplication.

The SearchWeekly and SearchMonthly classes are identical in structure, differing only in their names. While this explicit approach provides type safety, you could reduce duplication by using a generic base class or a parameterized type.

Consider this alternative approach using composition:

@Getter
@Builder
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class PagedRankingResponse {
    private final Integer totalPages;
    private final Long totalItems;
    private final Integer page;
    private final Integer size;
    private final List<Item> items;
    
    protected static PagedRankingResponse fromOutput(
            Integer totalPages, Long totalItems, Integer page, Integer size, 
            List<? extends RankingOutput.Item> items) {
        return builder()
                .totalPages(totalPages)
                .totalItems(totalItems)
                .page(page)
                .size(size)
                .items(items.stream().map(Item::from).toList())
                .build();
    }
}

public static class SearchWeekly extends PagedRankingResponse {
    public static SearchWeekly from(RankingOutput.SearchWeekly output) {
        PagedRankingResponse base = fromOutput(
            output.totalPages(), output.totalItems(), 
            output.page(), output.size(), output.items()
        );
        return new SearchWeekly(base);
    }
    // Constructor that copies from base...
}
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingOutput.java (1)

34-78: Consider extracting common pagination logic.

Similar to the response layer, SearchWeekly and SearchMonthly have identical structure and factory methods. Consider reducing duplication while maintaining type safety.

You could reduce duplication by extracting a common factory method:

private static <T> List<Item> mapDetailsToItems(List<ProductResult.GetProductDetail> details) {
    return details.stream().map(Item::from).toList();
}

public static SearchWeekly from(RankingResult.SearchWeekly ranks, List<ProductResult.GetProductDetail> details) {
    return new SearchWeekly(
        ranks.totalPages(),
        ranks.totalItems(),
        ranks.page(),
        ranks.size(),
        mapDetailsToItems(details)
    );
}
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingResult.java (1)

36-84: Consider consolidating duplicate Item records.

The nested Item records in SearchWeekly and SearchMonthly are identical to the one in SearchDaily. Consider extracting a single shared Item record to reduce duplication.

Apply this refactor to consolidate the Item records:

 public record RankingResult() {
+    
+    public record Item(
+            Long productId,
+            Long rank
+    ) {
+    }

     public record SearchDaily(
             Integer totalPages,
             Long totalItems,
             Integer page,
             Integer size,
             List<Item> items
     ) {
         public static SearchDaily from(Page<RankingQueryResult.SearchRanks> page, Pageable pageable) {
             return new SearchDaily(
                     page.getTotalPages(),
                     page.getTotalElements(),
                     pageable.getPageNumber(),
                     pageable.getPageSize(),
                     page.map(result -> new Item(result.productId(), result.rank())).toList()
             );
         }
-
-        public record Item(
-                Long productId,
-                Long rank
-        ) {
-        }
     }

     // Similar changes for SearchWeekly and SearchMonthly...
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99066ca and 833ee77.

📒 Files selected for processing (43)
  • README.md (1 hunks)
  • apps/commerce-api/build.gradle.kts (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInput.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingOutput.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/config/web/WebConfig.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/config/web/converter/YearWeekFormatter.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingCommand.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingRepository.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingResult.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankingMonthlyJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankingWeeklyJpaRepository.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (3 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/test/assertion/ConcurrentAssertion.java (1 hunks)
  • apps/commerce-batch/build.gradle.kts (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingDaily.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingCommand.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingRepository.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingResult.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingService.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingDailyJpaRepository.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingMonthlyJpaRepository.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingWeeklyJpaRepository.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/ranking/RankingScheduler.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java (1 hunks)
  • apps/commerce-batch/src/main/java/com/loopers/support/CommonJobListener.java (1 hunks)
  • apps/commerce-batch/src/main/resources/application.yml (1 hunks)
  • build.gradle.kts (1 hunks)
  • http/commerce-api/rank.http (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-11T17:37:17.705Z
Learnt from: ImSejin
PR: loopers-bootcamp/e-commerce#9
File: apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingResult.java:28-30
Timestamp: 2025-09-11T17:37:17.705Z
Learning: In the RankingResult.SearchRanks class at apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingResult.java, the pageable() method intentionally uses the page value directly in PageRequest.of(page, size) without converting from 1-based to 0-based indexing, as confirmed by the maintainer.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingRepository.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingResult.java
🧬 Code graph analysis (16)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1)
  • Getter (11-49)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java (4)
  • Getter (16-28)
  • Getter (32-44)
  • Getter (48-60)
  • NoArgsConstructor (13-62)
apps/commerce-api/src/main/java/com/loopers/config/web/WebConfig.java (1)
apps/commerce-api/src/main/java/com/loopers/config/web/converter/YearWeekFormatter.java (1)
  • YearWeekFormatter (8-20)
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInput.java (1)
k6/api/v1/products/search-products.js (1)
  • page (46-46)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingCommand.java (1)
k6/api/v1/products/search-products.js (1)
  • page (46-46)
apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
  • SpringBootApplication (10-24)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (2)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingDaily.java (1)
  • Getter (12-84)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1)
  • Getter (15-77)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
  • Slf4j (13-49)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java (1)
  • Slf4j (25-64)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java (1)
  • Slf4j (26-73)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java (1)
  • Slf4j (27-74)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingDaily.java (2)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1)
  • Getter (13-74)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1)
  • Getter (15-77)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java (3)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java (5)
  • NoArgsConstructor (8-103)
  • Getter (11-30)
  • Getter (34-53)
  • Getter (57-76)
  • Getter (80-101)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1)
  • Getter (11-49)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1)
  • Getter (10-48)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java (3)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java (2)
  • Slf4j (116-129)
  • Slf4j (131-144)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java (1)
  • Slf4j (25-64)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java (1)
  • Slf4j (27-74)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1)
  • Repository (24-231)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java (2)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java (2)
  • Slf4j (116-129)
  • Slf4j (131-144)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java (1)
  • Slf4j (25-64)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (2)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingDaily.java (1)
  • Getter (12-84)
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (1)
  • Getter (13-74)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/DailyRankingAggregationTasklet.java (3)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java (2)
  • Slf4j (116-129)
  • Slf4j (131-144)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java (1)
  • Slf4j (26-73)
apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java (1)
  • Slf4j (27-74)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1)
  • Getter (10-48)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java (4)
  • Getter (16-28)
  • Getter (32-44)
  • Getter (48-60)
  • NoArgsConstructor (13-62)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java (3)
  • Getter (16-28)
  • Getter (32-44)
  • Getter (48-60)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductResult.java (4)
  • Getter (11-52)
  • Getter (41-51)
  • Getter (98-128)
  • Getter (110-127)
🔇 Additional comments (41)
apps/commerce-api/src/test/java/com/loopers/test/assertion/ConcurrentAssertion.java (1)

73-76: Excellent addition of explicit InterruptedException handling.

This change properly handles thread interruption by restoring the interrupt status and tracking the interruption as an error. This is a best practice for concurrent code as it preserves the interruption signal for higher-level code that might need to respond to cancellation requests.

README.md (1)

30-30: LGTM! Documentation correctly reflects the new commerce-batch app.

The addition of the commerce-batch entry accurately documents the new Spring Batch application added as part of this PR's ranking system implementation.

build.gradle.kts (1)

57-58: LGTM! threeten-extra dependency added correctly.

ThreeTen-Extra provides additional date-time classes that complement those in JDK 8, including YearWeek as "an immutable date-time object that represents the combination of a week-based-year and week-of-week-based-year". This dependency enables the YearWeek and YearMonth types used throughout the ranking system implementation.

apps/commerce-api/build.gradle.kts (1)

20-20: LGTM! Dependency properly leverages the root-level version management.

The threeten-extra dependency correctly references the version managed at the root level in build.gradle.kts, maintaining consistency across modules.

apps/commerce-batch/build.gradle.kts (1)

1-22: LGTM! Well-structured dependencies for batch processing.

The dependency configuration appropriately includes:

  • Core modules (JPA, Redis) for data access
  • Spring Batch runtime and testing support
  • threeten-extra for YearWeek/YearMonth types
  • QueryDSL processors for advanced querying
  • Test fixtures for reusing test infrastructure

All dependencies align with the batch-oriented ranking aggregation implementation.

apps/commerce-batch/src/main/java/com/loopers/support/CommonJobListener.java (1)

1-34: LGTM! Well-implemented Spring Batch job listener.

The implementation provides comprehensive job lifecycle logging with appropriate log levels:

  • Info-level logging for start and completion events
  • Error-level logging for job failures with exit codes
  • Proper separation of concerns for batch job monitoring
http/commerce-api/rank.http (1)

1-14: LGTM! HTTP examples align well with the new ranking endpoints

The HTTP examples correctly demonstrate the three new ranking endpoints (daily, weekly, monthly) with appropriate parameter formats and authentication headers. The date formats match the expected patterns:

  • Daily: ISO date format (2025-09-10)
  • Weekly: ISO week format (2025-W36)
  • Monthly: Year-month format (2025-08)
apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankingWeeklyJpaRepository.java (1)

12-13: Confirm whether secondary ORDER BY p.id is required

Query currently orders by p.rank, p.id. A unique constraint on (year_week, ref_product_id) prevents duplicate product rows but does not guarantee rank values are unique — verify how ranks are assigned (batch/job/entity). If rank ties are impossible, remove p.id; if ties can occur, keep p.id to ensure deterministic ordering/pagination.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java (2)

36-37: Decide & enforce temporal validation for SearchWeekly.yearWeek

SearchDaily.date is annotated @PastOrPresent and SearchMonthly.yearMonth is annotated @past, but SearchWeekly.yearWeek is only @NotNull — either add @PastOrPresent or @past to align with intended semantics, or confirm and document that future weeks are allowed.

Location: apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingRequest.java — SearchWeekly.yearWeek


52-53: @past is correct for SearchMonthly — Monthly aggregation only runs when the job’s date parameter is the first day of the month (triggered by the scheduler on the 2nd of each month), aggregating the previous month, so SearchMonthly should exclude the current YearMonth.

apps/commerce-api/src/main/java/com/loopers/config/web/WebConfig.java (1)

8-16: Consider adding a YearMonthFormatter for consistency

WebConfig currently registers only YearWeekFormatter (apps/commerce-api/src/main/java/com/loopers/config/web/WebConfig.java). No YearMonth formatter or conclusive YearMonth usage was found in the repo; if monthly endpoints accept java.time.YearMonth, register registry.addFormatter(new YearMonthFormatter()) here or annotate controller params with @DateTimeFormat(pattern = "yyyy-MM").

apps/commerce-api/src/main/java/com/loopers/config/web/converter/YearWeekFormatter.java (2)

8-19: LGTM! Clean and straightforward formatter implementation.

The formatter correctly implements Spring's Formatter<YearWeek> interface with proper delegation to YearWeek.parse() and toString(). The locale-agnostic approach is appropriate since ISO-8601 year-week format is standardized.


3-4: Verify threeten-extra dependency version.

I can't access the project's build files — paste the pom.xml or build.gradle (or provide a repo URL) so I can confirm whether org.threeten.extra:threeten-extra is declared and which version is used; ensure it's a current compatible release.

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInput.java (2)

10-15: LGTM! Proper renaming for clarity.

The renaming from SearchRankings to SearchDaily improves API clarity and aligns with the expanded daily/weekly/monthly ranking structure.


19-33: LGTM! Consistent structure across time periods.

The new SearchWeekly and SearchMonthly records follow a consistent pattern with appropriate temporal types (YearWeek and YearMonth). The structure maintains consistency with pagination parameters.

apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingRepository.java (1)

10-15: LGTM! Consistent merge operation interface.

The merge methods provide a consistent interface for persisting different ranking period types, with boolean return values indicating operation success.

apps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/ranking/RankingScheduler.java (1)

24-24: Verify the cron schedule aligns with PR requirements.

The cron expression "5 0 0 * * *" runs at 00:00:05 daily, which matches the PR objectives. The 5-second delay helps avoid potential conflicts with other midnight processes.

apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankingMonthlyJpaRepository.java (1)

13-14: LGTM! Proper JPA query with sorting.

The query correctly uses JPQL syntax and includes proper ordering by rank and ID for consistent results. The parameterized query prevents SQL injection.

apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingResult.java (2)

11-22: LGTM! Clean factory method implementation.

The from() method provides a clean way to transform ProductRankingDaily entities to the result format using streams. The implementation is concise and follows good functional programming practices.


24-30: LGTM! Well-structured nested record.

The Item record properly encapsulates the daily ranking data with appropriate field types. The nested structure keeps related concepts together while maintaining clarity.

apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java (1)

15-15: Resolved — mysqlMainDataSource bean present

Bean defined in modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java (method mysqlMainDataSource, annotated @bean and @primary, returns HikariDataSource); @EnableBatchProcessing(dataSourceRef = "mysqlMainDataSource") is valid.

apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingCommand.java (1)

1-43: LGTM! Clean command structure for daily/weekly/monthly rankings.

The command records are well-organized with clear separation of concerns. The use of YearWeek from threeten-extra and standard YearMonth appropriately models the time periods for rankings.

apps/commerce-batch/src/main/resources/application.yml (1)

24-28: LGTM! Safe defaults for batch job configuration.

Good practice to have the batch job disabled by default with NONE as the fallback job name. This prevents accidental job execution in non-intended environments.

apps/commerce-batch/src/main/java/com/loopers/domain/ranking/RankingCommand.java (1)

7-39: LGTM! Well-structured command records for batch operations.

The command records clearly define the input data for different ranking aggregation operations. The use of Map.Entry<Long, Double> for daily entries appropriately captures product-score pairs.

apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java (1)

1-48: LGTM! Well-structured weekly ranking entity.

The entity is properly configured with appropriate JPA annotations, constraints, and indexes. The use of YearWeek from threeten-extra is a good choice for modeling weekly periods.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)

11-36: LGTM! Well-structured API specification

The API specification cleanly separates daily, weekly, and monthly ranking endpoints with appropriate request/response types and clear operation descriptions.

apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (2)

75-87: Incorrect — repository method exists.
The method findByYearWeek(YearWeek, Pageable) is defined in apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankingWeeklyJpaRepository.java:13, so the original comment is incorrect.

Likely an incorrect or invalid review comment.


90-102: Incorrect — repository method exists

productRankingMonthlyJpaRepository.findByYearMonth(YearMonth, Pageable) is defined at apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankingMonthlyJpaRepository.java:14 — the "Missing repository method" claim is incorrect.

Likely an incorrect or invalid review comment.

apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java (1)

53-58: LGTM! Good use of parallel execution.

The configuration properly uses Spring Batch's split functionality to run weekly and monthly aggregations in parallel after daily aggregation completes, which is an efficient design.

apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/RankingRepositoryImpl.java (1)

27-36: merge() return-value handling is correct — no change needed.

The ProductRankingDaily/Weekly/Monthly repositories use @Modifying methods that return int and execute INSERT ... ON CONFLICT DO UPDATE, which yields 1 for both insert and update; comparing == 1 is valid.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (3)

19-33: LGTM! Clean implementation of daily rankings endpoint.

The refactored daily rankings endpoint maintains good separation of concerns with proper request/response mapping and delegation to the facade layer.


35-49: LGTM! Well-structured weekly rankings endpoint.

The weekly rankings implementation follows the same clean pattern as the daily endpoint, with appropriate use of YearWeek for time-based queries.


51-65: LGTM! Consistent monthly rankings implementation.

The monthly rankings endpoint maintains consistency with the other time-period endpoints, properly utilizing YearMonth for temporal scoping.

apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (3)

25-31: LGTM! Clear daily search implementation.

The method correctly builds a pageable request and delegates to the repository with appropriate date filtering.


33-39: LGTM! Weekly search correctly implemented.

The method properly uses YearWeek for weekly aggregation and follows the same clean pattern as the daily search.


41-47: LGTM! Monthly search follows established pattern.

The monthly search implementation correctly uses YearMonth and maintains consistency with the other time-period methods.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingResponse.java (2)

11-30: LGTM! Clean refactoring of daily response with centralized item mapping.

The refactored SearchDaily response correctly delegates item mapping to the shared Item.from() method, reducing code duplication.


80-101: LGTM! Well-structured shared Item class.

The extracted Item class with its centralized from() factory method effectively eliminates duplication and provides a clean mapping from the output layer.

apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingOutput.java (2)

10-30: LGTM! Clean daily output implementation.

The renamed SearchDaily record properly uses the centralized Item::from mapping to reduce duplication.


82-99: LGTM! Clean Item mapper implementation.

The Item record with its from() factory method provides a clear and type-safe mapping from ProductResult.GetProductDetail.

apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingResult.java (1)

10-32: LGTM! Well-structured daily result with proper pagination handling.

The SearchDaily record correctly uses the pageable's page number directly, which aligns with the documented learning about intentional 0-based indexing.

*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_ranking_weekly_id", nullable = false, updatable = false)

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

Fix incorrect column name for the monthly ranking entity's primary key.

The column name product_ranking_weekly_id appears to be a copy-paste error from the weekly entity. It should be product_ranking_monthly_id for consistency.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
-   @Column(name = "product_ranking_weekly_id", nullable = false, updatable = false)
+   @Column(name = "product_ranking_monthly_id", nullable = false, updatable = false)
    private Long id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column(name = "product_ranking_weekly_id", nullable = false, updatable = false)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_ranking_monthly_id", nullable = false, updatable = false)
private Long id;
🤖 Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java
around line 26, the JPA @Column annotation uses the wrong column name
"product_ranking_weekly_id" (copy-paste error); change it to
"product_ranking_monthly_id" and keep nullable=false, updatable=false as-is so
the primary key column name matches the monthly entity.

Comment on lines +5 to +8
import org.threeten.extra.YearWeek;

import java.time.LocalDate;
import java.time.YearMonth;

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

🧩 Analysis chain

Consider adding JPA AttributeConverters for YearWeek and YearMonth.

JPA offers the AttributeConverter for mapping custom types. When using JPA, the AttributeConverter interface can define the conversion logic between Java YearMonth/YearWeek types and database column types. Since JPA doesn't support YearMonth and YearWeek out-of-the-box, you'll need converters for proper persistence.

Run the following script to check if AttributeConverters are already implemented:

Also applies to: 17-20


🏁 Script executed:

#!/bin/bash
# Description: Check for existing JPA AttributeConverters for YearWeek and YearMonth types

echo "Searching for YearWeek AttributeConverter..."
rg -nP --type=java "AttributeConverter.*YearWeek|YearWeek.*AttributeConverter" -A 5 -B 2

echo -e "\nSearching for YearMonth AttributeConverter..."  
rg -nP --type=java "AttributeConverter.*YearMonth|YearMonth.*AttributeConverter" -A 5 -B 2

echo -e "\nSearching for @Converter annotations..."
rg -nP --type=java "@Converter" -A 10 -B 2

echo -e "\nSearching for any existing converters..."
ast-grep --pattern 'class $_ implements AttributeConverter<$_, $_>'

Length of output: 16236


Add JPA AttributeConverters for YearWeek and YearMonth

No converters found for YearWeek or YearMonth — add YearMonthConverter and YearWeekConverter (implement AttributeConverter<YearMonth,String> and AttributeConverter<YearWeek,String>, annotate @converter) in apps/commerce-api/src/main/java/com/loopers/config/jpa/converter/ to persist as ISO strings (YYYY-MM / YYYY-Www).

Comment on lines +28 to +29
@Column(name = "product_ranking_weekly_id", nullable = false, updatable = false)
private Long id;

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

Fix incorrect column name in @column annotation

The column is named product_ranking_weekly_id but this is the ProductRankingMonthly entity. This appears to be a copy-paste error from the weekly entity.

Apply this diff to fix the column name:

-    @Column(name = "product_ranking_weekly_id", nullable = false, updatable = false)
+    @Column(name = "product_ranking_monthly_id", nullable = false, updatable = false)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column(name = "product_ranking_weekly_id", nullable = false, updatable = false)
private Long id;
@Column(name = "product_ranking_monthly_id", nullable = false, updatable = false)
private Long id;
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingMonthly.java
around lines 28-29, the @Column annotation uses the wrong column name
product_ranking_weekly_id; change it to product_ranking_monthly_id to match this
entity (keep nullable = false and updatable = false as-is).

Comment on lines +18 to +24
@Query("""
insert into ProductRankingDaily (date, productId, rank, createdAt, updatedAt)
values (:date, :productId, :rank, :createdAt, :updatedAt)
on conflict (date, productId) do update set
rank = :rank,
updatedAt = :updatedAt
""")

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

Fix the native SQL query to use correct column names.

The @Query annotation uses entity property names instead of database column names. Since this is a native SQL query (indicated by the INSERT statement), you must use the actual database column names.

Based on the entity mapping patterns seen in other files, apply this fix:

    @Query("""
-               insert into ProductRankingDaily (date, productId, rank, createdAt, updatedAt)
+               insert into product_rankings_daily (date, ref_product_id, rank, created_at, updated_at)
                values (:date, :productId, :rank, :createdAt, :updatedAt)
-               on conflict (date, productId) do update set
+               on conflict (date, ref_product_id) do update set
                             rank = :rank,
-                            updatedAt = :updatedAt
+                            updated_at = :updatedAt
            """)

Additionally, you should add nativeQuery = true to the @Query annotation since this is native SQL:

    @Modifying
-   @Query("""
+   @Query(value = """
                insert into product_rankings_daily (date, ref_product_id, rank, created_at, updated_at)
                values (:date, :productId, :rank, :createdAt, :updatedAt)
                on conflict (date, ref_product_id) do update set
                             rank = :rank,
                             updated_at = :updatedAt
-           """)
+           """, nativeQuery = true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Query("""
insert into ProductRankingDaily (date, productId, rank, createdAt, updatedAt)
values (:date, :productId, :rank, :createdAt, :updatedAt)
on conflict (date, productId) do update set
rank = :rank,
updatedAt = :updatedAt
""")
@Query(value = """
insert into product_rankings_daily (date, ref_product_id, rank, created_at, updated_at)
values (:date, :productId, :rank, :createdAt, :updatedAt)
on conflict (date, ref_product_id) do update set
rank = :rank,
updated_at = :updatedAt
""", nativeQuery = true)
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingDailyJpaRepository.java
around lines 18 to 24, the @Query uses entity property names instead of DB
column names and is missing nativeQuery=true; change the SQL to use actual
column names (e.g. product_id, created_at, updated_at) in both the INSERT and ON
CONFLICT clauses and add nativeQuery = true to the @Query annotation; also
update the ON CONFLICT DO UPDATE assignment to reference the excluded values
(e.g. set rank = EXCLUDED.rank, updated_at = EXCLUDED.updated_at) so the native
upsert behaves correctly.

Comment on lines +16 to +31
@Modifying
@Query("""
insert into ProductRankingMonthly (yearMonth, productId, rank, createdAt, updatedAt)
values (:yearMonth, :productId, :rank, :createdAt, :updatedAt)
on conflict (yearMonth, productId) do update set
rank = :rank,
updatedAt = :updatedAt
""")
int merge(
@Param("yearMonth") YearMonth yearMonth,
@Param("productId") Long productId,
@Param("rank") Integer rank,
@Param("createdAt") ZonedDateTime createdAt,
@Param("updatedAt") ZonedDateTime updatedAt
);

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

🧩 Analysis chain

Verify "on conflict" syntax compatibility with your database.

The query uses PostgreSQL-specific on conflict ... do update syntax. Ensure this is compatible with your target database system.

Run the following script to check the database configuration and verify syntax compatibility:


🏁 Script executed:

#!/bin/bash
# Description: Check database configuration and JPA dialect settings

echo "Checking database configuration in application files..."
fd -e yml -e yaml -e properties --exec rg -nH "spring\.datasource|hibernate\.dialect|database" {}

echo -e "\nSearching for similar upsert/merge queries..."
rg -nP --type=java "on conflict|merge into|upsert" -A 3 -B 1

echo -e "\nChecking for database-specific annotations or configurations..."
rg -nP --type=java "@Query.*merge|@Query.*conflict" -A 5 -B 2

Length of output: 7597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Searching for PostgreSQL JDBC driver or 'postgres' mentions in build files and docs..."
rg -n --hidden --no-ignore --glob '!**/target/**' --glob '!**/build/**' "org\.postgresql|postgresql|jdbc:postgresql|jdbc:postgres" -S || true

echo -e "\n2) Searching for spring datasource / JPA database-platform / hibernate dialect entries..."
rg -n --hidden --no-ignore --glob '!**/target/**' --glob '!**/build/**' "spring\.datasource\.url|spring\.datasource\.driver-class-name|spring\.jpa\.database-platform|hibernate\.dialect|spring\.datasource" -S || true

echo -e "\n3) Searching for Docker / compose / k8s / workflows mentioning postgres or image: postgres..."
rg -n --hidden --no-ignore --glob '!**/target/**' --glob '!**/build/**' "image:\s*postgres|postgres:|postgresql:|POSTGRES_USER|POSTGRES_PASSWORD|postgres" -S || true

echo -e "\n4) Search in .github workflows and Dockerfiles for DATABASE_URL / PGHOST / PGUSER / POSTGRES env vars..."
rg -n --hidden --no-ignore --glob '!**/target/**' --glob '!**/build/**' "DATABASE_URL|PGHOST|PGUSER|POSTGRES|POSTGRES_DB|POSTGRES_USER|POSTGRES_PASSWORD" -S || true

echo -e "\n5) Search for 'on conflict' usage count (sanity)..."
rg -n --hidden --no-ignore --glob '!**/target/**' --glob '!**/build/**' "on conflict" -S || true

echo -e "\n6) Search README / docs for 'Postgres' mentions..."
rg -n --hidden --no-ignore --glob '!**/target/**' --glob '!**/build/**' -g 'README*' "postgres|postgresql" -S || true

Length of output: 1231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "A) Search for Postgres/JDBC driver mentions..."
rg -n --hidden -uu -S "org\.postgresql|jdbc:postgresql|jdbc:postgres|postgresql" || true

echo -e "\nB) Search for spring datasource / JPA / hibernate dialect settings..."
rg -n --hidden -uu -S "spring\.datasource|spring\.jpa\.database-platform|hibernate\.dialect|spring\.datasource\.url|spring\.datasource\.driver-class-name|spring\.jpa" || true

echo -e "\nC) Search for 'on conflict' occurrences (sanity)..."
rg -n --hidden -uu -S "on conflict" || true

echo -e "\nD) Search for Docker/compose/workflow envs or images mentioning Postgres..."
rg -n --hidden -uu -S "image:\s*postgres|postgres:|postgresql:|POSTGRES_|POSTGRES_DB|PGHOST|DATABASE_URL" || true

echo -e "\nE) Show modules/jpa application-jpa.yml (if exists)..."
if [ -f "modules/jpa/src/main/resources/application-jpa.yml" ]; then
  echo "---- modules/jpa/src/main/resources/application-jpa.yml ----"
  sed -n '1,240p' modules/jpa/src/main/resources/application-jpa.yml || true
else
  echo "modules/jpa/src/main/resources/application-jpa.yml not found"
fi

echo -e "\nF) Show top-level README mentions of Postgres (if any)..."
rg -n --hidden -uu -S "postgres|postgresql" README* || true

Length of output: 5899


'ON CONFLICT' is PostgreSQL-only — fix required

modules/jpa/src/main/resources/application-jpa.yml configures mysql-jpa (com.mysql.cj.jdbc.Driver, jdbc:mysql://...), but multiple @query SQL use "on conflict" (e.g. apps/commerce-batch/.../ProductRankingMonthlyJpaRepository.java, ProductRankingDailyJpaRepository.java, ProductRankingWeeklyJpaRepository.java; apps/commerce-api/.../OutboxJpaRepository.java, InboxJpaRepository.java, LikedProductJpaRepository.java; apps/commerce-streamer/.../ProductMetricDailyJpaRepository.java). "ON CONFLICT" will fail on MySQL — either convert these queries to MySQL-compatible upsert syntax (INSERT ... ON DUPLICATE KEY UPDATE / INSERT IGNORE) or switch the datasource/dialect to PostgreSQL and update driver/config.

🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingMonthlyJpaRepository.java
around lines 16 to 31, the SQL uses PostgreSQL's "ON CONFLICT" which will fail
against the project's MySQL datasource; replace the PostgreSQL upsert with
MySQL-compatible syntax (INSERT ... ON DUPLICATE KEY UPDATE) or switch the
datasource/dialect to PostgreSQL. Specifically, change the query to use INSERT
... ON DUPLICATE KEY UPDATE setting rank=:rank, updatedAt=:updatedAt, ensure a
unique constraint/index exists on (yearMonth, productId) so the duplicate-key
upsert works, and verify any @Query/nativeQuery flags and parameter names remain
correct after the change.

Comment on lines +16 to +20
insert into ProductRankingWeekly (yearWeek, productId, rank, createdAt, updatedAt)
values (:yearWeek, :productId, :rank, :createdAt, :updatedAt)
on conflict (yearWeek, productId) do update set
rank = :rank,
updatedAt = :updatedAt

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

🧩 Analysis chain

Verify table and column names match database schema

When using native queries, table and column names must match the actual database schema exactly (case-sensitive in PostgreSQL). Ensure that ProductRankingWeekly, yearWeek, productId, etc., match your database naming convention.


🏁 Script executed:

#!/bin/bash
# Check for database migration files or entity definitions to verify the table schema
fd -e sql -e java | xargs rg -A5 -B5 "CREATE TABLE.*ProductRankingWeekly|product_ranking_weekly" --ignore-case

Length of output: 4730


Fix native SQL identifiers to match DB table/column names

Postgres folds unquoted identifiers to lower‑case; the entity uses snake_case (e.g. @column(name = "product_ranking_weekly_id")), so the native query's CamelCase/camelCase identifiers will not match the actual table/columns.

Action (files): apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingWeeklyJpaRepository.java (lines ~16–20). Verify against apps/commerce-batch/src/main/java/com/loopers/domain/ranking/ProductRankingWeekly.java.

Suggested fix — use snake_case (or quote identifiers). Example:
insert into product_ranking_weekly (year_week, product_id, rank, created_at, updated_at)
values (:yearWeek, :productId, :rank, :createdAt, :updatedAt)
on conflict (year_week, product_id) do update set
rank = EXCLUDED.rank,
updated_at = EXCLUDED.updated_at

🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/ProductRankingWeeklyJpaRepository.java
around lines 16–20, the native INSERT uses CamelCase identifiers that won't
match Postgres lower‑cased snake_case column/table names; update the SQL to use
the actual snake_case table and column names from ProductRankingWeekly (e.g.
product_ranking_weekly, year_week, product_id, created_at, updated_at), and in
the ON CONFLICT DO UPDATE use EXCLUDED.rank and EXCLUDED.updated_at (or properly
quoted identifiers) so the conflict update references the incoming values
correctly.

Comment on lines +25 to +31
public void launchRankingJob() throws Exception {
LocalDate yesterday = LocalDate.now().minusDays(1);
JobParameters jobParameters = new JobParametersBuilder()
.addLocalDate("date", yesterday)
.toJobParameters();
jobLauncher.run(job, jobParameters);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and logging for job execution.

The scheduler method lacks error handling and logging, which are crucial for monitoring batch job execution in production.

Apply this diff to add proper error handling and logging:

+    @Slf4j
     @Scheduled(cron = "5 0 0 * * *", zone = "Asia/Seoul")
     public void launchRankingJob() throws Exception {
+        try {
+            log.info("Starting daily ranking job for date: {}", LocalDate.now().minusDays(1));
             LocalDate yesterday = LocalDate.now().minusDays(1);
             JobParameters jobParameters = new JobParametersBuilder()
                     .addLocalDate("date", yesterday)
                     .toJobParameters();
             jobLauncher.run(job, jobParameters);
+            log.info("Successfully launched daily ranking job");
+        } catch (Exception e) {
+            log.error("Failed to launch daily ranking job", e);
+            throw e;
+        }
     }

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

🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/ranking/RankingScheduler.java
around lines 25 to 31, the launchRankingJob method currently runs the job
without any logging or exception handling; add start/finish info logs, capture
the JobExecution returned by jobLauncher.run, log the execution id and status on
success, and wrap the run call in a try/catch to log any Exception (including
the stack/exception message) with context; in the catch block record a failure
log and either rethrow a wrapped exception or handle the failure according to
existing application patterns (e.g., update metrics or alerting) so job failures
are observable in production.

Comment on lines +40 to +41
LocalDate startDate = date.with(TemporalAdjusters.firstDayOfMonth()).minusMonths(1);
LocalDate endDate = date.minusDays(1);

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

Date calculation logic may not handle edge cases correctly

The current logic calculates the previous month's range incorrectly. Line 40 gets the first day of the current month, then subtracts a month. This could fail for edge cases like March 31st (which would try to get February 31st before adjusting).

Apply this diff for more robust date calculations:

-        LocalDate startDate = date.with(TemporalAdjusters.firstDayOfMonth()).minusMonths(1);
-        LocalDate endDate = date.minusDays(1);
+        // Get the previous month first, then find its first day
+        LocalDate previousMonth = date.minusMonths(1);
+        LocalDate startDate = previousMonth.with(TemporalAdjusters.firstDayOfMonth());
+        LocalDate endDate = previousMonth.with(TemporalAdjusters.lastDayOfMonth());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LocalDate startDate = date.with(TemporalAdjusters.firstDayOfMonth()).minusMonths(1);
LocalDate endDate = date.minusDays(1);
// Get the previous month first, then find its first day
LocalDate previousMonth = date.minusMonths(1);
LocalDate startDate = previousMonth.with(TemporalAdjusters.firstDayOfMonth());
LocalDate endDate = previousMonth.with(TemporalAdjusters.lastDayOfMonth());
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/job/ranking/MonthlyRankingAggregationTasklet.java
around lines 40-41, the date-range calculation for the previous month should be
made robust by using YearMonth to avoid edge-case invalid dates; derive
YearMonth.from(date).minusMonths(1), then set startDate to that Year's month
first day and endDate to that month's last day (atEndOfMonth) so you always get
a valid first and last day for the previous month.

Comment on lines +119 to +121
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
LocalDate date = jobExecution.getJobParameters().getLocalDate("date");
String status = date.getDayOfWeek() == DayOfWeek.MONDAY ? "RUN" : "SKIP";

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

🧩 Analysis chain

Verify the weekly decider logic aligns with the tasklet's date calculation.

The decider runs weekly aggregation on Mondays, but given the date calculation issue in WeeklyRankingAggregationTasklet, ensure both components agree on which week's data to process.


🏁 Script executed:

#!/bin/bash
# Check how the date parameter is passed to the job
echo "Checking RankingScheduler to see how date is set..."
fd -e java "RankingScheduler" --exec grep -B 5 -A 10 "JobParameters\|date"

echo -e "\nChecking if there's documentation about the date parameter semantics..."
fd README -e md --exec grep -i -A 3 -B 3 "ranking\|date\|weekly"

Length of output: 1161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "-> Locating ProductRankingJobConfig.java"
pfile=$(fd -e java ProductRankingJobConfig.java --strip-cwd-prefix || true)
pfile=$(echo "$pfile" | head -n1 || true)
if [ -n "$pfile" ]; then
  echo "FOUND: $pfile"
  sed -n '1,260p' "$pfile"
else
  echo "NOT FOUND by filename; searching for class declaration..."
  rg -n --hidden "class\s+ProductRankingJobConfig" -S || true
fi

echo; echo "-> Locating WeeklyRankingAggregationTasklet.java"
tfile=$(fd -e java WeeklyRankingAggregationTasklet.java --strip-cwd-prefix || true)
tfile=$(echo "$tfile" | head -n1 || true)
if [ -n "$tfile" ]; then
  echo "FOUND: $tfile"
  sed -n '1,400p' "$tfile"
else
  echo "NOT FOUND by filename; searching for 'Weekly' + 'Aggregation' occurrences..."
  rg -n --hidden "Weekly.*Aggregation|Aggregation.*Weekly" -S || true
fi

echo; echo "-> Searching for usages of the job parameter 'date'"
rg -n --hidden "\\.getLocalDate\\(\"date\"\\)|addLocalDate\\(\"date\"\\)|getJobParameters\\(\\)\\.getLocalDate\\(\"date\"\\)" -S || true

Length of output: 8997


Align 'date' semantics between RankingScheduler and deciders (weekly/monthly)

  • RankingScheduler passes job param "date" = LocalDate.now().minusDays(1) (yesterday). WeeklyDecider returns RUN only when date.getDayOfWeek() == MONDAY, so the weekly flow will SKIP on Mondays (scheduler passes Sunday). WeeklyRankingAggregationTasklet computes start/end with previousOrSame(MONDAY)/previousOrSame(SUNDAY) and expects date to represent the week-end (Sunday).
  • Fix (pick one):
    • A) Keep scheduler passing yesterday and change WeeklyDecider to check for SUNDAY (and change MonthlyDecider to check for last-day-of-month), or
    • B) Pass LocalDate.now() from RankingScheduler and keep deciders as MONDAY / day==1.
  • Files to update: apps/commerce-batch/src/main/java/com/loopers/interfaces/scheduler/ranking/RankingScheduler.java; apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java; apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java.
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/job/ranking/ProductRankingJobConfig.java
around lines 119-121, the WeeklyDecider currently checks date.getDayOfWeek() ==
MONDAY while RankingScheduler passes date = LocalDate.now().minusDays(1)
(yesterday), causing weekly runs to SKIP on Mondays; align semantics by
implementing option A: keep the scheduler passing yesterday and change deciders
to treat the passed date as the period-end. Concretely, update WeeklyDecider to
check for DayOfWeek.SUNDAY instead of MONDAY; update MonthlyDecider to detect
month-end by comparing date to date.with(TemporalAdjusters.lastDayOfMonth()) (or
date.getDayOfMonth() == date.lengthOfMonth()); leave RankingScheduler unchanged;
review WeeklyRankingAggregationTasklet to ensure its
previousOrSame(MONDAY)/previousOrSame(SUNDAY) logic remains correct given date
represents the week-end and adjust only if any inverted assumptions remain.

Comment on lines +40 to +43
// 지난 주 월요일 ~ 일요일
LocalDate startDate = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
LocalDate endDate = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));

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

Potential logic error in weekly date range calculation.

The date range calculation uses previousOrSame for both start and end dates, which creates incorrect ranges. When date is a Monday, this produces Monday-Sunday of the current week, not the previous week as the comment suggests.

Apply this fix to correctly calculate the previous week's range:

-        // 지난 주 월요일 ~ 일요일
-        LocalDate startDate = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
-        LocalDate endDate = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
+        // 지난 주 월요일 ~ 일요일
+        LocalDate endDate = date.minusDays(1); // Previous day (Sunday if date is Monday)
+        LocalDate startDate = endDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));

The current logic would aggregate the wrong week's data when executed on Monday.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 지난 주 월요일 ~ 일요일
LocalDate startDate = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
LocalDate endDate = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
// 지난 주 월요일 ~ 일요일
LocalDate endDate = date.minusDays(1); // Previous day (Sunday if date is Monday)
LocalDate startDate = endDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
🤖 Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/job/ranking/WeeklyRankingAggregationTasklet.java
around lines 40-43, the code uses previousOrSame for both start and end which
yields the current week when run on Monday; change the logic to compute the
previous week's range by shifting the computed boundaries one week back (e.g.,
compute startDate as date.with(previousOrSame(MONDAY)).minusWeeks(1) and compute
endDate either as startDate.plusDays(6) or as
date.with(previousOrSame(SUNDAY)).minusWeeks(1)) so the tasklet consistently
covers the full previous week.

@ImSejin

ImSejin commented Sep 28, 2025

Copy link
Copy Markdown
Member Author

종합 피드백 (9월 24일 2025)

일간 랭킹 테이블 대신 실시간 랭킹의 TTL을 한 달 이상으로 한다면?
맨 처음에 실시간으로 집계되는 일간 랭킹이 레디스에 있으니 이를 활용하자고 생각했습니다.
하지만 바로 문제가 생겼습니다. TTL이 3일로 지정되어 있었기 때문입니다.
TTL을 한 달 이상으로 설정하면 해결은 되겠으나, 메모리를 상당히 많이 차지할 거 같습니다.
분기별/연간 랭킹 요구사항이 들어온다면 TTL을 또 늘려야 하니 비효율적일 거 같다고 생각합니다.

그럼에도 불구하고, 실무에서 레디스에 집계되는 실시간 랭킹의 TTL을 배치를 위해 길게 잡는 경우가 있을까요?

음.. 보통 무기한 ttl로 잡고 write-through로 하는 패턴은 간헐적으로 있는것 같습니다. 개인적으로는 거의 write-through로 하는 경우가 많은것 같고 혹은 caffein cache를통해 지속적으로 refresh하는 경우도 많은것 같아요!

자주 봅시다 세진님!

Implementation Quest: PASS

Writing Quest: PASS

@ImSejin ImSejin merged commit 828f0ff into master Sep 28, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant