// @ts-nocheck /** * 플로우 관리 컨트롤러 */ import { Request, Response } from "express"; import { FlowDefinitionService } from "../services/flowDefinitionService"; import { FlowStepService } from "../services/flowStepService"; import { FlowConnectionService } from "../services/flowConnectionService"; import { FlowExecutionService } from "../services/flowExecutionService"; import { FlowDataMoveService } from "../services/flowDataMoveService"; import { FlowProcedureService } from "../services/flowProcedureService"; export class FlowController { private flowDefinitionService: FlowDefinitionService; private flowStepService: FlowStepService; private flowConnectionService: FlowConnectionService; private flowExecutionService: FlowExecutionService; private flowDataMoveService: FlowDataMoveService; private flowProcedureService: FlowProcedureService; constructor() { this.flowDefinitionService = new FlowDefinitionService(); this.flowStepService = new FlowStepService(); this.flowConnectionService = new FlowConnectionService(); this.flowExecutionService = new FlowExecutionService(); this.flowDataMoveService = new FlowDataMoveService(); this.flowProcedureService = new FlowProcedureService(); } // ==================== 플로우 정의 ==================== /** * 플로우 정의 생성 */ createFlowDefinition = async (req: Request, res: Response): Promise => { try { const { name, description, tableName, dbSourceType, dbConnectionId, // REST API 관련 필드 restApiConnectionId, restApiEndpoint, restApiJsonPath, } = req.body; const userId = (req as any).user?.userId || "system"; const userCompanyCode = (req as any).user?.companyCode; if (!name) { res.status(400).json({ success: false, message: "Name is required", }); return; } // REST API 또는 다중 연결인 경우 테이블 존재 확인 스킵 const isRestApi = dbSourceType === "restapi" || dbSourceType === "multi_restapi"; const isMultiConnection = dbSourceType === "multi_restapi" || dbSourceType === "multi_external_db"; // 테이블 이름이 제공된 경우에만 존재 확인 (REST API 및 다중 연결 제외) if (tableName && !isRestApi && !isMultiConnection && !tableName.startsWith("_restapi_") && !tableName.startsWith("_multi_restapi_") && !tableName.startsWith("_multi_external_db_")) { const tableExists = await this.flowDefinitionService.checkTableExists(tableName); if (!tableExists) { res.status(400).json({ success: false, message: `Table '${tableName}' does not exist`, }); return; } } const flowDef = await this.flowDefinitionService.create( { name, description, tableName, dbSourceType, dbConnectionId, restApiConnectionId, restApiEndpoint, restApiJsonPath, restApiConnections: req.body.restApiConnections, // 다중 REST API 설정 }, userId, userCompanyCode ); res.json({ success: true, data: flowDef, }); } catch (error: any) { console.error("Error creating flow definition:", error); res.status(500).json({ success: false, message: error.message || "Failed to create flow definition", }); } }; /** * 플로우 정의 목록 조회 */ getFlowDefinitions = async (req: Request, res: Response): Promise => { try { const { tableName, isActive } = req.query; const user = (req as any).user; const userCompanyCode = user?.companyCode; const flows = await this.flowDefinitionService.findAll( tableName as string | undefined, isActive !== undefined ? isActive === "true" : undefined, userCompanyCode ); res.json({ success: true, data: flows, }); } catch (error: any) { console.error("Error fetching flow definitions:", error); res.status(500).json({ success: false, message: error.message || "Failed to fetch flow definitions", }); } }; /** * 플로우 정의 상세 조회 (단계 및 연결 포함) */ getFlowDefinitionDetail = async ( req: Request, res: Response ): Promise => { try { const { id } = req.params; const flowId = parseInt(id); const userCompanyCode = (req as any).user?.companyCode; const definition = await this.flowDefinitionService.findById(flowId, userCompanyCode); if (!definition) { res.status(404).json({ success: false, message: "Flow definition not found", }); return; } const steps = await this.flowStepService.findByFlowId(flowId); const connections = await this.flowConnectionService.findByFlowId(flowId); res.json({ success: true, data: { definition, steps, connections, }, }); } catch (error: any) { console.error("Error fetching flow definition detail:", error); res.status(500).json({ success: false, message: error.message || "Failed to fetch flow definition detail", }); } }; /** * 플로우 정의 수정 */ updateFlowDefinition = async (req: Request, res: Response): Promise => { try { const { id } = req.params; const flowId = parseInt(id); const { name, description, isActive } = req.body; const userCompanyCode = (req as any).user?.companyCode; const flowDef = await this.flowDefinitionService.update(flowId, { name, description, isActive, }, userCompanyCode); if (!flowDef) { res.status(404).json({ success: false, message: "Flow definition not found", }); return; } res.json({ success: true, data: flowDef, }); } catch (error: any) { console.error("Error updating flow definition:", error); res.status(500).json({ success: false, message: error.message || "Failed to update flow definition", }); } }; /** * 플로우 정의 삭제 */ deleteFlowDefinition = async (req: Request, res: Response): Promise => { try { const { id } = req.params; const flowId = parseInt(id); const userCompanyCode = (req as any).user?.companyCode; const success = await this.flowDefinitionService.delete(flowId, userCompanyCode); if (!success) { res.status(404).json({ success: false, message: "Flow definition not found", }); return; } res.json({ success: true, message: "Flow definition deleted successfully", }); } catch (error: any) { console.error("Error deleting flow definition:", error); res.status(500).json({ success: false, message: error.message || "Failed to delete flow definition", }); } }; // ==================== 플로우 단계 ==================== /** * 플로우 단계 목록 조회 */ getFlowSteps = async (req: Request, res: Response): Promise => { try { const { flowId } = req.params; const flowDefinitionId = parseInt(flowId); const steps = await this.flowStepService.findByFlowId(flowDefinitionId); res.json({ success: true, data: steps, }); return; } catch (error: any) { console.error("Error fetching flow steps:", error); res.status(500).json({ success: false, message: error.message || "Failed to fetch flow steps", }); return; } }; /** * 플로우 단계 생성 */ createFlowStep = async (req: Request, res: Response): Promise => { try { const { flowId } = req.params; const flowDefinitionId = parseInt(flowId); const userCompanyCode = (req as any).user?.companyCode; const { stepName, stepOrder, tableName, conditionJson, color, positionX, positionY, } = req.body; if (!stepName || stepOrder === undefined) { res.status(400).json({ success: false, message: "stepName and stepOrder are required", }); return; } // 플로우 소유권 검증 const flowDef = await this.flowDefinitionService.findById(flowDefinitionId, userCompanyCode); if (!flowDef) { res.status(404).json({ success: false, message: "Flow definition not found or access denied", }); return; } const step = await this.flowStepService.create({ flowDefinitionId, stepName, stepOrder, tableName, conditionJson, color, positionX, positionY, }); res.json({ success: true, data: step, }); } catch (error: any) { console.error("Error creating flow step:", error); res.status(500).json({ success: false, message: error.message || "Failed to create flow step", }); } }; /** * 플로우 단계 수정 */ updateFlowStep = async (req: Request, res: Response): Promise => { try { const { stepId } = req.params; const id = parseInt(stepId); const userCompanyCode = (req as any).user?.companyCode; const { stepName, stepOrder, tableName, conditionJson, color, positionX, positionY, moveType, statusColumn, statusValue, targetTable, fieldMappings, integrationType, integrationConfig, displayConfig, } = req.body; // 스텝 소유권 검증: 스텝이 속한 플로우가 사용자 회사 소유인지 확인 const existingStep = await this.flowStepService.findById(id); if (existingStep) { const flowDef = await this.flowDefinitionService.findById(existingStep.flowDefinitionId, userCompanyCode); if (!flowDef) { res.status(403).json({ success: false, message: "Access denied: flow does not belong to your company", }); return; } } const step = await this.flowStepService.update(id, { stepName, stepOrder, tableName, conditionJson, color, positionX, positionY, moveType, statusColumn, statusValue, targetTable, fieldMappings, integrationType, integrationConfig, displayConfig, }); if (!step) { res.status(404).json({ success: false, message: "Flow step not found", }); return; } res.json({ success: true, data: step, }); } catch (error: any) { console.error("Error updating flow step:", error); res.status(500).json({ success: false, message: error.message || "Failed to update flow step", }); } }; /** * 플로우 단계 삭제 */ deleteFlowStep = async (req: Request, res: Response): Promise => { try { const { stepId } = req.params; const id = parseInt(stepId); const userCompanyCode = (req as any).user?.companyCode; // 스텝 소유권 검증 const existingStep = await this.flowStepService.findById(id); if (existingStep) { const flowDef = await this.flowDefinitionService.findById(existingStep.flowDefinitionId, userCompanyCode); if (!flowDef) { res.status(403).json({ success: false, message: "Access denied: flow does not belong to your company", }); return; } } const success = await this.flowStepService.delete(id); if (!success) { res.status(404).json({ success: false, message: "Flow step not found", }); return; } res.json({ success: true, message: "Flow step deleted successfully", }); } catch (error: any) { console.error("Error deleting flow step:", error); res.status(500).json({ success: false, message: error.message || "Failed to delete flow step", }); } }; // ==================== 플로우 연결 ==================== /** * 플로우 연결 목록 조회 */ getFlowConnections = async (req: Request, res: Response): Promise => { try { const { flowId } = req.params; const flowDefinitionId = parseInt(flowId); const connections = await this.flowConnectionService.findByFlowId(flowDefinitionId); res.json({ success: true, data: connections, }); return; } catch (error: any) { console.error("Error fetching flow connections:", error); res.status(500).json({ success: false, message: error.message || "Failed to fetch flow connections", }); return; } }; /** * 플로우 연결 생성 */ createConnection = async (req: Request, res: Response): Promise => { try { const { flowDefinitionId, fromStepId, toStepId, label } = req.body; const userCompanyCode = (req as any).user?.companyCode; if (!flowDefinitionId || !fromStepId || !toStepId) { res.status(400).json({ success: false, message: "flowDefinitionId, fromStepId, and toStepId are required", }); return; } // 플로우 소유권 검증 const flowDef = await this.flowDefinitionService.findById(flowDefinitionId, userCompanyCode); if (!flowDef) { res.status(404).json({ success: false, message: "Flow definition not found or access denied", }); return; } // fromStepId, toStepId가 해당 flow에 속하는지 검증 const fromStep = await this.flowStepService.findById(fromStepId); const toStep = await this.flowStepService.findById(toStepId); if (!fromStep || fromStep.flowDefinitionId !== flowDefinitionId || !toStep || toStep.flowDefinitionId !== flowDefinitionId) { res.status(400).json({ success: false, message: "fromStepId and toStepId must belong to the specified flow", }); return; } const connection = await this.flowConnectionService.create({ flowDefinitionId, fromStepId, toStepId, label, }); res.json({ success: true, data: connection, }); } catch (error: any) { console.error("Error creating connection:", error); res.status(500).json({ success: false, message: error.message || "Failed to create connection", }); } }; /** * 플로우 연결 삭제 */ deleteConnection = async (req: Request, res: Response): Promise => { try { const { connectionId } = req.params; const id = parseInt(connectionId); const userCompanyCode = (req as any).user?.companyCode; // 연결 소유권 검증 const existingConn = await this.flowConnectionService.findById(id); if (existingConn) { const flowDef = await this.flowDefinitionService.findById(existingConn.flowDefinitionId, userCompanyCode); if (!flowDef) { res.status(403).json({ success: false, message: "Access denied: flow does not belong to your company", }); return; } } const success = await this.flowConnectionService.delete(id); if (!success) { res.status(404).json({ success: false, message: "Connection not found", }); return; } res.json({ success: true, message: "Connection deleted successfully", }); } catch (error: any) { console.error("Error deleting connection:", error); res.status(500).json({ success: false, message: error.message || "Failed to delete connection", }); } }; // ==================== 플로우 실행 ==================== /** * 단계별 데이터 카운트 조회 */ getStepDataCount = async (req: Request, res: Response): Promise => { try { const { flowId, stepId } = req.params; const count = await this.flowExecutionService.getStepDataCount( parseInt(flowId), parseInt(stepId) ); res.json({ success: true, data: { count }, }); } catch (error: any) { console.error("Error getting step data count:", error); res.status(500).json({ success: false, message: error.message || "Failed to get step data count", }); } }; /** * 단계별 데이터 리스트 조회 */ getStepDataList = async (req: Request, res: Response): Promise => { try { const { flowId, stepId } = req.params; const { page = 1, pageSize = 20 } = req.query; const data = await this.flowExecutionService.getStepDataList( parseInt(flowId), parseInt(stepId), parseInt(page as string), parseInt(pageSize as string) ); res.json({ success: true, data, }); } catch (error: any) { console.error("Error getting step data list:", error); res.status(500).json({ success: false, message: error.message || "Failed to get step data list", }); } }; /** * 플로우 스텝의 컬럼 라벨 조회 */ getStepColumnLabels = async (req: Request, res: Response): Promise => { try { const { flowId, stepId } = req.params; const step = await this.flowStepService.findById(parseInt(stepId)); if (!step) { res.status(404).json({ success: false, message: "Step not found", }); return; } const flowDef = await this.flowDefinitionService.findById( parseInt(flowId) ); if (!flowDef) { res.status(404).json({ success: false, message: "Flow definition not found", }); return; } // 테이블명 결정 (스텝 테이블 우선, 없으면 플로우 테이블) const tableName = step.tableName || flowDef.tableName; if (!tableName) { res.json({ success: true, data: {}, }); return; } // table_type_columns 테이블에서 라벨 정보 조회 const { query } = await import("../database/db"); const labelRows = await query<{ column_name: string; column_label: string | null; }>( `SELECT column_name, column_label FROM table_type_columns WHERE table_name = $1 AND column_label IS NOT NULL AND company_code = '*'`, [tableName] ); // { columnName: label } 형태의 객체로 변환 const labels: Record = {}; labelRows.forEach((row) => { if (row.column_label) { labels[row.column_name] = row.column_label; } }); res.json({ success: true, data: labels, }); } catch (error: any) { console.error("❌ [FlowController] 컬럼 라벨 조회 오류:", error); res.status(500).json({ success: false, message: error.message || "Failed to get step column labels", }); } }; /** * 플로우의 모든 단계별 카운트 조회 */ getAllStepCounts = async (req: Request, res: Response): Promise => { try { const { flowId } = req.params; const counts = await this.flowExecutionService.getAllStepCounts( parseInt(flowId) ); res.json({ success: true, data: counts, }); } catch (error: any) { console.error("Error getting all step counts:", error); res.status(500).json({ success: false, message: error.message || "Failed to get all step counts", }); } }; /** * 데이터를 다음 단계로 이동 */ moveData = async (req: Request, res: Response): Promise => { try { const { flowId, fromStepId, recordId, toStepId, note } = req.body; const userId = (req as any).user?.userId || "system"; if (!flowId || !fromStepId || !recordId || !toStepId) { res.status(400).json({ success: false, message: "flowId, fromStepId, recordId, and toStepId are required", }); return; } await this.flowDataMoveService.moveDataToStep( flowId, fromStepId, toStepId, recordId, userId, note ? { note } : undefined ); res.json({ success: true, message: "Data moved successfully", }); } catch (error: any) { console.error("Error moving data:", error); res.status(500).json({ success: false, message: error.message || "Failed to move data", }); } }; /** * 여러 데이터를 동시에 이동 */ moveBatchData = async (req: Request, res: Response): Promise => { try { const { flowId, fromStepId, toStepId, dataIds } = req.body; const userId = (req as any).user?.userId || "system"; if ( !flowId || !fromStepId || !toStepId || !dataIds || !Array.isArray(dataIds) ) { res.status(400).json({ success: false, message: "flowId, fromStepId, toStepId, and dataIds (array) are required", }); return; } const result = await this.flowDataMoveService.moveBatchData( flowId, fromStepId, toStepId, dataIds, userId ); const successCount = result.results.filter((r) => r.success).length; const failureCount = result.results.filter((r) => !r.success).length; res.json({ success: result.success, message: result.success ? `${successCount}건의 데이터를 성공적으로 이동했습니다` : `${successCount}건 성공, ${failureCount}건 실패`, data: { successCount, failureCount, total: dataIds.length, }, results: result.results, }); } catch (error: any) { console.error("Error moving batch data:", error); res.status(500).json({ success: false, message: error.message || "Failed to move batch data", }); } }; /** * 데이터의 플로우 이력 조회 */ getAuditLogs = async (req: Request, res: Response): Promise => { try { const { flowId, recordId } = req.params; const logs = await this.flowDataMoveService.getAuditLogs( parseInt(flowId), recordId ); res.json({ success: true, data: logs, }); } catch (error: any) { console.error("Error getting audit logs:", error); res.status(500).json({ success: false, message: error.message || "Failed to get audit logs", }); } }; /** * 플로우의 모든 이력 조회 */ getFlowAuditLogs = async (req: Request, res: Response): Promise => { try { const { flowId } = req.params; const { limit = 100 } = req.query; const logs = await this.flowDataMoveService.getFlowAuditLogs( parseInt(flowId), parseInt(limit as string) ); res.json({ success: true, data: logs, }); } catch (error: any) { console.error("Error getting flow audit logs:", error); res.status(500).json({ success: false, message: error.message || "Failed to get flow audit logs", }); } }; /** * 스텝 데이터 업데이트 (인라인 편집) */ updateStepData = async (req: Request, res: Response): Promise => { try { const { flowId, stepId, recordId } = req.params; const updateData = req.body; const userId = (req as any).user?.userId || "system"; const userCompanyCode = (req as any).user?.companyCode; if (!flowId || !stepId || !recordId) { res.status(400).json({ success: false, message: "flowId, stepId, and recordId are required", }); return; } if (!updateData || Object.keys(updateData).length === 0) { res.status(400).json({ success: false, message: "Update data is required", }); return; } const result = await this.flowExecutionService.updateStepData( parseInt(flowId), parseInt(stepId), recordId, updateData, userId, userCompanyCode ); res.json({ success: true, message: "Data updated successfully", data: result, }); } catch (error: any) { console.error("Error updating step data:", error); res.status(500).json({ success: false, message: error.message || "Failed to update step data", }); } }; // ==================== 프로시저/함수 ==================== /** * 프로시저/함수 목록 조회 */ listProcedures = async (req: Request, res: Response): Promise => { try { const dbSource = (req.query.dbSource as string) || "internal"; const connectionId = req.query.connectionId ? parseInt(req.query.connectionId as string) : undefined; const schema = req.query.schema as string | undefined; if (dbSource !== "internal" && dbSource !== "external") { res.status(400).json({ success: false, message: "dbSource는 internal 또는 external이어야 합니다", }); return; } if (dbSource === "external" && !connectionId) { res.status(400).json({ success: false, message: "외부 DB 조회 시 connectionId가 필요합니다", }); return; } const procedures = await this.flowProcedureService.listProcedures( dbSource, connectionId, schema ); res.json({ success: true, data: procedures }); } catch (error: any) { console.error("프로시저 목록 조회 실패:", error); res.status(500).json({ success: false, message: error.message || "프로시저 목록 조회에 실패했습니다", }); } }; /** * 프로시저/함수 파라미터 조회 */ getProcedureParameters = async (req: Request, res: Response): Promise => { try { const { name } = req.params; const dbSource = (req.query.dbSource as string) || "internal"; const connectionId = req.query.connectionId ? parseInt(req.query.connectionId as string) : undefined; const schema = req.query.schema as string | undefined; if (!name) { res.status(400).json({ success: false, message: "프로시저 이름이 필요합니다", }); return; } if (dbSource !== "internal" && dbSource !== "external") { res.status(400).json({ success: false, message: "dbSource는 internal 또는 external이어야 합니다", }); return; } const parameters = await this.flowProcedureService.getProcedureParameters( name, dbSource as "internal" | "external", connectionId, schema ); res.json({ success: true, data: parameters }); } catch (error: any) { console.error("프로시저 파라미터 조회 실패:", error); res.status(500).json({ success: false, message: error.message || "프로시저 파라미터 조회에 실패했습니다", }); } }; }