데이터 저장 구현 #29
|
|
@ -8,8 +8,8 @@ import { Label } from "@/components/ui/label";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
|
||||
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo } from "@/lib/api/dataflow";
|
||||
import { Link, Key, Save, Globe, Plus, Zap, Trash2 } from "lucide-react";
|
||||
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
// 연결 정보 타입
|
||||
|
|
@ -36,7 +36,7 @@ interface ConnectionInfo {
|
|||
relationshipName: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
settings?: any;
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,6 @@ interface ConnectionSetupModalProps {
|
|||
isOpen: boolean;
|
||||
connection: ConnectionInfo | null;
|
||||
companyCode: string;
|
||||
diagramId?: number;
|
||||
onConfirm: (relationship: TableRelationship) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
|
@ -85,7 +84,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
isOpen,
|
||||
connection,
|
||||
companyCode,
|
||||
diagramId,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
|
|
@ -127,6 +125,12 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
||||
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
||||
|
||||
// 조건부 연결을 위한 새로운 상태들
|
||||
const [triggerType, setTriggerType] = useState<"insert" | "update" | "delete" | "insert_update">("insert");
|
||||
const [conditions, setConditions] = useState<ConditionNode[]>([]);
|
||||
const [rollbackOnError, setRollbackOnError] = useState(true);
|
||||
const [enableLogging, setEnableLogging] = useState(true);
|
||||
|
||||
// 테이블 목록 로드
|
||||
useEffect(() => {
|
||||
const loadTables = async () => {
|
||||
|
|
@ -166,7 +170,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
connectionType: (existingRel?.connectionType as "simple-key" | "data-save" | "external-call") || "simple-key",
|
||||
fromColumnName: "",
|
||||
toColumnName: "",
|
||||
description: existingRel?.settings?.description || `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`,
|
||||
description:
|
||||
(existingRel?.settings?.description as string) || `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`,
|
||||
settings: existingRel?.settings || {},
|
||||
});
|
||||
|
||||
|
|
@ -282,6 +287,32 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const fromTableName = selectedFromTable || connection.fromNode.tableName;
|
||||
const toTableName = selectedToTable || connection.toNode.tableName;
|
||||
|
||||
// 조건부 연결 설정 데이터 준비
|
||||
const conditionalSettings = isConditionalConnection()
|
||||
? {
|
||||
control: {
|
||||
triggerType,
|
||||
conditionTree:
|
||||
conditions.length > 0
|
||||
? {
|
||||
type: "group" as const,
|
||||
operator: "AND" as const,
|
||||
children: conditions,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
category: {
|
||||
type: config.connectionType,
|
||||
rollbackOnError,
|
||||
enableLogging,
|
||||
},
|
||||
plan: {
|
||||
sourceTable: fromTableName,
|
||||
targetActions: [], // 나중에 액션 설정 UI에서 채울 예정
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
||||
const relationshipData: TableRelationship = {
|
||||
relationship_name: config.relationshipName,
|
||||
|
|
@ -289,11 +320,12 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
to_table_name: toTableName,
|
||||
from_column_name: selectedFromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
to_column_name: selectedToColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
relationship_type: config.relationshipType as any,
|
||||
connection_type: config.connectionType as any,
|
||||
relationship_type: config.relationshipType,
|
||||
connection_type: config.connectionType,
|
||||
company_code: companyCode,
|
||||
settings: {
|
||||
...settings,
|
||||
...conditionalSettings, // 조건부 연결 설정 추가
|
||||
description: config.description,
|
||||
multiColumnMapping: {
|
||||
fromColumns: selectedFromColumns,
|
||||
|
|
@ -330,14 +362,165 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
|
||||
if (!connection) return null;
|
||||
|
||||
// 선택된 컬럼 데이터 가져오기
|
||||
const selectedColumnsData = connection.selectedColumnsData || {};
|
||||
const tableNames = Object.keys(selectedColumnsData);
|
||||
const fromTable = tableNames[0];
|
||||
const toTable = tableNames[1];
|
||||
// 선택된 컬럼 데이터 가져오기 (현재 사용되지 않음 - 향후 확장을 위해 유지)
|
||||
// const selectedColumnsData = connection.selectedColumnsData || {};
|
||||
|
||||
const fromTableData = selectedColumnsData[fromTable];
|
||||
const toTableData = selectedColumnsData[toTable];
|
||||
// 조건부 연결인지 확인하는 헬퍼 함수
|
||||
const isConditionalConnection = () => {
|
||||
return config.connectionType === "data-save" || config.connectionType === "external-call";
|
||||
};
|
||||
|
||||
// 조건 관리 헬퍼 함수들
|
||||
const addCondition = () => {
|
||||
const newCondition: ConditionNode = {
|
||||
type: "condition",
|
||||
field: "",
|
||||
operator_type: "=",
|
||||
value: "",
|
||||
dataType: "string",
|
||||
};
|
||||
setConditions([...conditions, newCondition]);
|
||||
};
|
||||
|
||||
const updateCondition = (index: number, field: keyof ConditionNode, value: string) => {
|
||||
const updatedConditions = [...conditions];
|
||||
updatedConditions[index] = { ...updatedConditions[index], [field]: value };
|
||||
setConditions(updatedConditions);
|
||||
};
|
||||
|
||||
const removeCondition = (index: number) => {
|
||||
const updatedConditions = conditions.filter((_, i) => i !== index);
|
||||
setConditions(updatedConditions);
|
||||
};
|
||||
|
||||
// 조건부 연결 설정 UI 렌더링
|
||||
const renderConditionalSettings = () => {
|
||||
return (
|
||||
<div className="rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 p-4">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-sm font-medium">조건부 실행 설정</span>
|
||||
</div>
|
||||
|
||||
{/* 트리거 타입 선택 */}
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="triggerType" className="mb-2 block text-sm font-medium">
|
||||
실행 시점
|
||||
</Label>
|
||||
<Select
|
||||
value={triggerType}
|
||||
onValueChange={(value: "insert" | "update" | "delete" | "insert_update") => setTriggerType(value)}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="insert">데이터 생성 시 (INSERT)</SelectItem>
|
||||
<SelectItem value="update">데이터 수정 시 (UPDATE)</SelectItem>
|
||||
<SelectItem value="delete">데이터 삭제 시 (DELETE)</SelectItem>
|
||||
<SelectItem value="insert_update">생성/수정 시 (INSERT/UPDATE)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 실행 조건 설정 */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">실행 조건</Label>
|
||||
<Button size="sm" variant="outline" onClick={() => addCondition()} className="h-7 text-xs">
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
조건 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 조건 목록 */}
|
||||
<div className="space-y-2">
|
||||
{conditions.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-gray-500">
|
||||
조건을 추가하면 해당 조건을 만족할 때만 실행됩니다.
|
||||
<br />
|
||||
조건이 없으면 항상 실행됩니다.
|
||||
</div>
|
||||
) : (
|
||||
conditions.map((condition, index) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded border bg-white p-2">
|
||||
<Select
|
||||
value={condition.field || ""}
|
||||
onValueChange={(value) => updateCondition(index, "field", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fromTableColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnName} ({column.dataType})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={condition.operator_type || "="}
|
||||
onValueChange={(value) => updateCondition(index, "operator_type", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-20 text-xs">
|
||||
<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="IS_NULL">IS NULL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="값"
|
||||
value={condition.value || ""}
|
||||
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
||||
<Button size="sm" variant="ghost" onClick={() => removeCondition(index)} className="h-8 w-8 p-0">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 추가 옵션 */}
|
||||
<div className="grid grid-cols-2 gap-4 border-t pt-2">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rollbackOnError}
|
||||
onChange={(e) => setRollbackOnError(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
오류 시 롤백
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableLogging}
|
||||
onChange={(e) => setEnableLogging(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
실행 로그 저장
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 연결 종류별 설정 패널 렌더링
|
||||
const renderConnectionTypeSettings = () => {
|
||||
|
|
@ -724,6 +907,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조건부 연결을 위한 조건 설정 */}
|
||||
{isConditionalConnection() && renderConditionalSettings()}
|
||||
|
||||
{/* 연결 종류별 상세 설정 */}
|
||||
{renderConnectionTypeSettings()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1254,7 +1254,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
isOpen={!!pendingConnection}
|
||||
connection={pendingConnection}
|
||||
companyCode={companyCode}
|
||||
diagramId={currentDiagramId || diagramId || (relationshipId ? parseInt(relationshipId) : undefined)}
|
||||
onConfirm={handleConfirmConnection}
|
||||
onCancel={handleCancelConnection}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue