From c634e1e0542cdc15a5da52af1a6b4a691a1d7016 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 18 Mar 2026 17:43:03 +0900 Subject: [PATCH] fix: update file handling and improve query logging - Added mes-architecture-guide.md to .gitignore to prevent unnecessary tracking. - Enhanced NodeFlowExecutionService to merge context data for WHERE clause, improving query accuracy. - Updated logging to include values in SQL query logs for better debugging. - Removed redundant event dispatches in V2Repeater to streamline save operations. - Adjusted DynamicComponentRenderer to conditionally refresh keys based on component type. - Improved FileUploadComponent to clear localStorage only for modal components, preventing unintended resets in non-modal contexts. These changes aim to enhance the overall functionality and maintainability of the application, ensuring better data handling and user experience. --- .gitignore | 1 + .../src/services/nodeFlowExecutionService.ts | 11 ++- frontend/components/v2/V2Repeater.tsx | 2 - .../lib/registry/DynamicComponentRenderer.tsx | 6 +- .../file-upload/FileUploadComponent.tsx | 30 +++---- .../ButtonPrimaryComponent.tsx | 36 ++++---- .../v2-file-upload/FileUploadComponent.tsx | 7 +- frontend/lib/utils/buttonActions.ts | 89 +++++++++---------- 8 files changed, 95 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index fb843160..2566257f 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,4 @@ mcp-task-queue/ # 파이프라인 회고록 (자동 생성) docs/retrospectives/ +mes-architecture-guide.md \ No newline at end of file diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index 6f0848e2..219159e0 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -952,13 +952,20 @@ export class NodeFlowExecutionService { } const schemaPrefix = schema ? `${schema}.` : ""; + + // WHERE 조건에서 field 값 조회를 위해 컨텍스트 데이터 전달 + // sourceData(저장된 폼 데이터) + buttonContext(인증 정보) 병합 + const contextForWhere = { + ...(context.buttonContext || {}), + ...(context.sourceData?.[0] || {}), + }; const whereResult = whereConditions - ? this.buildWhereClause(whereConditions) + ? this.buildWhereClause(whereConditions, contextForWhere) : { clause: "", values: [] }; const sql = `SELECT * FROM ${schemaPrefix}${tableName} ${whereResult.clause}`; - logger.info(`📊 테이블 전체 데이터 조회 SQL: ${sql}`); + logger.info(`📊 테이블 전체 데이터 조회 SQL: ${sql}`, { values: whereResult.values }); const result = await query(sql, whereResult.values); diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index b920e54f..f2f46f82 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -347,7 +347,6 @@ export const V2Repeater: React.FC = ({ if (!tableName || currentData.length === 0) { console.warn("🔴 [V2Repeater] 저장 스킵:", { tableName, dataLength: currentData.length }); toast.warning(`[디버그] V2Repeater 저장 스킵: data=${currentData.length}, table=${tableName}`); - window.dispatchEvent(new CustomEvent("repeaterSaveComplete")); return; } @@ -356,7 +355,6 @@ export const V2Repeater: React.FC = ({ const hasFkSource = sourceCol && mainFormData && mainFormData[sourceCol] !== undefined; if (!hasFkSource && !masterRecordId) { console.warn("🔴 [V2Repeater] FK 소스 값/masterRecordId 모두 없어 저장 스킵"); - window.dispatchEvent(new CustomEvent("repeaterSaveComplete")); return; } } diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 6e95e4d9..fb77df4b 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -999,7 +999,11 @@ export const DynamicComponentRenderer: React.FC = const rendererInstance = new RendererClass(rendererProps); renderedElement = rendererInstance.render(); } else { - renderedElement = ; + const needsKeyRefresh = + componentType === "v2-table-list" || + componentType === "table-list" || + componentType === "v2-repeater"; + renderedElement = ; } // 수평 라벨 → 라벨을 컴포넌트 영역 바깥에 absolute 배치, 입력은 100% 채움 diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index f0ee3594..73317dce 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -106,6 +106,7 @@ const FileUploadComponent: React.FC = ({ const [forceUpdate, setForceUpdate] = useState(0); const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); const fileInputRef = useRef(null); + const containerRef = useRef(null); // 🔑 레코드 모드 판단: formData에 id가 있으면 특정 행에 연결된 파일 관리 const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); @@ -217,18 +218,17 @@ const FileUploadComponent: React.FC = ({ } }, [component.id, getUniqueKey, recordId, isRecordMode]); - // 🆕 모달 닫힘/저장 성공 시 localStorage 파일 캐시 정리 (등록 후 재등록 시 이전 파일 잔존 방지) + // 모달 닫힘/저장 성공 시 localStorage 파일 캐시 정리 (등록 후 재등록 시 이전 파일 잔존 방지) + // 모달(Dialog) 내부의 컴포넌트만 초기화 대상 - 일반 화면의 파일 업로드는 초기화하지 않음 useEffect(() => { const handleClearFileCache = (event: Event) => { + // 모달 내부 컴포넌트만 초기화 (일반 화면에서는 스킵) + const isInModal = containerRef.current ? !!containerRef.current.closest('[role="dialog"]') : false; + if (!isInModal) { + return; + } + const backupKey = getUniqueKey(); - const eventType = event.type; - console.log("🧹 [DEBUG-CLEAR] 파일 캐시 정리 이벤트 수신:", { - eventType, - backupKey, - componentId: component.id, - currentFiles: uploadedFiles.length, - hasLocalStorage: !!localStorage.getItem(backupKey), - }); try { localStorage.removeItem(backupKey); setUploadedFiles([]); @@ -238,22 +238,15 @@ const FileUploadComponent: React.FC = ({ delete globalFileState[backupKey]; (window as any).globalFileState = globalFileState; } - console.log("🧹 [DEBUG-CLEAR] 정리 완료:", backupKey); } catch (e) { console.warn("파일 캐시 정리 실패:", e); } }; - // EditModal 닫힘, ScreenModal 연속 등록 저장 성공, 일반 저장 성공 모두 처리 window.addEventListener("closeEditModal", handleClearFileCache); window.addEventListener("saveSuccess", handleClearFileCache); window.addEventListener("saveSuccessInModal", handleClearFileCache); - console.log("🔎 [DEBUG-CLEAR] 이벤트 리스너 등록 완료:", { - componentId: component.id, - backupKey: getUniqueKey(), - }); - return () => { window.removeEventListener("closeEditModal", handleClearFileCache); window.removeEventListener("saveSuccess", handleClearFileCache); @@ -1190,10 +1183,11 @@ const FileUploadComponent: React.FC = ({ return (
= ({ target: "all", }); - // 2. 모달 닫기 (약간의 딜레이) - setTimeout(() => { - // EditModal 내부인지 확인 (isInModal prop 사용) - const isInEditModal = (props as any).isInModal; + // 2. 모달 닫기 (약간의 딜레이, 모달 내부에서만) + const isInEditModal = (props as any).isInModal; + const isInScreenModal = !!(props as any).isScreenModal || !!context.onClose; - if (isInEditModal) { - v2EventBus.emitSync(V2_EVENTS.MODAL_CLOSE, { - modalId: "edit-modal", - reason: "save", + if (isInEditModal || isInScreenModal) { + setTimeout(() => { + if (isInEditModal) { + v2EventBus.emitSync(V2_EVENTS.MODAL_CLOSE, { + modalId: "edit-modal", + reason: "save", + }); + } + + // ScreenModal 연속 등록 모드 지원 + v2EventBus.emitSync(V2_EVENTS.MODAL_SAVE_SUCCESS, { + modalId: "screen-modal", + savedData: context.formData || {}, + tableName: context.tableName || "", }); - } - - // ScreenModal은 연속 등록 모드를 지원하므로 saveSuccessInModal 이벤트 발생 - v2EventBus.emitSync(V2_EVENTS.MODAL_SAVE_SUCCESS, { - modalId: "screen-modal", - savedData: context.formData || {}, - tableName: context.tableName || "", - }); - }, 100); + }, 100); + } } } catch (error) { // 로딩 토스트 제거 diff --git a/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx index 58db0ad2..2baa6887 100644 --- a/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx @@ -105,6 +105,7 @@ const FileUploadComponent: React.FC = ({ const [forceUpdate, setForceUpdate] = useState(0); const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); const fileInputRef = useRef(null); + const containerRef = useRef(null); // objid 기반으로 파일이 로드되었는지 추적 (다른 이펙트가 덮어쓰지 않도록 방지) const filesLoadedFromObjidRef = useRef(false); @@ -197,7 +198,9 @@ const FileUploadComponent: React.FC = ({ useEffect(() => { if (!imageObjidFromFormData) { // formData에서 값이 사라지면 파일 목록도 초기화 (새 등록 시) - if (uploadedFiles.length > 0 && !isRecordMode) { + // 단, 모달 내부의 컴포넌트만 초기화 - 일반 화면에서는 저장 후 리셋으로 인한 초기화 방지 + const isInModal = containerRef.current ? !!containerRef.current.closest('[role="dialog"]') : false; + if (uploadedFiles.length > 0 && !isRecordMode && isInModal) { setUploadedFiles([]); filesLoadedFromObjidRef.current = false; } @@ -1058,11 +1061,11 @@ const FileUploadComponent: React.FC = ({ return (
= ( Object.keys(actualFormData).length > 0 ? actualFormData : context.formData || {} ) as Record; - console.log("📦 [executeAfterSaveControl] savedRecord 구조:", Object.keys(savedRecord)); - console.log("📦 [executeAfterSaveControl] actualFormData 추출:", Object.keys(formData)); - console.log("📦 [executeAfterSaveControl] formData.sabun:", formData.sabun); let parsedSectionData: any[] = []; - // comp_로 시작하는 필드에서 테이블 섹션 데이터 찾기 const compFieldKey = Object.keys(formData).find( (key) => key.startsWith("comp_") && typeof formData[key] === "string", ); @@ -1785,11 +1774,8 @@ export class ButtonActionExecutor { try { const sectionData = JSON.parse(formData[compFieldKey]); if (Array.isArray(sectionData) && sectionData.length > 0) { - // 공통 필드와 섹션 데이터 병합 parsedSectionData = sectionData.map((item: any) => { - // 섹션 데이터에서 불필요한 내부 필드 제거 const { _isNewItem, _targetTable, _existingRecord, ...cleanItem } = item; - // 공통 필드(comp_ 필드 제외) + 섹션 아이템 병합 const commonFields: Record = {}; Object.keys(formData).forEach((key) => { if (!key.startsWith("comp_") && !key.endsWith("_numberingRuleId")) { @@ -1804,14 +1790,14 @@ export class ButtonActionExecutor { } } - // 저장된 데이터를 context에 추가하여 플로우에 전달 - const contextWithSavedData = { - ...context, - savedData: formData, - // 파싱된 섹션 데이터가 있으면 selectedRowsData로 전달 - selectedRowsData: parsedSectionData.length > 0 ? parsedSectionData : context.selectedRowsData, + pendingDataflowControl = { + config, + context: { + ...context, + savedData: formData, + selectedRowsData: parsedSectionData.length > 0 ? parsedSectionData : context.selectedRowsData, + }, }; - await this.executeAfterSaveControl(config, contextWithSavedData); } } else { throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)"); @@ -1935,13 +1921,26 @@ export class ButtonActionExecutor { await repeaterSavePromise; } + // 리피터 저장 완료 후 제어관리 실행 (디테일 레코드가 DB에 있는 상태에서 실행) + console.log("🔍 [handleSave] 리피터 저장 완료, pendingDataflowControl:", !!pendingDataflowControl); + if (pendingDataflowControl) { + console.log("📦 [handleSave] 리피터 저장 완료 후 제어관리 실행 시작"); + await this.executeAfterSaveControl( + pendingDataflowControl.config, + pendingDataflowControl.context, + ); + } + // 테이블과 플로우 새로고침 (모달 닫기 전에 실행) context.onRefresh?.(); context.onFlowRefresh?.(); - // 저장 성공 후 모달 닫기 이벤트 발생 - window.dispatchEvent(new CustomEvent("closeEditModal")); - window.dispatchEvent(new CustomEvent("saveSuccessInModal")); + // 저장 성공 후 모달 닫기 이벤트 발생 (모달 내부에서만) + // 비모달 화면에서 이 이벤트를 발행하면 ScreenModal이 반응하여 컴포넌트 트리 재마운트 발생 + if (context.onClose) { + window.dispatchEvent(new CustomEvent("closeEditModal")); + window.dispatchEvent(new CustomEvent("saveSuccessInModal")); + } return true; } catch (error: any) {