From 036380d2672e071257d8c01623873968c590ab6c Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 12 Dec 2025 18:28:58 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=A4=EC=A4=91=20=EC=A0=9C=EC=96=B4?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/routes/cascadingAutoFillRoutes.ts | 1 + .../src/routes/cascadingConditionRoutes.ts | 1 + .../src/routes/cascadingHierarchyRoutes.ts | 1 + .../routes/cascadingMutualExclusionRoutes.ts | 1 + .../src/services/dynamicFormService.ts | 349 ++++++++++++---- docs/노드플로우_개선사항.md | 1 + docs/메일발송_기능_사용_가이드.md | 1 + .../ImprovedButtonControlConfigPanel.tsx | 381 ++++++++++++------ frontend/hooks/useAutoFill.ts | 1 + frontend/lib/utils/buttonActions.ts | 115 +++++- ..._임베딩_및_데이터_전달_시스템_구현_계획서.md | 1 + 화면_임베딩_시스템_Phase1-4_구현_완료.md | 1 + 화면_임베딩_시스템_충돌_분석_보고서.md | 1 + 13 files changed, 641 insertions(+), 214 deletions(-) diff --git a/backend-node/src/routes/cascadingAutoFillRoutes.ts b/backend-node/src/routes/cascadingAutoFillRoutes.ts index 5d922dd6..de4eb913 100644 --- a/backend-node/src/routes/cascadingAutoFillRoutes.ts +++ b/backend-node/src/routes/cascadingAutoFillRoutes.ts @@ -50,3 +50,4 @@ router.get("/data/:groupCode", getAutoFillData); export default router; + diff --git a/backend-node/src/routes/cascadingConditionRoutes.ts b/backend-node/src/routes/cascadingConditionRoutes.ts index 813dbff1..c2f12782 100644 --- a/backend-node/src/routes/cascadingConditionRoutes.ts +++ b/backend-node/src/routes/cascadingConditionRoutes.ts @@ -46,3 +46,4 @@ router.get("/filtered-options/:relationCode", getFilteredOptions); export default router; + diff --git a/backend-node/src/routes/cascadingHierarchyRoutes.ts b/backend-node/src/routes/cascadingHierarchyRoutes.ts index be37da49..71e6c418 100644 --- a/backend-node/src/routes/cascadingHierarchyRoutes.ts +++ b/backend-node/src/routes/cascadingHierarchyRoutes.ts @@ -62,3 +62,4 @@ router.get("/:groupCode/options/:levelOrder", getLevelOptions); export default router; + diff --git a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts index 46bbf427..d92d7d72 100644 --- a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts +++ b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts @@ -50,3 +50,4 @@ router.get("/options/:exclusionCode", getExcludedOptions); export default router; + diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 65efcd1b..7ec95626 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -903,7 +903,7 @@ export class DynamicFormService { return `${key} = $${index + 1}::numeric`; } else if (dataType === "boolean") { return `${key} = $${index + 1}::boolean`; - } else if (dataType === 'jsonb' || dataType === 'json') { + } else if (dataType === "jsonb" || dataType === "json") { // 🆕 JSONB/JSON 타입은 명시적 캐스팅 return `${key} = $${index + 1}::jsonb`; } else { @@ -917,9 +917,13 @@ export class DynamicFormService { const values: any[] = Object.keys(changedFields).map((key) => { const value = changedFields[key]; const dataType = columnTypes[key]; - + // JSONB/JSON 타입이고 배열/객체인 경우 JSON 문자열로 변환 - if ((dataType === 'jsonb' || dataType === 'json') && (Array.isArray(value) || (typeof value === 'object' && value !== null))) { + if ( + (dataType === "jsonb" || dataType === "json") && + (Array.isArray(value) || + (typeof value === "object" && value !== null)) + ) { return JSON.stringify(value); } return value; @@ -1588,6 +1592,7 @@ export class DynamicFormService { /** * 제어관리 실행 (화면에 설정된 경우) + * 다중 제어를 순서대로 순차 실행 지원 */ private async executeDataflowControlIfConfigured( screenId: number, @@ -1629,105 +1634,67 @@ export class DynamicFormService { hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig, hasDiagramId: !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId, + hasFlowControls: + !!properties?.webTypeConfig?.dataflowConfig?.flowControls, }); // 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우 if ( properties?.componentType === "button-primary" && properties?.componentConfig?.action?.type === "save" && - properties?.webTypeConfig?.enableDataflowControl === true && - properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId + properties?.webTypeConfig?.enableDataflowControl === true ) { - controlConfigFound = true; - const diagramId = - properties.webTypeConfig.dataflowConfig.selectedDiagramId; - const relationshipId = - properties.webTypeConfig.dataflowConfig.selectedRelationshipId; + const dataflowConfig = properties?.webTypeConfig?.dataflowConfig; - console.log(`🎯 제어관리 설정 발견:`, { - componentId: layout.component_id, - diagramId, - relationshipId, - triggerType, - }); + // 다중 제어 설정 확인 (flowControls 배열) + const flowControls = dataflowConfig?.flowControls || []; - // 노드 플로우 실행 (relationshipId가 없는 경우 노드 플로우로 간주) - let controlResult: any; + // flowControls가 있으면 다중 제어 실행, 없으면 기존 단일 제어 실행 + if (flowControls.length > 0) { + controlConfigFound = true; + console.log(`🎯 다중 제어관리 설정 발견: ${flowControls.length}개`); - if (!relationshipId) { - // 노드 플로우 실행 - console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`); - const { NodeFlowExecutionService } = await import( - "./nodeFlowExecutionService" + // 순서대로 정렬 + const sortedControls = [...flowControls].sort( + (a: any, b: any) => (a.order || 0) - (b.order || 0) ); - const executionResult = await NodeFlowExecutionService.executeFlow( + // 다중 제어 순차 실행 + await this.executeMultipleFlowControls( + sortedControls, + savedData, + screenId, + tableName, + triggerType, + userId, + companyCode + ); + } else if (dataflowConfig?.selectedDiagramId) { + // 기존 단일 제어 실행 (하위 호환성) + controlConfigFound = true; + const diagramId = dataflowConfig.selectedDiagramId; + const relationshipId = dataflowConfig.selectedRelationshipId; + + console.log(`🎯 단일 제어관리 설정 발견:`, { + componentId: layout.component_id, diagramId, - { - sourceData: [savedData], - dataSourceType: "formData", - buttonId: "save-button", - screenId: screenId, - userId: userId, - companyCode: companyCode, - formData: savedData, - } - ); + relationshipId, + triggerType, + }); - controlResult = { - success: executionResult.success, - message: executionResult.message, - executedActions: executionResult.nodes?.map((node) => ({ - nodeId: node.nodeId, - status: node.status, - duration: node.duration, - })), - errors: executionResult.nodes - ?.filter((node) => node.status === "failed") - .map((node) => node.error || "실행 실패"), - }; - } else { - // 관계 기반 제어관리 실행 - console.log( - `🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})` + await this.executeSingleFlowControl( + diagramId, + relationshipId, + savedData, + screenId, + tableName, + triggerType, + userId, + companyCode ); - controlResult = - await this.dataflowControlService.executeDataflowControl( - diagramId, - relationshipId, - triggerType, - savedData, - tableName, - userId - ); } - console.log(`🎯 제어관리 실행 결과:`, controlResult); - - if (controlResult.success) { - console.log(`✅ 제어관리 실행 성공: ${controlResult.message}`); - if ( - controlResult.executedActions && - controlResult.executedActions.length > 0 - ) { - console.log(`📊 실행된 액션들:`, controlResult.executedActions); - } - - // 오류가 있는 경우 경고 로그 출력 (성공이지만 일부 액션 실패) - if (controlResult.errors && controlResult.errors.length > 0) { - console.warn( - `⚠️ 제어관리 실행 중 일부 오류 발생:`, - controlResult.errors - ); - // 오류 정보를 별도로 저장하여 필요시 사용자에게 알림 가능 - // 현재는 로그만 출력하고 메인 저장 프로세스는 계속 진행 - } - } else { - console.warn(`⚠️ 제어관리 실행 실패: ${controlResult.message}`); - // 제어관리 실패는 메인 저장 프로세스에 영향을 주지 않음 - } - - // 첫 번째 설정된 제어관리만 실행 (여러 개가 있을 경우) + // 첫 번째 설정된 버튼의 제어관리만 실행 break; } } @@ -1741,6 +1708,218 @@ export class DynamicFormService { } } + /** + * 다중 제어 순차 실행 + */ + private async executeMultipleFlowControls( + flowControls: Array<{ + id: string; + flowId: number; + flowName: string; + executionTiming: string; + order: number; + }>, + savedData: Record, + screenId: number, + tableName: string, + triggerType: "insert" | "update" | "delete", + userId: string, + companyCode: string + ): Promise { + console.log(`🚀 다중 제어 순차 실행 시작: ${flowControls.length}개`); + + const { NodeFlowExecutionService } = await import( + "./nodeFlowExecutionService" + ); + + const results: Array<{ + order: number; + flowId: number; + flowName: string; + success: boolean; + message: string; + duration: number; + }> = []; + + for (let i = 0; i < flowControls.length; i++) { + const control = flowControls[i]; + const startTime = Date.now(); + + console.log( + `\n📍 [${i + 1}/${flowControls.length}] 제어 실행: ${control.flowName} (flowId: ${control.flowId})` + ); + + try { + // 유효하지 않은 flowId 스킵 + if (!control.flowId || control.flowId <= 0) { + console.warn(`⚠️ 유효하지 않은 flowId, 스킵: ${control.flowId}`); + results.push({ + order: control.order, + flowId: control.flowId, + flowName: control.flowName, + success: false, + message: "유효하지 않은 flowId", + duration: 0, + }); + continue; + } + + const executionResult = await NodeFlowExecutionService.executeFlow( + control.flowId, + { + sourceData: [savedData], + dataSourceType: "formData", + buttonId: "save-button", + screenId: screenId, + userId: userId, + companyCode: companyCode, + formData: savedData, + } + ); + + const duration = Date.now() - startTime; + + results.push({ + order: control.order, + flowId: control.flowId, + flowName: control.flowName, + success: executionResult.success, + message: executionResult.message, + duration, + }); + + if (executionResult.success) { + console.log( + `✅ [${i + 1}/${flowControls.length}] 제어 성공: ${control.flowName} (${duration}ms)` + ); + } else { + console.error( + `❌ [${i + 1}/${flowControls.length}] 제어 실패: ${control.flowName} - ${executionResult.message}` + ); + // 이전 제어 실패 시 다음 제어 실행 중단 + console.warn(`⚠️ 이전 제어 실패로 인해 나머지 제어 실행 중단`); + break; + } + } catch (error: any) { + const duration = Date.now() - startTime; + console.error( + `❌ [${i + 1}/${flowControls.length}] 제어 실행 오류: ${control.flowName}`, + error + ); + + results.push({ + order: control.order, + flowId: control.flowId, + flowName: control.flowName, + success: false, + message: error.message || "실행 오류", + duration, + }); + + // 오류 발생 시 다음 제어 실행 중단 + console.warn(`⚠️ 제어 실행 오류로 인해 나머지 제어 실행 중단`); + break; + } + } + + // 실행 결과 요약 + const successCount = results.filter((r) => r.success).length; + const failCount = results.filter((r) => !r.success).length; + const totalDuration = results.reduce((sum, r) => sum + r.duration, 0); + + console.log(`\n📊 다중 제어 실행 완료:`, { + total: flowControls.length, + executed: results.length, + success: successCount, + failed: failCount, + totalDuration: `${totalDuration}ms`, + }); + } + + /** + * 단일 제어 실행 (기존 로직, 하위 호환성) + */ + private async executeSingleFlowControl( + diagramId: number, + relationshipId: string | null, + savedData: Record, + screenId: number, + tableName: string, + triggerType: "insert" | "update" | "delete", + userId: string, + companyCode: string + ): Promise { + let controlResult: any; + + if (!relationshipId) { + // 노드 플로우 실행 + console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`); + const { NodeFlowExecutionService } = await import( + "./nodeFlowExecutionService" + ); + + const executionResult = await NodeFlowExecutionService.executeFlow( + diagramId, + { + sourceData: [savedData], + dataSourceType: "formData", + buttonId: "save-button", + screenId: screenId, + userId: userId, + companyCode: companyCode, + formData: savedData, + } + ); + + controlResult = { + success: executionResult.success, + message: executionResult.message, + executedActions: executionResult.nodes?.map((node) => ({ + nodeId: node.nodeId, + status: node.status, + duration: node.duration, + })), + errors: executionResult.nodes + ?.filter((node) => node.status === "failed") + .map((node) => node.error || "실행 실패"), + }; + } else { + // 관계 기반 제어관리 실행 + console.log( + `🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})` + ); + controlResult = await this.dataflowControlService.executeDataflowControl( + diagramId, + relationshipId, + triggerType, + savedData, + tableName, + userId + ); + } + + console.log(`🎯 제어관리 실행 결과:`, controlResult); + + if (controlResult.success) { + console.log(`✅ 제어관리 실행 성공: ${controlResult.message}`); + if ( + controlResult.executedActions && + controlResult.executedActions.length > 0 + ) { + console.log(`📊 실행된 액션들:`, controlResult.executedActions); + } + + if (controlResult.errors && controlResult.errors.length > 0) { + console.warn( + `⚠️ 제어관리 실행 중 일부 오류 발생:`, + controlResult.errors + ); + } + } else { + console.warn(`⚠️ 제어관리 실행 실패: ${controlResult.message}`); + } + } + /** * 특정 테이블의 특정 필드 값만 업데이트 * (다른 테이블의 레코드 업데이트 지원) diff --git a/docs/노드플로우_개선사항.md b/docs/노드플로우_개선사항.md index e80a1a61..985d730a 100644 --- a/docs/노드플로우_개선사항.md +++ b/docs/노드플로우_개선사항.md @@ -582,3 +582,4 @@ const result = await executeNodeFlow(flowId, { + diff --git a/docs/메일발송_기능_사용_가이드.md b/docs/메일발송_기능_사용_가이드.md index 2ef68524..285dc6ba 100644 --- a/docs/메일발송_기능_사용_가이드.md +++ b/docs/메일발송_기능_사용_가이드.md @@ -355,3 +355,4 @@ - [ ] 부모 화면에서 모달로 데이터가 전달되는가? - [ ] 발송 버튼의 데이터 소스가 올바르게 설정되어 있는가? + diff --git a/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx b/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx index 18a51bba..197b9759 100644 --- a/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx @@ -1,11 +1,14 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Separator } from "@/components/ui/separator"; -import { Settings, Clock, Info, Workflow } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Settings, Clock, Info, Workflow, Plus, Trash2, GripVertical, ChevronUp, ChevronDown } from "lucide-react"; import { ComponentData } from "@/types/screen"; import { getNodeFlows, NodeFlow } from "@/lib/api/nodeFlows"; @@ -14,11 +17,22 @@ interface ImprovedButtonControlConfigPanelProps { onUpdateProperty: (path: string, value: any) => void; } +// 다중 제어 설정 인터페이스 +interface FlowControlConfig { + id: string; + flowId: number; + flowName: string; + executionTiming: "before" | "after" | "replace"; + order: number; +} + /** - * 🔥 단순화된 버튼 제어 설정 패널 + * 🔥 다중 제어 지원 버튼 설정 패널 * - * 노드 플로우 실행만 지원: - * - 플로우 선택 및 실행 타이밍 설정 + * 기능: + * - 여러 개의 노드 플로우 선택 및 순서 지정 + * - 각 플로우별 실행 타이밍 설정 + * - 드래그앤드롭 또는 버튼으로 순서 변경 */ export const ImprovedButtonControlConfigPanel: React.FC = ({ component, @@ -27,6 +41,9 @@ export const ImprovedButtonControlConfigPanel: React.FC([]); const [loading, setLoading] = useState(false); @@ -58,24 +75,118 @@ export const ImprovedButtonControlConfigPanel: React.FC { - const selectedFlow = flows.find((f) => f.flowId.toString() === flowId); - if (selectedFlow) { - // 전체 dataflowConfig 업데이트 (selectedDiagramId 포함) - onUpdateProperty("webTypeConfig.dataflowConfig", { - ...dataflowConfig, - selectedDiagramId: selectedFlow.flowId, // 백엔드에서 사용 - selectedRelationshipId: null, // 노드 플로우는 관계 ID 불필요 - flowConfig: { - flowId: selectedFlow.flowId, - flowName: selectedFlow.flowName, - executionTiming: "before", // 기본값 - contextData: {}, - }, - }); - } + const handleAddControl = useCallback(() => { + const newControl: FlowControlConfig = { + id: `control_${Date.now()}`, + flowId: 0, + flowName: "", + executionTiming: "after", + order: flowControls.length + 1, + }; + + const updatedControls = [...flowControls, newControl]; + updateFlowControls(updatedControls); + }, [flowControls]); + + /** + * 🔥 제어 삭제 + */ + const handleRemoveControl = useCallback( + (controlId: string) => { + const updatedControls = flowControls + .filter((c) => c.id !== controlId) + .map((c, index) => ({ ...c, order: index + 1 })); + updateFlowControls(updatedControls); + }, + [flowControls], + ); + + /** + * 🔥 제어 플로우 선택 + */ + const handleFlowSelect = useCallback( + (controlId: string, flowId: string) => { + const selectedFlow = flows.find((f) => f.flowId.toString() === flowId); + if (selectedFlow) { + const updatedControls = flowControls.map((c) => + c.id === controlId ? { ...c, flowId: selectedFlow.flowId, flowName: selectedFlow.flowName } : c, + ); + updateFlowControls(updatedControls); + } + }, + [flows, flowControls], + ); + + /** + * 🔥 실행 타이밍 변경 + */ + const handleTimingChange = useCallback( + (controlId: string, timing: "before" | "after" | "replace") => { + const updatedControls = flowControls.map((c) => (c.id === controlId ? { ...c, executionTiming: timing } : c)); + updateFlowControls(updatedControls); + }, + [flowControls], + ); + + /** + * 🔥 순서 위로 이동 + */ + const handleMoveUp = useCallback( + (controlId: string) => { + const index = flowControls.findIndex((c) => c.id === controlId); + if (index > 0) { + const updatedControls = [...flowControls]; + [updatedControls[index - 1], updatedControls[index]] = [updatedControls[index], updatedControls[index - 1]]; + // 순서 번호 재정렬 + updatedControls.forEach((c, i) => (c.order = i + 1)); + updateFlowControls(updatedControls); + } + }, + [flowControls], + ); + + /** + * 🔥 순서 아래로 이동 + */ + const handleMoveDown = useCallback( + (controlId: string) => { + const index = flowControls.findIndex((c) => c.id === controlId); + if (index < flowControls.length - 1) { + const updatedControls = [...flowControls]; + [updatedControls[index], updatedControls[index + 1]] = [updatedControls[index + 1], updatedControls[index]]; + // 순서 번호 재정렬 + updatedControls.forEach((c, i) => (c.order = i + 1)); + updateFlowControls(updatedControls); + } + }, + [flowControls], + ); + + /** + * 🔥 제어 목록 업데이트 (백엔드 호환성 유지) + */ + const updateFlowControls = (controls: FlowControlConfig[]) => { + // 첫 번째 제어를 기존 형식으로도 저장 (하위 호환성) + const firstValidControl = controls.find((c) => c.flowId > 0); + + onUpdateProperty("webTypeConfig.dataflowConfig", { + ...dataflowConfig, + // 기존 형식 (하위 호환성) + selectedDiagramId: firstValidControl?.flowId || null, + selectedRelationshipId: null, + flowConfig: firstValidControl + ? { + flowId: firstValidControl.flowId, + flowName: firstValidControl.flowName, + executionTiming: firstValidControl.executionTiming, + contextData: {}, + } + : null, + // 새로운 다중 제어 형식 + flowControls: controls, + }); }; return ( @@ -98,32 +209,57 @@ export const ImprovedButtonControlConfigPanel: React.FC - + {/* 제어 목록 헤더 */} +
+
+ + +
+ +
- {dataflowConfig.flowConfig && ( -
- - - onUpdateProperty("webTypeConfig.dataflowConfig.flowConfig.executionTiming", timing) - } - /> + {/* 제어 목록 */} + {flowControls.length === 0 ? ( +
+ +

등록된 제어가 없습니다

+ +
+ ) : ( +
+ {flowControls.map((control, index) => ( + handleFlowSelect(control.id, flowId)} + onTimingChange={(timing) => handleTimingChange(control.id, timing)} + onMoveUp={() => handleMoveUp(control.id)} + onMoveDown={() => handleMoveDown(control.id)} + onRemove={() => handleRemoveControl(control.id)} + /> + ))} +
+ )} -
-
- -
-

노드 플로우 실행 정보:

-

선택한 플로우의 모든 노드가 순차적/병렬로 실행됩니다.

-

• 독립 트랜잭션: 각 액션은 독립적으로 커밋/롤백

-

• 연쇄 중단: 부모 노드 실패 시 자식 노드 스킵

-
+ {/* 안내 메시지 */} + {flowControls.length > 0 && ( +
+
+ +
+

다중 제어 실행 정보:

+

• 제어는 위에서 아래 순서대로 순차 실행됩니다

+

• 각 제어는 독립 트랜잭션으로 처리됩니다

+

• 이전 제어 실패 시 다음 제어는 실행되지 않습니다

@@ -135,90 +271,89 @@ export const ImprovedButtonControlConfigPanel: React.FC void; loading: boolean; -}> = ({ flows, selectedFlowId, onSelect, loading }) => { + isFirst: boolean; + isLast: boolean; + onFlowSelect: (flowId: string) => void; + onTimingChange: (timing: "before" | "after" | "replace") => void; + onMoveUp: () => void; + onMoveDown: () => void; + onRemove: () => void; +}> = ({ control, flows, loading, isFirst, isLast, onFlowSelect, onTimingChange, onMoveUp, onMoveDown, onRemove }) => { return ( -
-
- - -
+ +
+ {/* 순서 표시 및 이동 버튼 */} +
+ + {control.order} + +
+ + +
+
- 0 ? control.flowId.toString() : ""} onValueChange={onFlowSelect}> + + + + + {loading ? ( +
로딩 중...
+ ) : flows.length === 0 ? ( +
플로우가 없습니다
+ ) : ( + flows.map((flow) => ( + + {flow.flowName} + + )) + )} +
+ + + {/* 실행 타이밍 */} + -
- ); -}; + + After (사후 실행) + + + Replace (대체 실행) + + + +
-/** - * 🔥 실행 타이밍 선택 컴포넌트 - */ -const ExecutionTimingSelector: React.FC<{ - value: string; - onChange: (timing: "before" | "after" | "replace") => void; -}> = ({ value, onChange }) => { - return ( -
-
- - + {/* 삭제 버튼 */} +
- - -
+ ); }; diff --git a/frontend/hooks/useAutoFill.ts b/frontend/hooks/useAutoFill.ts index c437f2c5..835a4886 100644 --- a/frontend/hooks/useAutoFill.ts +++ b/frontend/hooks/useAutoFill.ts @@ -192,3 +192,4 @@ export function applyAutoFillToFormData( return result; } + diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index e7da5833..c039d55a 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -2670,6 +2670,7 @@ export class ButtonActionExecutor { /** * 저장 후 제어 실행 (After Timing) + * 다중 제어 순차 실행 지원 */ private static async executeAfterSaveControl( config: ButtonActionConfig, @@ -2681,12 +2682,6 @@ export class ButtonActionExecutor { dataflowTiming: config.dataflowTiming, }); - // dataflowTiming이 'after'가 아니면 실행하지 않음 - if (config.dataflowTiming && config.dataflowTiming !== "after") { - console.log("⏭️ dataflowTiming이 'after'가 아니므로 제어 실행 건너뜀:", config.dataflowTiming); - return; - } - // 제어 데이터 소스 결정 let controlDataSource = config.dataflowConfig?.controlDataSource; if (!controlDataSource) { @@ -2700,9 +2695,117 @@ export class ButtonActionExecutor { controlDataSource, }; + // 🔥 다중 제어 지원 (flowControls 배열) + const flowControls = config.dataflowConfig?.flowControls || []; + if (flowControls.length > 0) { + console.log(`🎯 다중 제어 순차 실행 시작: ${flowControls.length}개`); + + // 순서대로 정렬 + const sortedControls = [...flowControls].sort((a: any, b: any) => (a.order || 0) - (b.order || 0)); + + // 노드 플로우 실행 API + const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); + + // 데이터 소스 준비 + const sourceData: any = context.formData || {}; + + let allSuccess = true; + const results: Array<{ flowId: number; flowName: string; success: boolean; message?: string }> = []; + + for (let i = 0; i < sortedControls.length; i++) { + const control = sortedControls[i]; + + // 유효하지 않은 flowId 스킵 + if (!control.flowId || control.flowId <= 0) { + console.warn(`⚠️ [${i + 1}/${sortedControls.length}] 유효하지 않은 flowId, 스킵:`, control); + continue; + } + + // executionTiming 체크 (after만 실행) + if (control.executionTiming && control.executionTiming !== "after") { + console.log( + `⏭️ [${i + 1}/${sortedControls.length}] executionTiming이 'after'가 아님, 스킵:`, + control.executionTiming, + ); + continue; + } + + console.log( + `\n📍 [${i + 1}/${sortedControls.length}] 제어 실행: ${control.flowName} (flowId: ${control.flowId})`, + ); + + try { + const result = await executeNodeFlow(control.flowId, { + dataSourceType: controlDataSource, + sourceData, + context: extendedContext, + }); + + results.push({ + flowId: control.flowId, + flowName: control.flowName, + success: result.success, + message: result.message, + }); + + if (result.success) { + console.log(`✅ [${i + 1}/${sortedControls.length}] 제어 성공: ${control.flowName}`); + } else { + console.error(`❌ [${i + 1}/${sortedControls.length}] 제어 실패: ${control.flowName} - ${result.message}`); + allSuccess = false; + // 이전 제어 실패 시 다음 제어 실행 중단 + console.warn("⚠️ 이전 제어 실패로 인해 나머지 제어 실행 중단"); + break; + } + } catch (error: any) { + console.error(`❌ [${i + 1}/${sortedControls.length}] 제어 실행 오류: ${control.flowName}`, error); + results.push({ + flowId: control.flowId, + flowName: control.flowName, + success: false, + message: error.message, + }); + allSuccess = false; + break; + } + } + + // 결과 요약 + const successCount = results.filter((r) => r.success).length; + const failCount = results.filter((r) => !r.success).length; + console.log("\n📊 다중 제어 실행 완료:", { + total: sortedControls.length, + executed: results.length, + success: successCount, + failed: failCount, + }); + + if (allSuccess) { + toast.success(`${successCount}개 제어 실행 완료`); + } else { + toast.error(`제어 실행 중 오류 발생 (${successCount}/${results.length} 성공)`); + } + + return; + } + + // 🔥 기존 단일 제어 실행 (하위 호환성) + // dataflowTiming이 'after'가 아니면 실행하지 않음 + if (config.dataflowTiming && config.dataflowTiming !== "after") { + console.log("⏭️ dataflowTiming이 'after'가 아니므로 제어 실행 건너뜀:", config.dataflowTiming); + return; + } + // 노드 플로우 방식 실행 (flowConfig가 있는 경우) const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId; if (hasFlowConfig) { + // executionTiming 체크 + const flowTiming = config.dataflowConfig.flowConfig.executionTiming; + if (flowTiming && flowTiming !== "after") { + console.log("⏭️ flowConfig.executionTiming이 'after'가 아니므로 제어 실행 건너뜀:", flowTiming); + return; + } + console.log("🎯 저장 후 노드 플로우 실행:", config.dataflowConfig.flowConfig); const { flowId } = config.dataflowConfig.flowConfig; diff --git a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md index 9d3236af..48bae8dd 100644 --- a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md +++ b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md @@ -1684,3 +1684,4 @@ const 출고등록_설정: ScreenSplitPanel = { + diff --git a/화면_임베딩_시스템_Phase1-4_구현_완료.md b/화면_임베딩_시스템_Phase1-4_구현_완료.md index 1ef61a7d..179cdd9d 100644 --- a/화면_임베딩_시스템_Phase1-4_구현_완료.md +++ b/화면_임베딩_시스템_Phase1-4_구현_완료.md @@ -531,3 +531,4 @@ const { data: config } = await getScreenSplitPanel(screenId); + diff --git a/화면_임베딩_시스템_충돌_분석_보고서.md b/화면_임베딩_시스템_충돌_분석_보고서.md index 013b85bc..c5a9a585 100644 --- a/화면_임베딩_시스템_충돌_분석_보고서.md +++ b/화면_임베딩_시스템_충돌_분석_보고서.md @@ -518,3 +518,4 @@ function ScreenViewPage() { +