데이터 저장
This commit is contained in:
parent
6a04ae450d
commit
2c677c2fb8
|
|
@ -5331,7 +5331,7 @@ model dataflow_diagrams {
|
|||
|
||||
// 조건부 연결 관련 컬럼들
|
||||
control Json? // 조건 설정 (트리거 타입, 조건 트리)
|
||||
category String? @db.VarChar(50) // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
category Json? // 연결 종류 배열 (["simple-key", "data-save", "external-call"])
|
||||
plan Json? // 실행 계획 (대상 액션들)
|
||||
|
||||
company_code String @db.VarChar(50)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
|||
diagram_name,
|
||||
relationships,
|
||||
node_positions,
|
||||
category,
|
||||
control,
|
||||
plan,
|
||||
company_code,
|
||||
created_by,
|
||||
updated_by,
|
||||
|
|
@ -100,6 +103,9 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
|||
|
||||
logger.info(`새 관계도 생성 요청:`, { diagram_name, company_code });
|
||||
logger.info(`node_positions:`, node_positions);
|
||||
logger.info(`category:`, category);
|
||||
logger.info(`control:`, control);
|
||||
logger.info(`plan:`, plan);
|
||||
logger.info(`전체 요청 Body:`, JSON.stringify(req.body, null, 2));
|
||||
const companyCode =
|
||||
company_code ||
|
||||
|
|
@ -119,10 +125,27 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
|||
});
|
||||
}
|
||||
|
||||
// 🔍 백엔드에서 받은 실제 데이터 로깅
|
||||
console.log(
|
||||
"🔍 백엔드에서 받은 control 데이터:",
|
||||
JSON.stringify(control, null, 2)
|
||||
);
|
||||
console.log(
|
||||
"🔍 백엔드에서 받은 plan 데이터:",
|
||||
JSON.stringify(plan, null, 2)
|
||||
);
|
||||
console.log(
|
||||
"🔍 백엔드에서 받은 category 데이터:",
|
||||
JSON.stringify(category, null, 2)
|
||||
);
|
||||
|
||||
const newDiagram = await createDataflowDiagramService({
|
||||
diagram_name,
|
||||
relationships,
|
||||
node_positions,
|
||||
category,
|
||||
control,
|
||||
plan,
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ interface CreateDataflowDiagramData {
|
|||
relationships: Record<string, unknown>; // JSON 데이터
|
||||
node_positions?: Record<string, unknown> | null; // JSON 데이터 (노드 위치 정보)
|
||||
|
||||
// 조건부 연결 관련 필드
|
||||
control?: Record<string, unknown> | null; // JSON 데이터 (조건 설정)
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
plan?: Record<string, unknown> | null; // JSON 데이터 (실행 계획)
|
||||
// 🔥 수정: 배열 구조로 변경된 조건부 연결 관련 필드
|
||||
control?: Array<Record<string, unknown>> | null; // JSON 배열 (각 관계별 조건 설정)
|
||||
category?: Array<Record<string, unknown>> | null; // JSON 배열 (각 관계별 연결 종류)
|
||||
plan?: Array<Record<string, unknown>> | null; // JSON 배열 (각 관계별 실행 계획)
|
||||
|
||||
company_code: string;
|
||||
created_by: string;
|
||||
|
|
@ -24,10 +24,10 @@ interface UpdateDataflowDiagramData {
|
|||
relationships?: Record<string, unknown>; // JSON 데이터
|
||||
node_positions?: Record<string, unknown> | null; // JSON 데이터 (노드 위치 정보)
|
||||
|
||||
// 조건부 연결 관련 필드
|
||||
control?: Record<string, unknown> | null; // JSON 데이터 (조건 설정)
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
plan?: Record<string, unknown> | null; // JSON 데이터 (실행 계획)
|
||||
// 🔥 수정: 배열 구조로 변경된 조건부 연결 관련 필드
|
||||
control?: Array<Record<string, unknown>> | null; // JSON 배열 (각 관계별 조건 설정)
|
||||
category?: Array<Record<string, unknown>> | null; // JSON 배열 (각 관계별 연결 종류)
|
||||
plan?: Array<Record<string, unknown>> | null; // JSON 배열 (각 관계별 실행 계획)
|
||||
|
||||
updated_by: string;
|
||||
}
|
||||
|
|
@ -142,7 +142,11 @@ export const createDataflowDiagram = async (
|
|||
node_positions: data.node_positions as
|
||||
| Prisma.InputJsonValue
|
||||
| undefined,
|
||||
category: data.category || undefined,
|
||||
category: data.category
|
||||
? (data.category as Prisma.InputJsonValue)
|
||||
: undefined,
|
||||
control: data.control as Prisma.InputJsonValue | undefined,
|
||||
plan: data.plan as Prisma.InputJsonValue | undefined,
|
||||
company_code: data.company_code,
|
||||
created_by: data.created_by,
|
||||
updated_by: data.updated_by,
|
||||
|
|
@ -213,7 +217,17 @@ export const updateDataflowDiagram = async (
|
|||
? (data.node_positions as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
}),
|
||||
...(data.category !== undefined && { category: data.category }),
|
||||
...(data.category !== undefined && {
|
||||
category: data.category
|
||||
? (data.category as Prisma.InputJsonValue)
|
||||
: undefined,
|
||||
}),
|
||||
...(data.control !== undefined && {
|
||||
control: data.control as Prisma.InputJsonValue | undefined,
|
||||
}),
|
||||
...(data.plan !== undefined && {
|
||||
plan: data.plan as Prisma.InputJsonValue | undefined,
|
||||
}),
|
||||
updated_by: data.updated_by,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -91,34 +91,48 @@ export class EventTriggerService {
|
|||
const results: ExecutionResult[] = [];
|
||||
|
||||
try {
|
||||
// 해당 테이블과 트리거 타입에 맞는 조건부 연결들 조회
|
||||
const diagrams = await prisma.dataflow_diagrams.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
control: {
|
||||
path: ["triggerType"],
|
||||
equals:
|
||||
triggerType === "insert"
|
||||
? "insert"
|
||||
: triggerType === "update"
|
||||
? ["update", "insert_update"]
|
||||
: triggerType === "delete"
|
||||
? "delete"
|
||||
: triggerType,
|
||||
},
|
||||
plan: {
|
||||
path: ["sourceTable"],
|
||||
equals: tableName,
|
||||
},
|
||||
},
|
||||
// 🔥 수정: Raw SQL을 사용하여 JSON 배열 검색
|
||||
const diagrams = (await prisma.$queryRaw`
|
||||
SELECT * FROM dataflow_diagrams
|
||||
WHERE company_code = ${companyCode}
|
||||
AND (
|
||||
category::text = '"data-save"' OR
|
||||
category::jsonb ? 'data-save' OR
|
||||
category::jsonb @> '["data-save"]'
|
||||
)
|
||||
`) as any[];
|
||||
|
||||
// 각 관계도에서 해당 테이블을 소스로 하는 데이터 저장 관계들 필터링
|
||||
const matchingDiagrams = diagrams.filter((diagram) => {
|
||||
// category 배열에서 data-save 연결이 있는지 확인
|
||||
const categories = diagram.category as any[];
|
||||
const hasDataSave = Array.isArray(categories)
|
||||
? categories.some((cat) => cat.category === "data-save")
|
||||
: false;
|
||||
|
||||
if (!hasDataSave) return false;
|
||||
|
||||
// plan 배열에서 해당 테이블을 소스로 하는 항목이 있는지 확인
|
||||
const plans = diagram.plan as any[];
|
||||
const hasMatchingPlan = Array.isArray(plans)
|
||||
? plans.some((plan) => plan.sourceTable === tableName)
|
||||
: false;
|
||||
|
||||
// control 배열에서 해당 트리거 타입이 있는지 확인
|
||||
const controls = diagram.control as any[];
|
||||
const hasMatchingControl = Array.isArray(controls)
|
||||
? controls.some((control) => control.triggerType === triggerType)
|
||||
: false;
|
||||
|
||||
return hasDataSave && hasMatchingPlan && hasMatchingControl;
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Found ${diagrams.length} conditional connections for table ${tableName} with trigger ${triggerType}`
|
||||
`Found ${matchingDiagrams.length} matching data-save connections for table ${tableName} with trigger ${triggerType}`
|
||||
);
|
||||
|
||||
// 각 다이어그램에 대해 조건부 연결 실행
|
||||
for (const diagram of diagrams) {
|
||||
for (const diagram of matchingDiagrams) {
|
||||
try {
|
||||
const result = await this.executeDiagramTrigger(
|
||||
diagram,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 // 편집 모드: 기존 관계도 이름
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ export interface JsonRelationship {
|
|||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
connectionType: "simple-key" | "data-save" | "external-call";
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +221,48 @@ export interface CreateDiagramRequest {
|
|||
tables: string[];
|
||||
};
|
||||
node_positions?: NodePositions;
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
// 🔥 수정: 각 관계별 정보를 배열로 저장
|
||||
category?: Array<{
|
||||
id: string;
|
||||
category: "simple-key" | "data-save" | "external-call";
|
||||
}>;
|
||||
// 🔥 전체 실행 조건 - relationships의 id와 동일한 id 사용
|
||||
control?: Array<{
|
||||
id: string; // relationships의 id와 동일
|
||||
triggerType: "insert" | "update" | "delete";
|
||||
conditions?: Array<{
|
||||
field: string;
|
||||
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
||||
value: unknown;
|
||||
logicalOperator?: "AND" | "OR";
|
||||
}>;
|
||||
}>;
|
||||
// 🔥 저장 액션 - relationships의 id와 동일한 id 사용
|
||||
plan?: Array<{
|
||||
id: string; // relationships의 id와 동일
|
||||
sourceTable: string;
|
||||
actions: 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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface JsonDataFlowDiagramsResponse {
|
||||
|
|
|
|||
Loading…
Reference in New Issue