"use client"; import React, { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } 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 { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { RefreshCw, Save, ArrowLeft, Plus, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { BatchAPI, BatchConfig, BatchMapping, ConnectionInfo, } from "@/lib/api/batch"; import { BatchManagementAPI } from "@/lib/api/batchManagement"; interface BatchColumnInfo { column_name: string; data_type: string; is_nullable: string; } // 배치 타입 감지 함수 const detectBatchType = (mapping: BatchMapping): 'db-to-db' | 'restapi-to-db' | 'db-to-restapi' => { const fromType = mapping.from_connection_type; const toType = mapping.to_connection_type; if (fromType === 'restapi' && (toType === 'internal' || toType === 'external')) { return 'restapi-to-db'; } else if ((fromType === 'internal' || fromType === 'external') && toType === 'restapi') { return 'db-to-restapi'; } else { return 'db-to-db'; } }; export default function BatchEditPage() { const params = useParams(); const router = useRouter(); const batchId = parseInt(params.id as string); // 기본 상태 const [loading, setLoading] = useState(false); const [batchConfig, setBatchConfig] = useState(null); const [batchName, setBatchName] = useState(""); const [cronSchedule, setCronSchedule] = useState("0 12 * * *"); const [description, setDescription] = useState(""); const [isActive, setIsActive] = useState("Y"); const [saveMode, setSaveMode] = useState<"INSERT" | "UPSERT">("INSERT"); const [conflictKey, setConflictKey] = useState(""); const [authServiceName, setAuthServiceName] = useState(""); const [authServiceNames, setAuthServiceNames] = useState([]); const [dataArrayPath, setDataArrayPath] = useState(""); // 연결 정보 const [connections, setConnections] = useState([]); const [fromConnection, setFromConnection] = useState(null); const [toConnection, setToConnection] = useState(null); // 테이블 및 컬럼 정보 const [fromTables, setFromTables] = useState([]); const [toTables, setToTables] = useState([]); const [fromTable, setFromTable] = useState(""); const [toTable, setToTable] = useState(""); const [fromColumns, setFromColumns] = useState([]); const [toColumns, setToColumns] = useState([]); // 매핑 정보 const [mappings, setMappings] = useState([]); // 배치 타입 감지 const [batchType, setBatchType] = useState<'db-to-db' | 'restapi-to-db' | 'db-to-restapi' | null>(null); // REST API 미리보기 상태 const [apiPreviewData, setApiPreviewData] = useState([]); const [fromApiFields, setFromApiFields] = useState([]); // 인증 토큰 모드 (직접 입력 / DB에서 선택) const [authTokenMode, setAuthTokenMode] = useState<"direct" | "db">("direct"); const [fromApiKey, setFromApiKey] = useState(""); // API 파라미터 설정 const [apiParamType, setApiParamType] = useState<"none" | "url" | "query">("none"); const [apiParamName, setApiParamName] = useState(""); const [apiParamValue, setApiParamValue] = useState(""); const [apiParamSource, setApiParamSource] = useState<"static" | "dynamic">("static"); // 매핑 리스트 (새로운 UI용) interface MappingItem { id: string; dbColumn: string; sourceType: "api" | "fixed"; apiField: string; fixedValue: string; } const [mappingList, setMappingList] = useState([]); // 페이지 로드 시 배치 정보 조회 useEffect(() => { if (batchId) { loadBatchConfig(); loadConnections(); loadAuthServiceNames(); } }, [batchId]); // 인증 서비스명 목록 로드 const loadAuthServiceNames = async () => { try { const names = await BatchAPI.getAuthServiceNames(); setAuthServiceNames(names); } catch (error) { console.error("인증 서비스 목록 로드 실패:", error); } }; // 연결 정보가 로드된 후 배치 설정의 연결 정보 설정 useEffect(() => { if (batchConfig && connections.length > 0 && batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) { const firstMapping = batchConfig.batch_mappings[0]; console.log("🔗 연결 정보 설정 시작:", firstMapping); // FROM 연결 정보 설정 if (firstMapping.from_connection_type === 'internal') { setFromConnection({ type: 'internal', name: '내부 DB' }); // 내부 DB 테이블 목록 로드 BatchAPI.getTablesFromConnection({ type: 'internal', name: '내부 DB' }).then(tables => { console.log("📋 FROM 테이블 목록:", tables); setFromTables(tables); // 컬럼 정보도 로드 if (firstMapping.from_table_name) { BatchAPI.getTableColumns({ type: 'internal', name: '내부 DB' }, firstMapping.from_table_name).then(columns => { console.log("📊 FROM 컬럼 목록:", columns); setFromColumns(columns); }); } }); } else if (firstMapping.from_connection_id) { const fromConn = connections.find(c => c.id === firstMapping.from_connection_id); if (fromConn) { setFromConnection(fromConn); // 외부 DB 테이블 목록 로드 BatchAPI.getTablesFromConnection(fromConn).then(tables => { console.log("📋 FROM 테이블 목록:", tables); setFromTables(tables); // 컬럼 정보도 로드 if (firstMapping.from_table_name) { BatchAPI.getTableColumns(fromConn, firstMapping.from_table_name).then(columns => { console.log("📊 FROM 컬럼 목록:", columns); setFromColumns(columns); }); } }); } } // TO 연결 정보 설정 if (firstMapping.to_connection_type === 'internal') { setToConnection({ type: 'internal', name: '내부 DB' }); // 내부 DB 테이블 목록 로드 BatchAPI.getTablesFromConnection({ type: 'internal', name: '내부 DB' }).then(tables => { console.log("📋 TO 테이블 목록:", tables); setToTables(tables); // 컬럼 정보도 로드 if (firstMapping.to_table_name) { BatchAPI.getTableColumns({ type: 'internal', name: '내부 DB' }, firstMapping.to_table_name).then(columns => { console.log("📊 TO 컬럼 목록:", columns); setToColumns(columns); }); } }); } else if (firstMapping.to_connection_id) { const toConn = connections.find(c => c.id === firstMapping.to_connection_id); if (toConn) { setToConnection(toConn); // 외부 DB 테이블 목록 로드 BatchAPI.getTablesFromConnection(toConn).then(tables => { console.log("📋 TO 테이블 목록:", tables); setToTables(tables); // 컬럼 정보도 로드 if (firstMapping.to_table_name) { BatchAPI.getTableColumns(toConn, firstMapping.to_table_name).then(columns => { console.log("📊 TO 컬럼 목록:", columns); setToColumns(columns); }); } }); } } } }, [batchConfig, connections]); // 배치 설정 조회 const loadBatchConfig = async () => { try { setLoading(true); console.log("🔍 배치 설정 조회 시작:", batchId); const config = await BatchAPI.getBatchConfig(batchId); console.log("📋 조회된 배치 설정:", config); setBatchConfig(config); setBatchName(config.batch_name); setCronSchedule(config.cron_schedule); setDescription(config.description || ""); setIsActive(config.is_active || "Y"); setSaveMode((config as any).save_mode || "INSERT"); setConflictKey((config as any).conflict_key || ""); setAuthServiceName((config as any).auth_service_name || ""); setDataArrayPath((config as any).data_array_path || ""); // 인증 토큰 모드 설정 if ((config as any).auth_service_name) { setAuthTokenMode("db"); } else { setAuthTokenMode("direct"); } if (config.batch_mappings && config.batch_mappings.length > 0) { // API 키 설정 (첫 번째 매핑에서) const firstMappingForApiKey = config.batch_mappings[0]; if (firstMappingForApiKey.from_api_key) { setFromApiKey(firstMappingForApiKey.from_api_key); } console.log("📊 매핑 정보:", config.batch_mappings); console.log("📊 매핑 개수:", config.batch_mappings.length); config.batch_mappings.forEach((mapping, idx) => { console.log(`📊 매핑 #${idx + 1}:`, { from: `${mapping.from_column_name} (${mapping.from_column_type})`, to: `${mapping.to_column_name} (${mapping.to_column_type})`, type: mapping.mapping_type }); }); setMappings(config.batch_mappings); // 첫 번째 매핑에서 연결 및 테이블 정보 추출 const firstMapping = config.batch_mappings[0]; setFromTable(firstMapping.from_table_name); setToTable(firstMapping.to_table_name); // 배치 타입 감지 const detectedBatchType = detectBatchType(firstMapping); setBatchType(detectedBatchType); console.log("🎯 감지된 배치 타입:", detectedBatchType); // FROM 연결 정보 설정 if (firstMapping.from_connection_type === 'internal') { setFromConnection({ type: 'internal', name: '내부 DB' }); } else if (firstMapping.from_connection_id) { // 외부 연결은 connections 로드 후 설정 setTimeout(() => { const fromConn = connections.find(c => c.id === firstMapping.from_connection_id); if (fromConn) { setFromConnection(fromConn); } }, 100); } // TO 연결 정보 설정 if (firstMapping.to_connection_type === 'internal') { setToConnection({ type: 'internal', name: '내부 DB' }); } else if (firstMapping.to_connection_id) { // 외부 연결은 connections 로드 후 설정 setTimeout(() => { const toConn = connections.find(c => c.id === firstMapping.to_connection_id); if (toConn) { setToConnection(toConn); } }, 100); } console.log("🔗 테이블 정보 설정:", { fromTable: firstMapping.from_table_name, toTable: firstMapping.to_table_name, fromConnectionType: firstMapping.from_connection_type, toConnectionType: firstMapping.to_connection_type }); // 기존 매핑을 mappingList로 변환 const convertedMappingList: MappingItem[] = config.batch_mappings.map((mapping, index) => ({ id: `mapping-${index}-${Date.now()}`, dbColumn: mapping.to_column_name || "", sourceType: (mapping as any).mapping_type === "fixed" ? "fixed" as const : "api" as const, apiField: (mapping as any).mapping_type === "fixed" ? "" : mapping.from_column_name || "", fixedValue: (mapping as any).mapping_type === "fixed" ? mapping.from_column_name || "" : "", })); setMappingList(convertedMappingList); console.log("🔄 변환된 mappingList:", convertedMappingList); } } catch (error) { console.error("❌ 배치 설정 조회 오류:", error); toast.error("배치 설정을 불러오는데 실패했습니다."); } finally { setLoading(false); } }; // 연결 정보 조회 const loadConnections = async () => { try { const connectionList = await BatchAPI.getConnections(); setConnections(connectionList); } catch (error) { console.error("연결 정보 조회 오류:", error); toast.error("연결 정보를 불러오는데 실패했습니다."); } }; // FROM 연결 변경 시 const handleFromConnectionChange = async (connectionId: string) => { const connection = connections.find(c => c.id?.toString() === connectionId) || (connectionId === 'internal' ? { type: 'internal' as const, name: '내부 DB' } : null); if (connection) { setFromConnection(connection); try { const tables = await BatchAPI.getTablesFromConnection(connection); setFromTables(tables); setFromTable(""); setFromColumns([]); } catch (error) { console.error("테이블 목록 조회 오류:", error); toast.error("테이블 목록을 불러오는데 실패했습니다."); } } }; // TO 연결 변경 시 const handleToConnectionChange = async (connectionId: string) => { const connection = connections.find(c => c.id?.toString() === connectionId) || (connectionId === 'internal' ? { type: 'internal' as const, name: '내부 DB' } : null); if (connection) { setToConnection(connection); try { const tables = await BatchAPI.getTablesFromConnection(connection); setToTables(tables); setToTable(""); setToColumns([]); } catch (error) { console.error("테이블 목록 조회 오류:", error); toast.error("테이블 목록을 불러오는데 실패했습니다."); } } }; // FROM 테이블 변경 시 const handleFromTableChange = async (tableName: string) => { setFromTable(tableName); if (fromConnection && tableName) { try { const columns = await BatchAPI.getTableColumns(fromConnection, tableName); setFromColumns(columns); } catch (error) { console.error("컬럼 정보 조회 오류:", error); toast.error("컬럼 정보를 불러오는데 실패했습니다."); } } }; // TO 테이블 변경 시 const handleToTableChange = async (tableName: string) => { setToTable(tableName); if (toConnection && tableName) { try { const columns = await BatchAPI.getTableColumns(toConnection, tableName); setToColumns(columns); } catch (error) { console.error("컬럼 정보 조회 오류:", error); toast.error("컬럼 정보를 불러오는데 실패했습니다."); } } }; // 매핑 추가 const addMapping = () => { const newMapping: BatchMapping = { from_connection_type: fromConnection?.type === 'internal' ? 'internal' : 'external', from_connection_id: fromConnection?.type === 'internal' ? undefined : fromConnection?.id, from_table_name: fromTable, from_column_name: '', from_column_type: '', to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external', to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id, to_table_name: toTable, to_column_name: '', to_column_type: '', mapping_type: 'direct', mapping_order: mappings.length + 1 }; setMappings([...mappings, newMapping]); }; // REST API → DB 매핑 추가 const addRestapiToDbMapping = () => { if (!batchConfig || !batchConfig.batch_mappings || batchConfig.batch_mappings.length === 0) { return; } const first = batchConfig.batch_mappings[0] as any; const newMapping: BatchMapping = { // FROM: REST API (기존 설정 그대로 복사) from_connection_type: "restapi" as any, from_connection_id: first.from_connection_id, from_table_name: first.from_table_name, from_column_name: "", from_column_type: "", // TO: DB (기존 설정 그대로 복사) to_connection_type: first.to_connection_type as any, to_connection_id: first.to_connection_id, to_table_name: first.to_table_name, to_column_name: "", to_column_type: "", mapping_type: (first.mapping_type as any) || "direct", mapping_order: mappings.length + 1, }; setMappings((prev) => [...prev, newMapping]); }; // mappingList 관련 함수들 (새로운 UI용) const addMappingListItem = () => { const newId = `mapping-${Date.now()}`; setMappingList((prev) => [ ...prev, { id: newId, dbColumn: "", sourceType: "api", apiField: "", fixedValue: "", }, ]); }; const removeMappingListItem = (id: string) => { setMappingList((prev) => prev.filter((m) => m.id !== id)); }; const updateMappingListItem = (id: string, updates: Partial) => { setMappingList((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m))); }; // REST API 데이터 미리보기 (수정 화면용) const previewRestApiData = async () => { if (!mappings || mappings.length === 0) { toast.error("미리보기할 REST API 매핑이 없습니다."); return; } const first: any = mappings[0]; if (!first.from_api_url || !first.from_table_name) { toast.error("API URL과 엔드포인트 정보가 없습니다."); return; } // DB 선택 모드일 때만 서비스명 검증 (직접 입력 모드는 빈 값 허용 - 공개 API용) if (authTokenMode === "db" && !authServiceName) { toast.error("인증 토큰 서비스를 선택해주세요."); return; } try { const method = (first.from_api_method as "GET" | "POST" | "PUT" | "DELETE") || "GET"; const paramInfo = apiParamType !== "none" && apiParamName && apiParamValue ? { paramType: apiParamType, paramName: apiParamName, paramValue: apiParamValue, paramSource: apiParamSource, } : undefined; // authTokenMode에 따라 올바른 값 전달 const result = await BatchManagementAPI.previewRestApiData( first.from_api_url, authTokenMode === "direct" ? fromApiKey : "", // 직접 입력일 때만 API 키 전달 first.from_table_name, method, paramInfo, first.from_api_body || undefined, authTokenMode === "db" ? authServiceName : undefined, // DB 선택 모드일 때 서비스명 전달 dataArrayPath || undefined ); setApiPreviewData(result.samples || []); setFromApiFields(result.fields || []); toast.success( `API 데이터 미리보기 완료! ${result.fields.length}개 필드, ${result.samples.length}개 레코드` ); } catch (error: any) { console.error("REST API 미리보기 오류:", error); toast.error(error?.message || "API 데이터 미리보기에 실패했습니다."); } }; // 매핑 삭제 const removeMapping = (index: number) => { const updatedMappings = mappings.filter((_, i) => i !== index); setMappings(updatedMappings); }; // 매핑 업데이트 const updateMapping = (index: number, field: keyof BatchMapping, value: any) => { setMappings(prevMappings => { const updatedMappings = [...prevMappings]; updatedMappings[index] = { ...updatedMappings[index], [field]: value }; return updatedMappings; }); }; // 배치 설정 저장 const saveBatchConfig = async () => { // restapi-to-db인 경우 mappingList 사용, 아닌 경우 mappings 사용 const effectiveMappings = batchType === "restapi-to-db" ? mappingList : mappings; if (!batchName || !cronSchedule || effectiveMappings.length === 0) { toast.error("필수 항목을 모두 입력해주세요."); return; } try { setLoading(true); // restapi-to-db인 경우 mappingList를 mappings 형식으로 변환 let finalMappings: BatchMapping[] = mappings; if (batchType === "restapi-to-db" && batchConfig?.batch_mappings?.[0]) { const first = batchConfig.batch_mappings[0] as any; finalMappings = mappingList .filter((m) => m.dbColumn) // DB 컬럼이 선택된 것만 .map((m, index) => ({ // FROM: REST API (기존 설정 복사) from_connection_type: "restapi" as any, from_connection_id: first.from_connection_id, from_table_name: first.from_table_name, from_column_name: m.sourceType === "fixed" ? m.fixedValue : m.apiField, from_column_type: m.sourceType === "fixed" ? "text" : "text", from_api_url: mappings[0]?.from_api_url || first.from_api_url, from_api_key: authTokenMode === "direct" ? fromApiKey : first.from_api_key, from_api_method: mappings[0]?.from_api_method || first.from_api_method, from_api_body: mappings[0]?.from_api_body || first.from_api_body, // TO: DB (기존 설정 복사) to_connection_type: first.to_connection_type as any, to_connection_id: first.to_connection_id, to_table_name: toTable || first.to_table_name, to_column_name: m.dbColumn, to_column_type: toColumns.find((c) => c.column_name === m.dbColumn)?.data_type || "text", mapping_type: m.sourceType === "fixed" ? "fixed" : "direct", mapping_order: index + 1, })) as BatchMapping[]; } await BatchAPI.updateBatchConfig(batchId, { batchName, description, cronSchedule, isActive, mappings: finalMappings, saveMode, conflictKey: saveMode === "UPSERT" ? conflictKey : null, // INSERT면 null로 명시적 삭제 authServiceName: authTokenMode === "db" ? authServiceName : null, // 직접입력이면 null로 명시적 삭제 dataArrayPath: dataArrayPath || null }); toast.success("배치 설정이 성공적으로 수정되었습니다."); router.push("/admin/batchmng"); } catch (error) { console.error("배치 설정 수정 실패:", error); toast.error("배치 설정 수정에 실패했습니다."); } finally { setLoading(false); } }; if (loading && !batchConfig) { return (
배치 설정을 불러오는 중...
); } return (
{/* 페이지 헤더 */}

배치 설정 수정

{/* 기본 정보 */} 기본 정보 {batchType && ( {batchType === "db-to-db" && "DB -> DB"} {batchType === "restapi-to-db" && "REST API -> DB"} {batchType === "db-to-restapi" && "DB -> REST API"} )}
setBatchName(e.target.value)} placeholder="배치명을 입력하세요" />
setCronSchedule(e.target.value)} placeholder="0 12 * * *" />