diff --git a/apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java index a2c80823aa..4a48ae0d82 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java @@ -4,11 +4,16 @@ import com.loopers.domain.brand.BrandService; import com.loopers.domain.product.Product; import com.loopers.domain.product.ProductService; +import com.loopers.domain.ranking.MvRankingRepository; import com.loopers.domain.ranking.RankedProduct; import com.loopers.domain.ranking.RankingRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.List; import java.util.Map; @@ -18,18 +23,55 @@ @RequiredArgsConstructor public class RankingFacade { + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); + private final RankingRepository rankingRepository; + private final MvRankingRepository mvRankingRepository; private final ProductService productService; private final BrandService brandService; - public RankingPageInfo getRankings(String date, int page, int size) { + public RankingPageInfo getRankings(String period, String date, int page, int size) { int offset = (page - 1) * size; - List ranked = rankingRepository.findTopN(date, offset, size); + + List ranked; + long totalSize; + + switch (period) { + case "weekly" -> { + LocalDate targetDate = LocalDate.parse(date, DATE_FORMAT); + LocalDate periodStart = targetDate.with(DayOfWeek.MONDAY); + LocalDate periodEnd = targetDate.with(DayOfWeek.SUNDAY); + ranked = mvRankingRepository.findWeeklyRanking(periodStart, periodEnd, offset, size); + totalSize = mvRankingRepository.countWeeklyRanking(periodStart, periodEnd); + } + case "monthly" -> { + LocalDate targetDate = LocalDate.parse(date, DATE_FORMAT); + YearMonth yearMonth = YearMonth.from(targetDate); + LocalDate periodStart = yearMonth.atDay(1); + LocalDate periodEnd = yearMonth.atEndOfMonth(); + ranked = mvRankingRepository.findMonthlyRanking(periodStart, periodEnd, offset, size); + totalSize = mvRankingRepository.countMonthlyRanking(periodStart, periodEnd); + } + default -> { + // daily — Redis ZSET + ranked = rankingRepository.findTopN(date, offset, size); + totalSize = rankingRepository.getTotalSize(date); + } + } if (ranked.isEmpty()) { return new RankingPageInfo(date, Collections.emptyList(), 0, page, size); } + return buildPageInfo(date, ranked, totalSize, offset, page, size); + } + + public Long getProductRank(String date, Long productId) { + Long rank = rankingRepository.findRank(date, productId); + return rank != null ? rank + 1 : null; + } + + private RankingPageInfo buildPageInfo(String date, List ranked, long totalSize, int offset, int page, int size) { List productIds = ranked.stream().map(RankedProduct::productId).toList(); List products = productService.getByIds(productIds); Map productMap = products.stream() @@ -52,12 +94,6 @@ public RankingPageInfo getRankings(String date, int page, int size) { items.add(RankingInfo.of(rank++, rp.score(), product, brandName)); } - long totalSize = rankingRepository.getTotalSize(date); return new RankingPageInfo(date, items, totalSize, page, size); } - - public Long getProductRank(String date, Long productId) { - Long rank = rankingRepository.findRank(date, productId); - return rank != null ? rank + 1 : null; - } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java new file mode 100644 index 0000000000..f276f226ae --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java @@ -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; +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java new file mode 100644 index 0000000000..b29cf79530 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java @@ -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; +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvRankingRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvRankingRepository.java new file mode 100644 index 0000000000..4e3bdc67bc --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/ranking/MvRankingRepository.java @@ -0,0 +1,15 @@ +package com.loopers.domain.ranking; + +import java.time.LocalDate; +import java.util.List; + +public interface MvRankingRepository { + + List findWeeklyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size); + + List findMonthlyRanking(LocalDate periodStart, LocalDate periodEnd, int offset, int size); + + long countWeeklyRanking(LocalDate periodStart, LocalDate periodEnd); + + long countMonthlyRanking(LocalDate periodStart, LocalDate periodEnd); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java new file mode 100644 index 0000000000..60a2fd3e69 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java @@ -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 { + List findByPeriodStartAndPeriodEndOrderByRankPositionAsc( + LocalDate periodStart, LocalDate periodEnd, Pageable pageable); + long countByPeriodStartAndPeriodEnd(LocalDate periodStart, LocalDate periodEnd); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java new file mode 100644 index 0000000000..bac3b833d5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java @@ -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 { + List findByPeriodStartAndPeriodEndOrderByRankPositionAsc( + LocalDate periodStart, LocalDate periodEnd, Pageable pageable); + long countByPeriodStartAndPeriodEnd(LocalDate periodStart, LocalDate periodEnd); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvRankingRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvRankingRepositoryImpl.java new file mode 100644 index 0000000000..840883088f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/ranking/MvRankingRepositoryImpl.java @@ -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 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(); + } + + @Override + public List 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); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java index 28c127ab6e..1dee9c71ec 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java @@ -20,12 +20,13 @@ public class RankingV1Controller { @GetMapping public ApiResponse 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)); } } diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java new file mode 100644 index 0000000000..48636d989b --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java @@ -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) + .chunk(CHUNK_SIZE, transactionManager) + .reader(monthlyProductMetricsReader()) + .processor(monthlyRankingScoreProcessor()) + .writer(monthlyRankingWriter(null)) + .listener(stepMonitorListener) + .build(); + } + + @StepScope + @Bean + public JpaPagingItemReader monthlyProductMetricsReader() { + return new JpaPagingItemReaderBuilder() + .name("monthlyProductMetricsReader") + .entityManagerFactory(entityManagerFactory) + .queryString("SELECT pm FROM ProductMetrics pm ORDER BY pm.id ASC") + .pageSize(CHUNK_SIZE) + .build(); + } + + @StepScope + @Bean + public ItemProcessor 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 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 allScores = new ArrayList<>(items.getItems()); + allScores.sort(Comparator.comparingDouble(RankedProductScore::score).reversed()); + + List 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()); + }; + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankedProductScore.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankedProductScore.java new file mode 100644 index 0000000000..3f5f57f0d4 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/RankedProductScore.java @@ -0,0 +1,3 @@ +package com.loopers.batch.job.ranking; + +public record RankedProductScore(Long productId, double score) {} diff --git a/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java new file mode 100644 index 0000000000..43600256ff --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java @@ -0,0 +1,131 @@ +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.MvProductRankWeekly; +import com.loopers.infrastructure.ranking.MvProductRankWeeklyJpaRepository; +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.DayOfWeek; +import java.time.LocalDate; +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 = WeeklyRankingJobConfig.JOB_NAME) +@RequiredArgsConstructor +@Configuration +public class WeeklyRankingJobConfig { + + public static final String JOB_NAME = "weeklyRankingJob"; + private static final String STEP_NAME = "weeklyRankingStep"; + 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 MvProductRankWeeklyJpaRepository mvRepository; + + @Bean(JOB_NAME) + public Job weeklyRankingJob() { + return new JobBuilder(JOB_NAME, jobRepository) + .incrementer(new RunIdIncrementer()) + .start(weeklyRankingStep()) + .listener(jobListener) + .build(); + } + + @JobScope + @Bean(STEP_NAME) + public Step weeklyRankingStep() { + return new StepBuilder(STEP_NAME, jobRepository) + .chunk(CHUNK_SIZE, transactionManager) + .reader(productMetricsReader()) + .processor(rankingScoreProcessor()) + .writer(weeklyRankingWriter(null)) + .listener(stepMonitorListener) + .build(); + } + + @StepScope + @Bean + public JpaPagingItemReader productMetricsReader() { + return new JpaPagingItemReaderBuilder() + .name("productMetricsReader") + .entityManagerFactory(entityManagerFactory) + .queryString("SELECT pm FROM ProductMetrics pm ORDER BY pm.id ASC") + .pageSize(CHUNK_SIZE) + .build(); + } + + @StepScope + @Bean + public ItemProcessor rankingScoreProcessor() { + 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 weeklyRankingWriter( + @Value("#{jobParameters['requestDate']}") String requestDate + ) { + return items -> { + LocalDate date = LocalDate.parse(requestDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + LocalDate periodStart = date.with(DayOfWeek.MONDAY); + LocalDate periodEnd = date.with(DayOfWeek.SUNDAY); + + // 모든 chunk의 데이터를 모아서 정렬 후 Top N 저장 + List allScores = new ArrayList<>(items.getItems()); + allScores.sort(Comparator.comparingDouble(RankedProductScore::score).reversed()); + + List topN = allScores.stream().limit(TOP_N).toList(); + + // 기존 데이터 삭제 + mvRepository.deleteByPeriodStartAndPeriodEnd(periodStart, periodEnd); + + // Top N 저장 + int rank = 1; + for (RankedProductScore scored : topN) { + mvRepository.save(new MvProductRankWeekly( + scored.productId(), rank++, scored.score(), periodStart, periodEnd + )); + } + + log.info("주간 랭킹 저장 완료: period={} ~ {}, count={}", periodStart, periodEnd, topN.size()); + }; + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java b/apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java new file mode 100644 index 0000000000..3dc29889c3 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/domain/metrics/ProductMetrics.java @@ -0,0 +1,40 @@ +package com.loopers.domain.metrics; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; + +@Entity +@Table(name = "product_metrics") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductMetrics { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "product_id", nullable = false, unique = true) + private Long productId; + + @Column(name = "likes_count", nullable = false) + private Long likesCount; + + @Column(name = "view_count", nullable = false) + private Long viewCount; + + @Column(name = "order_count", nullable = false) + private Long orderCount; + + @Version + private Long version; + + @Column(name = "created_at", nullable = false, updatable = false) + private ZonedDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private ZonedDateTime updatedAt; +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java b/apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java new file mode 100644 index 0000000000..abb128d440 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankMonthly.java @@ -0,0 +1,51 @@ +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; + + public MvProductRankMonthly(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(); + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java b/apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java new file mode 100644 index 0000000000..2581ae020f --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/domain/ranking/MvProductRankWeekly.java @@ -0,0 +1,51 @@ +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; + + 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(); + } +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java new file mode 100644 index 0000000000..8df9402a28 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankMonthlyJpaRepository.java @@ -0,0 +1,12 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.MvProductRankMonthly; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; + +public interface MvProductRankMonthlyJpaRepository extends JpaRepository { + List findByPeriodStartAndPeriodEndOrderByRankPositionAsc(LocalDate periodStart, LocalDate periodEnd); + void deleteByPeriodStartAndPeriodEnd(LocalDate periodStart, LocalDate periodEnd); +} diff --git a/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java new file mode 100644 index 0000000000..3b659641f9 --- /dev/null +++ b/apps/commerce-batch/src/main/java/com/loopers/infrastructure/ranking/MvProductRankWeeklyJpaRepository.java @@ -0,0 +1,12 @@ +package com.loopers.infrastructure.ranking; + +import com.loopers.domain.ranking.MvProductRankWeekly; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; + +public interface MvProductRankWeeklyJpaRepository extends JpaRepository { + List findByPeriodStartAndPeriodEndOrderByRankPositionAsc(LocalDate periodStart, LocalDate periodEnd); + void deleteByPeriodStartAndPeriodEnd(LocalDate periodStart, LocalDate periodEnd); +}