/** * ๐Ÿ”ฅ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์ปจํŠธ๋กค๋Ÿฌ * * ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ 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 { 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 { 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 { 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 { 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 || []; 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 { 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; } // ๊ด€๊ณ„ ์ •๋ณด ์ฐพ๊ธฐ const relationship = (diagram.relationships as any)?.relationships?.find( (rel: any) => rel.id === relationshipId ); if (!relationship) { res.status(404).json({ success: false, message: "๊ด€๊ณ„๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", }); 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 { 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 { 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 { 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 ): Promise { // ๊ฐ„๋‹จํ•œ ์ง€์—ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ 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 ): Promise { // ๊ฐ„๋‹จํ•œ mock ๊ฒ€์ฆ await new Promise((resolve) => setTimeout(resolve, 10)); return { success: true, message: "๊ฒ€์ฆ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }; } /** * ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹คํ–‰ */ async function executeSimpleDataflowAction( config: any, contextData: Record, companyCode: string ): Promise { 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, 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์ดˆ ํ›„ ์‹คํ–‰ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ }