585 lines
23 KiB
TypeScript
585 lines
23 KiB
TypeScript
|
|
/**
|
||
|
|
* DELETE 조건 패널
|
||
|
|
* DELETE 액션용 조건 설정 및 안전장치 컴포넌트
|
||
|
|
*/
|
||
|
|
|
||
|
|
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 { Switch } from "@/components/ui/switch";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Plus, X, Search, AlertCircle, Shield, Trash2 } from "lucide-react";
|
||
|
|
import { Separator } from "@/components/ui/separator";
|
||
|
|
import { ColumnInfo, getColumnsFromConnection } from "@/lib/api/multiConnection";
|
||
|
|
import { useToast } from "@/hooks/use-toast";
|
||
|
|
|
||
|
|
export interface DeleteCondition {
|
||
|
|
id: string;
|
||
|
|
fromColumn: string;
|
||
|
|
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN" | "EXISTS" | "NOT EXISTS";
|
||
|
|
value: string | string[];
|
||
|
|
logicalOperator?: "AND" | "OR";
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DeleteWhereCondition {
|
||
|
|
id: string;
|
||
|
|
toColumn: string;
|
||
|
|
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
|
||
|
|
valueSource: "from_column" | "static" | "condition_result";
|
||
|
|
fromColumn?: string;
|
||
|
|
staticValue?: string;
|
||
|
|
logicalOperator?: "AND" | "OR";
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DeleteSafetySettings {
|
||
|
|
maxDeleteCount: number;
|
||
|
|
requireConfirmation: boolean;
|
||
|
|
dryRunFirst: boolean;
|
||
|
|
logAllDeletes: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DeleteConditionPanelProps {
|
||
|
|
action: any;
|
||
|
|
actionIndex: number;
|
||
|
|
settings: any;
|
||
|
|
onSettingsChange: (settings: any) => void;
|
||
|
|
fromConnectionId?: number;
|
||
|
|
toConnectionId?: number;
|
||
|
|
fromTableName?: string;
|
||
|
|
toTableName?: string;
|
||
|
|
disabled?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const DeleteConditionPanel: React.FC<DeleteConditionPanelProps> = ({
|
||
|
|
action,
|
||
|
|
actionIndex,
|
||
|
|
settings,
|
||
|
|
onSettingsChange,
|
||
|
|
fromConnectionId,
|
||
|
|
toConnectionId,
|
||
|
|
fromTableName,
|
||
|
|
toTableName,
|
||
|
|
disabled = false,
|
||
|
|
}) => {
|
||
|
|
const { toast } = useToast();
|
||
|
|
|
||
|
|
// 상태 관리
|
||
|
|
const [fromTableColumns, setFromTableColumns] = useState<ColumnInfo[]>([]);
|
||
|
|
const [toTableColumns, setToTableColumns] = useState<ColumnInfo[]>([]);
|
||
|
|
const [deleteConditions, setDeleteConditions] = useState<DeleteCondition[]>([]);
|
||
|
|
const [whereConditions, setWhereConditions] = useState<DeleteWhereCondition[]>([]);
|
||
|
|
const [safetySettings, setSafetySettings] = useState<DeleteSafetySettings>({
|
||
|
|
maxDeleteCount: 100,
|
||
|
|
requireConfirmation: true,
|
||
|
|
dryRunFirst: true,
|
||
|
|
logAllDeletes: true,
|
||
|
|
});
|
||
|
|
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),
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// DELETE 트리거 조건 추가
|
||
|
|
const addDeleteCondition = () => {
|
||
|
|
const newCondition: DeleteCondition = {
|
||
|
|
id: `delete_condition_${Date.now()}`,
|
||
|
|
fromColumn: "",
|
||
|
|
operator: "=",
|
||
|
|
value: "",
|
||
|
|
logicalOperator: deleteConditions.length > 0 ? "AND" : undefined,
|
||
|
|
};
|
||
|
|
setDeleteConditions([...deleteConditions, newCondition]);
|
||
|
|
};
|
||
|
|
|
||
|
|
// DELETE 트리거 조건 제거
|
||
|
|
const removeDeleteCondition = (id: string) => {
|
||
|
|
setDeleteConditions(deleteConditions.filter((c) => c.id !== id));
|
||
|
|
};
|
||
|
|
|
||
|
|
// DELETE 트리거 조건 수정
|
||
|
|
const updateDeleteCondition = (id: string, field: keyof DeleteCondition, value: any) => {
|
||
|
|
setDeleteConditions(deleteConditions.map((c) => (c.id === id ? { ...c, [field]: value } : c)));
|
||
|
|
};
|
||
|
|
|
||
|
|
// WHERE 조건 추가
|
||
|
|
const addWhereCondition = () => {
|
||
|
|
const newCondition: DeleteWhereCondition = {
|
||
|
|
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 DeleteWhereCondition, value: any) => {
|
||
|
|
setWhereConditions(whereConditions.map((c) => (c.id === id ? { ...c, [field]: value } : c)));
|
||
|
|
};
|
||
|
|
|
||
|
|
// 안전장치 설정 수정
|
||
|
|
const updateSafetySettings = (field: keyof DeleteSafetySettings, value: any) => {
|
||
|
|
setSafetySettings((prev) => ({ ...prev, [field]: value }));
|
||
|
|
};
|
||
|
|
|
||
|
|
// 자기 자신 테이블 작업 경고
|
||
|
|
const getSelfTableWarning = () => {
|
||
|
|
if (fromConnectionId === toConnectionId && fromTableName === toTableName) {
|
||
|
|
return "🚨 자기 자신 테이블 DELETE 작업입니다. 매우 위험할 수 있으므로 신중히 설정하세요.";
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 위험한 조건 검사
|
||
|
|
const getDangerousConditionsWarning = () => {
|
||
|
|
const dangerousOperators = ["!=", "NOT IN", "NOT EXISTS"];
|
||
|
|
const hasDangerousConditions = deleteConditions.some((condition) =>
|
||
|
|
dangerousOperators.includes(condition.operator),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (hasDangerousConditions) {
|
||
|
|
return "⚠️ 부정 조건(!=, NOT IN, NOT EXISTS)은 예상보다 많은 데이터를 삭제할 수 있습니다.";
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
};
|
||
|
|
|
||
|
|
const warningMessage = getSelfTableWarning();
|
||
|
|
const dangerousWarning = getDangerousConditionsWarning();
|
||
|
|
const filteredFromColumns = getFilteredColumns(fromTableColumns, fromColumnSearch);
|
||
|
|
const filteredToColumns = getFilteredColumns(toTableColumns, toColumnSearch);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* 경고 메시지 */}
|
||
|
|
{warningMessage && (
|
||
|
|
<Alert variant="destructive">
|
||
|
|
<AlertCircle className="h-4 w-4" />
|
||
|
|
<AlertDescription>{warningMessage}</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{dangerousWarning && (
|
||
|
|
<Alert variant="default">
|
||
|
|
<AlertCircle className="h-4 w-4" />
|
||
|
|
<AlertDescription>{dangerousWarning}</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* DELETE 트리거 조건 설정 */}
|
||
|
|
<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">
|
||
|
|
{deleteConditions.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) => updateDeleteCondition(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) => updateDeleteCondition(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) => updateDeleteCondition(condition.id, "operator", value)}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="w-24">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="=">=</SelectItem>
|
||
|
|
<SelectItem value="!=">
|
||
|
|
<span className="text-red-600">!=</span>
|
||
|
|
</SelectItem>
|
||
|
|
<SelectItem value=">">></SelectItem>
|
||
|
|
<SelectItem value="<"><</SelectItem>
|
||
|
|
<SelectItem value=">=">>=</SelectItem>
|
||
|
|
<SelectItem value="<="><=</SelectItem>
|
||
|
|
<SelectItem value="LIKE">LIKE</SelectItem>
|
||
|
|
<SelectItem value="IN">IN</SelectItem>
|
||
|
|
<SelectItem value="NOT IN">
|
||
|
|
<span className="text-red-600">NOT IN</span>
|
||
|
|
</SelectItem>
|
||
|
|
<SelectItem value="EXISTS">EXISTS</SelectItem>
|
||
|
|
<SelectItem value="NOT EXISTS">
|
||
|
|
<span className="text-red-600">NOT EXISTS</span>
|
||
|
|
</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
|
||
|
|
<Input
|
||
|
|
placeholder="값"
|
||
|
|
value={condition.value as string}
|
||
|
|
onChange={(e) => updateDeleteCondition(condition.id, "value", e.target.value)}
|
||
|
|
className="flex-1"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Button variant="outline" size="sm" onClick={() => removeDeleteCondition(condition.id)}>
|
||
|
|
<X className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
|
||
|
|
<Button variant="outline" onClick={addDeleteCondition} className="w-full" disabled={disabled}>
|
||
|
|
<Plus className="mr-2 h-4 w-4" />
|
||
|
|
삭제 조건 추가
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* DELETE WHERE 조건 설정 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center gap-2">🎯 삭제 대상 조건</CardTitle>
|
||
|
|
<CardDescription>TO 테이블에서 어떤 레코드를 삭제할지 WHERE 조건을 설정하세요</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>
|
||
|
|
|
||
|
|
{/* 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=">">></SelectItem>
|
||
|
|
<SelectItem value="<"><</SelectItem>
|
||
|
|
<SelectItem value=">=">>=</SelectItem>
|
||
|
|
<SelectItem value="<="><=</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="condition_result">조건 결과</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>안전을 위해 DELETE 작업에는 최소 하나 이상의 WHERE 조건이 필요합니다.</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* 안전장치 설정 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center gap-2">
|
||
|
|
<Shield className="h-4 w-4" />
|
||
|
|
삭제 안전장치
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>예상치 못한 대량 삭제를 방지하기 위한 안전장치를 설정하세요</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6">
|
||
|
|
{/* 최대 삭제 개수 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="maxDeleteCount">최대 삭제 개수</Label>
|
||
|
|
<Input
|
||
|
|
id="maxDeleteCount"
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="10000"
|
||
|
|
value={safetySettings.maxDeleteCount}
|
||
|
|
onChange={(e) => updateSafetySettings("maxDeleteCount", parseInt(e.target.value))}
|
||
|
|
className="w-32"
|
||
|
|
/>
|
||
|
|
<p className="text-muted-foreground text-sm">한 번에 삭제할 수 있는 최대 레코드 수를 제한합니다.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 안전장치 옵션들 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-0.5">
|
||
|
|
<Label htmlFor="requireConfirmation">삭제 확인 요구</Label>
|
||
|
|
<p className="text-muted-foreground text-sm">삭제 실행 전 추가 확인을 요구합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="requireConfirmation"
|
||
|
|
checked={safetySettings.requireConfirmation}
|
||
|
|
onCheckedChange={(checked) => updateSafetySettings("requireConfirmation", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-0.5">
|
||
|
|
<Label htmlFor="dryRunFirst">Dry Run 먼저 실행</Label>
|
||
|
|
<p className="text-muted-foreground text-sm">실제 삭제 전에 삭제 대상 개수를 먼저 확인합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="dryRunFirst"
|
||
|
|
checked={safetySettings.dryRunFirst}
|
||
|
|
onCheckedChange={(checked) => updateSafetySettings("dryRunFirst", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-0.5">
|
||
|
|
<Label htmlFor="logAllDeletes">모든 삭제 기록</Label>
|
||
|
|
<p className="text-muted-foreground text-sm">삭제된 모든 레코드를 로그에 기록합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="logAllDeletes"
|
||
|
|
checked={safetySettings.logAllDeletes}
|
||
|
|
onCheckedChange={(checked) => updateSafetySettings("logAllDeletes", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 자기 자신 테이블 추가 안전장치 */}
|
||
|
|
{fromConnectionId === toConnectionId && fromTableName === toTableName && (
|
||
|
|
<Alert variant="destructive">
|
||
|
|
<Trash2 className="h-4 w-4" />
|
||
|
|
<AlertDescription className="space-y-2">
|
||
|
|
<div className="font-medium">자기 자신 테이블 삭제 시 강화된 안전장치:</div>
|
||
|
|
<ul className="list-inside list-disc space-y-1 text-sm">
|
||
|
|
<li>최대 삭제 개수가 자동으로 {Math.min(safetySettings.maxDeleteCount, 10)}개로 제한됩니다</li>
|
||
|
|
<li>부정 조건(!=, NOT IN, NOT EXISTS) 사용이 금지됩니다</li>
|
||
|
|
<li>WHERE 조건을 2개 이상 설정하는 것을 강력히 권장합니다</li>
|
||
|
|
</ul>
|
||
|
|
</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|