데이터 저장

This commit is contained in:
hyeonsu
2025-09-15 20:07:28 +09:00
parent 6a04ae450d
commit 2c677c2fb8
7 changed files with 636 additions and 253 deletions

View File

@@ -62,9 +62,9 @@ interface DataSaveSettings {
conditions?: ConditionNode[];
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
sourceField: string;
targetTable?: string;
targetField: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}>;
@@ -188,6 +188,10 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
actions: [],
});
// 🔥 필드 선택 상태 초기화
setSelectedFromColumns([]);
setSelectedToColumns([]);
// 외부 호출 기본값 설정
setExternalCallSettings({
callType: "rest-api",
@@ -325,9 +329,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 단순 키값 연결일 때만 컬럼 선택 검증
if (config.connectionType === "simple-key") {
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
return;
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
return;
}
}
@@ -622,7 +626,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
const renderActionCondition = (condition: ConditionNode, condIndex: number, actionIndex: number) => {
// 그룹 시작 렌더링
if (condition.type === "group-start") {
return (
return (
<div key={condition.id} className="flex items-center gap-2">
{/* 그룹 시작 앞의 논리 연산자 */}
{condIndex > 0 && (
@@ -663,8 +667,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
>
<Trash2 className="h-2 w-2" />
</Button>
</div>
</div>
</div>
</div>
);
}
@@ -692,13 +696,13 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
>
<Trash2 className="h-2 w-2" />
</Button>
</div>
</div>
</div>
);
);
}
// 일반 조건 렌더링 (기존 로직 간소화)
return (
return (
<div key={condition.id} className="flex items-center gap-2">
{/* 그룹 내 첫 번째 조건이 아닐 때만 논리 연산자 표시 */}
{condIndex > 0 && dataSaveSettings.actions[actionIndex].conditions![condIndex - 1]?.type !== "group-start" && (
@@ -772,7 +776,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
if (dataType.includes("timestamp") || dataType.includes("datetime") || dataType.includes("date")) {
return (
<Input
<Input
type="datetime-local"
value={String(condition.value || "")}
onChange={(e) => {
@@ -785,7 +789,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
);
} else if (dataType.includes("time")) {
return (
<Input
<Input
type="time"
value={String(condition.value || "")}
onChange={(e) => {
@@ -876,9 +880,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
className="h-6 w-6 p-0"
>
<Trash2 className="h-2 w-2" />
</Button>
</div>
</div>
</Button>
</div>
</div>
);
};
@@ -889,7 +893,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<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>
{/* 실행 조건 설정 */}
<div className="mb-4">
@@ -906,7 +910,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<Button size="sm" variant="outline" onClick={() => addGroupEnd()} className="h-7 text-xs">
)
</Button>
</div>
</div>
</div>
{/* 조건 목록 */}
@@ -953,14 +957,14 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
);
</div>
</div>
);
}
// 그룹 끝 렌더링
if (condition.type === "group-end") {
return (
return (
<div key={condition.id} className="flex items-center gap-2">
<div
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
@@ -976,7 +980,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
</div>
);
}
@@ -991,13 +995,13 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "logicalOperator", value)}
>
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
</SelectContent>
</Select>
)}
{/* 그룹 레벨에 따른 들여쓰기와 조건 필드들 */}
@@ -1052,7 +1056,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
dataType.includes("date")
) {
return (
<Input
<Input
type="datetime-local"
value={String(condition.value || "")}
onChange={(e) => updateCondition(index, "value", e.target.value)}
@@ -1095,18 +1099,18 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
);
} else if (dataType.includes("bool")) {
return (
<Select
<Select
value={String(condition.value || "")}
onValueChange={(value) => updateCondition(index, "value", value)}
>
<SelectTrigger className="h-8 flex-1 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
</SelectTrigger>
<SelectContent>
<SelectItem value="true">TRUE</SelectItem>
<SelectItem value="false">FALSE</SelectItem>
</SelectContent>
</Select>
</SelectContent>
</Select>
);
} else {
return (
@@ -1124,147 +1128,147 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<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>
</div>
</div>
);
</div>
</div>
);
};
// 연결 종류별 설정 패널 렌더링
const renderConnectionTypeSettings = () => {
switch (config.connectionType) {
case "simple-key":
return (
<div className="space-y-4">
{/* 테이블 및 컬럼 선택 */}
<div className="rounded-lg border bg-gray-50 p-4">
<div className="mb-4 text-sm font-medium"> </div>
return (
<div className="space-y-4">
{/* 테이블 및 컬럼 선택 */}
<div className="rounded-lg border bg-gray-50 p-4">
<div className="mb-4 text-sm font-medium"> </div>
{/* 현재 선택된 테이블 표시 */}
<div className="mb-4 grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600">From </Label>
<div className="mt-1">
<span className="text-sm font-medium text-gray-800">
{availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable}
</span>
<span className="ml-2 text-xs text-gray-500">({selectedFromTable})</span>
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600">To </Label>
<div className="mt-1">
<span className="text-sm font-medium text-gray-800">
{availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable}
</span>
<span className="ml-2 text-xs text-gray-500">({selectedToTable})</span>
</div>
{/* 현재 선택된 테이블 표시 */}
<div className="mb-4 grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600">From </Label>
<div className="mt-1">
<span className="text-sm font-medium text-gray-800">
{availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable}
</span>
<span className="ml-2 text-xs text-gray-500">({selectedFromTable})</span>
</div>
</div>
{/* 컬럼 선택 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600">From </Label>
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
{fromTableColumns.map((column) => (
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
<input
type="checkbox"
checked={selectedFromColumns.includes(column.columnName)}
onChange={(e) => {
if (e.target.checked) {
setSelectedFromColumns((prev) => [...prev, column.columnName]);
} else {
setSelectedFromColumns((prev) => prev.filter((col) => col !== column.columnName));
}
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
{fromTableColumns.length === 0 && (
<div className="py-2 text-xs text-gray-500">
{selectedFromTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
</div>
)}
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600">To </Label>
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
{toTableColumns.map((column) => (
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
<input
type="checkbox"
checked={selectedToColumns.includes(column.columnName)}
onChange={(e) => {
if (e.target.checked) {
setSelectedToColumns((prev) => [...prev, column.columnName]);
} else {
setSelectedToColumns((prev) => prev.filter((col) => col !== column.columnName));
}
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
{toTableColumns.length === 0 && (
<div className="py-2 text-xs text-gray-500">
{selectedToTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
</div>
)}
</div>
<div>
<Label className="text-xs font-medium text-gray-600">To </Label>
<div className="mt-1">
<span className="text-sm font-medium text-gray-800">
{availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable}
</span>
<span className="ml-2 text-xs text-gray-500">({selectedToTable})</span>
</div>
</div>
{/* 선택된 컬럼 미리보기 */}
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
<div className="mt-4 grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600"> From </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedFromColumns.length > 0 ? (
selectedFromColumns.map((column) => (
<Badge key={column} variant="outline" className="text-xs">
{column}
</Badge>
))
) : (
<span className="text-xs text-gray-400"> </span>
)}
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600"> To </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedToColumns.length > 0 ? (
selectedToColumns.map((column) => (
<Badge key={column} variant="secondary" className="text-xs">
{column}
</Badge>
))
) : (
<span className="text-xs text-gray-400"> </span>
)}
</div>
</div>
</div>
)}
</div>
{/* 컬럼 선택 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600">From </Label>
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
{fromTableColumns.map((column) => (
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
<input
type="checkbox"
checked={selectedFromColumns.includes(column.columnName)}
onChange={(e) => {
if (e.target.checked) {
setSelectedFromColumns((prev) => [...prev, column.columnName]);
} else {
setSelectedFromColumns((prev) => prev.filter((col) => col !== column.columnName));
}
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
{fromTableColumns.length === 0 && (
<div className="py-2 text-xs text-gray-500">
{selectedFromTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
</div>
)}
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600">To </Label>
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
{toTableColumns.map((column) => (
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
<input
type="checkbox"
checked={selectedToColumns.includes(column.columnName)}
onChange={(e) => {
if (e.target.checked) {
setSelectedToColumns((prev) => [...prev, column.columnName]);
} else {
setSelectedToColumns((prev) => prev.filter((col) => col !== column.columnName));
}
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
{toTableColumns.length === 0 && (
<div className="py-2 text-xs text-gray-500">
{selectedToTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
</div>
)}
</div>
</div>
</div>
{/* 선택된 컬럼 미리보기 */}
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
<div className="mt-4 grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600"> From </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedFromColumns.length > 0 ? (
selectedFromColumns.map((column) => (
<Badge key={column} variant="outline" className="text-xs">
{column}
</Badge>
))
) : (
<span className="text-xs text-gray-400"> </span>
)}
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600"> To </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedToColumns.length > 0 ? (
selectedToColumns.map((column) => (
<Badge key={column} variant="secondary" className="text-xs">
{column}
</Badge>
))
) : (
<span className="text-xs text-gray-400"> </span>
)}
</div>
</div>
</div>
)}
</div>
{/* 단순 키값 연결 설정 */}
<div className="rounded-lg border border-l-4 border-l-blue-500 bg-blue-50/30 p-4">
<div className="mb-3 flex items-center gap-2">
@@ -1272,7 +1276,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<span className="text-sm font-medium"> </span>
</div>
<div className="space-y-3">
<div>
<div>
<Label htmlFor="notes" className="text-sm">
</Label>
@@ -1339,7 +1343,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
{dataSaveSettings.actions.map((action, actionIndex) => (
<div key={action.id} className="rounded border bg-white p-3">
<div className="mb-3 flex items-center justify-between">
<Input
<Input
value={action.name}
onChange={(e) => {
const newActions = [...dataSaveSettings.actions];
@@ -1629,18 +1633,26 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
{/* 소스 */}
<div className="flex items-center gap-1 rounded bg-blue-50 px-2 py-1">
<Select
value={mapping.sourceTable || ""}
value={mapping.sourceTable || "__EMPTY__"}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = value;
// 🔥 "__EMPTY__" 값을 빈 문자열로 변환
const actualValue = value === "__EMPTY__" ? "" : value;
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = actualValue;
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = "";
// 🔥 소스 선택 시 기본값 초기화 (빈 값이 아닐 때만)
if (actualValue) {
newActions[actionIndex].fieldMappings[mappingIndex].defaultValue = "";
}
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
disabled={!!(mapping.defaultValue && mapping.defaultValue.trim())}
>
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
<SelectValue placeholder="테이블" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__EMPTY__"> ( )</SelectItem>
{availableTables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
<div className="truncate" title={table.tableName}>
@@ -1650,15 +1662,36 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
))}
</SelectContent>
</Select>
{/* 🔥 FROM 테이블 클리어 버튼 */}
{mapping.sourceTable && (
<button
onClick={() => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = "";
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = "";
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-gray-400 hover:bg-gray-200 hover:text-gray-600"
title="소스 테이블 지우기"
>
×
</button>
)}
<span className="text-gray-400">.</span>
<Select
value={mapping.sourceField}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = value;
// 🔥 소스 필드 선택 시 기본값 초기화
if (value) {
newActions[actionIndex].fieldMappings[mappingIndex].defaultValue = "";
}
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
disabled={!mapping.sourceTable}
disabled={
!mapping.sourceTable || !!(mapping.defaultValue && mapping.defaultValue.trim())
}
>
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
<SelectValue placeholder="컬럼" />
@@ -1734,8 +1767,14 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
onChange={(e) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].defaultValue = e.target.value;
// 🔥 기본값 입력 시 소스 테이블/필드 초기화
if (e.target.value.trim()) {
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = "";
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = "";
}
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
disabled={!!mapping.sourceTable}
className="h-6 w-20 text-xs"
placeholder="기본값"
/>
@@ -1811,31 +1850,31 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
value={externalCallSettings.apiUrl}
onChange={(e) => setExternalCallSettings({ ...externalCallSettings, apiUrl: e.target.value })}
placeholder="https://api.example.com/webhook"
className="text-sm"
/>
</div>
className="text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<div>
<Label htmlFor="httpMethod" className="text-sm">
HTTP Method
</Label>
<Select
<Select
value={externalCallSettings.httpMethod}
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
setExternalCallSettings({ ...externalCallSettings, httpMethod: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="headers" className="text-sm">
Headers
@@ -1957,7 +1996,52 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<Button variant="outline" onClick={handleCancel}>
</Button>
<Button onClick={handleConfirm} disabled={!config.relationshipName}>
<Button
onClick={handleConfirm}
disabled={(() => {
const hasRelationshipName = !!config.relationshipName;
const isDataSave = config.connectionType === "data-save";
const hasActions = dataSaveSettings.actions.length > 0;
const allActionsHaveMappings = dataSaveSettings.actions.every(
(action) => action.fieldMappings.length > 0,
);
const allMappingsComplete = dataSaveSettings.actions.every((action) =>
action.fieldMappings.every((mapping) => {
// 타겟은 항상 필요
if (!mapping.targetTable || !mapping.targetField) return false;
// 소스와 기본값 중 하나는 있어야 함
const hasSource = mapping.sourceTable && mapping.sourceField;
const hasDefault = mapping.defaultValue && mapping.defaultValue.trim();
// FROM 테이블이 비어있으면 기본값이 필요
if (!mapping.sourceTable) {
return !!hasDefault;
}
// FROM 테이블이 있으면 소스 매핑 완성 또는 기본값 필요
return hasSource || hasDefault;
}),
);
console.log("🔍 버튼 비활성화 디버깅:", {
hasRelationshipName,
isDataSave,
hasActions,
allActionsHaveMappings,
allMappingsComplete,
dataSaveSettings,
config,
});
const shouldDisable =
!hasRelationshipName ||
(isDataSave && (!hasActions || !allActionsHaveMappings || !allMappingsComplete));
console.log("🔍 최종 비활성화 여부:", shouldDisable);
return shouldDisable;
})()}
>
</Button>
</DialogFooter>

View File

@@ -73,7 +73,7 @@ interface DataFlowDesignerProps {
// 내부에서 사용할 확장된 JsonRelationship 타입 (connectionType 포함)
interface ExtendedJsonRelationship extends JsonRelationship {
connectionType: string;
connectionType: "simple-key" | "data-save" | "external-call";
}
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
@@ -111,7 +111,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
existingRelationship?: {
relationshipName: string;
connectionType: string;
settings?: any;
settings?: Record<string, unknown>;
};
} | null>(null);
const [relationships, setRelationships] = useState<TableRelationship[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
@@ -136,7 +136,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const [currentDiagramCategory, setCurrentDiagramCategory] = useState<string>("simple-key"); // 현재 관계도의 연결 종류
const [selectedEdgeForEdit, setSelectedEdgeForEdit] = useState<Edge | null>(null); // 수정/삭제할 엣지
const [showEdgeActions, setShowEdgeActions] = useState(false); // 엣지 액션 버튼 표시 상태
const [edgeActionPosition, setEdgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치
const [edgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치 (사용하지 않지만 기존 코드 호환성 유지)
const [editingRelationshipId, setEditingRelationshipId] = useState<string | null>(null); // 현재 수정 중인 관계 ID
const [showRelationshipListModal, setShowRelationshipListModal] = useState(false); // 관계 목록 모달 표시 상태
const [selectedTablePairRelationships, setSelectedTablePairRelationships] = useState<ExtendedJsonRelationship[]>([]); // 선택된 테이블 쌍의 관계들
@@ -225,15 +225,28 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("📋 관계 목록:", relationships);
console.log("📊 테이블 목록:", tableNames);
// 🔥 수정: category 배열에서 각 관계의 connectionType 복원
const categoryMap = new Map<string, string>();
if (Array.isArray(jsonDiagram.category)) {
jsonDiagram.category.forEach((cat: { id: string; category: string }) => {
if (cat.id && cat.category) {
categoryMap.set(cat.id, cat.category);
}
});
}
// 기존 데이터에서 relationshipName이 없는 경우 기본값 설정
// category를 각 관계의 connectionType으로 복원
const normalizedRelationships: ExtendedJsonRelationship[] = relationships.map((rel: JsonRelationship) => ({
...rel,
relationshipName: rel.relationshipName || `${rel.fromTable}${rel.toTable}`, // 기본값 설정
connectionType: jsonDiagram.category || "simple-key", // 관계도의 category를 각 관계의 connectionType으로 복원
connectionType: (rel.connectionType || categoryMap.get(rel.id) || "simple-key") as
| "simple-key"
| "data-save"
| "external-call", // category 배열에서 복원
}));
// 메모리에 관계 저장 (기존 관계도 편집 시)
console.log("🔥 정규화된 관계들:", normalizedRelationships);
setTempRelationships(normalizedRelationships);
setCurrentDiagramId(currentDiagramId);
setCurrentDiagramCategory(jsonDiagram.category || "simple-key"); // 관계도의 연결 종류 설정
@@ -341,15 +354,21 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const relationshipEdges: Edge[] = [];
const tableRelationshipCount: { [key: string]: number } = {}; // 테이블 쌍별 관계 개수
console.log("🔥 엣지 생성 시작 - 관계 개수:", normalizedRelationships.length);
normalizedRelationships.forEach((rel: ExtendedJsonRelationship) => {
console.log("🔥 관계 처리 중:", rel.id, rel.connectionType, rel.fromTable, "→", rel.toTable);
const fromTable = rel.fromTable;
const toTable = rel.toTable;
const fromColumns = rel.fromColumns || [];
const toColumns = rel.toColumns || [];
// 🔥 수정: 컬럼 정보가 없어도 엣지는 생성 (data-save 연결 등에서는 컬럼이 없을 수 있음)
if (fromColumns.length === 0 || toColumns.length === 0) {
console.warn("⚠️ 컬럼 정보가 없니다:", { fromColumns, toColumns });
return;
console.warn("⚠️ 컬럼 정보가 없지만 엣지는 생성합니다:", {
fromColumns,
toColumns,
connectionType: rel.connectionType,
});
}
// 테이블 쌍 키 생성 (양방향 동일하게 처리)
@@ -404,6 +423,15 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length);
console.log("📍 관계 에지 상세:", relationshipEdges);
console.log(
"🔥 최종 엣지 설정 전 확인:",
relationshipEdges.map((e) => ({
id: e.id,
source: e.source,
target: e.target,
connectionType: e.data?.connectionType,
})),
);
setEdges(relationshipEdges);
toast.success("관계도를 불러왔습니다.", { id: "load-diagram" });
} catch (error) {
@@ -708,7 +736,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
toTable,
fromColumns,
toColumns,
connectionType: relationship.connection_type,
connectionType: relationship.connection_type as "simple-key" | "data-save" | "external-call",
settings: relationship.settings || {},
};
@@ -723,6 +751,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setTempRelationships((prev) => [...prev, newRelationship]);
setHasUnsavedChanges(true);
console.log("🔥 새 관계 생성:", newRelationship);
console.log("🔥 연결 타입:", newRelationship.connectionType);
// 첫 번째 관계가 추가되면 관계도의 category를 해당 connectionType으로 설정
if (tempRelationships.length === 0) {
setCurrentDiagramCategory(relationship.connection_type);
@@ -743,18 +774,19 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
data: {
relationshipId: newRelationship.id,
relationshipName: newRelationship.relationshipName,
connectionType: relationship.connection_type,
connectionType: newRelationship.connectionType, // 🔥 수정: newRelationship 사용
fromTable,
toTable,
fromColumns,
toColumns,
details: {
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
connectionType: relationship.connection_type,
connectionType: newRelationship.connectionType, // 🔥 수정: newRelationship 사용
},
},
};
console.log("🔥 새 엣지 생성:", newEdge);
setEdges((eds) => [...eds, newEdge]);
setPendingConnection(null);
@@ -764,7 +796,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("메모리에 관계 생성 완료:", newRelationship);
toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요.");
},
[pendingConnection, setEdges],
[pendingConnection, setEdges, editingRelationshipId, tempRelationships.length],
);
// 연결 설정 취소
@@ -811,26 +843,114 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("📋 연결된 테이블 목록:", connectedTables);
console.log("🔗 관계 개수:", tempRelationships.length);
// 관계도의 주요 연결 타입 결정 (첫 번째 관계의 connectionType 사용)
const primaryConnectionType = tempRelationships.length > 0 ? tempRelationships[0].connectionType : "simple-key";
// 🔥 주요 연결 타입 변수 제거 (더 이상 사용하지 않음)
// connectionType을 관계에서 제거하고 관계도 레벨로 이동
const relationshipsWithoutConnectionType = tempRelationships.map((rel) => {
const { connectionType, ...relationshipWithoutType } = rel;
return relationshipWithoutType;
// 🔥 수정: relationships는 핵심 관계 정보만 포함, settings 전체 제거
const cleanRelationships = tempRelationships.map((rel) => {
// 🔥 settings 전체를 제거하고 핵심 정보만 유지
const cleanRel: JsonRelationship = {
id: rel.id,
fromTable: rel.fromTable,
toTable: rel.toTable,
relationshipName: rel.relationshipName,
connectionType: rel.connectionType,
// simple-key가 아닌 경우 컬럼 정보 제거
fromColumns: rel.connectionType === "simple-key" ? rel.fromColumns : [],
toColumns: rel.connectionType === "simple-key" ? rel.toColumns : [],
};
return cleanRel;
});
// 저장 요청 데이터 생성
const createRequest: CreateDiagramRequest = {
diagram_name: diagramName,
relationships: {
relationships: relationshipsWithoutConnectionType,
relationships: cleanRelationships as JsonRelationship[],
tables: connectedTables,
},
node_positions: nodePositions,
category: primaryConnectionType, // connectionType을 관계도 레벨의 category로 이동
// 🔥 수정: 각 관계별 category 정보를 배열로 저장
category: tempRelationships.map((rel) => ({
id: rel.id,
category: rel.connectionType,
})),
// 🔥 각 관계별 control 정보를 배열로 저장 (전체 실행 조건)
control: tempRelationships
.filter((rel) => rel.connectionType === "data-save")
.map((rel) => {
console.log("🔍 Control 데이터 추출 중:", {
id: rel.id,
settings: rel.settings,
control: rel.settings?.control,
settingsKeys: Object.keys(rel.settings || {}),
});
const controlData = rel.settings?.control as {
triggerType?: "insert" | "update" | "delete";
conditionTree?: Array<{
field: string;
operator_type: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: unknown;
logicalOperator?: "AND" | "OR";
}>;
};
console.log("🔍 추출된 controlData:", controlData);
console.log("🔍 conditionTree:", controlData?.conditionTree);
return {
id: rel.id, // relationships의 id와 동일
triggerType: (controlData?.triggerType as "insert" | "update" | "delete") || "insert",
// 🔥 실제 저장된 conditionTree에서 조건 추출
conditions: (controlData?.conditionTree || []).map((cond) => ({
field: cond.field,
operator: cond.operator_type,
value: cond.value,
logicalOperator: cond.logicalOperator || "AND",
})),
};
}),
// 🔥 각 관계별 plan 정보를 배열로 저장 (저장 액션)
plan: tempRelationships
.filter((rel) => rel.connectionType === "data-save")
.map((rel) => ({
id: rel.id, // relationships의 id와 동일
sourceTable: rel.fromTable,
// 🔥 실제 사용자가 설정한 액션들 사용
actions:
(rel.settings?.actions as Array<{
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}>;
conditions?: Array<{
id: string;
type: string;
field: string;
operator_type: string;
value: unknown;
logicalOperator?: string;
}>;
}>) || [],
})),
};
// 🔍 디버깅: tempRelationships 구조 확인
console.log("🔍 tempRelationships 전체 구조:", JSON.stringify(tempRelationships, null, 2));
tempRelationships.forEach((rel, index) => {
console.log(`🔍 관계 ${index + 1} settings:`, rel.settings);
console.log(`🔍 관계 ${index + 1} settings.control:`, rel.settings?.control);
console.log(`🔍 관계 ${index + 1} settings.actions:`, rel.settings?.actions);
});
console.log("🚀 API 요청 데이터:", JSON.stringify(createRequest, null, 2));
let savedDiagram;
@@ -1181,35 +1301,121 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
{/* 관계 목록 */}
<div className="p-3">
<div className="max-h-96 space-y-2 overflow-y-auto">
{selectedTablePairRelationships.map((relationship, index) => (
{selectedTablePairRelationships.map((relationship) => (
<div
key={relationship.id}
onClick={() => {
// 관계 선택 시 수정 모드로 전환
setEditingRelationshipId(relationship.id);
// 관련 컬럼 하이라이트
const newSelectedColumns: { [tableName: string]: string[] } = {};
if (relationship.fromTable && relationship.fromColumns) {
newSelectedColumns[relationship.fromTable] = [...relationship.fromColumns];
}
if (relationship.toTable && relationship.toColumns) {
newSelectedColumns[relationship.toTable] = [...relationship.toColumns];
}
setSelectedColumns(newSelectedColumns);
// 모달 닫기
setShowRelationshipListModal(false);
}}
className="cursor-pointer rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-blue-50"
className="rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-blue-50"
>
<div className="mb-1 flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">
{relationship.fromTable} {relationship.toTable}
</h4>
<svg className="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div className="flex items-center gap-1">
{/* 편집 버튼 */}
<button
onClick={(e) => {
e.stopPropagation();
// 관계 선택 시 수정 모드로 전환
setEditingRelationshipId(relationship.id);
// 관련 컬럼 하이라이트
const newSelectedColumns: { [tableName: string]: string[] } = {};
if (relationship.fromTable && relationship.fromColumns) {
newSelectedColumns[relationship.fromTable] = [...relationship.fromColumns];
}
if (relationship.toTable && relationship.toColumns) {
newSelectedColumns[relationship.toTable] = [...relationship.toColumns];
}
setSelectedColumns(newSelectedColumns);
// 🔥 수정: 연결 설정 모달 열기
const fromTable = nodes.find(
(node) => node.data?.table?.tableName === relationship.fromTable,
);
const toTable = nodes.find(
(node) => node.data?.table?.tableName === relationship.toTable,
);
if (fromTable && toTable) {
setPendingConnection({
fromNode: {
id: fromTable.id,
tableName: relationship.fromTable,
displayName: fromTable.data?.table?.displayName || relationship.fromTable,
},
toNode: {
id: toTable.id,
tableName: relationship.toTable,
displayName: toTable.data?.table?.displayName || relationship.toTable,
},
selectedColumnsData: {
[relationship.fromTable]: {
displayName: fromTable.data?.table?.displayName || relationship.fromTable,
columns: relationship.fromColumns || [],
},
[relationship.toTable]: {
displayName: toTable.data?.table?.displayName || relationship.toTable,
columns: relationship.toColumns || [],
},
},
existingRelationship: {
relationshipName: relationship.relationshipName,
connectionType: relationship.connectionType,
settings: relationship.settings || {},
},
});
}
// 모달 닫기
setShowRelationshipListModal(false);
}}
className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-blue-100 hover:text-blue-600"
title="관계 편집"
>
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
{/* 삭제 버튼 */}
<button
onClick={(e) => {
e.stopPropagation();
// 관계 삭제
setTempRelationships((prev) => prev.filter((rel) => rel.id !== relationship.id));
setEdges((prev) =>
prev.filter((edge) => edge.data?.relationshipId !== relationship.id),
);
setHasUnsavedChanges(true);
// 선택된 컬럼 초기화
setSelectedColumns({});
// 편집 모드 해제
if (editingRelationshipId === relationship.id) {
setEditingRelationshipId(null);
}
// 모달 닫기
setShowRelationshipListModal(false);
}}
className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-red-100 hover:text-red-600"
title="관계 삭제"
>
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
<div className="space-y-1 text-xs text-gray-600">
<p>: {relationship.connectionType}</p>
@@ -1474,7 +1680,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
isOpen={showSaveModal}
onClose={handleCloseSaveModal}
onSave={handleSaveDiagram}
relationships={tempRelationships}
relationships={tempRelationships as JsonRelationship[]} // 타입 단언 추가
defaultName={
diagramId && diagramId > 0 && currentDiagramName
? currentDiagramName // 편집 모드: 기존 관계도 이름