데이터 저장 방식을 json으로 변경
This commit is contained in:
parent
0a8413ee8c
commit
1b7bdab4c6
|
|
@ -5322,4 +5322,20 @@ model data_relationship_bridge {
|
|||
@@index([company_code, is_active], map: "idx_data_bridge_company_active")
|
||||
}
|
||||
|
||||
// 데이터플로우 관계도 - JSON 구조로 저장
|
||||
model dataflow_diagrams {
|
||||
diagram_id Int @id @default(autoincrement())
|
||||
diagram_name String @db.VarChar(255)
|
||||
relationships Json // 모든 관계 정보를 JSON으로 저장
|
||||
company_code String @db.VarChar(50)
|
||||
created_at DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_by String? @db.VarChar(50)
|
||||
|
||||
@@unique([company_code, diagram_name], map: "unique_diagram_name_per_company")
|
||||
@@index([company_code], map: "idx_dataflow_diagrams_company")
|
||||
@@index([diagram_name], map: "idx_dataflow_diagrams_name")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import dynamicFormRoutes from "./routes/dynamicFormRoutes";
|
|||
import fileRoutes from "./routes/fileRoutes";
|
||||
import companyManagementRoutes from "./routes/companyManagementRoutes";
|
||||
import dataflowRoutes from "./routes/dataflowRoutes";
|
||||
import dataflowDiagramRoutes from "./routes/dataflowDiagramRoutes";
|
||||
import webTypeStandardRoutes from "./routes/webTypeStandardRoutes";
|
||||
import buttonActionStandardRoutes from "./routes/buttonActionStandardRoutes";
|
||||
import screenStandardRoutes from "./routes/screenStandardRoutes";
|
||||
|
|
@ -108,6 +109,7 @@ app.use("/api/dynamic-form", dynamicFormRoutes);
|
|||
app.use("/api/files", fileRoutes);
|
||||
app.use("/api/company-management", companyManagementRoutes);
|
||||
app.use("/api/dataflow", dataflowRoutes);
|
||||
app.use("/api/dataflow-diagrams", dataflowDiagramRoutes);
|
||||
app.use("/api/admin/web-types", webTypeStandardRoutes);
|
||||
app.use("/api/admin/button-actions", buttonActionStandardRoutes);
|
||||
app.use("/api/admin/template-standards", templateStandardRoutes);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
import { Request, Response } from "express";
|
||||
import {
|
||||
getDataflowDiagrams as getDataflowDiagramsService,
|
||||
getDataflowDiagramById as getDataflowDiagramByIdService,
|
||||
createDataflowDiagram as createDataflowDiagramService,
|
||||
updateDataflowDiagram as updateDataflowDiagramService,
|
||||
deleteDataflowDiagram as deleteDataflowDiagramService,
|
||||
copyDataflowDiagram as copyDataflowDiagramService,
|
||||
} from "../services/dataflowDiagramService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* 관계도 목록 조회 (페이지네이션)
|
||||
*/
|
||||
export const getDataflowDiagrams = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const size = parseInt(req.query.size as string) || 20;
|
||||
const searchTerm = req.query.searchTerm as string;
|
||||
const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*";
|
||||
|
||||
const result = await getDataflowDiagramsService(companyCode, page, size, searchTerm);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 관계도 조회
|
||||
*/
|
||||
export const getDataflowDiagramById = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const diagramId = parseInt(req.params.diagramId);
|
||||
const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*";
|
||||
|
||||
if (isNaN(diagramId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 관계도 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const diagram = await getDataflowDiagramByIdService(diagramId, companyCode);
|
||||
|
||||
if (!diagram) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: diagram,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 조회 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 새로운 관계도 생성
|
||||
*/
|
||||
export const createDataflowDiagram = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { diagram_name, relationships, company_code, created_by, updated_by } = req.body;
|
||||
const companyCode = company_code || (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*";
|
||||
const userId = created_by || updated_by || req.headers["x-user-id"] as string || "SYSTEM";
|
||||
|
||||
if (!diagram_name || !relationships) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "관계도 이름과 관계 정보는 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const newDiagram = await createDataflowDiagramService({
|
||||
diagram_name,
|
||||
relationships,
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: newDiagram,
|
||||
message: "관계도가 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 생성 실패:", error);
|
||||
|
||||
// 중복 이름 에러 처리
|
||||
if (error instanceof Error && error.message.includes("unique constraint")) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 존재하는 관계도 이름입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 생성 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 수정
|
||||
*/
|
||||
export const updateDataflowDiagram = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const diagramId = parseInt(req.params.diagramId);
|
||||
const { updated_by } = req.body;
|
||||
const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*";
|
||||
const userId = updated_by || req.headers["x-user-id"] as string || "SYSTEM";
|
||||
|
||||
if (isNaN(diagramId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 관계도 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
...req.body,
|
||||
updated_by: userId,
|
||||
};
|
||||
|
||||
const updatedDiagram = await updateDataflowDiagramService(diagramId, updateData, companyCode);
|
||||
|
||||
if (!updatedDiagram) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: updatedDiagram,
|
||||
message: "관계도가 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 수정 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
*/
|
||||
export const deleteDataflowDiagram = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const diagramId = parseInt(req.params.diagramId);
|
||||
const companyCode = (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*";
|
||||
|
||||
if (isNaN(diagramId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 관계도 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await deleteDataflowDiagramService(diagramId, companyCode);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 삭제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 복제
|
||||
*/
|
||||
export const copyDataflowDiagram = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const diagramId = parseInt(req.params.diagramId);
|
||||
const { new_name, companyCode: bodyCompanyCode, userId: bodyUserId } = req.body;
|
||||
const companyCode = bodyCompanyCode || (req.query.companyCode as string) || req.headers["x-company-code"] as string || "*";
|
||||
const userId = bodyUserId || req.headers["x-user-id"] as string || "SYSTEM";
|
||||
|
||||
if (isNaN(diagramId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 관계도 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const copiedDiagram = await copyDataflowDiagramService(diagramId, companyCode, new_name, userId);
|
||||
|
||||
if (!copiedDiagram) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "복제할 관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: copiedDiagram,
|
||||
message: "관계도가 성공적으로 복제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 복제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 복제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import express from "express";
|
||||
import {
|
||||
getDataflowDiagrams,
|
||||
getDataflowDiagramById,
|
||||
createDataflowDiagram,
|
||||
updateDataflowDiagram,
|
||||
deleteDataflowDiagram,
|
||||
copyDataflowDiagram,
|
||||
} from "../controllers/dataflowDiagramController";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @route GET /api/dataflow-diagrams
|
||||
* @desc 관계도 목록 조회 (페이지네이션)
|
||||
*/
|
||||
router.get("/", getDataflowDiagrams);
|
||||
|
||||
/**
|
||||
* @route GET /api/dataflow-diagrams/:diagramId
|
||||
* @desc 특정 관계도 조회
|
||||
*/
|
||||
router.get("/:diagramId", getDataflowDiagramById);
|
||||
|
||||
/**
|
||||
* @route POST /api/dataflow-diagrams
|
||||
* @desc 새로운 관계도 생성
|
||||
*/
|
||||
router.post("/", createDataflowDiagram);
|
||||
|
||||
/**
|
||||
* @route PUT /api/dataflow-diagrams/:diagramId
|
||||
* @desc 관계도 수정
|
||||
*/
|
||||
router.put("/:diagramId", updateDataflowDiagram);
|
||||
|
||||
/**
|
||||
* @route DELETE /api/dataflow-diagrams/:diagramId
|
||||
* @desc 관계도 삭제
|
||||
*/
|
||||
router.delete("/:diagramId", deleteDataflowDiagram);
|
||||
|
||||
/**
|
||||
* @route POST /api/dataflow-diagrams/:diagramId/copy
|
||||
* @desc 관계도 복제
|
||||
*/
|
||||
router.post("/:diagramId/copy", copyDataflowDiagram);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 타입 정의
|
||||
interface CreateDataflowDiagramData {
|
||||
diagram_name: string;
|
||||
relationships: any; // JSON 데이터
|
||||
company_code: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
}
|
||||
|
||||
interface UpdateDataflowDiagramData {
|
||||
diagram_name?: string;
|
||||
relationships?: any; // JSON 데이터
|
||||
updated_by: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 목록 조회 (페이지네이션)
|
||||
*/
|
||||
export const getDataflowDiagrams = async (
|
||||
companyCode: string,
|
||||
page: number = 1,
|
||||
size: number = 20,
|
||||
searchTerm?: string
|
||||
) => {
|
||||
try {
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
// 검색 조건 구성
|
||||
const whereClause: any = {};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
whereClause.diagram_name = {
|
||||
contains: searchTerm,
|
||||
mode: "insensitive",
|
||||
};
|
||||
}
|
||||
|
||||
// 총 개수 조회
|
||||
const total = await prisma.dataflow_diagrams.count({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
// 데이터 조회
|
||||
const diagrams = await prisma.dataflow_diagrams.findMany({
|
||||
where: whereClause,
|
||||
orderBy: {
|
||||
updated_at: "desc",
|
||||
},
|
||||
skip: offset,
|
||||
take: size,
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / size);
|
||||
|
||||
return {
|
||||
diagrams,
|
||||
pagination: {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("관계도 목록 조회 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 관계도 조회
|
||||
*/
|
||||
export const getDataflowDiagramById = async (
|
||||
diagramId: number,
|
||||
companyCode: string
|
||||
) => {
|
||||
try {
|
||||
const whereClause: any = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
const diagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
return diagram;
|
||||
} catch (error) {
|
||||
logger.error("관계도 조회 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 새로운 관계도 생성
|
||||
*/
|
||||
export const createDataflowDiagram = async (
|
||||
data: CreateDataflowDiagramData
|
||||
) => {
|
||||
try {
|
||||
const newDiagram = await prisma.dataflow_diagrams.create({
|
||||
data: {
|
||||
diagram_name: data.diagram_name,
|
||||
relationships: data.relationships,
|
||||
company_code: data.company_code,
|
||||
created_by: data.created_by,
|
||||
updated_by: data.updated_by,
|
||||
},
|
||||
});
|
||||
|
||||
return newDiagram;
|
||||
} catch (error) {
|
||||
logger.error("관계도 생성 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 수정
|
||||
*/
|
||||
export const updateDataflowDiagram = async (
|
||||
diagramId: number,
|
||||
data: UpdateDataflowDiagramData,
|
||||
companyCode: string
|
||||
) => {
|
||||
try {
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const whereClause: any = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
const existingDiagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (!existingDiagram) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 업데이트 실행
|
||||
const updatedDiagram = await prisma.dataflow_diagrams.update({
|
||||
where: {
|
||||
diagram_id: diagramId,
|
||||
},
|
||||
data: {
|
||||
...(data.diagram_name && { diagram_name: data.diagram_name }),
|
||||
...(data.relationships && { relationships: data.relationships }),
|
||||
updated_by: data.updated_by,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return updatedDiagram;
|
||||
} catch (error) {
|
||||
logger.error("관계도 수정 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
*/
|
||||
export const deleteDataflowDiagram = async (
|
||||
diagramId: number,
|
||||
companyCode: string
|
||||
) => {
|
||||
try {
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const whereClause: any = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
const existingDiagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (!existingDiagram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 삭제 실행
|
||||
await prisma.dataflow_diagrams.delete({
|
||||
where: {
|
||||
diagram_id: diagramId,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("관계도 삭제 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 복제
|
||||
*/
|
||||
export const copyDataflowDiagram = async (
|
||||
diagramId: number,
|
||||
companyCode: string,
|
||||
newName?: string,
|
||||
userId: string = "SYSTEM"
|
||||
) => {
|
||||
try {
|
||||
// 원본 관계도 조회
|
||||
const whereClause: any = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
const originalDiagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (!originalDiagram) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 새로운 이름 생성 (제공되지 않은 경우)
|
||||
let copyName = newName;
|
||||
if (!copyName) {
|
||||
// 기존 이름에서 (n) 패턴을 찾아서 증가
|
||||
const baseNameMatch = originalDiagram.diagram_name.match(
|
||||
/^(.+?)(\s*\((\d+)\))?$/
|
||||
);
|
||||
const baseName = baseNameMatch
|
||||
? baseNameMatch[1]
|
||||
: originalDiagram.diagram_name;
|
||||
|
||||
// 같은 패턴의 이름들을 찾아서 가장 큰 번호 찾기
|
||||
const copyWhereClause: any = {
|
||||
diagram_name: {
|
||||
startsWith: baseName,
|
||||
},
|
||||
};
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
copyWhereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
const existingCopies = await prisma.dataflow_diagrams.findMany({
|
||||
where: copyWhereClause,
|
||||
select: {
|
||||
diagram_name: true,
|
||||
},
|
||||
});
|
||||
|
||||
let maxNumber = 0;
|
||||
existingCopies.forEach((copy) => {
|
||||
const match = copy.diagram_name.match(/\((\d+)\)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]);
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
copyName = `${baseName} (${maxNumber + 1})`;
|
||||
}
|
||||
|
||||
// 새로운 관계도 생성
|
||||
const copiedDiagram = await prisma.dataflow_diagrams.create({
|
||||
data: {
|
||||
diagram_name: copyName,
|
||||
relationships: originalDiagram.relationships as any,
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return copiedDiagram;
|
||||
} catch (error) {
|
||||
logger.error("관계도 복제 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
@ -23,9 +23,9 @@ export default function DataFlowEditPage() {
|
|||
// diagram_id로 관계도명 조회
|
||||
const fetchDiagramName = async () => {
|
||||
try {
|
||||
const relationships = await DataFlowAPI.getDiagramRelationshipsByDiagramId(id);
|
||||
if (relationships.length > 0) {
|
||||
setDiagramName(relationships[0].relationship_name);
|
||||
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(id);
|
||||
if (jsonDiagram && jsonDiagram.diagram_name) {
|
||||
setDiagramName(jsonDiagram.diagram_name);
|
||||
} else {
|
||||
setDiagramName(`관계도 ID: ${id}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,84 +151,73 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
}
|
||||
}, [isOpen, connection]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const handleConfirm = () => {
|
||||
if (!config.relationshipName || !connection) {
|
||||
toast.error("필수 정보를 모두 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 연결 종류별 설정을 준비
|
||||
let settings = {};
|
||||
// 연결 종류별 설정을 준비
|
||||
let settings = {};
|
||||
|
||||
switch (config.connectionType) {
|
||||
case "simple-key":
|
||||
settings = simpleKeySettings;
|
||||
break;
|
||||
case "data-save":
|
||||
settings = dataSaveSettings;
|
||||
break;
|
||||
case "external-call":
|
||||
settings = externalCallSettings;
|
||||
break;
|
||||
}
|
||||
|
||||
// 선택된 컬럼들 추출
|
||||
const selectedColumnsData = connection.selectedColumnsData || {};
|
||||
const tableNames = Object.keys(selectedColumnsData);
|
||||
const fromTable = tableNames[0];
|
||||
const toTable = tableNames[1];
|
||||
|
||||
const fromColumns = selectedColumnsData[fromTable]?.columns || [];
|
||||
const toColumns = selectedColumnsData[toTable]?.columns || [];
|
||||
|
||||
if (fromColumns.length === 0 || toColumns.length === 0) {
|
||||
toast.error("선택된 컬럼이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" });
|
||||
|
||||
// 단일 관계 데이터 준비 (모든 선택된 컬럼 정보 포함)
|
||||
// API 요청용 데이터 (camelCase)
|
||||
const apiRequestData = {
|
||||
...(diagramId && diagramId > 0 ? { diagramId: diagramId } : {}), // diagramId가 유효할 때만 추가
|
||||
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,
|
||||
settings: {
|
||||
...settings,
|
||||
multiColumnMapping: {
|
||||
fromColumns: fromColumns,
|
||||
toColumns: toColumns,
|
||||
fromTable: selectedColumnsData[fromTable]?.displayName || fromTable,
|
||||
toTable: selectedColumnsData[toTable]?.displayName || toTable,
|
||||
},
|
||||
isMultiColumn: fromColumns.length > 1 || toColumns.length > 1,
|
||||
columnCount: {
|
||||
from: fromColumns.length,
|
||||
to: toColumns.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// API 호출
|
||||
const createdRelationship = await DataFlowAPI.createRelationship(apiRequestData as any);
|
||||
|
||||
toast.success("관계가 성공적으로 생성되었습니다!", { id: "create-relationship" });
|
||||
|
||||
// 성공 콜백 호출
|
||||
onConfirm(createdRelationship);
|
||||
handleCancel(); // 모달 닫기
|
||||
} catch (error) {
|
||||
console.error("관계 생성 오류:", error);
|
||||
toast.error("관계 생성에 실패했습니다. 다시 시도해주세요.", { id: "create-relationship" });
|
||||
switch (config.connectionType) {
|
||||
case "simple-key":
|
||||
settings = simpleKeySettings;
|
||||
break;
|
||||
case "data-save":
|
||||
settings = dataSaveSettings;
|
||||
break;
|
||||
case "external-call":
|
||||
settings = externalCallSettings;
|
||||
break;
|
||||
}
|
||||
|
||||
// 선택된 컬럼들 추출
|
||||
const selectedColumnsData = connection.selectedColumnsData || {};
|
||||
const tableNames = Object.keys(selectedColumnsData);
|
||||
const fromTable = tableNames[0];
|
||||
const toTable = tableNames[1];
|
||||
|
||||
const fromColumns = selectedColumnsData[fromTable]?.columns || [];
|
||||
const toColumns = selectedColumnsData[toTable]?.columns || [];
|
||||
|
||||
if (fromColumns.length === 0 || toColumns.length === 0) {
|
||||
toast.error("선택된 컬럼이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
||||
const relationshipData: TableRelationship = {
|
||||
relationship_name: config.relationshipName,
|
||||
from_table_name: connection.fromNode.tableName,
|
||||
to_table_name: connection.toNode.tableName,
|
||||
from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||
relationship_type: config.relationshipType as any,
|
||||
connection_type: config.connectionType as any,
|
||||
company_code: companyCode,
|
||||
settings: {
|
||||
...settings,
|
||||
description: config.description,
|
||||
multiColumnMapping: {
|
||||
fromColumns: fromColumns,
|
||||
toColumns: toColumns,
|
||||
fromTable: selectedColumnsData[fromTable]?.displayName || fromTable,
|
||||
toTable: selectedColumnsData[toTable]?.displayName || toTable,
|
||||
},
|
||||
isMultiColumn: fromColumns.length > 1 || toColumns.length > 1,
|
||||
columnCount: {
|
||||
from: fromColumns.length,
|
||||
to: toColumns.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
toast.success("관계가 생성되었습니다!");
|
||||
|
||||
// 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이)
|
||||
onConfirm(relationshipData);
|
||||
handleCancel(); // 모달 닫기
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,16 @@ import "@xyflow/react/dist/style.css";
|
|||
import { TableNode } from "./TableNode";
|
||||
import { TableSelector } from "./TableSelector";
|
||||
import { ConnectionSetupModal } from "./ConnectionSetupModal";
|
||||
import { TableDefinition, TableRelationship, DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
import {
|
||||
TableDefinition,
|
||||
TableRelationship,
|
||||
DataFlowAPI,
|
||||
DataFlowDiagram,
|
||||
JsonRelationship,
|
||||
CreateDiagramRequest,
|
||||
} from "@/lib/api/dataflow";
|
||||
import SaveDiagramModal from "./SaveDiagramModal";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
// 고유 ID 생성 함수
|
||||
const generateUniqueId = (prefix: string, diagramId?: number): string => {
|
||||
|
|
@ -61,13 +70,18 @@ interface DataFlowDesignerProps {
|
|||
// TableRelationship 타입은 dataflow.ts에서 import
|
||||
|
||||
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||
companyCode = "*",
|
||||
companyCode: propCompanyCode = "*",
|
||||
diagramId,
|
||||
relationshipId, // 하위 호환성 유지
|
||||
onSave,
|
||||
onSave, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
selectedDiagram,
|
||||
onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
// 실제 사용자 회사 코드 사용 (prop보다 사용자 정보 우선)
|
||||
const companyCode = user?.company_code || user?.companyCode || propCompanyCode;
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
const [selectedColumns, setSelectedColumns] = useState<{
|
||||
|
|
@ -89,9 +103,46 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
} | null>(null);
|
||||
const [relationships, setRelationships] = useState<TableRelationship[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const [currentDiagramId, setCurrentDiagramId] = useState<number | null>(null); // 현재 화면의 diagram_id
|
||||
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<any | null>(null); // 선택된 엣지 정보
|
||||
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<{
|
||||
relationshipId: string;
|
||||
relationshipName: string;
|
||||
fromTable: string;
|
||||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
connectionInfo: string;
|
||||
} | null>(null); // 선택된 엣지 정보
|
||||
|
||||
// 새로운 메모리 기반 상태들
|
||||
const [tempRelationships, setTempRelationships] = useState<JsonRelationship[]>([]); // 메모리에 저장된 관계들
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 저장되지 않은 변경사항
|
||||
const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태
|
||||
const [isSaving, setIsSaving] = useState(false); // 저장 중 상태
|
||||
const [currentDiagramName, setCurrentDiagramName] = useState<string>(""); // 현재 편집 중인 관계도 이름
|
||||
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
// 편집 모드일 때 관계도 이름 로드
|
||||
useEffect(() => {
|
||||
const loadDiagramName = async () => {
|
||||
if (diagramId && diagramId > 0) {
|
||||
try {
|
||||
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode);
|
||||
if (jsonDiagram && jsonDiagram.diagram_name) {
|
||||
setCurrentDiagramName(jsonDiagram.diagram_name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("관계도 이름 로드 실패:", error);
|
||||
}
|
||||
} else {
|
||||
setCurrentDiagramName(""); // 신규 생성 모드
|
||||
}
|
||||
};
|
||||
|
||||
loadDiagramName();
|
||||
}, [diagramId, companyCode]);
|
||||
|
||||
// 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
@ -177,38 +228,28 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
if (!currentDiagramId || isNaN(currentDiagramId)) return;
|
||||
|
||||
try {
|
||||
console.log("🔍 관계도 로드 시작 (diagramId):", currentDiagramId);
|
||||
console.log("🔍 JSON 관계도 로드 시작 (diagramId):", currentDiagramId);
|
||||
toast.loading("관계도를 불러오는 중...", { id: "load-diagram" });
|
||||
|
||||
// diagramId로 해당 관계도의 모든 관계 조회
|
||||
const diagramRelationships = await DataFlowAPI.getDiagramRelationshipsByDiagramId(currentDiagramId);
|
||||
console.log("📋 관계도 관계 데이터:", diagramRelationships);
|
||||
// 새로운 JSON API로 관계도 조회
|
||||
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(currentDiagramId);
|
||||
console.log("📋 JSON 관계도 데이터:", jsonDiagram);
|
||||
|
||||
if (!Array.isArray(diagramRelationships)) {
|
||||
throw new Error("관계도 데이터 형식이 올바르지 않습니다.");
|
||||
if (!jsonDiagram || !jsonDiagram.relationships) {
|
||||
throw new Error("관계도 데이터를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
console.log("📋 첫 번째 관계 상세:", diagramRelationships[0]);
|
||||
console.log(
|
||||
"📋 관계 객체 키들:",
|
||||
diagramRelationships[0] ? Object.keys(diagramRelationships[0]) : "배열이 비어있음",
|
||||
);
|
||||
setRelationships(diagramRelationships);
|
||||
const relationships = jsonDiagram.relationships.relationships || [];
|
||||
const tableNames = jsonDiagram.relationships.tables || [];
|
||||
|
||||
// 현재 diagram_id 설정 (기존 관계도 편집 시)
|
||||
if (diagramRelationships.length > 0) {
|
||||
setCurrentDiagramId(diagramRelationships[0].diagram_id || null);
|
||||
}
|
||||
console.log("📋 관계 목록:", relationships);
|
||||
console.log("📊 테이블 목록:", tableNames);
|
||||
|
||||
// 관계도의 모든 테이블 추출
|
||||
const tableNames = new Set<string>();
|
||||
diagramRelationships.forEach((rel) => {
|
||||
if (rel && rel.from_table_name && rel.to_table_name) {
|
||||
tableNames.add(rel.from_table_name);
|
||||
tableNames.add(rel.to_table_name);
|
||||
}
|
||||
});
|
||||
console.log("📊 추출된 테이블 이름들:", Array.from(tableNames));
|
||||
// 메모리에 관계 저장 (기존 관계도 편집 시)
|
||||
setTempRelationships(relationships);
|
||||
setCurrentDiagramId(currentDiagramId);
|
||||
|
||||
// 테이블 노드 생성을 위한 테이블 정보 로드
|
||||
|
||||
// 테이블 정보 로드
|
||||
const allTables = await DataFlowAPI.getTables();
|
||||
|
|
@ -239,26 +280,15 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
[tableName: string]: { [columnName: string]: { direction: "source" | "target" | "both" } };
|
||||
} = {};
|
||||
|
||||
diagramRelationships.forEach((rel) => {
|
||||
if (!rel || !rel.from_table_name || !rel.to_table_name || !rel.from_column_name || !rel.to_column_name) {
|
||||
console.warn("⚠️ 관계 데이터가 불완전합니다:", rel);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromTable = rel.from_table_name;
|
||||
const toTable = rel.to_table_name;
|
||||
const fromColumns = rel.from_column_name
|
||||
.split(",")
|
||||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
const toColumns = rel.to_column_name
|
||||
.split(",")
|
||||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
relationships.forEach((rel: JsonRelationship) => {
|
||||
const fromTable = rel.fromTable;
|
||||
const toTable = rel.toTable;
|
||||
const fromColumns = rel.fromColumns || [];
|
||||
const toColumns = rel.toColumns || [];
|
||||
|
||||
// 소스 테이블의 컬럼들을 source로 표시
|
||||
if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {};
|
||||
fromColumns.forEach((col) => {
|
||||
fromColumns.forEach((col: string) => {
|
||||
if (connectedColumnsInfo[fromTable][col]) {
|
||||
connectedColumnsInfo[fromTable][col].direction = "both";
|
||||
} else {
|
||||
|
|
@ -268,7 +298,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
// 타겟 테이블의 컬럼들을 target으로 표시
|
||||
if (!connectedColumnsInfo[toTable]) connectedColumnsInfo[toTable] = {};
|
||||
toColumns.forEach((col) => {
|
||||
toColumns.forEach((col: string) => {
|
||||
if (connectedColumnsInfo[toTable][col]) {
|
||||
connectedColumnsInfo[toTable][col].direction = "both";
|
||||
} else {
|
||||
|
|
@ -312,25 +342,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
console.log("📍 테이블 노드 상세:", tableNodes);
|
||||
setNodes(tableNodes);
|
||||
|
||||
// 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결)
|
||||
// JSON 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결)
|
||||
const relationshipEdges: Edge[] = [];
|
||||
|
||||
diagramRelationships.forEach((rel) => {
|
||||
if (!rel || !rel.from_table_name || !rel.to_table_name || !rel.from_column_name || !rel.to_column_name) {
|
||||
console.warn("⚠️ 에지 생성 시 관계 데이터가 불완전합니다:", rel);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromTable = rel.from_table_name;
|
||||
const toTable = rel.to_table_name;
|
||||
const fromColumns = rel.from_column_name
|
||||
.split(",")
|
||||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
const toColumns = rel.to_column_name
|
||||
.split(",")
|
||||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
relationships.forEach((rel: JsonRelationship) => {
|
||||
const fromTable = rel.fromTable;
|
||||
const toTable = rel.toTable;
|
||||
const fromColumns = rel.fromColumns || [];
|
||||
const toColumns = rel.toColumns || [];
|
||||
|
||||
if (fromColumns.length === 0 || toColumns.length === 0) {
|
||||
console.warn("⚠️ 컬럼 정보가 없습니다:", { fromColumns, toColumns });
|
||||
|
|
@ -339,7 +358,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
// 테이블 간 하나의 번들 엣지 생성 (컬럼별 개별 엣지 대신)
|
||||
relationshipEdges.push({
|
||||
id: generateUniqueId("edge", rel.diagram_id),
|
||||
id: generateUniqueId("edge", currentDiagramId),
|
||||
source: `table-${fromTable}`,
|
||||
target: `table-${toTable}`,
|
||||
type: "smoothstep",
|
||||
|
|
@ -350,10 +369,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipName: rel.relationship_name,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
relationshipId: rel.id,
|
||||
relationshipName: "기존 관계",
|
||||
relationshipType: rel.relationshipType,
|
||||
connectionType: rel.connectionType,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumns: fromColumns,
|
||||
|
|
@ -361,8 +380,8 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
// 클릭 시 표시할 상세 정보
|
||||
details: {
|
||||
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
relationshipType: rel.relationshipType,
|
||||
connectionType: rel.connectionType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -383,57 +402,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
if (selectedDiagram) return; // 선택된 관계도가 있으면 실행하지 않음
|
||||
|
||||
try {
|
||||
const existingRelationships = await DataFlowAPI.getRelationshipsByCompany(companyCode);
|
||||
setRelationships(existingRelationships);
|
||||
// 새로운 JSON 기반 시스템에서는 기존 관계를 미리 로드하지 않음
|
||||
console.log("새 관계도 생성 모드: 빈 캔버스로 시작");
|
||||
setRelationships([]);
|
||||
|
||||
// 기존 관계를 엣지로 변환하여 표시 (컬럼별 연결)
|
||||
const existingEdges: Edge[] = [];
|
||||
|
||||
existingRelationships.forEach((rel) => {
|
||||
const fromTable = rel.from_table_name;
|
||||
const toTable = rel.to_table_name;
|
||||
const fromColumns = rel.from_column_name.split(",").map((col) => col.trim());
|
||||
const toColumns = rel.to_column_name.split(",").map((col) => col.trim());
|
||||
|
||||
// 각 from 컬럼을 각 to 컬럼에 연결
|
||||
const maxConnections = Math.max(fromColumns.length, toColumns.length);
|
||||
|
||||
for (let i = 0; i < maxConnections; i++) {
|
||||
const fromColumn = fromColumns[i] || fromColumns[0];
|
||||
const toColumn = toColumns[i] || toColumns[0];
|
||||
|
||||
existingEdges.push({
|
||||
id: generateUniqueId("edge", rel.diagram_id),
|
||||
source: `table-${fromTable}`,
|
||||
target: `table-${toTable}`,
|
||||
sourceHandle: `${fromTable}-${fromColumn}-source`,
|
||||
targetHandle: `${toTable}-${toColumn}-target`,
|
||||
type: "smoothstep",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#3b82f6",
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: rel.relationship_id,
|
||||
relationshipType: rel.relationship_type,
|
||||
connectionType: rel.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumn: fromColumn,
|
||||
toColumn: toColumn,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setEdges(existingEdges);
|
||||
// 빈 캔버스로 시작
|
||||
setEdges([]);
|
||||
} catch (error) {
|
||||
console.error("기존 관계 로드 실패:", error);
|
||||
toast.error("기존 관계를 불러오는데 실패했습니다.");
|
||||
}
|
||||
}, [companyCode, setEdges, selectedDiagram]);
|
||||
}, [setEdges, selectedDiagram]);
|
||||
|
||||
// 컴포넌트 마운트 시 관계 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -468,7 +447,21 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
// 엣지 클릭 시 연결 정보 표시
|
||||
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
||||
event.stopPropagation();
|
||||
const edgeData = edge.data as any;
|
||||
const edgeData = edge.data as {
|
||||
relationshipId: string;
|
||||
relationshipName: string;
|
||||
fromTable: string;
|
||||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
details?: {
|
||||
connectionInfo: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
};
|
||||
};
|
||||
if (edgeData) {
|
||||
setSelectedEdgeInfo({
|
||||
relationshipId: edgeData.relationshipId,
|
||||
|
|
@ -644,33 +637,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
[handleColumnClick, selectedColumns, setNodes],
|
||||
);
|
||||
|
||||
// 샘플 테이블 노드 추가 (개발용)
|
||||
const addSampleNode = useCallback(() => {
|
||||
const tableName = `sample_table_${nodes.length + 1}`;
|
||||
const newNode: Node<TableNodeData> = {
|
||||
id: `sample-${Date.now()}`,
|
||||
type: "tableNode",
|
||||
position: { x: Math.random() * 300, y: Math.random() * 200 },
|
||||
data: {
|
||||
table: {
|
||||
tableName,
|
||||
displayName: `샘플 테이블 ${nodes.length + 1}`,
|
||||
description: `샘플 테이블 설명 ${nodes.length + 1}`,
|
||||
columns: [
|
||||
{ name: "id", type: "INTEGER", description: "고유 식별자" },
|
||||
{ name: "name", type: "VARCHAR(100)", description: "이름" },
|
||||
{ name: "code", type: "VARCHAR(50)", description: "코드" },
|
||||
{ name: "created_date", type: "TIMESTAMP", description: "생성일시" },
|
||||
],
|
||||
},
|
||||
onColumnClick: handleColumnClick,
|
||||
selectedColumns: selectedColumns[tableName] || [],
|
||||
},
|
||||
};
|
||||
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}, [nodes.length, handleColumnClick, selectedColumns, setNodes]);
|
||||
|
||||
// 노드 전체 삭제
|
||||
const clearNodes = useCallback(() => {
|
||||
setNodes([]);
|
||||
|
|
@ -691,7 +657,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
(relationship: TableRelationship) => {
|
||||
if (!pendingConnection) return;
|
||||
|
||||
// 테이블 간 번들 에지 생성 (새 관계 생성 시)
|
||||
// 메모리 기반 관계 생성 (DB 저장 없이)
|
||||
const fromTable = relationship.from_table_name;
|
||||
const toTable = relationship.to_table_name;
|
||||
const fromColumns = relationship.from_column_name
|
||||
|
|
@ -703,8 +669,25 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
.map((col) => col.trim())
|
||||
.filter((col) => col);
|
||||
|
||||
// JSON 형태의 관계 객체 생성
|
||||
const newRelationship: JsonRelationship = {
|
||||
id: generateUniqueId("rel", Date.now()),
|
||||
fromTable,
|
||||
toTable,
|
||||
fromColumns,
|
||||
toColumns,
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
settings: relationship.settings || {},
|
||||
};
|
||||
|
||||
// 메모리에 관계 추가
|
||||
setTempRelationships((prev) => [...prev, newRelationship]);
|
||||
setHasUnsavedChanges(true);
|
||||
|
||||
// 캔버스에 엣지 즉시 표시
|
||||
const newEdge: Edge = {
|
||||
id: generateUniqueId("edge", relationship.diagram_id),
|
||||
id: generateUniqueId("edge", Date.now()),
|
||||
source: pendingConnection.fromNode.id,
|
||||
target: pendingConnection.toNode.id,
|
||||
type: "smoothstep",
|
||||
|
|
@ -715,15 +698,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
strokeDasharray: "none",
|
||||
},
|
||||
data: {
|
||||
relationshipId: relationship.relationship_id,
|
||||
relationshipName: relationship.relationship_name,
|
||||
relationshipId: newRelationship.id,
|
||||
relationshipName: "임시 관계",
|
||||
relationshipType: relationship.relationship_type,
|
||||
connectionType: relationship.connection_type,
|
||||
fromTable: fromTable,
|
||||
toTable: toTable,
|
||||
fromColumns: fromColumns,
|
||||
toColumns: toColumns,
|
||||
// 클릭 시 표시할 상세 정보
|
||||
fromTable,
|
||||
toTable,
|
||||
fromColumns,
|
||||
toColumns,
|
||||
details: {
|
||||
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
||||
relationshipType: relationship.relationship_type,
|
||||
|
|
@ -733,26 +715,16 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
};
|
||||
|
||||
setEdges((eds) => [...eds, newEdge]);
|
||||
setRelationships((prev) => [...prev, relationship]);
|
||||
setPendingConnection(null);
|
||||
|
||||
// 첫 번째 관계 생성 시 currentDiagramId 설정 (새 관계도 생성 시)
|
||||
if (!currentDiagramId && relationship.diagram_id) {
|
||||
setCurrentDiagramId(relationship.diagram_id);
|
||||
}
|
||||
|
||||
// 관계 생성 후 선택된 컬럼들 초기화
|
||||
setSelectedColumns({});
|
||||
setSelectionOrder([]);
|
||||
|
||||
console.log("관계 생성 완료:", relationship);
|
||||
// 관계 생성 완료 후 자동으로 목록 새로고침을 위한 콜백 (선택적)
|
||||
// 렌더링 중 상태 업데이트 방지를 위해 제거
|
||||
// if (onSave) {
|
||||
// onSave([...relationships, relationship]);
|
||||
// }
|
||||
console.log("메모리에 관계 생성 완료:", newRelationship);
|
||||
toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요.");
|
||||
},
|
||||
[pendingConnection, setEdges, currentDiagramId],
|
||||
[pendingConnection, setEdges],
|
||||
);
|
||||
|
||||
// 연결 설정 취소
|
||||
|
|
@ -760,6 +732,84 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
setPendingConnection(null);
|
||||
}, []);
|
||||
|
||||
// 관계도 저장 함수
|
||||
const handleSaveDiagram = useCallback(
|
||||
async (diagramName: string) => {
|
||||
if (tempRelationships.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 createRequest: CreateDiagramRequest = {
|
||||
diagram_name: diagramName,
|
||||
relationships: {
|
||||
relationships: tempRelationships,
|
||||
tables: connectedTables,
|
||||
},
|
||||
};
|
||||
|
||||
let savedDiagram;
|
||||
|
||||
// 편집 모드 vs 신규 생성 모드 구분
|
||||
if (diagramId && diagramId > 0) {
|
||||
// 편집 모드: 기존 관계도 업데이트
|
||||
savedDiagram = await DataFlowAPI.updateJsonDataFlowDiagram(
|
||||
diagramId,
|
||||
createRequest,
|
||||
companyCode,
|
||||
user?.userId || "SYSTEM",
|
||||
);
|
||||
toast.success(`관계도 "${diagramName}"가 성공적으로 수정되었습니다.`);
|
||||
} else {
|
||||
// 신규 생성 모드: 새로운 관계도 생성
|
||||
savedDiagram = await DataFlowAPI.createJsonDataFlowDiagram(
|
||||
createRequest,
|
||||
companyCode,
|
||||
user?.userId || "SYSTEM",
|
||||
);
|
||||
toast.success(`관계도 "${diagramName}"가 성공적으로 생성되었습니다.`);
|
||||
}
|
||||
|
||||
// 성공 처리
|
||||
setHasUnsavedChanges(false);
|
||||
setShowSaveModal(false);
|
||||
setCurrentDiagramId(savedDiagram.diagram_id);
|
||||
|
||||
console.log("관계도 저장 완료:", savedDiagram);
|
||||
} catch (error) {
|
||||
console.error("관계도 저장 실패:", error);
|
||||
toast.error("관계도 저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[tempRelationships, diagramId, companyCode, user?.userId],
|
||||
);
|
||||
|
||||
// 저장 모달 열기
|
||||
const handleOpenSaveModal = useCallback(() => {
|
||||
if (tempRelationships.length === 0) {
|
||||
toast.error("저장할 관계가 없습니다. 먼저 테이블을 연결해주세요.");
|
||||
return;
|
||||
}
|
||||
setShowSaveModal(true);
|
||||
}, [tempRelationships.length]);
|
||||
|
||||
// 저장 모달 닫기
|
||||
const handleCloseSaveModal = useCallback(() => {
|
||||
if (!isSaving) {
|
||||
setShowSaveModal(false);
|
||||
}
|
||||
}, [isSaving]);
|
||||
|
||||
return (
|
||||
<div className="data-flow-designer h-screen bg-gray-100">
|
||||
<div className="flex h-full">
|
||||
|
|
@ -777,13 +827,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
{/* 컨트롤 버튼들 */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={addSampleNode}
|
||||
className="w-full rounded-lg bg-gray-500 p-3 font-medium text-white transition-colors hover:bg-gray-600"
|
||||
>
|
||||
+ 샘플 테이블 추가 (개발용)
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={clearNodes}
|
||||
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
||||
|
|
@ -792,10 +835,13 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onSave && onSave([])}
|
||||
className="w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600"
|
||||
onClick={handleOpenSaveModal}
|
||||
disabled={tempRelationships.length === 0}
|
||||
className={`w-full rounded-lg p-3 font-medium text-white transition-colors ${
|
||||
tempRelationships.length > 0 ? "bg-green-500 hover:bg-green-600" : "cursor-not-allowed bg-gray-400"
|
||||
} ${hasUnsavedChanges ? "animate-pulse" : ""}`}
|
||||
>
|
||||
저장
|
||||
💾 관계도 저장 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -811,10 +857,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
<span>연결:</span>
|
||||
<span className="font-medium">{edges.length}개</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>메모리 관계:</span>
|
||||
<span className="font-medium text-orange-600">{tempRelationships.length}개</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>관계도 ID:</span>
|
||||
<span className="font-medium">{currentDiagramId || "미설정"}</span>
|
||||
</div>
|
||||
{hasUnsavedChanges && (
|
||||
<div className="mt-2 text-xs font-medium text-orange-600">⚠️ 저장되지 않은 변경사항이 있습니다</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -979,6 +1032,20 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
onConfirm={handleConfirmConnection}
|
||||
onCancel={handleCancelConnection}
|
||||
/>
|
||||
|
||||
{/* 관계도 저장 모달 */}
|
||||
<SaveDiagramModal
|
||||
isOpen={showSaveModal}
|
||||
onClose={handleCloseSaveModal}
|
||||
onSave={handleSaveDiagram}
|
||||
relationships={tempRelationships}
|
||||
defaultName={
|
||||
diagramId && diagramId > 0 && currentDiagramName
|
||||
? currentDiagramName // 편집 모드: 기존 관계도 이름
|
||||
: `관계도 ${new Date().toLocaleDateString()}` // 신규 생성 모드: 새로운 이름
|
||||
}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Database, Calendar, User } from "lucide-react";
|
||||
import { DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
interface DataFlowListProps {
|
||||
onDiagramSelect: (diagram: DataFlowDiagram) => void;
|
||||
|
|
@ -31,6 +32,7 @@ interface DataFlowListProps {
|
|||
}
|
||||
|
||||
export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesignDiagram }: DataFlowListProps) {
|
||||
const { user } = useAuth();
|
||||
const [diagrams, setDiagrams] = useState<DataFlowDiagram[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
|
@ -38,6 +40,9 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// 사용자 회사 코드 가져오기 (기본값: "*")
|
||||
const companyCode = user?.company_code || user?.companyCode || "*";
|
||||
|
||||
// 모달 상태
|
||||
const [showCopyModal, setShowCopyModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
|
@ -47,17 +52,35 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
const loadDiagrams = useCallback(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)));
|
||||
const response = await DataFlowAPI.getJsonDataFlowDiagrams(currentPage, 20, searchTerm, companyCode);
|
||||
|
||||
// JSON API 응답을 기존 형식으로 변환
|
||||
const convertedDiagrams = response.diagrams.map((diagram) => ({
|
||||
diagramId: diagram.diagram_id,
|
||||
relationshipId: diagram.diagram_id, // 호환성을 위해 추가
|
||||
diagramName: diagram.diagram_name,
|
||||
connectionType: "json-based", // 새로운 JSON 기반 타입
|
||||
relationshipType: "multi-relationship", // 다중 관계 타입
|
||||
relationshipCount: diagram.relationships?.relationships?.length || 0,
|
||||
tableCount: diagram.relationships?.tables?.length || 0,
|
||||
tables: diagram.relationships?.tables || [],
|
||||
createdAt: new Date(diagram.created_at || new Date()),
|
||||
createdBy: diagram.created_by || "SYSTEM",
|
||||
updatedAt: new Date(diagram.updated_at || diagram.created_at || new Date()),
|
||||
updatedBy: diagram.updated_by || "SYSTEM",
|
||||
lastUpdated: diagram.updated_at || diagram.created_at || new Date().toISOString(),
|
||||
}));
|
||||
|
||||
setDiagrams(convertedDiagrams);
|
||||
setTotal(response.pagination.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.pagination.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, searchTerm]);
|
||||
}, [currentPage, searchTerm, companyCode]);
|
||||
|
||||
// 관계도 목록 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -84,8 +107,13 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
|
||||
try {
|
||||
setLoading(true);
|
||||
const newDiagramName = await DataFlowAPI.copyDiagram(selectedDiagramForAction.diagramName);
|
||||
toast.success(`관계도가 성공적으로 복사되었습니다: ${newDiagramName}`);
|
||||
const copiedDiagram = await DataFlowAPI.copyJsonDataFlowDiagram(
|
||||
selectedDiagramForAction.diagramId,
|
||||
companyCode,
|
||||
undefined,
|
||||
user?.userId || "SYSTEM",
|
||||
);
|
||||
toast.success(`관계도가 성공적으로 복사되었습니다: ${copiedDiagram.diagram_name}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
|
|
@ -105,8 +133,8 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
|
||||
try {
|
||||
setLoading(true);
|
||||
const deletedCount = await DataFlowAPI.deleteDiagram(selectedDiagramForAction.diagramName);
|
||||
toast.success(`관계도가 삭제되었습니다 (${deletedCount}개 관계 삭제)`);
|
||||
await DataFlowAPI.deleteJsonDataFlowDiagram(selectedDiagramForAction.diagramId, companyCode);
|
||||
toast.success(`관계도가 삭제되었습니다: ${selectedDiagramForAction.diagramName}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
|
|
@ -141,6 +169,12 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
외부 호출
|
||||
</Badge>
|
||||
);
|
||||
case "json-based":
|
||||
return (
|
||||
<Badge variant="outline" className="border-indigo-200 bg-indigo-50 text-indigo-700">
|
||||
JSON 기반
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return <Badge variant="outline">{connectionType}</Badge>;
|
||||
}
|
||||
|
|
@ -173,6 +207,12 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig
|
|||
N:N
|
||||
</Badge>
|
||||
);
|
||||
case "multi-relationship":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-purple-100 text-purple-700">
|
||||
다중 관계
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return <Badge variant="secondary">{relationshipType}</Badge>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,212 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { JsonRelationship } from "@/lib/api/dataflow";
|
||||
|
||||
interface SaveDiagramModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (diagramName: string) => void;
|
||||
relationships: JsonRelationship[];
|
||||
defaultName?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
relationships,
|
||||
defaultName = "",
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [diagramName, setDiagramName] = useState(defaultName);
|
||||
const [nameError, setNameError] = useState("");
|
||||
|
||||
// defaultName이 변경될 때마다 diagramName 업데이트
|
||||
useEffect(() => {
|
||||
setDiagramName(defaultName);
|
||||
}, [defaultName]);
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmedName = diagramName.trim();
|
||||
|
||||
if (!trimmedName) {
|
||||
setNameError("관계도 이름을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedName.length < 2) {
|
||||
setNameError("관계도 이름은 2글자 이상이어야 합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedName.length > 100) {
|
||||
setNameError("관계도 이름은 100글자를 초과할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setNameError("");
|
||||
onSave(trimmedName);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isLoading) {
|
||||
setDiagramName(defaultName);
|
||||
setNameError("");
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !isLoading) {
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
// 관련된 테이블 목록 추출
|
||||
const connectedTables = Array.from(
|
||||
new Set([...relationships.map((rel) => rel.fromTable), ...relationships.map((rel) => rel.toTable)]),
|
||||
).sort();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold">📊 관계도 저장</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 관계도 이름 입력 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="diagram-name" className="text-sm font-medium">
|
||||
관계도 이름 *
|
||||
</Label>
|
||||
<Input
|
||||
id="diagram-name"
|
||||
value={diagramName}
|
||||
onChange={(e) => {
|
||||
setDiagramName(e.target.value);
|
||||
if (nameError) setNameError("");
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="예: 사용자-부서 관계도"
|
||||
disabled={isLoading}
|
||||
className={nameError ? "border-red-500 focus:border-red-500" : ""}
|
||||
/>
|
||||
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
|
||||
</div>
|
||||
|
||||
{/* 관계 요약 정보 */}
|
||||
<div className="grid grid-cols-3 gap-4 rounded-lg bg-gray-50 p-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">{relationships.length}</div>
|
||||
<div className="text-sm text-gray-600">관계 수</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{connectedTables.length}</div>
|
||||
<div className="text-sm text-gray-600">연결된 테이블</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">연결된 컬럼</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 연결된 테이블 목록 */}
|
||||
{connectedTables.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">연결된 테이블</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{connectedTables.map((table) => (
|
||||
<Badge key={table} variant="outline" className="text-xs">
|
||||
📋 {table}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 관계 목록 미리보기 */}
|
||||
{relationships.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">관계 목록</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="max-h-60 space-y-3 overflow-y-auto">
|
||||
{relationships.map((relationship, index) => (
|
||||
<div
|
||||
key={relationship.id || index}
|
||||
className="flex items-center justify-between rounded-lg border bg-white p-3 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{relationship.relationshipType}
|
||||
</Badge>
|
||||
<span className="font-medium">{relationship.fromTable}</span>
|
||||
<span className="text-gray-500">→</span>
|
||||
<span className="font-medium">{relationship.toTable}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-600">
|
||||
{relationship.fromColumns.join(", ")} → {relationship.toColumns.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{relationship.connectionType}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 관계가 없는 경우 안내 */}
|
||||
{relationships.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
<div className="mb-2 text-4xl">📭</div>
|
||||
<div className="text-sm">생성된 관계가 없습니다.</div>
|
||||
<div className="mt-1 text-xs text-gray-400">테이블을 추가하고 컬럼을 연결해서 관계를 생성해보세요.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || relationships.length === 0}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
|
||||
저장 중...
|
||||
</div>
|
||||
) : (
|
||||
"💾 저장하기"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveDiagramModal;
|
||||
|
|
@ -114,6 +114,50 @@ export interface DataFlowDiagramsResponse {
|
|||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
// 새로운 JSON 기반 타입들
|
||||
export interface JsonDataFlowDiagram {
|
||||
diagram_id: number;
|
||||
diagram_name: string;
|
||||
relationships: {
|
||||
relationships: JsonRelationship[];
|
||||
tables: string[];
|
||||
};
|
||||
company_code: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface JsonRelationship {
|
||||
id: string;
|
||||
fromTable: string;
|
||||
toTable: string;
|
||||
fromColumns: string[];
|
||||
toColumns: string[];
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
settings?: any;
|
||||
}
|
||||
|
||||
export interface CreateDiagramRequest {
|
||||
diagram_name: string;
|
||||
relationships: {
|
||||
relationships: JsonRelationship[];
|
||||
tables: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface JsonDataFlowDiagramsResponse {
|
||||
diagrams: JsonDataFlowDiagram[];
|
||||
pagination: {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 테이블 간 데이터 관계 설정 API 클래스
|
||||
export class DataFlowAPI {
|
||||
/**
|
||||
|
|
@ -446,21 +490,217 @@ export class DataFlowAPI {
|
|||
}
|
||||
}
|
||||
|
||||
// 특정 관계도의 모든 관계 조회 (diagram_id로)
|
||||
static async getDiagramRelationshipsByDiagramId(diagramId: number): Promise<TableRelationship[]> {
|
||||
// 특정 관계도의 모든 관계 조회 (diagram_id로) - JSON 기반 시스템
|
||||
static async getDiagramRelationshipsByDiagramId(
|
||||
diagramId: number,
|
||||
companyCode: string = "*",
|
||||
): Promise<TableRelationship[]> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<TableRelationship[]>>(
|
||||
`/dataflow/diagrams/${diagramId}/relationships`,
|
||||
);
|
||||
// 새로운 JSON 기반 시스템에서 관계도 조회
|
||||
const jsonDiagram = await this.getJsonDataFlowDiagramById(diagramId, companyCode);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 관계 조회에 실패했습니다.");
|
||||
if (!jsonDiagram || !jsonDiagram.relationships) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
// JSON 관계를 TableRelationship 형식으로 변환
|
||||
const relationshipsData = jsonDiagram.relationships as { relationships: JsonRelationship[]; tables: string[] };
|
||||
const relationships: TableRelationship[] = relationshipsData.relationships.map((rel: JsonRelationship) => ({
|
||||
relationship_id: 0, // JSON 기반에서는 개별 relationship_id가 없음
|
||||
relationship_name: rel.id || "관계",
|
||||
from_table_name: rel.fromTable,
|
||||
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",
|
||||
company_code: companyCode, // 실제 사용자 회사 코드 사용
|
||||
settings: rel.settings || {},
|
||||
created_at: jsonDiagram.created_at,
|
||||
updated_at: jsonDiagram.updated_at,
|
||||
created_by: jsonDiagram.created_by,
|
||||
updated_by: jsonDiagram.updated_by,
|
||||
}));
|
||||
|
||||
return relationships;
|
||||
} catch (error) {
|
||||
console.error("관계도 관계 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 새로운 JSON 기반 관계도 API ====================
|
||||
|
||||
/**
|
||||
* JSON 기반 관계도 목록 조회
|
||||
*/
|
||||
static async getJsonDataFlowDiagrams(
|
||||
page: number = 1,
|
||||
size: number = 20,
|
||||
searchTerm: string = "",
|
||||
companyCode: string = "*",
|
||||
): Promise<JsonDataFlowDiagramsResponse> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
size: size.toString(),
|
||||
companyCode: companyCode,
|
||||
...(searchTerm && { searchTerm }),
|
||||
});
|
||||
|
||||
const response = await apiClient.get<ApiResponse<JsonDataFlowDiagramsResponse>>(`/dataflow-diagrams?${params}`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as JsonDataFlowDiagramsResponse;
|
||||
} catch (error) {
|
||||
console.error("JSON 관계도 목록 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 기반 특정 관계도 조회
|
||||
*/
|
||||
static async getJsonDataFlowDiagramById(diagramId: number, companyCode: string = "*"): Promise<JsonDataFlowDiagram> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
companyCode: companyCode,
|
||||
});
|
||||
|
||||
const response = await apiClient.get<ApiResponse<JsonDataFlowDiagram>>(
|
||||
`/dataflow-diagrams/${diagramId}?${params}`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as JsonDataFlowDiagram;
|
||||
} catch (error) {
|
||||
console.error("JSON 관계도 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 기반 관계도 생성
|
||||
*/
|
||||
static async createJsonDataFlowDiagram(
|
||||
request: CreateDiagramRequest,
|
||||
companyCode: string = "*",
|
||||
userId: string = "SYSTEM",
|
||||
): Promise<JsonDataFlowDiagram> {
|
||||
try {
|
||||
const requestWithUserInfo = {
|
||||
...request,
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
};
|
||||
|
||||
const response = await apiClient.post<ApiResponse<JsonDataFlowDiagram>>(
|
||||
"/dataflow-diagrams",
|
||||
requestWithUserInfo,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 생성에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as JsonDataFlowDiagram;
|
||||
} catch (error) {
|
||||
console.error("JSON 관계도 생성 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 기반 관계도 수정
|
||||
*/
|
||||
static async updateJsonDataFlowDiagram(
|
||||
diagramId: number,
|
||||
request: Partial<CreateDiagramRequest>,
|
||||
companyCode: string = "*",
|
||||
userId: string = "SYSTEM",
|
||||
): Promise<JsonDataFlowDiagram> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
companyCode: companyCode,
|
||||
});
|
||||
|
||||
const requestWithUserInfo = {
|
||||
...request,
|
||||
updated_by: userId,
|
||||
};
|
||||
|
||||
const response = await apiClient.put<ApiResponse<JsonDataFlowDiagram>>(
|
||||
`/dataflow-diagrams/${diagramId}?${params}`,
|
||||
requestWithUserInfo,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 수정에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as JsonDataFlowDiagram;
|
||||
} catch (error) {
|
||||
console.error("JSON 관계도 수정 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 기반 관계도 삭제
|
||||
*/
|
||||
static async deleteJsonDataFlowDiagram(diagramId: number, companyCode: string = "*"): Promise<void> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
companyCode: companyCode,
|
||||
});
|
||||
|
||||
const response = await apiClient.delete<ApiResponse<void>>(`/dataflow-diagrams/${diagramId}?${params}`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 삭제에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("JSON 관계도 삭제 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 기반 관계도 복제
|
||||
*/
|
||||
static async copyJsonDataFlowDiagram(
|
||||
diagramId: number,
|
||||
companyCode: string = "*",
|
||||
newName?: string,
|
||||
userId: string = "SYSTEM",
|
||||
): Promise<JsonDataFlowDiagram> {
|
||||
try {
|
||||
const requestData = {
|
||||
companyCode: companyCode,
|
||||
userId: userId,
|
||||
...(newName && { new_name: newName }),
|
||||
};
|
||||
|
||||
const response = await apiClient.post<ApiResponse<JsonDataFlowDiagram>>(
|
||||
`/dataflow-diagrams/${diagramId}/copy`,
|
||||
requestData,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계도 복제에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as JsonDataFlowDiagram;
|
||||
} catch (error) {
|
||||
console.error("JSON 관계도 복제 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,325 @@
|
|||
import { Request, Response } from "express";
|
||||
import { DataflowDiagramService } from "../services/dataflowDiagramService";
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
companyCode: string;
|
||||
};
|
||||
}
|
||||
|
||||
const dataflowDiagramService = new DataflowDiagramService();
|
||||
|
||||
/**
|
||||
* 관계도 목록 조회
|
||||
* GET /api/dataflow-diagrams
|
||||
*/
|
||||
export const getDataflowDiagrams = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { page = 1, size = 20, searchTerm = "" } = req.query;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await dataflowDiagramService.getDataflowDiagrams(
|
||||
companyCode,
|
||||
parseInt(page as string),
|
||||
parseInt(size as string),
|
||||
searchTerm as string
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "관계도 목록을 성공적으로 조회했습니다.",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 목록 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 관계도 조회
|
||||
* GET /api/dataflow-diagrams/:diagramId
|
||||
*/
|
||||
export const getDataflowDiagramById = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { diagramId } = req.params;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const diagram = await dataflowDiagramService.getDataflowDiagramById(
|
||||
parseInt(diagramId),
|
||||
companyCode
|
||||
);
|
||||
|
||||
if (!diagram) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "관계도를 성공적으로 조회했습니다.",
|
||||
data: diagram,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("관계도 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 생성
|
||||
* POST /api/dataflow-diagrams
|
||||
*/
|
||||
export const createDataflowDiagram = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { diagram_name, relationships } = req.body;
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!diagram_name || !relationships) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "관계도 이름과 관계 정보는 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const diagram = await dataflowDiagramService.createDataflowDiagram({
|
||||
diagram_name,
|
||||
relationships,
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 생성되었습니다.",
|
||||
data: diagram,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("관계도 생성 실패:", error);
|
||||
|
||||
// 중복 이름 오류 처리
|
||||
if (
|
||||
error.code === "P2002" &&
|
||||
error.meta?.target?.includes("diagram_name")
|
||||
) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "같은 이름의 관계도가 이미 존재합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 생성 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 수정
|
||||
* PUT /api/dataflow-diagrams/:diagramId
|
||||
*/
|
||||
export const updateDataflowDiagram = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { diagramId } = req.params;
|
||||
const { diagram_name, relationships } = req.body;
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const diagram = await dataflowDiagramService.updateDataflowDiagram(
|
||||
parseInt(diagramId),
|
||||
companyCode,
|
||||
{
|
||||
diagram_name,
|
||||
relationships,
|
||||
updated_by: userId,
|
||||
}
|
||||
);
|
||||
|
||||
if (!diagram) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 수정되었습니다.",
|
||||
data: diagram,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("관계도 수정 실패:", error);
|
||||
|
||||
// 중복 이름 오류 처리
|
||||
if (
|
||||
error.code === "P2002" &&
|
||||
error.meta?.target?.includes("diagram_name")
|
||||
) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "같은 이름의 관계도가 이미 존재합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 수정 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
* DELETE /api/dataflow-diagrams/:diagramId
|
||||
*/
|
||||
export const deleteDataflowDiagram = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { diagramId } = req.params;
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await dataflowDiagramService.deleteDataflowDiagram(
|
||||
parseInt(diagramId),
|
||||
companyCode
|
||||
);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("관계도 삭제 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 삭제 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 관계도 복제
|
||||
* POST /api/dataflow-diagrams/:diagramId/copy
|
||||
*/
|
||||
export const copyDataflowDiagram = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { diagramId } = req.params;
|
||||
const { new_name } = req.body; // 선택적 새 이름
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "인증 정보가 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const copiedDiagram = await dataflowDiagramService.copyDataflowDiagram(
|
||||
parseInt(diagramId),
|
||||
companyCode,
|
||||
new_name,
|
||||
userId
|
||||
);
|
||||
|
||||
if (!copiedDiagram) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "원본 관계도를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "관계도가 성공적으로 복제되었습니다.",
|
||||
data: copiedDiagram,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("관계도 복제 실패:", error);
|
||||
|
||||
// 중복 이름 오류 처리
|
||||
if (
|
||||
error.code === "P2002" &&
|
||||
error.meta?.target?.includes("diagram_name")
|
||||
) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "같은 이름의 관계도가 이미 존재합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 복제 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { Router } from "express";
|
||||
import {
|
||||
getDataflowDiagrams,
|
||||
getDataflowDiagramById,
|
||||
createDataflowDiagram,
|
||||
updateDataflowDiagram,
|
||||
deleteDataflowDiagram,
|
||||
copyDataflowDiagram,
|
||||
} from "../controllers/dataflowDiagramController";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* 데이터플로우 관계도 관리 API
|
||||
*
|
||||
* 모든 엔드포인트는 인증이 필요하며, 회사 코드로 데이터를 격리합니다.
|
||||
*/
|
||||
|
||||
// 관계도 목록 조회 (페이지네이션, 검색 지원)
|
||||
// GET /api/dataflow-diagrams?page=1&size=20&searchTerm=검색어
|
||||
router.get("/", getDataflowDiagrams);
|
||||
|
||||
// 특정 관계도 조회
|
||||
// GET /api/dataflow-diagrams/:diagramId
|
||||
router.get("/:diagramId", getDataflowDiagramById);
|
||||
|
||||
// 관계도 생성
|
||||
// POST /api/dataflow-diagrams
|
||||
// Body: { diagram_name: string, relationships: object }
|
||||
router.post("/", createDataflowDiagram);
|
||||
|
||||
// 관계도 수정
|
||||
// PUT /api/dataflow-diagrams/:diagramId
|
||||
// Body: { diagram_name?: string, relationships?: object }
|
||||
router.put("/:diagramId", updateDataflowDiagram);
|
||||
|
||||
// 관계도 삭제
|
||||
// DELETE /api/dataflow-diagrams/:diagramId
|
||||
router.delete("/:diagramId", deleteDataflowDiagram);
|
||||
|
||||
// 관계도 복제
|
||||
// POST /api/dataflow-diagrams/:diagramId/copy
|
||||
// Body: { new_name?: string } (선택적)
|
||||
router.post("/:diagramId/copy", copyDataflowDiagram);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface DataflowDiagram {
|
||||
diagram_id: number;
|
||||
diagram_name: string;
|
||||
relationships: any; // JSON 타입
|
||||
company_code: string;
|
||||
created_at?: Date;
|
||||
updated_at?: Date;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface CreateDataflowDiagramData {
|
||||
diagram_name: string;
|
||||
relationships: any;
|
||||
company_code: string;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface UpdateDataflowDiagramData {
|
||||
diagram_name?: string;
|
||||
relationships?: any;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export class DataflowDiagramService {
|
||||
/**
|
||||
* 관계도 목록 조회 (페이지네이션)
|
||||
*/
|
||||
async getDataflowDiagrams(
|
||||
companyCode: string,
|
||||
page: number = 1,
|
||||
size: number = 20,
|
||||
searchTerm: string = ""
|
||||
) {
|
||||
const skip = (page - 1) * size;
|
||||
|
||||
const whereClause: any = {
|
||||
company_code: companyCode,
|
||||
};
|
||||
|
||||
if (searchTerm) {
|
||||
whereClause.diagram_name = {
|
||||
contains: searchTerm,
|
||||
mode: "insensitive",
|
||||
};
|
||||
}
|
||||
|
||||
const [diagrams, total] = await Promise.all([
|
||||
prisma.dataflow_diagrams.findMany({
|
||||
where: whereClause,
|
||||
orderBy: { created_at: "desc" },
|
||||
skip,
|
||||
take: size,
|
||||
}),
|
||||
prisma.dataflow_diagrams.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
diagrams,
|
||||
pagination: {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
totalPages: Math.ceil(total / size),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 관계도 조회
|
||||
*/
|
||||
async getDataflowDiagramById(
|
||||
diagramId: number,
|
||||
companyCode: string
|
||||
): Promise<DataflowDiagram | null> {
|
||||
return await prisma.dataflow_diagrams.findFirst({
|
||||
where: {
|
||||
diagram_id: diagramId,
|
||||
company_code: companyCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 생성
|
||||
*/
|
||||
async createDataflowDiagram(
|
||||
data: CreateDataflowDiagramData
|
||||
): Promise<DataflowDiagram> {
|
||||
return await prisma.dataflow_diagrams.create({
|
||||
data: {
|
||||
diagram_name: data.diagram_name,
|
||||
relationships: data.relationships,
|
||||
company_code: data.company_code,
|
||||
created_by: data.created_by,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 수정
|
||||
*/
|
||||
async updateDataflowDiagram(
|
||||
diagramId: number,
|
||||
companyCode: string,
|
||||
data: UpdateDataflowDiagramData
|
||||
): Promise<DataflowDiagram | null> {
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const existingDiagram = await this.getDataflowDiagramById(
|
||||
diagramId,
|
||||
companyCode
|
||||
);
|
||||
if (!existingDiagram) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await prisma.dataflow_diagrams.update({
|
||||
where: {
|
||||
diagram_id: diagramId,
|
||||
},
|
||||
data: {
|
||||
...(data.diagram_name && { diagram_name: data.diagram_name }),
|
||||
...(data.relationships && { relationships: data.relationships }),
|
||||
...(data.updated_by && { updated_by: data.updated_by }),
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 삭제
|
||||
*/
|
||||
async deleteDataflowDiagram(
|
||||
diagramId: number,
|
||||
companyCode: string
|
||||
): Promise<boolean> {
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const existingDiagram = await this.getDataflowDiagramById(
|
||||
diagramId,
|
||||
companyCode
|
||||
);
|
||||
if (!existingDiagram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await prisma.dataflow_diagrams.delete({
|
||||
where: {
|
||||
diagram_id: diagramId,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도 복제
|
||||
*/
|
||||
async copyDataflowDiagram(
|
||||
diagramId: number,
|
||||
companyCode: string,
|
||||
newName?: string,
|
||||
createdBy?: string
|
||||
): Promise<DataflowDiagram | null> {
|
||||
const originalDiagram = await this.getDataflowDiagramById(
|
||||
diagramId,
|
||||
companyCode
|
||||
);
|
||||
if (!originalDiagram) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 복제본 이름 생성
|
||||
let copyName = newName;
|
||||
if (!copyName) {
|
||||
// "(1)", "(2)" 형식으로 이름 생성
|
||||
const baseName = originalDiagram.diagram_name;
|
||||
let counter = 1;
|
||||
|
||||
while (true) {
|
||||
copyName = `${baseName} (${counter})`;
|
||||
const existing = await prisma.dataflow_diagrams.findFirst({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
diagram_name: copyName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) break;
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.createDataflowDiagram({
|
||||
diagram_name: copyName,
|
||||
relationships: originalDiagram.relationships,
|
||||
company_code: companyCode,
|
||||
created_by: createdBy,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue