snp-batch-validation/src/main/java/com/snp/batch/global/controller/BatchController.java

457 lines
22 KiB
Java
Raw Normal View 히스토리

2025-10-22 13:50:04 +09:00
package com.snp.batch.global.controller;
import com.snp.batch.global.dto.*;
2025-10-22 13:50:04 +09:00
import com.snp.batch.service.BatchService;
import com.snp.batch.service.ScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
2025-12-29 15:35:18 +09:00
import io.swagger.v3.oas.annotations.enums.Explode;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
2025-10-22 13:50:04 +09:00
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
2025-12-29 15:35:18 +09:00
import org.springdoc.core.annotations.ParameterObject;
2025-10-22 13:50:04 +09:00
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
2025-12-29 15:35:18 +09:00
import java.util.HashMap;
2025-10-22 13:50:04 +09:00
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/batch")
@RequiredArgsConstructor
@Tag(name = "Batch Management API", description = "배치 작업 실행 및 스케줄 관리 API")
public class BatchController {
private final BatchService batchService;
private final ScheduleService scheduleService;
@Operation(summary = "배치 작업 실행", description = "지정된 배치 작업을 즉시 실행합니다. 쿼리 파라미터로 Job Parameters 전달 가능")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "작업 실행 성공"),
@ApiResponse(responseCode = "500", description = "작업 실행 실패")
})
@PostMapping("/jobs/{jobName}/execute")
public ResponseEntity<Map<String, Object>> executeJob(
@Parameter(description = "실행할 배치 작업 이름", required = true, example = "sampleProductImportJob")
@PathVariable String jobName,
@Parameter(description = "Job Parameters (동적 파라미터)", required = false, example = "?param1=value1&param2=value2")
@RequestParam(required = false) Map<String, String> params) {
log.info("Received request to execute job: {} with params: {}", jobName, params);
2025-12-29 15:35:18 +09:00
try {
Long executionId = batchService.executeJob(jobName, params);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Job started successfully",
"executionId", executionId
));
} catch (Exception e) {
log.error("Error executing job: {}", jobName, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to start job: " + e.getMessage()
));
}
}
@Operation(summary = "배치 작업 실행", description = "지정된 배치 작업을 즉시 실행합니다. 쿼리 파라미터로 Job Parameters 전달 가능")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "작업 실행 성공"),
@ApiResponse(responseCode = "500", description = "작업 실행 실패")
})
@PostMapping("/jobs/{jobName}/executeJobTest")
public ResponseEntity<Map<String, Object>> executeJobTest(
@Parameter( description = "실행할 배치 작업 이름", required = true,example = "sampleProductImportJob")
@PathVariable String jobName,
@ParameterObject JobLaunchRequest request
) {
Map<String, String> params = new HashMap<>();
if (request.getStartDate() != null) params.put("startDate", request.getStartDate());
if (request.getStopDate() != null) params.put("stopDate", request.getStopDate());
log.info("Executing job: {} with params: {}", jobName, params);
2025-10-22 13:50:04 +09:00
try {
Long executionId = batchService.executeJob(jobName, params);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Job started successfully",
"executionId", executionId
));
} catch (Exception e) {
log.error("Error executing job: {}", jobName, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to start job: " + e.getMessage()
));
}
}
@Operation(summary = "배치 작업 목록 조회", description = "등록된 모든 배치 작업 목록을 조회합니다")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공")
})
@GetMapping("/jobs")
public ResponseEntity<List<String>> listJobs() {
log.debug("Received request to list all jobs");
2025-10-22 13:50:04 +09:00
List<String> jobs = batchService.listAllJobs();
return ResponseEntity.ok(jobs);
}
@Operation(summary = "배치 작업 실행 이력 조회", description = "특정 배치 작업의 실행 이력을 조회합니다")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공")
})
@GetMapping("/jobs/{jobName}/executions")
public ResponseEntity<List<JobExecutionDto>> getJobExecutions(
@Parameter(description = "배치 작업 이름", required = true, example = "sampleProductImportJob")
@PathVariable String jobName) {
log.info("Received request to get executions for job: {}", jobName);
List<JobExecutionDto> executions = batchService.getJobExecutions(jobName);
return ResponseEntity.ok(executions);
}
@Operation(summary = "최근 전체 실행 이력 조회", description = "Job 구분 없이 최근 실행 이력을 조회합니다")
@GetMapping("/executions/recent")
public ResponseEntity<List<JobExecutionDto>> getRecentExecutions(
@Parameter(description = "조회 건수", example = "50")
@RequestParam(defaultValue = "50") int limit) {
log.debug("Received request to get recent executions: limit={}", limit);
List<JobExecutionDto> executions = batchService.getRecentExecutions(limit);
return ResponseEntity.ok(executions);
}
2025-10-22 13:50:04 +09:00
@GetMapping("/executions/{executionId}")
public ResponseEntity<JobExecutionDto> getExecutionDetails(@PathVariable Long executionId) {
log.info("Received request to get execution details for: {}", executionId);
try {
JobExecutionDto execution = batchService.getExecutionDetails(executionId);
return ResponseEntity.ok(execution);
} catch (Exception e) {
log.error("Error getting execution details: {}", executionId, e);
return ResponseEntity.notFound().build();
}
}
@GetMapping("/executions/{executionId}/detail")
public ResponseEntity<com.snp.batch.global.dto.JobExecutionDetailDto> getExecutionDetailWithSteps(@PathVariable Long executionId) {
log.info("Received request to get detailed execution for: {}", executionId);
try {
com.snp.batch.global.dto.JobExecutionDetailDto detail = batchService.getExecutionDetailWithSteps(executionId);
return ResponseEntity.ok(detail);
} catch (Exception e) {
log.error("Error getting detailed execution: {}", executionId, e);
return ResponseEntity.notFound().build();
}
}
@PostMapping("/executions/{executionId}/stop")
public ResponseEntity<Map<String, Object>> stopExecution(@PathVariable Long executionId) {
log.info("Received request to stop execution: {}", executionId);
try {
batchService.stopExecution(executionId);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Execution stop requested"
));
} catch (Exception e) {
log.error("Error stopping execution: {}", executionId, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to stop execution: " + e.getMessage()
));
}
}
@Operation(summary = "스케줄 목록 조회", description = "등록된 모든 스케줄을 조회합니다")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공")
})
@GetMapping("/schedules")
public ResponseEntity<Map<String, Object>> getSchedules() {
log.info("Received request to get all schedules");
List<ScheduleResponse> schedules = scheduleService.getAllSchedules();
return ResponseEntity.ok(Map.of(
"schedules", schedules,
"count", schedules.size()
));
}
@GetMapping("/schedules/{jobName}")
public ResponseEntity<ScheduleResponse> getSchedule(@PathVariable String jobName) {
log.debug("Received request to get schedule for job: {}", jobName);
try {
ScheduleResponse schedule = scheduleService.getScheduleByJobName(jobName);
return ResponseEntity.ok(schedule);
} catch (IllegalArgumentException e) {
// 스케줄이 없는 경우 - 정상적인 시나리오 (UI에서 존재 여부 확인용)
log.debug("Schedule not found for job: {} (정상 - 존재 확인)", jobName);
return ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Error getting schedule for job: {}", jobName, e);
return ResponseEntity.notFound().build();
}
}
@Operation(summary = "스케줄 생성", description = "새로운 배치 작업 스케줄을 등록합니다")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "생성 성공"),
@ApiResponse(responseCode = "500", description = "생성 실패")
})
@PostMapping("/schedules")
public ResponseEntity<Map<String, Object>> createSchedule(
@Parameter(description = "스케줄 생성 요청 데이터", required = true)
@RequestBody ScheduleRequest request) {
log.info("Received request to create schedule for job: {}", request.getJobName());
try {
ScheduleResponse schedule = scheduleService.createSchedule(request);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Schedule created successfully",
"data", schedule
));
} catch (Exception e) {
log.error("Error creating schedule for job: {}", request.getJobName(), e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to create schedule: " + e.getMessage()
));
}
}
@PostMapping("/schedules/{jobName}/update")
2025-10-22 13:50:04 +09:00
public ResponseEntity<Map<String, Object>> updateSchedule(
@PathVariable String jobName,
@RequestBody Map<String, String> request) {
log.info("Received request to update schedule for job: {}", jobName);
try {
String cronExpression = request.get("cronExpression");
String description = request.get("description");
ScheduleResponse schedule = scheduleService.updateSchedule(jobName, cronExpression, description);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Schedule updated successfully",
"data", schedule
));
} catch (Exception e) {
log.error("Error updating schedule for job: {}", jobName, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to update schedule: " + e.getMessage()
));
}
}
@Operation(summary = "스케줄 삭제", description = "배치 작업 스케줄을 삭제합니다")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "삭제 성공"),
@ApiResponse(responseCode = "500", description = "삭제 실패")
})
@PostMapping("/schedules/{jobName}/delete")
2025-10-22 13:50:04 +09:00
public ResponseEntity<Map<String, Object>> deleteSchedule(
@Parameter(description = "배치 작업 이름", required = true)
@PathVariable String jobName) {
log.info("Received request to delete schedule for job: {}", jobName);
try {
scheduleService.deleteSchedule(jobName);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Schedule deleted successfully"
));
} catch (Exception e) {
log.error("Error deleting schedule for job: {}", jobName, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to delete schedule: " + e.getMessage()
));
}
}
@PostMapping("/schedules/{jobName}/toggle")
2025-10-22 13:50:04 +09:00
public ResponseEntity<Map<String, Object>> toggleSchedule(
@PathVariable String jobName,
@RequestBody Map<String, Boolean> request) {
log.info("Received request to toggle schedule for job: {}", jobName);
try {
Boolean active = request.get("active");
ScheduleResponse schedule = scheduleService.toggleScheduleActive(jobName, active);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Schedule toggled successfully",
"data", schedule
));
} catch (Exception e) {
log.error("Error toggling schedule for job: {}", jobName, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to toggle schedule: " + e.getMessage()
));
}
}
@GetMapping("/timeline")
public ResponseEntity<com.snp.batch.global.dto.TimelineResponse> getTimeline(
@RequestParam String view,
@RequestParam String date) {
log.debug("Received request to get timeline: view={}, date={}", view, date);
2025-10-22 13:50:04 +09:00
try {
com.snp.batch.global.dto.TimelineResponse timeline = batchService.getTimeline(view, date);
return ResponseEntity.ok(timeline);
} catch (Exception e) {
log.error("Error getting timeline", e);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/dashboard")
public ResponseEntity<com.snp.batch.global.dto.DashboardResponse> getDashboard() {
log.debug("Received request to get dashboard data");
2025-10-22 13:50:04 +09:00
try {
com.snp.batch.global.dto.DashboardResponse dashboard = batchService.getDashboardData();
return ResponseEntity.ok(dashboard);
} catch (Exception e) {
log.error("Error getting dashboard data", e);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/timeline/period-executions")
public ResponseEntity<List<JobExecutionDto>> getPeriodExecutions(
@RequestParam String jobName,
@RequestParam String view,
@RequestParam String periodKey) {
log.info("Received request to get period executions: jobName={}, view={}, periodKey={}", jobName, view, periodKey);
try {
List<JobExecutionDto> executions = batchService.getPeriodExecutions(jobName, view, periodKey);
return ResponseEntity.ok(executions);
} catch (Exception e) {
log.error("Error getting period executions", e);
return ResponseEntity.internalServerError().build();
}
}
// ── F1: 강제 종료(Abandon) API ─────────────────────────────
@Operation(summary = "오래된 실행 중 목록 조회", description = "지정된 시간(분) 이상 STARTED/STARTING 상태인 실행 목록을 조회합니다")
@GetMapping("/executions/stale")
public ResponseEntity<List<JobExecutionDto>> getStaleExecutions(
@Parameter(description = "임계 시간(분)", example = "60")
@RequestParam(defaultValue = "60") int thresholdMinutes) {
log.info("Received request to get stale executions: thresholdMinutes={}", thresholdMinutes);
List<JobExecutionDto> executions = batchService.getStaleExecutions(thresholdMinutes);
return ResponseEntity.ok(executions);
}
@Operation(summary = "실행 강제 종료", description = "특정 실행을 ABANDONED 상태로 강제 변경합니다")
@PostMapping("/executions/{executionId}/abandon")
public ResponseEntity<Map<String, Object>> abandonExecution(@PathVariable Long executionId) {
log.info("Received request to abandon execution: {}", executionId);
try {
batchService.abandonExecution(executionId);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Execution abandoned successfully"
));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
} catch (Exception e) {
log.error("Error abandoning execution: {}", executionId, e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to abandon execution: " + e.getMessage()
));
}
}
@Operation(summary = "오래된 실행 전체 강제 종료", description = "지정된 시간(분) 이상 실행 중인 모든 Job을 ABANDONED로 변경합니다")
@PostMapping("/executions/stale/abandon-all")
public ResponseEntity<Map<String, Object>> abandonAllStaleExecutions(
@Parameter(description = "임계 시간(분)", example = "60")
@RequestParam(defaultValue = "60") int thresholdMinutes) {
log.info("Received request to abandon all stale executions: thresholdMinutes={}", thresholdMinutes);
try {
int count = batchService.abandonAllStaleExecutions(thresholdMinutes);
return ResponseEntity.ok(Map.of(
"success", true,
"message", count + "건의 실행이 강제 종료되었습니다",
"abandonedCount", count
));
} catch (Exception e) {
log.error("Error abandoning all stale executions", e);
return ResponseEntity.internalServerError().body(Map.of(
"success", false,
"message", "Failed to abandon stale executions: " + e.getMessage()
));
}
}
// ── F4: 실행 이력 검색 API ─────────────────────────────────
@Operation(summary = "실행 이력 검색", description = "조건별 실행 이력 검색 (페이지네이션 지원)")
@GetMapping("/executions/search")
public ResponseEntity<ExecutionSearchResponse> searchExecutions(
@Parameter(description = "Job 이름 (콤마 구분, 복수 가능)") @RequestParam(required = false) String jobNames,
@Parameter(description = "상태 (필터)", example = "COMPLETED") @RequestParam(required = false) String status,
@Parameter(description = "시작일 (yyyy-MM-dd'T'HH:mm:ss)") @RequestParam(required = false) String startDate,
@Parameter(description = "종료일 (yyyy-MM-dd'T'HH:mm:ss)") @RequestParam(required = false) String endDate,
@Parameter(description = "페이지 번호 (0부터)") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "50") int size) {
log.debug("Search executions: jobNames={}, status={}, startDate={}, endDate={}, page={}, size={}",
jobNames, status, startDate, endDate, page, size);
List<String> jobNameList = (jobNames != null && !jobNames.isBlank())
? java.util.Arrays.stream(jobNames.split(","))
.map(String::trim).filter(s -> !s.isEmpty()).toList()
: null;
LocalDateTime start = startDate != null ? LocalDateTime.parse(startDate) : null;
LocalDateTime end = endDate != null ? LocalDateTime.parse(endDate) : null;
ExecutionSearchResponse response = batchService.searchExecutions(jobNameList, status, start, end, page, size);
return ResponseEntity.ok(response);
}
// ── F7: Job 상세 목록 API ──────────────────────────────────
@Operation(summary = "Job 상세 목록 조회", description = "모든 Job의 최근 실행 상태 및 스케줄 정보를 조회합니다")
@GetMapping("/jobs/detail")
public ResponseEntity<List<JobDetailDto>> getJobsDetail() {
log.debug("Received request to get jobs with detail");
List<JobDetailDto> jobs = batchService.getJobsWithDetail();
return ResponseEntity.ok(jobs);
}
// ── F8: 실행 통계 API ──────────────────────────────────────
@Operation(summary = "전체 실행 통계", description = "전체 배치 작업의 일별 실행 통계를 조회합니다")
@GetMapping("/statistics")
public ResponseEntity<ExecutionStatisticsDto> getStatistics(
@Parameter(description = "조회 기간(일)", example = "30")
@RequestParam(defaultValue = "30") int days) {
log.debug("Received request to get statistics: days={}", days);
ExecutionStatisticsDto stats = batchService.getStatistics(days);
return ResponseEntity.ok(stats);
}
@Operation(summary = "Job별 실행 통계", description = "특정 배치 작업의 일별 실행 통계를 조회합니다")
@GetMapping("/statistics/{jobName}")
public ResponseEntity<ExecutionStatisticsDto> getJobStatistics(
@Parameter(description = "Job 이름", required = true) @PathVariable String jobName,
@Parameter(description = "조회 기간(일)", example = "30")
@RequestParam(defaultValue = "30") int days) {
log.debug("Received request to get statistics for job: {}, days={}", jobName, days);
ExecutionStatisticsDto stats = batchService.getJobStatistics(jobName, days);
return ResponseEntity.ok(stats);
}
2025-10-22 13:50:04 +09:00
}