const BASE = import.meta.env.DEV ? '/snp-api/api/batch' : '/snp-api/api/batch'; async function fetchJson(url: string): Promise { const res = await fetch(url); if (!res.ok) throw new Error(`API Error: ${res.status} ${res.statusText}`); return res.json(); } async function postJson(url: string, body?: unknown): Promise { 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 | 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; 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; } 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; } 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(`${BASE}/dashboard`), getJobs: () => fetchJson(`${BASE}/jobs`), getJobsDetail: () => fetchJson(`${BASE}/jobs/detail`), executeJob: (jobName: string, params?: Record) => { 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(`${BASE}/jobs/${jobName}/executions`), getRecentExecutions: (limit = 50) => fetchJson(`${BASE}/executions/recent?limit=${limit}`), getExecutionDetail: (id: number) => fetchJson(`${BASE}/executions/${id}/detail`), stopExecution: (id: number) => postJson<{ success: boolean; message: string }>(`${BASE}/executions/${id}/stop`), // F1: Abandon getStaleExecutions: (thresholdMinutes = 60) => fetchJson(`${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(`${BASE}/executions/search?${qs.toString()}`); }, // F8: Statistics getStatistics: (days = 30) => fetchJson(`${BASE}/statistics?days=${days}`), getJobStatistics: (jobName: string, days = 30) => fetchJson(`${BASE}/statistics/${jobName}?days=${days}`), // Schedule getSchedules: () => fetchJson<{ schedules: ScheduleResponse[]; count: number }>(`${BASE}/schedules`), getSchedule: (jobName: string) => fetchJson(`${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(`${BASE}/timeline?view=${view}&date=${date}`), getPeriodExecutions: (jobName: string, view: string, periodKey: string) => fetchJson( `${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(`${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( `${BASE}/steps/${stepExecutionId}/api-logs?${qs.toString()}`); }, getRecollectionDetail: (historyId: number) => fetchJson(`${BASE}/recollection-histories/${historyId}`), getRecollectionStats: () => fetchJson(`${BASE}/recollection-histories/stats`), getCollectionPeriods: () => fetchJson(`${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(`${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 }), 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()}`); }, };