생성된 관계도 확인
This commit is contained in:
parent
989c118ad2
commit
7260ad733b
|
|
@ -608,3 +608,113 @@ export async function getTableData(req: Request, res: Response): Promise<void> {
|
|||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 그룹 목록 조회 (관계도 이름별로 그룹화)
|
||||
*/
|
||||
export async function getDataFlowDiagrams(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 관계도 목록 조회 시작 ===");
|
||||
|
||||
const { page = 1, size = 20, searchTerm = "" } = req.query;
|
||||
|
||||
// 사용자 정보에서 회사 코드 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
const pageNum = parseInt(page as string, 10);
|
||||
const sizeNum = parseInt(size as string, 10);
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const result = await dataflowService.getDataFlowDiagrams(
|
||||
companyCode,
|
||||
pageNum,
|
||||
sizeNum,
|
||||
searchTerm as string
|
||||
);
|
||||
|
||||
logger.info(`관계도 목록 조회 완료: ${result.total}개`);
|
||||
|
||||
const response: ApiResponse<typeof result> = {
|
||||
success: true,
|
||||
message: "관계도 목록을 성공적으로 조회했습니다.",
|
||||
data: result,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("관계도 목록 조회 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 목록 조회 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "DATAFLOW_DIAGRAMS_LIST_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 관계도의 모든 관계 조회
|
||||
*/
|
||||
export async function getDiagramRelationships(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 관계도 관계 조회 시작 ===");
|
||||
|
||||
const { diagramName } = req.params;
|
||||
|
||||
if (!diagramName) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 이름이 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_DIAGRAM_NAME",
|
||||
details: "diagramName 파라미터가 필요합니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 정보에서 회사 코드 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const relationships = await dataflowService.getDiagramRelationships(
|
||||
companyCode,
|
||||
decodeURIComponent(diagramName)
|
||||
);
|
||||
|
||||
logger.info(`관계도 관계 조회 완료: ${relationships.length}개`);
|
||||
|
||||
const response: ApiResponse<any[]> = {
|
||||
success: true,
|
||||
message: "관계도 관계를 성공적으로 조회했습니다.",
|
||||
data: relationships,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("관계도 관계 조회 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계도 관계 조회 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "DIAGRAM_RELATIONSHIPS_GET_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
getLinkedDataByRelationship,
|
||||
deleteDataLink,
|
||||
getTableData,
|
||||
getDataFlowDiagrams,
|
||||
getDiagramRelationships,
|
||||
} from "../controllers/dataflowController";
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -78,4 +80,18 @@ router.delete("/data-links/:bridgeId", deleteDataLink);
|
|||
*/
|
||||
router.get("/table-data/:tableName", getTableData);
|
||||
|
||||
// ==================== 관계도 관리 라우트 ====================
|
||||
|
||||
/**
|
||||
* 관계도 목록 조회 (관계도 이름별로 그룹화)
|
||||
* GET /api/dataflow/diagrams
|
||||
*/
|
||||
router.get("/diagrams", getDataFlowDiagrams);
|
||||
|
||||
/**
|
||||
* 특정 관계도의 모든 관계 조회
|
||||
* GET /api/dataflow/diagrams/:diagramName/relationships
|
||||
*/
|
||||
router.get("/diagrams/:diagramName/relationships", getDiagramRelationships);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -729,4 +729,164 @@ export class DataflowService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 그룹 목록 조회 (관계도 이름별로 그룹화)
|
||||
*/
|
||||
async getDataFlowDiagrams(
|
||||
companyCode: string,
|
||||
page: number = 1,
|
||||
size: number = 20,
|
||||
searchTerm: string = ""
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 관계도 목록 조회 시작 - ${companyCode}, page: ${page}, size: ${size}, search: ${searchTerm}`
|
||||
);
|
||||
|
||||
// 관계도 이름별로 그룹화하여 조회
|
||||
const whereCondition = {
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
...(searchTerm && {
|
||||
OR: [
|
||||
{
|
||||
relationship_name: {
|
||||
contains: searchTerm,
|
||||
mode: "insensitive" as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
from_table_name: {
|
||||
contains: searchTerm,
|
||||
mode: "insensitive" as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
to_table_name: {
|
||||
contains: searchTerm,
|
||||
mode: "insensitive" as any,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
// 관계도별로 그룹화된 데이터 조회 (관계도 이름을 기준으로)
|
||||
const relationships = await prisma.table_relationships.findMany({
|
||||
where: whereCondition,
|
||||
select: {
|
||||
relationship_name: true,
|
||||
from_table_name: true,
|
||||
to_table_name: true,
|
||||
connection_type: true,
|
||||
relationship_type: true,
|
||||
created_date: true,
|
||||
created_by: true,
|
||||
updated_date: true,
|
||||
updated_by: true,
|
||||
},
|
||||
orderBy: [{ relationship_name: "asc" }, { created_date: "desc" }],
|
||||
});
|
||||
|
||||
// 관계도 이름별로 그룹화
|
||||
const diagramMap = new Map<string, any>();
|
||||
|
||||
relationships.forEach((rel) => {
|
||||
const diagramName = rel.relationship_name;
|
||||
|
||||
if (!diagramMap.has(diagramName)) {
|
||||
diagramMap.set(diagramName, {
|
||||
diagramName: diagramName,
|
||||
connectionType: rel.connection_type,
|
||||
relationshipType: rel.relationship_type,
|
||||
tableCount: new Set<string>(),
|
||||
relationshipCount: 0,
|
||||
createdAt: rel.created_date,
|
||||
createdBy: rel.created_by,
|
||||
updatedAt: rel.updated_date,
|
||||
updatedBy: rel.updated_by,
|
||||
tables: [],
|
||||
});
|
||||
}
|
||||
|
||||
const diagram = diagramMap.get(diagramName);
|
||||
diagram.tableCount.add(rel.from_table_name);
|
||||
diagram.tableCount.add(rel.to_table_name);
|
||||
diagram.relationshipCount++;
|
||||
|
||||
// 최신 업데이트 시간 유지
|
||||
if (rel.updated_date && rel.updated_date > diagram.updatedAt) {
|
||||
diagram.updatedAt = rel.updated_date;
|
||||
diagram.updatedBy = rel.updated_by;
|
||||
}
|
||||
});
|
||||
|
||||
// Set을 배열로 변환하고 테이블 개수 계산
|
||||
const diagrams = Array.from(diagramMap.values()).map((diagram) => ({
|
||||
...diagram,
|
||||
tableCount: diagram.tableCount.size,
|
||||
tables: Array.from(diagram.tableCount),
|
||||
}));
|
||||
|
||||
// 페이징 처리
|
||||
const total = diagrams.length;
|
||||
const startIndex = (page - 1) * size;
|
||||
const endIndex = startIndex + size;
|
||||
const paginatedDiagrams = diagrams.slice(startIndex, endIndex);
|
||||
|
||||
const result = {
|
||||
diagrams: paginatedDiagrams,
|
||||
total,
|
||||
page,
|
||||
size,
|
||||
totalPages: Math.ceil(total / size),
|
||||
hasNext: page < Math.ceil(total / size),
|
||||
hasPrev: page > 1,
|
||||
};
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 관계도 목록 조회 완료 - 총 ${total}개 관계도 중 ${paginatedDiagrams.length}개 조회`
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 관계도 목록 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 관계도의 모든 관계 조회
|
||||
*/
|
||||
async getDiagramRelationships(companyCode: string, diagramName: string) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 관계도 관계 조회 시작 - ${companyCode}, diagram: ${diagramName}`
|
||||
);
|
||||
|
||||
const relationships = await prisma.table_relationships.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
relationship_name: diagramName,
|
||||
is_active: "Y",
|
||||
},
|
||||
orderBy: {
|
||||
created_date: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 관계도 관계 조회 완료 - ${diagramName}, ${relationships.length}개 관계`
|
||||
);
|
||||
|
||||
return relationships;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`DataflowService: 관계도 관계 조회 실패 - ${diagramName}`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,136 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { useState } from "react";
|
||||
import { DataFlowDesigner } from "@/components/dataflow/DataFlowDesigner";
|
||||
import DataFlowList from "@/components/dataflow/DataFlowList";
|
||||
import { TableRelationship, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { TableRelationship } from "@/lib/api/dataflow";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Step = "list" | "design";
|
||||
|
||||
export default function DataFlowPage() {
|
||||
const { user } = useAuth();
|
||||
const [currentStep, setCurrentStep] = useState<Step>("list");
|
||||
const [selectedDiagram, setSelectedDiagram] = useState<DataFlowDiagram | null>(null);
|
||||
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
|
||||
|
||||
// 단계별 제목과 설명
|
||||
const stepConfig = {
|
||||
list: {
|
||||
title: "데이터 흐름 관계도 관리",
|
||||
description: "생성된 관계도들을 확인하고 관리하세요",
|
||||
icon: "📊",
|
||||
},
|
||||
design: {
|
||||
title: selectedDiagram ? `${selectedDiagram.diagramName} 관계도 설계` : "새 관계도 설계",
|
||||
description: selectedDiagram
|
||||
? "기존 관계도를 수정하거나 새로운 관계를 추가하세요"
|
||||
: "테이블 간 데이터 관계를 시각적으로 설계하세요",
|
||||
icon: "🎨",
|
||||
},
|
||||
};
|
||||
|
||||
// 다음 단계로 이동
|
||||
const goToNextStep = (nextStep: Step) => {
|
||||
setStepHistory((prev) => [...prev, nextStep]);
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
// 이전 단계로 이동
|
||||
const goToPreviousStep = () => {
|
||||
if (stepHistory.length > 1) {
|
||||
const newHistory = stepHistory.slice(0, -1);
|
||||
const previousStep = newHistory[newHistory.length - 1];
|
||||
setStepHistory(newHistory);
|
||||
setCurrentStep(previousStep);
|
||||
|
||||
// list로 돌아갈 때 선택된 관계도 초기화
|
||||
if (previousStep === "list") {
|
||||
setSelectedDiagram(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 특정 단계로 이동
|
||||
const goToStep = (step: Step) => {
|
||||
setCurrentStep(step);
|
||||
// 해당 단계까지의 히스토리만 유지
|
||||
const stepIndex = stepHistory.findIndex((s) => s === step);
|
||||
if (stepIndex !== -1) {
|
||||
setStepHistory(stepHistory.slice(0, stepIndex + 1));
|
||||
}
|
||||
|
||||
// list로 이동할 때 선택된 관계도 초기화
|
||||
if (step === "list") {
|
||||
setSelectedDiagram(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = (relationships: TableRelationship[]) => {
|
||||
console.log("저장된 관계:", relationships);
|
||||
// TODO: API 호출로 관계 저장
|
||||
// 저장 후 목록으로 돌아가기
|
||||
goToStep("list");
|
||||
};
|
||||
|
||||
const handleDiagramSelect = (diagram: DataFlowDiagram) => {
|
||||
setSelectedDiagram(diagram);
|
||||
};
|
||||
|
||||
const handleDesignDiagram = (diagram: DataFlowDiagram | null) => {
|
||||
setSelectedDiagram(diagram);
|
||||
goToNextStep("design");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-gray-50">
|
||||
<DataFlowDesigner companyCode={user?.company_code || "COMP001"} onSave={handleSave} />
|
||||
<Toaster />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{currentStep !== "list" && (
|
||||
<Button variant="outline" size="sm" onClick={goToPreviousStep} className="flex items-center">
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="flex items-center text-2xl font-bold text-gray-900">
|
||||
<span className="mr-2">{stepConfig[currentStep].icon}</span>
|
||||
{stepConfig[currentStep].title}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-600">{stepConfig[currentStep].description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 관계도 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="h-full p-6">
|
||||
<DataFlowList
|
||||
onDiagramSelect={handleDiagramSelect}
|
||||
selectedDiagram={selectedDiagram}
|
||||
onDesignDiagram={handleDesignDiagram}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 관계도 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="h-full">
|
||||
<DataFlowDesigner
|
||||
companyCode={user?.company_code || "COMP001"}
|
||||
onSave={handleSave}
|
||||
selectedDiagram={selectedDiagram}
|
||||
onBackToList={() => goToStep("list")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,15 +188,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" });
|
||||
|
||||
// 단일 관계 데이터 준비 (모든 선택된 컬럼 정보 포함)
|
||||
const relationshipData: Omit<TableRelationship, "relationshipId"> = {
|
||||
relationshipName: config.relationshipName,
|
||||
fromTableName: connection.fromNode.tableName,
|
||||
fromColumnName: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
toTableName: connection.toNode.tableName,
|
||||
toColumnName: toColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
relationshipType: config.relationshipType,
|
||||
connectionType: config.connectionType,
|
||||
companyCode: companyCode,
|
||||
const relationshipData: Omit<TableRelationship, "relationship_id"> = {
|
||||
relationship_name: config.relationshipName,
|
||||
from_table_name: connection.fromNode.tableName,
|
||||
from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
to_table_name: connection.toNode.tableName,
|
||||
to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
relationship_type: config.relationshipType,
|
||||
connection_type: config.connectionType,
|
||||
company_code: companyCode,
|
||||
settings: {
|
||||
...settings,
|
||||
multiColumnMapping: {
|
||||
|
|
@ -211,7 +211,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
to: toColumns.length,
|
||||
},
|
||||
},
|
||||
isActive: "Y",
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// API 호출
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import "@xyflow/react/dist/style.css";
|
|||
import { TableNode } from "./TableNode";
|
||||
import { TableSelector } from "./TableSelector";
|
||||
import { ConnectionSetupModal } from "./ConnectionSetupModal";
|
||||
import { TableDefinition, TableRelationship, DataFlowAPI } from "@/lib/api/dataflow";
|
||||
import { TableDefinition, TableRelationship, DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
|
||||
// 고유 ID 생성 함수
|
||||
const generateUniqueId = (prefix: string, relationshipId?: number): string => {
|
||||
|
|
@ -52,11 +52,18 @@ const edgeTypes = {};
|
|||
interface DataFlowDesignerProps {
|
||||
companyCode: string;
|
||||
onSave?: (relationships: TableRelationship[]) => void;
|
||||
selectedDiagram?: DataFlowDiagram | null;
|
||||
onBackToList?: () => void;
|
||||
}
|
||||
|
||||
// TableRelationship 타입은 dataflow.ts에서 import
|
||||
|
||||
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode, onSave }) => {
|
||||
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||
companyCode,
|
||||
onSave,
|
||||
selectedDiagram,
|
||||
onBackToList,
|
||||
}) => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
const [selectedColumns, setSelectedColumns] = useState<{
|
||||
|
|
@ -113,56 +120,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedNodes, setNodes]);
|
||||
|
||||
// 기존 관계 로드
|
||||
const loadExistingRelationships = useCallback(async () => {
|
||||
try {
|
||||
const existingRelationships = await DataFlowAPI.getRelationshipsByCompany(companyCode);
|
||||
setRelationships(existingRelationships);
|
||||
|
||||
// 기존 관계를 엣지로 변환하여 표시
|
||||
const existingEdges = existingRelationships.map((rel) => ({
|
||||
id: generateUniqueId("edge", rel.relationshipId),
|
||||
source: `table-${rel.fromTableName}`,
|
||||
target: `table-${rel.toTableName}`,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: rel.relationshipId,
|
||||
relationshipType: rel.relationshipType,
|
||||
connectionType: rel.connectionType,
|
||||
label: rel.relationshipName,
|
||||
fromColumn: rel.fromColumnName,
|
||||
toColumn: rel.toColumnName,
|
||||
},
|
||||
}));
|
||||
|
||||
setEdges(existingEdges);
|
||||
} catch (error) {
|
||||
console.error("기존 관계 로드 실패:", error);
|
||||
toast.error("기존 관계를 불러오는데 실패했습니다.");
|
||||
}
|
||||
}, [companyCode, setEdges]);
|
||||
|
||||
// 컴포넌트 마운트 시 기존 관계 로드
|
||||
useEffect(() => {
|
||||
if (companyCode) {
|
||||
loadExistingRelationships();
|
||||
}
|
||||
}, [companyCode, loadExistingRelationships]);
|
||||
|
||||
// 노드 선택 변경 핸들러
|
||||
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
|
||||
const selectedNodeIds = nodes.map((node) => node.id);
|
||||
setSelectedNodes(selectedNodeIds);
|
||||
}, []);
|
||||
|
||||
// 빈 onConnect 함수 (드래그 연결 비활성화)
|
||||
const onConnect = useCallback(() => {
|
||||
// 드래그로 연결하는 것을 방지
|
||||
return;
|
||||
}, []);
|
||||
|
||||
// 컬럼 클릭 처리 (토글 방식, 최대 2개 테이블만 허용)
|
||||
const handleColumnClick = useCallback((tableName: string, columnName: string) => {
|
||||
setSelectedColumns((prev) => {
|
||||
|
|
@ -184,39 +141,191 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
}
|
||||
return { ...prev, [tableName]: newColumns };
|
||||
} else {
|
||||
// 선택 추가 - 새로운 테이블이고 이미 2개 테이블이 선택되어 있으면 거부
|
||||
if (!prev[tableName] && selectedTables.length >= 2) {
|
||||
// 토스트 중복 방지를 위한 ref 사용
|
||||
if (!toastShownRef.current) {
|
||||
toastShownRef.current = true;
|
||||
setTimeout(() => {
|
||||
toast.error("최대 2개의 테이블에서만 컬럼을 선택할 수 있습니다.", {
|
||||
duration: 3000,
|
||||
position: "top-center",
|
||||
});
|
||||
// 3초 후 플래그 리셋
|
||||
setTimeout(() => {
|
||||
toastShownRef.current = false;
|
||||
}, 3000);
|
||||
}, 0);
|
||||
}
|
||||
// 새 선택
|
||||
if (selectedTables.length >= 2 && !selectedTables.includes(tableName)) {
|
||||
toast.error("최대 2개 테이블까지만 선택할 수 있습니다.");
|
||||
return prev;
|
||||
}
|
||||
|
||||
// 새로운 테이블이면 선택 순서에 추가, 기존 테이블이면 맨 뒤로 이동 (다음 렌더링에서)
|
||||
const newColumns = [...currentColumns, columnName];
|
||||
const newSelection = { ...prev, [tableName]: newColumns };
|
||||
|
||||
// 선택 순서 업데이트 (다음 렌더링에서)
|
||||
setTimeout(() => {
|
||||
setSelectionOrder((order) => {
|
||||
// 기존에 있던 테이블이면 제거 후 맨 뒤에 추가 (순서 갱신)
|
||||
const filteredOrder = order.filter((name) => name !== tableName);
|
||||
return [...filteredOrder, tableName];
|
||||
if (!order.includes(tableName)) {
|
||||
return [...order, tableName];
|
||||
}
|
||||
return order;
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return { ...prev, [tableName]: [...currentColumns, columnName] };
|
||||
return newSelection;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 선택된 관계도의 관계 로드
|
||||
const loadSelectedDiagramRelationships = useCallback(async () => {
|
||||
if (!selectedDiagram) return;
|
||||
|
||||
try {
|
||||
console.log("🔍 관계도 로드 시작:", selectedDiagram.diagramName);
|
||||
toast.loading("관계도를 불러오는 중...", { id: "load-diagram" });
|
||||
const diagramRelationships = await DataFlowAPI.getDiagramRelationships(selectedDiagram.diagramName);
|
||||
console.log("📋 관계도 관계 데이터:", diagramRelationships);
|
||||
console.log("📋 첫 번째 관계 상세:", diagramRelationships[0]);
|
||||
console.log(
|
||||
"📋 관계 객체 키들:",
|
||||
diagramRelationships[0] ? Object.keys(diagramRelationships[0]) : "배열이 비어있음",
|
||||
);
|
||||
setRelationships(diagramRelationships);
|
||||
|
||||
// 관계도의 모든 테이블 추출
|
||||
const tableNames = new Set<string>();
|
||||
diagramRelationships.forEach((rel) => {
|
||||
tableNames.add(rel.from_table_name);
|
||||
tableNames.add(rel.to_table_name);
|
||||
});
|
||||
console.log("📊 추출된 테이블 이름들:", Array.from(tableNames));
|
||||
|
||||
// 테이블 정보 로드
|
||||
const allTables = await DataFlowAPI.getTables();
|
||||
console.log("🏢 전체 테이블 수:", allTables.length);
|
||||
const tableDefinitions: TableDefinition[] = [];
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
const foundTable = allTables.find((t) => t.tableName === tableName);
|
||||
console.log(`🔍 테이블 ${tableName} 검색 결과:`, foundTable);
|
||||
if (foundTable) {
|
||||
// 각 테이블의 컬럼 정보를 별도로 가져옴
|
||||
const columns = await DataFlowAPI.getTableColumns(tableName);
|
||||
console.log(`📋 테이블 ${tableName}의 컬럼 수:`, columns.length);
|
||||
tableDefinitions.push({
|
||||
tableName: foundTable.tableName,
|
||||
displayName: foundTable.displayName,
|
||||
description: foundTable.description,
|
||||
columns: columns,
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ 테이블 ${tableName}을 찾을 수 없습니다`);
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블을 노드로 변환 (자동 레이아웃)
|
||||
const tableNodes = tableDefinitions.map((table, index) => {
|
||||
const x = (index % 3) * 400 + 100; // 3열 배치
|
||||
const y = Math.floor(index / 3) * 300 + 100;
|
||||
|
||||
return {
|
||||
id: `table-${table.tableName}`,
|
||||
type: "tableNode",
|
||||
position: { x, y },
|
||||
data: {
|
||||
table: {
|
||||
tableName: table.tableName,
|
||||
displayName: table.displayName,
|
||||
description: table.description || "",
|
||||
columns: table.columns.map((col) => ({
|
||||
name: col.columnName,
|
||||
type: col.dataType || "varchar",
|
||||
description: col.description || "",
|
||||
})),
|
||||
},
|
||||
onColumnClick: handleColumnClick,
|
||||
selectedColumns: selectedColumns[table.tableName] || [],
|
||||
} as TableNodeData,
|
||||
};
|
||||
});
|
||||
|
||||
console.log("🎨 생성된 테이블 노드 수:", tableNodes.length);
|
||||
console.log("📍 테이블 노드 상세:", tableNodes);
|
||||
setNodes(tableNodes);
|
||||
|
||||
// 관계를 엣지로 변환하여 표시
|
||||
const relationshipEdges = diagramRelationships.map((rel) => ({
|
||||
id: generateUniqueId("edge", rel.relationship_id),
|
||||
source: `table-${rel.from_table_name}`,
|
||||
target: `table-${rel.to_table_name}`,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
label: rel.relationship_name,
|
||||
fromColumn: rel.from_column_name,
|
||||
toColumn: rel.to_column_name,
|
||||
},
|
||||
}));
|
||||
|
||||
console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length);
|
||||
console.log("📍 관계 에지 상세:", relationshipEdges);
|
||||
setEdges(relationshipEdges);
|
||||
toast.success(`"${selectedDiagram.diagramName}" 관계도를 불러왔습니다.`, { id: "load-diagram" });
|
||||
} catch (error) {
|
||||
console.error("선택된 관계도 로드 실패:", error);
|
||||
toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" });
|
||||
}
|
||||
}, [selectedDiagram, companyCode, setNodes, setEdges, selectedColumns, handleColumnClick]);
|
||||
|
||||
// 기존 관계 로드 (새 관계도 생성 시)
|
||||
const loadExistingRelationships = useCallback(async () => {
|
||||
if (selectedDiagram) return; // 선택된 관계도가 있으면 실행하지 않음
|
||||
|
||||
try {
|
||||
const existingRelationships = await DataFlowAPI.getRelationshipsByCompany(companyCode);
|
||||
setRelationships(existingRelationships);
|
||||
|
||||
// 기존 관계를 엣지로 변환하여 표시
|
||||
const existingEdges = existingRelationships.map((rel) => ({
|
||||
id: generateUniqueId("edge", rel.relationship_id),
|
||||
source: `table-${rel.from_table_name}`,
|
||||
target: `table-${rel.to_table_name}`,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
label: rel.relationship_name,
|
||||
fromColumn: rel.from_column_name,
|
||||
toColumn: rel.to_column_name,
|
||||
},
|
||||
}));
|
||||
|
||||
setEdges(existingEdges);
|
||||
} catch (error) {
|
||||
console.error("기존 관계 로드 실패:", error);
|
||||
toast.error("기존 관계를 불러오는데 실패했습니다.");
|
||||
}
|
||||
}, [companyCode, setEdges, selectedDiagram]);
|
||||
|
||||
// 컴포넌트 마운트 시 관계 로드
|
||||
useEffect(() => {
|
||||
if (companyCode) {
|
||||
if (selectedDiagram) {
|
||||
loadSelectedDiagramRelationships();
|
||||
} else {
|
||||
loadExistingRelationships();
|
||||
}
|
||||
}
|
||||
}, [companyCode, selectedDiagram, loadExistingRelationships, loadSelectedDiagramRelationships]);
|
||||
|
||||
// 노드 선택 변경 핸들러
|
||||
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
|
||||
const selectedNodeIds = nodes.map((node) => node.id);
|
||||
setSelectedNodes(selectedNodeIds);
|
||||
}, []);
|
||||
|
||||
// 빈 onConnect 함수 (드래그 연결 비활성화)
|
||||
const onConnect = useCallback(() => {
|
||||
// 드래그로 연결하는 것을 방지
|
||||
return;
|
||||
}, []);
|
||||
|
||||
// 선택된 컬럼이 변경될 때마다 기존 노드들 업데이트 및 selectionOrder 정리
|
||||
useEffect(() => {
|
||||
setNodes((prevNodes) =>
|
||||
|
|
@ -380,19 +489,19 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
if (!pendingConnection) return;
|
||||
|
||||
const newEdge = {
|
||||
id: generateUniqueId("edge", relationship.relationshipId),
|
||||
id: generateUniqueId("edge", relationship.relationship_id),
|
||||
source: pendingConnection.fromNode.id,
|
||||
target: pendingConnection.toNode.id,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipId: relationship.relationshipId,
|
||||
relationshipType: relationship.relationshipType,
|
||||
connectionType: relationship.connectionType,
|
||||
label: relationship.relationshipName,
|
||||
fromColumn: relationship.fromColumnName,
|
||||
toColumn: relationship.toColumnName,
|
||||
relationshipId: relationship.relationship_id,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
label: relationship.relationship_name,
|
||||
fromColumn: relationship.from_column_name,
|
||||
toColumn: relationship.to_column_name,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Network, Database, Calendar, User } from "lucide-react";
|
||||
import { DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface DataFlowListProps {
|
||||
onDiagramSelect: (diagram: DataFlowDiagram) => void;
|
||||
selectedDiagram: DataFlowDiagram | null;
|
||||
onDesignDiagram: (diagram: DataFlowDiagram | null) => void;
|
||||
}
|
||||
|
||||
export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesignDiagram }: DataFlowListProps) {
|
||||
const [diagrams, setDiagrams] = useState<DataFlowDiagram[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// 관계도 목록 로드
|
||||
useEffect(() => {
|
||||
let abort = false;
|
||||
const load = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm);
|
||||
if (abort) return;
|
||||
|
||||
setDiagrams(response.diagrams || []);
|
||||
setTotal(response.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
setDiagrams([]);
|
||||
setTotalPages(1);
|
||||
setTotal(0);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
if (!abort) setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => {
|
||||
abort = true;
|
||||
};
|
||||
}, [currentPage, searchTerm]);
|
||||
|
||||
// 관계도 목록 다시 로드
|
||||
const reloadDiagrams = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm);
|
||||
setDiagrams(response.diagrams || []);
|
||||
setTotal(response.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiagramSelect = (diagram: DataFlowDiagram) => {
|
||||
onDiagramSelect(diagram);
|
||||
};
|
||||
|
||||
const handleEdit = (diagram: DataFlowDiagram) => {
|
||||
// 편집 모달 열기
|
||||
console.log("편집:", diagram);
|
||||
};
|
||||
|
||||
const handleDelete = (diagram: DataFlowDiagram) => {
|
||||
if (confirm(`"${diagram.diagramName}" 관계도를 삭제하시겠습니까?`)) {
|
||||
// 삭제 API 호출
|
||||
console.log("삭제:", diagram);
|
||||
toast.info("삭제 기능은 아직 구현되지 않았습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = (diagram: DataFlowDiagram) => {
|
||||
console.log("복사:", diagram);
|
||||
toast.info("복사 기능은 아직 구현되지 않았습니다.");
|
||||
};
|
||||
|
||||
const handleView = (diagram: DataFlowDiagram) => {
|
||||
// 미리보기 모달 열기
|
||||
console.log("미리보기:", diagram);
|
||||
toast.info("미리보기 기능은 아직 구현되지 않았습니다.");
|
||||
};
|
||||
|
||||
// 연결 타입에 따른 배지 색상
|
||||
const getConnectionTypeBadge = (connectionType: string) => {
|
||||
switch (connectionType) {
|
||||
case "simple-key":
|
||||
return (
|
||||
<Badge variant="outline" className="border-blue-200 bg-blue-50 text-blue-700">
|
||||
단순 키값
|
||||
</Badge>
|
||||
);
|
||||
case "data-save":
|
||||
return (
|
||||
<Badge variant="outline" className="border-green-200 bg-green-50 text-green-700">
|
||||
데이터 저장
|
||||
</Badge>
|
||||
);
|
||||
case "external-call":
|
||||
return (
|
||||
<Badge variant="outline" className="border-purple-200 bg-purple-50 text-purple-700">
|
||||
외부 호출
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return <Badge variant="outline">{connectionType}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
// 관계 타입에 따른 배지 색상
|
||||
const getRelationshipTypeBadge = (relationshipType: string) => {
|
||||
switch (relationshipType) {
|
||||
case "one-to-one":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700">
|
||||
1:1
|
||||
</Badge>
|
||||
);
|
||||
case "one-to-many":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-orange-100 text-orange-700">
|
||||
1:N
|
||||
</Badge>
|
||||
);
|
||||
case "many-to-one":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-700">
|
||||
N:1
|
||||
</Badge>
|
||||
);
|
||||
case "many-to-many":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-red-100 text-red-700">
|
||||
N:N
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return <Badge variant="secondary">{relationshipType}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-gray-500">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 검색 및 필터 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
placeholder="관계도명, 테이블명으로 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-80 pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => onDesignDiagram(null)}>
|
||||
<Plus className="mr-2 h-4 w-4" />새 관계도 생성
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 관계도 목록 테이블 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center">
|
||||
<Network className="mr-2 h-5 w-5" />
|
||||
데이터 흐름 관계도 ({total})
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>관계도명</TableHead>
|
||||
<TableHead>연결 타입</TableHead>
|
||||
<TableHead>관계 타입</TableHead>
|
||||
<TableHead>테이블 수</TableHead>
|
||||
<TableHead>관계 수</TableHead>
|
||||
<TableHead>최근 수정</TableHead>
|
||||
<TableHead>작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{diagrams.map((diagram) => (
|
||||
<TableRow
|
||||
key={diagram.diagramName}
|
||||
className={`cursor-pointer hover:bg-gray-50 ${
|
||||
selectedDiagram?.diagramName === diagram.diagramName ? "border-blue-200 bg-blue-50" : ""
|
||||
}`}
|
||||
onClick={() => handleDiagramSelect(diagram)}
|
||||
>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="flex items-center font-medium text-gray-900">
|
||||
<Database className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{diagram.diagramName}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-gray-500">
|
||||
테이블: {diagram.tables.slice(0, 3).join(", ")}
|
||||
{diagram.tables.length > 3 && ` 외 ${diagram.tables.length - 3}개`}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getConnectionTypeBadge(diagram.connectionType)}</TableCell>
|
||||
<TableCell>{getRelationshipTypeBadge(diagram.relationshipType)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Database className="mr-1 h-3 w-3 text-gray-400" />
|
||||
{diagram.tableCount}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Network className="mr-1 h-3 w-3 text-gray-400" />
|
||||
{diagram.relationshipCount}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Calendar className="mr-1 h-3 w-3 text-gray-400" />
|
||||
{new Date(diagram.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-gray-400">
|
||||
<User className="mr-1 h-3 w-3" />
|
||||
{diagram.updatedBy}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onDesignDiagram(diagram)}>
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
관계도 설계
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleView(diagram)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
미리보기
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(diagram)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
편집
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCopy(diagram)}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
복사
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(diagram)} className="text-red-600">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{diagrams.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
<Network className="mx-auto mb-4 h-12 w-12 text-gray-300" />
|
||||
<div className="mb-2 text-lg font-medium">관계도가 없습니다</div>
|
||||
<div className="text-sm">새 관계도를 생성하여 테이블 간 데이터 관계를 설정해보세요.</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<span className="text-sm text-gray-600">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -38,17 +38,17 @@ export interface TableInfo {
|
|||
}
|
||||
|
||||
export interface TableRelationship {
|
||||
relationshipId?: number;
|
||||
relationshipName: string;
|
||||
fromTableName: string;
|
||||
fromColumnName: string;
|
||||
toTableName: string;
|
||||
toColumnName: string;
|
||||
relationshipType: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
connectionType: "simple-key" | "data-save" | "external-call";
|
||||
relationship_id?: number;
|
||||
relationship_name: string;
|
||||
from_table_name: string;
|
||||
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";
|
||||
settings?: Record<string, unknown>;
|
||||
companyCode: string;
|
||||
isActive?: string;
|
||||
company_code: string;
|
||||
is_active?: string;
|
||||
}
|
||||
|
||||
// 데이터 연결 중계 테이블 타입
|
||||
|
|
@ -87,6 +87,31 @@ export interface TableDataResponse {
|
|||
};
|
||||
}
|
||||
|
||||
// 관계도 정보 인터페이스
|
||||
export interface DataFlowDiagram {
|
||||
diagramName: string;
|
||||
connectionType: string;
|
||||
relationshipType: string;
|
||||
tableCount: number;
|
||||
relationshipCount: number;
|
||||
tables: string[];
|
||||
createdAt: Date;
|
||||
createdBy: string;
|
||||
updatedAt: Date;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
// 관계도 목록 응답 인터페이스
|
||||
export interface DataFlowDiagramsResponse {
|
||||
diagrams: DataFlowDiagram[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
// 테이블 간 데이터 관계 설정 API 클래스
|
||||
export class DataFlowAPI {
|
||||
/**
|
||||
|
|
@ -323,4 +348,51 @@ export class DataFlowAPI {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 관계도 관리 ====================
|
||||
|
||||
// 관계도 목록 조회
|
||||
static async getDataFlowDiagrams(
|
||||
page: number = 1,
|
||||
size: number = 20,
|
||||
searchTerm: string = "",
|
||||
): Promise<DataFlowDiagramsResponse> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
size: size.toString(),
|
||||
...(searchTerm && { searchTerm }),
|
||||
});
|
||||
|
||||
const response = await apiClient.get<ApiResponse<DataFlowDiagramsResponse>>(`/dataflow/diagrams?${params}`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as DataFlowDiagramsResponse;
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 관계도의 모든 관계 조회
|
||||
static async getDiagramRelationships(diagramName: string): Promise<TableRelationship[]> {
|
||||
try {
|
||||
const encodedDiagramName = encodeURIComponent(diagramName);
|
||||
const response = await apiClient.get<ApiResponse<TableRelationship[]>>(
|
||||
`/dataflow/diagrams/${encodedDiagramName}/relationships`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 관계 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as TableRelationship[];
|
||||
} catch (error) {
|
||||
console.error("관계도 관계 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue