From 16885225a020d7a53de556a486b9af2260ac8e07 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 15 Dec 2025 14:46:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(edit-modal):=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=9B=84=20=EC=A0=9C=EC=96=B4=EB=A1=9C?= =?UTF-8?q?=EC=A7=81(=EB=85=B8=EB=93=9C=20=ED=94=8C=EB=A1=9C=EC=9A=B0)=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=8B=A4=ED=96=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditModal에서 INSERT/UPDATE/그룹 저장 완료 후 제어로직 자동 실행 - loadSaveButtonConfig(): 모달 내부 저장 버튼의 제어로직 설정 조회 - findSaveButtonInComponents(): 재귀적으로 저장 버튼 탐색 (conditional-container 내부 포함) - buttonActions.ts: openEditModal 이벤트에 buttonConfig, buttonContext 전달 - executeAfterSaveControl()을 public으로 변경하여 외부 호출 가능 - 제어로직 실행 오류 시 저장 성공 유지, 경고 토스트만 표시 --- frontend/components/screen/EditModal.tsx | 234 +++++++++++++++++- .../button-primary/ButtonPrimaryComponent.tsx | 12 + frontend/lib/utils/buttonActions.ts | 5 +- 3 files changed, 248 insertions(+), 3 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 294bca7f..5a123b3f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -26,12 +26,56 @@ interface EditModalState { onSave?: () => void; groupByColumns?: string[]; // 🆕 그룹핑 컬럼 (예: ["order_no"]) tableName?: string; // 🆕 테이블명 (그룹 조회용) + buttonConfig?: any; // 🆕 버튼 설정 (제어로직 실행용) + buttonContext?: any; // 🆕 버튼 컨텍스트 (screenId, userId 등) + saveButtonConfig?: { + enableDataflowControl?: boolean; + dataflowConfig?: any; + dataflowTiming?: string; + }; // 🆕 모달 내부 저장 버튼의 제어로직 설정 } interface EditModalProps { className?: string; } +/** + * 모달 내부에서 저장 버튼 찾기 (재귀적으로 탐색) + * action.type이 "save"인 button-primary 컴포넌트를 찾음 + */ +const findSaveButtonInComponents = (components: any[]): any | null => { + if (!components || !Array.isArray(components)) return null; + + for (const comp of components) { + // button-primary이고 action.type이 save인 경우 + if ( + comp.componentType === "button-primary" && + comp.componentConfig?.action?.type === "save" + ) { + return comp; + } + + // conditional-container의 sections 내부 탐색 + if (comp.componentType === "conditional-container" && comp.componentConfig?.sections) { + for (const section of comp.componentConfig.sections) { + if (section.screenId) { + // 조건부 컨테이너의 내부 화면은 별도로 로드해야 함 + // 여기서는 null 반환하고, loadSaveButtonConfig에서 처리 + continue; + } + } + } + + // 자식 컴포넌트가 있으면 재귀 탐색 + if (comp.children && Array.isArray(comp.children)) { + const found = findSaveButtonInComponents(comp.children); + if (found) return found; + } + } + + return null; +}; + export const EditModal: React.FC = ({ className }) => { const { user } = useAuth(); const [modalState, setModalState] = useState({ @@ -44,6 +88,9 @@ export const EditModal: React.FC = ({ className }) => { onSave: undefined, groupByColumns: undefined, tableName: undefined, + buttonConfig: undefined, + buttonContext: undefined, + saveButtonConfig: undefined, }); const [screenData, setScreenData] = useState<{ @@ -115,10 +162,88 @@ export const EditModal: React.FC = ({ className }) => { }; }; + // 🆕 모달 내부 저장 버튼의 제어로직 설정 조회 + const loadSaveButtonConfig = async (targetScreenId: number): Promise<{ + enableDataflowControl?: boolean; + dataflowConfig?: any; + dataflowTiming?: string; + } | null> => { + try { + // 1. 대상 화면의 레이아웃 조회 + const layoutData = await screenApi.getLayout(targetScreenId); + + if (!layoutData?.components) { + console.log("[EditModal] 레이아웃 컴포넌트 없음:", targetScreenId); + return null; + } + + // 2. 저장 버튼 찾기 + let saveButton = findSaveButtonInComponents(layoutData.components); + + // 3. conditional-container가 있는 경우 내부 화면도 탐색 + if (!saveButton) { + for (const comp of layoutData.components) { + if (comp.componentType === "conditional-container" && comp.componentConfig?.sections) { + for (const section of comp.componentConfig.sections) { + if (section.screenId) { + try { + const innerLayoutData = await screenApi.getLayout(section.screenId); + saveButton = findSaveButtonInComponents(innerLayoutData?.components || []); + if (saveButton) { + console.log("[EditModal] 조건부 컨테이너 내부에서 저장 버튼 발견:", { + sectionScreenId: section.screenId, + sectionLabel: section.label, + }); + break; + } + } catch (innerError) { + console.warn("[EditModal] 내부 화면 레이아웃 조회 실패:", section.screenId); + } + } + } + if (saveButton) break; + } + } + } + + if (!saveButton) { + console.log("[EditModal] 저장 버튼을 찾을 수 없음:", targetScreenId); + return null; + } + + // 4. webTypeConfig에서 제어로직 설정 추출 + const webTypeConfig = saveButton.webTypeConfig; + if (webTypeConfig?.enableDataflowControl) { + const config = { + enableDataflowControl: webTypeConfig.enableDataflowControl, + dataflowConfig: webTypeConfig.dataflowConfig, + dataflowTiming: webTypeConfig.dataflowConfig?.flowConfig?.executionTiming || "after", + }; + console.log("[EditModal] 저장 버튼 제어로직 설정 발견:", config); + return config; + } + + console.log("[EditModal] 저장 버튼에 제어로직 설정 없음"); + return null; + } catch (error) { + console.warn("[EditModal] 저장 버튼 설정 조회 실패:", error); + return null; + } + }; + // 전역 모달 이벤트 리스너 useEffect(() => { - const handleOpenEditModal = (event: CustomEvent) => { - const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode } = event.detail; + const handleOpenEditModal = async (event: CustomEvent) => { + const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode, buttonConfig, buttonContext } = event.detail; + + // 🆕 모달 내부 저장 버튼의 제어로직 설정 조회 + let saveButtonConfig: EditModalState["saveButtonConfig"] = undefined; + if (screenId) { + const config = await loadSaveButtonConfig(screenId); + if (config) { + saveButtonConfig = config; + } + } setModalState({ isOpen: true, @@ -130,6 +255,9 @@ export const EditModal: React.FC = ({ className }) => { onSave, groupByColumns, // 🆕 그룹핑 컬럼 tableName, // 🆕 테이블명 + buttonConfig, // 🆕 버튼 설정 + buttonContext, // 🆕 버튼 컨텍스트 + saveButtonConfig, // 🆕 모달 내부 저장 버튼의 제어로직 설정 }); // 편집 데이터로 폼 데이터 초기화 @@ -581,6 +709,46 @@ export const EditModal: React.FC = ({ className }) => { } } + // 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어) + // 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig) + try { + const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig; + + console.log("[EditModal] 그룹 저장 완료 후 제어로직 실행 시도", { + hasSaveButtonConfig: !!modalState.saveButtonConfig, + hasButtonConfig: !!modalState.buttonConfig, + controlConfig, + }); + + if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); + + // buttonActions의 executeAfterSaveControl 동적 import + const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); + + // 제어로직 실행 + await ButtonActionExecutor.executeAfterSaveControl( + controlConfig, + { + formData: modalState.editData, + screenId: modalState.buttonContext?.screenId || modalState.screenId, + tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName, + userId: user?.userId, + companyCode: user?.companyCode, + onRefresh: modalState.onSave, + } + ); + + console.log("✅ [EditModal] 제어로직 실행 완료"); + } else { + console.log("ℹ️ [EditModal] 저장 후 실행할 제어로직 없음"); + } + } catch (controlError) { + console.error("❌ [EditModal] 제어로직 실행 오류:", controlError); + // 제어로직 오류는 저장 성공을 방해하지 않음 (경고만 표시) + toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); + } + handleClose(); } else { toast.info("변경된 내용이 없습니다."); @@ -615,6 +783,37 @@ export const EditModal: React.FC = ({ className }) => { } } + // 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어) + // 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig) + try { + const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig; + + console.log("[EditModal] INSERT 완료 후 제어로직 실행 시도", { controlConfig }); + + if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); + + const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); + + await ButtonActionExecutor.executeAfterSaveControl( + controlConfig, + { + formData, + screenId: modalState.buttonContext?.screenId || modalState.screenId, + tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName, + userId: user?.userId, + companyCode: user?.companyCode, + onRefresh: modalState.onSave, + } + ); + + console.log("✅ [EditModal] 제어로직 실행 완료"); + } + } catch (controlError) { + console.error("❌ [EditModal] 제어로직 실행 오류:", controlError); + toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); + } + handleClose(); } else { throw new Error(response.message || "생성에 실패했습니다."); @@ -657,6 +856,37 @@ export const EditModal: React.FC = ({ className }) => { } } + // 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어) + // 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig) + try { + const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig; + + console.log("[EditModal] UPDATE 완료 후 제어로직 실행 시도", { controlConfig }); + + if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); + + const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); + + await ButtonActionExecutor.executeAfterSaveControl( + controlConfig, + { + formData, + screenId: modalState.buttonContext?.screenId || modalState.screenId, + tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName, + userId: user?.userId, + companyCode: user?.companyCode, + onRefresh: modalState.onSave, + } + ); + + console.log("✅ [EditModal] 제어로직 실행 완료"); + } + } catch (controlError) { + console.error("❌ [EditModal] 제어로직 실행 오류:", controlError); + toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); + } + handleClose(); } else { throw new Error(response.message || "수정에 실패했습니다."); diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 1942d268..4b88c565 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -376,6 +376,7 @@ export const ButtonPrimaryComponent: React.FC = ({ // 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴) enableDataflowControl: component.webTypeConfig?.enableDataflowControl, dataflowConfig: component.webTypeConfig?.dataflowConfig, + dataflowTiming: component.webTypeConfig?.dataflowTiming, }; } else if (componentConfig.action && typeof componentConfig.action === "object") { // 🔥 이미 객체인 경우에도 제어관리 설정 추가 @@ -383,8 +384,19 @@ export const ButtonPrimaryComponent: React.FC = ({ ...componentConfig.action, enableDataflowControl: component.webTypeConfig?.enableDataflowControl, dataflowConfig: component.webTypeConfig?.dataflowConfig, + dataflowTiming: component.webTypeConfig?.dataflowTiming, }; } + + // 🔍 디버깅: processedConfig.action 확인 + console.log("[ButtonPrimaryComponent] processedConfig.action 생성 완료", { + actionType: processedConfig.action?.type, + enableDataflowControl: processedConfig.action?.enableDataflowControl, + dataflowTiming: processedConfig.action?.dataflowTiming, + dataflowConfig: processedConfig.action?.dataflowConfig, + webTypeConfigRaw: component.webTypeConfig, + componentText: component.text, + }); // 스타일 계산 // height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감 diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 1ced2836..ede92868 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -2086,6 +2086,8 @@ export class ButtonActionExecutor { editData: rowData, groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달 tableName: context.tableName, // 🆕 테이블명 전달 + buttonConfig: config, // 🆕 버튼 설정 전달 (제어로직 실행용) + buttonContext: context, // 🆕 버튼 컨텍스트 전달 (screenId, userId 등) onSave: () => { context.onRefresh?.(); }, @@ -2621,8 +2623,9 @@ export class ButtonActionExecutor { /** * 저장 후 제어 실행 (After Timing) + * EditModal 등 외부에서도 호출 가능하도록 public으로 변경 */ - private static async executeAfterSaveControl( + public static async executeAfterSaveControl( config: ButtonActionConfig, context: ButtonActionContext, ): Promise {