210 lines
7.4 KiB
TypeScript
210 lines
7.4 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* DELETE 액션 노드 속성 편집
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { useEffect, useState } from "react";
|
|||
|
|
import { Plus, Trash2, AlertTriangle } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|||
|
|
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
|||
|
|
import type { DeleteActionNodeData } from "@/types/node-editor";
|
|||
|
|
|
|||
|
|
interface DeleteActionPropertiesProps {
|
|||
|
|
nodeId: string;
|
|||
|
|
data: DeleteActionNodeData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const OPERATORS = [
|
|||
|
|
{ value: "EQUALS", label: "=" },
|
|||
|
|
{ value: "NOT_EQUALS", label: "≠" },
|
|||
|
|
{ value: "GREATER_THAN", label: ">" },
|
|||
|
|
{ value: "LESS_THAN", label: "<" },
|
|||
|
|
{ value: "IN", label: "IN" },
|
|||
|
|
{ value: "NOT_IN", label: "NOT IN" },
|
|||
|
|
] as const;
|
|||
|
|
|
|||
|
|
export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesProps) {
|
|||
|
|
const { updateNode } = useFlowEditorStore();
|
|||
|
|
|
|||
|
|
const [displayName, setDisplayName] = useState(data.displayName || `${data.targetTable} 삭제`);
|
|||
|
|
const [targetTable, setTargetTable] = useState(data.targetTable);
|
|||
|
|
const [whereConditions, setWhereConditions] = useState(data.whereConditions || []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setDisplayName(data.displayName || `${data.targetTable} 삭제`);
|
|||
|
|
setTargetTable(data.targetTable);
|
|||
|
|
setWhereConditions(data.whereConditions || []);
|
|||
|
|
}, [data]);
|
|||
|
|
|
|||
|
|
const handleAddCondition = () => {
|
|||
|
|
setWhereConditions([
|
|||
|
|
...whereConditions,
|
|||
|
|
{
|
|||
|
|
field: "",
|
|||
|
|
operator: "EQUALS",
|
|||
|
|
value: "",
|
|||
|
|
},
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRemoveCondition = (index: number) => {
|
|||
|
|
setWhereConditions(whereConditions.filter((_, i) => i !== index));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleConditionChange = (index: number, field: string, value: any) => {
|
|||
|
|
const newConditions = [...whereConditions];
|
|||
|
|
newConditions[index] = { ...newConditions[index], [field]: value };
|
|||
|
|
setWhereConditions(newConditions);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSave = () => {
|
|||
|
|
updateNode(nodeId, {
|
|||
|
|
displayName,
|
|||
|
|
targetTable,
|
|||
|
|
whereConditions,
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<ScrollArea className="h-full">
|
|||
|
|
<div className="space-y-4 p-4">
|
|||
|
|
{/* 경고 */}
|
|||
|
|
<div className="rounded-lg border-2 border-red-200 bg-red-50 p-4">
|
|||
|
|
<div className="flex items-start gap-3">
|
|||
|
|
<AlertTriangle className="h-5 w-5 flex-shrink-0 text-red-600" />
|
|||
|
|
<div className="text-sm">
|
|||
|
|
<p className="font-semibold text-red-800">위험한 작업입니다!</p>
|
|||
|
|
<p className="mt-1 text-xs text-red-700">
|
|||
|
|
DELETE 작업은 되돌릴 수 없습니다. WHERE 조건을 반드시 설정하세요.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 기본 정보 */}
|
|||
|
|
<div>
|
|||
|
|
<h3 className="mb-3 text-sm font-semibold">기본 정보</h3>
|
|||
|
|
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<div>
|
|||
|
|
<Label htmlFor="displayName" className="text-xs">
|
|||
|
|
표시 이름
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="displayName"
|
|||
|
|
value={displayName}
|
|||
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
|||
|
|
className="mt-1"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<Label htmlFor="targetTable" className="text-xs">
|
|||
|
|
타겟 테이블
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="targetTable"
|
|||
|
|
value={targetTable}
|
|||
|
|
onChange={(e) => setTargetTable(e.target.value)}
|
|||
|
|
className="mt-1"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* WHERE 조건 */}
|
|||
|
|
<div>
|
|||
|
|
<div className="mb-2 flex items-center justify-between">
|
|||
|
|
<h3 className="text-sm font-semibold">WHERE 조건 (필수)</h3>
|
|||
|
|
<Button size="sm" variant="outline" onClick={handleAddCondition} className="h-7">
|
|||
|
|
<Plus className="mr-1 h-3 w-3" />
|
|||
|
|
추가
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{whereConditions.length > 0 ? (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{whereConditions.map((condition, index) => (
|
|||
|
|
<div key={index} className="rounded border-2 border-red-200 bg-red-50 p-2">
|
|||
|
|
<div className="mb-2 flex items-center justify-between">
|
|||
|
|
<span className="text-xs font-medium text-red-700">조건 #{index + 1}</span>
|
|||
|
|
<Button
|
|||
|
|
size="sm"
|
|||
|
|
variant="ghost"
|
|||
|
|
onClick={() => handleRemoveCondition(index)}
|
|||
|
|
className="h-6 w-6 p-0"
|
|||
|
|
>
|
|||
|
|
<Trash2 className="h-3 w-3" />
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div>
|
|||
|
|
<Label className="text-xs text-gray-600">필드</Label>
|
|||
|
|
<Input
|
|||
|
|
value={condition.field}
|
|||
|
|
onChange={(e) => handleConditionChange(index, "field", e.target.value)}
|
|||
|
|
placeholder="조건 필드명"
|
|||
|
|
className="mt-1 h-8 text-xs"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<Label className="text-xs text-gray-600">연산자</Label>
|
|||
|
|
<Select
|
|||
|
|
value={condition.operator}
|
|||
|
|
onValueChange={(value) => handleConditionChange(index, "operator", value)}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger className="mt-1 h-8 text-xs">
|
|||
|
|
<SelectValue />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
{OPERATORS.map((op) => (
|
|||
|
|
<SelectItem key={op.value} value={op.value}>
|
|||
|
|
{op.label}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<Label className="text-xs text-gray-600">값</Label>
|
|||
|
|
<Input
|
|||
|
|
value={condition.value as string}
|
|||
|
|
onChange={(e) => handleConditionChange(index, "value", e.target.value)}
|
|||
|
|
placeholder="비교 값"
|
|||
|
|
className="mt-1 h-8 text-xs"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="rounded border-2 border-dashed border-red-300 bg-red-50 p-4 text-center text-xs text-red-600">
|
|||
|
|
⚠️ WHERE 조건이 없습니다! 모든 데이터가 삭제됩니다!
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</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">
|
|||
|
|
🚨 WHERE 조건 없이 삭제하면 테이블의 모든 데이터가 영구 삭제됩니다!
|
|||
|
|
</div>
|
|||
|
|
<div className="rounded bg-red-50 p-3 text-xs text-red-700">💡 실행 전 WHERE 조건을 꼭 확인하세요.</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</ScrollArea>
|
|||
|
|
);
|
|||
|
|
}
|