From 4c69e3f407f231a259e3a5b35d21e717e410b48b Mon Sep 17 00:00:00 2001 From: dmori Date: Sun, 1 Feb 2026 20:23:32 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=9A=94=EC=95=BD=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8B=B1=20=EC=8A=A4=ED=85=9D=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminController.java | 32 +++++++++++++++++++ .../source/config/RssCrawlingJobConfig.java | 8 +++++ 2 files changed, 40 insertions(+) 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..8b3ddf9 100644 --- a/src/main/java/com/techfork/domain/admin/controller/AdminController.java +++ b/src/main/java/com/techfork/domain/admin/controller/AdminController.java @@ -9,12 +9,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 +31,8 @@ public class AdminController { private final AuthService authService; + private final JobLauncher jobLauncher; + private final Job summaryAndEmbeddingJob; @Operation( summary = "개발자 토큰 발급 (ADMIN 전용)", @@ -35,4 +45,26 @@ 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); + } + } } \ 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) From 3e7c761cf74739e2846dcdd70dc4626ec8cc4c82 Mon Sep 17 00:00:00 2001 From: dmori Date: Sun, 1 Feb 2026 21:06:43 +0900 Subject: [PATCH 2/4] =?UTF-8?q?improve:=20ADMIN=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EC=9D=98=20API=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminController.java | 9 ++++++ .../source/controller/BatchController.java | 28 ------------------- .../techfork/global/constant/Constants.java | 1 - 3 files changed, 9 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/techfork/domain/source/controller/BatchController.java 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 8b3ddf9..850e55d 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; @@ -33,6 +34,7 @@ public class AdminController { private final AuthService authService; private final JobLauncher jobLauncher; private final Job summaryAndEmbeddingJob; + private final CrawlingService crawlingService; @Operation( summary = "개발자 토큰 발급 (ADMIN 전용)", @@ -67,4 +69,11 @@ public ResponseEntity> runSummaryAndEmbeddingBatch() { throw new RuntimeException("배치 실행 중 오류 발생: " + e.getMessage(), e); } } + + @Operation(summary = "RSS 크롤링 실행", description = "모든 테크 블로그의 RSS를 크롤링하여 DB에 저장합니다.") + @PostMapping("/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/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/**" }; From d399a8469d39c81d9ab79d9287b7ee22c34bbdb2 Mon Sep 17 00:00:00 2001 From: dmori Date: Sun, 1 Feb 2026 21:17:53 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20RSS=20=ED=81=AC=EB=A1=A4=EB=A7=81?= =?UTF-8?q?=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=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 --- .../com/techfork/domain/admin/controller/AdminController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 850e55d..e46f37b 100644 --- a/src/main/java/com/techfork/domain/admin/controller/AdminController.java +++ b/src/main/java/com/techfork/domain/admin/controller/AdminController.java @@ -71,7 +71,7 @@ public ResponseEntity> runSummaryAndEmbeddingBatch() { } @Operation(summary = "RSS 크롤링 실행", description = "모든 테크 블로그의 RSS를 크롤링하여 DB에 저장합니다.") - @PostMapping("/crawl-rss") + @PostMapping("/batch/crawl-rss") public ResponseEntity> crawlRss() { crawlingService.executeCrawling(); return BaseResponse.of(SuccessCode.OK, "RSS 크롤링이 성공적으로 시작되었습니다."); From 4d22ac3c0daaf5ffeec827bdb9fe344195700831 Mon Sep 17 00:00:00 2001 From: dmori Date: Sun, 1 Feb 2026 21:18:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=EB=B3=B4=EC=95=88=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=EB=A1=9C=20=EC=9A=94=EC=B2=AD=20=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EB=8D=98=20=EA=B1=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techfork/global/security/SecurityIntegrationTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 -> {