diff --git a/src/main/java/com/techfork/domain/admin/controller/AdminController.java b/src/main/java/com/techfork/domain/admin/controller/AdminController.java index 1da1e0a..e46f37b 100644 --- a/src/main/java/com/techfork/domain/admin/controller/AdminController.java +++ b/src/main/java/com/techfork/domain/admin/controller/AdminController.java @@ -2,6 +2,7 @@ import com.techfork.domain.auth.dto.DeveloperTokenResponse; import com.techfork.domain.auth.service.AuthService; +import com.techfork.domain.source.service.CrawlingService; import com.techfork.global.common.code.SuccessCode; import com.techfork.global.response.BaseResponse; import com.techfork.global.security.oauth.UserPrincipal; @@ -9,12 +10,20 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.*; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRestartException; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; + @Tag(name = "Admin", description = "관리자 API") @Slf4j @RestController @@ -23,6 +32,9 @@ public class AdminController { private final AuthService authService; + private final JobLauncher jobLauncher; + private final Job summaryAndEmbeddingJob; + private final CrawlingService crawlingService; @Operation( summary = "개발자 토큰 발급 (ADMIN 전용)", @@ -35,4 +47,33 @@ public ResponseEntity> generateDeveloperTok DeveloperTokenResponse response = authService.generateDeveloperToken(userPrincipal.getId()); return BaseResponse.of(SuccessCode.OK, response); } + + @Operation( + summary = "요약 추출 + 임베딩 생성 배치 실행 (ADMIN 전용)", + description = "요약이 없는 게시글의 요약을 추출하고, 임베딩을 생성하여 Elasticsearch에 색인합니다. (크롤링 제외)" + ) + @PostMapping("/batch/summary-and-embedding") + public ResponseEntity> runSummaryAndEmbeddingBatch() { + try { + JobParameters jobParameters = new JobParametersBuilder() + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + jobLauncher.run(summaryAndEmbeddingJob, jobParameters); + + return BaseResponse.of(SuccessCode.OK); + + } catch (JobExecutionAlreadyRunningException | JobRestartException | + JobInstanceAlreadyCompleteException | JobParametersInvalidException e) { + log.error("배치 실행 실패", e); + throw new RuntimeException("배치 실행 중 오류 발생: " + e.getMessage(), e); + } + } + + @Operation(summary = "RSS 크롤링 실행", description = "모든 테크 블로그의 RSS를 크롤링하여 DB에 저장합니다.") + @PostMapping("/batch/crawl-rss") + public ResponseEntity> crawlRss() { + crawlingService.executeCrawling(); + return BaseResponse.of(SuccessCode.OK, "RSS 크롤링이 성공적으로 시작되었습니다."); + } } \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java b/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java index 04661c7..9d86fa0 100644 --- a/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java +++ b/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java @@ -77,6 +77,14 @@ public Job rssCrawlingJob() { .build(); } + @Bean + public Job summaryAndEmbeddingJob() { + return new JobBuilder("summaryAndEmbeddingJob", jobRepository) + .start(extractSummaryStep()) + .next(embedAndIndexStep()) + .build(); + } + @Bean public Step fetchAndSaveRssStep() { return new StepBuilder("fetchAndSaveRssStep", jobRepository) diff --git a/src/main/java/com/techfork/domain/source/controller/BatchController.java b/src/main/java/com/techfork/domain/source/controller/BatchController.java deleted file mode 100644 index 53999fb..0000000 --- a/src/main/java/com/techfork/domain/source/controller/BatchController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.techfork.domain.source.controller; - -import com.techfork.domain.source.service.CrawlingService; -import com.techfork.global.common.code.SuccessCode; -import com.techfork.global.response.BaseResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Batch", description = "배치 작업 API") -@RestController -@RequestMapping("/api/v1/batch") -@RequiredArgsConstructor -public class BatchController { - - private final CrawlingService crawlingService; - - @Operation(summary = "RSS 크롤링 실행", description = "모든 테크 블로그의 RSS를 크롤링하여 DB에 저장합니다.") - @PostMapping("/crawl-rss") - public ResponseEntity> crawlRss() { - crawlingService.executeCrawling(); - return BaseResponse.of(SuccessCode.OK, "RSS 크롤링이 성공적으로 시작되었습니다."); - } -} diff --git a/src/main/java/com/techfork/global/constant/Constants.java b/src/main/java/com/techfork/global/constant/Constants.java index 67d5b85..5691921 100644 --- a/src/main/java/com/techfork/global/constant/Constants.java +++ b/src/main/java/com/techfork/global/constant/Constants.java @@ -26,7 +26,6 @@ private Constants() {} }; public static final String[] ADMIN_ENDPOINTS = { - "/api/v1/batch/**", "/api/v1/admin/**" }; diff --git a/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java b/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java index 6217bfd..4af1d87 100644 --- a/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java +++ b/src/test/java/com/techfork/global/security/SecurityIntegrationTest.java @@ -203,7 +203,7 @@ void success_ValidTokenAccess() throws Exception { @DisplayName("403 - 일반 사용자가 관리자 전용 엔드포인트 접근") void forbidden_UserAccessAdminEndpoint() throws Exception { // When & Then - 일반 사용자 토큰으로 관리자 엔드포인트 접근 - mockMvc.perform(get("/api/v1/batch/run") + mockMvc.perform(get("/api/v1/admin/developer-token") .header("Authorization", "Bearer " + validAccessToken)) .andDo(print()) .andExpect(status().isForbidden()) @@ -231,8 +231,7 @@ void forbidden_WithdrawnUserAccessAPI() throws Exception { @DisplayName("200 - 관리자가 관리자 전용 엔드포인트 접근 (정상)") void success_AdminAccessAdminEndpoint() throws Exception { // When & Then - 관리자 토큰으로 접근 시 정상 처리 - // 실제 배치 실행은 안 되고 404나 다른 에러가 날 수 있지만, 403이 아니어야 함 - mockMvc.perform(get("/api/v1/batch/run") + mockMvc.perform(get("/api/v1/admin/batch/crawl-rss") .header("Authorization", "Bearer " + adminAccessToken)) .andDo(print()) .andExpect(result -> {