diff --git a/src/main/java/com/nova/nova_server/domain/batch/common/service/BatchJobService.java b/src/main/java/com/nova/nova_server/domain/batch/common/service/BatchJobService.java index 65ddef1..e0dab69 100644 --- a/src/main/java/com/nova/nova_server/domain/batch/common/service/BatchJobService.java +++ b/src/main/java/com/nova/nova_server/domain/batch/common/service/BatchJobService.java @@ -1,6 +1,7 @@ package com.nova.nova_server.domain.batch.common.service; import com.nova.nova_server.domain.batch.articleingestion.service.ArticleFlowFactory; +import com.nova.nova_server.domain.batch.statistics.StatisticStepFactory; import com.nova.nova_server.domain.batch.summary.service.SummaryStepFactory; import com.nova.nova_server.domain.post.service.ArticleApiService; import com.nova.nova_server.domain.post.service.ArticleApiServiceFactory; @@ -27,6 +28,7 @@ public class BatchJobService { private final ArticleApiServiceFactory articleApiServiceFactory; private final ArticleFlowFactory articleFlowFactory; private final SummaryStepFactory summaryStepFactory; + private final StatisticStepFactory statisticStepFactory; public void runArticleIngestionBatch() { List articleApiServices = articleApiServiceFactory.createAllAvailableServices(); @@ -49,10 +51,14 @@ public void runSummaryBatch() { public void runArticleIngestionAndSummaryBatch() { List articleApiServices = articleApiServiceFactory.createAllAvailableServices(); Flow flow = articleFlowFactory.createCombinedFlow(articleApiServices); - Step step = summaryStepFactory.createStep(); + Step summaryStep = summaryStepFactory.createStep(); + Step statisticsStep = statisticStepFactory.createStep(); + Job job = new JobBuilder("articleIngestionAndSummaryBatch", jobRepository) .start(flow) - .next(step) + .next(summaryStep) + // 통계 배치 + .next(statisticsStep) .build() .build(); runBatch(job); diff --git a/src/main/java/com/nova/nova_server/domain/batch/statistics/KeywordStatisticsTasklet.java b/src/main/java/com/nova/nova_server/domain/batch/statistics/KeywordStatisticsTasklet.java new file mode 100644 index 0000000..61f377f --- /dev/null +++ b/src/main/java/com/nova/nova_server/domain/batch/statistics/KeywordStatisticsTasklet.java @@ -0,0 +1,59 @@ +package com.nova.nova_server.domain.batch.statistics; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import java.sql.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KeywordStatisticsTasklet implements Tasklet { + + private final JdbcTemplate jdbcTemplate; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + + LocalDate targetDate = LocalDate.now().minusDays(1); + LocalDateTime startDateTime = targetDate.atStartOfDay(); + LocalDateTime endDateTime = targetDate.plusDays(1).atStartOfDay(); + + log.info("Starting Keyword Statistics Aggregation for date: {}", targetDate); + + String deleteSql = "DELETE FROM keyword_statistics WHERE stat_date = ?"; + int deletedCount = jdbcTemplate.update(deleteSql, Date.valueOf(targetDate)); + log.info("Deleted {} existing statistics records for date: {}", deletedCount, targetDate); + + // 통계 집계 및 저장 + String insertSql = """ + INSERT INTO keyword_statistics (keyword_id, stat_date, mention_count, created_at, updated_at) + SELECT + keyword_id, + ?, + COUNT(*), + NOW(), + NOW() + FROM card_news_keyword + WHERE created_at >= ? AND created_at < ? + GROUP BY keyword_id + """; + + int insertedCount = jdbcTemplate.update(insertSql, + Date.valueOf(targetDate), + startDateTime, + endDateTime); + + log.info("Inserted {} aggregated statistics records for date: {}", insertedCount, targetDate); + + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/com/nova/nova_server/domain/batch/statistics/StatisticStepFactory.java b/src/main/java/com/nova/nova_server/domain/batch/statistics/StatisticStepFactory.java new file mode 100644 index 0000000..aa8b6c6 --- /dev/null +++ b/src/main/java/com/nova/nova_server/domain/batch/statistics/StatisticStepFactory.java @@ -0,0 +1,23 @@ +package com.nova.nova_server.domain.batch.statistics; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; + +@Component +@RequiredArgsConstructor +public class StatisticStepFactory { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final KeywordStatisticsTasklet keywordStatisticsTasklet; + + public Step createStep() { + return new StepBuilder("keywordStatisticsStep", jobRepository) + .tasklet(keywordStatisticsTasklet, transactionManager) + .build(); + } +} diff --git a/src/main/java/com/nova/nova_server/domain/trend/controller/TrendController.java b/src/main/java/com/nova/nova_server/domain/trend/controller/TrendController.java index e123cc5..a2e0bee 100644 --- a/src/main/java/com/nova/nova_server/domain/trend/controller/TrendController.java +++ b/src/main/java/com/nova/nova_server/domain/trend/controller/TrendController.java @@ -19,7 +19,7 @@ public class TrendController implements com.nova.nova_server.domain.trend.docs.T @GetMapping("/keywords/keywordtop") public ApiResponse getTopKeywords() { - return ApiResponse.success(trendService.getTopKeywords(LocalDate.now())); + return ApiResponse.success(trendService.getTopKeywords(LocalDate.now().minusDays(1))); } @GetMapping("/interests/skilltop")