Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,37 +37,12 @@ public Page<MonthlyRankEntity> getMonthlyRanking(String yearMonth, Pageable page
yearMonth, pageable.getPageNumber(), pageable.getPageSize());

// 1. 전체 랭킹 조회 (순위 순으로 정렬됨)
List<MonthlyRankEntity> allRankings = monthlyRankRepository.findByYearMonth(yearMonth);
Page<MonthlyRankEntity> pagedRankings = monthlyRankRepository.findByYearMonth(yearMonth, pageable);

if (allRankings.isEmpty()) {
log.debug("월간 랭킹 데이터 없음: yearMonth={}", yearMonth);
return Page.empty(pageable);
}

// 2. 페이징 처리
int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), allRankings.size());

if (start >= allRankings.size()) {
return Page.empty(pageable);
}

List<MonthlyRankEntity> pagedRankings = allRankings.subList(start, end);

log.debug("월간 랭킹 조회 완료: yearMonth={}, 전체={}, 페이지={}",
yearMonth, allRankings.size(), pagedRankings.size());

return new PageImpl<>(pagedRankings, pageable, allRankings.size());
}
yearMonth, pagedRankings.getTotalPages(), pagedRankings.getNumber());

/**
* 특정 월의 전체 랭킹 개수를 조회합니다.
*
* @param yearMonth 조회할 월
* @return 랭킹 개수
*/
public long getMonthlyRankingCount(String yearMonth) {
List<MonthlyRankEntity> rankings = monthlyRankRepository.findByYearMonth(yearMonth);
return rankings.size();
return pagedRankings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,14 @@ public Page<WeeklyRankEntity> getWeeklyRanking(String yearWeek, Pageable pageabl
yearWeek, pageable.getPageNumber(), pageable.getPageSize());

// 1. 전체 랭킹 조회 (순위 순으로 정렬됨)
List<WeeklyRankEntity> allRankings = weeklyRankRepository.findByYearWeek(yearWeek);
Page<WeeklyRankEntity> pagedRankings = weeklyRankRepository.findByYearWeek(yearWeek , pageable);

if (allRankings.isEmpty()) {
if (pagedRankings.isEmpty()) {
log.debug("주간 랭킹 데이터 없음: yearWeek={}", yearWeek);
return Page.empty(pageable);
}

// 2. 페이징 처리
int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), allRankings.size());

if (start >= allRankings.size()) {
return Page.empty(pageable);
}

List<WeeklyRankEntity> pagedRankings = allRankings.subList(start, end);

log.debug("주간 랭킹 조회 완료: yearWeek={}, 전체={}, 페이지={}",
yearWeek, allRankings.size(), pagedRankings.size());

return new PageImpl<>(pagedRankings, pageable, allRankings.size());
return pagedRankings;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -26,7 +27,7 @@ public interface MonthlyRankJpaRepository extends JpaRepository<MonthlyRankEntit
* 특정 월의 랭킹을 순위 순으로 페이지네이션하여 조회합니다.
*/
@Query("SELECT m FROM MonthlyRankEntity m WHERE m.id.yearMonth = :yearMonth ORDER BY m.rankPosition ASC")
List<MonthlyRankEntity> findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable);
Page<MonthlyRankEntity> findByIdYearMonthOrderByRankPosition(@Param("yearMonth") String yearMonth, Pageable pageable);

/**
* 특정 월의 모든 랭킹을 삭제합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
Expand Down Expand Up @@ -31,13 +32,7 @@ public List<MonthlyRankEntity> saveAll(List<MonthlyRankEntity> entities) {
}

@Override
public List<MonthlyRankEntity> findByYearMonth(String yearMonth) {
return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth);
}

@Override
public List<MonthlyRankEntity> findByYearMonthWithPagination(String yearMonth, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
public Page<MonthlyRankEntity> findByYearMonth(String yearMonth, Pageable pageable) {
return jpaRepository.findByIdYearMonthOrderByRankPosition(yearMonth, pageable);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -26,7 +27,7 @@ public interface WeeklyRankJpaRepository extends JpaRepository<WeeklyRankEntity,
* 특정 주차의 랭킹을 순위 순으로 페이지네이션하여 조회합니다.
*/
@Query("SELECT w FROM WeeklyRankEntity w WHERE w.id.yearWeek = :yearWeek ORDER BY w.rankPosition ASC")
List<WeeklyRankEntity> findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable);
Page<WeeklyRankEntity> findByIdYearWeekOrderByRankPosition(@Param("yearWeek") String yearWeek, Pageable pageable);

/**
* 특정 주차의 모든 랭킹을 삭제합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
Expand Down Expand Up @@ -34,15 +35,13 @@ public List<WeeklyRankEntity> saveAll(List<WeeklyRankEntity> entities) {
public List<WeeklyRankEntity> findByYearWeek(String yearWeek) {
return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek);
}

@Override
public List<WeeklyRankEntity> findByYearWeekWithPagination(String yearWeek, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable);
public long deleteByYearWeek(String yearWeek) {
return jpaRepository.deleteByIdYearWeek(yearWeek);
}

@Override
public long deleteByYearWeek(String yearWeek) {
return jpaRepository.deleteByIdYearWeek(yearWeek);
public Page<WeeklyRankEntity> findByYearWeek(String yearWeek, Pageable pageable) {
return jpaRepository.findByIdYearWeekOrderByRankPosition(yearWeek, pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ private String processYearWeekParameter(String yearWeek, LocalDate date) {
// date가 있으면 해당 날짜의 주차, 없으면 현재 주차
LocalDate targetDate = date != null ? date : LocalDate.now();

WeekFields weekFields = WeekFields.of(Locale.getDefault());
WeekFields weekFields = WeekFields.ISO;
int year = targetDate.getYear();
int week = targetDate.get(weekFields.weekOfYear());
int week = targetDate.get(weekFields.weekOfWeekBasedYear());

return String.format("%d-W%02d", year, week);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.loopers.fixtures;

import java.math.BigDecimal;
import java.time.LocalDate;

import org.assertj.core.api.Assertions;
Expand Down Expand Up @@ -165,20 +166,20 @@ public static class InvalidGender {
* 사용자의 포인트가 0인지 검증하는 헬퍼 메서드
*/
public static void assertUserPointIsZero(UserEntity user) {
Assertions.assertThat(user.getPointAmount()).isEqualByComparingTo(java.math.BigDecimal.ZERO.setScale(2));
Assertions.assertThat(user.getPointAmount()).isEqualByComparingTo(BigDecimal.ZERO.setScale(2));
}

/**
* 사용자의 포인트 금액 검증 헬퍼 메서드
*/
public static void assertUserPointAmount(UserEntity user, java.math.BigDecimal expectedAmount) {
public static void assertUserPointAmount(UserEntity user, BigDecimal expectedAmount) {
Assertions.assertThat(user.getPointAmount()).isEqualByComparingTo(expectedAmount);
}

/**
* 포인트 충전 실패 검증 헬퍼 메서드
*/
public static void assertChargePointFails(UserEntity user, java.math.BigDecimal amount, String expectedMessage) {
public static void assertChargePointFails(UserEntity user, BigDecimal amount, String expectedMessage) {
Assertions.assertThatThrownBy(() -> user.chargePoint(amount))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(expectedMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ void should_return_products_in_ranking_order() {
() -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK),
() -> assertThat(Objects.requireNonNull(response.getBody()).data().content()).hasSize(3),
() -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(0).productId()).isEqualTo(product1),
() -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(2).productId()).isEqualTo(product2),
() -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3)
() -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(1).productId()).isEqualTo(product3),
() -> assertThat(Objects.requireNonNull(response.getBody()).data().content().get(2).productId()).isEqualTo(product2)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.loopers.batch.job.ranking.dto;

import java.math.BigDecimal;

import com.loopers.batch.job.ranking.support.ScoreCalculator;

import lombok.Getter;
Expand All @@ -17,16 +19,18 @@ public class RankingAggregation {
private final long likeCount;
private final long salesCount;
private final long orderCount;
private final BigDecimal totalSalesAmount;
private final long totalScore;
private int rankPosition; // 가변 필드 (순위 부여용)

private RankingAggregation(Long productId, long viewCount, long likeCount,
long salesCount, long orderCount, long totalScore) {
long salesCount, long orderCount, BigDecimal totalSalesAmount, long totalScore) {
this.productId = productId;
this.viewCount = viewCount;
this.likeCount = likeCount;
this.salesCount = salesCount;
this.orderCount = orderCount;
this.totalSalesAmount = totalSalesAmount;
this.totalScore = totalScore;
this.rankPosition = 0; // 초기값
}
Expand All @@ -40,7 +44,7 @@ private RankingAggregation(Long productId, long viewCount, long likeCount,
* @throws IllegalArgumentException row가 null이거나 형식이 잘못된 경우
*/
public static RankingAggregation from(Object[] row, ScoreCalculator calculator) {
if (row == null || row.length < 5) {
if (row == null || row.length < 4) {
throw new IllegalArgumentException("집계 결과 배열이 null이거나 길이가 부족합니다.");
}

Expand All @@ -50,10 +54,11 @@ public static RankingAggregation from(Object[] row, ScoreCalculator calculator)
long likeCount = ((Number) row[2]).longValue();
long salesCount = ((Number) row[3]).longValue();
long orderCount = ((Number) row[4]).longValue();
BigDecimal totalSalesAmount = (BigDecimal) row[5];

long totalScore = calculator.calculate(viewCount, likeCount, salesCount, orderCount);
long totalScore = calculator.calculate(viewCount, likeCount, totalSalesAmount);

return new RankingAggregation(productId, viewCount, likeCount, salesCount, orderCount, totalScore);
return new RankingAggregation(productId, viewCount, likeCount, salesCount, orderCount, totalSalesAmount, totalScore);
} catch (ClassCastException | NullPointerException e) {
throw new IllegalArgumentException("집계 결과 데이터 형식이 올바르지 않습니다.", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.loopers.batch.job.ranking.reader;

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

import org.springframework.batch.item.ItemReader;

import com.loopers.batch.job.ranking.dto.RankingAggregation;
import com.loopers.batch.job.ranking.support.RankingAggregator;
import com.loopers.domain.metrics.ProductMetricsRepository;

import lombok.extern.slf4j.Slf4j;

/**
* 메트릭 Reader 공통 추상 클래스
* - 특정 기간의 데이터를 집계하고 랭킹을 생성하는 공통 로직을 포함
*/
@Slf4j
public abstract class AbstractMetricsReader implements ItemReader<RankingAggregation> {

protected final ProductMetricsRepository productMetricsRepository;
protected final RankingAggregator rankingAggregator;

private Iterator<RankingAggregation> iterator;

protected AbstractMetricsReader(ProductMetricsRepository productMetricsRepository, RankingAggregator rankingAggregator) {
this.productMetricsRepository = productMetricsRepository;
this.rankingAggregator = rankingAggregator;
}

@Override
public RankingAggregation read() throws Exception {
if (iterator == null) {
initializeIterator();
}

return iterator.hasNext() ? iterator.next() : null;
}

private void initializeIterator() {
String logIdentifier = getLogIdentifier();
log.info("{} 랭킹 집계 시작: parameter={}", logIdentifier, getParameterValue());

try {
// 1. 기간 파싱 (추상 메서드 호출)
LocalDate[] dateRange = parseDateRange();
LocalDate startDate = dateRange[0];
LocalDate endDate = dateRange[1];

log.info("집계 기간: {} ~ {}", startDate, endDate);

// 2. DB에서 집계 쿼리 실행
List<Object[]> aggregationResults = productMetricsRepository.aggregateByDateRange(startDate, endDate);
log.info("집계 대상 상품 수: {}", aggregationResults.size());

// 3. 랭킹 처리 (정렬 + TOP 100 + 순위 부여)
List<RankingAggregation> rankings = rankingAggregator.processRankings(aggregationResults);
log.info("생성된 랭킹 수: {}", rankings.size());

iterator = rankings.iterator();

} catch (Exception e) {
log.error("{} 랭킹 집계 중 오류 발생: parameter={}", logIdentifier, getParameterValue(), e);
throw new RuntimeException(logIdentifier + " 랭킹 집계 실패", e);
}
}

/**
* 기간에 해당하는 LocalDate 범위를 반환합니다.
*/
protected abstract LocalDate[] parseDateRange();

/**
* 로그 식별자를 반환합니다. (예: "월간", "주간")
*/
protected abstract String getLogIdentifier();

/**
* 현재 사용 중인 파라미터 값을 반환합니다.
*/
protected abstract String getParameterValue();
}
Loading