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 데이터가 있으면 병합