테이블 연결 생성방식 수정
This commit is contained in:
parent
353d8d2bb0
commit
e459025d8a
|
|
@ -5331,7 +5331,7 @@ model dataflow_diagrams {
|
|||
|
||||
// 조건부 연결 관련 컬럼들
|
||||
control Json? // 조건 설정 (트리거 타입, 조건 트리)
|
||||
category Json? // 연결 종류 (타입, 기본 옵션)
|
||||
category String? @db.VarChar(50) // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
plan Json? // 실행 계획 (대상 액션들)
|
||||
|
||||
company_code String @db.VarChar(50)
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
|||
created_by,
|
||||
updated_by,
|
||||
} = req.body;
|
||||
|
||||
logger.info(`새 관계도 생성 요청:`, { diagram_name, company_code });
|
||||
logger.info(`node_positions:`, node_positions);
|
||||
logger.info(`전체 요청 Body:`, JSON.stringify(req.body, null, 2));
|
||||
const companyCode =
|
||||
company_code ||
|
||||
(req.query.companyCode as string) ||
|
||||
|
|
@ -162,6 +166,14 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => {
|
|||
const userId =
|
||||
updated_by || (req.headers["x-user-id"] as string) || "SYSTEM";
|
||||
|
||||
logger.info(`관계도 수정 요청 - ID: ${diagramId}, Company: ${companyCode}`);
|
||||
logger.info(`요청 Body:`, JSON.stringify(req.body, null, 2));
|
||||
logger.info(`node_positions:`, req.body.node_positions);
|
||||
logger.info(`요청 Body 키들:`, Object.keys(req.body));
|
||||
logger.info(`요청 Body 타입:`, typeof req.body);
|
||||
logger.info(`node_positions 타입:`, typeof req.body.node_positions);
|
||||
logger.info(`node_positions 값:`, req.body.node_positions);
|
||||
|
||||
if (isNaN(diagramId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaClient, Prisma } from "@prisma/client";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
|
@ -6,13 +6,13 @@ const prisma = new PrismaClient();
|
|||
// 타입 정의
|
||||
interface CreateDataflowDiagramData {
|
||||
diagram_name: string;
|
||||
relationships: any; // JSON 데이터
|
||||
node_positions?: any; // JSON 데이터 (노드 위치 정보)
|
||||
relationships: Record<string, unknown>; // JSON 데이터
|
||||
node_positions?: Record<string, unknown> | null; // JSON 데이터 (노드 위치 정보)
|
||||
|
||||
// 조건부 연결 관련 필드
|
||||
control?: any; // JSON 데이터 (조건 설정)
|
||||
category?: any; // JSON 데이터 (연결 종류)
|
||||
plan?: any; // JSON 데이터 (실행 계획)
|
||||
control?: Record<string, unknown> | null; // JSON 데이터 (조건 설정)
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
plan?: Record<string, unknown> | null; // JSON 데이터 (실행 계획)
|
||||
|
||||
company_code: string;
|
||||
created_by: string;
|
||||
|
|
@ -21,13 +21,13 @@ interface CreateDataflowDiagramData {
|
|||
|
||||
interface UpdateDataflowDiagramData {
|
||||
diagram_name?: string;
|
||||
relationships?: any; // JSON 데이터
|
||||
node_positions?: any; // JSON 데이터 (노드 위치 정보)
|
||||
relationships?: Record<string, unknown>; // JSON 데이터
|
||||
node_positions?: Record<string, unknown> | null; // JSON 데이터 (노드 위치 정보)
|
||||
|
||||
// 조건부 연결 관련 필드
|
||||
control?: any; // JSON 데이터 (조건 설정)
|
||||
category?: any; // JSON 데이터 (연결 종류)
|
||||
plan?: any; // JSON 데이터 (실행 계획)
|
||||
control?: Record<string, unknown> | null; // JSON 데이터 (조건 설정)
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
plan?: Record<string, unknown> | null; // JSON 데이터 (실행 계획)
|
||||
|
||||
updated_by: string;
|
||||
}
|
||||
|
|
@ -45,7 +45,13 @@ export const getDataflowDiagrams = async (
|
|||
const offset = (page - 1) * size;
|
||||
|
||||
// 검색 조건 구성
|
||||
const whereClause: any = {};
|
||||
const whereClause: {
|
||||
company_code?: string;
|
||||
diagram_name?: {
|
||||
contains: string;
|
||||
mode: "insensitive";
|
||||
};
|
||||
} = {};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
|
|
@ -99,7 +105,10 @@ export const getDataflowDiagramById = async (
|
|||
companyCode: string
|
||||
) => {
|
||||
try {
|
||||
const whereClause: any = {
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
|
|
@ -129,8 +138,11 @@ export const createDataflowDiagram = async (
|
|||
const newDiagram = await prisma.dataflow_diagrams.create({
|
||||
data: {
|
||||
diagram_name: data.diagram_name,
|
||||
relationships: data.relationships,
|
||||
node_positions: data.node_positions || null,
|
||||
relationships: data.relationships as Prisma.InputJsonValue,
|
||||
node_positions: data.node_positions as
|
||||
| Prisma.InputJsonValue
|
||||
| undefined,
|
||||
category: data.category || undefined,
|
||||
company_code: data.company_code,
|
||||
created_by: data.created_by,
|
||||
updated_by: data.updated_by,
|
||||
|
|
@ -153,8 +165,15 @@ export const updateDataflowDiagram = async (
|
|||
companyCode: string
|
||||
) => {
|
||||
try {
|
||||
logger.info(
|
||||
`관계도 수정 서비스 시작 - ID: ${diagramId}, Company: ${companyCode}`
|
||||
);
|
||||
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const whereClause: any = {
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
|
|
@ -167,7 +186,15 @@ export const updateDataflowDiagram = async (
|
|||
where: whereClause,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`기존 관계도 조회 결과:`,
|
||||
existingDiagram ? `ID ${existingDiagram.diagram_id} 발견` : "관계도 없음"
|
||||
);
|
||||
|
||||
if (!existingDiagram) {
|
||||
logger.warn(
|
||||
`관계도 ID ${diagramId}를 찾을 수 없음 - Company: ${companyCode}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -178,10 +205,15 @@ export const updateDataflowDiagram = async (
|
|||
},
|
||||
data: {
|
||||
...(data.diagram_name && { diagram_name: data.diagram_name }),
|
||||
...(data.relationships && { relationships: data.relationships }),
|
||||
...(data.node_positions !== undefined && {
|
||||
node_positions: data.node_positions,
|
||||
...(data.relationships && {
|
||||
relationships: data.relationships as Prisma.InputJsonValue,
|
||||
}),
|
||||
...(data.node_positions !== undefined && {
|
||||
node_positions: data.node_positions
|
||||
? (data.node_positions as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
}),
|
||||
...(data.category !== undefined && { category: data.category }),
|
||||
updated_by: data.updated_by,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
|
|
@ -203,7 +235,10 @@ export const deleteDataflowDiagram = async (
|
|||
) => {
|
||||
try {
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const whereClause: any = {
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
|
|
@ -245,7 +280,10 @@ export const copyDataflowDiagram = async (
|
|||
) => {
|
||||
try {
|
||||
// 원본 관계도 조회
|
||||
const whereClause: any = {
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
|
|
@ -274,7 +312,12 @@ export const copyDataflowDiagram = async (
|
|||
: originalDiagram.diagram_name;
|
||||
|
||||
// 같은 패턴의 이름들을 찾아서 가장 큰 번호 찾기
|
||||
const copyWhereClause: any = {
|
||||
const copyWhereClause: {
|
||||
diagram_name: {
|
||||
startsWith: string;
|
||||
};
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_name: {
|
||||
startsWith: baseName,
|
||||
},
|
||||
|
|
@ -310,7 +353,11 @@ export const copyDataflowDiagram = async (
|
|||
const copiedDiagram = await prisma.dataflow_diagrams.create({
|
||||
data: {
|
||||
diagram_name: copyName,
|
||||
relationships: originalDiagram.relationships as any,
|
||||
relationships: originalDiagram.relationships as Prisma.InputJsonValue,
|
||||
node_positions: originalDiagram.node_positions
|
||||
? (originalDiagram.node_positions as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
category: originalDiagram.category || undefined,
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ interface ConnectionInfo {
|
|||
};
|
||||
existingRelationship?: {
|
||||
relationshipName: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
|
|
@ -43,12 +42,10 @@ interface ConnectionInfo {
|
|||
// 연결 설정 타입
|
||||
interface ConnectionConfig {
|
||||
relationshipName: string;
|
||||
relationshipType: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
connectionType: "simple-key" | "data-save" | "external-call";
|
||||
fromColumnName: string;
|
||||
toColumnName: string;
|
||||
settings?: Record<string, unknown>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 단순 키값 연결 설정
|
||||
|
|
@ -105,11 +102,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
}) => {
|
||||
const [config, setConfig] = useState<ConnectionConfig>({
|
||||
relationshipName: "",
|
||||
relationshipType: "one-to-one",
|
||||
connectionType: "simple-key",
|
||||
fromColumnName: "",
|
||||
toColumnName: "",
|
||||
description: "",
|
||||
settings: {},
|
||||
});
|
||||
|
||||
|
|
@ -177,14 +172,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const existingRel = connection.existingRelationship;
|
||||
setConfig({
|
||||
relationshipName: existingRel?.relationshipName || `${fromDisplayName} → ${toDisplayName}`,
|
||||
relationshipType:
|
||||
(existingRel?.relationshipType as "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many") ||
|
||||
"one-to-one",
|
||||
connectionType: (existingRel?.connectionType as "simple-key" | "data-save" | "external-call") || "simple-key",
|
||||
fromColumnName: "",
|
||||
toColumnName: "",
|
||||
description:
|
||||
(existingRel?.settings?.description as string) || `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`,
|
||||
settings: existingRel?.settings || {},
|
||||
});
|
||||
|
||||
|
|
@ -333,10 +323,12 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
break;
|
||||
}
|
||||
|
||||
// 선택된 컬럼들 검증
|
||||
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
|
||||
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
|
||||
return;
|
||||
// 단순 키값 연결일 때만 컬럼 선택 검증
|
||||
if (config.connectionType === "simple-key") {
|
||||
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
|
||||
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 테이블과 컬럼 정보 사용
|
||||
|
|
@ -377,31 +369,24 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
}
|
||||
: {};
|
||||
|
||||
// 컬럼 정보는 단순 키값 연결일 때만 사용
|
||||
const finalFromColumns = config.connectionType === "simple-key" ? selectedFromColumns : [];
|
||||
const finalToColumns = config.connectionType === "simple-key" ? selectedToColumns : [];
|
||||
|
||||
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
||||
const relationshipData: TableRelationship = {
|
||||
relationship_name: config.relationshipName,
|
||||
from_table_name: fromTableName,
|
||||
to_table_name: toTableName,
|
||||
from_column_name: selectedFromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
to_column_name: selectedToColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
relationship_type: config.relationshipType,
|
||||
from_column_name: finalFromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
to_column_name: finalToColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
connection_type: config.connectionType,
|
||||
company_code: companyCode,
|
||||
settings: {
|
||||
...settings,
|
||||
...conditionalSettings, // 조건부 연결 설정 추가
|
||||
description: config.description,
|
||||
multiColumnMapping: {
|
||||
fromColumns: selectedFromColumns,
|
||||
toColumns: selectedToColumns,
|
||||
fromTable: fromTableName,
|
||||
toTable: toTableName,
|
||||
},
|
||||
isMultiColumn: selectedFromColumns.length > 1 || selectedToColumns.length > 1,
|
||||
columnCount: {
|
||||
from: selectedFromColumns.length,
|
||||
to: selectedToColumns.length,
|
||||
},
|
||||
// 중복 제거: multiColumnMapping, isMultiColumn, columnCount, description 제거
|
||||
// 필요시 from_column_name, to_column_name에서 split으로 추출 가능
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -415,11 +400,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const handleCancel = () => {
|
||||
setConfig({
|
||||
relationshipName: "",
|
||||
relationshipType: "one-to-one",
|
||||
connectionType: "simple-key",
|
||||
fromColumnName: "",
|
||||
toColumnName: "",
|
||||
description: "",
|
||||
});
|
||||
onCancel();
|
||||
};
|
||||
|
|
@ -791,7 +774,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||
|
|
@ -804,7 +787,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
type="time"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||
|
|
@ -817,7 +800,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
type="date"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||
|
|
@ -837,7 +820,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
<Input
|
||||
type="number"
|
||||
placeholder="숫자"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||
|
|
@ -849,7 +832,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
} else if (dataType.includes("bool")) {
|
||||
return (
|
||||
<Select
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onValueChange={(value) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = value;
|
||||
|
|
@ -869,7 +852,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
placeholder="값"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => {
|
||||
const newActions = [...dataSaveSettings.actions];
|
||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||
|
|
@ -1071,7 +1054,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
|
@ -1080,7 +1063,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
type="time"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
|
@ -1089,7 +1072,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
type="date"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
|
@ -1105,7 +1088,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
<Input
|
||||
type="number"
|
||||
placeholder="숫자"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
|
@ -1113,7 +1096,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
} else if (dataType.includes("bool")) {
|
||||
return (
|
||||
<Select
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onValueChange={(value) => updateCondition(index, "value", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||
|
|
@ -1129,7 +1112,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return (
|
||||
<Input
|
||||
placeholder="값"
|
||||
value={condition.value || ""}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
|
@ -1157,24 +1140,151 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
switch (config.connectionType) {
|
||||
case "simple-key":
|
||||
return (
|
||||
<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">
|
||||
<Key className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">단순 키값 연결 설정</span>
|
||||
<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>
|
||||
</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="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="notes" className="text-sm">
|
||||
연결 설명
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={simpleKeySettings.notes}
|
||||
onChange={(e) => setSimpleKeySettings({ ...simpleKeySettings, notes: e.target.value })}
|
||||
placeholder="데이터 연결에 대한 설명을 입력하세요"
|
||||
rows={2}
|
||||
className="text-sm"
|
||||
/>
|
||||
|
||||
{/* 단순 키값 연결 설정 */}
|
||||
<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">
|
||||
<Key className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">단순 키값 연결 설정</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="notes" className="text-sm">
|
||||
연결 설명
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={simpleKeySettings.notes}
|
||||
onChange={(e) => setSimpleKeySettings({ ...simpleKeySettings, notes: e.target.value })}
|
||||
placeholder="데이터 연결에 대한 설명을 입력하세요"
|
||||
rows={2}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1777,130 +1887,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
</DialogHeader>
|
||||
|
||||
<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>
|
||||
</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="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,11 @@ interface DataFlowDesignerProps {
|
|||
|
||||
// TableRelationship 타입은 dataflow.ts에서 import
|
||||
|
||||
// 내부에서 사용할 확장된 JsonRelationship 타입 (connectionType 포함)
|
||||
interface ExtendedJsonRelationship extends JsonRelationship {
|
||||
connectionType: string;
|
||||
}
|
||||
|
||||
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||
companyCode: propCompanyCode = "*",
|
||||
diagramId,
|
||||
|
|
@ -88,7 +93,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
const [selectedColumns, setSelectedColumns] = useState<{
|
||||
[tableName: string]: string[];
|
||||
}>({});
|
||||
const [selectionOrder, setSelectionOrder] = useState<string[]>([]);
|
||||
// selectionOrder는 더 이상 사용하지 않음 (테이블 노드 선택 방식으로 변경)
|
||||
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
|
||||
const [pendingConnection, setPendingConnection] = useState<{
|
||||
fromNode: { id: string; tableName: string; displayName: string };
|
||||
|
|
@ -103,7 +108,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
};
|
||||
existingRelationship?: {
|
||||
relationshipName: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
settings?: any;
|
||||
};
|
||||
|
|
@ -117,17 +121,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
connectionInfo: string;
|
||||
} | null>(null); // 선택된 엣지 정보
|
||||
|
||||
// 새로운 메모리 기반 상태들
|
||||
const [tempRelationships, setTempRelationships] = useState<JsonRelationship[]>([]); // 메모리에 저장된 관계들
|
||||
const [tempRelationships, setTempRelationships] = useState<ExtendedJsonRelationship[]>([]); // 메모리에 저장된 관계들
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 저장되지 않은 변경사항
|
||||
const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태
|
||||
const [isSaving, setIsSaving] = useState(false); // 저장 중 상태
|
||||
const [currentDiagramName, setCurrentDiagramName] = useState<string>(""); // 현재 편집 중인 관계도 이름
|
||||
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 }); // 액션 버튼 위치
|
||||
|
|
@ -174,8 +178,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
return newColumns;
|
||||
});
|
||||
|
||||
// 선택 순서도 정리
|
||||
setSelectionOrder((prev) => prev.filter((tableName) => !deletedTableNames.includes(tableName)));
|
||||
// selectionOrder는 더 이상 사용하지 않음
|
||||
|
||||
// 선택된 노드 초기화
|
||||
setSelectedNodes([]);
|
||||
|
|
@ -188,49 +191,11 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedNodes, setNodes]);
|
||||
|
||||
// 컬럼 클릭 처리 (토글 방식, 최대 2개 테이블만 허용)
|
||||
// 컬럼 클릭 처리 비활성화 (테이블 노드 선택 방식으로 변경)
|
||||
const handleColumnClick = useCallback((tableName: string, columnName: string) => {
|
||||
setSelectedColumns((prev) => {
|
||||
const currentColumns = prev[tableName] || [];
|
||||
const isSelected = currentColumns.includes(columnName);
|
||||
const selectedTables = Object.keys(prev).filter((name) => prev[name] && prev[name].length > 0);
|
||||
|
||||
if (isSelected) {
|
||||
// 선택 해제
|
||||
const newColumns = currentColumns.filter((column) => column !== columnName);
|
||||
if (newColumns.length === 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [tableName]: removed, ...rest } = prev;
|
||||
// 선택 순서에서도 제거 (다음 렌더링에서)
|
||||
setTimeout(() => {
|
||||
setSelectionOrder((order) => order.filter((name) => name !== tableName));
|
||||
}, 0);
|
||||
return rest;
|
||||
}
|
||||
return { ...prev, [tableName]: newColumns };
|
||||
} else {
|
||||
// 새 선택
|
||||
if (selectedTables.length >= 2 && !selectedTables.includes(tableName)) {
|
||||
toast.error("최대 2개 테이블까지만 선택할 수 있습니다.");
|
||||
return prev;
|
||||
}
|
||||
|
||||
const newColumns = [...currentColumns, columnName];
|
||||
const newSelection = { ...prev, [tableName]: newColumns };
|
||||
|
||||
// 선택 순서 업데이트 (다음 렌더링에서)
|
||||
setTimeout(() => {
|
||||
setSelectionOrder((order) => {
|
||||
if (!order.includes(tableName)) {
|
||||
return [...order, tableName];
|
||||
}
|
||||
return order;
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return newSelection;
|
||||
}
|
||||
});
|
||||
// 컬럼 클릭으로는 더 이상 선택하지 않음
|
||||
console.log(`컬럼 클릭 무시됨: ${tableName}.${columnName}`);
|
||||
return;
|
||||
}, []);
|
||||
|
||||
// 선택된 관계도의 관계 로드
|
||||
|
|
@ -257,14 +222,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
console.log("📊 테이블 목록:", tableNames);
|
||||
|
||||
// 기존 데이터에서 relationshipName이 없는 경우 기본값 설정
|
||||
const normalizedRelationships = relationships.map((rel: JsonRelationship) => ({
|
||||
// category를 각 관계의 connectionType으로 복원
|
||||
const normalizedRelationships: ExtendedJsonRelationship[] = relationships.map((rel: JsonRelationship) => ({
|
||||
...rel,
|
||||
relationshipName: rel.relationshipName || `${rel.fromTable} → ${rel.toTable}`, // 기본값 설정
|
||||
connectionType: jsonDiagram.category || "simple-key", // 관계도의 category를 각 관계의 connectionType으로 복원
|
||||
}));
|
||||
|
||||
// 메모리에 관계 저장 (기존 관계도 편집 시)
|
||||
setTempRelationships(normalizedRelationships);
|
||||
setCurrentDiagramId(currentDiagramId);
|
||||
setCurrentDiagramCategory(jsonDiagram.category || "simple-key"); // 관계도의 연결 종류 설정
|
||||
|
||||
// 테이블 노드 생성을 위한 테이블 정보 로드
|
||||
|
||||
|
|
@ -368,7 +336,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
// JSON 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결)
|
||||
const relationshipEdges: Edge[] = [];
|
||||
|
||||
relationships.forEach((rel: JsonRelationship) => {
|
||||
normalizedRelationships.forEach((rel: ExtendedJsonRelationship) => {
|
||||
const fromTable = rel.fromTable;
|
||||
const toTable = rel.toTable;
|
||||
const fromColumns = rel.fromColumns || [];
|
||||
|
|
@ -394,7 +362,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
data: {
|
||||
relationshipId: rel.id,
|
||||
relationshipName: rel.relationshipName,
|
||||
relationshipType: rel.relationshipType,
|
||||
connectionType: rel.connectionType,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
|
|
@ -403,7 +370,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
// 클릭 시 표시할 상세 정보
|
||||
details: {
|
||||
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
||||
relationshipType: rel.relationshipType,
|
||||
connectionType: rel.connectionType,
|
||||
},
|
||||
},
|
||||
|
|
@ -448,10 +414,56 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
}
|
||||
}, [companyCode, diagramId, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]);
|
||||
|
||||
// 노드 선택 변경 핸들러
|
||||
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
|
||||
const selectedNodeIds = nodes.map((node) => node.id);
|
||||
setSelectedNodes(selectedNodeIds);
|
||||
// 노드 클릭 핸들러 (커스텀 다중 선택 구현)
|
||||
const onNodeClick = useCallback(
|
||||
(event: React.MouseEvent, node: Node<TableNodeData>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const nodeId = node.id;
|
||||
const isCurrentlySelected = selectedNodes.includes(nodeId);
|
||||
|
||||
if (isCurrentlySelected) {
|
||||
// 이미 선택된 노드를 클릭하면 선택 해제
|
||||
const newSelection = selectedNodes.filter((id) => id !== nodeId);
|
||||
setSelectedNodes(newSelection);
|
||||
|
||||
// React Flow 노드 상태 업데이트
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((n) => ({
|
||||
...n,
|
||||
selected: newSelection.includes(n.id),
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
// 새로운 노드 선택
|
||||
let newSelection: string[];
|
||||
|
||||
if (selectedNodes.length >= 2) {
|
||||
// 이미 2개가 선택되어 있으면 첫 번째를 제거하고 새로운 것을 추가 (FIFO)
|
||||
newSelection = [selectedNodes[1], nodeId];
|
||||
} else {
|
||||
// 2개 미만이면 추가
|
||||
newSelection = [...selectedNodes, nodeId];
|
||||
}
|
||||
|
||||
setSelectedNodes(newSelection);
|
||||
|
||||
// React Flow 노드 상태 업데이트
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((n) => ({
|
||||
...n,
|
||||
selected: newSelection.includes(n.id),
|
||||
})),
|
||||
);
|
||||
}
|
||||
},
|
||||
[selectedNodes, setNodes],
|
||||
);
|
||||
|
||||
// 노드 선택 변경 핸들러 (React Flow 자체 선택 이벤트는 무시)
|
||||
const onSelectionChange = useCallback(() => {
|
||||
// React Flow의 자동 선택 변경은 무시하고 우리의 커스텀 로직만 사용
|
||||
// 이 함수는 비워두거나 최소한의 동기화만 수행
|
||||
}, []);
|
||||
|
||||
// 캔버스 클릭 시 엣지 정보 섹션 닫기
|
||||
|
|
@ -483,11 +495,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
details?: {
|
||||
connectionInfo: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -501,7 +511,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
toTable: edgeData.toTable,
|
||||
fromColumns: edgeData.fromColumns || [],
|
||||
toColumns: edgeData.toColumns || [],
|
||||
relationshipType: edgeData.relationshipType,
|
||||
connectionType: edgeData.connectionType,
|
||||
connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`,
|
||||
});
|
||||
|
|
@ -582,44 +591,25 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
})),
|
||||
);
|
||||
|
||||
// selectionOrder에서 선택되지 않은 테이블들 제거
|
||||
const activeTables = Object.keys(selectedColumns).filter(
|
||||
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
|
||||
);
|
||||
setSelectionOrder((prev) => prev.filter((tableName) => activeTables.includes(tableName)));
|
||||
// selectionOrder는 더 이상 사용하지 않음
|
||||
}, [selectedColumns, setNodes]);
|
||||
|
||||
// 연결 가능한 상태인지 확인
|
||||
// 연결 가능한 상태인지 확인 (테이블 노드 선택 기반으로 변경)
|
||||
const canCreateConnection = () => {
|
||||
const selectedTables = Object.keys(selectedColumns).filter(
|
||||
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
|
||||
);
|
||||
|
||||
// 최소 2개의 서로 다른 테이블에서 컬럼이 선택되어야 함
|
||||
return selectedTables.length >= 2;
|
||||
// 최소 2개의 테이블 노드가 선택되어야 함
|
||||
return selectedNodes.length >= 2;
|
||||
};
|
||||
|
||||
// 컬럼 연결 설정 모달 열기
|
||||
// 테이블 노드 연결 설정 모달 열기 (컬럼 선택 불필요)
|
||||
const openConnectionModal = () => {
|
||||
const selectedTables = Object.keys(selectedColumns).filter(
|
||||
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
|
||||
);
|
||||
if (selectedNodes.length < 2) return;
|
||||
|
||||
if (selectedTables.length < 2) return;
|
||||
|
||||
// 선택 순서에 따라 첫 번째와 두 번째 테이블 설정
|
||||
const orderedTables = selectionOrder.filter((name) => selectedTables.includes(name));
|
||||
const firstTableName = orderedTables[0];
|
||||
const secondTableName = orderedTables[1];
|
||||
const firstNode = nodes.find((node) => node.data.table.tableName === firstTableName);
|
||||
const secondNode = nodes.find((node) => node.data.table.tableName === secondTableName);
|
||||
// 선택된 첫 번째와 두 번째 노드 찾기
|
||||
const firstNode = nodes.find((node) => node.id === selectedNodes[0]);
|
||||
const secondNode = nodes.find((node) => node.id === selectedNodes[1]);
|
||||
|
||||
if (!firstNode || !secondNode) return;
|
||||
|
||||
// 첫 번째로 선택된 컬럼들 가져오기
|
||||
const firstTableColumns = selectedColumns[firstTableName] || [];
|
||||
const secondTableColumns = selectedColumns[secondTableName] || [];
|
||||
|
||||
setPendingConnection({
|
||||
fromNode: {
|
||||
id: firstNode.id,
|
||||
|
|
@ -631,24 +621,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
tableName: secondNode.data.table.tableName,
|
||||
displayName: secondNode.data.table.displayName,
|
||||
},
|
||||
// 선택된 첫 번째 컬럼을 연결 컬럼으로 설정
|
||||
fromColumn: firstTableColumns[0] || "",
|
||||
toColumn: secondTableColumns[0] || "",
|
||||
// 선택된 모든 컬럼 정보를 선택 순서대로 전달
|
||||
selectedColumnsData: (() => {
|
||||
const orderedData: { [key: string]: { displayName: string; columns: string[] } } = {};
|
||||
// selectionOrder 순서대로 데이터 구성 (첫 번째 선택이 먼저)
|
||||
orderedTables.forEach((tableName) => {
|
||||
const node = nodes.find((n) => n.data.table.tableName === tableName);
|
||||
if (node && selectedColumns[tableName]) {
|
||||
orderedData[tableName] = {
|
||||
displayName: node.data.table.displayName,
|
||||
columns: selectedColumns[tableName],
|
||||
};
|
||||
}
|
||||
});
|
||||
return orderedData;
|
||||
})(),
|
||||
// 컬럼 선택 정보는 제거 (단순 키값 연결에서만 필요)
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -712,15 +685,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
|
||||
// JSON 형태의 관계 객체 생성
|
||||
const newRelationship: JsonRelationship = {
|
||||
// JSON 형태의 관계 객체 생성 (중복 필드 제거)
|
||||
const newRelationship: ExtendedJsonRelationship = {
|
||||
id: editingRelationshipId || generateUniqueId("rel", Date.now()), // 수정 모드면 기존 ID 사용
|
||||
relationshipName: relationship.relationship_name, // 연결 이름 추가
|
||||
fromTable,
|
||||
toTable,
|
||||
fromColumns,
|
||||
toColumns,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
settings: relationship.settings || {},
|
||||
};
|
||||
|
|
@ -736,6 +708,11 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
setTempRelationships((prev) => [...prev, newRelationship]);
|
||||
setHasUnsavedChanges(true);
|
||||
|
||||
// 첫 번째 관계가 추가되면 관계도의 category를 해당 connectionType으로 설정
|
||||
if (tempRelationships.length === 0) {
|
||||
setCurrentDiagramCategory(relationship.connection_type);
|
||||
}
|
||||
|
||||
// 캔버스에 엣지 즉시 표시
|
||||
const newEdge: Edge = {
|
||||
id: generateUniqueId("edge", Date.now()),
|
||||
|
|
@ -751,7 +728,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
data: {
|
||||
relationshipId: newRelationship.id,
|
||||
relationshipName: newRelationship.relationshipName,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
fromTable,
|
||||
toTable,
|
||||
|
|
@ -759,7 +735,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
toColumns,
|
||||
details: {
|
||||
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
},
|
||||
},
|
||||
|
|
@ -770,7 +745,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
// 관계 생성 후 선택된 컬럼들 초기화
|
||||
setSelectedColumns({});
|
||||
setSelectionOrder([]);
|
||||
|
||||
console.log("메모리에 관계 생성 완료:", newRelationship);
|
||||
toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요.");
|
||||
|
|
@ -792,17 +766,19 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
// 관계도 저장 함수
|
||||
const handleSaveDiagram = useCallback(
|
||||
async (diagramName: string) => {
|
||||
if (tempRelationships.length === 0) {
|
||||
toast.error("저장할 관계가 없습니다.");
|
||||
// 🔥 수정: 관계가 없어도 노드가 있으면 저장 가능
|
||||
if (nodes.length === 0) {
|
||||
toast.error("저장할 테이블이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// 연결된 테이블 목록 추출
|
||||
const connectedTables = Array.from(
|
||||
new Set([...tempRelationships.map((rel) => rel.fromTable), ...tempRelationships.map((rel) => rel.toTable)]),
|
||||
).sort();
|
||||
// 🔥 수정: 현재 캔버스의 모든 테이블 기반으로 변경
|
||||
const connectedTables = nodes
|
||||
.map((node) => node.data?.table?.tableName)
|
||||
.filter((tableName) => tableName)
|
||||
.sort();
|
||||
|
||||
// 현재 노드 위치 추출
|
||||
const nodePositions: NodePositions = {};
|
||||
|
|
@ -815,16 +791,33 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
console.log("🔍 저장할 노드 위치 정보:", nodePositions);
|
||||
console.log("📊 현재 노드 개수:", nodes.length);
|
||||
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;
|
||||
});
|
||||
|
||||
// 저장 요청 데이터 생성
|
||||
const createRequest: CreateDiagramRequest = {
|
||||
diagram_name: diagramName,
|
||||
relationships: {
|
||||
relationships: tempRelationships,
|
||||
relationships: relationshipsWithoutConnectionType,
|
||||
tables: connectedTables,
|
||||
},
|
||||
node_positions: nodePositions,
|
||||
category: primaryConnectionType, // connectionType을 관계도 레벨의 category로 이동
|
||||
};
|
||||
|
||||
console.log("🚀 API 요청 데이터:", JSON.stringify(createRequest, null, 2));
|
||||
|
||||
let savedDiagram;
|
||||
|
||||
// 편집 모드 vs 신규 생성 모드 구분
|
||||
|
|
@ -860,7 +853,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[tempRelationships, diagramId, companyCode, user?.userId],
|
||||
[tempRelationships, diagramId, companyCode, user?.userId, nodes],
|
||||
);
|
||||
|
||||
// 저장 모달 열기
|
||||
|
|
@ -878,7 +871,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
// 고립된 노드 제거 함수
|
||||
const removeOrphanedNodes = useCallback(
|
||||
(updatedRelationships: JsonRelationship[], showMessage = true) => {
|
||||
(updatedRelationships: ExtendedJsonRelationship[], showMessage = true) => {
|
||||
setNodes((currentNodes) => {
|
||||
// 현재 관계에서 사용되는 테이블들 추출
|
||||
const usedTables = new Set<string>();
|
||||
|
|
@ -953,7 +946,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
};
|
||||
|
||||
|
|
@ -1000,7 +992,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
// 기존 관계 정보 추가 (연결 이름 유지를 위해)
|
||||
existingRelationship: {
|
||||
relationshipName: existingRelationship.relationshipName,
|
||||
relationshipType: existingRelationship.relationshipType,
|
||||
connectionType: existingRelationship.connectionType,
|
||||
settings: existingRelationship.settings,
|
||||
},
|
||||
|
|
@ -1097,6 +1088,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
<span>관계도 ID:</span>
|
||||
<span className="font-medium">{currentDiagramId || "미설정"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>연결 종류:</span>
|
||||
<span className="font-medium">
|
||||
{currentDiagramCategory === "simple-key" && "단순 키값"}
|
||||
{currentDiagramCategory === "data-save" && "데이터 저장"}
|
||||
{currentDiagramCategory === "external-call" && "외부 호출"}
|
||||
</span>
|
||||
</div>
|
||||
{hasUnsavedChanges && (
|
||||
<div className="mt-2 text-xs font-medium text-orange-600">⚠️ 저장되지 않은 변경사항이 있습니다</div>
|
||||
)}
|
||||
|
|
@ -1117,6 +1116,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
|
|
@ -1129,7 +1129,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
zoomOnScroll={true}
|
||||
zoomOnPinch={true}
|
||||
panOnDrag={[1, 2]}
|
||||
selectionOnDrag={true}
|
||||
selectionOnDrag={false}
|
||||
multiSelectionKeyCode={null}
|
||||
selectNodesOnDrag={false}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
|
|
@ -1138,100 +1138,116 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#E5E7EB" />
|
||||
</ReactFlow>
|
||||
|
||||
{/* 선택된 컬럼 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}
|
||||
{Object.keys(selectedColumns).length > 0 &&
|
||||
!showEdgeActions &&
|
||||
!pendingConnection &&
|
||||
!editingRelationshipId && (
|
||||
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-sm text-blue-600">📋</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800">선택된 컬럼</div>
|
||||
<div className="text-xs text-gray-500">{Object.keys(selectedColumns).length}개 테이블</div>
|
||||
{/* 선택된 테이블 노드 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}
|
||||
{selectedNodes.length > 0 && !showEdgeActions && !pendingConnection && !editingRelationshipId && (
|
||||
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-sm text-blue-600">📋</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800">선택된 테이블</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{selectedNodes.length === 1
|
||||
? "FROM 테이블 선택됨"
|
||||
: selectedNodes.length === 2
|
||||
? "FROM → TO 연결 준비"
|
||||
: `${selectedNodes.length}개 테이블`}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedColumns({});
|
||||
setSelectionOrder([]);
|
||||
}}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedNodes([]);
|
||||
}}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 컨텐츠 */}
|
||||
<div className="max-h-80 overflow-y-auto p-3">
|
||||
<div className="space-y-3">
|
||||
{[...new Set(selectionOrder)]
|
||||
.filter((tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0)
|
||||
.map((tableName, index, filteredOrder) => {
|
||||
const columns = selectedColumns[tableName];
|
||||
const node = nodes.find((n) => n.data.table.tableName === tableName);
|
||||
const displayName = node?.data.table.displayName || tableName;
|
||||
return (
|
||||
<div key={`selected-${tableName}-${index}`}>
|
||||
{/* 테이블 정보 */}
|
||||
<div className="rounded-lg bg-blue-50 p-2">
|
||||
<div className="mb-1 text-xs font-medium text-blue-700">{displayName}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{columns.map((column, columnIndex) => (
|
||||
<span
|
||||
key={`${tableName}-${column}-${columnIndex}`}
|
||||
className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
|
||||
title={column}
|
||||
>
|
||||
{column}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* 컨텐츠 */}
|
||||
<div className="max-h-80 overflow-y-auto p-3">
|
||||
<div className="space-y-3">
|
||||
{selectedNodes.map((nodeId, index) => {
|
||||
const node = nodes.find((n) => n.id === nodeId);
|
||||
if (!node) return null;
|
||||
|
||||
const { tableName, displayName } = node.data.table;
|
||||
return (
|
||||
<div key={`selected-${nodeId}-${index}`}>
|
||||
{/* 테이블 정보 */}
|
||||
<div
|
||||
className={`rounded-lg p-2 ${
|
||||
index === 0
|
||||
? "border-l-4 border-emerald-400 bg-emerald-50"
|
||||
: index === 1
|
||||
? "border-l-4 border-blue-400 bg-blue-50"
|
||||
: "bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div
|
||||
className={`text-xs font-medium ${
|
||||
index === 0 ? "text-emerald-700" : index === 1 ? "text-blue-700" : "text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{displayName}
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
{index === 0 && filteredOrder.length > 1 && (
|
||||
<div className="flex justify-center py-1">
|
||||
<div className="text-sm text-gray-400">↓</div>
|
||||
{selectedNodes.length === 2 && (
|
||||
<div
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
|
||||
index === 0 ? "bg-emerald-200 text-emerald-800" : "bg-blue-200 text-blue-800"
|
||||
}`}
|
||||
>
|
||||
{index === 0 ? "FROM" : "TO"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">{tableName}</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex gap-2 border-t border-blue-100 p-3">
|
||||
<button
|
||||
onClick={openConnectionModal}
|
||||
disabled={!canCreateConnection()}
|
||||
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
|
||||
canCreateConnection()
|
||||
? "bg-blue-500 text-white hover:bg-blue-600"
|
||||
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<span>🔗</span>
|
||||
<span>연결 설정</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedColumns({});
|
||||
setSelectionOrder([]);
|
||||
}}
|
||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
<span>초기화</span>
|
||||
</button>
|
||||
{/* 연결 화살표 (마지막이 아닌 경우) */}
|
||||
{index < selectedNodes.length - 1 && (
|
||||
<div className="flex justify-center py-1">
|
||||
<div className="text-gray-400">→</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex gap-2 border-t border-blue-100 p-3">
|
||||
<button
|
||||
onClick={openConnectionModal}
|
||||
disabled={!canCreateConnection()}
|
||||
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
|
||||
canCreateConnection()
|
||||
? "bg-blue-500 text-white hover:bg-blue-600"
|
||||
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<span>🔗</span>
|
||||
<span>연결 설정</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedColumns({});
|
||||
setSelectedNodes([]);
|
||||
}}
|
||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
<span>초기화</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 안내 메시지 */}
|
||||
{nodes.length === 0 && (
|
||||
|
|
@ -1296,13 +1312,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
{/* 관계 정보 요약 */}
|
||||
<div className="border-b border-gray-100 bg-gray-50 p-3">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase">관계 유형</div>
|
||||
<div className="mt-1 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-800">
|
||||
{selectedEdgeInfo.relationshipType}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-300">|</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase">연결 유형</div>
|
||||
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface ConditionNode {
|
|||
type: "condition" | "group-start" | "group-end";
|
||||
field?: string;
|
||||
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
||||
value?: any;
|
||||
value?: string | number | boolean;
|
||||
dataType?: string;
|
||||
logicalOperator?: "AND" | "OR"; // 다음 조건과의 논리 연산자
|
||||
groupId?: string; // 그룹 ID (group-start와 group-end가 같은 groupId를 가짐)
|
||||
|
|
@ -102,7 +102,6 @@ export interface TableRelationship {
|
|||
from_column_name: string;
|
||||
to_table_name: string;
|
||||
to_column_name: string;
|
||||
relationship_type: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
connection_type: "simple-key" | "data-save" | "external-call" | "conditional-link";
|
||||
settings?: Record<string, unknown>;
|
||||
company_code: string;
|
||||
|
|
@ -196,6 +195,7 @@ export interface JsonDataFlowDiagram {
|
|||
tables: string[];
|
||||
};
|
||||
node_positions?: NodePositions;
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
company_code: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
|
@ -210,9 +210,7 @@ export interface JsonRelationship {
|
|||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
settings?: any;
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreateDiagramRequest {
|
||||
|
|
@ -222,6 +220,7 @@ export interface CreateDiagramRequest {
|
|||
tables: string[];
|
||||
};
|
||||
node_positions?: NodePositions;
|
||||
category?: string; // 연결 종류 ("simple-key", "data-save", "external-call")
|
||||
}
|
||||
|
||||
export interface JsonDataFlowDiagramsResponse {
|
||||
|
|
@ -303,7 +302,7 @@ export class DataFlowAPI {
|
|||
* 테이블 관계 생성
|
||||
*/
|
||||
static async createRelationship(
|
||||
relationship: any, // 백엔드 API 형식 (camelCase)
|
||||
relationship: Omit<TableRelationship, "relationship_id">, // 백엔드 API 형식 (camelCase)
|
||||
): Promise<TableRelationship> {
|
||||
try {
|
||||
const response = await apiClient.post<ApiResponse<TableRelationship>>(
|
||||
|
|
@ -588,8 +587,7 @@ export class DataFlowAPI {
|
|||
to_table_name: rel.toTable,
|
||||
from_column_name: rel.fromColumns.join(","),
|
||||
to_column_name: rel.toColumns.join(","),
|
||||
relationship_type: rel.relationshipType as "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many",
|
||||
connection_type: rel.connectionType as "simple-key" | "data-save" | "external-call",
|
||||
connection_type: (jsonDiagram.category as "simple-key" | "data-save" | "external-call") || "simple-key", // 관계도의 category 사용
|
||||
company_code: companyCode, // 실제 사용자 회사 코드 사용
|
||||
settings: rel.settings || {},
|
||||
created_at: jsonDiagram.created_at,
|
||||
|
|
|
|||
Loading…
Reference in New Issue