- 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)
535 lines
16 KiB
TypeScript
535 lines
16 KiB
TypeScript
const BASE = import.meta.env.DEV ? '/snp-api/api/batch' : '/snp-api/api/batch';
|
|
|
|
async function fetchJson<T>(url: string): Promise<T> {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`API Error: ${res.status} ${res.statusText}`);
|
|
return res.json();
|
|
}
|
|
|
|
async function postJson<T>(url: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (!res.ok) throw new Error(`API Error: ${res.status} ${res.statusText}`);
|
|
return res.json();
|
|
}
|
|
|
|
// ── Dashboard ────────────────────────────────────────────────
|
|
|
|
export interface DashboardStats {
|
|
totalSchedules: number;
|
|
activeSchedules: number;
|
|
inactiveSchedules: number;
|
|
totalJobs: number;
|
|
}
|
|
|
|
export interface RunningJob {
|
|
jobName: string;
|
|
executionId: number;
|
|
status: string;
|
|
startTime: string;
|
|
}
|
|
|
|
export interface RecentExecution {
|
|
executionId: number;
|
|
jobName: string;
|
|
status: string;
|
|
startTime: string;
|
|
endTime: string | null;
|
|
}
|
|
|
|
export interface RecentFailure {
|
|
executionId: number;
|
|
jobName: string;
|
|
status: string;
|
|
startTime: string;
|
|
endTime: string | null;
|
|
exitMessage: string | null;
|
|
}
|
|
|
|
export interface FailureStats {
|
|
last24h: number;
|
|
last7d: number;
|
|
}
|
|
|
|
export interface DashboardResponse {
|
|
stats: DashboardStats;
|
|
runningJobs: RunningJob[];
|
|
recentExecutions: RecentExecution[];
|
|
recentFailures: RecentFailure[];
|
|
staleExecutionCount: number;
|
|
failureStats: FailureStats;
|
|
}
|
|
|
|
// ── Job Execution ────────────────────────────────────────────
|
|
|
|
export interface JobExecutionDto {
|
|
executionId: number;
|
|
jobName: string;
|
|
status: string;
|
|
startTime: string;
|
|
endTime: string | null;
|
|
exitCode: string | null;
|
|
exitMessage: string | null;
|
|
failedRecordCount: number | null;
|
|
}
|
|
|
|
export interface ApiCallInfo {
|
|
apiUrl: string;
|
|
method: string;
|
|
parameters: Record<string, unknown> | null;
|
|
totalCalls: number;
|
|
completedCalls: number;
|
|
lastCallTime: string;
|
|
}
|
|
|
|
export interface StepExecutionDto {
|
|
stepExecutionId: number;
|
|
stepName: string;
|
|
status: string;
|
|
startTime: string;
|
|
endTime: string | null;
|
|
readCount: number;
|
|
writeCount: number;
|
|
commitCount: number;
|
|
rollbackCount: number;
|
|
readSkipCount: number;
|
|
processSkipCount: number;
|
|
writeSkipCount: number;
|
|
filterCount: number;
|
|
exitCode: string;
|
|
exitMessage: string | null;
|
|
duration: number | null;
|
|
apiCallInfo: ApiCallInfo | null;
|
|
apiLogSummary: StepApiLogSummary | null;
|
|
failedRecords?: FailedRecordDto[] | null;
|
|
}
|
|
|
|
export interface ApiLogEntryDto {
|
|
logId: number;
|
|
requestUri: string;
|
|
httpMethod: string;
|
|
statusCode: number | null;
|
|
responseTimeMs: number | null;
|
|
responseCount: number | null;
|
|
errorMessage: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface StepApiLogSummary {
|
|
totalCalls: number;
|
|
successCount: number;
|
|
errorCount: number;
|
|
avgResponseMs: number;
|
|
maxResponseMs: number;
|
|
minResponseMs: number;
|
|
totalResponseMs: number;
|
|
totalRecordCount: number;
|
|
}
|
|
|
|
export interface ApiLogPageResponse {
|
|
content: ApiLogEntryDto[];
|
|
page: number;
|
|
size: number;
|
|
totalElements: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export type ApiLogStatus = 'ALL' | 'SUCCESS' | 'ERROR';
|
|
|
|
export interface FailedRecordDto {
|
|
id: number;
|
|
jobName: string;
|
|
recordKey: string;
|
|
errorMessage: string | null;
|
|
retryCount: number;
|
|
status: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface JobExecutionDetailDto {
|
|
executionId: number;
|
|
jobName: string;
|
|
status: string;
|
|
startTime: string;
|
|
endTime: string | null;
|
|
exitCode: string;
|
|
exitMessage: string | null;
|
|
jobParameters: Record<string, string>;
|
|
jobInstanceId: number;
|
|
duration: number | null;
|
|
readCount: number;
|
|
writeCount: number;
|
|
skipCount: number;
|
|
filterCount: number;
|
|
stepExecutions: StepExecutionDto[];
|
|
}
|
|
|
|
// ── Schedule ─────────────────────────────────────────────────
|
|
|
|
export interface ScheduleResponse {
|
|
id: number;
|
|
jobName: string;
|
|
cronExpression: string;
|
|
description: string | null;
|
|
active: boolean;
|
|
nextFireTime: string | null;
|
|
previousFireTime: string | null;
|
|
triggerState: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface ScheduleRequest {
|
|
jobName: string;
|
|
cronExpression: string;
|
|
description?: string;
|
|
active?: boolean;
|
|
}
|
|
|
|
// ── Timeline ─────────────────────────────────────────────────
|
|
|
|
export interface PeriodInfo {
|
|
key: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface ExecutionInfo {
|
|
executionId: number | null;
|
|
status: string;
|
|
startTime: string | null;
|
|
endTime: string | null;
|
|
}
|
|
|
|
export interface ScheduleTimeline {
|
|
jobName: string;
|
|
executions: Record<string, ExecutionInfo | null>;
|
|
}
|
|
|
|
export interface TimelineResponse {
|
|
periodLabel: string;
|
|
periods: PeriodInfo[];
|
|
schedules: ScheduleTimeline[];
|
|
}
|
|
|
|
// ── F4: Execution Search ─────────────────────────────────────
|
|
|
|
export interface ExecutionSearchResponse {
|
|
executions: JobExecutionDto[];
|
|
totalCount: number;
|
|
page: number;
|
|
size: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
// ── F7: Job Detail ───────────────────────────────────────────
|
|
|
|
export interface LastExecution {
|
|
executionId: number;
|
|
status: string;
|
|
startTime: string;
|
|
endTime: string | null;
|
|
}
|
|
|
|
export interface JobDetailDto {
|
|
jobName: string;
|
|
displayName: string | null;
|
|
lastExecution: LastExecution | null;
|
|
scheduleCron: string | null;
|
|
}
|
|
|
|
// ── F8: Statistics ───────────────────────────────────────────
|
|
|
|
export interface DailyStat {
|
|
date: string;
|
|
successCount: number;
|
|
failedCount: number;
|
|
otherCount: number;
|
|
avgDurationMs: number;
|
|
}
|
|
|
|
export interface ExecutionStatisticsDto {
|
|
dailyStats: DailyStat[];
|
|
totalExecutions: number;
|
|
totalSuccess: number;
|
|
totalFailed: number;
|
|
avgDurationMs: number;
|
|
}
|
|
|
|
// ── Recollection History ─────────────────────────────────────
|
|
|
|
export interface RecollectionHistoryDto {
|
|
historyId: number;
|
|
apiKey: string;
|
|
apiKeyName: string | null;
|
|
jobName: string;
|
|
jobExecutionId: number | null;
|
|
rangeFromDate: string;
|
|
rangeToDate: string;
|
|
executionStatus: string;
|
|
executionStartTime: string | null;
|
|
executionEndTime: string | null;
|
|
durationMs: number | null;
|
|
readCount: number | null;
|
|
writeCount: number | null;
|
|
skipCount: number | null;
|
|
apiCallCount: number | null;
|
|
executor: string | null;
|
|
recollectionReason: string | null;
|
|
failureReason: string | null;
|
|
hasOverlap: boolean | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface RecollectionSearchResponse {
|
|
content: RecollectionHistoryDto[];
|
|
totalElements: number;
|
|
number: number;
|
|
size: number;
|
|
totalPages: number;
|
|
failedRecordCounts: Record<number, number>;
|
|
}
|
|
|
|
export interface RecollectionStatsResponse {
|
|
totalCount: number;
|
|
completedCount: number;
|
|
failedCount: number;
|
|
runningCount: number;
|
|
overlapCount: number;
|
|
recentHistories: RecollectionHistoryDto[];
|
|
}
|
|
|
|
export interface ApiStatsDto {
|
|
callCount: number;
|
|
totalMs: number;
|
|
avgMs: number;
|
|
maxMs: number;
|
|
minMs: number;
|
|
}
|
|
|
|
export interface RecollectionDetailResponse {
|
|
history: RecollectionHistoryDto;
|
|
overlappingHistories: RecollectionHistoryDto[];
|
|
apiStats: ApiStatsDto | null;
|
|
collectionPeriod: CollectionPeriodDto | null;
|
|
stepExecutions: StepExecutionDto[];
|
|
}
|
|
|
|
export interface CollectionPeriodDto {
|
|
apiKey: string;
|
|
apiKeyName: string | null;
|
|
jobName: string | null;
|
|
orderSeq: number | null;
|
|
rangeFromDate: string | null;
|
|
rangeToDate: string | null;
|
|
}
|
|
|
|
// ── Last Collection Status ───────────────────────────────────
|
|
|
|
export interface LastCollectionStatusDto {
|
|
apiKey: string;
|
|
apiDesc: string | null;
|
|
lastSuccessDate: string | null;
|
|
updatedAt: string | null;
|
|
elapsedMinutes: number;
|
|
}
|
|
|
|
// ── Job Display Name ──────────────────────────────────────────
|
|
|
|
export interface JobDisplayName {
|
|
id: number;
|
|
jobName: string;
|
|
displayName: string;
|
|
apiKey: string | null;
|
|
}
|
|
|
|
// ── API Functions ────────────────────────────────────────────
|
|
|
|
export const batchApi = {
|
|
getDashboard: () =>
|
|
fetchJson<DashboardResponse>(`${BASE}/dashboard`),
|
|
|
|
getJobs: () =>
|
|
fetchJson<string[]>(`${BASE}/jobs`),
|
|
|
|
getJobsDetail: () =>
|
|
fetchJson<JobDetailDto[]>(`${BASE}/jobs/detail`),
|
|
|
|
executeJob: (jobName: string, params?: Record<string, string>) => {
|
|
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
return postJson<{ success: boolean; message: string; executionId?: number }>(
|
|
`${BASE}/jobs/${jobName}/execute${qs}`);
|
|
},
|
|
|
|
retryFailedRecords: (jobName: string, recordKeys: string[], stepExecutionId: number) => {
|
|
const qs = new URLSearchParams({
|
|
retryRecordKeys: recordKeys.join(','),
|
|
sourceStepExecutionId: String(stepExecutionId),
|
|
executionMode: 'RECOLLECT',
|
|
executor: 'MANUAL_RETRY',
|
|
reason: `실패 건 수동 재수집 (${recordKeys.length}건)`,
|
|
});
|
|
return postJson<{ success: boolean; message: string; executionId?: number }>(
|
|
`${BASE}/jobs/${jobName}/execute?${qs.toString()}`);
|
|
},
|
|
|
|
getJobExecutions: (jobName: string) =>
|
|
fetchJson<JobExecutionDto[]>(`${BASE}/jobs/${jobName}/executions`),
|
|
|
|
getRecentExecutions: (limit = 50) =>
|
|
fetchJson<JobExecutionDto[]>(`${BASE}/executions/recent?limit=${limit}`),
|
|
|
|
getExecutionDetail: (id: number) =>
|
|
fetchJson<JobExecutionDetailDto>(`${BASE}/executions/${id}/detail`),
|
|
|
|
stopExecution: (id: number) =>
|
|
postJson<{ success: boolean; message: string }>(`${BASE}/executions/${id}/stop`),
|
|
|
|
// F1: Abandon
|
|
getStaleExecutions: (thresholdMinutes = 60) =>
|
|
fetchJson<JobExecutionDto[]>(`${BASE}/executions/stale?thresholdMinutes=${thresholdMinutes}`),
|
|
|
|
abandonExecution: (id: number) =>
|
|
postJson<{ success: boolean; message: string }>(`${BASE}/executions/${id}/abandon`),
|
|
|
|
abandonAllStale: (thresholdMinutes = 60) =>
|
|
postJson<{ success: boolean; message: string; abandonedCount?: number }>(
|
|
`${BASE}/executions/stale/abandon-all?thresholdMinutes=${thresholdMinutes}`),
|
|
|
|
// F4: Search
|
|
searchExecutions: (params: {
|
|
jobNames?: string[];
|
|
status?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
page?: number;
|
|
size?: number;
|
|
}) => {
|
|
const qs = new URLSearchParams();
|
|
if (params.jobNames && params.jobNames.length > 0) qs.set('jobNames', params.jobNames.join(','));
|
|
if (params.status) qs.set('status', params.status);
|
|
if (params.startDate) qs.set('startDate', params.startDate);
|
|
if (params.endDate) qs.set('endDate', params.endDate);
|
|
qs.set('page', String(params.page ?? 0));
|
|
qs.set('size', String(params.size ?? 50));
|
|
return fetchJson<ExecutionSearchResponse>(`${BASE}/executions/search?${qs.toString()}`);
|
|
},
|
|
|
|
// F8: Statistics
|
|
getStatistics: (days = 30) =>
|
|
fetchJson<ExecutionStatisticsDto>(`${BASE}/statistics?days=${days}`),
|
|
|
|
getJobStatistics: (jobName: string, days = 30) =>
|
|
fetchJson<ExecutionStatisticsDto>(`${BASE}/statistics/${jobName}?days=${days}`),
|
|
|
|
// Schedule
|
|
getSchedules: () =>
|
|
fetchJson<{ schedules: ScheduleResponse[]; count: number }>(`${BASE}/schedules`),
|
|
|
|
getSchedule: (jobName: string) =>
|
|
fetchJson<ScheduleResponse>(`${BASE}/schedules/${jobName}`),
|
|
|
|
createSchedule: (data: ScheduleRequest) =>
|
|
postJson<{ success: boolean; message: string; data?: ScheduleResponse }>(`${BASE}/schedules`, data),
|
|
|
|
updateSchedule: (jobName: string, data: { cronExpression: string; description?: string }) =>
|
|
postJson<{ success: boolean; message: string; data?: ScheduleResponse }>(
|
|
`${BASE}/schedules/${jobName}/update`, data),
|
|
|
|
deleteSchedule: (jobName: string) =>
|
|
postJson<{ success: boolean; message: string }>(`${BASE}/schedules/${jobName}/delete`),
|
|
|
|
toggleSchedule: (jobName: string, active: boolean) =>
|
|
postJson<{ success: boolean; message: string; data?: ScheduleResponse }>(
|
|
`${BASE}/schedules/${jobName}/toggle`, { active }),
|
|
|
|
// Timeline
|
|
getTimeline: (view: string, date: string) =>
|
|
fetchJson<TimelineResponse>(`${BASE}/timeline?view=${view}&date=${date}`),
|
|
|
|
getPeriodExecutions: (jobName: string, view: string, periodKey: string) =>
|
|
fetchJson<JobExecutionDto[]>(
|
|
`${BASE}/timeline/period-executions?jobName=${jobName}&view=${view}&periodKey=${periodKey}`),
|
|
|
|
// Recollection
|
|
searchRecollections: (params: {
|
|
apiKey?: string;
|
|
jobName?: string;
|
|
status?: string;
|
|
fromDate?: string;
|
|
toDate?: string;
|
|
page?: number;
|
|
size?: number;
|
|
}) => {
|
|
const qs = new URLSearchParams();
|
|
if (params.apiKey) qs.set('apiKey', params.apiKey);
|
|
if (params.jobName) qs.set('jobName', params.jobName);
|
|
if (params.status) qs.set('status', params.status);
|
|
if (params.fromDate) qs.set('fromDate', params.fromDate);
|
|
if (params.toDate) qs.set('toDate', params.toDate);
|
|
qs.set('page', String(params.page ?? 0));
|
|
qs.set('size', String(params.size ?? 20));
|
|
return fetchJson<RecollectionSearchResponse>(`${BASE}/recollection-histories?${qs.toString()}`);
|
|
},
|
|
|
|
getStepApiLogs: (stepExecutionId: number, params?: {
|
|
page?: number; size?: number; status?: ApiLogStatus;
|
|
}) => {
|
|
const qs = new URLSearchParams();
|
|
qs.set('page', String(params?.page ?? 0));
|
|
qs.set('size', String(params?.size ?? 50));
|
|
if (params?.status && params.status !== 'ALL') qs.set('status', params.status);
|
|
return fetchJson<ApiLogPageResponse>(
|
|
`${BASE}/steps/${stepExecutionId}/api-logs?${qs.toString()}`);
|
|
},
|
|
|
|
getRecollectionDetail: (historyId: number) =>
|
|
fetchJson<RecollectionDetailResponse>(`${BASE}/recollection-histories/${historyId}`),
|
|
|
|
getRecollectionStats: () =>
|
|
fetchJson<RecollectionStatsResponse>(`${BASE}/recollection-histories/stats`),
|
|
|
|
getCollectionPeriods: () =>
|
|
fetchJson<CollectionPeriodDto[]>(`${BASE}/collection-periods`),
|
|
|
|
resetCollectionPeriod: (apiKey: string) =>
|
|
postJson<{ success: boolean; message: string }>(`${BASE}/collection-periods/${apiKey}/reset`),
|
|
|
|
updateCollectionPeriod: (apiKey: string, body: { rangeFromDate: string; rangeToDate: string }) =>
|
|
postJson<{ success: boolean; message: string }>(`${BASE}/collection-periods/${apiKey}/update`, body),
|
|
|
|
// Last Collection Status
|
|
getLastCollectionStatuses: () =>
|
|
fetchJson<LastCollectionStatusDto[]>(`${BASE}/last-collections`),
|
|
|
|
// Display Names
|
|
getDisplayNames: () =>
|
|
fetchJson<JobDisplayName[]>(`${BASE}/display-names`),
|
|
|
|
resolveFailedRecords: (ids: number[]) =>
|
|
postJson<{ success: boolean; message: string; resolvedCount?: number }>(
|
|
`${BASE}/failed-records/resolve`, { ids }),
|
|
|
|
resetRetryCount: (ids: number[]) =>
|
|
postJson<{ success: boolean; message: string; resetCount?: number }>(
|
|
`${BASE}/failed-records/reset-retry`, { ids }),
|
|
|
|
exportRecollectionHistories: (params: {
|
|
apiKey?: string;
|
|
jobName?: string;
|
|
status?: string;
|
|
fromDate?: string;
|
|
toDate?: string;
|
|
}) => {
|
|
const qs = new URLSearchParams();
|
|
if (params.apiKey) qs.set('apiKey', params.apiKey);
|
|
if (params.jobName) qs.set('jobName', params.jobName);
|
|
if (params.status) qs.set('status', params.status);
|
|
if (params.fromDate) qs.set('fromDate', params.fromDate);
|
|
if (params.toDate) qs.set('toDate', params.toDate);
|
|
window.open(`${BASE}/recollection-histories/export?${qs.toString()}`);
|
|
},
|
|
};
|