-
Notifications
You must be signed in to change notification settings - Fork 44
[volume-10] Spring Batch 기반 주간/월간 랭킹 시스템 구현 #396
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: move-wook
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,38 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.ZonedDateTime; | ||
|
|
||
| @Entity | ||
| @Table(name = "mv_product_rank_monthly") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class MvProductRankMonthly { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(name = "rank_position", nullable = false) | ||
| private Integer rankPosition; | ||
|
|
||
| @Column(name = "score", nullable = false) | ||
| private Double score; | ||
|
|
||
| @Column(name = "period_start", nullable = false) | ||
| private LocalDate periodStart; | ||
|
|
||
| @Column(name = "period_end", nullable = false) | ||
| private LocalDate periodEnd; | ||
|
|
||
| @Column(name = "created_at", nullable = false, updatable = false) | ||
| private ZonedDateTime createdAt; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.ZonedDateTime; | ||
|
|
||
| @Entity | ||
| @Table(name = "mv_product_rank_weekly") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class MvProductRankWeekly { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; | ||
|
|
||
| @Column(name = "rank_position", nullable = false) | ||
| private Integer rankPosition; | ||
|
|
||
| @Column(name = "score", nullable = false) | ||
| private Double score; | ||
|
|
||
| @Column(name = "period_start", nullable = false) | ||
| private LocalDate periodStart; | ||
|
|
||
| @Column(name = "period_end", nullable = false) | ||
| private LocalDate periodEnd; | ||
|
|
||
| @Column(name = "created_at", nullable = false, updatable = false) | ||
| private ZonedDateTime createdAt; | ||
| } | ||
|
Comment on lines
+14
to
+38
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. 생성자와
운영 관점:
🐛 수정안 `@Column`(name = "created_at", nullable = false, updatable = false)
private ZonedDateTime createdAt;
+
+public MvProductRankWeekly(Long productId, Integer rankPosition, Double score, LocalDate periodStart, LocalDate periodEnd) {
+ this.productId = productId;
+ this.rankPosition = rankPosition;
+ this.score = score;
+ this.periodStart = periodStart;
+ this.periodEnd = periodEnd;
+}
+
+@PrePersist
+private void prePersist() {
+ this.createdAt = ZonedDateTime.now();
+}추가 테스트: 배치 Job 통합 테스트에서 주간 랭킹 저장 후 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| public interface MvRankingRepository { | ||
|
|
||
| List<RankedProduct> findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size); | ||
|
|
||
| List<RankedProduct> findMonthlyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size); | ||
|
|
||
| long countWeeklyRanking(LocalDate periodStart, LocalDate periodEnd); | ||
|
|
||
| long countMonthlyRanking(LocalDate periodStart, LocalDate periodEnd); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.domain.ranking.MvProductRankMonthly; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| public interface MvProductRankMonthlyJpaRepository extends JpaRepository<MvProductRankMonthly, Long> { | ||
| List<MvProductRankMonthly> findByPeriodStartAndPeriodEndOrderByRankPositionAsc( | ||
| LocalDate periodStart, LocalDate periodEnd, Pageable pageable); | ||
| long countByPeriodStartAndPeriodEnd(LocalDate periodStart, LocalDate periodEnd); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.domain.ranking.MvProductRankWeekly; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| public interface MvProductRankWeeklyJpaRepository extends JpaRepository<MvProductRankWeekly, Long> { | ||
| List<MvProductRankWeekly> findByPeriodStartAndPeriodEndOrderByRankPositionAsc( | ||
| LocalDate periodStart, LocalDate periodEnd, Pageable pageable); | ||
| long countByPeriodStartAndPeriodEnd(LocalDate periodStart, LocalDate periodEnd); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.loopers.infrastructure.ranking; | ||
|
|
||
| import com.loopers.domain.ranking.MvRankingRepository; | ||
| import com.loopers.domain.ranking.RankedProduct; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class MvRankingRepositoryImpl implements MvRankingRepository { | ||
|
|
||
| private final MvProductRankWeeklyJpaRepository weeklyRepository; | ||
| private final MvProductRankMonthlyJpaRepository monthlyRepository; | ||
|
|
||
| @Override | ||
| public List<RankedProduct> findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size) { | ||
| return weeklyRepository.findByPeriodStartAndPeriodEndOrderByRankPositionAsc( | ||
| periodStart, periodEnd, PageRequest.of(offset / size, size) | ||
| ).stream().map(mv -> new RankedProduct(mv.getProductId(), mv.getScore())).toList(); | ||
|
Comment on lines
+20
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. size=0 입력 시 ArithmeticException 발생 가능
🛡️ 방어 코드 추가 제안 `@Override`
public List<RankedProduct> findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size) {
+ if (size <= 0) {
+ return Collections.emptyList();
+ }
return weeklyRepository.findByPeriodStartAndPeriodEndOrderByRankPositionAsc(
periodStart, periodEnd, PageRequest.of(offset / size, size)
).stream().map(mv -> new RankedProduct(mv.getProductId(), mv.getScore())).toList();
}🤖 Prompt for AI Agents |
||
| } | ||
|
Comment on lines
+19
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. offset → page 변환 로직 오류 - 페이지네이션 결과 불일치 가능
문제 시나리오:
운영 영향: API 호출자가 예상과 다른 랭킹 데이터를 받게 되어 UX 불일치 발생. 수정안: 호출부( ♻️ 인터페이스 시그니처 변경 제안 public interface MvRankingRepository {
- List<RankedProduct> findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size);
- List<RankedProduct> findMonthlyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size);
+ List<RankedProduct> findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int page, int size);
+ List<RankedProduct> findMonthlyRanking(LocalDate periodStart, LocalDate periodEnd, int page, int size);
long countWeeklyRanking(LocalDate periodStart, LocalDate periodEnd);
long countMonthlyRanking(LocalDate periodStart, LocalDate periodEnd);
}그리고 구현체: public List<RankedProduct> findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int page, int size) {
return weeklyRepository.findByPeriodStartAndPeriodEndOrderByRankPositionAsc(
- periodStart, periodEnd, PageRequest.of(offset / size, size)
+ periodStart, periodEnd, PageRequest.of(page, size)
).stream().map(mv -> new RankedProduct(mv.getProductId(), mv.getScore())).toList();
}🤖 Prompt for AI Agents |
||
|
|
||
| @Override | ||
| public List<RankedProduct> findMonthlyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size) { | ||
| return monthlyRepository.findByPeriodStartAndPeriodEndOrderByRankPositionAsc( | ||
| periodStart, periodEnd, PageRequest.of(offset / size, size) | ||
| ).stream().map(mv -> new RankedProduct(mv.getProductId(), mv.getScore())).toList(); | ||
| } | ||
|
|
||
| @Override | ||
| public long countWeeklyRanking(LocalDate periodStart, LocalDate periodEnd) { | ||
| return weeklyRepository.countByPeriodStartAndPeriodEnd(periodStart, periodEnd); | ||
| } | ||
|
|
||
| @Override | ||
| public long countMonthlyRanking(LocalDate periodStart, LocalDate periodEnd) { | ||
| return monthlyRepository.countByPeriodStartAndPeriodEnd(periodStart, periodEnd); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,12 +20,13 @@ public class RankingV1Controller { | |
|
|
||
| @GetMapping | ||
| public ApiResponse<RankingV1Dto.PageResponse> getRankings( | ||
| @RequestParam(defaultValue = "daily") String period, | ||
| @RequestParam(required = false) String date, | ||
| @RequestParam(defaultValue = "1") int page, | ||
| @RequestParam(defaultValue = "20") int size | ||
| ) { | ||
| String targetDate = (date != null && !date.isBlank()) ? date : LocalDate.now().format(DATE_FORMAT); | ||
| RankingPageInfo pageInfo = rankingFacade.getRankings(targetDate, page, size); | ||
| RankingPageInfo pageInfo = rankingFacade.getRankings(period, targetDate, page, size); | ||
| return ApiResponse.success(RankingV1Dto.PageResponse.from(pageInfo)); | ||
|
Comment on lines
22
to
30
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
# Description: CoreException 및 ErrorType 사용 패턴 확인
# 기존 Controller에서 유효성 검증 후 CoreException throw 패턴이 있는지 확인
rg -n "throw new CoreException" --type=java apps/commerce-api/src/main/java/com/loopers/interfaces/api/ -C2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: # RankingV1Controller 전체 코드 확인
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1541 🏁 Script executed: # RankingFacade 구현 확인
cat -n apps/commerce-api/src/main/java/com/loopers/application/facade/RankingFacade.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 198 🏁 Script executed: # 다른 Controller에서의 파라미터 검증 패턴 확인
rg -A5 "@RequestParam" --type=java apps/commerce-api/src/main/java/com/loopers/interfaces/api/ | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 6730 🏁 Script executed: # CoreException 사용 패턴 검색 (전체 codebase)
rg "throw new CoreException" --type=java -C2 | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2934 🏁 Script executed: # Bean Validation 또는 검증 로직이 있는 컨트롤러 예제 확인
rg "@Valid|@Pattern|@NotNull" --type=java apps/commerce-api/src/main/java/com/loopers/interfaces/api/ | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: # RankingFacade 파일 위치 찾기
find apps/commerce-api -name "*RankingFacade*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 167 🏁 Script executed: # getRankings 메서드 검색
rg "def getRankings|void getRankings|getRankings(" --type=java apps/commerce-api -A8 | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 225 🏁 Script executed: # period 파라미터를 다루는 로직 전체 찾기
rg "period" --type=java apps/commerce-api -B2 -A2 | grep -A3 -B3 "getRankings\|switch\|period"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 13429 🏁 Script executed: cat -n apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 4914 🏁 Script executed: # ErrorType 열거형 확인
rg "enum ErrorType|INVALID_PARAMETER" --type=java -A2 apps/commerce-api | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 470 🏁 Script executed: # RankingV1Controller 테스트 파일 확인
find apps/commerce-api -name "*RankingV1ControllerTest*" -o -name "*RankingTest*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: # 다른 컨트롤러의 파라미터 검증 테스트 패턴
rg "invalid.*parameter|Invalid.*parameter" --type=java -B2 -A2 apps/commerce-api | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: # ErrorType 전체 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1046 🏁 Script executed: # 파라미터 검증으로 CoreException을 throw하는 예제 찾기
rg "throw new CoreException.*INVALID\|throw new CoreException.*BAD_REQUEST" --type=java -B2 -A1 apps/commerce-api | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67
운영 관점:
♻️ 명시적 검증 추가 권장Controller에서 검증 후 CoreException throw: +import java.util.Set;
+
`@RestController`
`@RequestMapping`("/api/v1/rankings")
`@RequiredArgsConstructor`
public class RankingV1Controller {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
+ private static final Set<String> VALID_PERIODS = Set.of("daily", "weekly", "monthly");
private final RankingFacade rankingFacade;
`@GetMapping`
public ApiResponse<RankingV1Dto.PageResponse> getRankings(
`@RequestParam`(defaultValue = "daily") String period,
`@RequestParam`(required = false) String date,
`@RequestParam`(defaultValue = "1") int page,
`@RequestParam`(defaultValue = "20") int size
) {
+ if (!VALID_PERIODS.contains(period)) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "지원하지 않는 period입니다: " + period);
+ }
String targetDate = (date != null && !date.isBlank()) ? date : LocalDate.now().format(DATE_FORMAT);추가 테스트: 잘못된 period 값 입력 시 400 응답과 에러 메시지를 검증하는 테스트 케이스 추가 필요하다. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| package com.loopers.batch.job.ranking; | ||
|
|
||
| import com.loopers.batch.listener.JobListener; | ||
| import com.loopers.batch.listener.StepMonitorListener; | ||
| import com.loopers.domain.metrics.ProductMetrics; | ||
| import com.loopers.domain.ranking.MvProductRankMonthly; | ||
| import com.loopers.infrastructure.ranking.MvProductRankMonthlyJpaRepository; | ||
| import jakarta.persistence.EntityManagerFactory; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.batch.core.Job; | ||
| import org.springframework.batch.core.Step; | ||
| import org.springframework.batch.core.configuration.annotation.JobScope; | ||
| import org.springframework.batch.core.configuration.annotation.StepScope; | ||
| import org.springframework.batch.core.job.builder.JobBuilder; | ||
| import org.springframework.batch.core.launch.support.RunIdIncrementer; | ||
| import org.springframework.batch.core.repository.JobRepository; | ||
| import org.springframework.batch.core.step.builder.StepBuilder; | ||
| import org.springframework.batch.item.ItemProcessor; | ||
| import org.springframework.batch.item.ItemWriter; | ||
| import org.springframework.batch.item.database.JpaPagingItemReader; | ||
| import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.transaction.PlatformTransactionManager; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.YearMonth; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.ArrayList; | ||
| import java.util.Comparator; | ||
| import java.util.List; | ||
|
|
||
| @Slf4j | ||
| @ConditionalOnProperty(name = "spring.batch.job.name", havingValue = MonthlyRankingJobConfig.JOB_NAME) | ||
| @RequiredArgsConstructor | ||
| @Configuration | ||
| public class MonthlyRankingJobConfig { | ||
|
|
||
| public static final String JOB_NAME = "monthlyRankingJob"; | ||
| private static final String STEP_NAME = "monthlyRankingStep"; | ||
| private static final int CHUNK_SIZE = 100; | ||
| private static final int TOP_N = 100; | ||
|
|
||
| private static final double WEIGHT_VIEW = 0.1; | ||
| private static final double WEIGHT_LIKE = 0.2; | ||
| private static final double WEIGHT_ORDER = 0.7; | ||
|
|
||
| private final JobRepository jobRepository; | ||
| private final PlatformTransactionManager transactionManager; | ||
| private final JobListener jobListener; | ||
| private final StepMonitorListener stepMonitorListener; | ||
| private final EntityManagerFactory entityManagerFactory; | ||
| private final MvProductRankMonthlyJpaRepository mvRepository; | ||
|
|
||
| @Bean(JOB_NAME) | ||
| public Job monthlyRankingJob() { | ||
| return new JobBuilder(JOB_NAME, jobRepository) | ||
| .incrementer(new RunIdIncrementer()) | ||
| .start(monthlyRankingStep()) | ||
| .listener(jobListener) | ||
| .build(); | ||
| } | ||
|
|
||
| @JobScope | ||
| @Bean(STEP_NAME) | ||
| public Step monthlyRankingStep() { | ||
| return new StepBuilder(STEP_NAME, jobRepository) | ||
| .<ProductMetrics, RankedProductScore>chunk(CHUNK_SIZE, transactionManager) | ||
| .reader(monthlyProductMetricsReader()) | ||
| .processor(monthlyRankingScoreProcessor()) | ||
| .writer(monthlyRankingWriter(null)) | ||
| .listener(stepMonitorListener) | ||
| .build(); | ||
| } | ||
|
|
||
| @StepScope | ||
| @Bean | ||
| public JpaPagingItemReader<ProductMetrics> monthlyProductMetricsReader() { | ||
| return new JpaPagingItemReaderBuilder<ProductMetrics>() | ||
| .name("monthlyProductMetricsReader") | ||
| .entityManagerFactory(entityManagerFactory) | ||
| .queryString("SELECT pm FROM ProductMetrics pm ORDER BY pm.id ASC") | ||
| .pageSize(CHUNK_SIZE) | ||
| .build(); | ||
| } | ||
|
|
||
| @StepScope | ||
| @Bean | ||
| public ItemProcessor<ProductMetrics, RankedProductScore> monthlyRankingScoreProcessor() { | ||
| return metrics -> { | ||
| double score = metrics.getViewCount() * WEIGHT_VIEW | ||
| + metrics.getLikesCount() * WEIGHT_LIKE | ||
| + metrics.getOrderCount() * WEIGHT_ORDER; | ||
| return new RankedProductScore(metrics.getProductId(), score); | ||
| }; | ||
| } | ||
|
|
||
| @StepScope | ||
| @Bean | ||
| public ItemWriter<RankedProductScore> monthlyRankingWriter( | ||
| @Value("#{jobParameters['requestDate']}") String requestDate | ||
| ) { | ||
| return items -> { | ||
| LocalDate date = LocalDate.parse(requestDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); | ||
| YearMonth yearMonth = YearMonth.from(date); | ||
| LocalDate periodStart = yearMonth.atDay(1); | ||
| LocalDate periodEnd = yearMonth.atEndOfMonth(); | ||
|
|
||
| List<RankedProductScore> allScores = new ArrayList<>(items.getItems()); | ||
| allScores.sort(Comparator.comparingDouble(RankedProductScore::score).reversed()); | ||
|
|
||
| List<RankedProductScore> topN = allScores.stream().limit(TOP_N).toList(); | ||
|
|
||
| mvRepository.deleteByPeriodStartAndPeriodEnd(periodStart, periodEnd); | ||
|
|
||
| int rank = 1; | ||
| for (RankedProductScore scored : topN) { | ||
| mvRepository.save(new MvProductRankMonthly( | ||
| scored.productId(), rank++, scored.score(), periodStart, periodEnd | ||
| )); | ||
| } | ||
|
|
||
| log.info("월간 랭킹 저장 완료: period={} ~ {}, count={}", periodStart, periodEnd, topN.size()); | ||
| }; | ||
| } | ||
|
Comment on lines
+101
to
+128
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. WeeklyRankingJobConfig와 동일한 치명적 로직 오류
동일한 수정이 필요하다. 🤖 Prompt for AI Agents |
||
| } | ||
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.
날짜 파싱 예외 처리 미흡 - 500 에러 노출 가능
LocalDate.parse(date, DATE_FORMAT)에서 잘못된 형식의 date가 입력되면DateTimeParseException이 발생하여 500 에러로 노출된다.운영 영향: 사용자에게 불친절한 에러 메시지 노출, 에러 모니터링 시 불필요한 노이즈 발생.
수정안:
CoreException으로 변환하여 일관된 에러 응답 제공. (이 레포지토리의 통합 에러 처리 패턴 준수)🛡️ 에러 처리 추가
🤖 Prompt for AI Agents