ERP-node/frontend/components/dataflow/connection/UpdateFieldMappingPanel.tsx

562 lines
21 KiB
TypeScript
Raw Normal View History

/**
* UPDATE
* UPDATE
*/
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Plus, X, Search, AlertCircle, ArrowRight } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { ColumnInfo, getColumnsFromConnection } from "@/lib/api/multiConnection";
import { useToast } from "@/hooks/use-toast";
export interface UpdateCondition {
id: string;
fromColumn: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
value: string | string[];
logicalOperator?: "AND" | "OR";
}
export interface UpdateFieldMapping {
id: string;
fromColumn: string;
toColumn: string;
transformFunction?: string;
defaultValue?: string;
}
export interface WhereCondition {
id: string;
toColumn: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
valueSource: "from_column" | "static" | "current_timestamp";
fromColumn?: string;
staticValue?: string;
logicalOperator?: "AND" | "OR";
}
export interface UpdateFieldMappingPanelProps {
action: any;
actionIndex: number;
settings: any;
onSettingsChange: (settings: any) => void;
fromConnectionId?: number;
toConnectionId?: number;
fromTableName?: string;
toTableName?: string;
disabled?: boolean;
}
export const UpdateFieldMappingPanel: React.FC<UpdateFieldMappingPanelProps> = ({
action,
actionIndex,
settings,
onSettingsChange,
fromConnectionId,
toConnectionId,
fromTableName,
toTableName,
disabled = false,
}) => {
const { toast } = useToast();
// 상태 관리
const [fromTableColumns, setFromTableColumns] = useState<ColumnInfo[]>([]);
const [toTableColumns, setToTableColumns] = useState<ColumnInfo[]>([]);
const [updateConditions, setUpdateConditions] = useState<UpdateCondition[]>([]);
const [updateFields, setUpdateFields] = useState<UpdateFieldMapping[]>([]);
const [whereConditions, setWhereConditions] = useState<WhereCondition[]>([]);
const [loading, setLoading] = useState(false);
// 검색 상태
const [fromColumnSearch, setFromColumnSearch] = useState("");
const [toColumnSearch, setToColumnSearch] = useState("");
// 컬럼 정보 로드
useEffect(() => {
if (fromConnectionId !== undefined && fromTableName) {
loadColumnInfo(fromConnectionId, fromTableName, setFromTableColumns, "FROM");
}
}, [fromConnectionId, fromTableName]);
useEffect(() => {
if (toConnectionId !== undefined && toTableName) {
loadColumnInfo(toConnectionId, toTableName, setToTableColumns, "TO");
}
}, [toConnectionId, toTableName]);
// 컬럼 정보 로드 함수
const loadColumnInfo = async (
connectionId: number,
tableName: string,
setColumns: React.Dispatch<React.SetStateAction<ColumnInfo[]>>,
type: "FROM" | "TO",
) => {
try {
setLoading(true);
const columns = await getColumnsFromConnection(connectionId, tableName);
setColumns(columns);
} catch (error) {
console.error(`${type} 컬럼 정보 로드 실패:`, error);
toast({
title: "컬럼 로드 실패",
description: `${type} 테이블의 컬럼 정보를 불러오는데 실패했습니다.`,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
// 컬럼 필터링
const getFilteredColumns = (columns: ColumnInfo[], searchTerm: string) => {
if (!searchTerm.trim()) return columns;
const term = searchTerm.toLowerCase();
return columns.filter(
(col) => col.columnName.toLowerCase().includes(term) || col.displayName.toLowerCase().includes(term),
);
};
// UPDATE 조건 추가
const addUpdateCondition = () => {
const newCondition: UpdateCondition = {
id: `condition_${Date.now()}`,
fromColumn: "",
operator: "=",
value: "",
logicalOperator: updateConditions.length > 0 ? "AND" : undefined,
};
setUpdateConditions([...updateConditions, newCondition]);
};
// UPDATE 조건 제거
const removeUpdateCondition = (id: string) => {
setUpdateConditions(updateConditions.filter((c) => c.id !== id));
};
// UPDATE 조건 수정
const updateCondition = (id: string, field: keyof UpdateCondition, value: any) => {
setUpdateConditions(updateConditions.map((c) => (c.id === id ? { ...c, [field]: value } : c)));
};
// UPDATE 필드 매핑 추가
const addUpdateFieldMapping = () => {
const newMapping: UpdateFieldMapping = {
id: `mapping_${Date.now()}`,
fromColumn: "",
toColumn: "",
transformFunction: "",
defaultValue: "",
};
setUpdateFields([...updateFields, newMapping]);
};
// UPDATE 필드 매핑 제거
const removeUpdateFieldMapping = (id: string) => {
setUpdateFields(updateFields.filter((m) => m.id !== id));
};
// UPDATE 필드 매핑 수정
const updateFieldMapping = (id: string, field: keyof UpdateFieldMapping, value: any) => {
setUpdateFields(updateFields.map((m) => (m.id === id ? { ...m, [field]: value } : m)));
};
// WHERE 조건 추가
const addWhereCondition = () => {
const newCondition: WhereCondition = {
id: `where_${Date.now()}`,
toColumn: "",
operator: "=",
valueSource: "from_column",
fromColumn: "",
staticValue: "",
logicalOperator: whereConditions.length > 0 ? "AND" : undefined,
};
setWhereConditions([...whereConditions, newCondition]);
};
// WHERE 조건 제거
const removeWhereCondition = (id: string) => {
setWhereConditions(whereConditions.filter((c) => c.id !== id));
};
// WHERE 조건 수정
const updateWhereCondition = (id: string, field: keyof WhereCondition, value: any) => {
setWhereConditions(whereConditions.map((c) => (c.id === id ? { ...c, [field]: value } : c)));
};
// 자기 자신 테이블 작업 경고
const getSelfTableWarning = () => {
if (fromConnectionId === toConnectionId && fromTableName === toTableName) {
return "⚠️ 자기 자신 테이블 UPDATE 작업입니다. 무한 루프 및 데이터 손상에 주의하세요.";
}
return null;
};
const warningMessage = getSelfTableWarning();
const filteredFromColumns = getFilteredColumns(fromTableColumns, fromColumnSearch);
const filteredToColumns = getFilteredColumns(toTableColumns, toColumnSearch);
return (
<div className="space-y-6">
{/* 경고 메시지 */}
{warningMessage && (
<Alert variant="default">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{warningMessage}</AlertDescription>
</Alert>
)}
{/* UPDATE 조건 설정 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">🔍 </CardTitle>
<CardDescription>
FROM TO
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 검색 필드 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">FROM </label>
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
<Input
placeholder="컬럼 검색..."
value={fromColumnSearch}
onChange={(e) => setFromColumnSearch(e.target.value)}
className="pl-9"
/>
</div>
</div>
</div>
{/* 업데이트 조건 리스트 */}
<div className="space-y-3">
{updateConditions.map((condition, index) => (
<div key={condition.id} className="flex items-center gap-2 rounded-lg border p-3">
{index > 0 && (
<Select
value={condition.logicalOperator || "AND"}
onValueChange={(value) => updateCondition(condition.id, "logicalOperator", value)}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
)}
<Select
value={condition.fromColumn}
onValueChange={(value) => updateCondition(condition.id, "fromColumn", value)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="FROM 컬럼" />
</SelectTrigger>
<SelectContent>
{filteredFromColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span>{col.columnName}</span>
<Badge variant="outline" className="text-xs">
{col.dataType}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={condition.operator}
onValueChange={(value) => updateCondition(condition.id, "operator", value)}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">&gt;</SelectItem>
<SelectItem value="<">&lt;</SelectItem>
<SelectItem value=">=">&gt;=</SelectItem>
<SelectItem value="<=">&lt;=</SelectItem>
<SelectItem value="LIKE">LIKE</SelectItem>
<SelectItem value="IN">IN</SelectItem>
<SelectItem value="NOT IN">NOT IN</SelectItem>
</SelectContent>
</Select>
<Input
placeholder="값"
value={condition.value as string}
onChange={(e) => updateCondition(condition.id, "value", e.target.value)}
className="flex-1"
/>
<Button variant="outline" size="sm" onClick={() => removeUpdateCondition(condition.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addUpdateCondition} className="w-full" disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<Separator />
{/* UPDATE 필드 매핑 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">📝 </CardTitle>
<CardDescription>FROM TO </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 검색 필드 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">FROM </label>
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
<Input
placeholder="컬럼 검색..."
value={fromColumnSearch}
onChange={(e) => setFromColumnSearch(e.target.value)}
className="pl-9"
/>
</div>
</div>
<div>
<label className="text-sm font-medium">TO </label>
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
<Input
placeholder="컬럼 검색..."
value={toColumnSearch}
onChange={(e) => setToColumnSearch(e.target.value)}
className="pl-9"
/>
</div>
</div>
</div>
{/* 필드 매핑 리스트 */}
<div className="space-y-3">
{updateFields.map((mapping) => (
<div key={mapping.id} className="flex items-center gap-2 rounded-lg border p-3">
<Select
value={mapping.fromColumn}
onValueChange={(value) => updateFieldMapping(mapping.id, "fromColumn", value)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="FROM 컬럼" />
</SelectTrigger>
<SelectContent>
{filteredFromColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span>{col.columnName}</span>
<Badge variant="outline" className="text-xs">
{col.dataType}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<ArrowRight className="text-muted-foreground h-4 w-4" />
<Select
value={mapping.toColumn}
onValueChange={(value) => updateFieldMapping(mapping.id, "toColumn", value)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="TO 컬럼" />
</SelectTrigger>
<SelectContent>
{filteredToColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span>{col.columnName}</span>
<Badge variant="outline" className="text-xs">
{col.dataType}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="기본값 (선택사항)"
value={mapping.defaultValue || ""}
onChange={(e) => updateFieldMapping(mapping.id, "defaultValue", e.target.value)}
className="w-32"
/>
<Button variant="outline" size="sm" onClick={() => removeUpdateFieldMapping(mapping.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addUpdateFieldMapping} className="w-full" disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<Separator />
{/* WHERE 조건 설정 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">🎯 </CardTitle>
<CardDescription>TO WHERE </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* WHERE 조건 리스트 */}
<div className="space-y-3">
{whereConditions.map((condition, index) => (
<div key={condition.id} className="flex items-center gap-2 rounded-lg border p-3">
{index > 0 && (
<Select
value={condition.logicalOperator || "AND"}
onValueChange={(value) => updateWhereCondition(condition.id, "logicalOperator", value)}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
)}
<Select
value={condition.toColumn}
onValueChange={(value) => updateWhereCondition(condition.id, "toColumn", value)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="TO 컬럼" />
</SelectTrigger>
<SelectContent>
{filteredToColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span>{col.columnName}</span>
<Badge variant="outline" className="text-xs">
{col.dataType}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={condition.operator}
onValueChange={(value) => updateWhereCondition(condition.id, "operator", value)}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">&gt;</SelectItem>
<SelectItem value="<">&lt;</SelectItem>
<SelectItem value=">=">&gt;=</SelectItem>
<SelectItem value="<=">&lt;=</SelectItem>
<SelectItem value="LIKE">LIKE</SelectItem>
<SelectItem value="IN">IN</SelectItem>
<SelectItem value="NOT IN">NOT IN</SelectItem>
</SelectContent>
</Select>
<Select
value={condition.valueSource}
onValueChange={(value) => updateWhereCondition(condition.id, "valueSource", value)}
>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="from_column">FROM </SelectItem>
<SelectItem value="static"></SelectItem>
<SelectItem value="current_timestamp"> </SelectItem>
</SelectContent>
</Select>
{condition.valueSource === "from_column" && (
<Select
value={condition.fromColumn || ""}
onValueChange={(value) => updateWhereCondition(condition.id, "fromColumn", value)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="FROM 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{filteredFromColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{condition.valueSource === "static" && (
<Input
placeholder="고정값"
value={condition.staticValue || ""}
onChange={(e) => updateWhereCondition(condition.id, "staticValue", e.target.value)}
className="w-32"
/>
)}
<Button variant="outline" size="sm" onClick={() => removeWhereCondition(condition.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addWhereCondition} className="w-full" disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
WHERE
</Button>
</div>
{whereConditions.length === 0 && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription> UPDATE WHERE .</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
);
};