- job_display_name 테이블 신규 생성 (jobName, displayName, apiKey) - 정적 Map 제거 → DB 캐시 기반 표시명 조회로 전환 - 초기 데이터 시드 20건 (테이블 비어있을 때 자동 삽입) - 표시명 조회/수정 REST API 추가 (GET/PUT /api/batch/display-names) - 재수집 이력 생성 시 displayName 우선 적용 - 전체 화면 displayName 통합 (Dashboard, Executions, Recollects, RecollectDetail, Schedules, Timeline)
792 lines
41 KiB
Java
792 lines
41 KiB
Java
package com.snp.batch.global.controller;
|
|
|
|
import com.snp.batch.global.dto.*;
|
|
import com.snp.batch.global.model.BatchCollectionPeriod;
|
|
import com.snp.batch.global.model.BatchRecollectionHistory;
|
|
import com.snp.batch.global.model.JobDisplayNameEntity;
|
|
import com.snp.batch.global.repository.JobDisplayNameRepository;
|
|
import com.snp.batch.service.BatchFailedRecordService;
|
|
import com.snp.batch.service.BatchService;
|
|
import com.snp.batch.service.RecollectionHistoryService;
|
|
import com.snp.batch.service.ScheduleService;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.Parameter;
|
|
import io.swagger.v3.oas.annotations.enums.Explode;
|
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
|
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
|
|
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;
|
|
import org.springdoc.core.annotations.ParameterObject;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import org.springframework.data.domain.Page;
|
|
import org.springframework.data.domain.PageRequest;
|
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
import java.io.IOException;
|
|
import java.io.PrintWriter;
|
|
import java.time.LocalDateTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
|
|
@Slf4j
|
|
@RestController
|
|
@RequestMapping("/api/batch")
|
|
@RequiredArgsConstructor
|
|
@Tag(name = "Batch Management API", description = "배치 작업 실행 및 스케줄 관리 API")
|
|
public class BatchController {
|
|
|
|
private final BatchService batchService;
|
|
private final ScheduleService scheduleService;
|
|
private final RecollectionHistoryService recollectionHistoryService;
|
|
private final BatchFailedRecordService batchFailedRecordService;
|
|
private final JobDisplayNameRepository jobDisplayNameRepository;
|
|
|
|
@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¶m2=value2")
|
|
@RequestParam(required = false) Map<String, String> params) {
|
|
log.info("Received request to execute job: {} with params: {}", jobName, params);
|
|
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);
|
|
|
|
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");
|
|
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);
|
|
}
|
|
|
|
@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")
|
|
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")
|
|
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")
|
|
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);
|
|
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");
|
|
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();
|
|
}
|
|
}
|
|
|
|
// ── Step API 로그 페이징 조회 ─────────────────────────────
|
|
|
|
@Operation(summary = "Step API 호출 로그 페이징 조회", description = "Step 실행의 개별 API 호출 로그를 페이징 + 상태 필터로 조회합니다")
|
|
@GetMapping("/steps/{stepExecutionId}/api-logs")
|
|
public ResponseEntity<JobExecutionDetailDto.ApiLogPageResponse> getStepApiLogs(
|
|
@Parameter(description = "Step 실행 ID", required = true) @PathVariable Long stepExecutionId,
|
|
@Parameter(description = "페이지 번호 (0부터)") @RequestParam(defaultValue = "0") int page,
|
|
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "50") int size,
|
|
@Parameter(description = "상태 필터 (ALL, SUCCESS, ERROR)") @RequestParam(defaultValue = "ALL") String status) {
|
|
log.debug("Get step API logs: stepExecutionId={}, page={}, size={}, status={}", stepExecutionId, page, size, status);
|
|
JobExecutionDetailDto.ApiLogPageResponse response = batchService.getStepApiLogs(
|
|
stepExecutionId, status, PageRequest.of(page, size));
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
// ── 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);
|
|
}
|
|
|
|
// ── 재수집 이력 관리 API ─────────────────────────────────────
|
|
|
|
@Operation(summary = "재수집 이력 목록 조회", description = "필터 조건으로 재수집 이력을 페이징 조회합니다")
|
|
@GetMapping("/recollection-histories")
|
|
public ResponseEntity<Map<String, Object>> getRecollectionHistories(
|
|
@Parameter(description = "API Key") @RequestParam(required = false) String apiKey,
|
|
@Parameter(description = "Job 이름") @RequestParam(required = false) String jobName,
|
|
@Parameter(description = "실행 상태") @RequestParam(required = false) String status,
|
|
@Parameter(description = "시작일 (yyyy-MM-dd'T'HH:mm:ss)") @RequestParam(required = false) String fromDate,
|
|
@Parameter(description = "종료일 (yyyy-MM-dd'T'HH:mm:ss)") @RequestParam(required = false) String toDate,
|
|
@Parameter(description = "페이지 번호 (0부터)") @RequestParam(defaultValue = "0") int page,
|
|
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size) {
|
|
log.debug("Search recollection histories: apiKey={}, jobName={}, status={}, page={}, size={}",
|
|
apiKey, jobName, status, page, size);
|
|
|
|
LocalDateTime from = fromDate != null ? LocalDateTime.parse(fromDate) : null;
|
|
LocalDateTime to = toDate != null ? LocalDateTime.parse(toDate) : null;
|
|
|
|
Page<BatchRecollectionHistory> histories = recollectionHistoryService
|
|
.getHistories(apiKey, jobName, status, from, to, PageRequest.of(page, size));
|
|
|
|
// 목록의 jobExecutionId들로 실패건수 한번에 조회
|
|
List<Long> jobExecutionIds = histories.getContent().stream()
|
|
.map(BatchRecollectionHistory::getJobExecutionId)
|
|
.filter(Objects::nonNull)
|
|
.toList();
|
|
Map<Long, Long> failedRecordCounts = recollectionHistoryService
|
|
.getFailedRecordCounts(jobExecutionIds);
|
|
|
|
Map<String, Object> response = new HashMap<>();
|
|
response.put("content", histories.getContent());
|
|
response.put("totalElements", histories.getTotalElements());
|
|
response.put("totalPages", histories.getTotalPages());
|
|
response.put("number", histories.getNumber());
|
|
response.put("size", histories.getSize());
|
|
response.put("failedRecordCounts", failedRecordCounts);
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
@Operation(summary = "재수집 이력 상세 조회", description = "재수집 이력의 상세 정보 (Step Execution + Collection Period + 중복 이력 + API 통계 포함)")
|
|
@GetMapping("/recollection-histories/{historyId}")
|
|
public ResponseEntity<Map<String, Object>> getRecollectionHistoryDetail(
|
|
@Parameter(description = "이력 ID") @PathVariable Long historyId) {
|
|
log.debug("Get recollection history detail: historyId={}", historyId);
|
|
try {
|
|
Map<String, Object> detail = recollectionHistoryService.getHistoryDetailWithSteps(historyId);
|
|
return ResponseEntity.ok(detail);
|
|
} catch (IllegalArgumentException e) {
|
|
return ResponseEntity.notFound().build();
|
|
}
|
|
}
|
|
|
|
@Operation(summary = "재수집 통계 조회", description = "재수집 실행 통계 및 최근 10건 조회")
|
|
@GetMapping("/recollection-histories/stats")
|
|
public ResponseEntity<Map<String, Object>> getRecollectionHistoryStats() {
|
|
log.debug("Get recollection history stats");
|
|
Map<String, Object> stats = recollectionHistoryService.getHistoryStats();
|
|
stats.put("recentHistories", recollectionHistoryService.getRecentHistories());
|
|
return ResponseEntity.ok(stats);
|
|
}
|
|
|
|
// ── 마지막 수집 성공일시 모니터링 API ──────────────────────────
|
|
|
|
@Operation(summary = "마지막 수집 성공일시 목록 조회",
|
|
description = "모든 API의 마지막 수집 성공 일시를 조회합니다. 오래된 순으로 정렬됩니다.")
|
|
@GetMapping("/last-collections")
|
|
public ResponseEntity<List<LastCollectionStatusResponse>> getLastCollectionStatuses() {
|
|
log.debug("Received request to get last collection statuses");
|
|
List<LastCollectionStatusResponse> statuses = batchService.getLastCollectionStatuses();
|
|
return ResponseEntity.ok(statuses);
|
|
}
|
|
|
|
// ── 수집 기간 관리 API ───────────────────────────────────────
|
|
|
|
@Operation(summary = "수집 기간 목록 조회", description = "모든 API의 수집 기간 설정을 조회합니다")
|
|
@GetMapping("/collection-periods")
|
|
public ResponseEntity<List<BatchCollectionPeriod>> getCollectionPeriods() {
|
|
log.debug("Get all collection periods");
|
|
return ResponseEntity.ok(recollectionHistoryService.getAllCollectionPeriods());
|
|
}
|
|
|
|
@Operation(summary = "수집 기간 수정", description = "특정 API의 수집 기간을 수정합니다")
|
|
@PostMapping("/collection-periods/{apiKey}/update")
|
|
public ResponseEntity<Map<String, Object>> updateCollectionPeriod(
|
|
@Parameter(description = "API Key") @PathVariable String apiKey,
|
|
@RequestBody Map<String, String> request) {
|
|
log.info("Update collection period: apiKey={}", apiKey);
|
|
try {
|
|
String rangeFromStr = request.get("rangeFromDate");
|
|
String rangeToStr = request.get("rangeToDate");
|
|
|
|
if (rangeFromStr == null || rangeToStr == null) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "rangeFromDate와 rangeToDate는 필수입니다"));
|
|
}
|
|
|
|
LocalDateTime rangeFrom = LocalDateTime.parse(rangeFromStr);
|
|
LocalDateTime rangeTo = LocalDateTime.parse(rangeToStr);
|
|
|
|
if (rangeTo.isBefore(rangeFrom)) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "rangeToDate는 rangeFromDate보다 이후여야 합니다"));
|
|
}
|
|
|
|
recollectionHistoryService.updateCollectionPeriod(apiKey, rangeFrom, rangeTo);
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", "수집 기간이 수정되었습니다"));
|
|
} catch (Exception e) {
|
|
log.error("Error updating collection period: apiKey={}", apiKey, e);
|
|
return ResponseEntity.internalServerError().body(Map.of(
|
|
"success", false,
|
|
"message", "수집 기간 수정 실패: " + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
@Operation(summary = "수집 기간 초기화", description = "특정 API의 수집 기간을 null로 초기화합니다")
|
|
@PostMapping("/collection-periods/{apiKey}/reset")
|
|
public ResponseEntity<Map<String, Object>> resetCollectionPeriod(
|
|
@Parameter(description = "API Key") @PathVariable String apiKey) {
|
|
log.info("Reset collection period: apiKey={}", apiKey);
|
|
try {
|
|
recollectionHistoryService.resetCollectionPeriod(apiKey);
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", "수집 기간이 초기화되었습니다"));
|
|
} catch (Exception e) {
|
|
log.error("Error resetting collection period: apiKey={}", apiKey, e);
|
|
return ResponseEntity.internalServerError().body(Map.of(
|
|
"success", false,
|
|
"message", "수집 기간 초기화 실패: " + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ── 실패 레코드 관리 API ──────────────────────────────────────
|
|
|
|
@Operation(summary = "실패 레코드 일괄 RESOLVED 처리", description = "특정 Job의 FAILED 상태 레코드를 일괄 RESOLVED 처리합니다")
|
|
@PostMapping("/failed-records/resolve")
|
|
public ResponseEntity<Map<String, Object>> resolveFailedRecords(
|
|
@RequestBody Map<String, Object> request) {
|
|
@SuppressWarnings("unchecked")
|
|
List<Integer> rawIds = (List<Integer>) request.get("ids");
|
|
if (rawIds == null || rawIds.isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "ids는 필수이며 비어있을 수 없습니다"));
|
|
}
|
|
List<Long> ids = rawIds.stream().map(Integer::longValue).toList();
|
|
log.info("Resolve failed records: ids count={}", ids.size());
|
|
try {
|
|
int resolved = batchFailedRecordService.resolveByIds(ids);
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"resolvedCount", resolved,
|
|
"message", resolved + "건의 실패 레코드가 RESOLVED 처리되었습니다"));
|
|
} catch (Exception e) {
|
|
log.error("Error resolving failed records", e);
|
|
return ResponseEntity.internalServerError().body(Map.of(
|
|
"success", false,
|
|
"message", "실패 레코드 RESOLVED 처리 실패: " + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
@Operation(summary = "실패 레코드 재시도 횟수 초기화", description = "재시도 횟수를 초과한 FAILED 레코드의 retryCount를 0으로 초기화하여 자동 재수집 대상으로 복원합니다")
|
|
@PostMapping("/failed-records/reset-retry")
|
|
public ResponseEntity<Map<String, Object>> resetRetryCount(
|
|
@RequestBody Map<String, Object> request) {
|
|
@SuppressWarnings("unchecked")
|
|
List<Integer> rawIds = (List<Integer>) request.get("ids");
|
|
if (rawIds == null || rawIds.isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "ids는 필수이며 비어있을 수 없습니다"));
|
|
}
|
|
List<Long> ids = rawIds.stream().map(Integer::longValue).toList();
|
|
log.info("Reset retry count: ids count={}", ids.size());
|
|
try {
|
|
int reset = batchFailedRecordService.resetRetryCount(ids);
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"resetCount", reset,
|
|
"message", reset + "건의 실패 레코드 재시도 횟수가 초기화되었습니다"));
|
|
} catch (Exception e) {
|
|
log.error("Error resetting retry count", e);
|
|
return ResponseEntity.internalServerError().body(Map.of(
|
|
"success", false,
|
|
"message", "재시도 횟수 초기화 실패: " + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ── 재수집 이력 CSV 내보내기 API ──────────────────────────────
|
|
|
|
@Operation(summary = "재수집 이력 CSV 내보내기", description = "필터 조건으로 재수집 이력을 CSV 파일로 내보냅니다 (최대 10,000건)")
|
|
@GetMapping("/recollection-histories/export")
|
|
public void exportRecollectionHistories(
|
|
@Parameter(description = "API Key") @RequestParam(required = false) String apiKey,
|
|
@Parameter(description = "Job 이름") @RequestParam(required = false) String jobName,
|
|
@Parameter(description = "실행 상태") @RequestParam(required = false) String status,
|
|
@Parameter(description = "시작일 (yyyy-MM-dd'T'HH:mm:ss)") @RequestParam(required = false) String fromDate,
|
|
@Parameter(description = "종료일 (yyyy-MM-dd'T'HH:mm:ss)") @RequestParam(required = false) String toDate,
|
|
HttpServletResponse response) throws IOException {
|
|
log.info("Export recollection histories: apiKey={}, jobName={}, status={}", apiKey, jobName, status);
|
|
|
|
LocalDateTime from = fromDate != null ? LocalDateTime.parse(fromDate) : null;
|
|
LocalDateTime to = toDate != null ? LocalDateTime.parse(toDate) : null;
|
|
|
|
List<BatchRecollectionHistory> histories = recollectionHistoryService
|
|
.getHistoriesForExport(apiKey, jobName, status, from, to);
|
|
|
|
response.setContentType("text/csv; charset=UTF-8");
|
|
response.setHeader("Content-Disposition", "attachment; filename=recollection-histories.csv");
|
|
// BOM for Excel UTF-8
|
|
response.getOutputStream().write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});
|
|
|
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
PrintWriter writer = response.getWriter();
|
|
writer.println("이력ID,API Key,작업명,Job실행ID,수집시작일,수집종료일,상태,실행시작,실행종료,소요시간(ms),읽기,쓰기,스킵,API호출,실행자,사유,실패사유,중복여부,생성일");
|
|
|
|
for (BatchRecollectionHistory h : histories) {
|
|
writer.println(String.join(",",
|
|
safeStr(h.getHistoryId()),
|
|
safeStr(h.getApiKey()),
|
|
safeStr(h.getJobName()),
|
|
safeStr(h.getJobExecutionId()),
|
|
h.getRangeFromDate() != null ? h.getRangeFromDate().format(formatter) : "",
|
|
h.getRangeToDate() != null ? h.getRangeToDate().format(formatter) : "",
|
|
safeStr(h.getExecutionStatus()),
|
|
h.getExecutionStartTime() != null ? h.getExecutionStartTime().format(formatter) : "",
|
|
h.getExecutionEndTime() != null ? h.getExecutionEndTime().format(formatter) : "",
|
|
safeStr(h.getDurationMs()),
|
|
safeStr(h.getReadCount()),
|
|
safeStr(h.getWriteCount()),
|
|
safeStr(h.getSkipCount()),
|
|
safeStr(h.getApiCallCount()),
|
|
escapeCsvField(h.getExecutor()),
|
|
escapeCsvField(h.getRecollectionReason()),
|
|
escapeCsvField(h.getFailureReason()),
|
|
h.getHasOverlap() != null ? (h.getHasOverlap() ? "Y" : "N") : "",
|
|
h.getCreatedAt() != null ? h.getCreatedAt().format(formatter) : ""
|
|
));
|
|
}
|
|
writer.flush();
|
|
}
|
|
|
|
private String safeStr(Object value) {
|
|
return value != null ? value.toString() : "";
|
|
}
|
|
|
|
private String escapeCsvField(String value) {
|
|
if (value == null) {
|
|
return "";
|
|
}
|
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
|
return "\"" + value.replace("\"", "\"\"") + "\"";
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// ── Job 한글 표시명 관리 API ──────────────────────────────────
|
|
|
|
@Operation(summary = "Job 표시명 전체 조회", description = "등록된 모든 Job의 한글 표시명을 조회합니다")
|
|
@GetMapping("/display-names")
|
|
public ResponseEntity<List<JobDisplayNameEntity>> getDisplayNames() {
|
|
log.debug("Received request to get all display names");
|
|
return ResponseEntity.ok(jobDisplayNameRepository.findAll());
|
|
}
|
|
|
|
@Operation(summary = "Job 표시명 수정", description = "특정 Job의 한글 표시명을 수정합니다")
|
|
@PutMapping("/display-names/{jobName}")
|
|
public ResponseEntity<Map<String, Object>> updateDisplayName(
|
|
@Parameter(description = "배치 작업 이름", required = true) @PathVariable String jobName,
|
|
@RequestBody Map<String, String> request) {
|
|
log.info("Update display name: jobName={}", jobName);
|
|
try {
|
|
String displayName = request.get("displayName");
|
|
if (displayName == null || displayName.isBlank()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "displayName은 필수입니다"));
|
|
}
|
|
|
|
JobDisplayNameEntity entity = jobDisplayNameRepository.findByJobName(jobName)
|
|
.orElseGet(() -> JobDisplayNameEntity.builder().jobName(jobName).build());
|
|
entity.setDisplayName(displayName);
|
|
jobDisplayNameRepository.save(entity);
|
|
|
|
batchService.refreshDisplayNameCache();
|
|
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", "표시명이 수정되었습니다",
|
|
"data", Map.of("jobName", jobName, "displayName", displayName)));
|
|
} catch (Exception e) {
|
|
log.error("Error updating display name: jobName={}", jobName, e);
|
|
return ResponseEntity.internalServerError().body(Map.of(
|
|
"success", false,
|
|
"message", "표시명 수정 실패: " + e.getMessage()));
|
|
}
|
|
}
|
|
}
|