-
Notifications
You must be signed in to change notification settings - Fork 44
[Volume - 10] Spring Batch 기반 주간/월간 랭킹 집계 시스템 구현 #411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: ukukdin
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.loopers.domain.model.ranking; | ||
|
|
||
| public enum RankingPeriod { | ||
| DAILY, | ||
| WEEKLY, | ||
| MONTHLY | ||
| } | ||
|
Comment on lines
+3
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -nP --type=java -C3 'RankingPeriod\s+period'
rg -nP --type=java -C5 '@RequestParam.*RankingPeriod'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 4467 🏁 Script executed: #!/bin/bash
# Find ApiControllerAdvice or similar exception handlers
fd -t f -e java | xargs rg -l 'ApiControllerAdvice|@ControllerAdvice' | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: #!/bin/bash
# Check full RankingPeriod enum with all annotations
cat -n apps/commerce-api/src/main/java/com/loopers/domain/model/ranking/RankingPeriod.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 241 🏁 Script executed: #!/bin/bash
# Search for RankingController tests
find . -path '*test*' -name '*RankingController*' -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: #!/bin/bash
# Search for custom converters or JsonCreator for enum handling
rg -nP --type=java '@JsonCreator|Converter.*Ranking|ConfigureWebMvc' apps/commerce-api/srcRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: #!/bin/bash
# Search for any ControllerAdvice or exception handler more broadly
rg -nP --type=java 'ControllerAdvice|RestControllerAdvice|ExceptionHandler' apps/commerce-api/src/mainRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1473 🏁 Script executed: #!/bin/bash
# Search for all test files
find . -type f -name '*.java' | grep -i test | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1808 🏁 Script executed: #!/bin/bash
# Check RankingController file completely
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingController.java | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2068 🏁 Script executed: #!/bin/bash
# Check the full GlobalExceptionHandler to see how Exception is handled
cat -n apps/commerce-api/src/main/java/com/loopers/support/error/GlobalExceptionHandler.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 3933 🏁 Script executed: #!/bin/bash
# Search specifically for RankingController tests
find . -type f -name '*.java' | xargs rg -l 'RankingController' | grep -i testRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 MethodArgumentTypeMismatchException에 대한 에러 처리 추가가 필수이다.
다음을 적용할 것을 권고한다.
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.loopers.domain.model.ranking; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.time.temporal.WeekFields; | ||
| import java.util.Locale; | ||
|
|
||
| public final class RankingPeriodKeyResolver { | ||
|
|
||
| private static final DateTimeFormatter YEAR_MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM"); | ||
|
|
||
| private RankingPeriodKeyResolver() {} | ||
|
|
||
| public static String toYearWeek(LocalDate date) { | ||
| WeekFields weekFields = WeekFields.of(Locale.KOREA); | ||
| int year = date.getYear(); | ||
| int week = date.get(weekFields.weekOfWeekBasedYear()); | ||
| return String.format("%d-W%02d", year, week); | ||
| } | ||
|
Comment on lines
+14
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Java's java.time.temporal.WeekFields, weekOfWeekBasedYear returns a TemporalField for the week number within the week-based year, and weekBasedYear returns the TemporalField for the week-based year itself. These fields define a week-based-year system where each week belongs to exactly one year: Week 1 is the first week starting on the first day-of-week (e.g., Monday in ISO) with at least the minimal days in the first week (e.g., 4). The first/last weeks may overlap calendar years. Key difference from standard year/week-of-year: week-based-year can differ from the proleptic calendar year near year boundaries. Example from docs (Monday first, min 4 days): 2008-12-31 (Wed) is week 1 of 2009 in one config, week 53 of 2008 in another. For Locale.KOREA (ko_KR), WeekFields.of(Locale.KOREA) uses locale-specific firstDayOfWeek (Sunday, as Korean weekdays start with 일요일) and minimalDaysInFirstWeek (likely 1, similar to US). This affects week numbering: e.g., first days of calendar year may belong to prior week-based-year's last week. Usage example: WeekFields wf = WeekFields.of(Locale.KOREA); LocalDate date = LocalDate.of(2026, 1, 1); int wby = date.get(wf.weekBasedYear); // May be 2025 if Jan 1 in last week of prior WB-year int wowby = date.get(wf.weekOfWeekBasedYear); // e.g., 52 or 53 No explicit numerical "difference" found; the fields work together for week-date representation (WB-year + week-of-WB-year + day-of-week), differing from ChronoField.YEAR + weekOfYear by strict week-year alignment. Citations:
🐛 제안 수정 public static String toYearWeek(LocalDate date) {
WeekFields weekFields = WeekFields.of(Locale.KOREA);
- int year = date.getYear();
- int week = date.get(weekFields.weekOfWeekBasedYear());
+ int year = date.get(weekFields.weekBasedYear());
+ int week = date.get(weekFields.weekOfWeekBasedYear());
return String.format("%d-W%02d", year, week);
}연말/연초 경계일자(2023-01-01, 2024-12-30, 2024-12-31, 2025-01-01 등)에 대해 API·배치 두 Resolver가 동일한 키를 생성하는지 검증하는 파라미터화 테스트를 추가해야 한다. 🤖 Prompt for AI Agents |
||
|
|
||
| public static String toYearMonth(LocalDate date) { | ||
| return date.format(YEAR_MONTH_FORMAT); | ||
| } | ||
|
Comment on lines
+8
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 동일 로직이
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.loopers.domain.repository; | ||
|
|
||
| import com.loopers.domain.repository.RankingRepository.RankedProduct; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| public interface PeriodRankingRepository { | ||
|
|
||
| List<RankedProduct> getWeeklyRankings(LocalDate date, int offset, int size); | ||
|
|
||
| long getWeeklyTotalCount(LocalDate date); | ||
|
|
||
| List<RankedProduct> getMonthlyRankings(LocalDate date, int offset, int size); | ||
|
|
||
| long getMonthlyTotalCount(LocalDate date); | ||
|
Comment on lines
+10
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 저장된 랭크의 1-based ↔ 0-based 변환 규약을 인터페이스 문서로 명시할 것을 권고한다. 동일
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.domain.model.ranking.RankingPeriodKeyResolver; | ||
| import com.loopers.domain.repository.PeriodRankingRepository; | ||
| import com.loopers.domain.repository.RankingRepository.RankedProduct; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| public class JpaPeriodRankingRepository implements PeriodRankingRepository { | ||
|
|
||
| private final ProductRankWeeklyJpaRepository weeklyRepository; | ||
| private final ProductRankMonthlyJpaRepository monthlyRepository; | ||
|
|
||
| public JpaPeriodRankingRepository(ProductRankWeeklyJpaRepository weeklyRepository, | ||
| ProductRankMonthlyJpaRepository monthlyRepository) { | ||
| this.weeklyRepository = weeklyRepository; | ||
| this.monthlyRepository = monthlyRepository; | ||
| } | ||
|
|
||
| @Override | ||
| public List<RankedProduct> getWeeklyRankings(LocalDate date, int offset, int size) { | ||
| String yearWeek = RankingPeriodKeyResolver.toYearWeek(date); | ||
| PageRequest pageable = PageRequest.of(offset / size, size); | ||
| return weeklyRepository.findByYearWeekOrderByRanking(yearWeek, pageable).stream() | ||
| .map(e -> new RankedProduct(e.getProductId(), e.getScore(), e.getRanking() - 1L)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엔티티의
운영 관점에서는 고객이 "1위 상품이 없고 0위가 있다"는 UI 이슈로 문의를 올리기 쉬우므로, 엔티티 생성 시 Also applies to: 43-43 🤖 Prompt for AI Agents |
||
| .toList(); | ||
| } | ||
|
|
||
| @Override | ||
| public long getWeeklyTotalCount(LocalDate date) { | ||
| return weeklyRepository.countByYearWeek(RankingPeriodKeyResolver.toYearWeek(date)); | ||
| } | ||
|
|
||
| @Override | ||
| public List<RankedProduct> getMonthlyRankings(LocalDate date, int offset, int size) { | ||
| String yearMonth = RankingPeriodKeyResolver.toYearMonth(date); | ||
| PageRequest pageable = PageRequest.of(offset / size, size); | ||
| return monthlyRepository.findByYearMonthOrderByRanking(yearMonth, pageable).stream() | ||
| .map(e -> new RankedProduct(e.getProductId(), e.getScore(), e.getRanking() - 1L)) | ||
| .toList(); | ||
| } | ||
|
|
||
| @Override | ||
| public long getMonthlyTotalCount(LocalDate date) { | ||
| return monthlyRepository.countByYearMonth(RankingPeriodKeyResolver.toYearMonth(date)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.infrastructure.ranking.entity.ProductRankMonthlyJpaEntity; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface ProductRankMonthlyJpaRepository extends JpaRepository<ProductRankMonthlyJpaEntity, Long> { | ||
|
|
||
| @Query("SELECT e FROM ProductRankMonthlyJpaEntity e WHERE e.yearMonth = :yearMonth ORDER BY e.ranking ASC") | ||
| List<ProductRankMonthlyJpaEntity> findByYearMonthOrderByRanking( | ||
| @Param("yearMonth") String yearMonth, Pageable pageable); | ||
|
|
||
| long countByYearMonth(String yearMonth); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.infrastructure.ranking.entity.ProductRankWeeklyJpaEntity; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface ProductRankWeeklyJpaRepository extends JpaRepository<ProductRankWeeklyJpaEntity, Long> { | ||
|
|
||
| @Query("SELECT e FROM ProductRankWeeklyJpaEntity e WHERE e.yearWeek = :yearWeek ORDER BY e.ranking ASC") | ||
| List<ProductRankWeeklyJpaEntity> findByYearWeekOrderByRanking( | ||
| @Param("yearWeek") String yearWeek, Pageable pageable); | ||
|
|
||
| long countByYearWeek(String yearWeek); | ||
|
Comment on lines
+13
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 주간/월간 MV 테이블 스키마 및 인덱스 선언을 레포 내에서 조사
rg -nP -C3 'mv_product_rank_(weekly|monthly)'
fd -t f -e sql | xargs -I{} grep -l -iE 'mv_product_rank' {} 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 4336 🏁 Script executed: #!/bin/bash
# 1. Flyway/Liquibase 마이그레이션 파일 찾기
echo "=== Migration files ==="
fd -e sql -e yaml -e xml | grep -iE '(flyway|liquibase|migration|schema)' | head -20
# 2. ProductRankWeeklyJpaEntity 의 인덱스 정의 확인
echo "=== ProductRankWeeklyJpaEntity content ==="
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/entity/ProductRankWeeklyJpaEntity.java
# 3. JpaPeriodRankingRepository 찾기
echo "=== JpaPeriodRankingRepository ==="
find . -type f -name '*JpaPeriodRankingRepository*'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1600 🏁 Script executed: #!/bin/bash
# ProductRankWeeklyJpaRepository 전체 내용 확인
echo "=== ProductRankWeeklyJpaRepository full content ==="
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/ProductRankWeeklyJpaRepository.java
# 호출부 찾기
echo "=== Callers of ProductRankWeeklyJpaRepository ==="
rg 'ProductRankWeeklyJpaRepository|findByYearWeekOrderByRanking' -A2 -B2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 6556 🏁 Script executed: #!/bin/bash
# 스키마 정의 파일 검색
echo "=== Schema SQL files ==="
fd -e sql
echo ""
echo "=== application.yml/properties DB config ==="
find . -name 'application*.yml' -o -name 'application*.properties' | xargs grep -l 'jpa\|hibernate' 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 519 🏁 Script executed: #!/bin/bash
# V5__create_product_indexes.sql 확인
echo "=== V5__create_product_indexes.sql ==="
cat -n scripts/V5__create_product_indexes.sql
# MV 테이블 생성 스크립트 찾기
echo ""
echo "=== MV table creation scripts ==="
rg -n 'CREATE TABLE.*mv_product_rank' -A10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1298 🏁 Script executed: #!/bin/bash
# 모든 SQL 스크립트에서 mv_product_rank 관련 내용 확인
echo "=== All mentions of mv_product_rank in SQL files ==="
fd -e sql -x grep -l -i 'mv_product_rank' {} \; | while read f; do
echo "=== File: $f ==="
cat -n "$f" | grep -i -E '(CREATE TABLE|INDEX|mv_product_rank)' -A2 -B2
doneRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 137 🏁 Script executed: #!/bin/bash
# Flyway/Liquibase 설정 확인
echo "=== Flyway config in application.yml ==="
grep -A20 -i 'flyway\|liquibase' apps/commerce-api/src/main/resources/application.yml
# 모든 마이그레이션 디렉토리 확인
echo ""
echo "=== Migration directories ==="
find . -type d -name 'db' -o -name 'migration' -o -name 'flyway' -o -name 'liquibase' 2>/dev/null | head -20
# V1, V2, V3, V4 등 다른 버전 스크립트 찾기
echo ""
echo "=== All migration scripts ==="
find . -name 'V*.sql' -o -name 'V*.yaml'
# 테이블 생성 관련 파일 찾기
echo ""
echo "=== Table creation queries ==="
rg 'CREATE TABLE' -nRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 433 🏁 Script executed: #!/bin/bash
# 엔티티 매핑 재확인 - JPA가 테이블 자동 생성하는지 확인
echo "=== JPA DDL settings in application.yml ==="
grep -E 'hibernate.*ddl|jpa.*ddl|generate-ddl|spring.jpa' apps/commerce-api/src/main/resources/application.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 128 🏁 Script executed: #!/bin/bash
echo "=== commerce-api application.yml (full) ==="
cat -n apps/commerce-api/src/main/resources/application.yml | head -80
echo ""
echo "=== Spring Data JPA related settings ==="
rg -i 'jpa|hibernate|ddl' apps/commerce-api/src/main/resources/application.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2639 🏁 Script executed: #!/bin/bash
# MV 테이블 생성 관련 문서 확인
echo "=== Documentation about MV tables ==="
cat -n claudedocs/10weeks/lec10.md | grep -A15 -B5 'Materialized View\|mv_product'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2918 🏁 Script executed: #!/bin/bash
echo "=== jpa.yml content ==="
find . -name 'jpa.yml' -exec cat -n {} \;
echo ""
echo "=== batch application.yml for JPA settings ==="
grep -E 'jpa|hibernate|ddl' apps/commerce-batch/src/main/resources/application.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2709 🏁 Script executed: #!/bin/bash
# 배치에서 MV 테이블을 어떻게 생성/관리하는지 확인
echo "=== ProductRankWeeklyEntity in batch (with table definition) ==="
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/entity/ProductRankWeeklyEntity.java
echo ""
echo "=== WeeklyRankingJobConfig ==="
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java | head -100Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 7831
ProductRankWeeklyJpaEntity와 배치 ProcessRankWeeklyEntity 모두 `@Entity`
`@Table`(name = "mv_product_rank_weekly",
indexes = `@Index`(name = "idx_year_week_ranking", columnList = "yearWeek, ranking"))
public class ProductRankWeeklyJpaEntity {
// ...
}현재 호출부( 이후 운영 환경에서 쿼리 실행 계획(EXPLAIN)을 검증해 인덱스 스캔이 실제로 발생하는지 확인할 것. 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.loopers.infrastructure.ranking.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Table(name = "mv_product_rank_monthly") | ||
| public class ProductRankMonthlyJpaEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(nullable = false, length = 7) | ||
| private String yearMonth; | ||
|
|
||
| @Column(nullable = false) | ||
| private long likeCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private long orderCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private long viewCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private long totalSalesAmount; | ||
|
|
||
| @Column(nullable = false) | ||
| private double score; | ||
|
|
||
| @Column(nullable = false) | ||
| private int ranking; | ||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime updatedAt; | ||
|
Comment on lines
+14
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 스키마 선언 및 엔티티 생성자/빌더 확인
fd -t f ProductRankMonthlyEntity.java
rg -nP -C3 'ProductRankMonthlyJpaEntity|mv_product_rank_monthly'
rg -nP -C3 '@PrePersist|@PreUpdate'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 6175 🏁 Script executed: # ProductRankMonthlyJpaEntity 전체 파일 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/entity/ProductRankMonthlyJpaEntity.java
# ProductRankMonthlyEntity 전체 파일 내용 확인
cat -n apps/commerce-batch/src/main/java/com/loopers/batch/infrastructure/entity/ProductRankMonthlyEntity.java
# BaseEntity 확인
cat -n modules/jpa/src/main/java/com/loopers/domain/BaseEntity.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 6211 🏁 Script executed: # API 엔티티 사용처 확인
rg -n "ProductRankMonthlyJpaEntity" apps/commerce-api --type java -A 2 -B 2 | head -80
# API 엔티티 저장/수정 로직이 있는지 확인
rg -n "save|saveAll|persist" apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 3065 API 엔티티가 유일성 제약과 타임스탬프 자동화를 누락하여 배치와 스키마 정의가 불일치한다. 현재 운영상 문제점:
다음을 수정한다:
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||
| package com.loopers.infrastructure.ranking.entity; | ||||||||||
|
|
||||||||||
| import jakarta.persistence.*; | ||||||||||
| import lombok.AccessLevel; | ||||||||||
| import lombok.Getter; | ||||||||||
| import lombok.NoArgsConstructor; | ||||||||||
|
|
||||||||||
| import java.time.LocalDateTime; | ||||||||||
|
|
||||||||||
| @Entity | ||||||||||
| @Getter | ||||||||||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||||||||||
| @Table(name = "mv_product_rank_weekly") | ||||||||||
| public class ProductRankWeeklyJpaEntity { | ||||||||||
|
|
||||||||||
| @Id | ||||||||||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||||||||||
| private Long id; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private Long productId; | ||||||||||
|
|
||||||||||
| @Column(nullable = false, length = 7) | ||||||||||
| private String yearWeek; | ||||||||||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 배치 쪽 동일 엔티티의 yearWeek 길이 및 키 생성 로직 확인
fd -t f ProductRankWeeklyEntity.java
rg -nP -C2 'yearWeek' --type=java
rg -nP -C2 'toYearWeek|%d-W%02d' --type=javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 13581
이 문제는 수정안- `@Column`(nullable = false, length = 7)
+ `@Column`(nullable = false, length = 8)
private String yearWeek;양쪽 엔티티 모두 적용 필요:
추가 검증 필요: 배치 INSERT 후 API 조회가 실제로 데이터를 반환하는지 확인하는 통합 테스트 작성. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private long likeCount; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private long orderCount; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private long viewCount; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private long totalSalesAmount; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private double score; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private int ranking; | ||||||||||
|
|
||||||||||
| @Column(nullable = false) | ||||||||||
| private LocalDateTime updatedAt; | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.loopers.batch.infrastructure.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Table(name = "product_metrics") | ||
| public class ProductMetricsEntity { | ||
|
|
||
| @Id | ||
| private Long productId; | ||
|
|
||
| @Column(nullable = false) | ||
| private long likeCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private long orderCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private long viewCount; | ||
|
|
||
| @Column(nullable = false) | ||
| private long totalSalesAmount; | ||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime updatedAt; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력 가드 부재로 size/page 경계값에서 500이 발생한다.
현재 메서드는
size <= 0,page < 0에 대한 사전 검증이 없다. 아래 경로에서 즉시 런타임 예외가 발생한다:size = 0: 하위JpaPeriodRankingRepository에서PageRequest.of(offset / size, size)→ArithmeticException, 또한 71행totalCount / size도 동일.page < 0:offset = page * size가 음수가 되어PageRequest.of에서IllegalArgumentException.이 서비스는 컨트롤러 외에도 재사용될 수 있고, 컨트롤러가 Bean Validation을 쓰지 않는 정책(학습)이므로 유효성은 도메인/서비스 계층에서 막아야 한다. 진입부에서
CoreException(BAD_REQUEST)로 일관된 에러 응답을 반환하도록 가드를 추가할 것을 권장한다.🛡️ 제안 diff
public PageResult<RankingItemInfo> getRankings(LocalDate date, int page, int size, RankingPeriod period) { + if (size <= 0 || page < 0 || period == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "invalid ranking query parameters"); + } int offset = page * size;추가 테스트로
size=0,page=-1,period=null각각에 대해 표준 에러 포맷이 반환되는지 확인하는 단위 테스트를 제안한다.As per coding guidelines("예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다.") — 예외 메시지는 사용자용/로그용을 분리해 민감한 쿼리 파라미터를 그대로 노출하지 않도록 주의한다.
🤖 Prompt for AI Agents