From bd1d5a624780256d88a731560082eb22304dc27e Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 10 Sep 2025 18:09:29 +0900 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=20health=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../health/{api => controller}/HealthCheckController.java | 8 ++++---- .../{FastApiClient.java => HealthCheckService.java} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename apps/user-service/src/main/java/site/icebang/common/health/{api => controller}/HealthCheckController.java (80%) rename apps/user-service/src/main/java/site/icebang/common/health/service/{FastApiClient.java => HealthCheckService.java} (97%) diff --git a/apps/user-service/src/main/java/site/icebang/common/health/api/HealthCheckController.java b/apps/user-service/src/main/java/site/icebang/common/health/controller/HealthCheckController.java similarity index 80% rename from apps/user-service/src/main/java/site/icebang/common/health/api/HealthCheckController.java rename to apps/user-service/src/main/java/site/icebang/common/health/controller/HealthCheckController.java index 8b65e7a0..62365cd6 100644 --- a/apps/user-service/src/main/java/site/icebang/common/health/api/HealthCheckController.java +++ b/apps/user-service/src/main/java/site/icebang/common/health/controller/HealthCheckController.java @@ -1,4 +1,4 @@ -package site.icebang.common.health.api; +package site.icebang.common.health.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -6,13 +6,13 @@ import lombok.RequiredArgsConstructor; -import site.icebang.common.health.service.FastApiClient; +import site.icebang.common.health.service.HealthCheckService; @RestController @RequiredArgsConstructor public class HealthCheckController { - private final FastApiClient fastApiClient; + private final HealthCheckService healthCheckService; /** * Spring Boot와 FastAPI 서버 간의 연결 상태를 확인하는 헬스 체크 API @@ -21,7 +21,7 @@ public class HealthCheckController { */ @GetMapping("/ping") public ResponseEntity pingFastApi() { - String result = fastApiClient.ping(); + String result = healthCheckService.ping(); if (result.startsWith("ERROR")) { // FastAPI 연결 실패 시 503 Service Unavailable 상태 코드와 함께 에러 메시지 반환 diff --git a/apps/user-service/src/main/java/site/icebang/common/health/service/FastApiClient.java b/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java similarity index 97% rename from apps/user-service/src/main/java/site/icebang/common/health/service/FastApiClient.java rename to apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java index 8d8ff496..98366f11 100644 --- a/apps/user-service/src/main/java/site/icebang/common/health/service/FastApiClient.java +++ b/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java @@ -10,7 +10,7 @@ @Slf4j @Service @RequiredArgsConstructor -public class FastApiClient { +public class HealthCheckService { // WebConfig에서 생성하고 타임아웃이 설정된 RestTemplate Bean을 주입받습니다. private final RestTemplate restTemplate; From eee8bf3e23b457d4bc6d8d1ce79eb7f2bc8b4a76 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 11 Sep 2025 16:32:43 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat(config):=20@ConfigurationProperties?= =?UTF-8?q?=EB=A1=9C=20=ED=83=80=EC=9E=85-=EC=95=88=EC=A0=84=ED=95=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EA=B4=80=EB=A6=AC=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존의 @Value나 하드코딩 방식은 오타에 취약하고, 설정값 누락 시 런타임 오류를 유발할 수 있는 타입 불안전성 문제가 있었습니다. 이를 해결하기 위해 @ConfigurationProperties를 사용하는 `FastApiProperties` 클래스를 도입하여 FastAPI 연동 설정을 중앙화하고, 애플리케이션 시작 시점에 설정값의 타입과 유효성을 검증하도록 개선했습니다. 이를 통해 잠재적인 런타임 장애를 원천 차단하고, 코드의 안정성과 유지보수성을 크게 향상시켰습니다. --- .../health/service/HealthCheckService.java | 20 ++++--------- .../config/properties/FastApiProperties.java | 30 +++++++++++++++++++ .../src/main/resources/application.yml | 8 ++++- 3 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java diff --git a/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java b/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java index 98366f11..b6a1ab75 100644 --- a/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java +++ b/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import site.icebang.global.config.properties.FastApiProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,30 +13,21 @@ @RequiredArgsConstructor public class HealthCheckService { - // WebConfig에서 생성하고 타임아웃이 설정된 RestTemplate Bean을 주입받습니다. private final RestTemplate restTemplate; - // FastAPI 서버의 ping 엔드포인트 URL을 상수로 하드코딩합니다. - private static final String FASTAPI_PING_URL = "http://localhost:8000/ping"; + private final FastApiProperties fastApiProperties; /** * FastAPI 서버의 /ping 엔드포인트를 호출하여 연결을 테스트합니다. - * - * @return 연결 성공 시 FastAPI로부터 받은 응답, 실패 시 에러 메시지 */ public String ping() { - log.info("Attempting to connect to FastAPI server at: {}", FASTAPI_PING_URL); + String url = fastApiProperties.getUrl() + "/ping"; + log.info("Attempting to connect to FastAPI server at: {}", url); try { - // FastAPI 서버에 GET 요청을 보내고, 응답을 String으로 받습니다. - // WebConfig에 설정된 5초 타임아웃이 여기서 적용됩니다. - String response = restTemplate.getForObject(FASTAPI_PING_URL, String.class); - log.info("Successfully received response from FastAPI: {}", response); - return response; + return restTemplate.getForObject(url, String.class); } catch (RestClientException e) { - // RestClientException은 연결 실패, 타임아웃 등 모든 통신 오류를 포함합니다. - log.error( - "Failed to connect to FastAPI server at {}. Error: {}", FASTAPI_PING_URL, e.getMessage()); + log.error("Failed to connect to FastAPI server at {}. Error: {}", url, e.getMessage()); return "ERROR: Cannot connect to FastAPI"; } } diff --git a/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java b/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java new file mode 100644 index 00000000..c5ccbee5 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java @@ -0,0 +1,30 @@ +package site.icebang.global.config.properties; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * FastAPI 연동을 위한 설정값을 application.yml에서 바인딩하는 클래스 + */ +@Getter +@Setter +@Component // Component로 등록하여 Spring이 Bean으로 관리하도록 함 +@ConfigurationProperties(prefix = "api.fastapi") // yml의 "api.fastapi" 접두사를 가진 설정을 매핑 +@Validated // 아래의 유효성 검사 어노테이션을 활성화 +public class FastApiProperties { + + /** + * FastAPI 서버의 기본 URL + */ + @NotBlank // 값이 비어있을 수 없음을 검증 + private String url; + + /** + * API 호출 시 적용될 타임아웃 (밀리초 단위) + */ + private int timeout = 5000; // 기본값 5초 설정 +} diff --git a/apps/user-service/src/main/resources/application.yml b/apps/user-service/src/main/resources/application.yml index d0357684..d6f68b0e 100644 --- a/apps/user-service/src/main/resources/application.yml +++ b/apps/user-service/src/main/resources/application.yml @@ -10,4 +10,10 @@ spring: mybatis: # Mapper XML 파일 위치 mapper-locations: classpath:mapper/**/*.xml - type-handlers-package: site.icebang.config.mybatis.typehandler \ No newline at end of file + type-handlers-package: site.icebang.config.mybatis.typehandler + +# 외부 API 연동을 위한 설정 섹션 +api: + fastapi: + url: http://pre-processing-service:8000 # FastAPI 서버의 기본 URL + timeout: 10000 # API 요청 타임아웃 (밀리초 단위) \ No newline at end of file From b83bf0723079a12307ffed184104ed383e939dd4 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 11 Sep 2025 16:51:17 +0900 Subject: [PATCH 03/10] refactor: Code Formatting --- .../health/service/HealthCheckService.java | 7 +++--- .../config/properties/FastApiProperties.java | 25 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java b/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java index b6a1ab75..30dc6373 100644 --- a/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java +++ b/apps/user-service/src/main/java/site/icebang/common/health/service/HealthCheckService.java @@ -3,11 +3,12 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import site.icebang.global.config.properties.FastApiProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import site.icebang.global.config.properties.FastApiProperties; + @Slf4j @Service @RequiredArgsConstructor @@ -17,9 +18,7 @@ public class HealthCheckService { private final FastApiProperties fastApiProperties; - /** - * FastAPI 서버의 /ping 엔드포인트를 호출하여 연결을 테스트합니다. - */ + /** FastAPI 서버의 /ping 엔드포인트를 호출하여 연결을 테스트합니다. */ public String ping() { String url = fastApiProperties.getUrl() + "/ping"; log.info("Attempting to connect to FastAPI server at: {}", url); diff --git a/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java b/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java index c5ccbee5..24fa309d 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/properties/FastApiProperties.java @@ -1,15 +1,14 @@ package site.icebang.global.config.properties; -import jakarta.validation.constraints.NotBlank; -import lombok.Getter; -import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; -/** - * FastAPI 연동을 위한 설정값을 application.yml에서 바인딩하는 클래스 - */ +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** FastAPI 연동을 위한 설정값을 application.yml에서 바인딩하는 클래스 */ @Getter @Setter @Component // Component로 등록하여 Spring이 Bean으로 관리하도록 함 @@ -17,14 +16,10 @@ @Validated // 아래의 유효성 검사 어노테이션을 활성화 public class FastApiProperties { - /** - * FastAPI 서버의 기본 URL - */ - @NotBlank // 값이 비어있을 수 없음을 검증 - private String url; + /** FastAPI 서버의 기본 URL */ + @NotBlank // 값이 비어있을 수 없음을 검증 + private String url; - /** - * API 호출 시 적용될 타임아웃 (밀리초 단위) - */ - private int timeout = 5000; // 기본값 5초 설정 + /** API 호출 시 적용될 타임아웃 (밀리초 단위) */ + private int timeout = 5000; // 기본값 5초 설정 } From 902ff1c535904d3217aaff22a18d26d178ecddae Mon Sep 17 00:00:00 2001 From: jihukimme Date: Sat, 13 Sep 2025 16:46:31 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feature:=20=EB=B8=94=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=ED=99=94=20=EB=B0=B0=EC=B9=98=20Job=20?= =?UTF-8?q?=EB=B0=8F=20Tasklet=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 콘텐츠 자동화 워크플로우의 핵심 실행 로직인 Spring Batch Job과 그를 구성하는 7개의 Tasklet을 구현합니다. ### 주요 구현 내용: 1. **`BlogAutomationJobConfig.java`**: - 워크플로우의 전체 실행 흐름을 담당하는 두 개의 Job(`productSelectionJob`, `contentPublishingJob`)과 각 Job을 구성하는 Step들을 정의했습니다. - '상품 선정/수집'과 '콘텐츠 생성/발행'이라는 역할을 명확히 분리하여 Job의 재사용성과 독립성을 확보했습니다. 2. **7개의 Tasklet 구현**: - `ExtractTrendKeywordTasklet`: 트렌드 키워드 추출 - `SearchProductsFromMallTasklet`: 키워드로 쇼핑몰 상품 목록 검색 - `MatchProductWithKeywordTasklet`: 키워드와 상품명 매칭 - `FindSimilarProductsTasklet`: 매칭된 상품의 유사 상품 탐색 - `CrawlSelectedProductTasklet`: 최종 선택된 상품 상세 정보 크롤링 - `GenerateBlogContentTasklet`: 수집된 정보로 AI 블로그 원고 생성 - `PublishBlogPostTasklet`: 완성된 원고를 블로그에 발행 3. **Step 간 데이터 전달**: - 각 Tasklet은 `JobExecutionContext`를 통해 다음 단계로 필요한 데이터(추출된 키워드, 선택된 상품 정보, 생성된 콘텐츠 등)를 전달하도록 구현되었습니다. --- .../batch/job/BlogAutomationJobConfig.java | 115 ++++++++++++++++++ .../batch/job/BlogContentJobConfig.java | 51 -------- .../tasklet/ContentGenerationTasklet.java | 49 -------- .../tasklet/CrawlSelectedProductTasklet.java | 48 ++++++++ .../tasklet/ExtractTrendKeywordTasklet.java | 41 +++++++ .../tasklet/FindSimilarProductsTasklet.java | 35 ++++++ .../tasklet/GenerateBlogContentTasklet.java | 46 +++++++ .../tasklet/KeywordExtractionTasklet.java | 47 ------- .../MatchProductWithKeywordTasklet.java | 42 +++++++ .../batch/tasklet/PublishBlogPostTasklet.java | 39 ++++++ .../SearchProductsFromMallTasklet.java | 46 +++++++ 11 files changed, 412 insertions(+), 147 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/job/BlogContentJobConfig.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/ContentGenerationTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/KeywordExtractionTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java create mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java diff --git a/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java b/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java new file mode 100644 index 00000000..16242b57 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java @@ -0,0 +1,115 @@ +package site.icebang.batch.job; // 패키지 경로 수정 + +import site.icebang.batch.tasklet.*; // import 경로 수정 +import lombok.RequiredArgsConstructor; +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; + +/** + * [배치 시스템 구현] + * 트렌드 기반 블로그 자동화 워크플로우를 구성하는 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(); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/batch/job/BlogContentJobConfig.java b/apps/user-service/src/main/java/site/icebang/batch/job/BlogContentJobConfig.java deleted file mode 100644 index 5e85fe9f..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/job/BlogContentJobConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -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.ContentGenerationTasklet; -import site.icebang.batch.tasklet.KeywordExtractionTasklet; - -@Configuration -@RequiredArgsConstructor -public class BlogContentJobConfig { - - // 변경점 1: Factory 대신 실제 Tasklet만 필드로 주입받습니다. - private final KeywordExtractionTasklet keywordExtractionTasklet; - private final ContentGenerationTasklet contentGenerationTasklet; - - @Bean - public Job blogContentJob( - JobRepository jobRepository, Step keywordExtractionStep, Step contentGenerationStep) { - return new JobBuilder("blogContentJob", jobRepository) // 변경점 2: JobBuilder를 직접 생성합니다. - .start(keywordExtractionStep) - .next(contentGenerationStep) - .build(); - } - - @Bean - public Step keywordExtractionStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("keywordExtractionStep", jobRepository) // 변경점 3: StepBuilder를 직접 생성합니다. - .tasklet( - keywordExtractionTasklet, - transactionManager) // 변경점 4: tasklet에 transactionManager를 함께 전달합니다. - .build(); - } - - @Bean - public Step contentGenerationStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("contentGenerationStep", jobRepository) - .tasklet(contentGenerationTasklet, transactionManager) - .build(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ContentGenerationTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ContentGenerationTasklet.java deleted file mode 100644 index a6ef4505..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ContentGenerationTasklet.java +++ /dev/null @@ -1,49 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; - -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; - -@Slf4j -@Component -@RequiredArgsConstructor -public class ContentGenerationTasklet implements Tasklet { - - // private final ContentService contentService; // 비즈니스 로직을 담은 서비스 - // private final FastApiClient fastApiClient; // FastAPI 통신을 위한 클라이언트 - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - log.info(">>>> [Step 2] ContentGenerationTasklet executed."); - - // --- 핵심: JobExecutionContext에서 이전 Step의 결과물 가져오기 --- - ExecutionContext jobExecutionContext = - chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - - // KeywordExtractionTasklet이 저장한 "extractedKeywordIds" Key로 데이터 조회 - List keywordIds = (List) jobExecutionContext.get("extractedKeywordIds"); - - if (keywordIds == null || keywordIds.isEmpty()) { - log.warn(">>>> No keyword IDs found from previous step. Skipping content generation."); - return RepeatStatus.FINISHED; - } - - log.info(">>>> Received Keyword IDs for content generation: {}", keywordIds); - - // TODO: 1. 전달받은 키워드 ID 목록으로 DB에서 상세 정보 조회 - // TODO: 2. 각 키워드/상품 정보에 대해 외부 AI 서비스(FastAPI/LangChain)를 호출하여 콘텐츠 생성을 요청 - // TODO: 3. 생성된 콘텐츠를 DB에 저장 - - log.info(">>>> [Step 2] ContentGenerationTasklet finished."); - return RepeatStatus.FINISHED; - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java new file mode 100644 index 00000000..66d14d8a --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java @@ -0,0 +1,48 @@ +package site.icebang.batch.tasklet; + +import java.util.Map; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CrawlSelectedProductTasklet implements Tasklet { + + public static final String CRAWLED_PRODUCT_DATA = "crawledProductData"; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 5] 최종 선택 상품 크롤링 Task 실행 시작"); + + ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + Long selectedProductId = (Long) jobExecutionContext.get(MatchProductWithKeywordTasklet.MATCHED_PRODUCT_ID); + + if (selectedProductId == null) { + log.warn(">>>> 이전 Step에서 전달된 최종 상품 ID가 없습니다. Step 5를 건너뜁니다."); + return RepeatStatus.FINISHED; + } + + log.info(">>>> 상품 ID {}에 대한 상세 정보 크롤링 시작...", selectedProductId); + // TODO: FastAPI 워커 등을 호출하여 해당 상품의 상세 페이지를 크롤링하는 로직 구현 + // 예시: 크롤링 결과로 상품 상세 정보를 Map 형태로 가져왔다고 가정 + Map crawledData = Map.of( + "productId", selectedProductId, + "productName", "초경량 캠핑 릴렉스 체어", + "price", 35000, + "description", "매우 편안하고 가벼운 캠핑 의자입니다." + ); + + jobExecutionContext.put(CRAWLED_PRODUCT_DATA, crawledData); + log.info(">>>> 크롤링된 데이터: {}, JobExecutionContext에 저장 완료", crawledData); + + log.info(">>>> [Step 5] 최종 선택 상품 크롤링 Task 실행 완료"); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java new file mode 100644 index 00000000..699b4080 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java @@ -0,0 +1,41 @@ +package site.icebang.batch.tasklet; + +import java.util.List; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ExtractTrendKeywordTasklet implements Tasklet { + + public static final String KEYWORD_LIST = "keywordList"; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 1] 트렌드 키워드 추출 Task 실행 시작"); + + // TODO: 실제 네이버 트렌드 등에서 키워드를 추출하는 로직 구현 + // 예시: "캠핑 의자"라는 키워드 목록을 추출했다고 가정 + List keywordList = List.of("캠핑 의자", "경량 체어", "릴렉스 체어"); + + // JobExecution 전체에서 공유되는 ExecutionContext를 가져옴 + ExecutionContext jobExecutionContext = chunkContext.getStepContext() + .getStepExecution() + .getJobExecution() + .getExecutionContext(); + + // 다음 Step으로 전달하기 위해 추출된 키워드 목록을 저장 + jobExecutionContext.put(KEYWORD_LIST, keywordList); + log.info(">>>> 추출된 키워드: {}, JobExecutionContext에 저장 완료", keywordList); + + log.info(">>>> [Step 1] 트렌드 키워드 추출 Task 실행 완료"); + return RepeatStatus.FINISHED; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java new file mode 100644 index 00000000..1c73a3ab --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java @@ -0,0 +1,35 @@ +package site.icebang.batch.tasklet; + +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FindSimilarProductsTasklet implements Tasklet { + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 4] 유사 상품 탐색 Task 실행 시작"); + + ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + Long matchedProductId = (Long) jobExecutionContext.get(MatchProductWithKeywordTasklet.MATCHED_PRODUCT_ID); + + if (matchedProductId == null) { + log.warn(">>>> 이전 Step에서 전달된 매칭 상품 ID가 없습니다. Step 4를 건너뜁니다."); + return RepeatStatus.FINISHED; + } + + // TODO: 선택된 상품과 유사한 다른 상품들을 탐색하여 콘텐츠를 풍부하게 만드는 로직 구현 + log.info(">>>> 상품 ID {}에 대한 유사 상품 탐색 수행...", matchedProductId); + + log.info(">>>> [Step 4] 유사 상품 탐색 Task 실행 완료"); + return RepeatStatus.FINISHED; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java new file mode 100644 index 00000000..373ef40f --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java @@ -0,0 +1,46 @@ +package site.icebang.batch.tasklet; + +import java.util.Map; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GenerateBlogContentTasklet implements Tasklet { + + public static final String BLOG_CONTENT = "blogContent"; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Task 실행 시작"); + + ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + Map productData = (Map) jobExecutionContext.get(CrawlSelectedProductTasklet.CRAWLED_PRODUCT_DATA); + + if (productData == null) { + log.warn(">>>> 이전 Job에서 전달된 상품 정보가 없습니다. Step 6을 건너뜁니다."); + return RepeatStatus.FINISHED; + } + + log.info(">>>> 상품 정보 '{}'를 기반으로 콘텐츠 생성 시작...", productData.get("productName")); + // TODO: FastAPI/LangChain 등을 호출하여 상품 정보로 블로그 원고를 생성하는 로직 구현 + // 예시: AI가 생성한 블로그 콘텐츠 + Map blogContent = Map.of( + "title", "오늘의 추천! " + productData.get("productName"), + "body", productData.get("description") + " 이 상품은 정말 최고입니다! 가격은 " + productData.get("price") + "원!" + ); + + jobExecutionContext.put(BLOG_CONTENT, blogContent); + log.info(">>>> 생성된 블로그 콘텐츠: {}, JobExecutionContext에 저장 완료", blogContent.get("title")); + + log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Task 실행 완료"); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/KeywordExtractionTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/KeywordExtractionTasklet.java deleted file mode 100644 index ebc27117..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/KeywordExtractionTasklet.java +++ /dev/null @@ -1,47 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; - -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; - -@Slf4j -@Component -@RequiredArgsConstructor -public class KeywordExtractionTasklet implements Tasklet { - - // private final TrendKeywordService trendKeywordService; // 비즈니스 로직을 담은 서비스 - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - log.info(">>>> [Step 1] KeywordExtractionTasklet executed."); - - // TODO: 1. DB에서 카테고리 정보 조회 - // TODO: 2. 외부 API 또는 내부 로직을 통해 트렌드 키워드 추출 - // TODO: 3. 추출된 키워드를 DB에 저장 - - // --- 핵심: 다음 Step에 전달할 데이터 생성 --- - // 예시: 새로 생성된 키워드 ID 목록을 가져왔다고 가정 - List extractedKeywordIds = List.of(1L, 2L, 3L); // 실제로는 DB 저장 후 반환된 ID 목록 - log.info(">>>> Extracted Keyword IDs: {}", extractedKeywordIds); - - // --- 핵심: JobExecutionContext에 결과물 저장 --- - // JobExecution 전체에서 공유되는 컨텍스트를 가져옵니다. - ExecutionContext jobExecutionContext = - chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - - // "extractedKeywordIds" 라는 Key로 데이터 저장 - jobExecutionContext.put("extractedKeywordIds", extractedKeywordIds); - - log.info(">>>> [Step 1] KeywordExtractionTasklet finished."); - return RepeatStatus.FINISHED; - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java new file mode 100644 index 00000000..5626c1dd --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java @@ -0,0 +1,42 @@ +package site.icebang.batch.tasklet; + +import java.util.List; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MatchProductWithKeywordTasklet implements Tasklet { + + public static final String MATCHED_PRODUCT_ID = "matchedProductId"; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 3] 상품-키워드 매칭 Task 실행 시작"); + + ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + List productCandidates = (List) jobExecutionContext.get(SearchProductsFromMallTasklet.PRODUCT_CANDIDATES); + + if (productCandidates == null || productCandidates.isEmpty()) { + log.warn(">>>> 이전 Step에서 전달된 상품 후보가 없습니다. Step 3을 건너뜁니다."); + return RepeatStatus.FINISHED; + } + + // TODO: 키워드와 상품 후보 목록 간의 매칭 점수를 계산하여 최적의 상품을 찾는 로직 구현 + // 예시: 첫 번째 상품이 가장 적합하다고 선택 + Long matchedProductId = productCandidates.get(0); + + jobExecutionContext.put(MATCHED_PRODUCT_ID, matchedProductId); + log.info(">>>> 최종 매칭 상품 ID: {}, JobExecutionContext에 저장 완료", matchedProductId); + + log.info(">>>> [Step 3] 상품-키워드 매칭 Task 실행 완료"); + return RepeatStatus.FINISHED; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java new file mode 100644 index 00000000..e012ef70 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java @@ -0,0 +1,39 @@ +package site.icebang.batch.tasklet; + +import java.util.Map; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PublishBlogPostTasklet implements Tasklet { + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 7] 블로그 포스팅 Task 실행 시작"); + + ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + Map blogContent = (Map) jobExecutionContext.get(GenerateBlogContentTasklet.BLOG_CONTENT); + + if (blogContent == null) { + log.warn(">>>> 이전 Step에서 전달된 블로그 콘텐츠가 없습니다. Step 7을 건너뜁니다."); + return RepeatStatus.FINISHED; + } + + // TODO: 네이버 블로그 API 등을 호출하여 실제 포스팅을 발행하는 로직 구현 + log.info(">>>> 블로그에 콘텐츠 발행: '{}'", blogContent.get("title")); + String publishedUrl = "https://blog.naver.com/myblog/12345"; // 예시 URL + + log.info(">>>> 발행 완료! URL: {}", publishedUrl); + log.info(">>>> [Step 7] 블로그 포스팅 Task 실행 완료"); + + return RepeatStatus.FINISHED; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java new file mode 100644 index 00000000..79a7bfb5 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java @@ -0,0 +1,46 @@ +package site.icebang.batch.tasklet; + +import java.util.List; +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.item.ExecutionContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SearchProductsFromMallTasklet implements Tasklet { + + public static final String PRODUCT_CANDIDATES = "productCandidates"; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info(">>>> [Step 2] 쇼핑몰 상품 검색 Task 실행 시작"); + + ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + + // 이전 Step에서 저장한 키워드 목록을 가져옴 + List keywordList = (List) jobExecutionContext.get(ExtractTrendKeywordTasklet.KEYWORD_LIST); + + if (keywordList == null || keywordList.isEmpty()) { + log.warn(">>>> 이전 Step에서 전달된 키워드가 없습니다. Step 2를 건너뜁니다."); + return RepeatStatus.FINISHED; + } + + log.info(">>>> 키워드 '{}'(으)로 상품 검색 시작", keywordList.get(0)); + // TODO: FastAPI 워커 등을 호출하여 실제 쇼핑몰에서 상품 목록을 검색하는 로직 구현 + // 예시: 상품 후보 3개를 찾았다고 가정 + List productCandidates = List.of(1001L, 1002L, 1003L); // 상품 ID 목록 + + // 다음 Step으로 전달하기 위해 후보 상품 목록을 저장 + jobExecutionContext.put(PRODUCT_CANDIDATES, productCandidates); + log.info(">>>> 상품 후보 목록: {}, JobExecutionContext에 저장 완료", productCandidates); + + log.info(">>>> [Step 2] 쇼핑몰 상품 검색 Task 실행 완료"); + return RepeatStatus.FINISHED; + } +} From 7b779eed1c136138883c855a30d95258a0344b85 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Sun, 14 Sep 2025 15:45:36 +0900 Subject: [PATCH 05/10] =?UTF-8?q?chore:=20fastapi=EC=99=80=20=EC=83=81?= =?UTF-8?q?=ED=98=B8=EC=9E=91=EC=9A=A9=EC=9D=84=20=EB=8B=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20external=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icebang/batch/common/JobContextKeys.java | 16 +++ .../fastapi/adapter/FastApiAdapter.java | 122 +++++++++++++++++ .../external/fastapi/dto/FastApiDto.java | 125 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java create mode 100644 apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java create mode 100644 apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java diff --git a/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java b/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java new file mode 100644 index 00000000..45d31851 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java @@ -0,0 +1,16 @@ +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"; +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java b/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java new file mode 100644 index 00000000..26b22d8e --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java @@ -0,0 +1,122 @@ +package site.icebang.external.fastapi.adapter; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import site.icebang.external.fastapi.dto.FastApiDto.*; +import site.icebang.global.config.properties.FastApiProperties; + +/** + * FastAPI 서버와의 통신을 전담하는 어댑터 클래스. + * 모든 외부 API 호출은 이 클래스를 통해 이루어집니다. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FastApiAdapter { + + private final RestTemplate restTemplate; + private final FastApiProperties properties; + + /** + * TASK 1: 네이버 키워드 추출을 FastAPI에 요청합니다. + */ + public ResponseNaverSearch requestNaverKeywordSearch(RequestNaverSearch request) { + String url = properties.getUrl() + "/keyword/search"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseNaverSearch.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI keyword search API. Error: {}", e.getMessage()); + // TODO: 비즈니스 요구사항에 맞는 예외 처리 (재시도, 기본값 반환, 특정 예외 던지기 등) + return null; + } + } + + /** + * TASK 2: 싸다구몰 상품 검색을 FastAPI에 요청합니다. + */ + public ResponseSsadaguSearch requestSsadaguProductSearch(RequestSsadaguSearch request) { + String url = properties.getUrl() + "/product/search"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguSearch.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product search API. Error: {}", e.getMessage()); + return null; + } + } + + /** + * TASK 3: 상품 매칭을 FastAPI에 요청합니다. + */ + public ResponseSsadaguMatch requestProductMatch(RequestSsadaguMatch request) { + String url = properties.getUrl() + "/product/match"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguMatch.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product match API. Error: {}", e.getMessage()); + return null; + } + } + + /** + * TASK 4: 상품 유사도 분석을 FastAPI에 요청합니다. (메서드명 수정) + */ + public ResponseSsadaguSimilarity requestProductSimilarity(RequestSsadaguSimilarity request) { + String url = properties.getUrl() + "/product/similarity"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguSimilarity.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product similarity API. Error: {}", e.getMessage()); + return null; + } + } + + + /** + * TASK 5: 상품 상세 정보 크롤링을 FastAPI에 요청합니다. + */ + public ResponseSsadaguCrawl requestProductCrawl(RequestSsadaguCrawl request) { + String url = properties.getUrl() + "/product/crawl"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguCrawl.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product crawl API. Error: {}", e.getMessage()); + return null; + } + } + + /** + * TASK 6: 블로그 콘텐츠 생성을 FastAPI에 요청합니다. + */ + public ResponseBlogCreate requestBlogCreation(RequestBlogCreate request) { + String url = properties.getUrl() + "/blog/rag/create"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseBlogCreate.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI blog creation API. Error: {}", e.getMessage()); + return null; + } + } + + /** + * TASK 7: 블로그 발행을 FastAPI에 요청합니다. + */ + public ResponseBlogPublish requestBlogPost(RequestBlogPublish request) { + String url = properties.getUrl() + "/blog/publish"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseBlogPublish.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI blog publish API. Error: {}", e.getMessage()); + return null; + } + } +} diff --git a/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java b/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java new file mode 100644 index 00000000..ca0b7972 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java @@ -0,0 +1,125 @@ +package site.icebang.external.fastapi.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +/** + * FastAPI 서버와 통신하기 위한 DTO 클래스 모음. + * Java의 record를 사용하여 불변 데이터 객체를 간결하게 정의합니다. + */ +public final class FastApiDto { + + // --- 1. 네이버 키워드 추출 --- + public record RequestNaverSearch( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String tag, + String category, + @JsonProperty("start_date") String startDate, + @JsonProperty("end_date") String endDate + ) {} + + public record ResponseNaverSearch( + String status, + String category, + String keyword, + @JsonProperty("total_keyword") Map totalKeyword + ) {} + + // --- 2. 상품 검색 --- + public record RequestSsadaguSearch( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String keyword + ) {} + + public record ResponseSsadaguSearch( + String status, + String keyword, + @JsonProperty("search_results") List> searchResults + ) {} + + // --- 3. 상품 매칭 --- + public record RequestSsadaguMatch( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String keyword, + @JsonProperty("search_results") List> searchResults + ) {} + + public record ResponseSsadaguMatch( + String status, + String keyword, + @JsonProperty("matched_products") List> matchedProducts + ) {} + + + // --- 4. 상품 유사도 --- + public record RequestSsadaguSimilarity( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String keyword, + @JsonProperty("matched_products") List> matchedProducts, + @JsonProperty("search_results") List> searchResults + ) {} + + public record ResponseSsadaguSimilarity( + String status, + String keyword, + @JsonProperty("selected_product") Map selectedProduct, + String reason + ) {} + + + // --- 5. 상품 크롤링 --- + public record RequestSsadaguCrawl( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String tag, + @JsonProperty("product_url") String productUrl + ) {} + + public record ResponseSsadaguCrawl( + String status, + String tag, + @JsonProperty("product_url") String productUrl, + @JsonProperty("product_detail") Map productDetail, + @JsonProperty("crawled_at") String crawledAt + ) {} + + + // --- 6. 블로그 콘텐츠 생성 --- + public record RequestBlogCreate( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId + ) {} + + public record ResponseBlogCreate( + String status + ) {} + + // --- 7. 블로그 발행 --- + public record RequestBlogPublish( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String tag, + @JsonProperty("blog_id") String blogId, + @JsonProperty("blog_pw") String blogPw, + @JsonProperty("post_title") String postTitle, + @JsonProperty("post_content") String postContent, + @JsonProperty("post_tags") List postTags + ) {} + + public record ResponseBlogPublish( + String status, + Map metadata + ) {} +} From 8678d41d022a4a6dd5ecade3208e9c927c675d32 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Sun, 14 Sep 2025 15:50:27 +0900 Subject: [PATCH 06/10] =?UTF-8?q?chore:=20task=20=EA=B0=84=EC=97=90=20?= =?UTF-8?q?=EA=B2=B0=ED=95=A9=EB=8F=84=EB=A5=BC=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 특정 step에서 만든 데이터를 다음 step에서 사용하는 경우가 있음(`JobExecutionContext`를 통해 데이터를 주고 받음) - `JobContextKeys`라는 인터페이스를 정의해 다른 task의 내부 변수를 직접 참조하지 않도록 함 --- .../tasklet/ExtractTrendKeywordTasklet.java | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java index 699b4080..ea43ac5a 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java @@ -1,6 +1,5 @@ package site.icebang.batch.tasklet; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; @@ -9,33 +8,47 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +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 { - public static final String KEYWORD_LIST = "keywordList"; + // ExecutionContext에서 사용할 데이터 Key + public static final String EXTRACTED_KEYWORD = "extractedKeyword"; + + private final FastApiAdapter fastApiAdapter; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 1] 트렌드 키워드 추출 Task 실행 시작"); + log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작"); + + // 1. FastAPI에 보낼 요청 DTO 생성 (실제 값은 JobParameters 등에서 동적으로 가져와야 함) + RequestNaverSearch request = new RequestNaverSearch(1, 1, null, "naver", "50000000", null, null); - // TODO: 실제 네이버 트렌드 등에서 키워드를 추출하는 로직 구현 - // 예시: "캠핑 의자"라는 키워드 목록을 추출했다고 가정 - List keywordList = List.of("캠핑 의자", "경량 체어", "릴렉스 체어"); + // 2. FastApiAdapter를 통해 FastAPI 호출 + ResponseNaverSearch response = fastApiAdapter.requestNaverKeywordSearch(request); - // JobExecution 전체에서 공유되는 ExecutionContext를 가져옴 - ExecutionContext jobExecutionContext = chunkContext.getStepContext() - .getStepExecution() - .getJobExecution() - .getExecutionContext(); + // 3. 응답 검증 + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI로부터 키워드를 추출하는 데 실패했습니다."); + } + String extractedKeyword = response.keyword(); + log.info(">>>> FastAPI로부터 추출된 키워드: {}", extractedKeyword); - // 다음 Step으로 전달하기 위해 추출된 키워드 목록을 저장 - jobExecutionContext.put(KEYWORD_LIST, keywordList); - log.info(">>>> 추출된 키워드: {}, JobExecutionContext에 저장 완료", keywordList); + // 4. 다음 Step으로 전달하기 위해 결과를 JobExecutionContext에 저장 + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + jobExecutionContext.put(EXTRACTED_KEYWORD, extractedKeyword); - log.info(">>>> [Step 1] 트렌드 키워드 추출 Task 실행 완료"); + log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } + From 37db7ce769b9288924d424eb2dc6de9d8a38c391 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 15 Sep 2025 12:04:58 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=20FastApiAdapter=20=EB=B0=8F=20?= =?UTF-8?q?JobContextKeys=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tasklet/CrawlSelectedProductTasklet.java | 48 +++++++++++-------- .../tasklet/ExtractTrendKeywordTasklet.java | 12 ++--- .../tasklet/FindSimilarProductsTasklet.java | 37 ++++++++++---- .../tasklet/GenerateBlogContentTasklet.java | 46 +++++++++++------- .../MatchProductWithKeywordTasklet.java | 37 +++++++++----- .../batch/tasklet/PublishBlogPostTasklet.java | 47 +++++++++++++----- .../SearchProductsFromMallTasklet.java | 43 ++++++++++------- 7 files changed, 173 insertions(+), 97 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java index 66d14d8a..63ec6082 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java @@ -9,40 +9,48 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +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 { - public static final String CRAWLED_PRODUCT_DATA = "crawledProductData"; + private final FastApiAdapter fastApiAdapter; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 5] 최종 선택 상품 크롤링 Task 실행 시작"); + log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - Long selectedProductId = (Long) jobExecutionContext.get(MatchProductWithKeywordTasklet.MATCHED_PRODUCT_ID); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + Map selectedProduct = (Map) jobExecutionContext.get(JobContextKeys.SELECTED_PRODUCT); - if (selectedProductId == null) { - log.warn(">>>> 이전 Step에서 전달된 최종 상품 ID가 없습니다. Step 5를 건너뜁니다."); - return RepeatStatus.FINISHED; + if (selectedProduct == null || !selectedProduct.containsKey("link")) { + throw new RuntimeException("크롤링할 상품 URL이 없습니다."); } + String productUrl = (String) selectedProduct.get("link"); - log.info(">>>> 상품 ID {}에 대한 상세 정보 크롤링 시작...", selectedProductId); - // TODO: FastAPI 워커 등을 호출하여 해당 상품의 상세 페이지를 크롤링하는 로직 구현 - // 예시: 크롤링 결과로 상품 상세 정보를 Map 형태로 가져왔다고 가정 - Map crawledData = Map.of( - "productId", selectedProductId, - "productName", "초경량 캠핑 릴렉스 체어", - "price", 35000, - "description", "매우 편안하고 가벼운 캠핑 의자입니다." - ); + RequestSsadaguCrawl request = new RequestSsadaguCrawl(1, 1, null, "detail", productUrl); + ResponseSsadaguCrawl response = fastApiAdapter.requestProductCrawl(request); - jobExecutionContext.put(CRAWLED_PRODUCT_DATA, crawledData); - log.info(">>>> 크롤링된 데이터: {}, JobExecutionContext에 저장 완료", crawledData); + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 크롤링에 실패했습니다."); + } + + Map productDetail = response.productDetail(); + log.info(">>>> FastAPI로부터 크롤링된 상품 상세 정보 획득"); + + jobExecutionContext.put(JobContextKeys.CRAWLED_PRODUCT_DETAIL, productDetail); - log.info(">>>> [Step 5] 최종 선택 상품 크롤링 Task 실행 완료"); + log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } -} \ No newline at end of file + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } +} + diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java index ea43ac5a..fe3d84d6 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java @@ -8,6 +8,7 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +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; @@ -17,31 +18,24 @@ @RequiredArgsConstructor public class ExtractTrendKeywordTasklet implements Tasklet { - // ExecutionContext에서 사용할 데이터 Key - public static final String EXTRACTED_KEYWORD = "extractedKeyword"; - private final FastApiAdapter fastApiAdapter; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작"); - // 1. FastAPI에 보낼 요청 DTO 생성 (실제 값은 JobParameters 등에서 동적으로 가져와야 함) RequestNaverSearch request = new RequestNaverSearch(1, 1, null, "naver", "50000000", null, null); - - // 2. FastApiAdapter를 통해 FastAPI 호출 ResponseNaverSearch response = fastApiAdapter.requestNaverKeywordSearch(request); - // 3. 응답 검증 if (response == null || !"200".equals(response.status())) { throw new RuntimeException("FastAPI로부터 키워드를 추출하는 데 실패했습니다."); } String extractedKeyword = response.keyword(); log.info(">>>> FastAPI로부터 추출된 키워드: {}", extractedKeyword); - // 4. 다음 Step으로 전달하기 위해 결과를 JobExecutionContext에 저장 ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - jobExecutionContext.put(EXTRACTED_KEYWORD, extractedKeyword); + // 다른 클래스의 상수를 직접 참조하는 대신 공용 인터페이스의 키를 사용 + jobExecutionContext.put(JobContextKeys.EXTRACTED_KEYWORD, extractedKeyword); log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); return RepeatStatus.FINISHED; diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java index 1c73a3ab..981d110d 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java @@ -1,5 +1,7 @@ package site.icebang.batch.tasklet; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; @@ -8,28 +10,45 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +import site.icebang.batch.common.JobContextKeys; +import site.icebang.external.fastapi.adapter.FastApiAdapter; +import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguSimilarity; +import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguSimilarity; @Slf4j @Component @RequiredArgsConstructor public class FindSimilarProductsTasklet implements Tasklet { + private final FastApiAdapter fastApiAdapter; + @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 4] 유사 상품 탐색 Task 실행 시작"); + log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 시작"); + + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); + List> matchedProducts = (List>) jobExecutionContext.get(JobContextKeys.MATCHED_PRODUCTS); + List> searchResults = (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); - ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - Long matchedProductId = (Long) jobExecutionContext.get(MatchProductWithKeywordTasklet.MATCHED_PRODUCT_ID); + RequestSsadaguSimilarity request = new RequestSsadaguSimilarity(1, 1, null, keyword, matchedProducts, searchResults); + ResponseSsadaguSimilarity response = fastApiAdapter.requestProductSimilarity(request); - if (matchedProductId == null) { - log.warn(">>>> 이전 Step에서 전달된 매칭 상품 ID가 없습니다. Step 4를 건너뜁니다."); - return RepeatStatus.FINISHED; + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 유사도 분석에 실패했습니다."); } - // TODO: 선택된 상품과 유사한 다른 상품들을 탐색하여 콘텐츠를 풍부하게 만드는 로직 구현 - log.info(">>>> 상품 ID {}에 대한 유사 상품 탐색 수행...", matchedProductId); + Map selectedProduct = response.selectedProduct(); + log.info(">>>> FastAPI로부터 최종 선택된 상품: {}", selectedProduct.get("title")); + + jobExecutionContext.put(JobContextKeys.SELECTED_PRODUCT, selectedProduct); - log.info(">>>> [Step 4] 유사 상품 탐색 Task 실행 완료"); + log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } + diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java index 373ef40f..1d792a1f 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java @@ -1,5 +1,6 @@ package site.icebang.batch.tasklet; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -9,38 +10,49 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +import site.icebang.batch.common.JobContextKeys; +import site.icebang.external.fastapi.adapter.FastApiAdapter; +import site.icebang.external.fastapi.dto.FastApiDto.RequestBlogCreate; +import site.icebang.external.fastapi.dto.FastApiDto.ResponseBlogCreate; @Slf4j @Component @RequiredArgsConstructor public class GenerateBlogContentTasklet implements Tasklet { - public static final String BLOG_CONTENT = "blogContent"; + private final FastApiAdapter fastApiAdapter; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Task 실행 시작"); + log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - Map productData = (Map) jobExecutionContext.get(CrawlSelectedProductTasklet.CRAWLED_PRODUCT_DATA); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + Map productDetail = (Map) jobExecutionContext.get(JobContextKeys.CRAWLED_PRODUCT_DETAIL); - if (productData == null) { - log.warn(">>>> 이전 Job에서 전달된 상품 정보가 없습니다. Step 6을 건너뜁니다."); - return RepeatStatus.FINISHED; + // TODO: productDetail을 기반으로 LLM에 전달할 프롬프트 생성 + RequestBlogCreate request = new RequestBlogCreate(1, 1, null); + ResponseBlogCreate response = fastApiAdapter.requestBlogCreation(request); + + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 블로그 콘텐츠 생성에 실패했습니다."); } - log.info(">>>> 상품 정보 '{}'를 기반으로 콘텐츠 생성 시작...", productData.get("productName")); - // TODO: FastAPI/LangChain 등을 호출하여 상품 정보로 블로그 원고를 생성하는 로직 구현 - // 예시: AI가 생성한 블로그 콘텐츠 - Map blogContent = Map.of( - "title", "오늘의 추천! " + productData.get("productName"), - "body", productData.get("description") + " 이 상품은 정말 최고입니다! 가격은 " + productData.get("price") + "원!" + // TODO: 실제 생성된 콘텐츠를 response로부터 받아와야 함 (현재는 더미 데이터) + Map generatedContent = Map.of( + "title", "엄청난 상품을 소개합니다! " + productDetail.get("title"), + "content", "이 상품은 정말... 좋습니다. 상세 정보: " + productDetail.toString(), + "tags", List.of("상품리뷰", "최고") ); + log.info(">>>> FastAPI로부터 블로그 콘텐츠 생성 완료"); - jobExecutionContext.put(BLOG_CONTENT, blogContent); - log.info(">>>> 생성된 블로그 콘텐츠: {}, JobExecutionContext에 저장 완료", blogContent.get("title")); + jobExecutionContext.put(JobContextKeys.GENERATED_CONTENT, generatedContent); - log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Task 실행 완료"); + log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } -} \ No newline at end of file + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } +} + diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java index 5626c1dd..95cc8456 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java @@ -1,6 +1,7 @@ package site.icebang.batch.tasklet; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; @@ -9,34 +10,44 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +import site.icebang.batch.common.JobContextKeys; +import site.icebang.external.fastapi.adapter.FastApiAdapter; +import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguMatch; +import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguMatch; @Slf4j @Component @RequiredArgsConstructor public class MatchProductWithKeywordTasklet implements Tasklet { - public static final String MATCHED_PRODUCT_ID = "matchedProductId"; + private final FastApiAdapter fastApiAdapter; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 3] 상품-키워드 매칭 Task 실행 시작"); + log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - List productCandidates = (List) jobExecutionContext.get(SearchProductsFromMallTasklet.PRODUCT_CANDIDATES); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); + List> searchResults = (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); - if (productCandidates == null || productCandidates.isEmpty()) { - log.warn(">>>> 이전 Step에서 전달된 상품 후보가 없습니다. Step 3을 건너뜁니다."); - return RepeatStatus.FINISHED; + RequestSsadaguMatch request = new RequestSsadaguMatch(1, 1, null, keyword, searchResults); + ResponseSsadaguMatch response = fastApiAdapter.requestProductMatch(request); + + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 매칭에 실패했습니다."); } - // TODO: 키워드와 상품 후보 목록 간의 매칭 점수를 계산하여 최적의 상품을 찾는 로직 구현 - // 예시: 첫 번째 상품이 가장 적합하다고 선택 - Long matchedProductId = productCandidates.get(0); + List> matchedProducts = response.matchedProducts(); + log.info(">>>> FastAPI로부터 매칭된 상품 {}개", matchedProducts.size()); - jobExecutionContext.put(MATCHED_PRODUCT_ID, matchedProductId); - log.info(">>>> 최종 매칭 상품 ID: {}, JobExecutionContext에 저장 완료", matchedProductId); + jobExecutionContext.put(JobContextKeys.MATCHED_PRODUCTS, matchedProducts); - log.info(">>>> [Step 3] 상품-키워드 매칭 Task 실행 완료"); + log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } + diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java index e012ef70..3a82806e 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java @@ -1,5 +1,6 @@ package site.icebang.batch.tasklet; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -9,31 +10,53 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +import site.icebang.batch.common.JobContextKeys; +import site.icebang.external.fastapi.adapter.FastApiAdapter; +import site.icebang.external.fastapi.dto.FastApiDto.RequestBlogPublish; +import site.icebang.external.fastapi.dto.FastApiDto.ResponseBlogPublish; @Slf4j @Component @RequiredArgsConstructor public class PublishBlogPostTasklet implements Tasklet { + private final FastApiAdapter fastApiAdapter; + @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 7] 블로그 포스팅 Task 실행 시작"); + log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - Map blogContent = (Map) jobExecutionContext.get(GenerateBlogContentTasklet.BLOG_CONTENT); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + Map content = (Map) jobExecutionContext.get(JobContextKeys.GENERATED_CONTENT); - if (blogContent == null) { - log.warn(">>>> 이전 Step에서 전달된 블로그 콘텐츠가 없습니다. Step 7을 건너뜁니다."); - return RepeatStatus.FINISHED; - } + // TODO: UserConfig 등에서 실제 블로그 정보(ID, PW)를 가져와야 함 + String blogId = "my_blog_id"; + String blogPw = "my_blog_password"; + + RequestBlogPublish request = new RequestBlogPublish( + 1, 1, null, + "naver", + blogId, + blogPw, + (String) content.get("title"), + (String) content.get("content"), + (List) content.get("tags") + ); + + ResponseBlogPublish response = fastApiAdapter.requestBlogPost(request); - // TODO: 네이버 블로그 API 등을 호출하여 실제 포스팅을 발행하는 로직 구현 - log.info(">>>> 블로그에 콘텐츠 발행: '{}'", blogContent.get("title")); - String publishedUrl = "https://blog.naver.com/myblog/12345"; // 예시 URL + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 블로그 발행에 실패했습니다."); + } - log.info(">>>> 발행 완료! URL: {}", publishedUrl); - log.info(">>>> [Step 7] 블로그 포스팅 Task 실행 완료"); + log.info(">>>> FastAPI를 통해 블로그 발행 성공: {}", response.metadata()); + log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } + diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java index 79a7bfb5..0d54aab4 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java @@ -1,6 +1,7 @@ package site.icebang.batch.tasklet; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; @@ -9,38 +10,46 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +import site.icebang.batch.common.JobContextKeys; +import site.icebang.external.fastapi.adapter.FastApiAdapter; +import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguSearch; +import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguSearch; @Slf4j @Component @RequiredArgsConstructor public class SearchProductsFromMallTasklet implements Tasklet { - public static final String PRODUCT_CANDIDATES = "productCandidates"; + private final FastApiAdapter fastApiAdapter; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 2] 쇼핑몰 상품 검색 Task 실행 시작"); + log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - // 이전 Step에서 저장한 키워드 목록을 가져옴 - List keywordList = (List) jobExecutionContext.get(ExtractTrendKeywordTasklet.KEYWORD_LIST); - - if (keywordList == null || keywordList.isEmpty()) { - log.warn(">>>> 이전 Step에서 전달된 키워드가 없습니다. Step 2를 건너뜁니다."); - return RepeatStatus.FINISHED; + if (keyword == null) { + throw new RuntimeException("이전 Step에서 키워드를 전달받지 못했습니다."); } - log.info(">>>> 키워드 '{}'(으)로 상품 검색 시작", keywordList.get(0)); - // TODO: FastAPI 워커 등을 호출하여 실제 쇼핑몰에서 상품 목록을 검색하는 로직 구현 - // 예시: 상품 후보 3개를 찾았다고 가정 - List productCandidates = List.of(1001L, 1002L, 1003L); // 상품 ID 목록 + RequestSsadaguSearch request = new RequestSsadaguSearch(1, 1, null, keyword); + ResponseSsadaguSearch response = fastApiAdapter.requestSsadaguProductSearch(request); + + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 검색에 실패했습니다."); + } + List> searchResults = response.searchResults(); + log.info(">>>> FastAPI로부터 검색된 상품 {}개", searchResults.size()); - // 다음 Step으로 전달하기 위해 후보 상품 목록을 저장 - jobExecutionContext.put(PRODUCT_CANDIDATES, productCandidates); - log.info(">>>> 상품 후보 목록: {}, JobExecutionContext에 저장 완료", productCandidates); + jobExecutionContext.put(JobContextKeys.SEARCHED_PRODUCTS, searchResults); - log.info(">>>> [Step 2] 쇼핑몰 상품 검색 Task 실행 완료"); + log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } + + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } + From 9f640cbc11d03ce11a244ced8916c629656daf50 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 15 Sep 2025 12:06:04 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20schedule=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20domain=20=EB=B0=96=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{domain => }/schedule/mapper/ScheduleMapper.java | 4 ++-- .../icebang/{domain => }/schedule/model/Schedule.java | 2 +- .../schedule/runner/SchedulerInitializer.java | 8 ++++---- .../schedule/service/DynamicSchedulerService.java | 4 ++-- .../src/main/resources/mybatis/mapper/ScheduleMapper.xml | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) rename apps/user-service/src/main/java/site/icebang/{domain => }/schedule/mapper/ScheduleMapper.java (63%) rename apps/user-service/src/main/java/site/icebang/{domain => }/schedule/model/Schedule.java (84%) rename apps/user-service/src/main/java/site/icebang/{domain => }/schedule/runner/SchedulerInitializer.java (78%) rename apps/user-service/src/main/java/site/icebang/{domain => }/schedule/service/DynamicSchedulerService.java (95%) diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java b/apps/user-service/src/main/java/site/icebang/schedule/mapper/ScheduleMapper.java similarity index 63% rename from apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java rename to apps/user-service/src/main/java/site/icebang/schedule/mapper/ScheduleMapper.java index c757fc36..b1a92f1e 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java +++ b/apps/user-service/src/main/java/site/icebang/schedule/mapper/ScheduleMapper.java @@ -1,10 +1,10 @@ -package site.icebang.domain.schedule.mapper; +package site.icebang.schedule.mapper; import java.util.List; import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.schedule.model.Schedule; +import site.icebang.schedule.model.Schedule; @Mapper public interface ScheduleMapper { diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java b/apps/user-service/src/main/java/site/icebang/schedule/model/Schedule.java similarity index 84% rename from apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java rename to apps/user-service/src/main/java/site/icebang/schedule/model/Schedule.java index 65c48366..ced2900c 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java +++ b/apps/user-service/src/main/java/site/icebang/schedule/model/Schedule.java @@ -1,4 +1,4 @@ -package site.icebang.domain.schedule.model; +package site.icebang.schedule.model; import lombok.Getter; import lombok.Setter; diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java b/apps/user-service/src/main/java/site/icebang/schedule/runner/SchedulerInitializer.java similarity index 78% rename from apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java rename to apps/user-service/src/main/java/site/icebang/schedule/runner/SchedulerInitializer.java index 0dfb8b33..ee8580dd 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java +++ b/apps/user-service/src/main/java/site/icebang/schedule/runner/SchedulerInitializer.java @@ -1,4 +1,4 @@ -package site.icebang.domain.schedule.runner; +package site.icebang.schedule.runner; import java.util.List; @@ -9,9 +9,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import site.icebang.domain.schedule.mapper.ScheduleMapper; -import site.icebang.domain.schedule.model.Schedule; -import site.icebang.domain.schedule.service.DynamicSchedulerService; +import site.icebang.schedule.mapper.ScheduleMapper; +import site.icebang.schedule.model.Schedule; +import site.icebang.schedule.service.DynamicSchedulerService; @Slf4j @Component diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java b/apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java similarity index 95% rename from apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java rename to apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java index 372e0e1d..b81e30eb 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java +++ b/apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java @@ -1,4 +1,4 @@ -package site.icebang.domain.schedule.service; +package site.icebang.schedule.service; import java.time.LocalDateTime; import java.util.Map; @@ -16,7 +16,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import site.icebang.domain.schedule.model.Schedule; +import site.icebang.schedule.model.Schedule; @Slf4j @Service diff --git a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml index 3cdcc90e..f9629b8a 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml @@ -1,16 +1,16 @@ - + - SELECT id AS scheduleId, workflow_id AS workflowId, cron_expression AS cronExpression, is_active AS isActive - FROM + FROM schedule - WHERE + WHERE is_active = #{isActive} From 211f7b57926bbe9427da7a06625661384a146a79 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 01:46:50 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20AOP=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20Tasklet=20=EC=8B=A4=ED=96=89=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=A4=91=EC=95=99=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 각 Tasklet 클래스 내부에 흩어져 있던 시작/종료 로그는 코드 중복을 유발하고, 비즈니스 로직의 가독성을 저해하는 문제가 있었습니다. 이 문제를 해결하기 위해, AOP를 사용하여 모든 Tasklet의 실행을 자동으로 로깅하는 `LoggingAspect`를 확장 구현합니다. ### 주요 변경 사항: - `LoggingAspect.java`에 `batch.tasklet` 패키지를 대상으로 하는 새로운 Pointcut(`taskletMethods`)을 추가했습니다. - `@Around` 어드바이스를 통해 모든 Tasklet의 실행 시작, 종료, 그리고 소요 시간을 자동으로 기록하도록 구현했습니다. ### 기대 효과: - **코드 중복 제거**: 각 Tasklet 클래스에 수동으로 작성했던 시작/종료 로그를 제거하여, 비즈니스 로직의 가독성을 크게 향상시킵니다. - **관심사의 분리(SoC)**: 핵심 로직과 로깅이라는 부가 기능 로직을 완벽하게 분리했습니다. --- .../batch/tasklet/CrawlSelectedProductTasklet.java | 4 ++-- .../batch/tasklet/ExtractTrendKeywordTasklet.java | 4 ++-- .../batch/tasklet/FindSimilarProductsTasklet.java | 4 ++-- .../batch/tasklet/GenerateBlogContentTasklet.java | 4 ++-- .../tasklet/MatchProductWithKeywordTasklet.java | 2 +- .../batch/tasklet/PublishBlogPostTasklet.java | 4 ++-- .../tasklet/SearchProductsFromMallTasklet.java | 4 ++-- .../icebang/global/aop/logging/LoggingAspect.java | 14 ++++++++++++++ 8 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java index 63ec6082..230cf55f 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java @@ -23,7 +23,7 @@ public class CrawlSelectedProductTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작"); + // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작"); ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); Map selectedProduct = (Map) jobExecutionContext.get(JobContextKeys.SELECTED_PRODUCT); @@ -45,7 +45,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon jobExecutionContext.put(JobContextKeys.CRAWLED_PRODUCT_DETAIL, productDetail); - log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료"); + // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java index fe3d84d6..c5d825ed 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java @@ -22,7 +22,7 @@ public class ExtractTrendKeywordTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작"); + // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작"); RequestNaverSearch request = new RequestNaverSearch(1, 1, null, "naver", "50000000", null, null); ResponseNaverSearch response = fastApiAdapter.requestNaverKeywordSearch(request); @@ -37,7 +37,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon // 다른 클래스의 상수를 직접 참조하는 대신 공용 인터페이스의 키를 사용 jobExecutionContext.put(JobContextKeys.EXTRACTED_KEYWORD, extractedKeyword); - log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); + // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java index 981d110d..bf57cd67 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java @@ -24,7 +24,7 @@ public class FindSimilarProductsTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 시작"); + // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 시작"); ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); @@ -43,7 +43,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon jobExecutionContext.put(JobContextKeys.SELECTED_PRODUCT, selectedProduct); - log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 완료"); + // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java index 1d792a1f..8b1f9a97 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java @@ -24,7 +24,7 @@ public class GenerateBlogContentTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 시작"); + // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 시작"); ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); Map productDetail = (Map) jobExecutionContext.get(JobContextKeys.CRAWLED_PRODUCT_DETAIL); @@ -47,7 +47,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon jobExecutionContext.put(JobContextKeys.GENERATED_CONTENT, generatedContent); - log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 완료"); + // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java index 95cc8456..8e9f76aa 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java @@ -24,7 +24,7 @@ public class MatchProductWithKeywordTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 시작"); + // log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 시작"); ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java index 3a82806e..e9bba5a9 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java @@ -24,7 +24,7 @@ public class PublishBlogPostTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 시작"); + // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 시작"); ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); Map content = (Map) jobExecutionContext.get(JobContextKeys.GENERATED_CONTENT); @@ -51,7 +51,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon log.info(">>>> FastAPI를 통해 블로그 발행 성공: {}", response.metadata()); - log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 완료"); + // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java index 0d54aab4..74890374 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java @@ -24,7 +24,7 @@ public class SearchProductsFromMallTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 시작"); + // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 시작"); ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); @@ -44,7 +44,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon jobExecutionContext.put(JobContextKeys.SEARCHED_PRODUCTS, searchResults); - log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 완료"); + // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 완료"); return RepeatStatus.FINISHED; } diff --git a/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java b/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java index 126c7d35..b1806cff 100644 --- a/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java +++ b/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java @@ -22,6 +22,9 @@ public void serviceMethods() {} @Pointcut("execution(public * site.icebang..service..mapper..*(..))") public void repositoryMethods() {} + @Pointcut("execution(public * site.icebang.batch.tasklet..*(..))") + public void taskletMethods() {} + @Around("controllerMethods()") public Object logController(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); @@ -51,4 +54,15 @@ public Object logRepository(ProceedingJoinPoint joinPoint) throws Throwable { log.debug("[REPOSITORY] End: {} ({}ms)", joinPoint.getSignature(), duration); return result; } + + @Around("taskletMethods()") + public Object logTasklet(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + // Tasklet 이름만으로도 구분이 되므로, 클래스명 + 메서드명으로 로그를 남깁니다. + log.info(">>>> [TASKLET] Start: {}", joinPoint.getSignature().toShortString()); + Object result = joinPoint.proceed(); // 실제 Tasklet의 execute() 메서드 실행 + long duration = System.currentTimeMillis() - start; + log.info("<<<< [TASKLET] End: {} ({}ms)", joinPoint.getSignature().toShortString(), duration); + return result; + } } From 4d573ca8628535b9a0d5cd6e450a92f35d52dd14 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 10:30:42 +0900 Subject: [PATCH 10/10] refactor: Code Formating --- .../icebang/batch/common/JobContextKeys.java | 19 +- .../batch/job/BlogAutomationJobConfig.java | 106 ++++----- .../tasklet/CrawlSelectedProductTasklet.java | 58 ++--- .../tasklet/ExtractTrendKeywordTasklet.java | 49 ++-- .../tasklet/FindSimilarProductsTasklet.java | 56 ++--- .../tasklet/GenerateBlogContentTasklet.java | 62 ++--- .../MatchProductWithKeywordTasklet.java | 52 +++-- .../batch/tasklet/PublishBlogPostTasklet.java | 70 +++--- .../SearchProductsFromMallTasklet.java | 55 ++--- .../fastapi/adapter/FastApiAdapter.java | 172 +++++++------- .../external/fastapi/dto/FastApiDto.java | 212 ++++++++---------- 11 files changed, 451 insertions(+), 460 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java b/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java index 45d31851..d28b7bd0 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java +++ b/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java @@ -1,16 +1,15 @@ package site.icebang.batch.common; /** - * Spring Batch의 JobExecutionContext에서 Step 간 데이터 공유를 위해 사용되는 - * Key들을 상수로 정의하는 인터페이스. - * 모든 Tasklet은 이 인터페이스를 참조하여 데이터의 일관성을 유지합니다. + * 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"; -} \ No newline at end of file + 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"; +} diff --git a/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java b/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java index 16242b57..d0c934b9 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java +++ b/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java @@ -1,7 +1,5 @@ package site.icebang.batch.job; // 패키지 경로 수정 -import site.icebang.batch.tasklet.*; // import 경로 수정 -import lombok.RequiredArgsConstructor; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.job.builder.JobBuilder; @@ -11,10 +9,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; -/** - * [배치 시스템 구현] - * 트렌드 기반 블로그 자동화 워크플로우를 구성하는 Job들을 정의합니다. - */ +import lombok.RequiredArgsConstructor; + +import site.icebang.batch.tasklet.*; + +/** [배치 시스템 구현] 트렌드 기반 블로그 자동화 워크플로우를 구성하는 Job들을 정의합니다. */ @Configuration @RequiredArgsConstructor public class BlogAutomationJobConfig { @@ -28,88 +27,89 @@ public class BlogAutomationJobConfig { private final GenerateBlogContentTasklet generateBlogContentTask; private final PublishBlogPostTasklet publishBlogPostTask; - /** - * Job 1: 상품 선정 및 정보 수집 - * 키워드 추출부터 최종 상품 정보 크롤링까지의 과정을 책임집니다. - */ + /** Job 1: 상품 선정 및 정보 수집 키워드 추출부터 최종 상품 정보 크롤링까지의 과정을 책임집니다. */ @Bean - public Job productSelectionJob(JobRepository jobRepository, - Step extractTrendKeywordStep, - Step searchProductsFromMallStep, - Step matchProductWithKeywordStep, - Step findSimilarProductsStep, - Step crawlSelectedProductStep) { + 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(); + .start(extractTrendKeywordStep) + .next(searchProductsFromMallStep) + .next(matchProductWithKeywordStep) + .next(findSimilarProductsStep) + .next(crawlSelectedProductStep) + .build(); } - /** - * Job 2: 콘텐츠 생성 및 발행 - * 수집된 상품 정보로 블로그 콘텐츠를 생성하고 발행합니다. - */ + /** Job 2: 콘텐츠 생성 및 발행 수집된 상품 정보로 블로그 콘텐츠를 생성하고 발행합니다. */ @Bean - public Job contentPublishingJob(JobRepository jobRepository, - Step generateBlogContentStep, - Step publishBlogPostStep) { + public Job contentPublishingJob( + JobRepository jobRepository, Step generateBlogContentStep, Step publishBlogPostStep) { return new JobBuilder("contentPublishingJob", jobRepository) - .start(generateBlogContentStep) - .next(publishBlogPostStep) - .build(); + .start(generateBlogContentStep) + .next(publishBlogPostStep) + .build(); } // --- Steps for productSelectionJob --- @Bean - public Step extractTrendKeywordStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step extractTrendKeywordStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("extractTrendKeywordStep", jobRepository) - .tasklet(extractTrendKeywordTask, transactionManager) - .build(); + .tasklet(extractTrendKeywordTask, transactionManager) + .build(); } @Bean - public Step searchProductsFromMallStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step searchProductsFromMallStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("searchProductsFromMallStep", jobRepository) - .tasklet(searchProductsFromMallTask, transactionManager) - .build(); + .tasklet(searchProductsFromMallTask, transactionManager) + .build(); } @Bean - public Step matchProductWithKeywordStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step matchProductWithKeywordStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("matchProductWithKeywordStep", jobRepository) - .tasklet(matchProductWithKeywordTask, transactionManager) - .build(); + .tasklet(matchProductWithKeywordTask, transactionManager) + .build(); } @Bean - public Step findSimilarProductsStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step findSimilarProductsStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("findSimilarProductsStep", jobRepository) - .tasklet(findSimilarProductsTask, transactionManager) - .build(); + .tasklet(findSimilarProductsTask, transactionManager) + .build(); } @Bean - public Step crawlSelectedProductStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step crawlSelectedProductStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("crawlSelectedProductStep", jobRepository) - .tasklet(crawlSelectedProductTask, transactionManager) - .build(); + .tasklet(crawlSelectedProductTask, transactionManager) + .build(); } // --- Steps for contentPublishingJob --- @Bean - public Step generateBlogContentStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step generateBlogContentStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("generateBlogContentStep", jobRepository) - .tasklet(generateBlogContentTask, transactionManager) - .build(); + .tasklet(generateBlogContentTask, transactionManager) + .build(); } @Bean - public Step publishBlogPostStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + public Step publishBlogPostStep( + JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("publishBlogPostStep", jobRepository) - .tasklet(publishBlogPostTask, transactionManager) - .build(); + .tasklet(publishBlogPostTask, transactionManager) + .build(); } } diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java index 230cf55f..6a182c37 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java @@ -1,14 +1,17 @@ package site.icebang.batch.tasklet; import java.util.Map; -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.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; @@ -19,38 +22,39 @@ @RequiredArgsConstructor public class CrawlSelectedProductTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작"); + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - Map selectedProduct = (Map) jobExecutionContext.get(JobContextKeys.SELECTED_PRODUCT); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + Map selectedProduct = + (Map) jobExecutionContext.get(JobContextKeys.SELECTED_PRODUCT); - if (selectedProduct == null || !selectedProduct.containsKey("link")) { - throw new RuntimeException("크롤링할 상품 URL이 없습니다."); - } - String productUrl = (String) selectedProduct.get("link"); + 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); + 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 상품 크롤링에 실패했습니다."); - } + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 크롤링에 실패했습니다."); + } - Map productDetail = response.productDetail(); - log.info(">>>> FastAPI로부터 크롤링된 상품 상세 정보 획득"); + Map productDetail = response.productDetail(); + log.info(">>>> FastAPI로부터 크롤링된 상품 상세 정보 획득"); - jobExecutionContext.put(JobContextKeys.CRAWLED_PRODUCT_DETAIL, productDetail); + jobExecutionContext.put(JobContextKeys.CRAWLED_PRODUCT_DETAIL, productDetail); - // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java index c5d825ed..a35bebf9 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java @@ -1,13 +1,15 @@ package site.icebang.batch.tasklet; -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.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; @@ -18,31 +20,32 @@ @RequiredArgsConstructor public class ExtractTrendKeywordTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작"); + @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); + 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); + 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); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + // 다른 클래스의 상수를 직접 참조하는 대신 공용 인터페이스의 키를 사용 + jobExecutionContext.put(JobContextKeys.EXTRACTED_KEYWORD, extractedKeyword); - // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java index bf57cd67..316641e1 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java @@ -2,14 +2,17 @@ import java.util.List; import java.util.Map; -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.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.RequestSsadaguSimilarity; @@ -20,35 +23,38 @@ @RequiredArgsConstructor public class FindSimilarProductsTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 시작"); + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - List> matchedProducts = (List>) jobExecutionContext.get(JobContextKeys.MATCHED_PRODUCTS); - List> searchResults = (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); + List> matchedProducts = + (List>) jobExecutionContext.get(JobContextKeys.MATCHED_PRODUCTS); + List> searchResults = + (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); - RequestSsadaguSimilarity request = new RequestSsadaguSimilarity(1, 1, null, keyword, matchedProducts, searchResults); - ResponseSsadaguSimilarity response = fastApiAdapter.requestProductSimilarity(request); + RequestSsadaguSimilarity request = + new RequestSsadaguSimilarity(1, 1, null, keyword, matchedProducts, searchResults); + ResponseSsadaguSimilarity response = fastApiAdapter.requestProductSimilarity(request); - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 유사도 분석에 실패했습니다."); - } + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 유사도 분석에 실패했습니다."); + } - Map selectedProduct = response.selectedProduct(); - log.info(">>>> FastAPI로부터 최종 선택된 상품: {}", selectedProduct.get("title")); + Map selectedProduct = response.selectedProduct(); + log.info(">>>> FastAPI로부터 최종 선택된 상품: {}", selectedProduct.get("title")); - jobExecutionContext.put(JobContextKeys.SELECTED_PRODUCT, selectedProduct); + jobExecutionContext.put(JobContextKeys.SELECTED_PRODUCT, selectedProduct); - // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java index 8b1f9a97..ecf44cbb 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java @@ -2,14 +2,17 @@ import java.util.List; import java.util.Map; -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.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.RequestBlogCreate; @@ -20,39 +23,40 @@ @RequiredArgsConstructor public class GenerateBlogContentTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 시작"); + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - Map productDetail = (Map) jobExecutionContext.get(JobContextKeys.CRAWLED_PRODUCT_DETAIL); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + Map productDetail = + (Map) jobExecutionContext.get(JobContextKeys.CRAWLED_PRODUCT_DETAIL); - // TODO: productDetail을 기반으로 LLM에 전달할 프롬프트 생성 - RequestBlogCreate request = new RequestBlogCreate(1, 1, null); - ResponseBlogCreate response = fastApiAdapter.requestBlogCreation(request); + // TODO: productDetail을 기반으로 LLM에 전달할 프롬프트 생성 + RequestBlogCreate request = new RequestBlogCreate(1, 1, null); + ResponseBlogCreate response = fastApiAdapter.requestBlogCreation(request); - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 블로그 콘텐츠 생성에 실패했습니다."); - } + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 블로그 콘텐츠 생성에 실패했습니다."); + } - // TODO: 실제 생성된 콘텐츠를 response로부터 받아와야 함 (현재는 더미 데이터) - Map generatedContent = Map.of( - "title", "엄청난 상품을 소개합니다! " + productDetail.get("title"), - "content", "이 상품은 정말... 좋습니다. 상세 정보: " + productDetail.toString(), - "tags", List.of("상품리뷰", "최고") - ); - log.info(">>>> FastAPI로부터 블로그 콘텐츠 생성 완료"); + // TODO: 실제 생성된 콘텐츠를 response로부터 받아와야 함 (현재는 더미 데이터) + Map generatedContent = + Map.of( + "title", "엄청난 상품을 소개합니다! " + productDetail.get("title"), + "content", "이 상품은 정말... 좋습니다. 상세 정보: " + productDetail.toString(), + "tags", List.of("상품리뷰", "최고")); + log.info(">>>> FastAPI로부터 블로그 콘텐츠 생성 완료"); - jobExecutionContext.put(JobContextKeys.GENERATED_CONTENT, generatedContent); + jobExecutionContext.put(JobContextKeys.GENERATED_CONTENT, generatedContent); - // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java index 8e9f76aa..bdb15200 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java @@ -2,14 +2,17 @@ import java.util.List; import java.util.Map; -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.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.RequestSsadaguMatch; @@ -20,34 +23,35 @@ @RequiredArgsConstructor public class MatchProductWithKeywordTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 시작"); + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + // log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - List> searchResults = (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); + List> searchResults = + (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); - RequestSsadaguMatch request = new RequestSsadaguMatch(1, 1, null, keyword, searchResults); - ResponseSsadaguMatch response = fastApiAdapter.requestProductMatch(request); + RequestSsadaguMatch request = new RequestSsadaguMatch(1, 1, null, keyword, searchResults); + ResponseSsadaguMatch response = fastApiAdapter.requestProductMatch(request); - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 매칭에 실패했습니다."); - } + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 매칭에 실패했습니다."); + } - List> matchedProducts = response.matchedProducts(); - log.info(">>>> FastAPI로부터 매칭된 상품 {}개", matchedProducts.size()); + List> matchedProducts = response.matchedProducts(); + log.info(">>>> FastAPI로부터 매칭된 상품 {}개", matchedProducts.size()); - jobExecutionContext.put(JobContextKeys.MATCHED_PRODUCTS, matchedProducts); + jobExecutionContext.put(JobContextKeys.MATCHED_PRODUCTS, matchedProducts); - log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java index e9bba5a9..e1b75a18 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java @@ -2,14 +2,17 @@ import java.util.List; import java.util.Map; -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.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.RequestBlogPublish; @@ -20,43 +23,46 @@ @RequiredArgsConstructor public class PublishBlogPostTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 시작"); + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - Map content = (Map) jobExecutionContext.get(JobContextKeys.GENERATED_CONTENT); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + Map content = + (Map) jobExecutionContext.get(JobContextKeys.GENERATED_CONTENT); - // TODO: UserConfig 등에서 실제 블로그 정보(ID, PW)를 가져와야 함 - String blogId = "my_blog_id"; - String blogPw = "my_blog_password"; + // TODO: UserConfig 등에서 실제 블로그 정보(ID, PW)를 가져와야 함 + String blogId = "my_blog_id"; + String blogPw = "my_blog_password"; - RequestBlogPublish request = new RequestBlogPublish( - 1, 1, null, - "naver", - blogId, - blogPw, - (String) content.get("title"), - (String) content.get("content"), - (List) content.get("tags") - ); + RequestBlogPublish request = + new RequestBlogPublish( + 1, + 1, + null, + "naver", + blogId, + blogPw, + (String) content.get("title"), + (String) content.get("content"), + (List) content.get("tags")); - ResponseBlogPublish response = fastApiAdapter.requestBlogPost(request); + ResponseBlogPublish response = fastApiAdapter.requestBlogPost(request); - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 블로그 발행에 실패했습니다."); - } + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 블로그 발행에 실패했습니다."); + } - log.info(">>>> FastAPI를 통해 블로그 발행 성공: {}", response.metadata()); + log.info(">>>> FastAPI를 통해 블로그 발행 성공: {}", response.metadata()); - // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java index 74890374..3480f391 100644 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java +++ b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java @@ -2,14 +2,17 @@ import java.util.List; import java.util.Map; -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.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.RequestSsadaguSearch; @@ -20,36 +23,36 @@ @RequiredArgsConstructor public class SearchProductsFromMallTasklet implements Tasklet { - private final FastApiAdapter fastApiAdapter; + private final FastApiAdapter fastApiAdapter; - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 시작"); + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 시작"); - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); + ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); + String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - if (keyword == null) { - throw new RuntimeException("이전 Step에서 키워드를 전달받지 못했습니다."); - } + if (keyword == null) { + throw new RuntimeException("이전 Step에서 키워드를 전달받지 못했습니다."); + } - RequestSsadaguSearch request = new RequestSsadaguSearch(1, 1, null, keyword); - ResponseSsadaguSearch response = fastApiAdapter.requestSsadaguProductSearch(request); + RequestSsadaguSearch request = new RequestSsadaguSearch(1, 1, null, keyword); + ResponseSsadaguSearch response = fastApiAdapter.requestSsadaguProductSearch(request); - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 검색에 실패했습니다."); - } - List> searchResults = response.searchResults(); - log.info(">>>> FastAPI로부터 검색된 상품 {}개", searchResults.size()); + if (response == null || !"200".equals(response.status())) { + throw new RuntimeException("FastAPI 상품 검색에 실패했습니다."); + } + List> searchResults = response.searchResults(); + log.info(">>>> FastAPI로부터 검색된 상품 {}개", searchResults.size()); - jobExecutionContext.put(JobContextKeys.SEARCHED_PRODUCTS, searchResults); + jobExecutionContext.put(JobContextKeys.SEARCHED_PRODUCTS, searchResults); - // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } + // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 완료"); + return RepeatStatus.FINISHED; + } - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } + private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { + return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); + } } - diff --git a/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java b/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java index 26b22d8e..e4e81a73 100644 --- a/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java +++ b/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java @@ -1,122 +1,106 @@ package site.icebang.external.fastapi.adapter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import site.icebang.external.fastapi.dto.FastApiDto.*; import site.icebang.global.config.properties.FastApiProperties; -/** - * FastAPI 서버와의 통신을 전담하는 어댑터 클래스. - * 모든 외부 API 호출은 이 클래스를 통해 이루어집니다. - */ +/** FastAPI 서버와의 통신을 전담하는 어댑터 클래스. 모든 외부 API 호출은 이 클래스를 통해 이루어집니다. */ @Slf4j @Component @RequiredArgsConstructor public class FastApiAdapter { - private final RestTemplate restTemplate; - private final FastApiProperties properties; + private final RestTemplate restTemplate; + private final FastApiProperties properties; - /** - * TASK 1: 네이버 키워드 추출을 FastAPI에 요청합니다. - */ - public ResponseNaverSearch requestNaverKeywordSearch(RequestNaverSearch request) { - String url = properties.getUrl() + "/keyword/search"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseNaverSearch.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI keyword search API. Error: {}", e.getMessage()); - // TODO: 비즈니스 요구사항에 맞는 예외 처리 (재시도, 기본값 반환, 특정 예외 던지기 등) - return null; - } + /** TASK 1: 네이버 키워드 추출을 FastAPI에 요청합니다. */ + public ResponseNaverSearch requestNaverKeywordSearch(RequestNaverSearch request) { + String url = properties.getUrl() + "/keyword/search"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseNaverSearch.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI keyword search API. Error: {}", e.getMessage()); + // TODO: 비즈니스 요구사항에 맞는 예외 처리 (재시도, 기본값 반환, 특정 예외 던지기 등) + return null; } + } - /** - * TASK 2: 싸다구몰 상품 검색을 FastAPI에 요청합니다. - */ - public ResponseSsadaguSearch requestSsadaguProductSearch(RequestSsadaguSearch request) { - String url = properties.getUrl() + "/product/search"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseSsadaguSearch.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI product search API. Error: {}", e.getMessage()); - return null; - } + /** TASK 2: 싸다구몰 상품 검색을 FastAPI에 요청합니다. */ + public ResponseSsadaguSearch requestSsadaguProductSearch(RequestSsadaguSearch request) { + String url = properties.getUrl() + "/product/search"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguSearch.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product search API. Error: {}", e.getMessage()); + return null; } + } - /** - * TASK 3: 상품 매칭을 FastAPI에 요청합니다. - */ - public ResponseSsadaguMatch requestProductMatch(RequestSsadaguMatch request) { - String url = properties.getUrl() + "/product/match"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseSsadaguMatch.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI product match API. Error: {}", e.getMessage()); - return null; - } + /** TASK 3: 상품 매칭을 FastAPI에 요청합니다. */ + public ResponseSsadaguMatch requestProductMatch(RequestSsadaguMatch request) { + String url = properties.getUrl() + "/product/match"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguMatch.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product match API. Error: {}", e.getMessage()); + return null; } + } - /** - * TASK 4: 상품 유사도 분석을 FastAPI에 요청합니다. (메서드명 수정) - */ - public ResponseSsadaguSimilarity requestProductSimilarity(RequestSsadaguSimilarity request) { - String url = properties.getUrl() + "/product/similarity"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseSsadaguSimilarity.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI product similarity API. Error: {}", e.getMessage()); - return null; - } + /** TASK 4: 상품 유사도 분석을 FastAPI에 요청합니다. (메서드명 수정) */ + public ResponseSsadaguSimilarity requestProductSimilarity(RequestSsadaguSimilarity request) { + String url = properties.getUrl() + "/product/similarity"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguSimilarity.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product similarity API. Error: {}", e.getMessage()); + return null; } + } - - /** - * TASK 5: 상품 상세 정보 크롤링을 FastAPI에 요청합니다. - */ - public ResponseSsadaguCrawl requestProductCrawl(RequestSsadaguCrawl request) { - String url = properties.getUrl() + "/product/crawl"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseSsadaguCrawl.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI product crawl API. Error: {}", e.getMessage()); - return null; - } + /** TASK 5: 상품 상세 정보 크롤링을 FastAPI에 요청합니다. */ + public ResponseSsadaguCrawl requestProductCrawl(RequestSsadaguCrawl request) { + String url = properties.getUrl() + "/product/crawl"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseSsadaguCrawl.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI product crawl API. Error: {}", e.getMessage()); + return null; } + } - /** - * TASK 6: 블로그 콘텐츠 생성을 FastAPI에 요청합니다. - */ - public ResponseBlogCreate requestBlogCreation(RequestBlogCreate request) { - String url = properties.getUrl() + "/blog/rag/create"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseBlogCreate.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI blog creation API. Error: {}", e.getMessage()); - return null; - } + /** TASK 6: 블로그 콘텐츠 생성을 FastAPI에 요청합니다. */ + public ResponseBlogCreate requestBlogCreation(RequestBlogCreate request) { + String url = properties.getUrl() + "/blog/rag/create"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseBlogCreate.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI blog creation API. Error: {}", e.getMessage()); + return null; } + } - /** - * TASK 7: 블로그 발행을 FastAPI에 요청합니다. - */ - public ResponseBlogPublish requestBlogPost(RequestBlogPublish request) { - String url = properties.getUrl() + "/blog/publish"; - log.info("Requesting to FastAPI [POST {}]", url); - try { - return restTemplate.postForObject(url, request, ResponseBlogPublish.class); - } catch (RestClientException e) { - log.error("Failed to call FastAPI blog publish API. Error: {}", e.getMessage()); - return null; - } + /** TASK 7: 블로그 발행을 FastAPI에 요청합니다. */ + public ResponseBlogPublish requestBlogPost(RequestBlogPublish request) { + String url = properties.getUrl() + "/blog/publish"; + log.info("Requesting to FastAPI [POST {}]", url); + try { + return restTemplate.postForObject(url, request, ResponseBlogPublish.class); + } catch (RestClientException e) { + log.error("Failed to call FastAPI blog publish API. Error: {}", e.getMessage()); + return null; } + } } diff --git a/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java b/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java index ca0b7972..88ffe284 100644 --- a/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java +++ b/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java @@ -1,125 +1,103 @@ package site.icebang.external.fastapi.dto; -import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Map; -/** - * FastAPI 서버와 통신하기 위한 DTO 클래스 모음. - * Java의 record를 사용하여 불변 데이터 객체를 간결하게 정의합니다. - */ +import com.fasterxml.jackson.annotation.JsonProperty; + +/** FastAPI 서버와 통신하기 위한 DTO 클래스 모음. Java의 record를 사용하여 불변 데이터 객체를 간결하게 정의합니다. */ public final class FastApiDto { - // --- 1. 네이버 키워드 추출 --- - public record RequestNaverSearch( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId, - String tag, - String category, - @JsonProperty("start_date") String startDate, - @JsonProperty("end_date") String endDate - ) {} - - public record ResponseNaverSearch( - String status, - String category, - String keyword, - @JsonProperty("total_keyword") Map totalKeyword - ) {} - - // --- 2. 상품 검색 --- - public record RequestSsadaguSearch( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId, - String keyword - ) {} - - public record ResponseSsadaguSearch( - String status, - String keyword, - @JsonProperty("search_results") List> searchResults - ) {} - - // --- 3. 상품 매칭 --- - public record RequestSsadaguMatch( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId, - String keyword, - @JsonProperty("search_results") List> searchResults - ) {} - - public record ResponseSsadaguMatch( - String status, - String keyword, - @JsonProperty("matched_products") List> matchedProducts - ) {} - - - // --- 4. 상품 유사도 --- - public record RequestSsadaguSimilarity( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId, - String keyword, - @JsonProperty("matched_products") List> matchedProducts, - @JsonProperty("search_results") List> searchResults - ) {} - - public record ResponseSsadaguSimilarity( - String status, - String keyword, - @JsonProperty("selected_product") Map selectedProduct, - String reason - ) {} - - - // --- 5. 상품 크롤링 --- - public record RequestSsadaguCrawl( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId, - String tag, - @JsonProperty("product_url") String productUrl - ) {} - - public record ResponseSsadaguCrawl( - String status, - String tag, - @JsonProperty("product_url") String productUrl, - @JsonProperty("product_detail") Map productDetail, - @JsonProperty("crawled_at") String crawledAt - ) {} - - - // --- 6. 블로그 콘텐츠 생성 --- - public record RequestBlogCreate( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId - ) {} - - public record ResponseBlogCreate( - String status - ) {} - - // --- 7. 블로그 발행 --- - public record RequestBlogPublish( - @JsonProperty("job_id") int jobId, - @JsonProperty("schedule_id") int scheduleId, - @JsonProperty("schedule_his_id") Integer scheduleHisId, - String tag, - @JsonProperty("blog_id") String blogId, - @JsonProperty("blog_pw") String blogPw, - @JsonProperty("post_title") String postTitle, - @JsonProperty("post_content") String postContent, - @JsonProperty("post_tags") List postTags - ) {} - - public record ResponseBlogPublish( - String status, - Map metadata - ) {} + // --- 1. 네이버 키워드 추출 --- + public record RequestNaverSearch( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String tag, + String category, + @JsonProperty("start_date") String startDate, + @JsonProperty("end_date") String endDate) {} + + public record ResponseNaverSearch( + String status, + String category, + String keyword, + @JsonProperty("total_keyword") Map totalKeyword) {} + + // --- 2. 상품 검색 --- + public record RequestSsadaguSearch( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String keyword) {} + + public record ResponseSsadaguSearch( + String status, + String keyword, + @JsonProperty("search_results") List> searchResults) {} + + // --- 3. 상품 매칭 --- + public record RequestSsadaguMatch( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String keyword, + @JsonProperty("search_results") List> searchResults) {} + + public record ResponseSsadaguMatch( + String status, + String keyword, + @JsonProperty("matched_products") List> matchedProducts) {} + + // --- 4. 상품 유사도 --- + public record RequestSsadaguSimilarity( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String keyword, + @JsonProperty("matched_products") List> matchedProducts, + @JsonProperty("search_results") List> searchResults) {} + + public record ResponseSsadaguSimilarity( + String status, + String keyword, + @JsonProperty("selected_product") Map selectedProduct, + String reason) {} + + // --- 5. 상품 크롤링 --- + public record RequestSsadaguCrawl( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String tag, + @JsonProperty("product_url") String productUrl) {} + + public record ResponseSsadaguCrawl( + String status, + String tag, + @JsonProperty("product_url") String productUrl, + @JsonProperty("product_detail") Map productDetail, + @JsonProperty("crawled_at") String crawledAt) {} + + // --- 6. 블로그 콘텐츠 생성 --- + public record RequestBlogCreate( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId) {} + + public record ResponseBlogCreate(String status) {} + + // --- 7. 블로그 발행 --- + public record RequestBlogPublish( + @JsonProperty("job_id") int jobId, + @JsonProperty("schedule_id") int scheduleId, + @JsonProperty("schedule_his_id") Integer scheduleHisId, + String tag, + @JsonProperty("blog_id") String blogId, + @JsonProperty("blog_pw") String blogPw, + @JsonProperty("post_title") String postTitle, + @JsonProperty("post_content") String postContent, + @JsonProperty("post_tags") List postTags) {} + + public record ResponseBlogPublish(String status, Map metadata) {} }