ERP-node/frontend/app/(main)/admin/batchmng/edit/[id]/page.tsx

1062 lines
40 KiB
TypeScript

"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<BatchConfig | null>(null);
const [batchName, setBatchName] = useState("");
const [cronSchedule, setCronSchedule] = useState("0 12 * * *");
const [description, setDescription] = useState("");
const [isActive, setIsActive] = useState("Y");
// 연결 정보
const [connections, setConnections] = useState<ConnectionInfo[]>([]);
const [fromConnection, setFromConnection] = useState<ConnectionInfo | null>(null);
const [toConnection, setToConnection] = useState<ConnectionInfo | null>(null);
// 테이블 및 컬럼 정보
const [fromTables, setFromTables] = useState<string[]>([]);
const [toTables, setToTables] = useState<string[]>([]);
const [fromTable, setFromTable] = useState("");
const [toTable, setToTable] = useState("");
const [fromColumns, setFromColumns] = useState<BatchColumnInfo[]>([]);
const [toColumns, setToColumns] = useState<BatchColumnInfo[]>([]);
// 매핑 정보
const [mappings, setMappings] = useState<BatchMapping[]>([]);
// 배치 타입 감지
const [batchType, setBatchType] = useState<'db-to-db' | 'restapi-to-db' | 'db-to-restapi' | null>(null);
// REST API 미리보기 상태
const [apiPreviewData, setApiPreviewData] = useState<any[]>([]);
// 페이지 로드 시 배치 정보 조회
useEffect(() => {
if (batchId) {
loadBatchConfig();
loadConnections();
}
}, [batchId]);
// 연결 정보가 로드된 후 배치 설정의 연결 정보 설정
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");
if (config.batch_mappings && config.batch_mappings.length > 0) {
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
});
}
} 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]);
};
// 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;
}
try {
const method =
(first.from_api_method as "GET" | "POST" | "PUT" | "DELETE") || "GET";
const paramInfo =
first.from_api_param_type &&
first.from_api_param_name &&
first.from_api_param_value
? {
paramType: first.from_api_param_type as "url" | "query",
paramName: first.from_api_param_name as string,
paramValue: first.from_api_param_value as string,
paramSource:
(first.from_api_param_source as "static" | "dynamic") ||
"static",
}
: undefined;
const result = await BatchManagementAPI.previewRestApiData(
first.from_api_url,
first.from_api_key || "",
first.from_table_name,
method,
paramInfo,
first.from_api_body || undefined
);
setApiPreviewData(result.samples || []);
toast.success(
`API 데이터 미리보기 완료! ${result.fields.length}개 필드, ${result.samples.length}개 레코드`
);
} catch (error) {
console.error("REST API 미리보기 오류:", error);
toast.error("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 () => {
if (!batchName || !cronSchedule || mappings.length === 0) {
toast.error("필수 항목을 모두 입력해주세요.");
return;
}
try {
setLoading(true);
await BatchAPI.updateBatchConfig(batchId, {
batchName,
description,
cronSchedule,
isActive,
mappings
});
toast.success("배치 설정이 성공적으로 수정되었습니다.");
router.push("/admin/batchmng");
} catch (error) {
console.error("배치 설정 수정 실패:", error);
toast.error("배치 설정 수정에 실패했습니다.");
} finally {
setLoading(false);
}
};
if (loading && !batchConfig) {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-center h-64">
<RefreshCw className="w-8 h-8 animate-spin" />
<span className="ml-2"> ...</span>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button
variant="outline"
onClick={() => router.push("/admin/batchmng")}
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<h1 className="text-3xl font-bold"> </h1>
</div>
<div className="flex space-x-2">
<Button
onClick={loadBatchConfig}
variant="outline"
disabled={loading}
>
<RefreshCw
className={`w-4 h-4 mr-2 ${loading ? "animate-spin" : ""}`}
/>
</Button>
</div>
</div>
{/* 기본 정보 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="batchName"> *</Label>
<Input
id="batchName"
value={batchName}
onChange={(e) => setBatchName(e.target.value)}
placeholder="배치명을 입력하세요"
/>
</div>
<div>
<Label htmlFor="cronSchedule"> (Cron) *</Label>
<Input
id="cronSchedule"
value={cronSchedule}
onChange={(e) => setCronSchedule(e.target.value)}
placeholder="0 12 * * *"
/>
</div>
</div>
<div>
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="배치에 대한 설명을 입력하세요"
rows={3}
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="isActive"
checked={isActive === 'Y'}
onCheckedChange={(checked) => setIsActive(checked ? 'Y' : 'N')}
/>
<Label htmlFor="isActive"></Label>
</div>
</CardContent>
</Card>
{/* 배치 타입 표시 */}
{batchType && (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<span> </span>
<Badge variant="outline">
{batchType === 'db-to-db' && 'DB → DB'}
{batchType === 'restapi-to-db' && 'REST API → DB'}
{batchType === 'db-to-restapi' && 'DB → REST API'}
</Badge>
</CardTitle>
</CardHeader>
</Card>
)}
{/* 연결 설정 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{batchType === 'db-to-db' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* FROM 설정 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">FROM ()</h3>
<div>
<Label></Label>
<Select
value={fromConnection?.type === 'internal' ? 'internal' : fromConnection?.id?.toString() || ''}
onValueChange={handleFromConnectionChange}
>
<SelectTrigger>
<SelectValue placeholder="소스 연결을 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internal"> DB</SelectItem>
{connections.filter(conn => conn.id).map((conn) => (
<SelectItem key={conn.id} value={conn.id!.toString()}>
{conn.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select value={fromTable} onValueChange={handleFromTableChange}>
<SelectTrigger>
<SelectValue placeholder="소스 테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{fromTables.map((table) => (
<SelectItem key={table} value={table}>
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* TO 설정 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">TO ()</h3>
<div>
<Label></Label>
<Select
value={toConnection?.type === 'internal' ? 'internal' : toConnection?.id?.toString() || ''}
onValueChange={handleToConnectionChange}
>
<SelectTrigger>
<SelectValue placeholder="대상 연결을 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internal"> DB</SelectItem>
{connections.filter(conn => conn.id).map((conn) => (
<SelectItem key={conn.id} value={conn.id!.toString()}>
{conn.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select value={toTable} onValueChange={handleToTableChange}>
<SelectTrigger>
<SelectValue placeholder="대상 테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{toTables.map((table) => (
<SelectItem key={table} value={table}>
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
)}
{batchType === 'restapi-to-db' && (
<div className="space-y-6">
<div className="text-center py-4 bg-blue-50 rounded-lg">
<h3 className="text-lg font-semibold text-blue-800">REST API DB </h3>
<p className="text-sm text-blue-600"> REST API에서 .</p>
</div>
{mappings.length > 0 && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>API URL</Label>
<Input value={mappings[0]?.from_api_url || ""} readOnly />
</div>
<div>
<Label>API </Label>
<Input
value={mappings[0]?.from_table_name || ""}
readOnly
/>
</div>
<div>
<Label>HTTP </Label>
<Input
value={mappings[0]?.from_api_method || "GET"}
readOnly
/>
</div>
<div>
<Label> </Label>
<Input
value={mappings[0]?.to_table_name || ""}
readOnly
/>
</div>
</div>
{/* Request Body (JSON) 편집 UI */}
<div>
<Label>Request Body (JSON)</Label>
<Textarea
rows={5}
className="font-mono text-sm"
placeholder='{"id": "wace", "pwd": "wace!$%Pwdmo^^"}'
value={mappings[0]?.from_api_body || ""}
onChange={(e) => {
const value = e.target.value;
setMappings((prev) => {
if (prev.length === 0) return prev;
const updated = [...prev];
updated[0] = {
...updated[0],
from_api_body: value,
} as any;
return updated;
});
}}
/>
<p className="text-xs text-muted-foreground mt-1.5">
POST JSON Request Body를 .
.
</p>
</div>
{/* API 데이터 미리보기 */}
<div className="space-y-3">
<Button
variant="outline"
size="sm"
onClick={previewRestApiData}
className="mt-2"
>
<RefreshCw className="w-4 h-4 mr-2" />
API
</Button>
{apiPreviewData.length > 0 && (
<div className="mt-2 rounded-lg border bg-muted p-3">
<p className="text-sm font-medium text-muted-foreground">
( 3)
</p>
<div className="mt-2 space-y-2 max-h-60 overflow-y-auto">
{apiPreviewData.slice(0, 3).map((item, index) => (
<pre
key={index}
className="whitespace-pre-wrap rounded border bg-background p-2 text-xs font-mono"
>
{JSON.stringify(item, null, 2)}
</pre>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
)}
{batchType === 'db-to-restapi' && (
<div className="space-y-6">
<div className="text-center py-4 bg-green-50 rounded-lg">
<h3 className="text-lg font-semibold text-green-800">DB REST API </h3>
<p className="text-sm text-green-600"> REST API로 .</p>
</div>
{mappings.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label> </Label>
<Input value={mappings[0]?.from_table_name || ''} readOnly />
</div>
<div>
<Label>API URL</Label>
<Input value={mappings[0]?.to_api_url || ''} readOnly />
</div>
<div>
<Label>API </Label>
<Input value={mappings[0]?.to_table_name || ''} readOnly />
</div>
<div>
<Label>HTTP </Label>
<Input value={mappings[0]?.to_api_method || 'POST'} readOnly />
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* 컬럼 매핑 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
{batchType === 'db-to-db' && '컬럼 매핑'}
{batchType === 'restapi-to-db' && 'API 필드 → DB 컬럼 매핑'}
{batchType === 'db-to-restapi' && 'DB 컬럼 → API 필드 매핑'}
{batchType === 'db-to-db' && (
<Button onClick={addMapping} size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
)}
{batchType === 'restapi-to-db' && (
<Button onClick={addRestapiToDbMapping} size="sm">
<Plus className="w-4 h-4 mr-2" />
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
{mappings.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{batchType === 'db-to-db' && '매핑을 추가해주세요.'}
{batchType === 'restapi-to-db' && 'API 필드와 DB 컬럼 매핑 정보가 없습니다.'}
{batchType === 'db-to-restapi' && 'DB 컬럼과 API 필드 매핑 정보가 없습니다.'}
</div>
) : (
<div className="space-y-4">
{batchType === 'db-to-db' && mappings.map((mapping, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="font-medium"> #{index + 1}</h4>
{mapping.from_column_name && mapping.to_column_name && (
<p className="text-sm text-gray-600">
{mapping.from_column_name} {mapping.to_column_name}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeMapping(index)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>FROM </Label>
<Select
value={mapping.from_column_name || ''}
onValueChange={(value) => {
console.log(`📝 FROM 컬럼 변경: ${value}`);
updateMapping(index, 'from_column_name', value);
// 컬럼 타입도 함께 업데이트
const selectedColumn = fromColumns.find(col => col.column_name === value);
if (selectedColumn) {
updateMapping(index, 'from_column_type', selectedColumn.data_type);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="소스 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{fromColumns.map((column) => (
<SelectItem key={column.column_name} value={column.column_name}>
{column.column_name} ({column.data_type})
</SelectItem>
))}
</SelectContent>
</Select>
{fromColumns.length === 0 && (
<p className="text-xs text-gray-500 mt-1">
.
</p>
)}
</div>
<div>
<Label>TO </Label>
<Select
value={mapping.to_column_name || ''}
onValueChange={(value) => {
console.log(`📝 TO 컬럼 변경: ${value}`);
updateMapping(index, 'to_column_name', value);
// 컬럼 타입도 함께 업데이트
const selectedColumn = toColumns.find(col => col.column_name === value);
if (selectedColumn) {
updateMapping(index, 'to_column_type', selectedColumn.data_type);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="대상 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{toColumns.map((column) => (
<SelectItem key={column.column_name} value={column.column_name}>
{column.column_name} ({column.data_type})
</SelectItem>
))}
</SelectContent>
</Select>
{toColumns.length === 0 && (
<p className="text-xs text-gray-500 mt-1">
.
</p>
)}
</div>
</div>
</div>
))}
{batchType === 'restapi-to-db' && mappings.map((mapping, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="font-medium"> #{index + 1}</h4>
{mapping.from_column_name && mapping.to_column_name && (
<p className="text-sm text-gray-600">
API : {mapping.from_column_name} DB : {mapping.to_column_name}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeMapping(index)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>API (JSON )</Label>
<Input
value={mapping.from_column_name || ""}
onChange={(e) =>
updateMapping(
index,
"from_column_name",
e.target.value
)
}
placeholder="response.access_token"
/>
</div>
<div>
<Label>DB </Label>
<Select
value={mapping.to_column_name || ""}
onValueChange={(value) => {
updateMapping(index, "to_column_name", value);
const selectedColumn = toColumns.find(
(col) => col.column_name === value
);
if (selectedColumn) {
updateMapping(
index,
"to_column_type",
selectedColumn.data_type
);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="대상 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{toColumns.map((column) => (
<SelectItem
key={column.column_name}
value={column.column_name}
>
{column.column_name} ({column.data_type})
</SelectItem>
))}
</SelectContent>
</Select>
{toColumns.length === 0 && (
<p className="text-xs text-gray-500 mt-1">
.
</p>
)}
</div>
</div>
</div>
))}
{batchType === 'db-to-restapi' && mappings.map((mapping, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="font-medium"> #{index + 1}</h4>
<p className="text-sm text-gray-600">
DB : {mapping.from_column_name} API : {mapping.to_column_name}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>DB </Label>
<Input value={mapping.from_column_name || ''} readOnly />
</div>
<div>
<Label>API </Label>
<Input value={mapping.to_column_name || ''} readOnly />
</div>
</div>
{mapping.to_api_body && (
<div className="mt-4">
<Label>Request Body 릿</Label>
<Textarea
value={mapping.to_api_body}
readOnly
rows={3}
className="font-mono text-sm"
/>
</div>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* 저장 버튼 */}
<div className="flex justify-end space-x-4">
<Button
variant="outline"
onClick={() => router.push("/admin/batchmng")}
>
</Button>
<Button
onClick={saveBatchConfig}
disabled={loading || mappings.length === 0}
className="flex items-center space-x-2"
>
{loading ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
<span>{loading ? "저장 중..." : "배치 설정 저장"}</span>
</Button>
</div>
</div>
);
}