From f3a0c925640e37bff0dc119f7b584d594ae88378 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 5 Feb 2026 10:08:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20EditModal=20=EB=B0=8F=20ButtonActionExe?= =?UTF-8?q?cutor=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=20=EC=A0=9C=EC=96=B4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditModal 컴포넌트에서 executionTiming 체크 로직을 추가하여 데이터 흐름 제어를 보다 유연하게 처리하도록 개선하였습니다. - ButtonActionExecutor에서 저장된 데이터 구조를 명확히 하여, API 응답에서 실제 폼 데이터를 올바르게 추출하도록 수정하였습니다. - 디버깅 로그를 추가하여 데이터 흐름 및 상태를 추적할 수 있도록 하여 개발 편의성을 높였습니다. --- .../src/services/nodeFlowExecutionService.ts | 3 + .../SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md | 149 ++++++++++++++++++ frontend/components/screen/EditModal.tsx | 21 ++- frontend/lib/utils/buttonActions.ts | 59 +++++-- 4 files changed, 214 insertions(+), 18 deletions(-) create mode 100644 docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index eadddf9f..cadfdefc 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -845,6 +845,9 @@ export class NodeFlowExecutionService { logger.info( `📊 컨텍스트 데이터 사용: ${context.dataSourceType}, ${context.sourceData.length}건` ); + // 🔍 디버깅: sourceData 내용 출력 + logger.info(`📊 [테이블소스] sourceData 필드: ${JSON.stringify(Object.keys(context.sourceData[0]))}`); + logger.info(`📊 [테이블소스] sourceData.sabun: ${context.sourceData[0]?.sabun}`); return context.sourceData; } diff --git a/docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md b/docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md new file mode 100644 index 00000000..1cdf3af1 --- /dev/null +++ b/docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md @@ -0,0 +1,149 @@ +# 저장 후 플로우 실행 시 폼 데이터 전달 오류 수정 + +## 오류 현상 + +사용자가 폼에서 데이터를 저장한 후, 연결된 노드 플로우(예: 비밀번호 자동 설정)가 실행될 때 `sabun` 값이 `undefined`로 전달되어 UPDATE 쿼리의 WHERE 조건이 작동하지 않는 문제. + +### 증상 +- 저장 버튼 클릭 시 INSERT는 정상 작동 +- 저장 후 실행되는 노드 플로우에서 `user_password` UPDATE가 실패 (0건 업데이트) +- 콘솔 로그에서 `savedData.sabun: undefined` 출력 + +``` +📦 [executeAfterSaveControl] savedData 필드: ['id', 'screenId', 'tableName', 'data', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] +📦 [executeAfterSaveControl] savedData.sabun: undefined +``` + +--- + +## 원인 분석 + +### API 응답 구조의 3단계 중첩 + +저장 API(`DynamicFormApi.saveFormData`)의 응답이 3단계로 중첩되어 있었음: + +```typescript +// 1단계: Axios 응답 +saveResult = { + data: { ... } // API 응답 +} + +// 2단계: API 응답 래핑 (ApiResponse 인터페이스) +saveResult.data = { + success: true, + data: { ... }, // 저장된 레코드 + message: "저장 완료" +} + +// 3단계: 저장된 레코드 (dynamic_form_data 테이블 구조) +saveResult.data.data = { + id: 123, + screenId: 106, + tableName: "user_info", + data: { sabun: "20260205-087", user_name: "TEST", ... }, // ← 실제 폼 데이터 + createdAt: "2026-02-05T...", + updatedAt: "2026-02-05T...", + createdBy: "admin", + updatedBy: "admin" +} + +// 4단계: 실제 폼 데이터 (우리가 필요한 데이터) +saveResult.data.data.data = { + sabun: "20260205-087", + user_name: "TEST", + user_id: "Kim1542", + ... +} +``` + +### 기존 코드의 문제점 + +```typescript +// 기존 코드 (buttonActions.ts:1619-1621) +const savedData = saveResult?.data?.data || saveResult?.data || {}; +const formData = savedData; // ← 2단계까지만 추출 + +// savedData = { id, screenId, tableName, data: {...}, createdAt, ... } +// savedData.sabun = undefined ← 문제 발생! +``` + +기존 코드는 2단계(`saveResult.data.data`)까지만 추출했기 때문에, `savedData`가 저장된 레코드 메타데이터를 가리키고 있었음. 실제 폼 데이터는 `savedData.data` 안에 있었음. + +--- + +## 해결 방법 + +### 수정된 코드 + +```typescript +// 수정된 코드 (buttonActions.ts:1619-1628) +// 🔧 수정: saveResult.data가 3단계로 중첩된 경우 실제 폼 데이터 추출 +// saveResult.data = API 응답 { success, data, message } +// saveResult.data.data = 저장된 레코드 { id, screenId, tableName, data, createdAt... } +// saveResult.data.data.data = 실제 폼 데이터 { sabun, user_name... } +const savedRecord = saveResult?.data?.data || saveResult?.data || {}; +const actualFormData = savedRecord?.data || savedRecord; // ← 3단계까지 추출 +const formData = (Object.keys(actualFormData).length > 0 ? actualFormData : context.formData || {}); +``` + +### 수정 핵심 +1. `savedRecord`: 저장된 레코드 메타데이터 (`{ id, screenId, tableName, data, ... }`) +2. `actualFormData`: `savedRecord.data`가 있으면 그것을 사용, 없으면 `savedRecord` 자체 사용 +3. 폴백: `actualFormData`가 비어있으면 `context.formData` 사용 + +--- + +## 수정된 파일 + +| 파일 | 수정 내용 | +|------|-----------| +| `frontend/lib/utils/buttonActions.ts` | 3단계 중첩 데이터 구조에서 실제 폼 데이터 추출 로직 수정 (라인 1619-1628) | + +--- + +## 검증 결과 + +### 수정 전 +``` +📦 [executeAfterSaveControl] savedData 필드: ['id', 'screenId', 'tableName', 'data', ...] +📦 [executeAfterSaveControl] savedData.sabun: undefined +``` + +### 수정 후 +``` +📦 [executeAfterSaveControl] savedRecord 구조: ['id', 'screenId', 'tableName', 'data', ...] +📦 [executeAfterSaveControl] actualFormData 추출: ['sabun', 'user_id', 'user_password', ...] +📦 [executeAfterSaveControl] formData.sabun: 20260205-087 +``` + +### DB 확인 +```sql +SELECT sabun, user_name, user_password FROM user_info WHERE sabun = '20260205-087'; +-- 결과: sabun: "20260205-087", user_name: "TEST", user_password: "1e538e2abdd9663437343212a4853591" +``` + +--- + +## 교훈 + +1. **API 응답 구조 확인**: API 응답이 여러 단계로 래핑될 수 있음. 프론트엔드에서 `apiClient`가 한 번, `ApiResponse` 인터페이스가 한 번, 그리고 실제 데이터 구조가 또 다른 레벨을 가질 수 있음. + +2. **로그 추가의 중요성**: 중간 단계마다 로그를 찍어 데이터 구조를 확인하는 것이 디버깅에 필수적. + +3. **폴백 처리**: 데이터 추출 시 여러 단계의 폴백을 두어 다양한 응답 구조에 대응. + +--- + +## 관련 이슈 + +- 비밀번호 자동 설정 노드 플로우가 저장 후 실행되지 않는 문제 +- 저장 후 연결된 UPDATE 플로우에서 WHERE 조건이 작동하지 않는 문제 + +--- + +## 작성 정보 + +- **작성일**: 2026-02-05 +- **작성자**: AI Assistant +- **관련 화면**: 부서관리 > 사용자 등록 모달 +- **관련 플로우**: flowId: 120 (부서관리 비밀번호 자동세팅) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index db722991..03d43b82 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -704,7 +704,12 @@ export const EditModal: React.FC = ({ className }) => { controlConfig, }); - if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + // 🔧 executionTiming 체크: dataflowTiming 또는 flowConfig.executionTiming 또는 flowControls 확인 + const flowTiming = controlConfig?.dataflowTiming + || controlConfig?.dataflowConfig?.flowConfig?.executionTiming + || (controlConfig?.dataflowConfig?.flowControls?.length > 0 ? "after" : null); + + if (controlConfig?.enableDataflowControl && flowTiming === "after") { console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); // buttonActions의 executeAfterSaveControl 동적 import @@ -863,7 +868,12 @@ export const EditModal: React.FC = ({ className }) => { console.log("[EditModal] INSERT 완료 후 제어로직 실행 시도", { controlConfig }); - if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + // 🔧 executionTiming 체크: dataflowTiming 또는 flowConfig.executionTiming 또는 flowControls 확인 + const flowTimingInsert = controlConfig?.dataflowTiming + || controlConfig?.dataflowConfig?.flowConfig?.executionTiming + || (controlConfig?.dataflowConfig?.flowControls?.length > 0 ? "after" : null); + + if (controlConfig?.enableDataflowControl && flowTimingInsert === "after") { console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); @@ -936,7 +946,12 @@ export const EditModal: React.FC = ({ className }) => { console.log("[EditModal] UPDATE 완료 후 제어로직 실행 시도", { controlConfig }); - if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + // 🔧 executionTiming 체크: dataflowTiming 또는 flowConfig.executionTiming 또는 flowControls 확인 + const flowTimingUpdate = controlConfig?.dataflowTiming + || controlConfig?.dataflowConfig?.flowConfig?.executionTiming + || (controlConfig?.dataflowConfig?.flowControls?.length > 0 ? "after" : null); + + if (controlConfig?.enableDataflowControl && flowTimingUpdate === "after") { console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index b1d66eea..3521c668 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1616,7 +1616,16 @@ export class ButtonActionExecutor { if (config.enableDataflowControl && config.dataflowConfig) { // 테이블 섹션 데이터 파싱 (comp_로 시작하는 필드에 JSON 배열이 있는 경우) // 입고 화면 등에서 품목 목록이 comp_xxx 필드에 JSON 문자열로 저장됨 - const formData: Record = (saveResult.data || context.formData || {}) as Record; + // 🔧 수정: saveResult.data가 3단계로 중첩된 경우 실제 폼 데이터 추출 + // saveResult.data = API 응답 { success, data, message } + // saveResult.data.data = 저장된 레코드 { id, screenId, tableName, data, createdAt... } + // saveResult.data.data.data = 실제 폼 데이터 { sabun, user_name... } + const savedRecord = saveResult?.data?.data || saveResult?.data || {}; + const actualFormData = savedRecord?.data || savedRecord; + const formData: Record = (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_로 시작하는 필드에서 테이블 섹션 데이터 찾기 @@ -4016,16 +4025,27 @@ export class ButtonActionExecutor { const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // 데이터 소스 준비: context-data 모드는 배열을 기대함 - // 우선순위: selectedRowsData > savedData > formData - // - selectedRowsData: 테이블 섹션에서 저장된 하위 항목들 (item_code, inbound_qty 등 포함) - // - savedData: 저장 API 응답 데이터 - // - formData: 폼에 입력된 데이터 + // 🔧 저장 후 제어: savedData > formData > selectedRowsData + // - 저장 후 제어에서는 방금 저장된 데이터(savedData)가 가장 중요! + // - selectedRowsData는 왼쪽 패널 선택 데이터일 수 있으므로 마지막 순위 let sourceData: any[]; - if (context.selectedRowsData && context.selectedRowsData.length > 0) { + if (context.savedData) { + // 저장된 데이터가 있으면 우선 사용 (저장 API 응답) + sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData]; + console.log("📦 [executeAfterSaveControl] savedData 사용:", sourceData); + console.log("📦 [executeAfterSaveControl] savedData 필드:", Object.keys(context.savedData)); + console.log("📦 [executeAfterSaveControl] savedData.sabun:", context.savedData.sabun); + } else if (context.formData && Object.keys(context.formData).length > 0) { + // 폼 데이터 사용 + sourceData = [context.formData]; + console.log("📦 [executeAfterSaveControl] formData 사용:", sourceData); + } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { + // 테이블 섹션 데이터 (마지막 순위) sourceData = context.selectedRowsData; + console.log("📦 [executeAfterSaveControl] selectedRowsData 사용:", sourceData); } else { - const savedData = context.savedData || context.formData || {}; - sourceData = Array.isArray(savedData) ? savedData : [savedData]; + sourceData = []; + console.warn("⚠️ [executeAfterSaveControl] 데이터 소스 없음!"); } let allSuccess = true; @@ -4125,16 +4145,25 @@ export class ButtonActionExecutor { const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // 데이터 소스 준비: context-data 모드는 배열을 기대함 - // 우선순위: selectedRowsData > savedData > formData - // - selectedRowsData: 테이블 섹션에서 저장된 하위 항목들 (item_code, inbound_qty 등 포함) - // - savedData: 저장 API 응답 데이터 - // - formData: 폼에 입력된 데이터 + // 🔧 저장 후 제어: savedData > formData > selectedRowsData + // - 저장 후 제어에서는 방금 저장된 데이터(savedData)가 가장 중요! + // - selectedRowsData는 왼쪽 패널 선택 데이터일 수 있으므로 마지막 순위 let sourceData: any[]; - if (context.selectedRowsData && context.selectedRowsData.length > 0) { + if (context.savedData) { + // 저장된 데이터가 있으면 우선 사용 (저장 API 응답) + sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData]; + console.log("📦 [executeSingleFlowControl] savedData 사용:", sourceData); + } else if (context.formData && Object.keys(context.formData).length > 0) { + // 폼 데이터 사용 + sourceData = [context.formData]; + console.log("📦 [executeSingleFlowControl] formData 사용:", sourceData); + } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { + // 테이블 섹션 데이터 (마지막 순위) sourceData = context.selectedRowsData; + console.log("📦 [executeSingleFlowControl] selectedRowsData 사용:", sourceData); } else { - const savedData = context.savedData || context.formData || {}; - sourceData = Array.isArray(savedData) ? savedData : [savedData]; + sourceData = []; + console.warn("⚠️ [executeSingleFlowControl] 데이터 소스 없음!"); } // repeat-screen-modal 데이터가 있으면 병합