From 47552bc35c61323fa1a87e761b3aaedf43685b3a Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 5 Dec 2025 17:28:44 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/dynamicFormService.ts | 18 ++++ .../src/services/nodeFlowExecutionService.ts | 101 +++++++++++------- .../components/webtypes/RepeaterInput.tsx | 14 ++- .../RepeaterFieldGroupRenderer.tsx | 28 +++-- 4 files changed, 110 insertions(+), 51 deletions(-) diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 04586d65..d52c184f 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -506,6 +506,24 @@ export class DynamicFormService { // 헤더 + 품목을 병합 const rawMergedData = { ...dataToInsert, ...item }; + // 🆕 새 레코드 저장 시 id 제거하여 새 UUID 생성되도록 함 + // _existingRecord가 명시적으로 true인 경우에만 기존 레코드로 처리 (UPDATE) + // 그 외의 경우는 모두 새 레코드로 처리 (INSERT) + const isExistingRecord = rawMergedData._existingRecord === true; + + if (!isExistingRecord) { + // 새 레코드: id 제거하여 새 UUID 자동 생성 + const oldId = rawMergedData.id; + delete rawMergedData.id; + console.log(`🆕 새 레코드로 처리 (id 제거됨: ${oldId})`); + } else { + console.log(`📝 기존 레코드 수정 (id 유지: ${rawMergedData.id})`); + } + + // 메타 플래그 제거 + delete rawMergedData._isNewItem; + delete rawMergedData._existingRecord; + // 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외) const validColumnNames = columnInfo.map((col) => col.column_name); const mergedData: Record = {}; diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index e70a1dae..7b5f6918 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -833,11 +833,18 @@ export class NodeFlowExecutionService { const sql = `SELECT * FROM ${schemaPrefix}${tableName} ${whereResult.clause}`; + logger.info(`📊 테이블 전체 데이터 조회 SQL: ${sql}`); + const result = await query(sql, whereResult.values); logger.info( `📊 테이블 전체 데이터 조회: ${tableName}, ${result.length}건` ); + + // 디버깅: 조회된 데이터 샘플 출력 + if (result.length > 0) { + logger.info(`📊 조회된 데이터 샘플: ${JSON.stringify(result[0])?.substring(0, 300)}`); + } return result; } @@ -1358,57 +1365,64 @@ export class NodeFlowExecutionService { let updatedCount = 0; const updatedDataArray: any[] = []; - // 🆕 table-all 모드: 단일 SQL로 일괄 업데이트 + // 🆕 table-all 모드: 각 그룹별로 UPDATE 실행 (집계 결과 반영) if (context.currentNodeDataSourceType === "table-all") { - console.log("🚀 table-all 모드: 단일 SQL로 일괄 업데이트 시작"); + console.log("🚀 table-all 모드: 그룹별 업데이트 시작 (총 " + dataArray.length + "개 그룹)"); - // 첫 번째 데이터를 참조하여 SET 절 생성 - const firstData = dataArray[0]; - const setClauses: string[] = []; - const values: any[] = []; - let paramIndex = 1; + // 🔥 각 그룹(데이터)별로 UPDATE 실행 + for (let i = 0; i < dataArray.length; i++) { + const data = dataArray[i]; + const setClauses: string[] = []; + const values: any[] = []; + let paramIndex = 1; - console.log("🗺️ 필드 매핑 처리 중..."); - fieldMappings.forEach((mapping: any) => { - const value = - mapping.staticValue !== undefined - ? mapping.staticValue - : firstData[mapping.sourceField]; + console.log(`\n📦 그룹 ${i + 1}/${dataArray.length} 처리 중...`); + console.log("🗺️ 필드 매핑 처리 중..."); + + fieldMappings.forEach((mapping: any) => { + const value = + mapping.staticValue !== undefined + ? mapping.staticValue + : data[mapping.sourceField]; - console.log( - ` ${mapping.sourceField} → ${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}` + console.log( + ` ${mapping.sourceField} → ${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}` + ); + + if (mapping.targetField) { + setClauses.push(`${mapping.targetField} = $${paramIndex}`); + values.push(value); + paramIndex++; + } + }); + + // WHERE 조건 (사용자 정의 조건만 사용, PK 자동 추가 안 함) + const whereResult = this.buildWhereClause( + whereConditions, + data, + paramIndex ); - if (mapping.targetField) { - setClauses.push(`${mapping.targetField} = $${paramIndex}`); - values.push(value); - paramIndex++; - } - }); + values.push(...whereResult.values); - // WHERE 조건 (사용자 정의 조건만 사용, PK 자동 추가 안 함) - const whereResult = this.buildWhereClause( - whereConditions, - firstData, - paramIndex - ); + const sql = ` + UPDATE ${targetTable} + SET ${setClauses.join(", ")} + ${whereResult.clause} + `; - values.push(...whereResult.values); + console.log("📝 실행할 SQL:", sql); + console.log("📊 바인딩 값:", values); - const sql = ` - UPDATE ${targetTable} - SET ${setClauses.join(", ")} - ${whereResult.clause} - `; - - console.log("📝 실행할 SQL (일괄 처리):", sql); - console.log("📊 바인딩 값:", values); - - const result = await txClient.query(sql, values); - updatedCount = result.rowCount || 0; + const result = await txClient.query(sql, values); + const rowCount = result.rowCount || 0; + updatedCount += rowCount; + + console.log(`✅ 그룹 ${i + 1} UPDATE 완료: ${rowCount}건`); + } logger.info( - `✅ UPDATE 완료 (내부 DB, 일괄 처리): ${targetTable}, ${updatedCount}건` + `✅ UPDATE 완료 (내부 DB, 그룹별 처리): ${targetTable}, 총 ${updatedCount}건` ); // 업데이트된 데이터는 원본 배열 반환 (실제 DB에서 다시 조회하지 않음) @@ -3216,10 +3230,12 @@ export class NodeFlowExecutionService { // 입력 데이터가 없으면 빈 배열 반환 if (!inputData || !Array.isArray(inputData) || inputData.length === 0) { logger.warn("⚠️ 집계할 입력 데이터가 없습니다."); + logger.warn(`⚠️ inputData 타입: ${typeof inputData}, 값: ${JSON.stringify(inputData)?.substring(0, 200)}`); return []; } logger.info(`📥 입력 데이터: ${inputData.length}건`); + logger.info(`📥 입력 데이터 샘플: ${JSON.stringify(inputData[0])?.substring(0, 300)}`); logger.info(`📊 그룹 기준: ${groupByFields.length > 0 ? groupByFields.map((f: any) => f.field).join(", ") : "전체"}`); logger.info(`📊 집계 연산: ${aggregations.length}개`); @@ -3239,6 +3255,11 @@ export class NodeFlowExecutionService { } logger.info(`📊 그룹 수: ${groups.size}개`); + + // 디버깅: 각 그룹의 데이터 출력 + for (const [groupKey, groupRows] of groups) { + logger.info(`📊 그룹 [${groupKey}]: ${groupRows.length}건, inbound_qty 합계: ${groupRows.reduce((sum, row) => sum + parseFloat(row.inbound_qty || 0), 0)}`); + } // 각 그룹에 대해 집계 수행 const results: any[] = []; diff --git a/frontend/components/webtypes/RepeaterInput.tsx b/frontend/components/webtypes/RepeaterInput.tsx index ce9d4cf6..3116b2c6 100644 --- a/frontend/components/webtypes/RepeaterInput.tsx +++ b/frontend/components/webtypes/RepeaterInput.tsx @@ -91,6 +91,8 @@ export const RepeaterInput: React.FC = ({ fields.forEach((field) => { item[field.name] = ""; }); + // 🆕 새 항목임을 표시하는 플래그 추가 (백엔드에서 새 레코드로 처리) + item._isNewItem = true; return item; } @@ -113,6 +115,11 @@ export const RepeaterInput: React.FC = ({ } }); + // 🆕 기존 레코드임을 표시 (id가 있는 경우) + if (updatedItem.id) { + updatedItem._existingRecord = true; + } + return hasChange ? updatedItem : item; }); @@ -125,7 +132,12 @@ export const RepeaterInput: React.FC = ({ : updatedValue; onChange?.(dataWithMeta); } else { - setItems(value); + // 🆕 기존 레코드 플래그 추가 + const valueWithFlag = value.map(item => ({ + ...item, + _existingRecord: !!item.id, + })); + setItems(valueWithFlag); } } }, [value]); diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index c47ff3c9..12219280 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -135,6 +135,7 @@ const RepeaterFieldGroupComponent: React.FC = (props) => ...item, _targetTable: targetTable, _originalItemIds: itemIds, // 🆕 원본 ID 목록도 함께 전달 + _existingRecord: !!item.id, // 🆕 기존 레코드 플래그 (id가 있으면 기존 레코드) })); onChange(dataWithMeta); } @@ -228,17 +229,23 @@ const RepeaterFieldGroupComponent: React.FC = (props) => // 반복 필드 그룹에 정의된 필드 + 시스템 필드만 유지 const definedFields = configRef.current.fields || []; const definedFieldNames = new Set(definedFields.map((f: any) => f.name)); - // 시스템 필드 및 필수 필드 추가 - const systemFields = new Set(['id', '_targetTable', 'created_date', 'updated_date', 'writer', 'company_code']); + // 시스템 필드 및 필수 필드 추가 (id는 제외 - 새 레코드로 처리하기 위해) + const systemFields = new Set(['_targetTable', '_isNewItem', 'created_date', 'updated_date', 'writer', 'company_code']); const filteredData = normalizedData.map((item: any) => { const filteredItem: Record = {}; Object.keys(item).forEach(key => { + // 🆕 id 필드는 제외 (새 레코드로 저장되도록) + if (key === 'id') { + return; // id 필드 제외 + } // 정의된 필드이거나 시스템 필드인 경우만 포함 if (definedFieldNames.has(key) || systemFields.has(key)) { filteredItem[key] = item[key]; } }); + // 🆕 새 항목임을 표시하는 플래그 추가 + filteredItem._isNewItem = true; return filteredItem; }); @@ -259,16 +266,16 @@ const RepeaterFieldGroupComponent: React.FC = (props) => newItems = filteredData; addedCount = filteredData.length; } else { - // 🆕 중복 체크: id 또는 고유 식별자를 기준으로 이미 존재하는 항목 제외 - const existingIds = new Set( + // 🆕 중복 체크: item_code를 기준으로 이미 존재하는 항목 제외 (id는 사용하지 않음) + const existingItemCodes = new Set( currentValue - .map((item: any) => item.id || item.po_item_id || item.item_id) + .map((item: any) => item.item_code) .filter(Boolean) ); const uniqueNewItems = filteredData.filter((item: any) => { - const itemId = item.id || item.po_item_id || item.item_id; - if (itemId && existingIds.has(itemId)) { + const itemCode = item.item_code; + if (itemCode && existingItemCodes.has(itemCode)) { duplicateCount++; return false; // 중복 항목 제외 } @@ -291,11 +298,12 @@ const RepeaterFieldGroupComponent: React.FC = (props) => setGroupedData(newItems); // 🆕 SplitPanelContext에 추가된 항목 ID 등록 (좌측 테이블 필터링용) + // item_code를 기준으로 등록 (id는 새 레코드라 없을 수 있음) if (splitPanelContext?.addItemIds && addedCount > 0) { - const newItemIds = newItems - .map((item: any) => String(item.id || item.po_item_id || item.item_id)) + const newItemCodes = newItems + .map((item: any) => String(item.item_code)) .filter(Boolean); - splitPanelContext.addItemIds(newItemIds); + splitPanelContext.addItemIds(newItemCodes); } // JSON 문자열로 변환하여 저장