diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 0f60d71..22ced13 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -29,6 +29,7 @@ - 재시도 초과 레코드 초기화 API/UI 추가 - IMO 기반 Risk 상세 조회 bypass API 추가 (#39) - 배치 작업 목록 한글 표시명 추가 (#40) +- Job 한글 표시명 DB 관리 및 전체 화면 통합 (#45) ### 수정 - 자동 재수집 JobParameter 오버플로우 수정 (VARCHAR 2500 제한 해결) diff --git a/frontend/src/api/batchApi.ts b/frontend/src/api/batchApi.ts index 30e45b4..581e7d6 100644 --- a/frontend/src/api/batchApi.ts +++ b/frontend/src/api/batchApi.ts @@ -336,6 +336,15 @@ export interface LastCollectionStatusDto { elapsedMinutes: number; } +// ── Job Display Name ────────────────────────────────────────── + +export interface JobDisplayName { + id: number; + jobName: string; + displayName: string; + apiKey: string | null; +} + // ── API Functions ──────────────────────────────────────────── export const batchApi = { @@ -495,6 +504,10 @@ export const batchApi = { getLastCollectionStatuses: () => fetchJson(`${BASE}/last-collections`), + // Display Names + getDisplayNames: () => + fetchJson(`${BASE}/display-names`), + resolveFailedRecords: (ids: number[]) => postJson<{ success: boolean; message: string; resolvedCount?: number }>( `${BASE}/failed-records/resolve`, { ids }), diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index b480f04..6e33fdb 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { batchApi, @@ -6,6 +6,7 @@ import { type DashboardStats, type ExecutionStatisticsDto, type RecollectionStatsResponse, + type JobDisplayName, } from '../api/batchApi'; import { usePoller } from '../hooks/usePoller'; import { useToastContext } from '../contexts/ToastContext'; @@ -49,6 +50,7 @@ export default function Dashboard() { const [abandoning, setAbandoning] = useState(false); const [statistics, setStatistics] = useState(null); const [recollectionStats, setRecollectionStats] = useState(null); + const [displayNames, setDisplayNames] = useState([]); const loadStatistics = useCallback(async () => { try { @@ -76,6 +78,19 @@ export default function Dashboard() { loadRecollectionStats(); }, [loadRecollectionStats]); + useEffect(() => { + batchApi.getDisplayNames().then(setDisplayNames).catch(() => {}); + }, []); + + const displayNameMap = useMemo>(() => { + const map: Record = {}; + for (const dn of displayNames) { + if (dn.apiKey) map[dn.apiKey] = dn.displayName; + map[dn.jobName] = dn.displayName; + } + return map; + }, [displayNames]); + const loadDashboard = useCallback(async () => { try { const data = await batchApi.getDashboard(); @@ -235,7 +250,7 @@ export default function Dashboard() { {runningJobs.map((job) => ( - {job.jobName} + {displayNameMap[job.jobName] || job.jobName} #{job.executionId} {formatDateTime(job.startTime)} @@ -290,7 +305,7 @@ export default function Dashboard() { #{exec.executionId} - {exec.jobName} + {displayNameMap[exec.jobName] || exec.jobName} {formatDateTime(exec.startTime)} {formatDateTime(exec.endTime)} @@ -337,7 +352,7 @@ export default function Dashboard() { #{fail.executionId} - {fail.jobName} + {displayNameMap[fail.jobName] || fail.jobName} {formatDateTime(fail.startTime)} {fail.exitMessage @@ -406,7 +421,7 @@ export default function Dashboard() { #{h.historyId} - {h.apiKeyName || h.jobName} + {displayNameMap[h.apiKey] || h.apiKeyName || h.jobName} {h.executor || '-'} {formatDateTime(h.executionStartTime)} diff --git a/frontend/src/pages/Executions.tsx b/frontend/src/pages/Executions.tsx index 2bb0f4a..20546b2 100644 --- a/frontend/src/pages/Executions.tsx +++ b/frontend/src/pages/Executions.tsx @@ -1,6 +1,6 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { batchApi, type JobExecutionDto, type ExecutionSearchResponse } from '../api/batchApi'; +import { batchApi, type JobExecutionDto, type ExecutionSearchResponse, type JobDisplayName } from '../api/batchApi'; import { formatDateTime, calculateDuration } from '../utils/formatters'; import { usePoller } from '../hooks/usePoller'; import { useToastContext } from '../contexts/ToastContext'; @@ -31,6 +31,7 @@ export default function Executions() { const jobFromQuery = searchParams.get('job') || ''; const [jobs, setJobs] = useState([]); + const [displayNames, setDisplayNames] = useState([]); const [executions, setExecutions] = useState([]); const [selectedJobs, setSelectedJobs] = useState(jobFromQuery ? [jobFromQuery] : []); const [jobDropdownOpen, setJobDropdownOpen] = useState(false); @@ -54,6 +55,18 @@ export default function Executions() { const { showToast } = useToastContext(); + useEffect(() => { + batchApi.getDisplayNames().then(setDisplayNames).catch(() => {}); + }, []); + + const displayNameMap = useMemo>(() => { + const map: Record = {}; + for (const dn of displayNames) { + map[dn.jobName] = dn.displayName; + } + return map; + }, [displayNames]); + const loadJobs = useCallback(async () => { try { const data = await batchApi.getJobs(); @@ -267,7 +280,7 @@ export default function Executions() { onChange={() => toggleJob(job)} className="rounded border-wing-border text-wing-accent focus:ring-wing-accent" /> - {job} + {displayNameMap[job] || job} ))} @@ -291,7 +304,7 @@ export default function Executions() { key={job} className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-wing-accent/15 text-wing-accent rounded-full" > - {job} + {displayNameMap[job] || job} ))} @@ -757,7 +768,7 @@ export default function Recollects() { selectedApiKey === p.apiKey ? 'bg-wing-accent/10 text-wing-accent font-medium' : 'text-wing-text' }`} > - {p.apiKeyName || p.apiKey} + {displayNameMap[p.apiKey] || p.apiKeyName || p.apiKey} ))} @@ -900,8 +911,8 @@ export default function Recollects() { #{hist.historyId} -
- {hist.apiKeyName || hist.apiKey} +
+ {displayNameMap[hist.apiKey] || hist.apiKeyName || hist.apiKey}
diff --git a/frontend/src/pages/Schedules.tsx b/frontend/src/pages/Schedules.tsx index 7ffab3a..3993886 100644 --- a/frontend/src/pages/Schedules.tsx +++ b/frontend/src/pages/Schedules.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { batchApi, type ScheduleResponse } from '../api/batchApi'; +import { batchApi, type ScheduleResponse, type JobDisplayName } from '../api/batchApi'; import { formatDateTime } from '../utils/formatters'; import { useToastContext } from '../contexts/ToastContext'; import ConfirmModal from '../components/ConfirmModal'; @@ -93,6 +93,7 @@ export default function Schedules() { // Schedule list state const [schedules, setSchedules] = useState([]); const [listLoading, setListLoading] = useState(true); + const [displayNames, setDisplayNames] = useState([]); // Confirm modal state const [confirmAction, setConfirmAction] = useState(null); @@ -125,8 +126,17 @@ export default function Schedules() { useEffect(() => { loadJobs(); loadSchedules(); + batchApi.getDisplayNames().then(setDisplayNames).catch(() => {}); }, [loadJobs, loadSchedules]); + const displayNameMap = useMemo>(() => { + const map: Record = {}; + for (const dn of displayNames) { + map[dn.jobName] = dn.displayName; + } + return map; + }, [displayNames]); + const handleJobSelect = async (jobName: string) => { setSelectedJob(jobName); setCronExpression(''); @@ -250,7 +260,7 @@ export default function Schedules() { {jobs.map((job) => ( ))} @@ -372,8 +382,8 @@ export default function Schedules() { {/* Header */}
-

- {schedule.jobName} +

+ {displayNameMap[schedule.jobName] || schedule.jobName}

([]); const [loading, setLoading] = useState(true); + const [displayNames, setDisplayNames] = useState([]); + + useEffect(() => { + batchApi.getDisplayNames().then(setDisplayNames).catch(() => {}); + }, []); + + const displayNameMap = useMemo>(() => { + const map: Record = {}; + for (const dn of displayNames) { + map[dn.jobName] = dn.displayName; + } + return map; + }, [displayNames]); + // Tooltip const [tooltip, setTooltip] = useState(null); const tooltipTimeoutRef = useRef | null>(null); @@ -288,9 +302,9 @@ export default function Timeline() {
- {schedule.jobName} + {displayNameMap[schedule.jobName] || schedule.jobName}
{/* Execution Cells */} @@ -345,7 +359,7 @@ export default function Timeline() { }} >
-
{tooltip.jobName}
+
{displayNameMap[tooltip.jobName] || tooltip.jobName}
기간: {tooltip.period.label}
@@ -379,7 +393,7 @@ export default function Timeline() {

- {selectedCell.jobName} + {displayNameMap[selectedCell.jobName] || selectedCell.jobName}

구간: {selectedCell.periodLabel} diff --git a/src/main/java/com/snp/batch/global/controller/BatchController.java b/src/main/java/com/snp/batch/global/controller/BatchController.java index a95aacf..859bd4c 100644 --- a/src/main/java/com/snp/batch/global/controller/BatchController.java +++ b/src/main/java/com/snp/batch/global/controller/BatchController.java @@ -3,6 +3,8 @@ 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; @@ -46,6 +48,7 @@ public class BatchController { private final ScheduleService scheduleService; private final RecollectionHistoryService recollectionHistoryService; private final BatchFailedRecordService batchFailedRecordService; + private final JobDisplayNameRepository jobDisplayNameRepository; @Operation(summary = "배치 작업 실행", description = "지정된 배치 작업을 즉시 실행합니다. 쿼리 파라미터로 Job Parameters 전달 가능") @ApiResponses(value = { @@ -743,4 +746,46 @@ public class BatchController { } return value; } + + // ── Job 한글 표시명 관리 API ────────────────────────────────── + + @Operation(summary = "Job 표시명 전체 조회", description = "등록된 모든 Job의 한글 표시명을 조회합니다") + @GetMapping("/display-names") + public ResponseEntity> 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> updateDisplayName( + @Parameter(description = "배치 작업 이름", required = true) @PathVariable String jobName, + @RequestBody Map 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())); + } + } } diff --git a/src/main/java/com/snp/batch/global/model/JobDisplayNameEntity.java b/src/main/java/com/snp/batch/global/model/JobDisplayNameEntity.java new file mode 100644 index 0000000..7e77d12 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/JobDisplayNameEntity.java @@ -0,0 +1,47 @@ +package com.snp.batch.global.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 배치 작업 한글 표시명을 관리하는 엔티티 + * 모든 화면(작업 목록, 재수집 이력 등)에서 공통으로 사용 + */ +@Entity +@Table(name = "job_display_name", indexes = { + @Index(name = "idx_job_display_name_job_name", columnList = "job_name", unique = true), + @Index(name = "idx_job_display_name_api_key", columnList = "api_key", unique = true) +}) +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JobDisplayNameEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 배치 작업 Bean 이름 + */ + @Column(name = "job_name", unique = true, nullable = false, length = 100) + private String jobName; + + /** + * 한글 표시명 + */ + @Column(name = "display_name", nullable = false, length = 200) + private String displayName; + + /** + * 외부 API 키 (batch_collection_period.api_key 연동용, nullable) + */ + @Column(name = "api_key", unique = true, length = 50) + private String apiKey; +} diff --git a/src/main/java/com/snp/batch/global/repository/JobDisplayNameRepository.java b/src/main/java/com/snp/batch/global/repository/JobDisplayNameRepository.java new file mode 100644 index 0000000..edb451c --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/JobDisplayNameRepository.java @@ -0,0 +1,13 @@ +package com.snp.batch.global.repository; + +import com.snp.batch.global.model.JobDisplayNameEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface JobDisplayNameRepository extends JpaRepository { + + Optional findByJobName(String jobName); + + Optional findByApiKey(String apiKey); +} diff --git a/src/main/java/com/snp/batch/service/BatchService.java b/src/main/java/com/snp/batch/service/BatchService.java index 227b85d..1aa27ff 100644 --- a/src/main/java/com/snp/batch/service/BatchService.java +++ b/src/main/java/com/snp/batch/service/BatchService.java @@ -5,7 +5,9 @@ import com.snp.batch.global.dto.*; import com.snp.batch.global.model.BatchApiLog; import com.snp.batch.global.model.BatchFailedRecord; import com.snp.batch.global.model.BatchLastExecution; +import com.snp.batch.global.model.JobDisplayNameEntity; import com.snp.batch.global.repository.BatchApiLogRepository; +import com.snp.batch.global.repository.JobDisplayNameRepository; import com.snp.batch.global.repository.BatchFailedRecordRepository; import com.snp.batch.global.repository.BatchLastExecutionRepository; import com.snp.batch.global.repository.TimelineRepository; @@ -37,39 +39,11 @@ import java.util.stream.Collectors; @Service public class BatchService { - /** Job Bean 이름 → 한글 표시명 매핑 */ - private static final Map JOB_DISPLAY_NAMES = Map.ofEntries( - // AIS - Map.entry("aisTargetImportJob", "AIS 선박위치 수집(1분 주기)"), - Map.entry("aisTargetDbSyncJob", "AIS 선박위치 DB 적재(15분 주기)"), - // 공통코드 - Map.entry("FlagCodeImportJob", "선박 국가코드 수집"), - Map.entry("Stat5CodeImportJob", "선박 유형코드 수집"), - // Compliance - Map.entry("ComplianceImportRangeJob", "선박 제재 정보 수집"), - Map.entry("CompanyComplianceImportRangeJob", "회사 제재 정보 수집"), - // Event - Map.entry("EventImportJob", "해양 사건/사고 수집"), - // Port - Map.entry("PortImportJob", "항구 시설 수집"), - // Movement - Map.entry("AnchorageCallsRangeImportJob", "정박지 기항 이력 수집"), - Map.entry("BerthCallsRangeImportJob", "선석 기항 이력 수집"), - Map.entry("CurrentlyAtRangeImportJob", "현재 위치 이력 수집"), - Map.entry("DestinationsRangeImportJob", "목적지 이력 수집"), - Map.entry("PortCallsRangeImportJob", "항구 기항 이력 수집"), - Map.entry("STSOperationRangeImportJob", "STS 작업 이력 수집"), - Map.entry("TerminalCallsRangeImportJob", "터미널 기항 이력 수집"), - Map.entry("TransitsRangeImportJob", "항해 이력 수집"), - // PSC - Map.entry("PSCDetailImportJob", "PSC 선박 검사 수집"), - // Risk - Map.entry("RiskRangeImportJob", "선박 위험지표 수집"), - // Ship - Map.entry("ShipDetailUpdateJob", "선박 제원정보 수집"), - // Infra - Map.entry("partitionManagerJob", "AIS 파티션 테이블 생성/관리") - ); + /** DB에서 로드한 Job 한글 표시명 캐시 */ + private Map jobDisplayNameCache = Map.of(); + + /** DB에서 로드한 apiKey → 한글 표시명 캐시 */ + private Map apiKeyDisplayNameCache = Map.of(); private final JobLauncher jobLauncher; private final JobExplorer jobExplorer; @@ -81,6 +55,7 @@ public class BatchService { private final BatchApiLogRepository apiLogRepository; private final BatchFailedRecordRepository failedRecordRepository; private final BatchLastExecutionRepository batchLastExecutionRepository; + private final JobDisplayNameRepository jobDisplayNameRepository; @Autowired public BatchService(JobLauncher jobLauncher, @@ -92,7 +67,8 @@ public class BatchService { RecollectionJobExecutionListener recollectionJobExecutionListener, BatchApiLogRepository apiLogRepository, BatchFailedRecordRepository failedRecordRepository, - BatchLastExecutionRepository batchLastExecutionRepository) { + BatchLastExecutionRepository batchLastExecutionRepository, + JobDisplayNameRepository jobDisplayNameRepository) { this.jobLauncher = jobLauncher; this.jobExplorer = jobExplorer; this.jobOperator = jobOperator; @@ -103,6 +79,7 @@ public class BatchService { this.apiLogRepository = apiLogRepository; this.failedRecordRepository = failedRecordRepository; this.batchLastExecutionRepository = batchLastExecutionRepository; + this.jobDisplayNameRepository = jobDisplayNameRepository; } /** @@ -110,13 +87,96 @@ public class BatchService { * 리스너 내부에서 executionMode 체크하므로 정상 실행에는 영향 없음 */ @PostConstruct - public void registerGlobalListeners() { + public void init() { + // 리스너 등록 jobMap.values().forEach(job -> { if (job instanceof AbstractJob abstractJob) { abstractJob.registerJobExecutionListener(recollectionJobExecutionListener); } }); log.info("[BatchService] RecollectionJobExecutionListener를 {}개 Job에 등록", jobMap.size()); + + // Job 한글 표시명 초기 데이터 시드 (테이블이 비어있을 때만) + seedDisplayNamesIfEmpty(); + + // Job 한글 표시명 캐시 로드 + refreshDisplayNameCache(); + } + + /** + * job_display_name 테이블이 비어있으면 기본 표시명을 시드합니다. + */ + private void seedDisplayNamesIfEmpty() { + if (jobDisplayNameRepository.count() > 0) { + return; + } + List entities = List.of( + buildDisplayName("aisTargetImportJob", "AIS 선박위치 수집(1분 주기)", null), + buildDisplayName("aisTargetDbSyncJob", "AIS 선박위치 DB 적재(15분 주기)", null), + buildDisplayName("FlagCodeImportJob", "선박 국가코드 수집", null), + buildDisplayName("Stat5CodeImportJob", "선박 유형코드 수집", null), + buildDisplayName("ComplianceImportRangeJob", "선박 제재 정보 수집", "COMPLIANCE_IMPORT_API"), + buildDisplayName("CompanyComplianceImportRangeJob", "회사 제재 정보 수집", "COMPANY_COMPLIANCE_IMPORT_API"), + buildDisplayName("EventImportJob", "해양 사건/사고 수집", "EVENT_IMPORT_API"), + buildDisplayName("PortImportJob", "항구 시설 수집", null), + buildDisplayName("AnchorageCallsRangeImportJob", "정박지 기항 이력 수집", "ANCHORAGE_CALLS_IMPORT_API"), + buildDisplayName("BerthCallsRangeImportJob", "선석 기항 이력 수집", "BERTH_CALLS_IMPORT_API"), + buildDisplayName("CurrentlyAtRangeImportJob", "현재 위치 이력 수집", "CURRENTLY_AT_IMPORT_API"), + buildDisplayName("DestinationsRangeImportJob", "목적지 이력 수집", "DESTINATIONS_IMPORT_API"), + buildDisplayName("PortCallsRangeImportJob", "항구 기항 이력 수집", "PORT_CALLS_IMPORT_API"), + buildDisplayName("STSOperationRangeImportJob", "STS 작업 이력 수집", "STS_OPERATION_IMPORT_API"), + buildDisplayName("TerminalCallsRangeImportJob", "터미널 기항 이력 수집", "TERMINAL_CALLS_IMPORT_API"), + buildDisplayName("TransitsRangeImportJob", "항해 이력 수집", "TRANSITS_IMPORT_API"), + buildDisplayName("PSCDetailImportJob", "PSC 선박 검사 수집", "PSC_IMPORT_API"), + buildDisplayName("RiskRangeImportJob", "선박 위험지표 수집", "RISK_IMPORT_API"), + buildDisplayName("ShipDetailUpdateJob", "선박 제원정보 수집", "SHIP_DETAIL_UPDATE_API"), + buildDisplayName("partitionManagerJob", "AIS 파티션 테이블 생성/관리", null) + ); + jobDisplayNameRepository.saveAll(entities); + log.info("[BatchService] Job 표시명 초기 데이터 시드: {} 건", entities.size()); + } + + private JobDisplayNameEntity buildDisplayName(String jobName, String displayName, String apiKey) { + return JobDisplayNameEntity.builder() + .jobName(jobName) + .displayName(displayName) + .apiKey(apiKey) + .build(); + } + + /** + * DB에서 Job 한글 표시명을 로드하여 캐시를 갱신합니다. + */ + public void refreshDisplayNameCache() { + List all = jobDisplayNameRepository.findAll(); + jobDisplayNameCache = all.stream() + .collect(Collectors.toMap( + JobDisplayNameEntity::getJobName, + JobDisplayNameEntity::getDisplayName, + (a, b) -> a + )); + apiKeyDisplayNameCache = all.stream() + .filter(e -> e.getApiKey() != null) + .collect(Collectors.toMap( + JobDisplayNameEntity::getApiKey, + JobDisplayNameEntity::getDisplayName, + (a, b) -> a + )); + log.info("[BatchService] Job 표시명 캐시 로드: {} 건 (apiKey: {} 건)", jobDisplayNameCache.size(), apiKeyDisplayNameCache.size()); + } + + /** + * Job의 한글 표시명을 반환합니다. DB에 없으면 null. + */ + public String getDisplayName(String jobName) { + return jobDisplayNameCache.get(jobName); + } + + /** + * apiKey로 한글 표시명을 반환합니다. DB에 없으면 null. + */ + public String getDisplayNameByApiKey(String apiKey) { + return apiKeyDisplayNameCache.get(apiKey); } public Long executeJob(String jobName) throws Exception { @@ -886,7 +946,7 @@ public class BatchService { return JobDetailDto.builder() .jobName(jobName) - .displayName(JOB_DISPLAY_NAMES.get(jobName)) + .displayName(jobDisplayNameCache.get(jobName)) .lastExecution(lastExec) .scheduleCron(cronMap.get(jobName)) .build(); diff --git a/src/main/java/com/snp/batch/service/RecollectionHistoryService.java b/src/main/java/com/snp/batch/service/RecollectionHistoryService.java index 700e67c..2eaa1dd 100644 --- a/src/main/java/com/snp/batch/service/RecollectionHistoryService.java +++ b/src/main/java/com/snp/batch/service/RecollectionHistoryService.java @@ -5,11 +5,13 @@ import com.snp.batch.global.model.BatchCollectionPeriod; import com.snp.batch.global.model.BatchLastExecution; import com.snp.batch.global.model.BatchRecollectionHistory; import com.snp.batch.global.model.BatchFailedRecord; +import com.snp.batch.global.model.JobDisplayNameEntity; import com.snp.batch.global.repository.BatchApiLogRepository; import com.snp.batch.global.repository.BatchCollectionPeriodRepository; import com.snp.batch.global.repository.BatchFailedRecordRepository; import com.snp.batch.global.repository.BatchLastExecutionRepository; import com.snp.batch.global.repository.BatchRecollectionHistoryRepository; +import com.snp.batch.global.repository.JobDisplayNameRepository; import jakarta.persistence.criteria.Predicate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,6 +43,7 @@ public class RecollectionHistoryService { private final BatchApiLogRepository apiLogRepository; private final BatchFailedRecordRepository failedRecordRepository; private final JobExplorer jobExplorer; + private final JobDisplayNameRepository jobDisplayNameRepository; /** * 재수집 실행 시작 기록 @@ -70,9 +73,11 @@ public class RecollectionHistoryService { if (isRetryByRecordKeys) { // 실패 건 재수집 (자동/수동): 날짜 범위가 아닌 실패 레코드 키 기반이므로 날짜 없이 이력 생성 if (apiKey != null) { - apiKeyName = periodRepository.findById(apiKey) - .map(BatchCollectionPeriod::getApiKeyName) - .orElse(null); + apiKeyName = jobDisplayNameRepository.findByApiKey(apiKey) + .map(JobDisplayNameEntity::getDisplayName) + .orElseGet(() -> periodRepository.findById(apiKey) + .map(BatchCollectionPeriod::getApiKeyName) + .orElse(null)); } log.info("[RecollectionHistory] 실패 건 재수집 이력 생성 (날짜 범위 없음): executor={}, apiKey={}, apiKeyName={}", executor, apiKey, apiKeyName); } else { @@ -86,7 +91,9 @@ public class RecollectionHistoryService { BatchCollectionPeriod cp = period.get(); rangeFrom = cp.getRangeFromDate(); rangeTo = cp.getRangeToDate(); - apiKeyName = cp.getApiKeyName(); + apiKeyName = jobDisplayNameRepository.findByApiKey(apiKey) + .map(JobDisplayNameEntity::getDisplayName) + .orElseGet(cp::getApiKeyName); // 기간 중복 검출 List overlaps = historyRepository