"use client"; import React, { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Trash2, Plus, ArrowRight, Save, RefreshCw, Globe, Database, Eye } from "lucide-react"; import { toast } from "sonner"; import { BatchManagementAPI } from "@/lib/api/batchManagement"; // 타입 정의 type BatchType = 'db-to-restapi' | 'restapi-to-db' | 'restapi-to-restapi'; interface BatchTypeOption { value: BatchType; label: string; description: string; } interface BatchConnectionInfo { id: number; name: string; type: string; } interface BatchColumnInfo { column_name: string; data_type: string; is_nullable: string; } export default function BatchManagementNewPage() { const router = useRouter(); // 기본 상태 const [batchName, setBatchName] = useState(""); const [cronSchedule, setCronSchedule] = useState("0 12 * * *"); const [description, setDescription] = useState(""); // 연결 정보 const [connections, setConnections] = useState([]); const [toConnection, setToConnection] = useState(null); const [toTables, setToTables] = useState([]); const [toTable, setToTable] = useState(""); const [toColumns, setToColumns] = useState([]); // REST API 설정 (REST API → DB용) const [fromApiUrl, setFromApiUrl] = useState(""); const [fromApiKey, setFromApiKey] = useState(""); const [fromEndpoint, setFromEndpoint] = useState(""); const [fromApiMethod, setFromApiMethod] = useState<'GET'>('GET'); // GET만 지원 // DB → REST API용 상태 const [fromConnection, setFromConnection] = useState(null); const [fromTables, setFromTables] = useState([]); const [fromTable, setFromTable] = useState(""); const [fromColumns, setFromColumns] = useState([]); const [selectedColumns, setSelectedColumns] = useState([]); // 선택된 컬럼들 const [dbToApiFieldMapping, setDbToApiFieldMapping] = useState>({}); // DB 컬럼 → API 필드 매핑 // REST API 대상 설정 (DB → REST API용) const [toApiUrl, setToApiUrl] = useState(""); const [toApiKey, setToApiKey] = useState(""); const [toEndpoint, setToEndpoint] = useState(""); const [toApiMethod, setToApiMethod] = useState<'POST' | 'PUT' | 'DELETE'>('POST'); const [toApiBody, setToApiBody] = useState(''); // Request Body 템플릿 const [toApiFields, setToApiFields] = useState([]); // TO API 필드 목록 const [urlPathColumn, setUrlPathColumn] = useState(""); // URL 경로에 사용할 컬럼 (PUT/DELETE용) // API 데이터 미리보기 const [fromApiData, setFromApiData] = useState([]); const [fromApiFields, setFromApiFields] = useState([]); // API 필드 → DB 컬럼 매핑 const [apiFieldMappings, setApiFieldMappings] = useState>({}); // 배치 타입 상태 const [batchType, setBatchType] = useState('restapi-to-db'); // 배치 타입 옵션 const batchTypeOptions: BatchTypeOption[] = [ { value: 'restapi-to-db', label: 'REST API → DB', description: 'REST API에서 데이터베이스로 데이터 수집' }, { value: 'db-to-restapi', label: 'DB → REST API', description: '데이터베이스에서 REST API로 데이터 전송' } ]; // 초기 데이터 로드 useEffect(() => { loadConnections(); }, []); // 배치 타입 변경 시 상태 초기화 useEffect(() => { // 공통 초기화 setApiFieldMappings({}); // REST API → DB 관련 초기화 setToConnection(null); setToTables([]); setToTable(""); setToColumns([]); setFromApiUrl(""); setFromApiKey(""); setFromEndpoint(""); setFromApiData([]); setFromApiFields([]); // DB → REST API 관련 초기화 setFromConnection(null); setFromTables([]); setFromTable(""); setFromColumns([]); setSelectedColumns([]); setDbToApiFieldMapping({}); setToApiUrl(""); setToApiKey(""); setToEndpoint(""); setToApiBody(""); setToApiFields([]); }, [batchType]); // 연결 목록 로드 const loadConnections = async () => { try { const result = await BatchManagementAPI.getAvailableConnections(); setConnections(result || []); } catch (error) { console.error("연결 목록 로드 오류:", error); toast.error("연결 목록을 불러오는데 실패했습니다."); } }; // TO 연결 변경 핸들러 const handleToConnectionChange = async (connectionValue: string) => { let connection: BatchConnectionInfo | null = null; if (connectionValue === 'internal') { // 내부 데이터베이스 선택 connection = connections.find(conn => conn.type === 'internal') || null; } else { // 외부 데이터베이스 선택 const connectionId = parseInt(connectionValue); connection = connections.find(conn => conn.id === connectionId) || null; } setToConnection(connection); setToTable(""); setToColumns([]); if (connection) { try { const connectionType = connection.type === 'internal' ? 'internal' : 'external'; const result = await BatchManagementAPI.getTablesFromConnection(connectionType, connection.id); const tableNames = Array.isArray(result) ? result.map((table: any) => typeof table === 'string' ? table : table.table_name || String(table)) : []; setToTables(tableNames); } catch (error) { console.error("테이블 목록 로드 오류:", error); toast.error("테이블 목록을 불러오는데 실패했습니다."); } } }; // TO 테이블 변경 핸들러 const handleToTableChange = async (tableName: string) => { console.log("🔍 테이블 변경:", { tableName, toConnection }); setToTable(tableName); setToColumns([]); if (toConnection && tableName) { try { const connectionType = toConnection.type === 'internal' ? 'internal' : 'external'; console.log("🔍 컬럼 조회 시작:", { connectionType, connectionId: toConnection.id, tableName }); const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, toConnection.id); console.log("🔍 컬럼 조회 결과:", result); if (result && result.length > 0) { setToColumns(result); console.log("✅ 컬럼 설정 완료:", result.length, "개"); } else { setToColumns([]); console.log("⚠️ 컬럼이 없음"); } } catch (error) { console.error("❌ 컬럼 목록 로드 오류:", error); toast.error("컬럼 목록을 불러오는데 실패했습니다."); setToColumns([]); } } }; // FROM 연결 변경 핸들러 (DB → REST API용) const handleFromConnectionChange = async (connectionValue: string) => { let connection: BatchConnectionInfo | null = null; if (connectionValue === 'internal') { connection = connections.find(conn => conn.type === 'internal') || null; } else { const connectionId = parseInt(connectionValue); connection = connections.find(conn => conn.id === connectionId) || null; } setFromConnection(connection); setFromTable(""); setFromColumns([]); if (connection) { try { const connectionType = connection.type === 'internal' ? 'internal' : 'external'; const result = await BatchManagementAPI.getTablesFromConnection(connectionType, connection.id); const tableNames = Array.isArray(result) ? result.map((table: any) => typeof table === 'string' ? table : table.table_name || String(table)) : []; setFromTables(tableNames); } catch (error) { console.error("테이블 목록 로드 오류:", error); toast.error("테이블 목록을 불러오는데 실패했습니다."); } } }; // FROM 테이블 변경 핸들러 (DB → REST API용) const handleFromTableChange = async (tableName: string) => { console.log("🔍 FROM 테이블 변경:", { tableName, fromConnection }); setFromTable(tableName); setFromColumns([]); setSelectedColumns([]); // 선택된 컬럼도 초기화 setDbToApiFieldMapping({}); // 매핑도 초기화 if (fromConnection && tableName) { try { const connectionType = fromConnection.type === 'internal' ? 'internal' : 'external'; console.log("🔍 FROM 컬럼 조회 시작:", { connectionType, connectionId: fromConnection.id, tableName }); const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, fromConnection.id); console.log("🔍 FROM 컬럼 조회 결과:", result); if (result && result.length > 0) { setFromColumns(result); console.log("✅ FROM 컬럼 설정 완료:", result.length, "개"); } else { setFromColumns([]); console.log("⚠️ FROM 컬럼이 없음"); } } catch (error) { console.error("❌ FROM 컬럼 목록 로드 오류:", error); toast.error("컬럼 목록을 불러오는데 실패했습니다."); setFromColumns([]); } } }; // TO API 미리보기 (DB → REST API용) const previewToApiData = async () => { if (!toApiUrl || !toApiKey || !toEndpoint) { toast.error("API URL, API Key, 엔드포인트를 모두 입력해주세요."); return; } try { console.log("🔍 TO API 미리보기 시작:", { toApiUrl, toApiKey, toEndpoint, toApiMethod }); const result = await BatchManagementAPI.previewRestApiData( toApiUrl, toApiKey, toEndpoint, 'GET' // 미리보기는 항상 GET으로 ); console.log("🔍 TO API 미리보기 결과:", result); if (result.fields && result.fields.length > 0) { setToApiFields(result.fields); toast.success(`TO API 필드 ${result.fields.length}개를 조회했습니다.`); } else { setToApiFields([]); toast.warning("TO API에서 필드를 찾을 수 없습니다."); } } catch (error) { console.error("❌ TO API 미리보기 오류:", error); toast.error("TO API 미리보기에 실패했습니다."); setToApiFields([]); } }; // REST API 데이터 미리보기 const previewRestApiData = async () => { if (!fromApiUrl || !fromApiKey || !fromEndpoint) { toast.error("API URL, API Key, 엔드포인트를 모두 입력해주세요."); return; } try { console.log("REST API 데이터 미리보기 시작..."); const result = await BatchManagementAPI.previewRestApiData( fromApiUrl, fromApiKey, fromEndpoint, fromApiMethod ); console.log("API 미리보기 결과:", result); console.log("result.fields:", result.fields); console.log("result.samples:", result.samples); console.log("result.totalCount:", result.totalCount); if (result.fields && result.fields.length > 0) { console.log("✅ 백엔드에서 fields 제공됨:", result.fields); setFromApiFields(result.fields); setFromApiData(result.samples); console.log("추출된 필드:", result.fields); toast.success(`API 데이터 미리보기 완료! ${result.fields.length}개 필드, ${result.totalCount}개 레코드`); } else if (result.samples && result.samples.length > 0) { // 백엔드에서 fields를 제대로 보내지 않은 경우, 프론트엔드에서 직접 추출 console.log("⚠️ 백엔드에서 fields가 없어서 프론트엔드에서 추출"); const extractedFields = Object.keys(result.samples[0]); console.log("프론트엔드에서 추출한 필드:", extractedFields); setFromApiFields(extractedFields); setFromApiData(result.samples); toast.success(`API 데이터 미리보기 완료! ${extractedFields.length}개 필드, ${result.samples.length}개 레코드`); } else { console.log("❌ 데이터가 없음"); setFromApiFields([]); setFromApiData([]); toast.warning("API에서 데이터를 가져올 수 없습니다."); } } catch (error) { console.error("REST API 미리보기 오류:", error); toast.error("API 데이터 미리보기에 실패했습니다."); setFromApiFields([]); setFromApiData([]); } }; // 배치 설정 저장 const handleSave = async () => { if (!batchName.trim()) { toast.error("배치명을 입력해주세요."); return; } // 배치 타입별 검증 및 저장 if (batchType === 'restapi-to-db') { const mappedFields = Object.keys(apiFieldMappings).filter(field => apiFieldMappings[field]); if (mappedFields.length === 0) { toast.error("최소 하나의 API 필드를 DB 컬럼에 매핑해주세요."); return; } // API 필드 매핑을 배치 매핑 형태로 변환 const apiMappings = mappedFields.map(apiField => ({ from_connection_type: 'restapi' as const, from_table_name: fromEndpoint, // API 엔드포인트 from_column_name: apiField, // API 필드명 from_api_url: fromApiUrl, from_api_key: fromApiKey, from_api_method: fromApiMethod, to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external', to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id, to_table_name: toTable, to_column_name: apiFieldMappings[apiField], // 매핑된 DB 컬럼 mapping_type: 'direct' as const })); console.log("REST API 배치 설정 저장:", { batchName, batchType, cronSchedule, description, apiMappings }); // 실제 API 호출 try { const result = await BatchManagementAPI.saveRestApiBatch({ batchName, batchType, cronSchedule, description, apiMappings }); if (result.success) { toast.success(result.message || "REST API 배치 설정이 저장되었습니다."); setTimeout(() => { router.push('/admin/batchmng'); }, 1000); } else { toast.error(result.message || "배치 저장에 실패했습니다."); } } catch (error) { console.error("배치 저장 오류:", error); toast.error("배치 저장 중 오류가 발생했습니다."); } return; } else if (batchType === 'db-to-restapi') { // DB → REST API 배치 검증 if (!fromConnection || !fromTable || selectedColumns.length === 0) { toast.error("소스 데이터베이스, 테이블, 컬럼을 선택해주세요."); return; } if (!toApiUrl || !toApiKey || !toEndpoint) { toast.error("대상 API URL, API Key, 엔드포인트를 입력해주세요."); return; } if ((toApiMethod === 'POST' || toApiMethod === 'PUT') && !toApiBody) { toast.error("POST/PUT 메서드의 경우 Request Body 템플릿을 입력해주세요."); return; } // DELETE의 경우 빈 Request Body라도 템플릿 로직을 위해 "{}" 설정 let finalToApiBody = toApiBody; if (toApiMethod === 'DELETE' && !finalToApiBody.trim()) { finalToApiBody = '{}'; } // DB → REST API 매핑 생성 (선택된 컬럼만) const selectedColumnObjects = fromColumns.filter(column => selectedColumns.includes(column.column_name)); const dbMappings = selectedColumnObjects.map((column, index) => ({ from_connection_type: fromConnection.type === 'internal' ? 'internal' : 'external', from_connection_id: fromConnection.type === 'internal' ? undefined : fromConnection.id, from_table_name: fromTable, from_column_name: column.column_name, from_column_type: column.data_type, to_connection_type: 'restapi' as const, to_table_name: toEndpoint, // API 엔드포인트 to_column_name: dbToApiFieldMapping[column.column_name] || column.column_name, // 매핑된 API 필드명 to_api_url: toApiUrl, to_api_key: toApiKey, to_api_method: toApiMethod, to_api_body: finalToApiBody, // Request Body 템플릿 mapping_type: 'template' as const, mapping_order: index + 1 })); // URL 경로 파라미터 매핑 추가 (PUT/DELETE용) if ((toApiMethod === 'PUT' || toApiMethod === 'DELETE') && urlPathColumn) { const urlPathColumnObject = fromColumns.find(col => col.column_name === urlPathColumn); if (urlPathColumnObject) { dbMappings.push({ from_connection_type: fromConnection.type === 'internal' ? 'internal' : 'external', from_connection_id: fromConnection.type === 'internal' ? undefined : fromConnection.id, from_table_name: fromTable, from_column_name: urlPathColumn, from_column_type: urlPathColumnObject.data_type, to_connection_type: 'restapi' as const, to_table_name: toEndpoint, to_column_name: 'URL_PATH_PARAM', // 특별한 식별자 to_api_url: toApiUrl, to_api_key: toApiKey, to_api_method: toApiMethod, to_api_body: finalToApiBody, mapping_type: 'url_path' as const, mapping_order: 999 // 마지막 순서 }); } } console.log("DB → REST API 배치 설정 저장:", { batchName, batchType, cronSchedule, description, dbMappings }); // 실제 API 호출 (기존 saveRestApiBatch 재사용) try { const result = await BatchManagementAPI.saveRestApiBatch({ batchName, batchType, cronSchedule, description, apiMappings: dbMappings }); if (result.success) { toast.success(result.message || "DB → REST API 배치 설정이 저장되었습니다."); setTimeout(() => { router.push('/admin/batchmng'); }, 1000); } else { toast.error(result.message || "배치 저장에 실패했습니다."); } } catch (error) { console.error("배치 저장 오류:", error); toast.error("배치 저장 중 오류가 발생했습니다."); } return; } toast.error("지원하지 않는 배치 타입입니다."); }; return (

고급 배치 생성

{/* 기본 정보 */} 기본 정보 {/* 배치 타입 선택 */}
{batchTypeOptions.map((option) => (
setBatchType(option.value)} >
{option.value === 'restapi-to-db' ? ( ) : ( )}
{option.label}
{option.description}
))}
setBatchName(e.target.value)} placeholder="배치명을 입력하세요" />
setCronSchedule(e.target.value)} placeholder="0 12 * * *" />