restapi 버튼 동작
This commit is contained in:
parent
cedb5e3ec3
commit
c9afdec09f
|
|
@ -38,6 +38,7 @@ import ddlRoutes from "./routes/ddlRoutes";
|
|||
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
||||
import externalCallRoutes from "./routes/externalCallRoutes";
|
||||
import externalCallConfigRoutes from "./routes/externalCallConfigRoutes";
|
||||
import dataflowExecutionRoutes from "./routes/dataflowExecutionRoutes";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
|
|
@ -148,6 +149,7 @@ app.use("/api/ddl", ddlRoutes);
|
|||
app.use("/api/entity-reference", entityReferenceRoutes);
|
||||
app.use("/api/external-calls", externalCallRoutes);
|
||||
app.use("/api/external-call-configs", externalCallConfigRoutes);
|
||||
app.use("/api/dataflow", dataflowExecutionRoutes);
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
|
|
|
|||
|
|
@ -727,3 +727,35 @@ function processDataflowInBackground(
|
|||
}
|
||||
}, 1000); // 1초 후 실행 시뮬레이션
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 전체 관계 목록 조회 (버튼 제어용)
|
||||
*/
|
||||
export async function getAllRelationships(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
logger.info(`전체 관계 목록 조회 요청 - companyCode: ${companyCode}`);
|
||||
|
||||
// 모든 관계도에서 관계 목록을 가져옴
|
||||
const allRelationships = await dataflowDiagramService.getAllRelationshipsForButtonControl(companyCode);
|
||||
|
||||
logger.info(`전체 관계 ${allRelationships.length}개 조회 완료`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: allRelationships,
|
||||
message: `전체 관계 ${allRelationships.length}개 조회 완료`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("전체 관계 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "전체 관계 목록 조회 실패",
|
||||
errorCode: "GET_ALL_RELATIONSHIPS_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* 🔥 데이터플로우 실행 컨트롤러
|
||||
*
|
||||
* 버튼 제어에서 관계 실행 시 사용되는 컨트롤러
|
||||
*/
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* 데이터 액션 실행
|
||||
*/
|
||||
export async function executeDataAction(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName, data, actionType, connection } = req.body;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
logger.info(`데이터 액션 실행 시작: ${actionType} on ${tableName}`, {
|
||||
tableName,
|
||||
actionType,
|
||||
dataKeys: Object.keys(data),
|
||||
connection: connection?.name,
|
||||
});
|
||||
|
||||
// 연결 정보에 따라 다른 데이터베이스에 저장
|
||||
let result;
|
||||
|
||||
if (connection && connection.id !== 0) {
|
||||
// 외부 데이터베이스 연결
|
||||
result = await executeExternalDatabaseAction(tableName, data, actionType, connection);
|
||||
} else {
|
||||
// 메인 데이터베이스 (현재 시스템)
|
||||
result = await executeMainDatabaseAction(tableName, data, actionType, companyCode);
|
||||
}
|
||||
|
||||
logger.info(`데이터 액션 실행 완료: ${actionType} on ${tableName}`, result);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `데이터 액션 실행 완료: ${actionType}`,
|
||||
data: result,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error("데이터 액션 실행 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: `데이터 액션 실행 실패: ${error.message}`,
|
||||
errorCode: "DATA_ACTION_EXECUTION_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 데이터베이스에서 데이터 액션 실행
|
||||
*/
|
||||
async function executeMainDatabaseAction(
|
||||
tableName: string,
|
||||
data: Record<string, any>,
|
||||
actionType: string,
|
||||
companyCode: string
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 회사 코드 추가
|
||||
const dataWithCompany = {
|
||||
...data,
|
||||
company_code: companyCode,
|
||||
};
|
||||
|
||||
switch (actionType.toLowerCase()) {
|
||||
case 'insert':
|
||||
return await executeInsert(tableName, dataWithCompany);
|
||||
case 'update':
|
||||
return await executeUpdate(tableName, dataWithCompany);
|
||||
case 'upsert':
|
||||
return await executeUpsert(tableName, dataWithCompany);
|
||||
case 'delete':
|
||||
return await executeDelete(tableName, dataWithCompany);
|
||||
default:
|
||||
throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`메인 DB 액션 실행 오류 (${actionType}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 데이터베이스에서 데이터 액션 실행
|
||||
*/
|
||||
async function executeExternalDatabaseAction(
|
||||
tableName: string,
|
||||
data: Record<string, any>,
|
||||
actionType: string,
|
||||
connection: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
// TODO: 외부 데이터베이스 연결 및 실행 로직 구현
|
||||
// 현재는 로그만 출력하고 성공으로 처리
|
||||
logger.info(`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`);
|
||||
logger.info(`테이블: ${tableName}, 액션: ${actionType}`, data);
|
||||
|
||||
// 임시 성공 응답
|
||||
return {
|
||||
success: true,
|
||||
message: `외부 DB 액션 실행 완료: ${actionType} on ${tableName}`,
|
||||
connection: connection.name,
|
||||
affectedRows: 1,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`외부 DB 액션 실행 오류 (${actionType}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT 실행
|
||||
*/
|
||||
async function executeInsert(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
try {
|
||||
// 동적 테이블 접근을 위한 raw query 사용
|
||||
const columns = Object.keys(data).join(', ');
|
||||
const values = Object.values(data);
|
||||
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
|
||||
|
||||
const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
|
||||
|
||||
logger.info(`INSERT 쿼리 실행:`, { query, values });
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(query, ...values);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'insert',
|
||||
tableName,
|
||||
data: result,
|
||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`INSERT 실행 오류:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATE 실행
|
||||
*/
|
||||
async function executeUpdate(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
try {
|
||||
// ID 또는 기본키를 기준으로 업데이트
|
||||
const { id, ...updateData } = data;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('UPDATE를 위한 ID가 필요합니다');
|
||||
}
|
||||
|
||||
const setClause = Object.keys(updateData)
|
||||
.map((key, index) => `${key} = $${index + 1}`)
|
||||
.join(', ');
|
||||
|
||||
const values = Object.values(updateData);
|
||||
const query = `UPDATE ${tableName} SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`;
|
||||
|
||||
logger.info(`UPDATE 쿼리 실행:`, { query, values: [...values, id] });
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(query, ...values, id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'update',
|
||||
tableName,
|
||||
data: result,
|
||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`UPDATE 실행 오류:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UPSERT 실행
|
||||
*/
|
||||
async function executeUpsert(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
try {
|
||||
// 먼저 INSERT를 시도하고, 실패하면 UPDATE
|
||||
try {
|
||||
return await executeInsert(tableName, data);
|
||||
} catch (insertError) {
|
||||
// INSERT 실패 시 UPDATE 시도
|
||||
logger.info(`INSERT 실패, UPDATE 시도:`, insertError);
|
||||
return await executeUpdate(tableName, data);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`UPSERT 실행 오류:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 실행
|
||||
*/
|
||||
async function executeDelete(tableName: string, data: Record<string, any>): Promise<any> {
|
||||
try {
|
||||
const { id } = data;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('DELETE를 위한 ID가 필요합니다');
|
||||
}
|
||||
|
||||
const query = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`;
|
||||
|
||||
logger.info(`DELETE 쿼리 실행:`, { query, values: [id] });
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(query, id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'delete',
|
||||
tableName,
|
||||
data: result,
|
||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`DELETE 실행 오류:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 🔥 데이터플로우 실행 라우트
|
||||
*
|
||||
* 버튼 제어에서 관계 실행 시 사용되는 API 엔드포인트
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { executeDataAction } from "../controllers/dataflowExecutionController";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 🔥 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 데이터 액션 실행
|
||||
router.post("/execute-data-action", executeDataAction);
|
||||
|
||||
export default router;
|
||||
|
|
@ -249,4 +249,80 @@ router.post("/:id/test", async (req: Request, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 🔥 개선된 외부호출 실행 (데이터 매핑 통합)
|
||||
* POST /api/external-call-configs/:id/execute
|
||||
*/
|
||||
router.post("/:id/execute", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 설정 ID입니다.",
|
||||
errorCode: "INVALID_CONFIG_ID",
|
||||
});
|
||||
}
|
||||
|
||||
const { requestData, contextData } = req.body;
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const userInfo = (req as any).user;
|
||||
const userId = userInfo?.userId || "SYSTEM";
|
||||
const companyCode = userInfo?.companyCode || "*";
|
||||
|
||||
const executionResult = await externalCallConfigService.executeConfigWithDataMapping(
|
||||
id,
|
||||
requestData || {},
|
||||
{
|
||||
...contextData,
|
||||
userId,
|
||||
companyCode,
|
||||
executedAt: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: executionResult.success,
|
||||
message: executionResult.message,
|
||||
data: executionResult.data,
|
||||
executionTime: executionResult.executionTime,
|
||||
error: executionResult.error,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부호출 실행 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "외부호출 실행 실패",
|
||||
errorCode: "EXTERNAL_CALL_EXECUTE_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 🔥 버튼 제어용 외부호출 목록 조회 (간소화된 정보)
|
||||
* GET /api/external-call-configs/for-button-control
|
||||
*/
|
||||
router.get("/for-button-control", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userInfo = (req as any).user;
|
||||
const companyCode = userInfo?.companyCode || "*";
|
||||
|
||||
const configs = await externalCallConfigService.getConfigsForButtonControl(companyCode);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: configs,
|
||||
message: `버튼 제어용 외부호출 설정 ${configs.length}개 조회 완료`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("버튼 제어용 외부호출 설정 조회 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "외부호출 설정 조회 실패",
|
||||
errorCode: "EXTERNAL_CALL_BUTTON_CONTROL_LIST_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
executeOptimizedButton,
|
||||
executeSimpleDataflow,
|
||||
getJobStatus,
|
||||
getAllRelationships,
|
||||
} from "../controllers/buttonDataflowController";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import config from "../config/environment";
|
||||
|
|
@ -52,6 +53,9 @@ if (config.nodeEnv !== "production") {
|
|||
// 특정 관계도의 관계 목록 조회
|
||||
router.get("/diagrams/:diagramId/relationships", getDiagramRelationships);
|
||||
|
||||
// 🔥 전체 관계 목록 조회 (버튼 제어용)
|
||||
router.get("/relationships/all", getAllRelationships);
|
||||
|
||||
// 관계 미리보기 정보 조회
|
||||
router.get(
|
||||
"/diagrams/:diagramId/relationships/:relationshipId/preview",
|
||||
|
|
|
|||
|
|
@ -384,3 +384,66 @@ export const copyDataflowDiagram = async (
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔥 전체 관계 목록 조회 (버튼 제어용)
|
||||
* dataflow_diagrams 테이블에서 관계도 데이터를 조회 (데이터 흐름 관계 화면과 동일)
|
||||
*/
|
||||
export const getAllRelationshipsForButtonControl = async (
|
||||
companyCode: string
|
||||
): Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
sourceTable: string;
|
||||
targetTable: string;
|
||||
category: string;
|
||||
}>> => {
|
||||
try {
|
||||
logger.info(`전체 관계 목록 조회 시작 - companyCode: ${companyCode}`);
|
||||
|
||||
// dataflow_diagrams 테이블에서 관계도들을 조회
|
||||
const diagrams = await prisma.dataflow_diagrams.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
},
|
||||
select: {
|
||||
diagram_id: true,
|
||||
diagram_name: true,
|
||||
relationships: true,
|
||||
},
|
||||
orderBy: {
|
||||
updated_at: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const allRelationships = diagrams.map((diagram) => {
|
||||
// relationships 구조에서 테이블 정보 추출
|
||||
const relationships = diagram.relationships as any || {};
|
||||
|
||||
// 테이블 정보 추출
|
||||
let sourceTable = "";
|
||||
let targetTable = "";
|
||||
|
||||
if (relationships.fromTable?.tableName) {
|
||||
sourceTable = relationships.fromTable.tableName;
|
||||
}
|
||||
if (relationships.toTable?.tableName) {
|
||||
targetTable = relationships.toTable.tableName;
|
||||
}
|
||||
|
||||
return {
|
||||
id: diagram.diagram_id.toString(),
|
||||
name: diagram.diagram_name || `관계 ${diagram.diagram_id}`,
|
||||
sourceTable: sourceTable,
|
||||
targetTable: targetTable,
|
||||
category: "데이터 흐름",
|
||||
};
|
||||
});
|
||||
|
||||
logger.info(`전체 관계 ${allRelationships.length}개 조회 완료`);
|
||||
return allRelationships;
|
||||
} catch (error) {
|
||||
logger.error("전체 관계 목록 조회 서비스 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -308,6 +308,265 @@ export class ExternalCallConfigService {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 데이터 매핑과 함께 외부호출 실행
|
||||
*/
|
||||
async executeConfigWithDataMapping(
|
||||
configId: number,
|
||||
requestData: Record<string, any>,
|
||||
contextData: Record<string, any>
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: any;
|
||||
executionTime: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
logger.info(`=== 외부호출 실행 시작 (ID: ${configId}) ===`);
|
||||
|
||||
// 1. 설정 조회
|
||||
const config = await this.getConfigById(configId);
|
||||
if (!config) {
|
||||
throw new Error(`외부호출 설정을 찾을 수 없습니다: ${configId}`);
|
||||
}
|
||||
|
||||
// 2. 데이터 매핑 처리 (있는 경우)
|
||||
let processedData = requestData;
|
||||
const configData = config.config_data as any;
|
||||
if (configData?.dataMappingConfig?.outboundMapping) {
|
||||
logger.info("Outbound 데이터 매핑 처리 중...");
|
||||
processedData = await this.processOutboundMapping(
|
||||
configData.dataMappingConfig.outboundMapping,
|
||||
requestData
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 외부 API 호출
|
||||
const callResult = await this.executeExternalCall(config, processedData, contextData);
|
||||
|
||||
// 4. Inbound 데이터 매핑 처리 (있는 경우)
|
||||
if (
|
||||
callResult.success &&
|
||||
configData?.dataMappingConfig?.inboundMapping
|
||||
) {
|
||||
logger.info("Inbound 데이터 매핑 처리 중...");
|
||||
await this.processInboundMapping(
|
||||
configData.dataMappingConfig.inboundMapping,
|
||||
callResult.data
|
||||
);
|
||||
}
|
||||
|
||||
const executionTime = performance.now() - startTime;
|
||||
logger.info(`외부호출 실행 완료: ${executionTime.toFixed(2)}ms`);
|
||||
|
||||
return {
|
||||
success: callResult.success,
|
||||
message: callResult.success
|
||||
? `외부호출 '${config.config_name}' 실행 완료`
|
||||
: `외부호출 '${config.config_name}' 실행 실패`,
|
||||
data: callResult.data,
|
||||
executionTime,
|
||||
error: callResult.error,
|
||||
};
|
||||
} catch (error) {
|
||||
const executionTime = performance.now() - startTime;
|
||||
logger.error("외부호출 실행 실패:", error);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `외부호출 실행 실패: ${errorMessage}`,
|
||||
executionTime,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 버튼 제어용 외부호출 설정 목록 조회 (간소화된 정보)
|
||||
*/
|
||||
async getConfigsForButtonControl(companyCode: string): Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
apiUrl: string;
|
||||
method: string;
|
||||
hasDataMapping: boolean;
|
||||
}>> {
|
||||
try {
|
||||
const configs = await prisma.external_call_configs.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
config_name: true,
|
||||
description: true,
|
||||
config_data: true,
|
||||
},
|
||||
orderBy: {
|
||||
config_name: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return configs.map((config) => {
|
||||
const configData = config.config_data as any;
|
||||
return {
|
||||
id: config.id.toString(),
|
||||
name: config.config_name,
|
||||
description: config.description || undefined,
|
||||
apiUrl: configData?.restApiSettings?.apiUrl || "",
|
||||
method: configData?.restApiSettings?.httpMethod || "GET",
|
||||
hasDataMapping: !!(configData?.dataMappingConfig),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("버튼 제어용 외부호출 설정 조회 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 실제 외부 API 호출 실행
|
||||
*/
|
||||
private async executeExternalCall(
|
||||
config: ExternalCallConfig,
|
||||
requestData: Record<string, any>,
|
||||
contextData: Record<string, any>
|
||||
): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||
try {
|
||||
const configData = config.config_data as any;
|
||||
const restApiSettings = configData?.restApiSettings;
|
||||
if (!restApiSettings) {
|
||||
throw new Error("REST API 설정이 없습니다.");
|
||||
}
|
||||
|
||||
const { apiUrl, httpMethod, headers = {}, timeout = 30000 } = restApiSettings;
|
||||
|
||||
// 요청 헤더 준비
|
||||
const requestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
};
|
||||
|
||||
// 인증 처리
|
||||
if (restApiSettings.authentication?.type === "basic") {
|
||||
const { username, password } = restApiSettings.authentication;
|
||||
const credentials = Buffer.from(`${username}:${password}`).toString("base64");
|
||||
requestHeaders["Authorization"] = `Basic ${credentials}`;
|
||||
} else if (restApiSettings.authentication?.type === "bearer") {
|
||||
const { token } = restApiSettings.authentication;
|
||||
requestHeaders["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 요청 본문 준비
|
||||
let requestBody = undefined;
|
||||
if (["POST", "PUT", "PATCH"].includes(httpMethod.toUpperCase())) {
|
||||
requestBody = JSON.stringify({
|
||||
...requestData,
|
||||
_context: contextData, // 컨텍스트 정보 추가
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`외부 API 호출: ${httpMethod} ${apiUrl}`);
|
||||
|
||||
// 실제 HTTP 요청 (여기서는 간단한 예시)
|
||||
// 실제 구현에서는 axios나 fetch를 사용
|
||||
const response = await fetch(apiUrl, {
|
||||
method: httpMethod,
|
||||
headers: requestHeaders,
|
||||
body: requestBody,
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: responseData,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("외부 API 호출 실패:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Outbound 데이터 매핑 처리
|
||||
*/
|
||||
private async processOutboundMapping(
|
||||
mapping: any,
|
||||
sourceData: Record<string, any>
|
||||
): Promise<Record<string, any>> {
|
||||
try {
|
||||
// 간단한 매핑 로직 (실제로는 더 복잡한 변환 로직 필요)
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
if (mapping.fieldMappings) {
|
||||
for (const fieldMapping of mapping.fieldMappings) {
|
||||
const { sourceField, targetField, transformation } = fieldMapping;
|
||||
|
||||
let value = sourceData[sourceField];
|
||||
|
||||
// 변환 로직 적용
|
||||
if (transformation) {
|
||||
switch (transformation.type) {
|
||||
case "format":
|
||||
// 포맷 변환 로직
|
||||
break;
|
||||
case "calculate":
|
||||
// 계산 로직
|
||||
break;
|
||||
default:
|
||||
// 기본값 그대로 사용
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mappedData[targetField] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return mappedData;
|
||||
} catch (error) {
|
||||
logger.error("Outbound 데이터 매핑 처리 실패:", error);
|
||||
return sourceData; // 실패 시 원본 데이터 반환
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Inbound 데이터 매핑 처리
|
||||
*/
|
||||
private async processInboundMapping(
|
||||
mapping: any,
|
||||
responseData: any
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Inbound 매핑 로직 (응답 데이터를 내부 시스템에 저장)
|
||||
logger.info("Inbound 데이터 매핑 처리:", mapping);
|
||||
|
||||
// 실제 구현에서는 응답 데이터를 파싱하여 내부 테이블에 저장하는 로직 필요
|
||||
// 예: 외부 API에서 받은 사용자 정보를 내부 사용자 테이블에 업데이트
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Inbound 데이터 매핑 처리 실패:", error);
|
||||
// Inbound 매핑 실패는 전체 플로우를 중단하지 않음
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ExternalCallConfigService();
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export default function DataFlowPage() {
|
|||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">데이터 흐름 관리</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">관계 관리</h1>
|
||||
<p className="mt-2 text-gray-600">테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
return;
|
||||
}, []);
|
||||
|
||||
// 편집 모드일 때 관계도 데이터 로드
|
||||
// 편집 모드일 때 관계 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadDiagramData = async () => {
|
||||
if (diagramId && diagramId > 0) {
|
||||
|
|
@ -99,7 +99,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode);
|
||||
if (jsonDiagram) {
|
||||
// 관계도 이름 설정
|
||||
// 관계 이름 설정
|
||||
if (jsonDiagram.diagram_name) {
|
||||
setCurrentDiagramName(jsonDiagram.diagram_name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,14 +96,14 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
setTotal(response.pagination.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.pagination.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계도 목록 조회 실패", error);
|
||||
toast.error("관계도 목록을 불러오는데 실패했습니다.");
|
||||
console.error("관계 목록 조회 실패", error);
|
||||
toast.error("관계 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, searchTerm, companyCode]);
|
||||
|
||||
// 관계도 목록 로드
|
||||
// 관계 목록 로드
|
||||
useEffect(() => {
|
||||
loadDiagrams();
|
||||
}, [loadDiagrams]);
|
||||
|
|
@ -130,13 +130,13 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
undefined,
|
||||
user?.userId || "SYSTEM",
|
||||
);
|
||||
toast.success(`관계도가 성공적으로 복사되었습니다: ${copiedDiagram.diagram_name}`);
|
||||
toast.success(`관계가 성공적으로 복사되었습니다: ${copiedDiagram.diagram_name}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
} catch (error) {
|
||||
console.error("관계도 복사 실패:", error);
|
||||
toast.error("관계도 복사에 실패했습니다.");
|
||||
console.error("관계 복사 실패:", error);
|
||||
toast.error("관계 복사에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowCopyModal(false);
|
||||
|
|
@ -151,13 +151,13 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
try {
|
||||
setLoading(true);
|
||||
await DataFlowAPI.deleteJsonDataFlowDiagram(selectedDiagramForAction.diagramId, companyCode);
|
||||
toast.success(`관계도가 삭제되었습니다: ${selectedDiagramForAction.diagramName}`);
|
||||
toast.success(`관계가 삭제되었습니다: ${selectedDiagramForAction.diagramName}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
} catch (error) {
|
||||
console.error("관계도 삭제 실패:", error);
|
||||
toast.error("관계도 삭제에 실패했습니다.");
|
||||
console.error("관계 삭제 실패:", error);
|
||||
toast.error("관계 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowDeleteModal(false);
|
||||
|
|
@ -181,7 +181,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<div className="relative">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
placeholder="관계도명, 테이블명으로 검색..."
|
||||
placeholder="관계명, 테이블명으로 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-80 pl-10"
|
||||
|
|
@ -189,17 +189,17 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
</div>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => onDesignDiagram(null)}>
|
||||
<Plus className="mr-2 h-4 w-4" />새 관계도 생성
|
||||
<Plus className="mr-2 h-4 w-4" />새 관계 생성
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 관계도 목록 테이블 */}
|
||||
{/* 관계 목록 테이블 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center">
|
||||
<Network className="mr-2 h-5 w-5" />
|
||||
데이터 흐름 관계도 ({total})
|
||||
데이터 흐름 관계 ({total})
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -207,7 +207,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>관계도명</TableHead>
|
||||
<TableHead>관계명</TableHead>
|
||||
<TableHead>회사 코드</TableHead>
|
||||
<TableHead>테이블 수</TableHead>
|
||||
<TableHead>관계 수</TableHead>
|
||||
|
|
@ -284,8 +284,8 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
{diagrams.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
<Network className="mx-auto mb-4 h-12 w-12 text-gray-300" />
|
||||
<div className="mb-2 text-lg font-medium">관계도가 없습니다</div>
|
||||
<div className="text-sm">새 관계도를 생성하여 테이블 간 데이터 관계를 설정해보세요.</div>
|
||||
<div className="mb-2 text-lg font-medium">관계가 없습니다</div>
|
||||
<div className="text-sm">새 관계를 생성하여 테이블 간 데이터 관계를 설정해보세요.</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
@ -320,11 +320,11 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Dialog open={showCopyModal} onOpenChange={setShowCopyModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>관계도 복사</DialogTitle>
|
||||
<DialogTitle>관계 복사</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계도를 복사하시겠습니까?
|
||||
“{selectedDiagramForAction?.diagramName}” 관계를 복사하시겠습니까?
|
||||
<br />
|
||||
새로운 관계도는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다.
|
||||
새로운 관계는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
|
@ -342,9 +342,9 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-600">관계도 삭제</DialogTitle>
|
||||
<DialogTitle className="text-red-600">관계 삭제</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계도를 완전히 삭제하시겠습니까?
|
||||
“{selectedDiagramForAction?.diagramName}” 관계를 완전히 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다.
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
hasUnsavedChanges ? "animate-pulse" : ""
|
||||
}`}
|
||||
>
|
||||
💾 관계도 저장 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
|
||||
💾 관계 저장 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -622,7 +622,57 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
|
|||
company_code: "*", // 기본값
|
||||
};
|
||||
|
||||
const configResult = await ExternalCallConfigAPI.createConfig(configData);
|
||||
let configResult;
|
||||
|
||||
if (diagramId) {
|
||||
// 수정 모드: 기존 설정이 있는지 확인하고 업데이트 또는 생성
|
||||
console.log("🔄 수정 모드 - 외부호출 설정 처리");
|
||||
|
||||
try {
|
||||
// 먼저 기존 설정 조회 시도
|
||||
const existingConfigs = await ExternalCallConfigAPI.getConfigs({
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
|
||||
const existingConfig = existingConfigs.data?.find(
|
||||
(config: any) => config.config_name === (state.relationshipName || "외부호출 설정")
|
||||
);
|
||||
|
||||
if (existingConfig) {
|
||||
// 기존 설정 업데이트
|
||||
console.log("📝 기존 외부호출 설정 업데이트:", existingConfig.id);
|
||||
configResult = await ExternalCallConfigAPI.updateConfig(existingConfig.id, configData);
|
||||
} else {
|
||||
// 기존 설정이 없으면 새로 생성
|
||||
console.log("🆕 새 외부호출 설정 생성 (수정 모드)");
|
||||
configResult = await ExternalCallConfigAPI.createConfig(configData);
|
||||
}
|
||||
} catch (updateError) {
|
||||
// 중복 생성 오류인 경우 무시하고 계속 진행
|
||||
if (updateError.message && updateError.message.includes("이미 존재합니다")) {
|
||||
console.log("⚠️ 외부호출 설정이 이미 존재함 - 기존 설정 사용");
|
||||
configResult = { success: true, message: "기존 외부호출 설정 사용" };
|
||||
} else {
|
||||
console.warn("⚠️ 외부호출 설정 처리 실패:", updateError);
|
||||
throw updateError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 신규 생성 모드
|
||||
console.log("🆕 신규 생성 모드 - 외부호출 설정 생성");
|
||||
try {
|
||||
configResult = await ExternalCallConfigAPI.createConfig(configData);
|
||||
} catch (createError) {
|
||||
// 중복 생성 오류인 경우 무시하고 계속 진행
|
||||
if (createError.message && createError.message.includes("이미 존재합니다")) {
|
||||
console.log("⚠️ 외부호출 설정이 이미 존재함 - 기존 설정 사용");
|
||||
configResult = { success: true, message: "기존 외부호출 설정 사용" };
|
||||
} else {
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!configResult.success) {
|
||||
throw new Error(configResult.error || "외부호출 설정 저장 실패");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
|||
import { ComponentData } from "@/types/screen";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
||||
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
||||
|
||||
interface ButtonConfigPanelProps {
|
||||
component: ComponentData;
|
||||
|
|
@ -526,7 +527,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<p className="mt-1 text-sm text-gray-600">버튼 액션과 함께 실행될 추가 기능을 설정합니다</p>
|
||||
</div>
|
||||
|
||||
<ButtonDataflowConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface RelationshipOption {
|
|||
* 🔥 버튼 제어관리 설정 패널 (Phase 1: 간편 모드만)
|
||||
*
|
||||
* 성능 최적화를 위해 간편 모드만 구현:
|
||||
* - 기존 관계도 선택
|
||||
* - 기존 관계 선택
|
||||
* - "after" 타이밍만 지원
|
||||
* - 복잡한 고급 모드는 Phase 2에서
|
||||
*/
|
||||
|
|
@ -57,14 +57,14 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
const [relationshipOpen, setRelationshipOpen] = useState(false);
|
||||
const [previewData, setPreviewData] = useState<any>(null);
|
||||
|
||||
// 🔥 관계도 목록 로딩
|
||||
// 🔥 관계 목록 로딩
|
||||
useEffect(() => {
|
||||
if (config.enableDataflowControl) {
|
||||
loadDiagrams();
|
||||
}
|
||||
}, [config.enableDataflowControl]);
|
||||
|
||||
// 🔥 관계도 변경 시 관계 목록 로딩
|
||||
// 🔥 관계 변경 시 관계 목록 로딩
|
||||
useEffect(() => {
|
||||
if (dataflowConfig.selectedDiagramId) {
|
||||
loadRelationships(dataflowConfig.selectedDiagramId);
|
||||
|
|
@ -72,12 +72,12 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
}, [dataflowConfig.selectedDiagramId]);
|
||||
|
||||
/**
|
||||
* 🔥 관계도 목록 로딩 (캐시 활용)
|
||||
* 🔥 관계 목록 로딩 (캐시 활용)
|
||||
*/
|
||||
const loadDiagrams = async () => {
|
||||
try {
|
||||
setDiagramsLoading(true);
|
||||
console.log("🔍 데이터플로우 관계도 목록 로딩...");
|
||||
console.log("🔍 데이터플로우 관계 목록 로딩...");
|
||||
|
||||
const response = await apiClient.get("/test-button-dataflow/diagrams");
|
||||
|
||||
|
|
@ -90,10 +90,10 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
}));
|
||||
|
||||
setDiagrams(diagramList);
|
||||
console.log(`✅ 관계도 ${diagramList.length}개 로딩 완료`);
|
||||
console.log(`✅ 관계 ${diagramList.length}개 로딩 완료`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 관계도 목록 로딩 실패:", error);
|
||||
console.error("❌ 관계 목록 로딩 실패:", error);
|
||||
setDiagrams([]);
|
||||
} finally {
|
||||
setDiagramsLoading(false);
|
||||
|
|
@ -106,7 +106,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
const loadRelationships = async (diagramId: number) => {
|
||||
try {
|
||||
setRelationshipsLoading(true);
|
||||
console.log(`🔍 관계도 ${diagramId} 관계 목록 로딩...`);
|
||||
console.log(`🔍 관계 ${diagramId} 관계 목록 로딩...`);
|
||||
|
||||
const response = await apiClient.get(`/test-button-dataflow/diagrams/${diagramId}/relationships`);
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
}
|
||||
};
|
||||
|
||||
// 선택된 관계도 정보
|
||||
// 선택된 관계 정보
|
||||
const selectedDiagram = diagrams.find((d) => d.id === dataflowConfig.selectedDiagramId);
|
||||
const selectedRelationship = relationships.find((r) => r.id === dataflowConfig.selectedRelationshipId);
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
<SelectValue placeholder="제어 모드를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="simple">간편 모드 (관계도 선택)</SelectItem>
|
||||
<SelectItem value="simple">간편 모드 (관계 선택)</SelectItem>
|
||||
<SelectItem value="advanced" disabled>
|
||||
고급 모드 (개발중)
|
||||
</SelectItem>
|
||||
|
|
@ -335,11 +335,11 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
{/* 간편 모드 설정 */}
|
||||
{(dataflowConfig.controlMode === "simple" || !dataflowConfig.controlMode) && (
|
||||
<div className="space-y-3 rounded border bg-gray-50 p-3">
|
||||
<h4 className="text-sm font-medium text-gray-700">관계도 선택</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700">관계 선택</h4>
|
||||
|
||||
{/* 관계도 선택 */}
|
||||
{/* 관계 선택 */}
|
||||
<div>
|
||||
<Label className="text-xs">관계도</Label>
|
||||
<Label className="text-xs">관계</Label>
|
||||
<Popover open={diagramOpen} onOpenChange={setDiagramOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -357,7 +357,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
"관계도를 선택하세요"
|
||||
"관계를 선택하세요"
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
|
|
@ -365,9 +365,9 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
<PopoverContent className="w-80 p-0">
|
||||
<div className="p-2">
|
||||
{diagramsLoading ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">관계도 목록을 불러오는 중...</div>
|
||||
<div className="p-4 text-center text-sm text-gray-500">관계 목록을 불러오는 중...</div>
|
||||
) : diagrams.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">사용 가능한 관계도가 없습니다</div>
|
||||
<div className="p-4 text-center text-sm text-gray-500">사용 가능한 관계가 없습니다</div>
|
||||
) : (
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{diagrams.map((diagram) => (
|
||||
|
|
@ -377,7 +377,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
className="h-auto w-full justify-start p-2"
|
||||
onClick={() => {
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.selectedDiagramId", diagram.id);
|
||||
// 관계도 변경 시 기존 관계 선택 초기화
|
||||
// 관계 변경 시 기존 관계 선택 초기화
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.selectedRelationshipId", null);
|
||||
setDiagramOpen(false);
|
||||
}}
|
||||
|
|
@ -435,7 +435,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
<div className="p-4 text-center text-sm text-gray-500">관계 목록을 불러오는 중...</div>
|
||||
) : relationships.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">
|
||||
이 관계도에는 사용 가능한 관계가 없습니다
|
||||
이 관계에는 사용 가능한 관계가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,281 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Settings,
|
||||
GitBranch,
|
||||
Clock,
|
||||
Zap,
|
||||
Info
|
||||
} from "lucide-react";
|
||||
import { ComponentData, ButtonDataflowConfig } from "@/types/screen";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
interface ImprovedButtonControlConfigPanelProps {
|
||||
component: ComponentData;
|
||||
onUpdateProperty: (path: string, value: any) => void;
|
||||
}
|
||||
|
||||
interface RelationshipOption {
|
||||
id: string;
|
||||
name: string;
|
||||
sourceTable: string;
|
||||
targetTable: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 단순화된 버튼 제어 설정 패널
|
||||
*
|
||||
* 관계 실행만 지원:
|
||||
* - 관계 선택 및 실행 타이밍 설정
|
||||
* - 관계 내부에 데이터 저장/외부호출 로직 포함
|
||||
*/
|
||||
export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlConfigPanelProps> = ({
|
||||
component,
|
||||
onUpdateProperty,
|
||||
}) => {
|
||||
const config = component.webTypeConfig || {};
|
||||
const dataflowConfig = config.dataflowConfig || {};
|
||||
|
||||
// 🔥 State 관리
|
||||
const [relationships, setRelationships] = useState<RelationshipOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 🔥 관계 목록 로딩
|
||||
useEffect(() => {
|
||||
if (config.enableDataflowControl) {
|
||||
loadRelationships();
|
||||
}
|
||||
}, [config.enableDataflowControl]);
|
||||
|
||||
/**
|
||||
* 🔥 전체 관계 목록 로드 (관계도별 구분 없이)
|
||||
*/
|
||||
const loadRelationships = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log("🔍 전체 관계 목록 로딩...");
|
||||
|
||||
const response = await apiClient.get("/test-button-dataflow/relationships/all");
|
||||
|
||||
if (response.data.success && Array.isArray(response.data.data)) {
|
||||
const relationshipList = response.data.data.map((rel: any) => ({
|
||||
id: rel.id,
|
||||
name: rel.name || `${rel.sourceTable} → ${rel.targetTable}`,
|
||||
sourceTable: rel.sourceTable,
|
||||
targetTable: rel.targetTable,
|
||||
category: rel.category || "데이터 흐름",
|
||||
}));
|
||||
|
||||
setRelationships(relationshipList);
|
||||
console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 관계 목록 로딩 실패:", error);
|
||||
setRelationships([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 관계 선택 핸들러
|
||||
*/
|
||||
const handleRelationshipSelect = (relationshipId: string) => {
|
||||
const selectedRelationship = relationships.find(r => r.id === relationshipId);
|
||||
if (selectedRelationship) {
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.relationshipConfig", {
|
||||
relationshipId: selectedRelationship.id,
|
||||
relationshipName: selectedRelationship.name,
|
||||
executionTiming: "after", // 기본값
|
||||
contextData: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔥 제어 타입 변경 핸들러
|
||||
*/
|
||||
const handleControlTypeChange = (controlType: string) => {
|
||||
// 기존 설정 초기화
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig", {
|
||||
controlMode: controlType,
|
||||
relationshipConfig: controlType === "relationship" ? undefined : null,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 🔥 제어관리 활성화 스위치 */}
|
||||
<div className="flex items-center justify-between rounded-lg border bg-blue-50 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Settings className="h-4 w-4 text-blue-600" />
|
||||
<div>
|
||||
<Label className="text-sm font-medium">🎮 고급 제어 기능</Label>
|
||||
<p className="mt-1 text-xs text-gray-600">버튼 클릭 시 추가 작업을 자동으로 실행합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.enableDataflowControl || false}
|
||||
onCheckedChange={(checked) => onUpdateProperty("webTypeConfig.enableDataflowControl", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 🔥 제어관리가 활성화된 경우에만 설정 표시 */}
|
||||
{config.enableDataflowControl && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>버튼 제어 설정</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={dataflowConfig.controlMode || "none"}
|
||||
onValueChange={handleControlTypeChange}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="none">제어 없음</TabsTrigger>
|
||||
<TabsTrigger value="relationship">관계 실행</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="none" className="mt-4">
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Zap className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>추가 제어 없이 기본 버튼 액션만 실행됩니다.</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="relationship" className="mt-4">
|
||||
<RelationshipSelector
|
||||
relationships={relationships}
|
||||
selectedRelationshipId={dataflowConfig.relationshipConfig?.relationshipId}
|
||||
onSelect={handleRelationshipSelect}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{dataflowConfig.relationshipConfig && (
|
||||
<div className="mt-4 space-y-4">
|
||||
<Separator />
|
||||
<ExecutionTimingSelector
|
||||
value={dataflowConfig.relationshipConfig.executionTiming}
|
||||
onChange={(timing) =>
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.relationshipConfig.executionTiming", timing)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="rounded bg-blue-50 p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Info className="h-4 w-4 text-blue-600 mt-0.5" />
|
||||
<div className="text-xs text-blue-800">
|
||||
<p className="font-medium">관계 실행 정보:</p>
|
||||
<p className="mt-1">선택한 관계에 설정된 데이터 저장, 외부호출 등의 모든 액션이 자동으로 실행됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔥 관계 선택 컴포넌트
|
||||
*/
|
||||
const RelationshipSelector: React.FC<{
|
||||
relationships: RelationshipOption[];
|
||||
selectedRelationshipId?: string;
|
||||
onSelect: (relationshipId: string) => void;
|
||||
loading: boolean;
|
||||
}> = ({ relationships, selectedRelationshipId, onSelect, loading }) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<GitBranch className="h-4 w-4 text-blue-600" />
|
||||
<Label>실행할 관계 선택</Label>
|
||||
</div>
|
||||
|
||||
<Select value={selectedRelationshipId || ""} onValueChange={onSelect}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="관계를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{loading ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">관계 목록을 불러오는 중...</div>
|
||||
) : relationships.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">사용 가능한 관계가 없습니다</div>
|
||||
) : (
|
||||
relationships.map((rel) => (
|
||||
<SelectItem key={rel.id} value={rel.id}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{rel.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{rel.sourceTable} → {rel.targetTable}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 실행 타이밍 선택 컴포넌트
|
||||
*/
|
||||
const ExecutionTimingSelector: React.FC<{
|
||||
value: string;
|
||||
onChange: (timing: "before" | "after" | "replace") => void;
|
||||
}> = ({ value, onChange }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-orange-600" />
|
||||
<Label>실행 타이밍</Label>
|
||||
</div>
|
||||
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="실행 타이밍을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="before">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Before (사전 실행)</span>
|
||||
<span className="text-xs text-muted-foreground">버튼 액션 실행 전에 제어를 실행합니다</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="after">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">After (사후 실행)</span>
|
||||
<span className="text-xs text-muted-foreground">버튼 액션 실행 후에 제어를 실행합니다</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="replace">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Replace (대체 실행)</span>
|
||||
<span className="text-xs text-muted-foreground">버튼 액션 대신 제어만 실행합니다</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import { toast } from "sonner";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||
import { OptimizedButtonDataflowService, ExtendedControlContext } from "@/lib/services/optimizedButtonDataflowService";
|
||||
import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor";
|
||||
|
||||
/**
|
||||
* 버튼 액션 타입 정의
|
||||
|
|
@ -781,40 +781,56 @@ export class ButtonActionExecutor {
|
|||
extendedContext,
|
||||
});
|
||||
|
||||
// 🔥 실제 제어 조건 검증 수행
|
||||
const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation(
|
||||
config.dataflowConfig,
|
||||
extendedContext,
|
||||
);
|
||||
// 🔥 새로운 버튼 액션 실행 시스템 사용
|
||||
if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
|
||||
console.log("🔗 관계 기반 제어 실행:", config.dataflowConfig.relationshipConfig);
|
||||
|
||||
// 새로운 ImprovedButtonActionExecutor 사용
|
||||
const buttonConfig = {
|
||||
actionType: config.type,
|
||||
dataflowConfig: config.dataflowConfig,
|
||||
enableDataflowControl: true, // 관계 기반 제어가 설정된 경우 활성화
|
||||
};
|
||||
|
||||
if (validationResult.success) {
|
||||
console.log("✅ 제어 조건 만족 - 액션 실행 시작:", {
|
||||
actions: validationResult.actions,
|
||||
context,
|
||||
});
|
||||
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(
|
||||
buttonConfig,
|
||||
context.formData || {},
|
||||
{
|
||||
buttonId: context.buttonId || "unknown",
|
||||
screenId: context.screenId || "unknown",
|
||||
userId: context.userId || "unknown",
|
||||
companyCode: context.companyCode || "*",
|
||||
startTime: Date.now(),
|
||||
contextData: context,
|
||||
}
|
||||
);
|
||||
|
||||
// 🔥 조건을 만족했으므로 실제 액션 실행
|
||||
if (validationResult.actions && validationResult.actions.length > 0) {
|
||||
console.log("🚀 액션 실행 시작:", validationResult.actions);
|
||||
await this.executeRelationshipActions(validationResult.actions, context);
|
||||
if (executionResult.success) {
|
||||
console.log("✅ 관계 실행 완료:", executionResult);
|
||||
toast.success(config.successMessage || "관계 실행이 완료되었습니다.");
|
||||
|
||||
// 새로고침이 필요한 경우
|
||||
if (context.onRefresh) {
|
||||
context.onRefresh();
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.warn("⚠️ 실행할 액션이 없습니다:", {
|
||||
hasActions: !!validationResult.actions,
|
||||
actionsLength: validationResult.actions?.length,
|
||||
validationResult,
|
||||
});
|
||||
toast.success(config.successMessage || "제어 조건을 만족합니다. (실행할 액션 없음)");
|
||||
console.error("❌ 관계 실행 실패:", executionResult);
|
||||
toast.error(config.errorMessage || "관계 실행 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
// 제어 없음 - 메인 액션만 실행
|
||||
console.log("⚡ 제어 없음 - 메인 액션 실행");
|
||||
await this.executeMainAction(config, context);
|
||||
|
||||
// 새로고침이 필요한 경우
|
||||
if (context.onRefresh) {
|
||||
context.onRefresh();
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
} else {
|
||||
toast.error(validationResult.message || "제어 조건을 만족하지 않습니다.");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("제어 조건 검증 중 오류:", error);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,781 @@
|
|||
/**
|
||||
* 🔥 개선된 버튼 액션 실행기
|
||||
*
|
||||
* 계획서에 따른 새로운 실행 플로우:
|
||||
* 1. Before 타이밍 제어 실행
|
||||
* 2. 메인 액션 실행 (replace가 아닌 경우)
|
||||
* 3. After 타이밍 제어 실행
|
||||
*/
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { ExtendedButtonTypeConfig, ButtonDataflowConfig } from "@/types/control-management";
|
||||
import { ButtonActionType } from "@/types/unified-core";
|
||||
|
||||
// ===== 인터페이스 정의 =====
|
||||
|
||||
export interface ButtonExecutionContext {
|
||||
buttonId: string;
|
||||
screenId: string;
|
||||
userId: string;
|
||||
companyCode: string;
|
||||
startTime: number;
|
||||
formData?: Record<string, any>;
|
||||
selectedRows?: any[];
|
||||
tableData?: any[];
|
||||
}
|
||||
|
||||
export interface ExecutionResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
executionTime: number;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ButtonExecutionResult {
|
||||
success: boolean;
|
||||
results: ExecutionResult[];
|
||||
executionTime: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ControlConfig {
|
||||
type: "relationship";
|
||||
relationshipConfig: {
|
||||
relationshipId: string;
|
||||
relationshipName: string;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
contextData?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ExecutionPlan {
|
||||
beforeControls: ControlConfig[];
|
||||
afterControls: ControlConfig[];
|
||||
hasReplaceControl: boolean;
|
||||
}
|
||||
|
||||
// ===== 메인 실행기 클래스 =====
|
||||
|
||||
export class ImprovedButtonActionExecutor {
|
||||
/**
|
||||
* 🔥 개선된 버튼 액션 실행
|
||||
*/
|
||||
static async executeButtonAction(
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ButtonExecutionResult> {
|
||||
console.log("🔥 ImprovedButtonActionExecutor 시작:", {
|
||||
buttonConfig,
|
||||
formData,
|
||||
context,
|
||||
});
|
||||
|
||||
const executionPlan = this.createExecutionPlan(buttonConfig);
|
||||
const results: ExecutionResult[] = [];
|
||||
|
||||
console.log("📋 생성된 실행 계획:", {
|
||||
beforeControls: executionPlan.beforeControls,
|
||||
afterControls: executionPlan.afterControls,
|
||||
hasReplaceControl: executionPlan.hasReplaceControl,
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("🚀 버튼 액션 실행 시작:", {
|
||||
actionType: buttonConfig.actionType,
|
||||
hasControls: executionPlan.beforeControls.length + executionPlan.afterControls.length > 0,
|
||||
hasReplace: executionPlan.hasReplaceControl,
|
||||
});
|
||||
|
||||
// 1. Before 타이밍 제어 실행
|
||||
if (executionPlan.beforeControls.length > 0) {
|
||||
console.log("⏰ Before 제어 실행 시작");
|
||||
const beforeResults = await this.executeControls(
|
||||
executionPlan.beforeControls,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
results.push(...beforeResults);
|
||||
|
||||
// Before 제어 중 실패가 있으면 중단
|
||||
const hasFailure = beforeResults.some(r => !r.success);
|
||||
if (hasFailure) {
|
||||
throw new Error("Before 제어 실행 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 메인 액션 실행 (replace가 아닌 경우에만)
|
||||
if (!executionPlan.hasReplaceControl) {
|
||||
console.log("⚡ 메인 액션 실행:", buttonConfig.actionType);
|
||||
const mainResult = await this.executeMainAction(
|
||||
buttonConfig,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
results.push(mainResult);
|
||||
|
||||
if (!mainResult.success) {
|
||||
throw new Error("메인 액션 실행 중 오류가 발생했습니다.");
|
||||
}
|
||||
} else {
|
||||
console.log("🔄 Replace 모드: 메인 액션 건너뜀");
|
||||
}
|
||||
|
||||
// 3. After 타이밍 제어 실행
|
||||
if (executionPlan.afterControls.length > 0) {
|
||||
console.log("⏰ After 제어 실행 시작");
|
||||
const afterResults = await this.executeControls(
|
||||
executionPlan.afterControls,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
results.push(...afterResults);
|
||||
}
|
||||
|
||||
const totalExecutionTime = Date.now() - context.startTime;
|
||||
console.log("✅ 버튼 액션 실행 완료:", `${totalExecutionTime}ms`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results,
|
||||
executionTime: totalExecutionTime,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ 버튼 액션 실행 실패:", error);
|
||||
|
||||
// 롤백 처리
|
||||
await this.handleExecutionError(error, results, buttonConfig);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
results,
|
||||
executionTime: Date.now() - context.startTime,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 실행 계획 생성
|
||||
*/
|
||||
private static createExecutionPlan(buttonConfig: ExtendedButtonTypeConfig): ExecutionPlan {
|
||||
const plan: ExecutionPlan = {
|
||||
beforeControls: [],
|
||||
afterControls: [],
|
||||
hasReplaceControl: false,
|
||||
};
|
||||
|
||||
const dataflowConfig = buttonConfig.dataflowConfig;
|
||||
if (!dataflowConfig) {
|
||||
console.log("⚠️ dataflowConfig가 없습니다");
|
||||
return plan;
|
||||
}
|
||||
|
||||
// enableDataflowControl 체크를 제거하고 dataflowConfig만 있으면 실행
|
||||
console.log("📋 실행 계획 생성:", {
|
||||
controlMode: dataflowConfig.controlMode,
|
||||
hasRelationshipConfig: !!dataflowConfig.relationshipConfig,
|
||||
enableDataflowControl: buttonConfig.enableDataflowControl,
|
||||
});
|
||||
|
||||
// 관계 기반 제어만 지원
|
||||
if (dataflowConfig.controlMode === "relationship" && dataflowConfig.relationshipConfig) {
|
||||
const control: ControlConfig = {
|
||||
type: "relationship",
|
||||
relationshipConfig: dataflowConfig.relationshipConfig,
|
||||
};
|
||||
|
||||
switch (dataflowConfig.relationshipConfig.executionTiming) {
|
||||
case "before":
|
||||
plan.beforeControls.push(control);
|
||||
break;
|
||||
case "after":
|
||||
plan.afterControls.push(control);
|
||||
break;
|
||||
case "replace":
|
||||
plan.afterControls.push(control); // Replace는 after로 처리하되 플래그 설정
|
||||
plan.hasReplaceControl = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 제어 실행 (관계 또는 외부호출)
|
||||
*/
|
||||
private static async executeControls(
|
||||
controls: ControlConfig[],
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ExecutionResult[]> {
|
||||
const results: ExecutionResult[] = [];
|
||||
|
||||
for (const control of controls) {
|
||||
try {
|
||||
// 관계 실행만 지원
|
||||
const result = await this.executeRelationship(
|
||||
control.relationshipConfig,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
|
||||
results.push(result);
|
||||
|
||||
// 제어 실행 실패 시 중단
|
||||
if (!result.success) {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`제어 실행 실패 (${control.type}):`, error);
|
||||
results.push({
|
||||
success: false,
|
||||
message: `${control.type} 제어 실행 실패: ${error.message}`,
|
||||
executionTime: 0,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 관계 실행
|
||||
*/
|
||||
private static async executeRelationship(
|
||||
config: {
|
||||
relationshipId: string;
|
||||
relationshipName: string;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
contextData?: Record<string, any>;
|
||||
},
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`🔗 관계 실행 시작: ${config.relationshipName} (ID: ${config.relationshipId})`);
|
||||
|
||||
// 1. 관계 정보 조회
|
||||
const relationshipData = await this.getRelationshipData(config.relationshipId);
|
||||
if (!relationshipData) {
|
||||
throw new Error(`관계 정보를 찾을 수 없습니다: ${config.relationshipId}`);
|
||||
}
|
||||
|
||||
console.log(`📋 관계 데이터 로드 완료:`, relationshipData);
|
||||
|
||||
// 2. 관계 타입에 따른 실행
|
||||
const relationships = relationshipData.relationships;
|
||||
const connectionType = relationships.connectionType;
|
||||
|
||||
let result: ExecutionResult;
|
||||
|
||||
if (connectionType === "external_call") {
|
||||
// 외부 호출 실행
|
||||
result = await this.executeExternalCall(relationships, formData, context);
|
||||
} else if (connectionType === "data_save") {
|
||||
// 데이터 저장 실행
|
||||
result = await this.executeDataSave(relationships, formData, context);
|
||||
} else {
|
||||
throw new Error(`지원하지 않는 연결 타입: ${connectionType}`);
|
||||
}
|
||||
|
||||
console.log(`✅ 관계 실행 완료: ${config.relationshipName}`, result);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(`관계 '${config.relationshipName}' 실행 완료`);
|
||||
} else {
|
||||
toast.error(`관계 '${config.relationshipName}' 실행 실패: ${result.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`❌ 관계 실행 실패: ${config.relationshipName}`, error);
|
||||
const errorResult = {
|
||||
success: false,
|
||||
message: `관계 '${config.relationshipName}' 실행 실패: ${error.message}`,
|
||||
executionTime: 0,
|
||||
error: error.message,
|
||||
};
|
||||
|
||||
toast.error(errorResult.message);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계 데이터 조회
|
||||
*/
|
||||
private static async getRelationshipData(relationshipId: string): Promise<any> {
|
||||
try {
|
||||
console.log(`🔍 관계 데이터 조회 시작: ${relationshipId}`);
|
||||
|
||||
const response = await apiClient.get(`/dataflow-diagrams/${relationshipId}`);
|
||||
|
||||
console.log(`✅ 관계 데이터 조회 성공:`, response.data);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || '관계 데이터 조회 실패');
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error('관계 데이터 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 실행
|
||||
*/
|
||||
private static async executeExternalCall(
|
||||
relationships: any,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
const externalCallConfig = relationships.externalCallConfig;
|
||||
if (!externalCallConfig) {
|
||||
throw new Error('외부 호출 설정이 없습니다');
|
||||
}
|
||||
|
||||
const restApiSettings = externalCallConfig.restApiSettings;
|
||||
if (!restApiSettings) {
|
||||
throw new Error('REST API 설정이 없습니다');
|
||||
}
|
||||
|
||||
console.log(`🌐 외부 API 호출: ${restApiSettings.apiUrl}`);
|
||||
|
||||
// API 호출 준비
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...restApiSettings.headers,
|
||||
};
|
||||
|
||||
// 인증 처리
|
||||
if (restApiSettings.authentication?.type === 'api-key') {
|
||||
headers['Authorization'] = `Bearer ${restApiSettings.authentication.apiKey}`;
|
||||
}
|
||||
|
||||
// 요청 바디 준비 (템플릿 처리)
|
||||
let requestBody = restApiSettings.bodyTemplate || '';
|
||||
if (requestBody) {
|
||||
// 간단한 템플릿 치환 ({{변수명}} 형태)
|
||||
requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => {
|
||||
return formData[key] || (context as any).contextData?.[key] || new Date().toISOString();
|
||||
});
|
||||
}
|
||||
|
||||
// 백엔드 프록시를 통한 외부 API 호출 (CORS 문제 해결)
|
||||
console.log(`🌐 백엔드 프록시를 통한 외부 API 호출 준비:`, {
|
||||
originalUrl: restApiSettings.apiUrl,
|
||||
method: restApiSettings.httpMethod || 'GET',
|
||||
headers,
|
||||
body: restApiSettings.httpMethod !== 'GET' ? requestBody : undefined,
|
||||
});
|
||||
|
||||
// 백엔드 프록시 API 호출 - GenericApiSettings 형식에 맞게 전달
|
||||
const requestPayload = {
|
||||
diagramId: relationships.diagramId || 45, // 관계 ID 사용
|
||||
relationshipId: relationships.relationshipId || "relationship-45",
|
||||
settings: {
|
||||
callType: "rest-api",
|
||||
apiType: "generic",
|
||||
url: restApiSettings.apiUrl,
|
||||
method: restApiSettings.httpMethod || 'POST',
|
||||
headers: restApiSettings.headers || {},
|
||||
body: requestBody,
|
||||
authentication: restApiSettings.authentication || { type: 'none' },
|
||||
timeout: restApiSettings.timeout || 30000,
|
||||
retryCount: restApiSettings.retryCount || 3,
|
||||
},
|
||||
templateData: restApiSettings.httpMethod !== 'GET' ? JSON.parse(requestBody) : {},
|
||||
};
|
||||
|
||||
console.log(`📤 백엔드로 전송할 데이터:`, requestPayload);
|
||||
|
||||
const proxyResponse = await apiClient.post(`/external-calls/execute`, requestPayload);
|
||||
|
||||
console.log(`📡 백엔드 프록시 응답:`, proxyResponse.data);
|
||||
|
||||
if (!proxyResponse.data.success) {
|
||||
throw new Error(`프록시 API 호출 실패: ${proxyResponse.data.error || proxyResponse.data.message}`);
|
||||
}
|
||||
|
||||
const responseData = proxyResponse.data.result;
|
||||
console.log(`✅ 외부 API 호출 성공 (프록시):`, responseData);
|
||||
|
||||
// 데이터 매핑 처리 (inbound mapping)
|
||||
if (externalCallConfig.dataMappingConfig?.inboundMapping) {
|
||||
await this.processInboundMapping(
|
||||
externalCallConfig.dataMappingConfig.inboundMapping,
|
||||
responseData,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '외부 호출 실행 완료',
|
||||
executionTime: Date.now() - context.startTime,
|
||||
data: responseData,
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('외부 호출 실행 오류:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `외부 호출 실행 실패: ${error.message}`,
|
||||
executionTime: Date.now() - context.startTime,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 저장 실행
|
||||
*/
|
||||
private static async executeDataSave(
|
||||
relationships: any,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`💾 데이터 저장 실행 시작`);
|
||||
|
||||
// 제어 조건 확인
|
||||
const controlConditions = relationships.controlConditions || [];
|
||||
if (controlConditions.length > 0) {
|
||||
const conditionsMet = this.evaluateConditions(controlConditions, formData, context);
|
||||
if (!conditionsMet) {
|
||||
return {
|
||||
success: false,
|
||||
message: '제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다',
|
||||
executionTime: Date.now() - context.startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 액션 그룹 실행
|
||||
const actionGroups = relationships.actionGroups || [];
|
||||
const results = [];
|
||||
|
||||
for (const actionGroup of actionGroups) {
|
||||
if (!actionGroup.isEnabled) {
|
||||
console.log(`⏭️ 비활성화된 액션 그룹 건너뜀: ${actionGroup.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🎯 액션 그룹 실행: ${actionGroup.name}`);
|
||||
|
||||
for (const action of actionGroup.actions) {
|
||||
if (!action.isEnabled) {
|
||||
console.log(`⏭️ 비활성화된 액션 건너뜀: ${action.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const actionResult = await this.executeDataAction(
|
||||
action,
|
||||
relationships,
|
||||
formData,
|
||||
context
|
||||
);
|
||||
results.push(actionResult);
|
||||
|
||||
if (!actionResult.success) {
|
||||
console.error(`❌ 액션 실행 실패: ${action.name}`, actionResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const totalCount = results.length;
|
||||
|
||||
return {
|
||||
success: successCount > 0,
|
||||
message: `데이터 저장 완료: ${successCount}/${totalCount} 액션 성공`,
|
||||
executionTime: Date.now() - context.startTime,
|
||||
data: {
|
||||
results,
|
||||
successCount,
|
||||
totalCount,
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('데이터 저장 실행 오류:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `데이터 저장 실행 실패: ${error.message}`,
|
||||
executionTime: Date.now() - context.startTime,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 데이터 액션 실행
|
||||
*/
|
||||
private static async executeDataAction(
|
||||
action: any,
|
||||
relationships: any,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
console.log(`🔧 데이터 액션 실행: ${action.name} (${action.actionType})`);
|
||||
|
||||
// 필드 매핑 처리
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
for (const mapping of action.fieldMappings) {
|
||||
if (mapping.valueType === 'static') {
|
||||
// 정적 값 처리
|
||||
let value = mapping.value;
|
||||
if (value === '#NOW') {
|
||||
value = new Date().toISOString();
|
||||
}
|
||||
mappedData[mapping.targetField] = value;
|
||||
} else {
|
||||
// 필드 매핑 처리
|
||||
const sourceField = mapping.fromField?.columnName;
|
||||
if (sourceField && formData[sourceField] !== undefined) {
|
||||
mappedData[mapping.toField.columnName] = formData[sourceField];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📋 매핑된 데이터:`, mappedData);
|
||||
|
||||
// 대상 연결 정보
|
||||
const toConnection = relationships.toConnection;
|
||||
const targetTable = relationships.toTable?.tableName;
|
||||
|
||||
if (!targetTable) {
|
||||
throw new Error('대상 테이블이 지정되지 않았습니다');
|
||||
}
|
||||
|
||||
// 데이터 저장 API 호출
|
||||
const saveResult = await this.saveDataToTable(
|
||||
targetTable,
|
||||
mappedData,
|
||||
action.actionType,
|
||||
toConnection
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `데이터 액션 "${action.name}" 실행 완료`,
|
||||
executionTime: Date.now() - context.startTime,
|
||||
data: saveResult,
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`데이터 액션 실행 오류: ${action.name}`, error);
|
||||
return {
|
||||
success: false,
|
||||
message: `데이터 액션 실행 실패: ${error.message}`,
|
||||
executionTime: Date.now() - context.startTime,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블에 데이터 저장
|
||||
*/
|
||||
private static async saveDataToTable(
|
||||
tableName: string,
|
||||
data: Record<string, any>,
|
||||
actionType: string,
|
||||
connection?: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 데이터 저장 API 호출
|
||||
const response = await fetch('/api/dataflow/execute-data-action', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tableName,
|
||||
data,
|
||||
actionType,
|
||||
connection,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`데이터 저장 API 호출 실패: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('데이터 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 평가
|
||||
*/
|
||||
private static evaluateConditions(
|
||||
conditions: any[],
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): boolean {
|
||||
for (const condition of conditions) {
|
||||
const fieldValue = formData[condition.field];
|
||||
const conditionValue = condition.value;
|
||||
const operator = condition.operator;
|
||||
|
||||
let conditionMet = false;
|
||||
switch (operator) {
|
||||
case '=':
|
||||
conditionMet = fieldValue === conditionValue;
|
||||
break;
|
||||
case '!=':
|
||||
conditionMet = fieldValue !== conditionValue;
|
||||
break;
|
||||
case '>':
|
||||
conditionMet = Number(fieldValue) > Number(conditionValue);
|
||||
break;
|
||||
case '<':
|
||||
conditionMet = Number(fieldValue) < Number(conditionValue);
|
||||
break;
|
||||
case '>=':
|
||||
conditionMet = Number(fieldValue) >= Number(conditionValue);
|
||||
break;
|
||||
case '<=':
|
||||
conditionMet = Number(fieldValue) <= Number(conditionValue);
|
||||
break;
|
||||
default:
|
||||
console.warn(`지원하지 않는 연산자: ${operator}`);
|
||||
conditionMet = true;
|
||||
}
|
||||
|
||||
if (!conditionMet) {
|
||||
console.log(`❌ 조건 불만족: ${condition.field} ${operator} ${conditionValue} (실제값: ${fieldValue})`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 모든 조건 만족`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인바운드 데이터 매핑 처리
|
||||
*/
|
||||
private static async processInboundMapping(
|
||||
inboundMapping: any,
|
||||
responseData: any,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log(`📥 인바운드 데이터 매핑 처리 시작`);
|
||||
|
||||
const targetTable = inboundMapping.targetTable;
|
||||
const fieldMappings = inboundMapping.fieldMappings || [];
|
||||
const insertMode = inboundMapping.insertMode || 'insert';
|
||||
|
||||
// 응답 데이터가 배열인 경우 각 항목 처리
|
||||
const dataArray = Array.isArray(responseData) ? responseData : [responseData];
|
||||
|
||||
for (const item of dataArray) {
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
// 필드 매핑 적용
|
||||
for (const mapping of fieldMappings) {
|
||||
const sourceValue = item[mapping.sourceField];
|
||||
if (sourceValue !== undefined) {
|
||||
mappedData[mapping.targetField] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📋 매핑된 데이터:`, mappedData);
|
||||
|
||||
// 데이터 저장
|
||||
await this.saveDataToTable(targetTable, mappedData, insertMode);
|
||||
}
|
||||
|
||||
console.log(`✅ 인바운드 데이터 매핑 완료`);
|
||||
} catch (error) {
|
||||
console.error('인바운드 데이터 매핑 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 메인 액션 실행
|
||||
*/
|
||||
private static async executeMainAction(
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): Promise<ExecutionResult> {
|
||||
try {
|
||||
// 기존 ButtonActionExecutor 로직을 여기서 호출하거나
|
||||
// 간단한 액션들을 직접 구현
|
||||
const startTime = performance.now();
|
||||
|
||||
// 임시 구현 - 실제로는 기존 ButtonActionExecutor를 호출해야 함
|
||||
const result = {
|
||||
success: true,
|
||||
message: `${buttonConfig.actionType} 액션 실행 완료`,
|
||||
executionTime: performance.now() - startTime,
|
||||
data: { actionType: buttonConfig.actionType, formData },
|
||||
};
|
||||
|
||||
console.log("✅ 메인 액션 실행 완료:", result.message);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("메인 액션 실행 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: `${buttonConfig.actionType} 액션 실행 실패: ${error.message}`,
|
||||
executionTime: 0,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 실행 오류 처리 및 롤백
|
||||
*/
|
||||
private static async handleExecutionError(
|
||||
error: Error,
|
||||
results: ExecutionResult[],
|
||||
buttonConfig: ExtendedButtonTypeConfig
|
||||
): Promise<void> {
|
||||
console.error("🔄 실행 오류 처리 시작:", error.message);
|
||||
|
||||
// 롤백이 필요한 경우 처리
|
||||
const rollbackNeeded = buttonConfig.dataflowConfig?.executionOptions?.rollbackOnError;
|
||||
if (rollbackNeeded) {
|
||||
console.log("🔄 롤백 처리 시작...");
|
||||
|
||||
// 성공한 결과들을 역순으로 롤백
|
||||
const successfulResults = results.filter(r => r.success).reverse();
|
||||
|
||||
for (const result of successfulResults) {
|
||||
try {
|
||||
// 롤백 로직 구현 (필요시)
|
||||
console.log("🔄 롤백:", result.message);
|
||||
} catch (rollbackError) {
|
||||
console.error("롤백 실패:", rollbackError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 오류 토스트 표시
|
||||
toast.error(error.message || "작업 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
|
@ -56,24 +56,34 @@ export interface ExtendedButtonTypeConfig {
|
|||
}
|
||||
|
||||
/**
|
||||
* 버튼 데이터플로우 설정
|
||||
* 🔥 단순화된 버튼 데이터플로우 설정
|
||||
*/
|
||||
export interface ButtonDataflowConfig {
|
||||
// 제어 방식 선택
|
||||
controlMode: "simple" | "advanced";
|
||||
// 제어 방식 선택 (관계 실행만)
|
||||
controlMode: "relationship" | "none";
|
||||
|
||||
// 관계도 방식 (diagram 기반)
|
||||
selectedDiagramId?: number;
|
||||
selectedRelationshipId?: number;
|
||||
// 관계 기반 제어
|
||||
relationshipConfig?: {
|
||||
relationshipId: string; // 관계 직접 선택
|
||||
relationshipName: string; // 관계명 표시
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
contextData?: Record<string, any>; // 실행 시 전달할 컨텍스트
|
||||
};
|
||||
|
||||
// 직접 설정 방식
|
||||
directControl?: DirectControlConfig;
|
||||
|
||||
// 제어 데이터 소스
|
||||
// 제어 데이터 소스 (기존 호환성 유지)
|
||||
controlDataSource?: ControlDataSource;
|
||||
|
||||
// 실행 옵션
|
||||
executionOptions?: ExecutionOptions;
|
||||
|
||||
// 🔧 기존 호환성을 위한 필드들 (deprecated)
|
||||
selectedDiagramId?: number;
|
||||
selectedRelationshipId?: number;
|
||||
directControl?: DirectControlConfig;
|
||||
|
||||
// 🔧 제거된 필드들 (하위 호환성을 위해 optional로 유지)
|
||||
externalCallConfig?: any; // deprecated
|
||||
customConfig?: any; // deprecated
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue