데이터 저장까지 구현 #30

Merged
hyeonsu merged 11 commits from dataflowMng into dev 2025-09-16 17:57:45 +09:00
4 changed files with 776 additions and 424 deletions
Showing only changes of commit d18e78e8a0 - Show all commits

View File

@ -6,7 +6,6 @@ import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { DataFlowDesigner } from "@/components/dataflow/DataFlowDesigner";
import { DataFlowAPI } from "@/lib/api/dataflow";
import { toast } from "sonner";
export default function DataFlowEditPage() {
const params = useParams();
@ -80,6 +79,7 @@ export default function DataFlowEditPage() {
{/* 데이터플로우 디자이너 */}
<div className="rounded-lg border border-gray-200 bg-white">
<DataFlowDesigner
key={diagramId}
selectedDiagram={diagramName}
diagramId={diagramId}
onBackToList={handleBackToList}

File diff suppressed because it is too large Load Diff

View File

@ -74,6 +74,56 @@ interface DataFlowDesignerProps {
// 내부에서 사용할 확장된 JsonRelationship 타입 (connectionType 포함)
interface ExtendedJsonRelationship extends JsonRelationship {
connectionType: "simple-key" | "data-save" | "external-call";
settings?: {
control?: {
triggerType?: "insert" | "update" | "delete";
conditionTree?: Array<{
id: string;
type: string;
field?: string;
operator_type?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
};
actions?: Array<{
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
conditions?: Array<{
id: string;
type: string;
field?: string;
operator_type?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
}>;
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
}>;
notes?: string;
apiCall?: {
url: string;
method: string;
headers: Array<{ key: string; value: string }>;
body: string;
successCriteria: string;
};
};
}
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
@ -249,7 +299,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("🔥 정규화된 관계들:", normalizedRelationships);
setTempRelationships(normalizedRelationships);
setCurrentDiagramId(currentDiagramId);
setCurrentDiagramCategory(jsonDiagram.category || "simple-key"); // 관계도의 연결 종류 설정
setCurrentDiagramCategory("simple-key"); // 관계도의 연결 종류 설정 (기본값)
// 테이블 노드 생성을 위한 테이블 정보 로드
@ -755,7 +805,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("🔥 연결 타입:", newRelationship.connectionType);
// 첫 번째 관계가 추가되면 관계도의 category를 해당 connectionType으로 설정
if (tempRelationships.length === 0) {
if ((tempRelationships || []).length === 0) {
setCurrentDiagramCategory(relationship.connection_type);
}
@ -796,7 +846,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("메모리에 관계 생성 완료:", newRelationship);
toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요.");
},
[pendingConnection, setEdges, editingRelationshipId, tempRelationships.length],
[pendingConnection, setEdges, editingRelationshipId, tempRelationships],
);
// 연결 설정 취소
@ -841,12 +891,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("🔍 저장할 노드 위치 정보:", nodePositions);
console.log("📊 현재 노드 개수:", nodes.length);
console.log("📋 연결된 테이블 목록:", connectedTables);
console.log("🔗 관계 개수:", tempRelationships.length);
console.log("🔗 관계 개수:", (tempRelationships || []).length);
// 🔥 주요 연결 타입 변수 제거 (더 이상 사용하지 않음)
// 🔥 수정: relationships는 핵심 관계 정보만 포함, settings 전체 제거
const cleanRelationships = tempRelationships.map((rel) => {
const cleanRelationships = (tempRelationships || []).map((rel) => {
// 🔥 settings 전체를 제거하고 핵심 정보만 유지
const cleanRel: JsonRelationship = {
id: rel.id,
@ -871,12 +921,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
},
node_positions: nodePositions,
// 🔥 수정: 각 관계별 category 정보를 배열로 저장
category: tempRelationships.map((rel) => ({
category: (tempRelationships || []).map((rel) => ({
id: rel.id,
category: rel.connectionType,
})),
// 🔥 각 관계별 control 정보를 배열로 저장 (전체 실행 조건)
control: tempRelationships
control: (tempRelationships || [])
.filter((rel) => rel.connectionType === "data-save")
.map((rel) => {
console.log("🔍 Control 데이터 추출 중:", {
@ -889,57 +939,159 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const controlData = rel.settings?.control as {
triggerType?: "insert" | "update" | "delete";
conditionTree?: Array<{
field: string;
operator_type: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: unknown;
id: string;
type: string;
field?: string;
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value?: unknown;
logicalOperator?: "AND" | "OR";
groupId?: string;
groupLevel?: number;
}>;
};
console.log("🔍 추출된 controlData:", controlData);
console.log("🔍 conditionTree:", controlData?.conditionTree);
// 🔥 조건 필터링: 괄호 포함 모든 유효한 조건 유지
const validConditions = (controlData?.conditionTree || [])
.filter((cond) => {
// 괄호 조건은 항상 유지
if (cond.type === "group-start" || cond.type === "group-end") {
return true;
}
// 실제 조건은 field와 value가 있어야 함
return cond.type === "condition" && cond.field && cond.value !== undefined && cond.value !== "";
})
.map((cond, index) => {
// 괄호 조건 처리
if (cond.type === "group-start" || cond.type === "group-end") {
return {
id: cond.id,
type: cond.type,
...(cond.groupId && { groupId: cond.groupId }),
...(cond.groupLevel !== undefined && { groupLevel: cond.groupLevel }),
// 첫 번째가 아닐 때만 logicalOperator 포함
...(index > 0 && cond.logicalOperator && { logicalOperator: cond.logicalOperator }),
};
}
// 일반 조건 처리
return {
id: cond.id,
type: cond.type,
field: cond.field,
operator: cond.operator_type,
value: cond.value,
dataType: "string", // 기본값
// 첫 번째 조건이 아닐 때만 logicalOperator 포함
...(index > 0 && cond.logicalOperator && { logicalOperator: cond.logicalOperator }),
};
});
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",
})),
conditions: validConditions,
};
}),
// 🔥 각 관계별 plan 정보를 배열로 저장 (저장 액션)
plan: tempRelationships
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<{
(
rel.settings?.actions as Array<{
id: string;
type: string;
field: string;
operator_type: string;
value: unknown;
logicalOperator?: string;
}>;
}>) || [],
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}>;
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
conditions?: Array<{
id: string;
type: string;
field?: string;
operator_type?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
}>
)?.map((action) => ({
...action,
// fieldMappings에서 불필요한 transformFunction 제거
fieldMappings: action.fieldMappings?.map((mapping) => ({
sourceTable: mapping.sourceTable,
sourceField: mapping.sourceField,
targetTable: mapping.targetTable,
targetField: mapping.targetField,
defaultValue: mapping.defaultValue,
// transformFunction 제거 - 불필요한 필드
})),
// splitConfig 처리 - 사용자가 설정하지 않은 경우 포함하지 않음
...(action.splitConfig &&
(action.splitConfig.sourceField ||
action.splitConfig.delimiter ||
action.splitConfig.targetField) && {
splitConfig: {
sourceField: action.splitConfig.sourceField || "",
delimiter: action.splitConfig.delimiter || "",
targetField: action.splitConfig.targetField || "",
},
}),
// 🔥 조건 처리: 괄호 포함 모든 유효한 조건 유지
...(action.conditions && {
conditions: action.conditions
.filter((cond) => {
// 괄호 조건은 항상 유지
if (cond.type === "group-start" || cond.type === "group-end") {
return true;
}
// 실제 조건은 field와 value가 있어야 함
return cond.type === "condition" && cond.field && cond.value !== undefined && cond.value !== "";
})
.map((cond, index) => {
// 괄호 조건 처리
if (cond.type === "group-start" || cond.type === "group-end") {
return {
id: cond.id,
type: cond.type,
...(cond.groupId && { groupId: cond.groupId }),
...(cond.groupLevel !== undefined && { groupLevel: cond.groupLevel }),
// 첫 번째가 아닐 때만 logicalOperator 포함
...(index > 0 && cond.logicalOperator && { logicalOperator: cond.logicalOperator }),
};
}
// 일반 조건 처리
return {
id: cond.id,
type: cond.type,
field: cond.field,
value: cond.value,
dataType: "string", // 기본값
operator_type: cond.operator_type,
// 첫 번째 조건이 아닐 때만 logicalOperator 포함
...(index > 0 && cond.logicalOperator && { logicalOperator: cond.logicalOperator }),
};
}),
}),
})) || [],
})),
};
@ -955,11 +1107,13 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
let savedDiagram;
// 편집 모드 vs 신규 생성 모드 구분
if (diagramId && diagramId > 0) {
// 편집 모드 vs 신규 생성 모드 구분 (currentDiagramId 우선 사용)
const effectiveDiagramId = currentDiagramId || diagramId;
if (effectiveDiagramId && effectiveDiagramId > 0) {
// 편집 모드: 기존 관계도 업데이트
savedDiagram = await DataFlowAPI.updateJsonDataFlowDiagram(
diagramId,
effectiveDiagramId,
createRequest,
companyCode,
user?.userId || "SYSTEM",
@ -981,7 +1135,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setCurrentDiagramId(savedDiagram.diagram_id);
// 관계도 이름 업데이트 (편집 모드일 때만)
if (diagramId && diagramId > 0 && onDiagramNameUpdate) {
if (effectiveDiagramId && effectiveDiagramId > 0 && onDiagramNameUpdate) {
onDiagramNameUpdate(diagramName);
}
@ -993,7 +1147,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setIsSaving(false);
}
},
[tempRelationships, diagramId, companyCode, user?.userId, nodes, onDiagramNameUpdate],
[tempRelationships, diagramId, currentDiagramId, companyCode, user?.userId, nodes, onDiagramNameUpdate],
);
// 저장 모달 열기
@ -1204,7 +1358,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
hasUnsavedChanges ? "animate-pulse" : ""
}`}
>
💾 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
💾 {(tempRelationships || []).length > 0 && `(${(tempRelationships || []).length})`}
</button>
</div>
@ -1222,7 +1376,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
</div>
<div className="flex justify-between">
<span> :</span>
<span className="font-medium text-orange-600">{tempRelationships.length}</span>
<span className="font-medium text-orange-600">{(tempRelationships || []).length}</span>
</div>
<div className="flex justify-between">
<span> ID:</span>
@ -1313,7 +1467,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
<div className="flex items-center gap-1">
{/* 편집 버튼 */}
<button
onClick={(e) => {
onClick={async (e) => {
e.stopPropagation();
// 관계 선택 시 수정 모드로 전환
setEditingRelationshipId(relationship.id);
@ -1328,7 +1482,34 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
}
setSelectedColumns(newSelectedColumns);
// 🔥 수정: 연결 설정 모달 열기
// 🔥 수정: 데이터베이스에서 관계 설정 정보 로드
let relationshipSettings = {};
if (diagramId && diagramId > 0) {
try {
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(
diagramId,
companyCode,
);
if (jsonDiagram && relationship.connectionType === "data-save") {
const control = jsonDiagram.control?.find((c) => c.id === relationship.id);
const plan = jsonDiagram.plan?.find((p) => p.id === relationship.id);
relationshipSettings = {
control: control
? {
triggerType: control.triggerType,
conditionTree: control.conditions || [],
}
: undefined,
actions: plan ? plan.actions || [] : [],
};
}
} catch (error) {
console.error("관계 설정 정보 로드 실패:", error);
}
}
// 연결 설정 모달 열기
const fromTable = nodes.find(
(node) => node.data?.table?.tableName === relationship.fromTable,
);
@ -1361,7 +1542,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
existingRelationship: {
relationshipName: relationship.relationshipName,
connectionType: relationship.connectionType,
settings: relationship.settings || {},
settings: relationshipSettings,
},
});
}
@ -1559,6 +1740,11 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
{/* 연결 설정 모달 */}
<ConnectionSetupModal
key={
pendingConnection
? `${pendingConnection.fromNode?.tableName || "unknown"}-${pendingConnection.toNode?.tableName || "unknown"}`
: "connection-modal"
}
isOpen={!!pendingConnection}
connection={pendingConnection}
companyCode={companyCode}

View File

@ -195,7 +195,56 @@ export interface JsonDataFlowDiagram {
tables: string[];
};
node_positions?: NodePositions;
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
category?: Array<{
id: string;
category: string;
}>;
control?: Array<{
id: string;
triggerType: "insert" | "update" | "delete";
conditions: Array<{
id?: string;
type?: string;
field?: string;
operator?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
}>;
plan?: Array<{
id: string;
sourceTable: string;
actions: Array<{
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
conditions?: Array<{
id: string;
type: string;
field?: string;
operator_type?: string;
value?: unknown;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}>;
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
}>;
}>;
company_code: string;
created_at?: string;
updated_at?: string;
@ -230,11 +279,16 @@ export interface CreateDiagramRequest {
control?: Array<{
id: string; // relationships의 id와 동일
triggerType: "insert" | "update" | "delete";
conditions?: Array<{
field: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: unknown;
conditions: Array<{
id?: string;
type?: string;
field?: string;
operator?: string;
value?: unknown;
dataType?: string;
logicalOperator?: "AND" | "OR";
groupId?: string;
groupLevel?: number;
}>;
}>;
// 🔥 저장 액션 - relationships의 id와 동일한 id 사용
@ -251,15 +305,22 @@ export interface CreateDiagramRequest {
targetTable?: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}>;
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
conditions?: Array<{
id: string;
type: string;
field: string;
operator_type: string;
value: unknown;
field?: string;
operator_type?: string;
value?: unknown;
dataType?: string;
logicalOperator?: string;
groupId?: string;
groupLevel?: number;
}>;
}>;
}>;
@ -629,7 +690,7 @@ export class DataFlowAPI {
to_table_name: rel.toTable,
from_column_name: rel.fromColumns.join(","),
to_column_name: rel.toColumns.join(","),
connection_type: (jsonDiagram.category as "simple-key" | "data-save" | "external-call") || "simple-key", // 관계도의 category 사용
connection_type: rel.connectionType || "simple-key", // 각 관계의 connectionType 사용
company_code: companyCode, // 실제 사용자 회사 코드 사용
settings: rel.settings || {},
created_at: jsonDiagram.created_at,