플로우 페이지네이션 안보임
This commit is contained in:
@@ -28,9 +28,23 @@ export function PropertiesPanel() {
|
||||
const selectedNode = selectedNodes.length === 1 ? nodes.find((n) => n.id === selectedNodes[0]) : null;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex h-16 shrink-0 items-center justify-between border-b bg-white p-4">
|
||||
<div
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
height: '64px'
|
||||
}}
|
||||
className="flex items-center justify-between border-b bg-white p-4"
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">속성</h3>
|
||||
{selectedNode && (
|
||||
@@ -44,11 +58,11 @@ export function PropertiesPanel() {
|
||||
|
||||
{/* 내용 - 스크롤 가능 영역 */}
|
||||
<div
|
||||
className="flex-1 overflow-y-scroll"
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 64px)',
|
||||
overflowY: 'scroll',
|
||||
WebkitOverflowScrolling: 'touch'
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
>
|
||||
{selectedNodes.length === 0 ? (
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 플로우 검증 결과 패널
|
||||
*
|
||||
* 모든 검증 결과를 사이드바에 표시
|
||||
*/
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import { AlertTriangle, AlertCircle, Info, ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import type { FlowValidation } from "@/lib/utils/flowValidation";
|
||||
import { summarizeValidations } from "@/lib/utils/flowValidation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ValidationPanelProps {
|
||||
validations: FlowValidation[];
|
||||
onNodeClick?: (nodeId: string) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ValidationPanel = memo(
|
||||
({ validations, onNodeClick, onClose }: ValidationPanelProps) => {
|
||||
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set());
|
||||
|
||||
const summary = useMemo(
|
||||
() => summarizeValidations(validations),
|
||||
[validations]
|
||||
);
|
||||
|
||||
// 타입별로 그룹화
|
||||
const groupedValidations = useMemo(() => {
|
||||
const groups = new Map<string, FlowValidation[]>();
|
||||
for (const validation of validations) {
|
||||
if (!groups.has(validation.type)) {
|
||||
groups.set(validation.type, []);
|
||||
}
|
||||
groups.get(validation.type)!.push(validation);
|
||||
}
|
||||
return Array.from(groups.entries()).sort((a, b) => {
|
||||
// 심각도 순으로 정렬
|
||||
const severityOrder = { error: 0, warning: 1, info: 2 };
|
||||
const aSeverity = Math.min(
|
||||
...a[1].map((v) => severityOrder[v.severity])
|
||||
);
|
||||
const bSeverity = Math.min(
|
||||
...b[1].map((v) => severityOrder[v.severity])
|
||||
);
|
||||
return aSeverity - bSeverity;
|
||||
});
|
||||
}, [validations]);
|
||||
|
||||
const toggleExpanded = (type: string) => {
|
||||
setExpandedTypes((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(type)) {
|
||||
next.delete(type);
|
||||
} else {
|
||||
next.add(type);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
"parallel-conflict": "병렬 실행 충돌",
|
||||
"missing-where": "WHERE 조건 누락",
|
||||
"circular-reference": "순환 참조",
|
||||
"data-source-mismatch": "데이터 소스 불일치",
|
||||
"parallel-table-access": "병렬 테이블 접근",
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
if (validations.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col border-l border-gray-200 bg-white">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900">검증 결과</h3>
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center p-8 text-center">
|
||||
<div>
|
||||
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<Info className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-900">문제 없음</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
플로우에 문제가 발견되지 않았습니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col border-l border-gray-200 bg-white">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900">검증 결과</h3>
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 요약 */}
|
||||
<div className="border-b border-gray-200 bg-gray-50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{summary.errorCount > 0 && (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
오류 {summary.errorCount}
|
||||
</Badge>
|
||||
)}
|
||||
{summary.warningCount > 0 && (
|
||||
<Badge className="gap-1 bg-yellow-500 hover:bg-yellow-600">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
경고 {summary.warningCount}
|
||||
</Badge>
|
||||
)}
|
||||
{summary.infoCount > 0 && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
정보 {summary.infoCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{summary.hasBlockingIssues && (
|
||||
<p className="mt-2 text-xs text-red-600">
|
||||
⛔ 오류를 해결해야 저장할 수 있습니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 검증 결과 목록 */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2">
|
||||
{groupedValidations.map(([type, typeValidations]) => {
|
||||
const isExpanded = expandedTypes.has(type);
|
||||
const firstValidation = typeValidations[0];
|
||||
const Icon =
|
||||
firstValidation.severity === "error"
|
||||
? AlertCircle
|
||||
: firstValidation.severity === "warning"
|
||||
? AlertTriangle
|
||||
: Info;
|
||||
|
||||
return (
|
||||
<div key={type} className="mb-2">
|
||||
{/* 그룹 헤더 */}
|
||||
<button
|
||||
onClick={() => toggleExpanded(type)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-lg p-3 text-left transition-colors",
|
||||
firstValidation.severity === "error"
|
||||
? "bg-red-50 hover:bg-red-100"
|
||||
: firstValidation.severity === "warning"
|
||||
? "bg-yellow-50 hover:bg-yellow-100"
|
||||
: "bg-blue-50 hover:bg-blue-100"
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0",
|
||||
firstValidation.severity === "error"
|
||||
? "text-red-600"
|
||||
: firstValidation.severity === "warning"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{getTypeLabel(type)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{typeValidations.length}개 발견
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 상세 내용 */}
|
||||
{isExpanded && (
|
||||
<div className="mt-1 space-y-1 pl-6 pr-2">
|
||||
{typeValidations.map((validation, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-3 transition-all hover:border-gray-300 hover:shadow-sm"
|
||||
onClick={() => onNodeClick?.(validation.nodeId)}
|
||||
>
|
||||
<div className="text-xs text-gray-700">
|
||||
{validation.message}
|
||||
</div>
|
||||
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
영향받는 노드: {validation.affectedNodes.length}개
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 text-[10px] text-gray-400 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
클릭하여 노드 보기
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ValidationPanel.displayName = "ValidationPanel";
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2 } 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 { ConditionNodeData } from "@/types/node-editor";
|
||||
@@ -214,7 +213,7 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 기본 정보 */}
|
||||
<div>
|
||||
@@ -420,6 +419,6 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2, Wand2, ArrowRight } 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 { DataTransformNodeData } from "@/types/node-editor";
|
||||
@@ -358,7 +357,7 @@ export function DataTransformProperties({ nodeId, data }: DataTransformPropertie
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-2 rounded-md bg-indigo-50 p-2">
|
||||
@@ -454,6 +453,6 @@ export function DataTransformProperties({ nodeId, data }: DataTransformPropertie
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2, AlertTriangle, Database, Globe, Link2, Check, ChevronsUpD
|
||||
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 { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -216,7 +215,7 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 경고 */}
|
||||
<div className="rounded-lg border-2 border-red-200 bg-red-50 p-4">
|
||||
@@ -714,6 +713,6 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
|
||||
<div className="rounded bg-red-50 p-3 text-xs text-red-700">💡 실행 전 WHERE 조건을 꼭 확인하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight, Database, Globe, Link2
|
||||
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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -49,9 +48,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
const [displayName, setDisplayName] = useState(data.displayName || data.targetTable);
|
||||
const [targetTable, setTargetTable] = useState(data.targetTable);
|
||||
const [fieldMappings, setFieldMappings] = useState(data.fieldMappings || []);
|
||||
const [batchSize, setBatchSize] = useState(data.options?.batchSize?.toString() || "");
|
||||
const [ignoreErrors, setIgnoreErrors] = useState(data.options?.ignoreErrors || false);
|
||||
const [ignoreDuplicates, setIgnoreDuplicates] = useState(data.options?.ignoreDuplicates || false);
|
||||
|
||||
// 내부 DB 테이블 관련 상태
|
||||
const [tables, setTables] = useState<TableOption[]>([]);
|
||||
@@ -92,9 +88,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
setDisplayName(data.displayName || data.targetTable);
|
||||
setTargetTable(data.targetTable);
|
||||
setFieldMappings(data.fieldMappings || []);
|
||||
setBatchSize(data.options?.batchSize?.toString() || "");
|
||||
setIgnoreErrors(data.options?.ignoreErrors || false);
|
||||
setIgnoreDuplicates(data.options?.ignoreDuplicates || false);
|
||||
}, [data]);
|
||||
|
||||
// 내부 DB 테이블 목록 로딩
|
||||
@@ -439,11 +432,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
displayName: selectedTable.label,
|
||||
targetTable: selectedTable.tableName,
|
||||
fieldMappings,
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors,
|
||||
ignoreDuplicates,
|
||||
},
|
||||
});
|
||||
|
||||
setTablesOpen(false);
|
||||
@@ -517,39 +505,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
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, {
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors,
|
||||
ignoreDuplicates: checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectedTableLabel = tables.find((t) => t.tableName === targetTable)?.label || targetTable;
|
||||
|
||||
// 🔥 타겟 타입 변경 핸들러
|
||||
@@ -575,17 +530,12 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
}
|
||||
|
||||
updates.fieldMappings = fieldMappings;
|
||||
updates.options = {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors,
|
||||
ignoreDuplicates,
|
||||
};
|
||||
|
||||
updateNode(nodeId, updates);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 🔥 타겟 타입 선택 */}
|
||||
<div>
|
||||
@@ -753,11 +703,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
externalDbType: selectedConnection?.db_type,
|
||||
externalTargetTable: undefined,
|
||||
fieldMappings,
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors,
|
||||
ignoreDuplicates,
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={externalConnectionsLoading}
|
||||
@@ -797,11 +742,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
externalConnectionId: selectedExternalConnectionId,
|
||||
externalTargetTable: value,
|
||||
fieldMappings,
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors,
|
||||
ignoreDuplicates,
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={externalTablesLoading}
|
||||
@@ -1240,51 +1180,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 옵션 */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">옵션</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="batchSize" className="text-xs">
|
||||
배치 크기
|
||||
</Label>
|
||||
<Input
|
||||
id="batchSize"
|
||||
type="number"
|
||||
value={batchSize}
|
||||
onChange={(e) => handleBatchSizeChange(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="한 번에 처리할 레코드 수"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="ignoreDuplicates"
|
||||
checked={ignoreDuplicates}
|
||||
onCheckedChange={(checked) => handleIgnoreDuplicatesChange(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="ignoreDuplicates" className="text-xs font-normal">
|
||||
중복 데이터 무시
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="ignoreErrors"
|
||||
checked={ignoreErrors}
|
||||
onCheckedChange={(checked) => handleIgnoreErrorsChange(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="ignoreErrors" className="text-xs font-normal">
|
||||
오류 발생 시 계속 진행
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 저장 버튼 */}
|
||||
|
||||
{/* 안내 */}
|
||||
<div className="rounded bg-green-50 p-3 text-xs text-green-700">
|
||||
✅ 테이블과 필드는 실제 데이터베이스에서 조회됩니다.
|
||||
@@ -1292,6 +1187,6 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
💡 소스 필드가 없으면 정적 값이 사용됩니다.
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2, Search } 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
@@ -262,7 +261,7 @@ export function ReferenceLookupProperties({ nodeId, data }: ReferenceLookupPrope
|
||||
const selectedTableLabel = tables.find((t) => t.tableName === referenceTable)?.label || referenceTable;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 기본 정보 */}
|
||||
<div>
|
||||
@@ -633,6 +632,6 @@ export function ReferenceLookupProperties({ nodeId, data }: ReferenceLookupPrope
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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";
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight, Database, Globe, Link2
|
||||
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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -65,8 +64,6 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
const [targetTable, setTargetTable] = useState(data.targetTable);
|
||||
const [fieldMappings, setFieldMappings] = useState(data.fieldMappings || []);
|
||||
const [whereConditions, setWhereConditions] = useState(data.whereConditions || []);
|
||||
const [batchSize, setBatchSize] = useState(data.options?.batchSize?.toString() || "");
|
||||
const [ignoreErrors, setIgnoreErrors] = useState(data.options?.ignoreErrors || false);
|
||||
|
||||
// 내부 DB 테이블 관련 상태
|
||||
const [tables, setTables] = useState<TableOption[]>([]);
|
||||
@@ -108,8 +105,6 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
setTargetTable(data.targetTable);
|
||||
setFieldMappings(data.fieldMappings || []);
|
||||
setWhereConditions(data.whereConditions || []);
|
||||
setBatchSize(data.options?.batchSize?.toString() || "");
|
||||
setIgnoreErrors(data.options?.ignoreErrors || false);
|
||||
}, [data]);
|
||||
|
||||
// 내부 DB 테이블 목록 로딩
|
||||
@@ -368,10 +363,6 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
targetTable: newTableName,
|
||||
fieldMappings,
|
||||
whereConditions,
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors,
|
||||
},
|
||||
});
|
||||
|
||||
setTablesOpen(false);
|
||||
@@ -511,31 +502,10 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
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, {
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
ignoreErrors: checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectedTableLabel = tables.find((t) => t.tableName === targetTable)?.label || targetTable;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 기본 정보 */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">기본 정보</h3>
|
||||
@@ -1268,38 +1238,6 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 옵션 */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">옵션</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="batchSize" className="text-xs">
|
||||
배치 크기
|
||||
</Label>
|
||||
<Input
|
||||
id="batchSize"
|
||||
type="number"
|
||||
value={batchSize}
|
||||
onChange={(e) => handleBatchSizeChange(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="예: 100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="ignoreErrors"
|
||||
checked={ignoreErrors}
|
||||
onCheckedChange={(checked) => handleIgnoreErrorsChange(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="ignoreErrors" className="cursor-pointer text-xs font-normal">
|
||||
오류 무시
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight, Database, Globe, Link2
|
||||
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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -51,8 +50,6 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
const [conflictKeys, setConflictKeys] = useState<string[]>(data.conflictKeys || []);
|
||||
const [conflictKeyLabels, setConflictKeyLabels] = useState<string[]>(data.conflictKeyLabels || []);
|
||||
const [fieldMappings, setFieldMappings] = useState(data.fieldMappings || []);
|
||||
const [batchSize, setBatchSize] = useState(data.options?.batchSize?.toString() || "");
|
||||
const [updateOnConflict, setUpdateOnConflict] = useState(data.options?.updateOnConflict ?? true);
|
||||
|
||||
// 🔥 외부 DB 관련 상태
|
||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||
@@ -95,8 +92,6 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
setConflictKeys(data.conflictKeys || []);
|
||||
setConflictKeyLabels(data.conflictKeyLabels || []);
|
||||
setFieldMappings(data.fieldMappings || []);
|
||||
setBatchSize(data.options?.batchSize?.toString() || "");
|
||||
setUpdateOnConflict(data.options?.updateOnConflict ?? true);
|
||||
}, [data]);
|
||||
|
||||
// 🔥 내부 DB 테이블 목록 로딩
|
||||
@@ -363,10 +358,6 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
conflictKeys,
|
||||
conflictKeyLabels,
|
||||
fieldMappings,
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
updateOnConflict,
|
||||
},
|
||||
});
|
||||
|
||||
setTablesOpen(false);
|
||||
@@ -460,30 +451,10 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
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, {
|
||||
options: {
|
||||
batchSize: batchSize ? parseInt(batchSize) : undefined,
|
||||
updateOnConflict: checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectedTableLabel = tables.find((t) => t.tableName === targetTable)?.label || targetTable;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
{/* 기본 정보 */}
|
||||
<div>
|
||||
@@ -1122,38 +1093,7 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 옵션 */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">옵션</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="batchSize" className="text-xs">
|
||||
배치 크기
|
||||
</Label>
|
||||
<Input
|
||||
id="batchSize"
|
||||
type="number"
|
||||
value={batchSize}
|
||||
onChange={(e) => setBatchSize(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="예: 100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="updateOnConflict"
|
||||
checked={updateOnConflict}
|
||||
onCheckedChange={(checked) => setUpdateOnConflict(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="updateOnConflict" className="cursor-pointer text-xs font-normal">
|
||||
충돌 시 업데이트 (ON CONFLICT DO UPDATE)
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user