"use client"; import React, { useState, useEffect, useMemo, memo } 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, ArrowLeft, 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; } // 통합 매핑 아이템 타입 interface MappingItem { id: string; dbColumn: string; sourceType: "api" | "fixed"; apiField: string; fixedValue: string; } interface RestApiToDbMappingCardProps { fromApiFields: string[]; toColumns: BatchColumnInfo[]; fromApiData: any[]; mappingList: MappingItem[]; setMappingList: React.Dispatch>; } interface DbToRestApiMappingCardProps { fromColumns: BatchColumnInfo[]; selectedColumns: string[]; toApiFields: string[]; dbToApiFieldMapping: Record; setDbToApiFieldMapping: React.Dispatch>>; setToApiBody: (body: string) => void; } export default function BatchManagementNewPage() { const router = useRouter(); // 기본 상태 const [batchName, setBatchName] = useState(""); const [cronSchedule, setCronSchedule] = useState("0 12 * * *"); const [description, setDescription] = useState(""); // 인증 토큰 설정 const [authTokenMode, setAuthTokenMode] = useState<"direct" | "db">("direct"); // 직접입력 / DB에서 선택 const [authServiceName, setAuthServiceName] = useState(""); const [authServiceNames, setAuthServiceNames] = 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" | "POST" | "PUT" | "DELETE">("GET"); const [fromApiBody, setFromApiBody] = useState(""); // Request Body (JSON) const [dataArrayPath, setDataArrayPath] = useState(""); // 데이터 배열 경로 (예: response, data.items) // REST API 파라미터 설정 const [apiParamType, setApiParamType] = useState<"none" | "url" | "query">("none"); const [apiParamName, setApiParamName] = useState(""); // 파라미터명 (예: userId, id) const [apiParamValue, setApiParamValue] = useState(""); // 파라미터 값 또는 템플릿 const [apiParamSource, setApiParamSource] = useState<"static" | "dynamic">("static"); // 정적 값 또는 동적 값 // 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([]); // 통합 매핑 리스트 const [mappingList, setMappingList] = useState([]); // INSERT/UPSERT 설정 const [saveMode, setSaveMode] = useState<"INSERT" | "UPSERT">("INSERT"); const [conflictKey, setConflictKey] = 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(); loadAuthServiceNames(); }, []); // 인증 서비스명 목록 로드 const loadAuthServiceNames = async () => { try { const serviceNames = await BatchManagementAPI.getAuthServiceNames(); setAuthServiceNames(serviceNames); } catch (error) { console.error("인증 서비스 목록 로드 실패:", error); } }; // 배치 타입 변경 시 상태 초기화 useEffect(() => { // 공통 초기화 setMappingList([]); // 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) => { setToTable(tableName); setToColumns([]); if (toConnection && tableName) { try { const connectionType = toConnection.type === "internal" ? "internal" : "external"; const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, toConnection.id); if (result && result.length > 0) { setToColumns(result); } else { setToColumns([]); } } 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) => { setFromTable(tableName); setFromColumns([]); setSelectedColumns([]); // 선택된 컬럼도 초기화 setDbToApiFieldMapping({}); // 매핑도 초기화 if (fromConnection && tableName) { try { const connectionType = fromConnection.type === "internal" ? "internal" : "external"; const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, fromConnection.id); if (result && result.length > 0) { setFromColumns(result); } else { setFromColumns([]); } } 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 { const result = await BatchManagementAPI.previewRestApiData( toApiUrl, toApiKey, toEndpoint, "GET", // 미리보기는 항상 GET으로 ); 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 () => { // API URL, 엔드포인트는 항상 필수 if (!fromApiUrl || !fromEndpoint) { toast.error("API URL과 엔드포인트를 모두 입력해주세요."); return; } // 직접 입력 모드일 때만 토큰 검증 if (authTokenMode === "direct" && !fromApiKey) { toast.error("인증 토큰을 입력해주세요."); return; } // DB 선택 모드일 때 서비스명 검증 if (authTokenMode === "db" && !authServiceName) { toast.error("인증 토큰 서비스를 선택해주세요."); return; } try { const result = await BatchManagementAPI.previewRestApiData( fromApiUrl, authTokenMode === "direct" ? fromApiKey : "", // 직접 입력일 때만 API 키 전달 fromEndpoint, fromApiMethod, // 파라미터 정보 추가 apiParamType !== "none" ? { paramType: apiParamType, paramName: apiParamName, paramValue: apiParamValue, paramSource: apiParamSource, } : undefined, // Request Body 추가 (POST/PUT/DELETE) fromApiMethod === "POST" || fromApiMethod === "PUT" || fromApiMethod === "DELETE" ? fromApiBody : undefined, // DB 선택 모드일 때 서비스명 전달 authTokenMode === "db" ? authServiceName : undefined, // 데이터 배열 경로 전달 dataArrayPath || undefined, ); if (result.fields && result.fields.length > 0) { setFromApiFields(result.fields); setFromApiData(result.samples); toast.success(`API 데이터 미리보기 완료! ${result.fields.length}개 필드, ${result.totalCount}개 레코드`); } else if (result.samples && result.samples.length > 0) { // 백엔드에서 fields를 제대로 보내지 않은 경우, 프론트엔드에서 직접 추출 const extractedFields = Object.keys(result.samples[0]); setFromApiFields(extractedFields); setFromApiData(result.samples); toast.success(`API 데이터 미리보기 완료! ${extractedFields.length}개 필드, ${result.samples.length}개 레코드`); } else { 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") { // 유효한 매핑만 필터링 (DB 컬럼이 선택되고, API 필드 또는 고정값이 있는 것) const validMappings = mappingList.filter( (m) => m.dbColumn && (m.sourceType === "api" ? m.apiField : m.fixedValue), ); if (validMappings.length === 0) { toast.error("최소 하나의 매핑을 설정해주세요."); return; } // UPSERT 모드일 때 conflict key 검증 if (saveMode === "UPSERT" && !conflictKey) { toast.error("UPSERT 모드에서는 충돌 기준 컬럼을 선택해주세요."); return; } // 통합 매핑 리스트를 배치 매핑 형태로 변환 // 고정값 매핑도 동일한 from_connection_type을 사용해야 같은 그룹으로 처리됨 const apiMappings = validMappings.map((mapping) => ({ from_connection_type: "restapi" as const, // 고정값도 동일한 소스 타입 사용 from_table_name: fromEndpoint, from_column_name: mapping.sourceType === "api" ? mapping.apiField : mapping.fixedValue, from_api_url: fromApiUrl, from_api_key: authTokenMode === "direct" ? fromApiKey : "", from_api_method: fromApiMethod, from_api_body: fromApiMethod === "POST" || fromApiMethod === "PUT" || fromApiMethod === "DELETE" ? fromApiBody : undefined, from_api_param_type: apiParamType !== "none" ? apiParamType : undefined, from_api_param_name: apiParamType !== "none" ? apiParamName : undefined, from_api_param_value: apiParamType !== "none" ? apiParamValue : undefined, from_api_param_source: apiParamType !== "none" ? apiParamSource : undefined, to_connection_type: toConnection?.type === "internal" ? "internal" : "external", to_connection_id: toConnection?.type === "internal" ? undefined : toConnection?.id, to_table_name: toTable, to_column_name: mapping.dbColumn, mapping_type: mapping.sourceType === "fixed" ? ("fixed" as const) : ("direct" as const), fixed_value: mapping.sourceType === "fixed" ? mapping.fixedValue : undefined, })); // 실제 API 호출 try { const result = await BatchManagementAPI.saveRestApiBatch({ batchName, batchType, cronSchedule, description, apiMappings, authServiceName: authTokenMode === "db" ? authServiceName : undefined, dataArrayPath: dataArrayPath || undefined, saveMode, conflictKey: saveMode === "UPSERT" ? conflictKey : undefined, }); 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, // 마지막 순서 }); } } // 실제 API 호출 (기존 saveRestApiBatch 재사용) try { const result = await BatchManagementAPI.saveRestApiBatch({ batchName, batchType, cronSchedule, description, apiMappings: dbMappings, authServiceName: authServiceName || undefined, }); 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 * * *" />