dev #46
|
|
@ -5100,3 +5100,72 @@ model code_info {
|
|||
@@id([code_category, code_value], map: "pk_code_info")
|
||||
@@index([code_category, sort_order], map: "idx_code_info_sort")
|
||||
}
|
||||
|
||||
// 테이블 간 관계 정의
|
||||
model table_relationships {
|
||||
relationship_id Int @id @default(autoincrement())
|
||||
relationship_name String @db.VarChar(200)
|
||||
from_table_name String @db.VarChar(100)
|
||||
from_column_name String @db.VarChar(100)
|
||||
to_table_name String @db.VarChar(100)
|
||||
to_column_name String @db.VarChar(100)
|
||||
relationship_type String @db.VarChar(20) // 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many'
|
||||
connection_type String @db.VarChar(20) // 'simple-key', 'data-save', 'external-call'
|
||||
company_code String @db.VarChar(50)
|
||||
settings Json? // 연결 종류별 세부 설정
|
||||
is_active String? @default("Y") @db.Char(1)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
|
||||
// 역참조 관계
|
||||
bridges data_relationship_bridge[]
|
||||
|
||||
@@index([company_code], map: "idx_table_relationships_company_code")
|
||||
@@index([from_table_name], map: "idx_table_relationships_from_table")
|
||||
@@index([to_table_name], map: "idx_table_relationships_to_table")
|
||||
}
|
||||
|
||||
// 테이블 간 데이터 관계 중계 테이블 - 실제 데이터 연결 정보 저장
|
||||
model data_relationship_bridge {
|
||||
bridge_id Int @id @default(autoincrement())
|
||||
relationship_id Int
|
||||
|
||||
// 소스 테이블 정보
|
||||
from_table_name String @db.VarChar(100)
|
||||
from_column_name String @db.VarChar(100)
|
||||
from_key_value String? @db.VarChar(500) // 실제 연결 키 값
|
||||
from_record_id String? @db.VarChar(100) // 소스 레코드의 Primary Key
|
||||
|
||||
// 타겟 테이블 정보
|
||||
to_table_name String @db.VarChar(100)
|
||||
to_column_name String @db.VarChar(100)
|
||||
to_key_value String? @db.VarChar(500) // 실제 연결 키 값
|
||||
to_record_id String? @db.VarChar(100) // 타겟 레코드의 Primary Key
|
||||
|
||||
// 메타데이터
|
||||
connection_type String @db.VarChar(20) // 'simple-key', 'data-save', 'external-call'
|
||||
company_code String @db.VarChar(50)
|
||||
created_at DateTime @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_at DateTime @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
is_active String @default("Y") @db.Char(1)
|
||||
|
||||
// 추가 설정 (JSON)
|
||||
bridge_data Json? // 연결 종류별 추가 데이터
|
||||
|
||||
// 관계 설정
|
||||
relationship table_relationships @relation(fields: [relationship_id], references: [relationship_id], onDelete: Cascade)
|
||||
|
||||
@@index([relationship_id], map: "idx_data_bridge_relationship")
|
||||
@@index([from_table_name, from_key_value], map: "idx_data_bridge_from_table")
|
||||
@@index([to_table_name, to_key_value], map: "idx_data_bridge_to_table")
|
||||
@@index([company_code], map: "idx_data_bridge_company")
|
||||
@@index([is_active], map: "idx_data_bridge_active")
|
||||
@@index([connection_type], map: "idx_data_bridge_connection_type")
|
||||
@@index([from_table_name, from_column_name, from_key_value], map: "idx_data_bridge_from_lookup")
|
||||
@@index([to_table_name, to_column_name, to_key_value], map: "idx_data_bridge_to_lookup")
|
||||
@@index([company_code, is_active], map: "idx_data_bridge_company_active")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import commonCodeRoutes from "./routes/commonCodeRoutes";
|
|||
import dynamicFormRoutes from "./routes/dynamicFormRoutes";
|
||||
import fileRoutes from "./routes/fileRoutes";
|
||||
import companyManagementRoutes from "./routes/companyManagementRoutes";
|
||||
import dataflowRoutes from "./routes/dataflowRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
|
|
@ -83,6 +84,7 @@ app.use("/api/common-codes", commonCodeRoutes);
|
|||
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/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,553 @@
|
|||
import { Request, Response } from "express";
|
||||
import { logger } from "../utils/logger";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { ApiResponse } from "../types/common";
|
||||
import { DataflowService } from "../services/dataflowService";
|
||||
|
||||
/**
|
||||
* 테이블 관계 생성
|
||||
*/
|
||||
export async function createTableRelationship(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 테이블 관계 생성 시작 ===");
|
||||
|
||||
const {
|
||||
relationshipName,
|
||||
fromTableName,
|
||||
fromColumnName,
|
||||
toTableName,
|
||||
toColumnName,
|
||||
relationshipType,
|
||||
connectionType,
|
||||
settings,
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (
|
||||
!relationshipName ||
|
||||
!fromTableName ||
|
||||
!fromColumnName ||
|
||||
!toTableName ||
|
||||
!toColumnName
|
||||
) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다.",
|
||||
error: {
|
||||
code: "MISSING_REQUIRED_FIELDS",
|
||||
details:
|
||||
"relationshipName, fromTableName, fromColumnName, toTableName, toColumnName는 필수입니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 정보에서 회사 코드 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
const userId = (req.user as any)?.userId || "system";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const relationship = await dataflowService.createTableRelationship({
|
||||
relationshipName,
|
||||
fromTableName,
|
||||
fromColumnName,
|
||||
toTableName,
|
||||
toColumnName,
|
||||
relationshipType: relationshipType || "one-to-one",
|
||||
connectionType: connectionType || "simple-key",
|
||||
companyCode,
|
||||
settings: settings || {},
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
logger.info(`테이블 관계 생성 완료: ${relationship.relationship_id}`);
|
||||
|
||||
const response: ApiResponse<any> = {
|
||||
success: true,
|
||||
message: "테이블 관계가 성공적으로 생성되었습니다.",
|
||||
data: relationship,
|
||||
};
|
||||
|
||||
res.status(201).json(response);
|
||||
} catch (error) {
|
||||
logger.error("테이블 관계 생성 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계 생성 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_CREATE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 관계 목록 조회 (회사별)
|
||||
*/
|
||||
export async function getTableRelationships(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 테이블 관계 목록 조회 시작 ===");
|
||||
|
||||
// 사용자 정보에서 회사 코드 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const relationships =
|
||||
await dataflowService.getTableRelationships(companyCode);
|
||||
|
||||
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: "TABLE_RELATIONSHIPS_LIST_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 관계 수정
|
||||
*/
|
||||
export async function updateTableRelationship(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 테이블 관계 수정 시작 ===");
|
||||
|
||||
const { relationshipId } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
if (!relationshipId) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계 ID가 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_RELATIONSHIP_ID",
|
||||
details: "relationshipId 파라미터가 누락되었습니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 정보에서 회사 코드와 사용자 ID 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
const userId = (req.user as any)?.userId || "system";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const relationship = await dataflowService.updateTableRelationship(
|
||||
parseInt(relationshipId),
|
||||
{
|
||||
...updateData,
|
||||
updatedBy: userId,
|
||||
},
|
||||
companyCode
|
||||
);
|
||||
|
||||
if (!relationship) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_NOT_FOUND",
|
||||
details: `관계 ID ${relationshipId}를 찾을 수 없습니다.`,
|
||||
},
|
||||
};
|
||||
res.status(404).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`테이블 관계 수정 완료: ${relationshipId}`);
|
||||
|
||||
const response: ApiResponse<any> = {
|
||||
success: true,
|
||||
message: "테이블 관계가 성공적으로 수정되었습니다.",
|
||||
data: relationship,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("테이블 관계 수정 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계 수정 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_UPDATE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 관계 삭제
|
||||
*/
|
||||
export async function deleteTableRelationship(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 테이블 관계 삭제 시작 ===");
|
||||
|
||||
const { relationshipId } = req.params;
|
||||
|
||||
if (!relationshipId) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계 ID가 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_RELATIONSHIP_ID",
|
||||
details: "relationshipId 파라미터가 누락되었습니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 정보에서 회사 코드 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const success = await dataflowService.deleteTableRelationship(
|
||||
parseInt(relationshipId),
|
||||
companyCode
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_NOT_FOUND",
|
||||
details: `관계 ID ${relationshipId}를 찾을 수 없습니다.`,
|
||||
},
|
||||
};
|
||||
res.status(404).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`테이블 관계 삭제 완료: ${relationshipId}`);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
message: "테이블 관계가 성공적으로 삭제되었습니다.",
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("테이블 관계 삭제 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계 삭제 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_DELETE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블 관계 조회
|
||||
*/
|
||||
export async function getTableRelationship(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 테이블 관계 조회 시작 ===");
|
||||
|
||||
const { relationshipId } = req.params;
|
||||
|
||||
if (!relationshipId) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "관계 ID가 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_RELATIONSHIP_ID",
|
||||
details: "relationshipId 파라미터가 누락되었습니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 정보에서 회사 코드 가져오기
|
||||
const companyCode = (req.user as any)?.company_code || "*";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const relationship = await dataflowService.getTableRelationship(
|
||||
parseInt(relationshipId),
|
||||
companyCode
|
||||
);
|
||||
|
||||
if (!relationship) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계를 찾을 수 없습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_NOT_FOUND",
|
||||
details: `관계 ID ${relationshipId}를 찾을 수 없습니다.`,
|
||||
},
|
||||
};
|
||||
res.status(404).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`테이블 관계 조회 완료: ${relationshipId}`);
|
||||
|
||||
const response: ApiResponse<any> = {
|
||||
success: true,
|
||||
message: "테이블 관계를 성공적으로 조회했습니다.",
|
||||
data: relationship,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("테이블 관계 조회 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 관계 조회 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "TABLE_RELATIONSHIP_GET_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 데이터 연결 관리 API ====================
|
||||
|
||||
/**
|
||||
* 데이터 관계 연결 생성
|
||||
*/
|
||||
export async function createDataLink(
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
relationshipId,
|
||||
fromTableName,
|
||||
fromColumnName,
|
||||
fromKeyValue,
|
||||
fromRecordId,
|
||||
toTableName,
|
||||
toColumnName,
|
||||
toKeyValue,
|
||||
toRecordId,
|
||||
connectionType,
|
||||
bridgeData,
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (
|
||||
!relationshipId ||
|
||||
!fromTableName ||
|
||||
!fromColumnName ||
|
||||
!fromKeyValue ||
|
||||
!toTableName ||
|
||||
!toColumnName ||
|
||||
!toKeyValue ||
|
||||
!connectionType
|
||||
) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다.",
|
||||
error: {
|
||||
code: "MISSING_REQUIRED_FIELDS",
|
||||
details:
|
||||
"필수 필드: relationshipId, fromTableName, fromColumnName, fromKeyValue, toTableName, toColumnName, toKeyValue, connectionType",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = (req as any).user;
|
||||
const companyCode = userInfo?.company_code || "*";
|
||||
const createdBy = userInfo?.userId || "system";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const bridge = await dataflowService.createDataLink({
|
||||
relationshipId,
|
||||
fromTableName,
|
||||
fromColumnName,
|
||||
fromKeyValue,
|
||||
fromRecordId,
|
||||
toTableName,
|
||||
toColumnName,
|
||||
toKeyValue,
|
||||
toRecordId,
|
||||
connectionType,
|
||||
companyCode,
|
||||
bridgeData,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
const response: ApiResponse<typeof bridge> = {
|
||||
success: true,
|
||||
message: "데이터 연결이 성공적으로 생성되었습니다.",
|
||||
data: bridge,
|
||||
};
|
||||
|
||||
res.status(201).json(response);
|
||||
} catch (error) {
|
||||
logger.error("데이터 연결 생성 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "데이터 연결 생성 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "DATA_LINK_CREATE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계별 연결된 데이터 조회
|
||||
*/
|
||||
export async function getLinkedDataByRelationship(
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const relationshipId = parseInt(req.params.relationshipId);
|
||||
|
||||
if (!relationshipId || isNaN(relationshipId)) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "유효하지 않은 관계 ID입니다.",
|
||||
error: {
|
||||
code: "INVALID_RELATIONSHIP_ID",
|
||||
details: "관계 ID는 숫자여야 합니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = (req as any).user;
|
||||
const companyCode = userInfo?.company_code || "*";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
const linkedData = await dataflowService.getLinkedDataByRelationship(
|
||||
relationshipId,
|
||||
companyCode
|
||||
);
|
||||
|
||||
const response: ApiResponse<typeof linkedData> = {
|
||||
success: true,
|
||||
message: "연결된 데이터를 성공적으로 조회했습니다.",
|
||||
data: linkedData,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("연결된 데이터 조회 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "연결된 데이터 조회 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "LINKED_DATA_GET_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 연결 삭제
|
||||
*/
|
||||
export async function deleteDataLink(
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const bridgeId = parseInt(req.params.bridgeId);
|
||||
|
||||
if (!bridgeId || isNaN(bridgeId)) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "유효하지 않은 Bridge ID입니다.",
|
||||
error: {
|
||||
code: "INVALID_BRIDGE_ID",
|
||||
details: "Bridge ID는 숫자여야 합니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = (req as any).user;
|
||||
const companyCode = userInfo?.company_code || "*";
|
||||
const deletedBy = userInfo?.userId || "system";
|
||||
|
||||
const dataflowService = new DataflowService();
|
||||
await dataflowService.deleteDataLink(bridgeId, companyCode, deletedBy);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
message: "데이터 연결이 성공적으로 삭제되었습니다.",
|
||||
data: null,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("데이터 연결 삭제 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "데이터 연결 삭제 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "DATA_LINK_DELETE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import express from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import {
|
||||
createTableRelationship,
|
||||
getTableRelationships,
|
||||
getTableRelationship,
|
||||
updateTableRelationship,
|
||||
deleteTableRelationship,
|
||||
createDataLink,
|
||||
getLinkedDataByRelationship,
|
||||
deleteDataLink,
|
||||
} from "../controllers/dataflowController";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* 테이블 관계 생성
|
||||
* POST /api/dataflow/table-relationships
|
||||
*/
|
||||
router.post("/table-relationships", createTableRelationship);
|
||||
|
||||
/**
|
||||
* 테이블 관계 목록 조회 (회사별)
|
||||
* GET /api/dataflow/table-relationships
|
||||
*/
|
||||
router.get("/table-relationships", getTableRelationships);
|
||||
|
||||
/**
|
||||
* 특정 테이블 관계 조회
|
||||
* GET /api/dataflow/table-relationships/:relationshipId
|
||||
*/
|
||||
router.get("/table-relationships/:relationshipId", getTableRelationship);
|
||||
|
||||
/**
|
||||
* 테이블 관계 수정
|
||||
* PUT /api/dataflow/table-relationships/:relationshipId
|
||||
*/
|
||||
router.put("/table-relationships/:relationshipId", updateTableRelationship);
|
||||
|
||||
/**
|
||||
* 테이블 관계 삭제
|
||||
* DELETE /api/dataflow/table-relationships/:relationshipId
|
||||
*/
|
||||
router.delete("/table-relationships/:relationshipId", deleteTableRelationship);
|
||||
|
||||
// ==================== 데이터 연결 관리 라우트 ====================
|
||||
|
||||
/**
|
||||
* 데이터 연결 생성
|
||||
* POST /api/dataflow/data-links
|
||||
*/
|
||||
router.post("/data-links", createDataLink);
|
||||
|
||||
/**
|
||||
* 관계별 연결된 데이터 조회
|
||||
* GET /api/dataflow/data-links/relationship/:relationshipId
|
||||
*/
|
||||
router.get(
|
||||
"/data-links/relationship/:relationshipId",
|
||||
getLinkedDataByRelationship
|
||||
);
|
||||
|
||||
/**
|
||||
* 데이터 연결 삭제
|
||||
* DELETE /api/dataflow/data-links/:bridgeId
|
||||
*/
|
||||
router.delete("/data-links/:bridgeId", deleteDataLink);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,665 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 테이블 관계 생성 데이터 타입
|
||||
interface CreateTableRelationshipData {
|
||||
relationshipName: string;
|
||||
fromTableName: string;
|
||||
fromColumnName: string;
|
||||
toTableName: string;
|
||||
toColumnName: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
companyCode: string;
|
||||
settings: any;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
// 테이블 관계 수정 데이터 타입
|
||||
interface UpdateTableRelationshipData {
|
||||
relationshipName?: string;
|
||||
fromTableName?: string;
|
||||
fromColumnName?: string;
|
||||
toTableName?: string;
|
||||
toColumnName?: string;
|
||||
relationshipType?: string;
|
||||
connectionType?: string;
|
||||
settings?: any;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
export class DataflowService {
|
||||
/**
|
||||
* 테이블 관계 생성
|
||||
*/
|
||||
async createTableRelationship(data: CreateTableRelationshipData) {
|
||||
try {
|
||||
logger.info("DataflowService: 테이블 관계 생성 시작", data);
|
||||
|
||||
// 중복 관계 확인
|
||||
const existingRelationship = await prisma.table_relationships.findFirst({
|
||||
where: {
|
||||
from_table_name: data.fromTableName,
|
||||
from_column_name: data.fromColumnName,
|
||||
to_table_name: data.toTableName,
|
||||
to_column_name: data.toColumnName,
|
||||
company_code: data.companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRelationship) {
|
||||
throw new Error(
|
||||
`이미 존재하는 관계입니다: ${data.fromTableName}.${data.fromColumnName} → ${data.toTableName}.${data.toColumnName}`
|
||||
);
|
||||
}
|
||||
|
||||
// 새 관계 생성
|
||||
const relationship = await prisma.table_relationships.create({
|
||||
data: {
|
||||
relationship_name: data.relationshipName,
|
||||
from_table_name: data.fromTableName,
|
||||
from_column_name: data.fromColumnName,
|
||||
to_table_name: data.toTableName,
|
||||
to_column_name: data.toColumnName,
|
||||
relationship_type: data.relationshipType,
|
||||
connection_type: data.connectionType,
|
||||
company_code: data.companyCode,
|
||||
settings: data.settings,
|
||||
created_by: data.createdBy,
|
||||
updated_by: data.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}`
|
||||
);
|
||||
return relationship;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블 관계 생성 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사별 테이블 관계 목록 조회
|
||||
*/
|
||||
async getTableRelationships(companyCode: string) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 목록 조회 시작 - 회사코드: ${companyCode}`
|
||||
);
|
||||
|
||||
// 관리자는 모든 회사의 관계를 볼 수 있음
|
||||
const whereCondition: any = {
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const relationships = await prisma.table_relationships.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: {
|
||||
created_date: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 목록 조회 완료 - ${relationships.length}개`
|
||||
);
|
||||
return relationships;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블 관계 목록 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블 관계 조회
|
||||
*/
|
||||
async getTableRelationship(relationshipId: number, companyCode: string) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 조회 시작 - ID: ${relationshipId}, 회사코드: ${companyCode}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
relationship_id: relationshipId,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const relationship = await prisma.table_relationships.findFirst({
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
if (relationship) {
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 조회 완료 - ID: ${relationshipId}`
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
`DataflowService: 테이블 관계를 찾을 수 없음 - ID: ${relationshipId}`
|
||||
);
|
||||
}
|
||||
|
||||
return relationship;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블 관계 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 관계 수정
|
||||
*/
|
||||
async updateTableRelationship(
|
||||
relationshipId: number,
|
||||
updateData: UpdateTableRelationshipData,
|
||||
companyCode: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 수정 시작 - ID: ${relationshipId}`,
|
||||
updateData
|
||||
);
|
||||
|
||||
// 기존 관계 확인
|
||||
const existingRelationship = await this.getTableRelationship(
|
||||
relationshipId,
|
||||
companyCode
|
||||
);
|
||||
if (!existingRelationship) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 관계 수정
|
||||
const relationship = await prisma.table_relationships.update({
|
||||
where: {
|
||||
relationship_id: relationshipId,
|
||||
},
|
||||
data: {
|
||||
...updateData,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 수정 완료 - ID: ${relationshipId}`
|
||||
);
|
||||
return relationship;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블 관계 수정 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 관계 삭제 (소프트 삭제)
|
||||
*/
|
||||
async deleteTableRelationship(relationshipId: number, companyCode: string) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 삭제 시작 - ID: ${relationshipId}, 회사코드: ${companyCode}`
|
||||
);
|
||||
|
||||
// 기존 관계 확인
|
||||
const existingRelationship = await this.getTableRelationship(
|
||||
relationshipId,
|
||||
companyCode
|
||||
);
|
||||
if (!existingRelationship) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 소프트 삭제 (is_active = 'N')
|
||||
await prisma.table_relationships.update({
|
||||
where: {
|
||||
relationship_id: relationshipId,
|
||||
},
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 테이블 관계 삭제 완료 - ID: ${relationshipId}`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블 관계 삭제 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블과 관련된 모든 관계 조회
|
||||
*/
|
||||
async getRelationshipsByTable(tableName: string, companyCode: string) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 테이블별 관계 조회 시작 - 테이블: ${tableName}, 회사코드: ${companyCode}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
OR: [{ from_table_name: tableName }, { to_table_name: tableName }],
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const relationships = await prisma.table_relationships.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: {
|
||||
created_date: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 테이블별 관계 조회 완료 - ${relationships.length}개`
|
||||
);
|
||||
return relationships;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블별 관계 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 타입별 관계 조회
|
||||
*/
|
||||
async getRelationshipsByConnectionType(
|
||||
connectionType: string,
|
||||
companyCode: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 연결타입별 관계 조회 시작 - 타입: ${connectionType}, 회사코드: ${companyCode}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
connection_type: connectionType,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const relationships = await prisma.table_relationships.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: {
|
||||
created_date: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 연결타입별 관계 조회 완료 - ${relationships.length}개`
|
||||
);
|
||||
return relationships;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 연결타입별 관계 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계 통계 조회
|
||||
*/
|
||||
async getRelationshipStats(companyCode: string) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 관계 통계 조회 시작 - 회사코드: ${companyCode}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
// 전체 관계 수
|
||||
const totalCount = await prisma.table_relationships.count({
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
// 관계 타입별 통계
|
||||
const relationshipTypeStats = await prisma.table_relationships.groupBy({
|
||||
by: ["relationship_type"],
|
||||
where: whereCondition,
|
||||
_count: {
|
||||
relationship_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 연결 타입별 통계
|
||||
const connectionTypeStats = await prisma.table_relationships.groupBy({
|
||||
by: ["connection_type"],
|
||||
where: whereCondition,
|
||||
_count: {
|
||||
relationship_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const stats = {
|
||||
totalCount,
|
||||
relationshipTypeStats: relationshipTypeStats.map((stat) => ({
|
||||
type: stat.relationship_type,
|
||||
count: stat._count.relationship_id,
|
||||
})),
|
||||
connectionTypeStats: connectionTypeStats.map((stat) => ({
|
||||
type: stat.connection_type,
|
||||
count: stat._count.relationship_id,
|
||||
})),
|
||||
};
|
||||
|
||||
logger.info(`DataflowService: 관계 통계 조회 완료`, stats);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 관계 통계 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 데이터 중계 관리 ====================
|
||||
|
||||
/**
|
||||
* 데이터 관계 연결 생성
|
||||
*/
|
||||
async createDataLink(linkData: {
|
||||
relationshipId: number;
|
||||
fromTableName: string;
|
||||
fromColumnName: string;
|
||||
fromKeyValue: string;
|
||||
fromRecordId?: string;
|
||||
toTableName: string;
|
||||
toColumnName: string;
|
||||
toKeyValue: string;
|
||||
toRecordId?: string;
|
||||
connectionType: string;
|
||||
companyCode: string;
|
||||
bridgeData?: any;
|
||||
createdBy: string;
|
||||
}) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 데이터 연결 생성 시작 - 관계ID: ${linkData.relationshipId}`
|
||||
);
|
||||
|
||||
const bridge = await prisma.data_relationship_bridge.create({
|
||||
data: {
|
||||
relationship_id: linkData.relationshipId,
|
||||
from_table_name: linkData.fromTableName,
|
||||
from_column_name: linkData.fromColumnName,
|
||||
from_key_value: linkData.fromKeyValue,
|
||||
from_record_id: linkData.fromRecordId,
|
||||
to_table_name: linkData.toTableName,
|
||||
to_column_name: linkData.toColumnName,
|
||||
to_key_value: linkData.toKeyValue,
|
||||
to_record_id: linkData.toRecordId,
|
||||
connection_type: linkData.connectionType,
|
||||
company_code: linkData.companyCode,
|
||||
bridge_data: linkData.bridgeData || {},
|
||||
created_by: linkData.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 데이터 연결 생성 완료 - Bridge ID: ${bridge.bridge_id}`
|
||||
);
|
||||
return bridge;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 데이터 연결 생성 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계별 연결된 데이터 조회
|
||||
*/
|
||||
async getLinkedDataByRelationship(
|
||||
relationshipId: number,
|
||||
companyCode: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 관계별 연결 데이터 조회 시작 - 관계ID: ${relationshipId}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
relationship_id: relationshipId,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const linkedData = await prisma.data_relationship_bridge.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: { created_at: "desc" },
|
||||
include: {
|
||||
relationship: {
|
||||
select: {
|
||||
relationship_name: true,
|
||||
relationship_type: true,
|
||||
connection_type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 관계별 연결 데이터 조회 완료 - ${linkedData.length}건`
|
||||
);
|
||||
return linkedData;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 관계별 연결 데이터 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블의 연결된 데이터 조회
|
||||
*/
|
||||
async getLinkedDataByTable(
|
||||
tableName: string,
|
||||
keyValue?: string,
|
||||
companyCode?: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 테이블별 연결 데이터 조회 시작 - 테이블: ${tableName}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
OR: [{ from_table_name: tableName }, { to_table_name: tableName }],
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 특정 키 값으로 필터링
|
||||
if (keyValue) {
|
||||
whereCondition.OR = [
|
||||
{ from_table_name: tableName, from_key_value: keyValue },
|
||||
{ to_table_name: tableName, to_key_value: keyValue },
|
||||
];
|
||||
}
|
||||
|
||||
// 회사코드 필터링
|
||||
if (companyCode && companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const linkedData = await prisma.data_relationship_bridge.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: { created_at: "desc" },
|
||||
include: {
|
||||
relationship: {
|
||||
select: {
|
||||
relationship_name: true,
|
||||
relationship_type: true,
|
||||
connection_type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 테이블별 연결 데이터 조회 완료 - ${linkedData.length}건`
|
||||
);
|
||||
return linkedData;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 테이블별 연결 데이터 조회 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 연결 수정
|
||||
*/
|
||||
async updateDataLink(
|
||||
bridgeId: number,
|
||||
updateData: {
|
||||
fromKeyValue?: string;
|
||||
fromRecordId?: string;
|
||||
toKeyValue?: string;
|
||||
toRecordId?: string;
|
||||
bridgeData?: any;
|
||||
updatedBy: string;
|
||||
},
|
||||
companyCode: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 데이터 연결 수정 시작 - Bridge ID: ${bridgeId}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
bridge_id: bridgeId,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const updatedBridge = await prisma.data_relationship_bridge.update({
|
||||
where: whereCondition,
|
||||
data: {
|
||||
...updateData,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 데이터 연결 수정 완료 - Bridge ID: ${bridgeId}`
|
||||
);
|
||||
return updatedBridge;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 데이터 연결 수정 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 연결 삭제 (소프트 삭제)
|
||||
*/
|
||||
async deleteDataLink(
|
||||
bridgeId: number,
|
||||
companyCode: string,
|
||||
deletedBy: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 데이터 연결 삭제 시작 - Bridge ID: ${bridgeId}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
bridge_id: bridgeId,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
await prisma.data_relationship_bridge.update({
|
||||
where: whereCondition,
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_at: new Date(),
|
||||
updated_by: deletedBy,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 데이터 연결 삭제 완료 - Bridge ID: ${bridgeId}`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 데이터 연결 삭제 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계 삭제 시 연결된 모든 데이터도 삭제
|
||||
*/
|
||||
async deleteAllLinkedDataByRelationship(
|
||||
relationshipId: number,
|
||||
companyCode: string,
|
||||
deletedBy: string
|
||||
) {
|
||||
try {
|
||||
logger.info(
|
||||
`DataflowService: 관계별 모든 데이터 연결 삭제 시작 - 관계ID: ${relationshipId}`
|
||||
);
|
||||
|
||||
const whereCondition: any = {
|
||||
relationship_id: relationshipId,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 관리자가 아닌 경우 회사코드 제한
|
||||
if (companyCode !== "*") {
|
||||
whereCondition.company_code = companyCode;
|
||||
}
|
||||
|
||||
const result = await prisma.data_relationship_bridge.updateMany({
|
||||
where: whereCondition,
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_at: new Date(),
|
||||
updated_by: deletedBy,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`DataflowService: 관계별 모든 데이터 연결 삭제 완료 - ${result.count}건`
|
||||
);
|
||||
return result.count;
|
||||
} catch (error) {
|
||||
logger.error("DataflowService: 관계별 모든 데이터 연결 삭제 실패", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,11 @@ import { Button } from "@/components/ui/button";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ArrowRight, Database, Link } from "lucide-react";
|
||||
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
|
||||
import { DataFlowAPI, TableRelationship } from "@/lib/api/dataflow";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
// 연결 정보 타입
|
||||
interface ConnectionInfo {
|
||||
|
|
@ -40,20 +41,44 @@ interface ConnectionConfig {
|
|||
connectionType: "simple-key" | "data-save" | "external-call";
|
||||
fromColumnName: string;
|
||||
toColumnName: string;
|
||||
settings?: Record<string, any>;
|
||||
settings?: Record<string, unknown>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 단순 키값 연결 설정
|
||||
interface SimpleKeySettings {
|
||||
syncDirection: "unidirectional" | "bidirectional";
|
||||
notes: string;
|
||||
}
|
||||
|
||||
// 데이터 저장 설정
|
||||
interface DataSaveSettings {
|
||||
sourceField: string;
|
||||
targetField: string;
|
||||
saveConditions: string;
|
||||
}
|
||||
|
||||
// 외부 호출 설정
|
||||
interface ExternalCallSettings {
|
||||
callType: "rest-api" | "email" | "webhook" | "ftp" | "queue";
|
||||
apiUrl?: string;
|
||||
httpMethod?: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: string;
|
||||
bodyTemplate?: string;
|
||||
}
|
||||
|
||||
interface ConnectionSetupModalProps {
|
||||
isOpen: boolean;
|
||||
connection: ConnectionInfo | null;
|
||||
onConfirm: (config: ConnectionConfig) => void;
|
||||
companyCode: string;
|
||||
onConfirm: (relationship: TableRelationship) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||
isOpen,
|
||||
connection,
|
||||
companyCode,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
|
|
@ -64,6 +89,27 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
fromColumnName: "",
|
||||
toColumnName: "",
|
||||
description: "",
|
||||
settings: {},
|
||||
});
|
||||
|
||||
// 연결 종류별 설정 상태
|
||||
const [simpleKeySettings, setSimpleKeySettings] = useState<SimpleKeySettings>({
|
||||
syncDirection: "bidirectional",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const [dataSaveSettings, setDataSaveSettings] = useState<DataSaveSettings>({
|
||||
sourceField: "",
|
||||
targetField: "",
|
||||
saveConditions: "",
|
||||
});
|
||||
|
||||
const [externalCallSettings, setExternalCallSettings] = useState<ExternalCallSettings>({
|
||||
callType: "rest-api",
|
||||
apiUrl: "",
|
||||
httpMethod: "POST",
|
||||
headers: "{}",
|
||||
bodyTemplate: "{}",
|
||||
});
|
||||
|
||||
// 모달이 열릴 때 기본값 설정
|
||||
|
|
@ -79,14 +125,82 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
fromColumnName: "",
|
||||
toColumnName: "",
|
||||
description: `${fromTableName}과 ${toTableName} 간의 데이터 관계`,
|
||||
settings: {},
|
||||
});
|
||||
|
||||
// 단순 키값 연결 기본값 설정
|
||||
setSimpleKeySettings({
|
||||
syncDirection: "bidirectional",
|
||||
notes: `${fromTableName}과 ${toTableName} 간의 키값 연결`,
|
||||
});
|
||||
|
||||
// 데이터 저장 기본값 설정
|
||||
setDataSaveSettings({
|
||||
sourceField: "",
|
||||
targetField: "",
|
||||
saveConditions: "데이터 저장 조건을 입력하세요",
|
||||
});
|
||||
|
||||
// 외부 호출 기본값 설정
|
||||
setExternalCallSettings({
|
||||
callType: "rest-api",
|
||||
apiUrl: "https://api.example.com/webhook",
|
||||
httpMethod: "POST",
|
||||
headers: "{}",
|
||||
bodyTemplate: "{}",
|
||||
});
|
||||
}
|
||||
}, [isOpen, connection]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (config.relationshipName && config.fromColumnName && config.toColumnName) {
|
||||
onConfirm(config);
|
||||
const handleConfirm = async () => {
|
||||
if (!config.relationshipName || !connection) {
|
||||
toast.error("필수 정보를 모두 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 연결 종류별 설정을 준비
|
||||
let settings = {};
|
||||
|
||||
switch (config.connectionType) {
|
||||
case "simple-key":
|
||||
settings = simpleKeySettings;
|
||||
break;
|
||||
case "data-save":
|
||||
settings = dataSaveSettings;
|
||||
break;
|
||||
case "external-call":
|
||||
settings = externalCallSettings;
|
||||
break;
|
||||
}
|
||||
|
||||
// API 호출을 위한 관계 데이터 준비
|
||||
const relationshipData: Omit<TableRelationship, "relationshipId"> = {
|
||||
relationshipName: config.relationshipName,
|
||||
fromTableName: connection.fromNode.tableName,
|
||||
fromColumnName: connection.fromColumn || "",
|
||||
toTableName: connection.toNode.tableName,
|
||||
toColumnName: connection.toColumn || "",
|
||||
relationshipType: config.relationshipType,
|
||||
connectionType: config.connectionType,
|
||||
companyCode: companyCode,
|
||||
settings: settings,
|
||||
isActive: "Y",
|
||||
};
|
||||
|
||||
toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" });
|
||||
|
||||
// API 호출
|
||||
const createdRelationship = await DataFlowAPI.createRelationship(relationshipData);
|
||||
|
||||
toast.success("관계가 성공적으로 생성되었습니다!", { id: "create-relationship" });
|
||||
|
||||
// 성공 콜백 호출
|
||||
onConfirm(createdRelationship);
|
||||
handleCancel(); // 모달 닫기
|
||||
} catch (error) {
|
||||
console.error("관계 생성 오류:", error);
|
||||
toast.error("관계 생성에 실패했습니다. 다시 시도해주세요.", { id: "create-relationship" });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -113,163 +227,344 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const fromTableData = selectedColumnsData[fromTable];
|
||||
const toTableData = selectedColumnsData[toTable];
|
||||
|
||||
// 연결 종류별 설정 패널 렌더링
|
||||
const renderConnectionTypeSettings = () => {
|
||||
switch (config.connectionType) {
|
||||
case "simple-key":
|
||||
return (
|
||||
<div className="rounded-lg border border-l-4 border-l-blue-500 bg-blue-50/30 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Key className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">단순 키값 연결 설정</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="syncDirection" className="text-sm">
|
||||
동기화 방향
|
||||
</Label>
|
||||
<Select
|
||||
value={simpleKeySettings.syncDirection}
|
||||
onValueChange={(value: "unidirectional" | "bidirectional") =>
|
||||
setSimpleKeySettings({ ...simpleKeySettings, syncDirection: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="unidirectional">단방향 (소스 → 타겟)</SelectItem>
|
||||
<SelectItem value="bidirectional">양방향 (소스 ↔ 타겟)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="notes" className="text-sm">
|
||||
연결 설명
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={simpleKeySettings.notes}
|
||||
onChange={(e) => setSimpleKeySettings({ ...simpleKeySettings, notes: e.target.value })}
|
||||
placeholder="데이터 연결에 대한 설명을 입력하세요"
|
||||
rows={2}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg bg-blue-50 p-3 text-xs text-blue-700">
|
||||
<div className="mb-1 font-medium">🔄 통합 중계 테이블 사용</div>
|
||||
<div>
|
||||
모든 데이터 연결은 <code>data_relationship_bridge</code> 테이블에서 통합 관리됩니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "data-save":
|
||||
return (
|
||||
<div className="rounded-lg border border-l-4 border-l-green-500 bg-green-50/30 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Save className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium">데이터 저장 설정</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="sourceField" className="text-sm">
|
||||
소스 필드
|
||||
</Label>
|
||||
<Input
|
||||
id="sourceField"
|
||||
value={dataSaveSettings.sourceField}
|
||||
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, sourceField: e.target.value })}
|
||||
placeholder="소스 필드"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="targetField" className="text-sm">
|
||||
대상 필드
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="targetField"
|
||||
value={dataSaveSettings.targetField}
|
||||
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, targetField: e.target.value })}
|
||||
placeholder="대상 필드"
|
||||
className="text-sm"
|
||||
/>
|
||||
<Button size="sm" variant="outline">
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="saveConditions" className="text-sm">
|
||||
저장 조건
|
||||
</Label>
|
||||
<Textarea
|
||||
id="saveConditions"
|
||||
value={dataSaveSettings.saveConditions}
|
||||
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, saveConditions: e.target.value })}
|
||||
placeholder="데이터 저장 조건을 입력하세요"
|
||||
rows={2}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "external-call":
|
||||
return (
|
||||
<div className="rounded-lg border border-l-4 border-l-orange-500 bg-orange-50/30 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm font-medium">외부 호출 설정</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="callType" className="text-sm">
|
||||
호출 유형
|
||||
</Label>
|
||||
<Select
|
||||
value={externalCallSettings.callType}
|
||||
onValueChange={(value: "rest-api" | "email" | "webhook" | "ftp" | "queue") =>
|
||||
setExternalCallSettings({ ...externalCallSettings, callType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="rest-api">REST API 호출</SelectItem>
|
||||
<SelectItem value="email">이메일 전송</SelectItem>
|
||||
<SelectItem value="webhook">웹훅</SelectItem>
|
||||
<SelectItem value="ftp">FTP 업로드</SelectItem>
|
||||
<SelectItem value="queue">메시지 큐</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{externalCallSettings.callType === "rest-api" && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="apiUrl" className="text-sm">
|
||||
API URL
|
||||
</Label>
|
||||
<Input
|
||||
id="apiUrl"
|
||||
value={externalCallSettings.apiUrl}
|
||||
onChange={(e) => setExternalCallSettings({ ...externalCallSettings, apiUrl: e.target.value })}
|
||||
placeholder="https://api.example.com/webhook"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="httpMethod" className="text-sm">
|
||||
HTTP Method
|
||||
</Label>
|
||||
<Select
|
||||
value={externalCallSettings.httpMethod}
|
||||
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
|
||||
setExternalCallSettings({ ...externalCallSettings, httpMethod: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="headers" className="text-sm">
|
||||
Headers
|
||||
</Label>
|
||||
<Textarea
|
||||
id="headers"
|
||||
value={externalCallSettings.headers}
|
||||
onChange={(e) => setExternalCallSettings({ ...externalCallSettings, headers: e.target.value })}
|
||||
placeholder="{}"
|
||||
rows={1}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="bodyTemplate" className="text-sm">
|
||||
Body Template
|
||||
</Label>
|
||||
<Textarea
|
||||
id="bodyTemplate"
|
||||
value={externalCallSettings.bodyTemplate}
|
||||
onChange={(e) =>
|
||||
setExternalCallSettings({ ...externalCallSettings, bodyTemplate: e.target.value })
|
||||
}
|
||||
placeholder="{}"
|
||||
rows={2}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
||||
<DialogContent className="max-h-[90vh] max-w-4xl overflow-y-auto">
|
||||
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-xl">
|
||||
<Link className="h-5 w-5" />
|
||||
테이블 간 컬럼 연결 설정
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Link className="h-4 w-4" />
|
||||
필드 연결 설정
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* 연결 정보 표시 */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{/* 시작 테이블 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<Database className="h-4 w-4" />
|
||||
시작 테이블
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium">{fromTableData?.displayName || fromTable}</div>
|
||||
<div className="text-xs text-gray-500">{fromTable}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{fromTableData?.columns.map((column, index) => (
|
||||
<Badge key={`${fromTable}-${column}-${index}`} variant="outline" className="text-xs">
|
||||
{column}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="flex items-center justify-center md:hidden">
|
||||
<ArrowRight className="h-6 w-6 text-gray-400" />
|
||||
<div className="rounded-lg border bg-gray-50 p-3">
|
||||
<div className="mb-2 text-sm font-medium">연결 정보</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="font-medium">{fromTableData?.displayName || fromTable}</span>
|
||||
<span className="text-xs text-gray-500">({fromTable})</span>
|
||||
<ArrowRight className="h-4 w-4 text-gray-400" />
|
||||
<span className="font-medium">{toTableData?.displayName || toTable}</span>
|
||||
<span className="text-xs text-gray-500">({toTable})</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{fromTableData?.columns.map((column, index) => (
|
||||
<Badge key={`${fromTable}-${column}-${index}`} variant="outline" className="text-xs">
|
||||
{column}
|
||||
</Badge>
|
||||
))}
|
||||
{toTableData?.columns.map((column, index) => (
|
||||
<Badge key={`${toTable}-${column}-${index}`} variant="secondary" className="text-xs">
|
||||
{column}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 대상 테이블 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<Database className="h-4 w-4" />
|
||||
대상 테이블
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium">{toTableData?.displayName || toTable}</div>
|
||||
<div className="text-xs text-gray-500">{toTable}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{toTableData?.columns.map((column, index) => (
|
||||
<Badge key={`${toTable}-${column}-${index}`} variant="outline" className="text-xs">
|
||||
{column}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 연결 설정 폼 */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="relationshipName">관계명</Label>
|
||||
<Input
|
||||
id="relationshipName"
|
||||
value={config.relationshipName}
|
||||
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
|
||||
placeholder="관계명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="relationshipType">관계 유형</Label>
|
||||
<Select
|
||||
value={config.relationshipType}
|
||||
onValueChange={(value: any) => setConfig({ ...config, relationshipType: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="one-to-one">1:1 (One-to-One)</SelectItem>
|
||||
<SelectItem value="one-to-many">1:N (One-to-Many)</SelectItem>
|
||||
<SelectItem value="many-to-one">N:1 (Many-to-One)</SelectItem>
|
||||
<SelectItem value="many-to-many">N:N (Many-to-Many)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="connectionType">연결 방식</Label>
|
||||
<Select
|
||||
value={config.connectionType}
|
||||
onValueChange={(value: any) => setConfig({ ...config, connectionType: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="simple-key">단순 키 연결</SelectItem>
|
||||
<SelectItem value="data-save">데이터 저장</SelectItem>
|
||||
<SelectItem value="external-call">외부 호출</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 기본 연결 설정 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="relationshipName">연결 이름</Label>
|
||||
<Input
|
||||
id="relationshipName"
|
||||
value={config.relationshipName}
|
||||
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
|
||||
placeholder="employee_id_department_id_연결"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="relationshipType">연결 방식</Label>
|
||||
<Select
|
||||
value={config.relationshipType}
|
||||
onValueChange={(value: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many") =>
|
||||
setConfig({ ...config, relationshipType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="one-to-one">1:1 (One to One)</SelectItem>
|
||||
<SelectItem value="one-to-many">1:N (One to Many)</SelectItem>
|
||||
<SelectItem value="many-to-one">N:1 (Many to One)</SelectItem>
|
||||
<SelectItem value="many-to-many">N:N (Many to Many)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="fromColumnName">시작 컬럼</Label>
|
||||
<Input
|
||||
id="fromColumnName"
|
||||
value={config.fromColumnName}
|
||||
onChange={(e) => setConfig({ ...config, fromColumnName: e.target.value })}
|
||||
placeholder="시작 컬럼명을 입력하세요"
|
||||
/>
|
||||
{/* 연결 종류 선택 */}
|
||||
<div>
|
||||
<Label className="text-sm font-medium">연결 종류</Label>
|
||||
<div className="mt-2 grid grid-cols-3 gap-2">
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
||||
config.connectionType === "simple-key"
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setConfig({ ...config, connectionType: "simple-key" })}
|
||||
>
|
||||
<Key className="mx-auto h-6 w-6 text-blue-500" />
|
||||
<div className="mt-1 text-xs font-medium">단순 키값 연결</div>
|
||||
<div className="text-xs text-gray-600">중계 테이블 생성</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="toColumnName">대상 컬럼</Label>
|
||||
<Input
|
||||
id="toColumnName"
|
||||
value={config.toColumnName}
|
||||
onChange={(e) => setConfig({ ...config, toColumnName: e.target.value })}
|
||||
placeholder="대상 컬럼명을 입력하세요"
|
||||
/>
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
||||
config.connectionType === "data-save"
|
||||
? "border-green-500 bg-green-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setConfig({ ...config, connectionType: "data-save" })}
|
||||
>
|
||||
<Save className="mx-auto h-6 w-6 text-green-500" />
|
||||
<div className="mt-1 text-xs font-medium">데이터 저장</div>
|
||||
<div className="text-xs text-gray-600">필드 매핑 저장</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={config.description}
|
||||
onChange={(e) => setConfig({ ...config, description: e.target.value })}
|
||||
placeholder="연결에 대한 설명을 입력하세요"
|
||||
rows={3}
|
||||
/>
|
||||
<div
|
||||
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
||||
config.connectionType === "external-call"
|
||||
? "border-orange-500 bg-orange-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setConfig({ ...config, connectionType: "external-call" })}
|
||||
>
|
||||
<Globe className="mx-auto h-6 w-6 text-orange-500" />
|
||||
<div className="mt-1 text-xs font-medium">외부 호출</div>
|
||||
<div className="text-xs text-gray-600">API/이메일 호출</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 연결 종류별 상세 설정 */}
|
||||
{renderConnectionTypeSettings()}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={!config.relationshipName || !config.fromColumnName || !config.toColumnName}
|
||||
>
|
||||
<Button onClick={handleConfirm} disabled={!config.relationshipName}>
|
||||
연결 생성
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,14 @@ import "@xyflow/react/dist/style.css";
|
|||
import { TableNode } from "./TableNode";
|
||||
import { TableSelector } from "./TableSelector";
|
||||
import { ConnectionSetupModal } from "./ConnectionSetupModal";
|
||||
import { TableDefinition } from "@/lib/api/dataflow";
|
||||
import { TableDefinition, TableRelationship, DataFlowAPI } from "@/lib/api/dataflow";
|
||||
|
||||
// 고유 ID 생성 함수
|
||||
const generateUniqueId = (prefix: string, relationshipId?: number): string => {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substr(2, 9);
|
||||
return `${prefix}-${relationshipId || timestamp}-${random}`;
|
||||
};
|
||||
|
||||
// 테이블 노드 데이터 타입 정의
|
||||
interface TableNodeData extends Record<string, unknown> {
|
||||
|
|
@ -47,19 +54,7 @@ interface DataFlowDesignerProps {
|
|||
onSave?: (relationships: TableRelationship[]) => void;
|
||||
}
|
||||
|
||||
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";
|
||||
settings?: Record<string, unknown>;
|
||||
companyCode: string;
|
||||
isActive?: string;
|
||||
}
|
||||
// TableRelationship 타입은 dataflow.ts에서 import
|
||||
|
||||
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode, onSave }) => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
|
||||
|
|
@ -81,6 +76,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
};
|
||||
};
|
||||
} | null>(null);
|
||||
const [relationships, setRelationships] = useState<TableRelationship[]>([]);
|
||||
const toastShownRef = useRef(false);
|
||||
|
||||
// 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제)
|
||||
|
|
@ -117,6 +113,44 @@ 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);
|
||||
|
|
@ -229,6 +263,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
|
||||
if (!firstNode || !secondNode) return;
|
||||
|
||||
// 첫 번째로 선택된 컬럼들 가져오기
|
||||
const firstTableColumns = selectedColumns[firstTableName] || [];
|
||||
const secondTableColumns = selectedColumns[secondTableName] || [];
|
||||
|
||||
setPendingConnection({
|
||||
fromNode: {
|
||||
id: firstNode.id,
|
||||
|
|
@ -240,6 +278,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
tableName: secondNode.data.table.tableName,
|
||||
displayName: secondNode.data.table.displayName,
|
||||
},
|
||||
// 선택된 첫 번째 컬럼을 연결 컬럼으로 설정
|
||||
fromColumn: firstTableColumns[0] || "",
|
||||
toColumn: secondTableColumns[0] || "",
|
||||
// 선택된 모든 컬럼 정보를 선택 순서대로 전달
|
||||
selectedColumnsData: (() => {
|
||||
const orderedData: { [key: string]: { displayName: string; columns: string[] } } = {};
|
||||
|
|
@ -335,28 +376,41 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
|
||||
// 연결 설정 확인
|
||||
const handleConfirmConnection = useCallback(
|
||||
(config: { relationshipType: string; connectionType: string; relationshipName: string }) => {
|
||||
(relationship: TableRelationship) => {
|
||||
if (!pendingConnection) return;
|
||||
|
||||
const newEdge = {
|
||||
id: `edge-${Date.now()}`,
|
||||
id: generateUniqueId("edge", relationship.relationshipId),
|
||||
source: pendingConnection.fromNode.id,
|
||||
target: pendingConnection.toNode.id,
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
type: "default",
|
||||
data: {
|
||||
relationshipType: config.relationshipType,
|
||||
connectionType: config.connectionType,
|
||||
label: config.relationshipName,
|
||||
relationshipId: relationship.relationshipId,
|
||||
relationshipType: relationship.relationshipType,
|
||||
connectionType: relationship.connectionType,
|
||||
label: relationship.relationshipName,
|
||||
fromColumn: relationship.fromColumnName,
|
||||
toColumn: relationship.toColumnName,
|
||||
},
|
||||
};
|
||||
|
||||
setEdges((eds) => [...eds, newEdge]);
|
||||
setRelationships((prev) => [...prev, relationship]);
|
||||
setPendingConnection(null);
|
||||
|
||||
// TODO: 백엔드 API 호출하여 관계 저장
|
||||
console.log("연결 설정:", config);
|
||||
console.log("관계 생성 완료:", relationship);
|
||||
// 저장 콜백 호출 (필요한 경우)
|
||||
if (onSave) {
|
||||
// 현재 모든 관계를 수집하여 전달
|
||||
setRelationships((currentRelationships) => {
|
||||
onSave([...currentRelationships, relationship]);
|
||||
return currentRelationships;
|
||||
});
|
||||
}
|
||||
},
|
||||
[pendingConnection, setEdges],
|
||||
[pendingConnection, setEdges, onSave],
|
||||
);
|
||||
|
||||
// 연결 설정 취소
|
||||
|
|
@ -536,6 +590,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
|||
<ConnectionSetupModal
|
||||
isOpen={!!pendingConnection}
|
||||
connection={pendingConnection}
|
||||
companyCode={companyCode}
|
||||
onConfirm={handleConfirmConnection}
|
||||
onCancel={handleCancelConnection}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
|
||||
interface TableColumn {
|
||||
name: string;
|
||||
|
|
@ -28,6 +29,10 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
|
||||
return (
|
||||
<div className="relative flex min-w-[280px] flex-col overflow-hidden rounded-lg border-2 border-gray-300 bg-white shadow-lg">
|
||||
{/* React Flow Handles - 숨김 처리 */}
|
||||
<Handle type="target" position={Position.Left} id="left" className="!invisible !h-1 !w-1" />
|
||||
<Handle type="source" position={Position.Right} id="right" className="!invisible !h-1 !w-1" />
|
||||
|
||||
{/* 테이블 헤더 */}
|
||||
<div className="bg-blue-600 p-3 text-white">
|
||||
<h3 className="truncate text-sm font-semibold">{table.displayName}</h3>
|
||||
|
|
|
|||
|
|
@ -46,11 +46,38 @@ export interface TableRelationship {
|
|||
toColumnName: string;
|
||||
relationshipType: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
||||
connectionType: "simple-key" | "data-save" | "external-call";
|
||||
settings?: Record<string, any>;
|
||||
settings?: Record<string, unknown>;
|
||||
companyCode: string;
|
||||
isActive?: string;
|
||||
}
|
||||
|
||||
// 데이터 연결 중계 테이블 타입
|
||||
export interface DataBridge {
|
||||
bridgeId: number;
|
||||
relationshipId: number;
|
||||
fromTableName: string;
|
||||
fromColumnName: string;
|
||||
fromKeyValue: string;
|
||||
fromRecordId?: string;
|
||||
toTableName: string;
|
||||
toColumnName: string;
|
||||
toKeyValue: string;
|
||||
toRecordId?: string;
|
||||
connectionType: string;
|
||||
companyCode: string;
|
||||
createdAt: string;
|
||||
createdBy?: string;
|
||||
updatedAt: string;
|
||||
updatedBy?: string;
|
||||
isActive: string;
|
||||
bridgeData?: Record<string, unknown>;
|
||||
relationship?: {
|
||||
relationshipName: string;
|
||||
relationshipType: string;
|
||||
connectionType: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 테이블 간 데이터 관계 설정 API 클래스
|
||||
export class DataFlowAPI {
|
||||
/**
|
||||
|
|
@ -113,7 +140,10 @@ export class DataFlowAPI {
|
|||
*/
|
||||
static async createRelationship(relationship: Omit<TableRelationship, "relationshipId">): Promise<TableRelationship> {
|
||||
try {
|
||||
const response = await apiClient.post<ApiResponse<TableRelationship>>("/table-relationships", relationship);
|
||||
const response = await apiClient.post<ApiResponse<TableRelationship>>(
|
||||
"/dataflow/table-relationships",
|
||||
relationship,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계 생성에 실패했습니다.");
|
||||
|
|
@ -131,7 +161,7 @@ export class DataFlowAPI {
|
|||
*/
|
||||
static async getRelationshipsByCompany(companyCode: string): Promise<TableRelationship[]> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<TableRelationship[]>>("/table-relationships", {
|
||||
const response = await apiClient.get<ApiResponse<TableRelationship[]>>("/dataflow/table-relationships", {
|
||||
params: { companyCode },
|
||||
});
|
||||
|
||||
|
|
@ -155,7 +185,7 @@ export class DataFlowAPI {
|
|||
): Promise<TableRelationship> {
|
||||
try {
|
||||
const response = await apiClient.put<ApiResponse<TableRelationship>>(
|
||||
`/table-relationships/${relationshipId}`,
|
||||
`/dataflow/table-relationships/${relationshipId}`,
|
||||
relationship,
|
||||
);
|
||||
|
||||
|
|
@ -175,7 +205,7 @@ export class DataFlowAPI {
|
|||
*/
|
||||
static async deleteRelationship(relationshipId: number): Promise<void> {
|
||||
try {
|
||||
const response = await apiClient.delete<ApiResponse<null>>(`/table-relationships/${relationshipId}`);
|
||||
const response = await apiClient.delete<ApiResponse<null>>(`/dataflow/table-relationships/${relationshipId}`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "관계 삭제에 실패했습니다.");
|
||||
|
|
@ -185,4 +215,72 @@ export class DataFlowAPI {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 데이터 연결 관리 API ====================
|
||||
|
||||
/**
|
||||
* 데이터 연결 생성
|
||||
*/
|
||||
static async createDataLink(linkData: {
|
||||
relationshipId: number;
|
||||
fromTableName: string;
|
||||
fromColumnName: string;
|
||||
fromKeyValue: string;
|
||||
fromRecordId?: string;
|
||||
toTableName: string;
|
||||
toColumnName: string;
|
||||
toKeyValue: string;
|
||||
toRecordId?: string;
|
||||
connectionType: string;
|
||||
bridgeData?: Record<string, unknown>;
|
||||
}): Promise<DataBridge> {
|
||||
try {
|
||||
const response = await apiClient.post<ApiResponse<DataBridge>>("/dataflow/data-links", linkData);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "데이터 연결 생성에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as DataBridge;
|
||||
} catch (error) {
|
||||
console.error("데이터 연결 생성 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계별 연결된 데이터 조회
|
||||
*/
|
||||
static async getLinkedDataByRelationship(relationshipId: number): Promise<DataBridge[]> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiResponse<DataBridge[]>>(
|
||||
`/dataflow/data-links/relationship/${relationshipId}`,
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "연결된 데이터 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data as DataBridge[];
|
||||
} catch (error) {
|
||||
console.error("연결된 데이터 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 연결 삭제
|
||||
*/
|
||||
static async deleteDataLink(bridgeId: number): Promise<void> {
|
||||
try {
|
||||
const response = await apiClient.delete<ApiResponse<null>>(`/dataflow/data-links/${bridgeId}`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "데이터 연결 삭제에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("데이터 연결 삭제 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue