/** * 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 = ({ action, actionIndex, settings, onSettingsChange, fromConnectionId, toConnectionId, fromTableName, toTableName, disabled = false, }) => { const { toast } = useToast(); // 상태 관리 const [fromTableColumns, setFromTableColumns] = useState([]); const [toTableColumns, setToTableColumns] = useState([]); const [deleteConditions, setDeleteConditions] = useState([]); const [whereConditions, setWhereConditions] = useState([]); const [safetySettings, setSafetySettings] = useState({ 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>, 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 (
{/* 경고 메시지 */} {warningMessage && ( {warningMessage} )} {dangerousWarning && ( {dangerousWarning} )} {/* DELETE 트리거 조건 설정 */} 🔥 삭제 트리거 조건 FROM 테이블에서 어떤 조건을 만족하는 데이터가 있을 때 TO 테이블에서 삭제를 실행할지 설정하세요 {/* 검색 필드 */}
setFromColumnSearch(e.target.value)} className="pl-9" />
{/* 삭제 트리거 조건 리스트 */}
{deleteConditions.map((condition, index) => (
{index > 0 && ( )} updateDeleteCondition(condition.id, "value", e.target.value)} className="flex-1" />
))}
{/* DELETE WHERE 조건 설정 */} 🎯 삭제 대상 조건 TO 테이블에서 어떤 레코드를 삭제할지 WHERE 조건을 설정하세요 {/* 검색 필드 */}
setFromColumnSearch(e.target.value)} className="pl-9" />
setToColumnSearch(e.target.value)} className="pl-9" />
{/* WHERE 조건 리스트 */}
{whereConditions.map((condition, index) => (
{index > 0 && ( )} {condition.valueSource === "from_column" && ( )} {condition.valueSource === "static" && ( updateWhereCondition(condition.id, "staticValue", e.target.value)} className="w-32" /> )}
))}
{whereConditions.length === 0 && ( 안전을 위해 DELETE 작업에는 최소 하나 이상의 WHERE 조건이 필요합니다. )}
{/* 안전장치 설정 */} 삭제 안전장치 예상치 못한 대량 삭제를 방지하기 위한 안전장치를 설정하세요 {/* 최대 삭제 개수 */}
updateSafetySettings("maxDeleteCount", parseInt(e.target.value))} className="w-32" />

한 번에 삭제할 수 있는 최대 레코드 수를 제한합니다.

{/* 안전장치 옵션들 */}

삭제 실행 전 추가 확인을 요구합니다.

updateSafetySettings("requireConfirmation", checked)} />

실제 삭제 전에 삭제 대상 개수를 먼저 확인합니다.

updateSafetySettings("dryRunFirst", checked)} />

삭제된 모든 레코드를 로그에 기록합니다.

updateSafetySettings("logAllDeletes", checked)} />
{/* 자기 자신 테이블 추가 안전장치 */} {fromConnectionId === toConnectionId && fromTableName === toTableName && (
자기 자신 테이블 삭제 시 강화된 안전장치:
  • 최대 삭제 개수가 자동으로 {Math.min(safetySettings.maxDeleteCount, 10)}개로 제한됩니다
  • 부정 조건(!=, NOT IN, NOT EXISTS) 사용이 금지됩니다
  • WHERE 조건을 2개 이상 설정하는 것을 강력히 권장합니다
)}
); };