제어관리 개선판

This commit is contained in:
kjs
2025-10-24 14:11:12 +09:00
parent 96252270d7
commit 8d1f0e7098
30 changed files with 2285 additions and 655 deletions

View File

@@ -28,9 +28,9 @@ export function PropertiesPanel() {
const selectedNode = selectedNodes.length === 1 ? nodes.find((n) => n.id === selectedNodes[0]) : null;
return (
<div className="flex h-full flex-col">
<div className="flex h-full w-full flex-col">
{/* 헤더 */}
<div className="flex items-center justify-between border-b p-4">
<div className="flex h-16 shrink-0 items-center justify-between border-b bg-white p-4">
<div>
<h3 className="text-sm font-semibold text-gray-900"></h3>
{selectedNode && (
@@ -42,8 +42,15 @@ export function PropertiesPanel() {
</Button>
</div>
{/* 내용 */}
<div className="flex-1 overflow-hidden">
{/* 내용 - 스크롤 가능 영역 */}
<div
className="flex-1 overflow-y-scroll"
style={{
maxHeight: 'calc(100vh - 64px)',
overflowY: 'scroll',
WebkitOverflowScrolling: 'touch'
}}
>
{selectedNodes.length === 0 ? (
<div className="flex h-full items-center justify-center p-4">
<div className="text-center text-sm text-gray-500">

View File

@@ -29,7 +29,7 @@ export function CommentProperties({ nodeId, data }: CommentPropertiesProps) {
};
return (
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
<div className="flex items-center gap-2 rounded-md bg-yellow-50 p-2">
<MessageSquare className="h-4 w-4 text-yellow-600" />
<span className="font-semibold text-yellow-600"></span>

View File

@@ -183,26 +183,39 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
};
const handleRemoveCondition = (index: number) => {
setConditions(conditions.filter((_, i) => i !== index));
const newConditions = conditions.filter((_, i) => i !== index);
setConditions(newConditions);
updateNode(nodeId, {
conditions: newConditions,
});
};
const handleDisplayNameChange = (newDisplayName: string) => {
setDisplayName(newDisplayName);
updateNode(nodeId, {
displayName: newDisplayName,
});
};
const handleConditionChange = (index: number, field: string, value: any) => {
const newConditions = [...conditions];
newConditions[index] = { ...newConditions[index], [field]: value };
setConditions(newConditions);
updateNode(nodeId, {
conditions: newConditions,
});
};
const handleSave = () => {
const handleLogicChange = (newLogic: "AND" | "OR") => {
setLogic(newLogic);
updateNode(nodeId, {
displayName,
conditions,
logic,
logic: newLogic,
});
};
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
@@ -215,7 +228,7 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
onChange={(e) => handleDisplayNameChange(e.target.value)}
className="mt-1"
placeholder="노드 표시 이름"
/>
@@ -225,7 +238,7 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
<Label htmlFor="logic" className="text-xs">
</Label>
<Select value={logic} onValueChange={(value: "AND" | "OR") => setLogic(value)}>
<Select value={logic} onValueChange={handleLogicChange}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
@@ -386,12 +399,6 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
)}
</div>
{/* 저장 버튼 */}
<div className="flex gap-2">
<Button onClick={handleSave} className="flex-1" size="sm">
</Button>
</div>
{/* 안내 */}
<div className="space-y-2">

View File

@@ -359,7 +359,7 @@ export function DataTransformProperties({ nodeId, data }: DataTransformPropertie
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 헤더 */}
<div className="flex items-center gap-2 rounded-md bg-indigo-50 p-2">
<Wand2 className="h-4 w-4 text-indigo-600" />
@@ -453,13 +453,6 @@ export function DataTransformProperties({ nodeId, data }: DataTransformPropertie
)}
</div>
{/* 적용 버튼 */}
<div className="sticky bottom-0 border-t bg-white pt-3">
<Button onClick={handleSave} className="w-full" size="sm">
</Button>
<p className="mt-2 text-center text-xs text-gray-500"> .</p>
</div>
</div>
</ScrollArea>
);

View File

@@ -217,7 +217,7 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 경고 */}
<div className="rounded-lg border-2 border-red-200 bg-red-50 p-4">
<div className="flex items-start gap-3">
@@ -706,9 +706,6 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
)}
</div>
<Button onClick={handleSave} variant="destructive" className="w-full" size="sm">
</Button>
<div className="space-y-2">
<div className="rounded bg-red-50 p-3 text-xs text-red-700">

View File

@@ -5,7 +5,7 @@
*/
import { useEffect, useState } from "react";
import { Database, RefreshCw } from "lucide-react";
import { Database, RefreshCw, Table, FileText } from "lucide-react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -43,6 +43,11 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro
const [selectedConnectionId, setSelectedConnectionId] = useState<number | undefined>(data.connectionId);
const [tableName, setTableName] = useState(data.tableName);
const [schema, setSchema] = useState(data.schema || "");
// 🆕 데이터 소스 타입 (기본값: context-data)
const [dataSourceType, setDataSourceType] = useState<"context-data" | "table-all">(
(data as any).dataSourceType || "context-data"
);
const [connections, setConnections] = useState<ExternalConnection[]>([]);
const [tables, setTables] = useState<ExternalTable[]>([]);
@@ -200,21 +205,26 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro
});
};
const handleSave = () => {
const handleDisplayNameChange = (newDisplayName: string) => {
setDisplayName(newDisplayName);
updateNode(nodeId, {
displayName,
connectionId: selectedConnectionId,
connectionName: selectedConnection?.connection_name || "",
tableName,
schema,
dbType: selectedConnection?.db_type,
displayName: newDisplayName,
});
toast.success("설정이 저장되었습니다.");
};
/**
* 🆕 데이터 소스 타입 변경 핸들러
*/
const handleDataSourceTypeChange = (newType: "context-data" | "table-all") => {
setDataSourceType(newType);
updateNode(nodeId, {
dataSourceType: newType,
});
console.log(`✅ 데이터 소스 타입 변경: ${newType}`);
};
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* DB 타입 정보 */}
<div
className="rounded-lg border-2 p-4"
@@ -302,7 +312,7 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
onChange={(e) => handleDisplayNameChange(e.target.value)}
className="mt-1"
placeholder="노드 표시 이름"
/>
@@ -340,6 +350,64 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro
</div>
)}
{/* 🆕 데이터 소스 설정 */}
{tableName && (
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
<div className="space-y-3">
<div>
<Label className="text-xs"> </Label>
<Select value={dataSourceType} onValueChange={handleDataSourceTypeChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="데이터 소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="context-data">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<div className="flex flex-col">
<span className="font-medium"> </span>
<span className="text-muted-foreground text-xs">
</span>
</div>
</div>
</SelectItem>
<SelectItem value="table-all">
<div className="flex items-center gap-2">
<Table className="h-4 w-4" />
<div className="flex flex-col">
<span className="font-medium"> </span>
<span className="text-muted-foreground text-xs">
DB의
</span>
</div>
</div>
</SelectItem>
</SelectContent>
</Select>
{/* 설명 텍스트 */}
<div className="mt-2 rounded bg-blue-50 p-3 text-xs text-blue-700">
{dataSourceType === "context-data" ? (
<>
<p className="font-medium mb-1">💡 </p>
<p> .</p>
</>
) : (
<>
<p className="font-medium mb-1">📊 </p>
<p> DB의 ** ** .</p>
<p className="mt-1 text-orange-600 font-medium"> </p>
</>
)}
</div>
</div>
</div>
</div>
)}
{/* 컬럼 정보 */}
{columns.length > 0 && (
<div>
@@ -347,14 +415,16 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro
{loadingColumns ? (
<p className="text-xs text-gray-500"> ... </p>
) : (
<div className="max-h-[200px] space-y-1 overflow-y-auto">
<div className="max-h-[300px] space-y-1 overflow-y-auto rounded border bg-gray-50 p-2">
{columns.map((col, index) => (
<div
key={index}
className="flex items-center justify-between rounded border bg-gray-50 px-3 py-2 text-xs"
className="flex items-center justify-between rounded bg-white px-2 py-1.5 text-xs"
>
<span className="font-medium">{col.column_name}</span>
<span className="font-mono text-gray-500">{col.data_type}</span>
<span className="truncate font-medium" title={col.column_name}>
{col.column_name}
</span>
<span className="ml-2 shrink-0 font-mono text-gray-500">{col.data_type}</span>
</div>
))}
</div>
@@ -362,14 +432,9 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro
</div>
)}
<Button onClick={handleSave} className="w-full" size="sm">
</Button>
<div className="rounded p-3 text-xs" style={{ backgroundColor: `${dbInfo.color}15`, color: dbInfo.color }}>
💡 DB "외부 DB 연결 관리" .
</div>
</div>
</ScrollArea>
</div>
);
}

View File

@@ -451,31 +451,22 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
};
const handleAddMapping = () => {
setFieldMappings([
const newMappings = [
...fieldMappings,
{
sourceField: null,
targetField: "",
staticValue: undefined,
},
]);
];
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleRemoveMapping = (index: number) => {
const newMappings = fieldMappings.filter((_, i) => i !== index);
setFieldMappings(newMappings);
// 즉시 반영
updateNode(nodeId, {
displayName,
targetTable,
fieldMappings: newMappings,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
ignoreErrors,
ignoreDuplicates,
},
});
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleMappingChange = (index: number, field: string, value: any) => {
@@ -490,28 +481,71 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
sourceFieldLabel: sourceField?.label,
};
} else if (field === "targetField") {
const targetColumn = targetColumns.find((c) => c.columnName === value);
const targetColumn = (() => {
if (targetType === "internal") {
return targetColumns.find((col) => col.column_name === value);
} else if (targetType === "external") {
return externalColumns.find((col) => col.column_name === value);
}
return null;
})();
newMappings[index] = {
...newMappings[index],
targetField: value,
targetFieldLabel: targetColumn?.columnLabel,
targetFieldLabel: targetColumn?.label_ko || targetColumn?.column_label || targetColumn?.displayName || value,
};
} else {
newMappings[index] = { ...newMappings[index], [field]: value };
newMappings[index] = {
...newMappings[index],
[field]: value,
};
}
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleSave = () => {
// 즉시 반영 핸들러들
const handleDisplayNameChange = (newDisplayName: string) => {
setDisplayName(newDisplayName);
updateNode(nodeId, { displayName: newDisplayName });
};
const handleFieldMappingsChange = (newMappings: any[]) => {
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleBatchSizeChange = (newBatchSize: string) => {
setBatchSize(newBatchSize);
updateNode(nodeId, {
options: {
batchSize: newBatchSize ? parseInt(newBatchSize) : undefined,
ignoreErrors,
ignoreDuplicates,
},
});
};
const handleIgnoreErrorsChange = (checked: boolean) => {
setIgnoreErrors(checked);
updateNode(nodeId, {
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
ignoreErrors: checked,
ignoreDuplicates,
},
});
};
const handleIgnoreDuplicatesChange = (checked: boolean) => {
setIgnoreDuplicates(checked);
updateNode(nodeId, {
displayName,
targetTable,
fieldMappings,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
ignoreErrors,
ignoreDuplicates,
ignoreDuplicates: checked,
},
});
};
@@ -552,7 +586,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 🔥 타겟 타입 선택 */}
<div>
<Label className="mb-2 block text-xs font-medium"> </Label>
@@ -1219,7 +1253,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
id="batchSize"
type="number"
value={batchSize}
onChange={(e) => setBatchSize(e.target.value)}
onChange={(e) => handleBatchSizeChange(e.target.value)}
className="mt-1"
placeholder="한 번에 처리할 레코드 수"
/>
@@ -1229,7 +1263,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
<Checkbox
id="ignoreDuplicates"
checked={ignoreDuplicates}
onCheckedChange={(checked) => setIgnoreDuplicates(checked as boolean)}
onCheckedChange={(checked) => handleIgnoreDuplicatesChange(checked as boolean)}
/>
<Label htmlFor="ignoreDuplicates" className="text-xs font-normal">
@@ -1240,7 +1274,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
<Checkbox
id="ignoreErrors"
checked={ignoreErrors}
onCheckedChange={(checked) => setIgnoreErrors(checked as boolean)}
onCheckedChange={(checked) => handleIgnoreErrorsChange(checked as boolean)}
/>
<Label htmlFor="ignoreErrors" className="text-xs font-normal">
@@ -1250,11 +1284,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
</div>
{/* 저장 버튼 */}
<div className="flex gap-2">
<Button onClick={handleSave} className="flex-1" size="sm">
</Button>
</div>
{/* 안내 */}
<div className="rounded bg-green-50 p-3 text-xs text-green-700">

View File

@@ -47,7 +47,7 @@ export function LogProperties({ nodeId, data }: LogPropertiesProps) {
const LevelIcon = selectedLevel?.icon || Info;
return (
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
<div className="flex items-center gap-2 rounded-md bg-gray-50 p-2">
<FileText className="h-4 w-4 text-gray-600" />
<span className="font-semibold text-gray-600"></span>

View File

@@ -263,7 +263,7 @@ export function ReferenceLookupProperties({ nodeId, data }: ReferenceLookupPrope
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
@@ -619,11 +619,6 @@ export function ReferenceLookupProperties({ nodeId, data }: ReferenceLookupPrope
</div>
{/* 저장 버튼 */}
<div className="flex gap-2">
<Button onClick={handleSave} className="flex-1" size="sm">
</Button>
</div>
{/* 안내 */}
<div className="space-y-2">

View File

@@ -124,7 +124,7 @@ export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertie
};
return (
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
<div className="flex items-center gap-2 rounded-md bg-teal-50 p-2">
<Globe className="h-4 w-4 text-teal-600" />
<span className="font-semibold text-teal-600">REST API </span>

View File

@@ -5,13 +5,14 @@
*/
import { useEffect, useState } from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { Check, ChevronsUpDown, Table, FileText } from "lucide-react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import { tableTypeApi } from "@/lib/api/screen";
@@ -34,6 +35,11 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro
const [displayName, setDisplayName] = useState(data.displayName || data.tableName);
const [tableName, setTableName] = useState(data.tableName);
// 🆕 데이터 소스 타입 (기본값: context-data)
const [dataSourceType, setDataSourceType] = useState<"context-data" | "table-all">(
(data as any).dataSourceType || "context-data"
);
// 테이블 선택 관련 상태
const [tables, setTables] = useState<TableOption[]>([]);
@@ -44,7 +50,8 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro
useEffect(() => {
setDisplayName(data.displayName || data.tableName);
setTableName(data.tableName);
}, [data.displayName, data.tableName]);
setDataSourceType((data as any).dataSourceType || "context-data");
}, [data.displayName, data.tableName, (data as any).dataSourceType]);
// 테이블 목록 로딩
useEffect(() => {
@@ -145,12 +152,22 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro
});
};
/**
* 🆕 데이터 소스 타입 변경 핸들러
*/
const handleDataSourceTypeChange = (newType: "context-data" | "table-all") => {
setDataSourceType(newType);
updateNode(nodeId, {
dataSourceType: newType,
});
console.log(`✅ 데이터 소스 타입 변경: ${newType}`);
};
// 현재 선택된 테이블의 라벨 찾기
const selectedTableLabel = tables.find((t) => t.tableName === tableName)?.label || tableName;
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
@@ -237,15 +254,77 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro
</div>
</div>
{/* 🆕 데이터 소스 설정 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
<div className="space-y-3">
<div>
<Label className="text-xs"> </Label>
<Select value={dataSourceType} onValueChange={handleDataSourceTypeChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="데이터 소스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="context-data">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<div className="flex flex-col">
<span className="font-medium"> </span>
<span className="text-muted-foreground text-xs">
(, )
</span>
</div>
</div>
</SelectItem>
<SelectItem value="table-all">
<div className="flex items-center gap-2">
<Table className="h-4 w-4" />
<div className="flex flex-col">
<span className="font-medium"> </span>
<span className="text-muted-foreground text-xs">
( )
</span>
</div>
</div>
</SelectItem>
</SelectContent>
</Select>
{/* 설명 텍스트 */}
<div className="mt-2 rounded bg-blue-50 p-3 text-xs text-blue-700">
{dataSourceType === "context-data" ? (
<>
<p className="font-medium mb-1">💡 </p>
<p> ( , ) .</p>
<p className="mt-1 text-blue-600"> 데이터: 1개 </p>
<p className="text-blue-600"> 선택: N개 </p>
</>
) : (
<>
<p className="font-medium mb-1">📊 </p>
<p> ** ** .</p>
<p className="mt-1 text-orange-600 font-medium"> </p>
</>
)}
</div>
</div>
</div>
</div>
{/* 필드 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
<h3 className="mb-3 text-sm font-semibold">
{data.fields && data.fields.length > 0 && `(${data.fields.length}개)`}
</h3>
{data.fields && data.fields.length > 0 ? (
<div className="space-y-1 rounded border p-2">
<div className="max-h-[300px] space-y-1 overflow-y-auto rounded border bg-gray-50 p-2">
{data.fields.map((field) => (
<div key={field.name} className="flex items-center justify-between text-xs">
<span className="font-mono text-gray-700">{field.name}</span>
<span className="text-gray-400">{field.type}</span>
<div key={field.name} className="flex items-center justify-between rounded bg-white px-2 py-1.5 text-xs">
<span className="truncate font-mono text-gray-700" title={field.name}>
{field.name}
</span>
<span className="ml-2 shrink-0 text-gray-400">{field.type}</span>
</div>
))}
</div>
@@ -254,9 +333,6 @@ export function TableSourceProperties({ nodeId, data }: TableSourcePropertiesPro
)}
</div>
{/* 안내 */}
<div className="rounded bg-green-50 p-3 text-xs text-green-700"> .</div>
</div>
</ScrollArea>
</div>
);
}

View File

@@ -378,31 +378,22 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
};
const handleAddMapping = () => {
setFieldMappings([
const newMappings = [
...fieldMappings,
{
sourceField: null,
targetField: "",
staticValue: undefined,
},
]);
];
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleRemoveMapping = (index: number) => {
const newMappings = fieldMappings.filter((_, i) => i !== index);
setFieldMappings(newMappings);
// 즉시 반영
updateNode(nodeId, {
displayName,
targetTable,
fieldMappings: newMappings,
whereConditions,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
ignoreErrors,
},
});
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleMappingChange = (index: number, field: string, value: any) => {
@@ -428,6 +419,7 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
}
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
// 🔥 타겟 타입 변경 핸들러
@@ -459,31 +451,22 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
};
const handleAddCondition = () => {
setWhereConditions([
const newConditions = [
...whereConditions,
{
field: "",
operator: "EQUALS",
staticValue: "",
},
]);
];
setWhereConditions(newConditions);
updateNode(nodeId, { whereConditions: newConditions });
};
const handleRemoveCondition = (index: number) => {
const newConditions = whereConditions.filter((_, i) => i !== index);
setWhereConditions(newConditions);
// 즉시 반영
updateNode(nodeId, {
displayName,
targetTable,
fieldMappings,
whereConditions: newConditions,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
ignoreErrors,
},
});
updateNode(nodeId, { whereConditions: newConditions });
};
const handleConditionChange = (index: number, field: string, value: any) => {
@@ -509,17 +492,41 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
}
setWhereConditions(newConditions);
updateNode(nodeId, { whereConditions: newConditions });
};
const handleSave = () => {
// 즉시 반영 핸들러들
const handleDisplayNameChange = (newDisplayName: string) => {
setDisplayName(newDisplayName);
updateNode(nodeId, { displayName: newDisplayName });
};
const handleFieldMappingsChange = (newMappings: any[]) => {
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleWhereConditionsChange = (newConditions: any[]) => {
setWhereConditions(newConditions);
updateNode(nodeId, { whereConditions: newConditions });
};
const handleBatchSizeChange = (newBatchSize: string) => {
setBatchSize(newBatchSize);
updateNode(nodeId, {
options: {
batchSize: newBatchSize ? parseInt(newBatchSize) : undefined,
ignoreErrors,
},
});
};
const handleIgnoreErrorsChange = (checked: boolean) => {
setIgnoreErrors(checked);
updateNode(nodeId, {
displayName,
targetTable,
fieldMappings,
whereConditions,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
ignoreErrors,
ignoreErrors: checked,
},
});
};
@@ -528,7 +535,7 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
@@ -1273,7 +1280,7 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
id="batchSize"
type="number"
value={batchSize}
onChange={(e) => setBatchSize(e.target.value)}
onChange={(e) => handleBatchSizeChange(e.target.value)}
className="mt-1"
placeholder="예: 100"
/>
@@ -1283,7 +1290,7 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
<Checkbox
id="ignoreErrors"
checked={ignoreErrors}
onCheckedChange={(checked) => setIgnoreErrors(checked as boolean)}
onCheckedChange={(checked) => handleIgnoreErrorsChange(checked as boolean)}
/>
<Label htmlFor="ignoreErrors" className="cursor-pointer text-xs font-normal">
@@ -1292,13 +1299,6 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
</div>
</div>
{/* 적용 버튼 */}
<div className="sticky bottom-0 border-t bg-white pt-3">
<Button onClick={handleSave} className="w-full" size="sm">
</Button>
<p className="mt-2 text-center text-xs text-gray-500"> .</p>
</div>
</div>
</ScrollArea>
);

View File

@@ -380,6 +380,10 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
setConflictKeys(newConflictKeys);
setConflictKeyLabels(newConflictKeyLabels);
updateNode(nodeId, {
conflictKeys: newConflictKeys,
conflictKeyLabels: newConflictKeyLabels,
});
}
};
@@ -389,48 +393,29 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
setConflictKeys(newKeys);
setConflictKeyLabels(newLabels);
// 즉시 반영
updateNode(nodeId, {
displayName,
targetTable,
conflictKeys: newKeys,
conflictKeyLabels: newLabels,
fieldMappings,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
updateOnConflict,
},
});
};
const handleAddMapping = () => {
setFieldMappings([
const newMappings = [
...fieldMappings,
{
sourceField: null,
targetField: "",
staticValue: undefined,
},
]);
];
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleRemoveMapping = (index: number) => {
const newMappings = fieldMappings.filter((_, i) => i !== index);
setFieldMappings(newMappings);
// 즉시 반영
updateNode(nodeId, {
displayName,
targetTable,
conflictKeys,
conflictKeyLabels,
fieldMappings: newMappings,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
updateOnConflict,
},
});
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleMappingChange = (index: number, field: string, value: any) => {
@@ -456,18 +441,41 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
}
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleSave = () => {
// 즉시 반영 핸들러들
const handleDisplayNameChange = (newDisplayName: string) => {
setDisplayName(newDisplayName);
updateNode(nodeId, { displayName: newDisplayName });
};
const handleConflictKeysChange = (newKeys: string[]) => {
setConflictKeys(newKeys);
updateNode(nodeId, { conflictKeys: newKeys });
};
const handleFieldMappingsChange = (newMappings: any[]) => {
setFieldMappings(newMappings);
updateNode(nodeId, { fieldMappings: newMappings });
};
const handleBatchSizeChange = (newBatchSize: string) => {
setBatchSize(newBatchSize);
updateNode(nodeId, {
options: {
batchSize: newBatchSize ? parseInt(newBatchSize) : undefined,
updateOnConflict,
},
});
};
const handleUpdateOnConflictChange = (checked: boolean) => {
setUpdateOnConflict(checked);
updateNode(nodeId, {
displayName,
targetTable,
conflictKeys,
conflictKeyLabels,
fieldMappings,
options: {
batchSize: batchSize ? parseInt(batchSize) : undefined,
updateOnConflict,
updateOnConflict: checked,
},
});
};
@@ -476,7 +484,7 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="space-y-4 p-4 pb-8">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
@@ -1145,13 +1153,6 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
</div>
</div>
{/* 적용 버튼 */}
<div className="sticky bottom-0 border-t bg-white pt-3">
<Button onClick={handleSave} className="w-full" size="sm">
</Button>
<p className="mt-2 text-center text-xs text-gray-500"> .</p>
</div>
</div>
</ScrollArea>
);