// 배치관리 API 클라이언트 (새로운 API로 업데이트) // 작성일: 2024-12-24 import { apiClient } from "./client"; import { isCrossTenantMode } from "@/lib/auth/crossTenantMode"; export type BatchExecutionType = "mapping" | "node_flow"; export interface BatchConfig { id?: number; batch_name: string; description?: string; cron_schedule: string; is_active?: string; company_code?: string; save_mode?: 'INSERT' | 'UPSERT'; conflict_key?: string; auth_service_name?: string; execution_type?: BatchExecutionType; node_flow_id?: number; node_flow_context?: Record; created_date?: Date; created_by?: string; updated_date?: Date; updated_by?: string; batch_mappings?: BatchMapping[]; last_status?: string; last_executed_at?: string; last_total_records?: number; } export interface NodeFlowInfo { flow_id: number; flow_name: string; description?: string; company_code?: string; node_count: number; } // 백엔드 mapper (batchManagement.xml) 가 snake_case 로 응답하므로 그대로 사용한다. // 프로젝트 컨벤션: Map 키 = snake_case (CLAUDE.md 백엔드 규칙 참조) export interface BatchStats { total_count: number; active_count: number; today_count: number; today_failed_count: number; yesterday_count: number; yesterday_failed_count: number; } export interface SparklineData { hour_slot: string; total_count: number; success_count: number; failed_count: number; } export interface RecentLog { id: number; started_at: string; finished_at: string | null; status: string; total_records: number; success_records: number; failed_records: number; error_message: string | null; duration_ms: number | null; } // 조건 변환 규칙 — mapping_type === 'conditional' 일 때 mapping_config 에 저장. // 평가: row[from_column_name] 값을 cfg.rules 의 when 과 문자열 동등 비교, 매칭되는 then 반환. // 매칭 없으면 cfg.default. export interface ConditionalRule { when: string; then: string; } export interface ConditionalConfig { rules: ConditionalRule[]; default: string; } export interface BatchMapping { id?: number; batch_config_id?: number; // FROM 정보 from_connection_type: 'internal' | 'external'; from_connection_id?: number; from_table_name: string; from_column_name: string; from_column_type?: string; // TO 정보 to_connection_type: 'internal' | 'external'; to_connection_id?: number; to_table_name: string; to_column_name: string; to_column_type?: string; // 매핑 유형 — 'direct' (그대로 복사) / 'fixed' (from_column_name 자체가 고정값) / 'conditional' (when/then 룰) mapping_type?: 'direct' | 'fixed' | 'conditional'; // conditional 일 때 ConditionalConfig 의 JSON. 백엔드는 JSONB 로 저장. // 요청 시 string(JSON) 또는 object 둘 다 허용 — 백엔드가 normalize. mapping_config?: ConditionalConfig | string | null; mapping_order?: number; created_date?: Date; created_by?: string; } export interface BatchConfigFilter { batch_name?: string; is_active?: string; company_code?: string; search?: string; page?: number; limit?: number; } export interface BatchJob { id: number; job_name: string; job_type: string; description?: string; cron_schedule: string; schedule_cron?: string; is_active: string; last_execution?: Date; last_executed_at?: string; next_execution?: Date; execution_count?: number; success_count?: number; failure_count?: number; status?: string; created_date?: Date; created_by?: string; } export interface ConnectionInfo { type: 'internal' | 'external'; id?: number; name: string; db_type?: string; } export interface ColumnInfo { column_name: string; data_type: string; is_nullable?: boolean; column_default?: string; } export interface TableInfo { table_name: string; columns: ColumnInfo[]; description?: string | null; } export interface BatchMappingRequest { batchName: string; description?: string; cronSchedule: string; mappings: BatchMapping[]; isActive?: boolean; executionType?: BatchExecutionType; nodeFlowId?: number; nodeFlowContext?: Record; } export interface ApiResponse { success: boolean; data?: T; message?: string; error?: string; } export class BatchAPI { private static readonly BASE_PATH = ""; /** * 배치 설정 목록 조회 * Spring 백엔드: GET /api/batch-management/batch-configs → { success, data: { list, total_count, ... } } * Node.js 백엔드: GET /api/batch-configs → { success, data: [...], pagination: { ... } } * 두 백엔드 응답 형식 모두 호환되도록 처리 */ static async getBatchConfigs(filter: BatchConfigFilter = {}): Promise<{ success: boolean; data: BatchConfig[]; pagination?: { page: number; limit: number; total: number; totalPages: number; }; message?: string; }> { try { const params = new URLSearchParams(); if (filter.is_active) params.append("is_active", filter.is_active); if (filter.company_code) params.append("company_code", filter.company_code); if (filter.search) params.append("search", filter.search); if (filter.page) params.append("page", filter.page.toString()); if (filter.limit) params.append("limit", filter.limit.toString()); // SUPER_ADMIN 전사 모드 → cross-tenant fan-out (행마다 company_code 박힘) if (isCrossTenantMode()) { const ctResponse = await apiClient.get( `/admin/cross-tenant/batches?${params.toString()}` ); const ct = ctResponse.data; if (ct && ct.success && ct.data) { const rows: BatchConfig[] = ct.data.rows || []; return { success: true, data: rows, // cross-tenant 1차는 페이지네이션 비지원 — 클라이언트 기준 1페이지로 표시 pagination: { page: 1, limit: rows.length, total: rows.length, totalPages: 1, }, message: ct.message, // CrossTenantBanner 메타 — truncated/failed 안내 박스용 cross_tenant_meta: { companies_queried: ct.data.companies_queried, companies_failed: ct.data.companies_failed, failed_company_codes: ct.data.failed_company_codes ?? [], truncated: ct.data.truncated, truncated_company_codes: ct.data.truncated_company_codes ?? [], per_company_limit: ct.data.per_company_limit, }, } as any; } } const response = await apiClient.get( `/batch-management/batch-configs?${params.toString()}` ); const raw = response.data; // Spring 응답: { success, data: { list: [...], total_count, ... } } // Node.js 응답: { success, data: [...], pagination: { ... } } let data: BatchConfig[]; let pagination: { page: number; limit: number; total: number; totalPages: number } | undefined; if (raw.data && Array.isArray(raw.data)) { // Node.js 형식: data가 직접 배열 data = raw.data; pagination = raw.pagination; } else if (raw.data && (raw.data.list || raw.data.data)) { // Spring 형식: data가 객체 (list 또는 data 키에 배열) data = raw.data.list || raw.data.data || []; pagination = { page: raw.data.page || 1, limit: raw.data.limit || data.length, total: raw.data.total_count ?? raw.data.total ?? data.length, totalPages: raw.data.total_page ?? raw.data.totalPages ?? 1, }; } else { data = []; } return { success: raw.success ?? true, data, pagination, message: raw.message, }; } catch (error) { console.error("배치 설정 목록 조회 오류:", error); return { success: false, data: [], message: error instanceof Error ? error.message : "배치 설정 목록 조회에 실패했습니다." }; } } /** * 특정 배치 설정 조회 (별칭) */ static async getBatchConfig(id: number): Promise { return this.getBatchConfigById(id); } /** * 특정 배치 설정 조회 */ static async getBatchConfigById(id: number): Promise { try { const response = await apiClient.get>( `/batch-management/batch-configs/${id}`, ); if (!response.data.success) { throw new Error(response.data.message || "배치 설정 조회에 실패했습니다."); } if (!response.data.data) { throw new Error("배치 설정을 찾을 수 없습니다."); } return response.data.data; } catch (error) { console.error("배치 설정 조회 오류:", error); throw error; } } /** * 배치 설정 생성 */ static async createBatchConfig(data: BatchMappingRequest): Promise { try { const response = await apiClient.post>( `/batch-management/batch-configs`, data, ); if (!response.data.success) { throw new Error(response.data.message || "배치 설정 생성에 실패했습니다."); } if (!response.data.data) { throw new Error("배치 설정 생성 결과를 받을 수 없습니다."); } return response.data.data; } catch (error) { console.error("배치 설정 생성 오류:", error); throw error; } } /** * 배치 설정 수정 */ static async updateBatchConfig( id: number, data: Partial ): Promise { try { const response = await apiClient.put>( `/batch-management/batch-configs/${id}`, data, ); if (!response.data.success) { throw new Error(response.data.message || "배치 설정 수정에 실패했습니다."); } if (!response.data.data) { throw new Error("배치 설정 수정 결과를 받을 수 없습니다."); } return response.data.data; } catch (error) { console.error("배치 설정 수정 오류:", error); throw error; } } /** * 배치 설정 삭제 */ static async deleteBatchConfig(id: number): Promise { try { const response = await apiClient.delete>( `/batch-management/batch-configs/${id}`, ); if (!response.data.success) { throw new Error(response.data.message || "배치 설정 삭제에 실패했습니다."); } } catch (error) { console.error("배치 설정 삭제 오류:", error); throw error; } } /** * 사용 가능한 커넥션 목록 조회 */ static async getConnections(): Promise { try { console.log("[BatchAPI] getAvailableConnections 호출 시작"); console.log("[BatchAPI] API URL:", `/batch-management/connections`); const response = await apiClient.get>( `/batch-management/connections`, ); console.log("[BatchAPI] API 응답:", response); console.log("[BatchAPI] 응답 데이터:", response.data); if (!response.data.success) { console.error("[BatchAPI] API 응답 실패:", response.data); throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다."); } const result = response.data.data || []; console.log("[BatchAPI] 최종 결과:", result); return result; } catch (error) { console.error("[BatchAPI] 커넥션 목록 조회 오류:", error); console.error("[BatchAPI] 오류 상세:", { message: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined }); throw error; } } /** * 특정 커넥션의 테이블 목록 조회 */ static async getTablesFromConnection( connection: ConnectionInfo ): Promise { try { let url = `/batch-management/connections/${connection.type}`; if (connection.type === 'external' && connection.id) { url += `/${connection.id}`; } url += '/tables'; const response = await apiClient.get>(url); if (!response.data.success) { throw new Error(response.data.message || "테이블 목록 조회에 실패했습니다."); } // TableInfo[]에서 table_name만 추출하여 string[]로 변환 const tables = response.data.data || []; return tables.map(table => table.table_name); } catch (error) { console.error("테이블 목록 조회 오류:", error); throw error; } } /** * 특정 테이블의 컬럼 정보 조회 */ static async getTableColumns( connection: ConnectionInfo, tableName: string ): Promise { try { let url = `/batch-management/connections/${connection.type}`; if (connection.type === 'external' && connection.id) { url += `/${connection.id}`; } url += `/tables/${tableName}/columns`; const response = await apiClient.get>(url); if (!response.data.success) { throw new Error(response.data.message || "컬럼 정보 조회에 실패했습니다."); } return response.data.data || []; } catch (error) { console.error("컬럼 정보 조회 오류:", error); throw error; } } /** * 배치 작업 목록 조회 */ static async getBatchJobs(): Promise { try { const response = await apiClient.get>('/batch-management/jobs'); if (!response.data.success) { throw new Error(response.data.message || "배치 작업 목록 조회에 실패했습니다."); } return response.data.data || []; } catch (error) { console.error("배치 작업 목록 조회 오류:", error); throw error; } } /** * 배치 수동 실행 */ static async executeBatchConfig(batchId: number): Promise<{ success: boolean; message?: string; data?: { batchId: string; totalRecords: number; successRecords: number; failedRecords: number; duration: number; }; }> { try { const response = await apiClient.post<{ success: boolean; message?: string; data?: { batchId: string; totalRecords: number; successRecords: number; failedRecords: number; duration: number; }; }>(`/batch-management/batch-configs/${batchId}/execute`); return response.data; } catch (error) { console.error("배치 실행 오류:", error); throw error; } } /** * 지원되는 배치 작업 타입 조회 */ static async getSupportedJobTypes(): Promise { try { const response = await apiClient.get>('/batch-management/job-types'); if (!response.data.success) { throw new Error(response.data.message || "작업 타입 조회에 실패했습니다."); } return response.data.data || []; } catch (error) { console.error("작업 타입 조회 오류:", error); return []; } } /** * 배치 작업 삭제 */ static async deleteBatchJob(id: number): Promise { try { const response = await apiClient.delete>(`/batch-management/jobs/${id}`); if (!response.data.success) { throw new Error(response.data.message || "배치 작업 삭제에 실패했습니다."); } } catch (error) { console.error("배치 작업 삭제 오류:", error); throw error; } } /** * 배치 작업 실행 */ static async executeBatchJob(id: number): Promise { try { const response = await apiClient.post>(`/batch-management/jobs/${id}/execute`); if (!response.data.success) { throw new Error(response.data.message || "배치 작업 실행에 실패했습니다."); } } catch (error) { console.error("배치 작업 실행 오류:", error); throw error; } } /** * auth_tokens 테이블의 서비스명 목록 조회 */ static async getAuthServiceNames(): Promise { try { const response = await apiClient.get<{ success: boolean; data: string[]; }>(`/batch-management/auth-services`); if (response.data.success) { return response.data.data || []; } return []; } catch (error) { console.error("인증 서비스 목록 조회 오류:", error); return []; } } /** * 노드 플로우 목록 조회 (배치 설정 시 플로우 선택용) */ static async getNodeFlows(): Promise { try { const response = await apiClient.get>( `/batch-management/node-flows` ); if (response.data.success) { return response.data.data || []; } return []; } catch (error) { console.error("노드 플로우 목록 조회 오류:", error); return []; } } /** * 배치 대시보드 통계 조회 */ static async getBatchStats(): Promise { try { const response = await apiClient.get>( `/batch-management/stats` ); if (response.data.success) { return response.data.data || null; } return null; } catch (error) { console.error("배치 통계 조회 오류:", error); return null; } } /** * 회사 전체 배치의 최근 24시간 스파크라인 데이터 (24개 슬롯 고정) */ static async getGlobalSparkline(): Promise { try { const response = await apiClient.get>( `/batch-management/sparkline` ); if (response.data.success) { return response.data.data || []; } return []; } catch (error) { console.error("글로벌 스파크라인 조회 오류:", error); return []; } } /** * 배치별 최근 24시간 스파크라인 데이터 */ static async getBatchSparkline(batchId: number): Promise { try { const response = await apiClient.get>( `/batch-management/batch-configs/${batchId}/sparkline` ); if (response.data.success) { return response.data.data || []; } return []; } catch (error) { console.error("스파크라인 조회 오류:", error); return []; } } /** * 배치별 최근 실행 로그 */ static async getBatchRecentLogs(batchId: number, limit: number = 5): Promise { try { const response = await apiClient.get>( `/batch-management/batch-configs/${batchId}/recent-logs?limit=${limit}` ); if (response.data.success) { return response.data.data || []; } return []; } catch (error) { console.error("최근 실행 로그 조회 오류:", error); return []; } } }