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
@@ -0,0 +1,15 @@
package site.icebang.batch.common;

/**
* Spring Batch의 JobExecutionContext에서 Step 간 데이터 공유를 위해 사용되는 Key들을 상수로 정의하는 인터페이스. 모든 Tasklet은 이
* 인터페이스를 참조하여 데이터의 일관성을 유지합니다.
*/
public interface JobContextKeys {

String EXTRACTED_KEYWORD = "extractedKeyword";
String SEARCHED_PRODUCTS = "searchedProducts";
String MATCHED_PRODUCTS = "matchedProducts";
String SELECTED_PRODUCT = "selectedProduct";
String CRAWLED_PRODUCT_DETAIL = "crawledProductDetail";
String GENERATED_CONTENT = "generatedContent";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package site.icebang.batch.job; // 패키지 경로 수정

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

import lombok.RequiredArgsConstructor;

import site.icebang.batch.tasklet.*;

/** [배치 시스템 구현] 트렌드 기반 블로그 자동화 워크플로우를 구성하는 Job들을 정의합니다. */
@Configuration
@RequiredArgsConstructor
public class BlogAutomationJobConfig {

// --- Tasklets ---
private final ExtractTrendKeywordTasklet extractTrendKeywordTask;
private final SearchProductsFromMallTasklet searchProductsFromMallTask;
private final MatchProductWithKeywordTasklet matchProductWithKeywordTask;
private final FindSimilarProductsTasklet findSimilarProductsTask;
private final CrawlSelectedProductTasklet crawlSelectedProductTask;
private final GenerateBlogContentTasklet generateBlogContentTask;
private final PublishBlogPostTasklet publishBlogPostTask;

/** Job 1: 상품 선정 및 정보 수집 키워드 추출부터 최종 상품 정보 크롤링까지의 과정을 책임집니다. */
@Bean
public Job productSelectionJob(
JobRepository jobRepository,
Step extractTrendKeywordStep,
Step searchProductsFromMallStep,
Step matchProductWithKeywordStep,
Step findSimilarProductsStep,
Step crawlSelectedProductStep) {
return new JobBuilder("productSelectionJob", jobRepository)
.start(extractTrendKeywordStep)
.next(searchProductsFromMallStep)
.next(matchProductWithKeywordStep)
.next(findSimilarProductsStep)
.next(crawlSelectedProductStep)
.build();
}

/** Job 2: 콘텐츠 생성 및 발행 수집된 상품 정보로 블로그 콘텐츠를 생성하고 발행합니다. */
@Bean
public Job contentPublishingJob(
JobRepository jobRepository, Step generateBlogContentStep, Step publishBlogPostStep) {
return new JobBuilder("contentPublishingJob", jobRepository)
.start(generateBlogContentStep)
.next(publishBlogPostStep)
.build();
}

// --- Steps for productSelectionJob ---
@Bean
public Step extractTrendKeywordStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("extractTrendKeywordStep", jobRepository)
.tasklet(extractTrendKeywordTask, transactionManager)
.build();
}

@Bean
public Step searchProductsFromMallStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("searchProductsFromMallStep", jobRepository)
.tasklet(searchProductsFromMallTask, transactionManager)
.build();
}

@Bean
public Step matchProductWithKeywordStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("matchProductWithKeywordStep", jobRepository)
.tasklet(matchProductWithKeywordTask, transactionManager)
.build();
}

@Bean
public Step findSimilarProductsStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("findSimilarProductsStep", jobRepository)
.tasklet(findSimilarProductsTask, transactionManager)
.build();
}

@Bean
public Step crawlSelectedProductStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("crawlSelectedProductStep", jobRepository)
.tasklet(crawlSelectedProductTask, transactionManager)
.build();
}

// --- Steps for contentPublishingJob ---
@Bean
public Step generateBlogContentStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("generateBlogContentStep", jobRepository)
.tasklet(generateBlogContentTask, transactionManager)
.build();
}

@Bean
public Step publishBlogPostStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("publishBlogPostStep", jobRepository)
.tasklet(publishBlogPostTask, transactionManager)
.build();
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package site.icebang.batch.tasklet;

import java.util.Map;

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.item.ExecutionContext;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import site.icebang.batch.common.JobContextKeys;
import site.icebang.external.fastapi.adapter.FastApiAdapter;
import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguCrawl;
import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguCrawl;

@Slf4j
@Component
@RequiredArgsConstructor
public class CrawlSelectedProductTasklet implements Tasklet {

private final FastApiAdapter fastApiAdapter;

@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
throws Exception {
// log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작");

ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext);
Map<String, Object> selectedProduct =
(Map<String, Object>) jobExecutionContext.get(JobContextKeys.SELECTED_PRODUCT);

if (selectedProduct == null || !selectedProduct.containsKey("link")) {
throw new RuntimeException("크롤링할 상품 URL이 없습니다.");
}
String productUrl = (String) selectedProduct.get("link");

RequestSsadaguCrawl request = new RequestSsadaguCrawl(1, 1, null, "detail", productUrl);
ResponseSsadaguCrawl response = fastApiAdapter.requestProductCrawl(request);

if (response == null || !"200".equals(response.status())) {
throw new RuntimeException("FastAPI 상품 크롤링에 실패했습니다.");
}

Map<String, Object> productDetail = response.productDetail();
log.info(">>>> FastAPI로부터 크롤링된 상품 상세 정보 획득");

jobExecutionContext.put(JobContextKeys.CRAWLED_PRODUCT_DETAIL, productDetail);

// log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료");
return RepeatStatus.FINISHED;
}

private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) {
return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package site.icebang.batch.tasklet;

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.item.ExecutionContext;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import site.icebang.batch.common.JobContextKeys;
import site.icebang.external.fastapi.adapter.FastApiAdapter;
import site.icebang.external.fastapi.dto.FastApiDto.RequestNaverSearch;
import site.icebang.external.fastapi.dto.FastApiDto.ResponseNaverSearch;

@Slf4j
@Component
@RequiredArgsConstructor
public class ExtractTrendKeywordTasklet implements Tasklet {

private final FastApiAdapter fastApiAdapter;

@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
throws Exception {
// log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작");

RequestNaverSearch request =
new RequestNaverSearch(1, 1, null, "naver", "50000000", null, null);
ResponseNaverSearch response = fastApiAdapter.requestNaverKeywordSearch(request);

if (response == null || !"200".equals(response.status())) {
throw new RuntimeException("FastAPI로부터 키워드를 추출하는 데 실패했습니다.");
}
String extractedKeyword = response.keyword();
log.info(">>>> FastAPI로부터 추출된 키워드: {}", extractedKeyword);

ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext);
// 다른 클래스의 상수를 직접 참조하는 대신 공용 인터페이스의 키를 사용
jobExecutionContext.put(JobContextKeys.EXTRACTED_KEYWORD, extractedKeyword);

// log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료");
return RepeatStatus.FINISHED;
}

private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) {
return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext();
}
}
Loading
Loading