ERP-node/backend-node/src/controllers/buttonDataflowController.ts

762 lines
20 KiB
TypeScript
Raw Normal View History

2025-09-18 10:05:50 +09:00
/**
* 🔥
*
* API :
* 1.
* 2.
* 3.
*/
import { Request, Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import EventTriggerService from "../services/eventTriggerService";
import * as dataflowDiagramService from "../services/dataflowDiagramService";
import logger from "../utils/logger";
/**
* 🔥 ( )
*/
export async function getButtonDataflowConfig(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { buttonId } = req.params;
const companyCode = req.user?.companyCode;
if (!buttonId) {
res.status(400).json({
success: false,
message: "버튼 ID가 필요합니다.",
});
return;
}
// 버튼별 제어관리 설정 조회
// TODO: 실제 버튼 설정 테이블에서 조회
// 현재는 mock 데이터 반환
const mockConfig = {
controlMode: "simple",
selectedDiagramId: 1,
selectedRelationshipId: "rel-123",
executionOptions: {
rollbackOnError: true,
enableLogging: true,
asyncExecution: true,
},
};
res.json({
success: true,
data: mockConfig,
});
} catch (error) {
logger.error("Failed to get button dataflow config:", error);
res.status(500).json({
success: false,
message: "버튼 설정 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥
*/
export async function updateButtonDataflowConfig(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { buttonId } = req.params;
const config = req.body;
const companyCode = req.user?.companyCode;
if (!buttonId) {
res.status(400).json({
success: false,
message: "버튼 ID가 필요합니다.",
});
return;
}
// TODO: 실제 버튼 설정 테이블에 저장
logger.info(`Button dataflow config updated: ${buttonId}`, config);
res.json({
success: true,
message: "버튼 설정이 업데이트되었습니다.",
});
} catch (error) {
logger.error("Failed to update button dataflow config:", error);
res.status(500).json({
success: false,
message: "버튼 설정 업데이트 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥
*/
export async function getAvailableDiagrams(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user?.companyCode;
if (!companyCode) {
res.status(400).json({
success: false,
message: "회사 코드가 필요합니다.",
});
return;
}
const diagramsResult = await dataflowDiagramService.getDataflowDiagrams(
companyCode,
1,
100
);
const diagrams = diagramsResult.diagrams;
res.json({
success: true,
data: diagrams,
});
} catch (error) {
logger.error("Failed to get available diagrams:", error);
res.status(500).json({
success: false,
message: "관계도 목록 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥
*/
export async function getDiagramRelationships(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { diagramId } = req.params;
const companyCode = req.user?.companyCode;
if (!diagramId || !companyCode) {
res.status(400).json({
success: false,
message: "관계도 ID와 회사 코드가 필요합니다.",
});
return;
}
const diagram = await dataflowDiagramService.getDataflowDiagramById(
parseInt(diagramId),
companyCode
);
if (!diagram) {
res.status(404).json({
success: false,
message: "관계도를 찾을 수 없습니다.",
});
return;
}
const relationships = (diagram.relationships as any)?.relationships || [];
console.log("🔍 백엔드 - 관계도 데이터:", {
diagramId: diagram.diagram_id,
diagramName: diagram.diagram_name,
relationshipsRaw: diagram.relationships,
relationshipsArray: relationships,
relationshipsCount: relationships.length,
});
// 각 관계의 구조도 로깅
relationships.forEach((rel: any, index: number) => {
console.log(`🔍 백엔드 - 관계 ${index + 1}:`, rel);
});
2025-09-18 10:05:50 +09:00
res.json({
success: true,
data: relationships,
});
} catch (error) {
logger.error("Failed to get diagram relationships:", error);
res.status(500).json({
success: false,
message: "관계 목록 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥
*/
export async function getRelationshipPreview(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { diagramId, relationshipId } = req.params;
const companyCode = req.user?.companyCode;
if (!diagramId || !relationshipId || !companyCode) {
res.status(400).json({
success: false,
message: "관계도 ID, 관계 ID, 회사 코드가 필요합니다.",
});
return;
}
const diagram = await dataflowDiagramService.getDataflowDiagramById(
parseInt(diagramId),
companyCode
);
if (!diagram) {
res.status(404).json({
success: false,
message: "관계도를 찾을 수 없습니다.",
});
return;
}
// 관계 정보 찾기
console.log("🔍 관계 미리보기 요청:", {
diagramId,
relationshipId,
diagramRelationships: diagram.relationships,
relationshipsArray: (diagram.relationships as any)?.relationships,
});
const relationships = (diagram.relationships as any)?.relationships || [];
console.log(
"🔍 사용 가능한 관계 목록:",
relationships.map((rel: any) => ({
id: rel.id,
name: rel.relationshipName || rel.name, // relationshipName 사용
sourceTable: rel.fromTable || rel.sourceTable, // fromTable 사용
targetTable: rel.toTable || rel.targetTable, // toTable 사용
originalData: rel, // 디버깅용
}))
);
const relationship = relationships.find(
2025-09-18 10:05:50 +09:00
(rel: any) => rel.id === relationshipId
);
console.log("🔍 찾은 관계:", relationship);
2025-09-18 10:05:50 +09:00
if (!relationship) {
console.log("❌ 관계를 찾을 수 없음:", {
requestedId: relationshipId,
availableIds: relationships.map((rel: any) => rel.id),
});
// 🔧 임시 해결책: 첫 번째 관계를 사용하거나 기본 응답 반환
if (relationships.length > 0) {
console.log("🔧 첫 번째 관계를 대신 사용:", relationships[0].id);
const fallbackRelationship = relationships[0];
console.log("🔍 fallback 관계 선택:", fallbackRelationship);
console.log("🔍 diagram.control 전체 구조:", diagram.control);
console.log("🔍 diagram.plan 전체 구조:", diagram.plan);
const fallbackControl = Array.isArray(diagram.control)
? diagram.control.find((c: any) => c.id === fallbackRelationship.id)
: null;
const fallbackPlan = Array.isArray(diagram.plan)
? diagram.plan.find((p: any) => p.id === fallbackRelationship.id)
: null;
console.log("🔍 찾은 fallback control:", fallbackControl);
console.log("🔍 찾은 fallback plan:", fallbackPlan);
const fallbackPreviewData = {
relationship: fallbackRelationship,
control: fallbackControl,
plan: fallbackPlan,
conditionsCount: (fallbackControl as any)?.conditions?.length || 0,
actionsCount: (fallbackPlan as any)?.actions?.length || 0,
};
console.log("🔍 최종 fallback 응답 데이터:", fallbackPreviewData);
res.json({
success: true,
data: fallbackPreviewData,
});
return;
}
2025-09-18 10:05:50 +09:00
res.status(404).json({
success: false,
message: `관계를 찾을 수 없습니다. 요청된 ID: ${relationshipId}, 사용 가능한 ID: ${relationships.map((rel: any) => rel.id).join(", ")}`,
2025-09-18 10:05:50 +09:00
});
return;
}
// 제어 및 계획 정보 추출
const control = Array.isArray(diagram.control)
? diagram.control.find((c: any) => c.id === relationshipId)
: null;
const plan = Array.isArray(diagram.plan)
? diagram.plan.find((p: any) => p.id === relationshipId)
: null;
const previewData = {
relationship,
control,
plan,
conditionsCount: (control as any)?.conditions?.length || 0,
actionsCount: (plan as any)?.actions?.length || 0,
};
res.json({
success: true,
data: previewData,
});
} catch (error) {
logger.error("Failed to get relationship preview:", error);
res.status(500).json({
success: false,
message: "관계 미리보기 조회 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥 ( )
*/
export async function executeOptimizedButton(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const {
buttonId,
actionType,
buttonConfig,
contextData,
timing = "after",
} = req.body;
const companyCode = req.user?.companyCode;
if (!buttonId || !actionType || !companyCode) {
res.status(400).json({
success: false,
message: "필수 파라미터가 누락되었습니다.",
});
return;
}
const startTime = Date.now();
// 🔥 타이밍에 따른 즉시 응답 처리
if (timing === "after") {
// After: 기존 액션 즉시 실행 + 백그라운드 제어관리
const immediateResult = await executeOriginalAction(
actionType,
contextData
);
// 제어관리는 백그라운드에서 처리 (실제로는 큐에 추가)
const jobId = `job_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
// TODO: 실제 작업 큐에 추가
processDataflowInBackground(
jobId,
buttonConfig,
contextData,
companyCode,
"normal"
);
const responseTime = Date.now() - startTime;
logger.info(`Button executed (after): ${responseTime}ms`);
res.json({
success: true,
data: {
jobId,
immediateResult,
isBackground: true,
timing: "after",
responseTime,
},
});
} else if (timing === "before") {
// Before: 간단한 검증 후 기존 액션
const isSimpleValidation = checkIfSimpleValidation(buttonConfig);
if (isSimpleValidation) {
// 간단한 검증: 즉시 처리
const validationResult = await validateQuickly(
buttonConfig,
contextData
);
if (!validationResult.success) {
res.json({
success: true,
data: {
jobId: "validation_failed",
immediateResult: validationResult,
timing: "before",
},
});
return;
}
// 검증 통과 시 기존 액션 실행
const actionResult = await executeOriginalAction(
actionType,
contextData
);
const responseTime = Date.now() - startTime;
logger.info(`Button executed (before-simple): ${responseTime}ms`);
res.json({
success: true,
data: {
jobId: "immediate",
immediateResult: actionResult,
timing: "before",
responseTime,
},
});
} else {
// 복잡한 검증: 백그라운드 처리
const jobId = `job_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
// TODO: 실제 작업 큐에 추가 (높은 우선순위)
processDataflowInBackground(
jobId,
buttonConfig,
contextData,
companyCode,
"high"
);
res.json({
success: true,
data: {
jobId,
immediateResult: {
success: true,
message: "검증 중입니다. 잠시만 기다려주세요.",
processing: true,
},
isBackground: true,
timing: "before",
},
});
}
} else if (timing === "replace") {
// Replace: 제어관리만 실행
const isSimpleControl = checkIfSimpleControl(buttonConfig);
if (isSimpleControl) {
// 간단한 제어: 즉시 실행
const result = await executeSimpleDataflowAction(
buttonConfig,
contextData,
companyCode
);
const responseTime = Date.now() - startTime;
logger.info(`Button executed (replace-simple): ${responseTime}ms`);
res.json({
success: true,
data: {
jobId: "immediate",
immediateResult: result,
timing: "replace",
responseTime,
},
});
} else {
// 복잡한 제어: 백그라운드 실행
const jobId = `job_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
// TODO: 실제 작업 큐에 추가
processDataflowInBackground(
jobId,
buttonConfig,
contextData,
companyCode,
"normal"
);
res.json({
success: true,
data: {
jobId,
immediateResult: {
success: true,
message: "사용자 정의 작업을 처리 중입니다...",
processing: true,
},
isBackground: true,
timing: "replace",
},
});
}
}
} catch (error) {
logger.error("Failed to execute optimized button:", error);
res.status(500).json({
success: false,
message: "버튼 실행 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥
*/
export async function executeSimpleDataflow(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { config, contextData } = req.body;
const companyCode = req.user?.companyCode;
if (!companyCode) {
res.status(400).json({
success: false,
message: "회사 코드가 필요합니다.",
});
return;
}
const result = await executeSimpleDataflowAction(
config,
contextData,
companyCode
);
res.json({
success: true,
data: result,
});
} catch (error) {
logger.error("Failed to execute simple dataflow:", error);
res.status(500).json({
success: false,
message: "간단한 제어관리 실행 중 오류가 발생했습니다.",
});
}
}
/**
* 🔥
*/
export async function getJobStatus(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { jobId } = req.params;
// TODO: 실제 작업 큐에서 상태 조회
// 현재는 mock 응답
const mockStatus = {
status: "completed",
result: {
success: true,
executedActions: 2,
message: "백그라운드 처리가 완료되었습니다.",
},
progress: 100,
};
res.json({
success: true,
data: mockStatus,
});
} catch (error) {
logger.error("Failed to get job status:", error);
res.status(500).json({
success: false,
message: "작업 상태 조회 중 오류가 발생했습니다.",
});
}
}
// ============================================================================
// 🔥 헬퍼 함수들
// ============================================================================
/**
* (mock)
*/
async function executeOriginalAction(
actionType: string,
contextData: Record<string, any>
): Promise<any> {
// 간단한 지연 시뮬레이션
await new Promise((resolve) => setTimeout(resolve, 50));
return {
success: true,
message: `${actionType} 액션이 완료되었습니다.`,
actionType,
timestamp: new Date().toISOString(),
data: contextData,
};
}
/**
*
*/
function checkIfSimpleValidation(buttonConfig: any): boolean {
if (buttonConfig?.dataflowConfig?.controlMode !== "advanced") {
return true;
}
const conditions =
buttonConfig?.dataflowConfig?.directControl?.conditions || [];
return (
conditions.length <= 5 &&
conditions.every(
(c: any) =>
c.type === "condition" &&
["=", "!=", ">", "<", ">=", "<="].includes(c.operator || "")
)
);
}
/**
*
*/
function checkIfSimpleControl(buttonConfig: any): boolean {
if (buttonConfig?.dataflowConfig?.controlMode === "simple") {
return true;
}
const actions = buttonConfig?.dataflowConfig?.directControl?.actions || [];
const conditions =
buttonConfig?.dataflowConfig?.directControl?.conditions || [];
return actions.length <= 3 && conditions.length <= 5;
}
/**
*
*/
async function validateQuickly(
buttonConfig: any,
contextData: Record<string, any>
): Promise<any> {
// 간단한 mock 검증
await new Promise((resolve) => setTimeout(resolve, 10));
return {
success: true,
message: "검증이 완료되었습니다.",
};
}
/**
*
*/
async function executeSimpleDataflowAction(
config: any,
contextData: Record<string, any>,
companyCode: string
): Promise<any> {
try {
// 실제로는 EventTriggerService 사용
const result = await EventTriggerService.executeEventTriggers(
"insert", // TODO: 동적으로 결정
"test_table", // TODO: 설정에서 가져오기
contextData,
companyCode
);
return {
success: true,
executedActions: result.length,
message: `${result.length}개의 액션이 실행되었습니다.`,
results: result,
};
} catch (error) {
logger.error("Simple dataflow execution failed:", error);
throw error;
}
}
/**
* ()
*/
function processDataflowInBackground(
jobId: string,
buttonConfig: any,
contextData: Record<string, any>,
companyCode: string,
priority: string = "normal"
): void {
// 실제로는 작업 큐에 추가
// 여기서는 간단한 setTimeout으로 시뮬레이션
setTimeout(async () => {
try {
logger.info(`Background job started: ${jobId}`);
// 실제 제어관리 로직 실행
const result = await executeSimpleDataflowAction(
buttonConfig.dataflowConfig,
contextData,
companyCode
);
logger.info(`Background job completed: ${jobId}`, result);
// 실제로는 WebSocket이나 polling으로 클라이언트에 알림
} catch (error) {
logger.error(`Background job failed: ${jobId}`, error);
}
}, 1000); // 1초 후 실행 시뮬레이션
}
2025-09-29 12:17:10 +09:00
/**
* 🔥 ( )
*/
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",
});
}
}