diff --git a/backend-node/src/services/masterDetailExcelService.ts b/backend-node/src/services/masterDetailExcelService.ts index 0c1dfafe..44cc42b1 100644 --- a/backend-node/src/services/masterDetailExcelService.ts +++ b/backend-node/src/services/masterDetailExcelService.ts @@ -744,7 +744,9 @@ class MasterDetailExcelService { result.masterInserted = 1; logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`); - // 4. 디테일 레코드들 생성 + // 4. 디테일 레코드들 생성 (삽입된 데이터 수집) + const insertedDetailRows: Record[] = []; + for (const row of detailData) { try { const detailRowData: Record = { @@ -764,17 +766,26 @@ class MasterDetailExcelService { const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`); const detailValues = detailCols.map(k => detailRowData[k]); - await client.query( + // RETURNING *로 삽입된 데이터 반환받기 + const insertResult = await client.query( `INSERT INTO "${detailTable}" (${detailCols.map(c => `"${c}"`).join(", ")}, created_date) - VALUES (${detailPlaceholders.join(", ")}, NOW())`, + VALUES (${detailPlaceholders.join(", ")}, NOW()) + RETURNING *`, detailValues ); + + if (insertResult.rows && insertResult.rows[0]) { + insertedDetailRows.push(insertResult.rows[0]); + } + result.detailInserted++; } catch (error: any) { result.errors.push(`디테일 행 처리 실패: ${error.message}`); logger.error(`디테일 행 처리 실패:`, error); } } + + logger.info(`디테일 레코드 ${insertedDetailRows.length}건 삽입 완료`); await client.query("COMMIT"); result.success = result.errors.length === 0 || result.detailInserted > 0; @@ -797,7 +808,7 @@ class MasterDetailExcelService { try { const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService"); - // 마스터 데이터를 제어에 전달 + // 마스터 데이터 구성 const masterData = { ...masterFieldValues, [relation!.masterKeyColumn]: result.generatedKey, @@ -809,17 +820,28 @@ class MasterDetailExcelService { // 순서대로 제어 실행 for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) { logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`); + logger.info(` 전달 데이터: 마스터 1건, 디테일 ${insertedDetailRows.length}건`); + // 🆕 삽입된 디테일 데이터를 sourceData로 전달 (성능 최적화) + // - 전체 테이블 조회 대신 방금 INSERT한 데이터만 처리 + // - tableSource 노드가 context-data 모드일 때 이 데이터를 사용 const controlResult = await NodeFlowExecutionService.executeFlow( parseInt(flow.flowId), { - sourceData: [masterData], - dataSourceType: "formData", + sourceData: insertedDetailRows.length > 0 ? insertedDetailRows : [masterData], + dataSourceType: "excelUpload", // 엑셀 업로드 데이터임을 명시 buttonId: "excel-upload-button", screenId: screenId, userId: userId, companyCode: companyCode, formData: masterData, + // 추가 컨텍스트: 마스터/디테일 정보 + masterData: masterData, + detailData: insertedDetailRows, + masterTable: relation!.masterTable, + detailTable: relation!.detailTable, + masterKeyColumn: relation!.masterKeyColumn, + detailFkColumn: relation!.detailFkColumn, } ); diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index bfd628ce..b5237f0b 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -4446,6 +4446,8 @@ export class NodeFlowExecutionService { /** * 산술 연산 계산 + * 다중 연산 지원: (leftOperand operator rightOperand) 이후 additionalOperations 순차 적용 + * 예: (width * height) / 1000000 * qty */ private static evaluateArithmetic( arithmetic: any, @@ -4472,27 +4474,67 @@ export class NodeFlowExecutionService { const leftNum = Number(left) || 0; const rightNum = Number(right) || 0; - switch (arithmetic.operator) { + // 기본 연산 수행 + let result = this.applyOperator(leftNum, arithmetic.operator, rightNum); + + if (result === null) { + return null; + } + + // 추가 연산 처리 (다중 연산 지원) + if (arithmetic.additionalOperations && Array.isArray(arithmetic.additionalOperations)) { + for (const addOp of arithmetic.additionalOperations) { + const operandValue = this.getOperandValue( + addOp.operand, + sourceRow, + targetRow, + resultValues + ); + const operandNum = Number(operandValue) || 0; + + result = this.applyOperator(result, addOp.operator, operandNum); + + if (result === null) { + logger.warn(`⚠️ 추가 연산 실패: ${addOp.operator}`); + return null; + } + + logger.info(` 추가 연산: ${addOp.operator} ${operandNum} = ${result}`); + } + } + + return result; + } + + /** + * 단일 연산자 적용 + */ + private static applyOperator( + left: number, + operator: string, + right: number + ): number | null { + switch (operator) { case "+": - return leftNum + rightNum; + return left + right; case "-": - return leftNum - rightNum; + return left - right; case "*": - return leftNum * rightNum; + return left * right; case "/": - if (rightNum === 0) { + if (right === 0) { logger.warn(`⚠️ 0으로 나누기 시도`); return null; } - return leftNum / rightNum; + return left / right; case "%": - if (rightNum === 0) { + if (right === 0) { logger.warn(`⚠️ 0으로 나머지 연산 시도`); return null; } - return leftNum % rightNum; + return left % right; default: - throw new Error(`지원하지 않는 연산자: ${arithmetic.operator}`); + throw new Error(`지원하지 않는 연산자: ${operator}`); } } diff --git a/frontend/components/dataflow/node-editor/nodes/FormulaTransformNode.tsx b/frontend/components/dataflow/node-editor/nodes/FormulaTransformNode.tsx index 981a5002..991e1bd4 100644 --- a/frontend/components/dataflow/node-editor/nodes/FormulaTransformNode.tsx +++ b/frontend/components/dataflow/node-editor/nodes/FormulaTransformNode.tsx @@ -28,6 +28,14 @@ const OPERATOR_LABELS: Record = { "%": "%", }; +// 피연산자를 문자열로 변환 +function getOperandStr(operand: any): string { + if (!operand) return "?"; + if (operand.type === "static") return String(operand.value || "?"); + if (operand.fieldLabel) return operand.fieldLabel; + return operand.field || operand.resultField || "?"; +} + // 수식 요약 생성 function getFormulaSummary(transformation: FormulaTransformNodeData["transformations"][0]): string { const { formulaType, arithmetic, function: func, condition, staticValue } = transformation; @@ -35,11 +43,19 @@ function getFormulaSummary(transformation: FormulaTransformNodeData["transformat switch (formulaType) { case "arithmetic": { if (!arithmetic) return "미설정"; - const left = arithmetic.leftOperand; - const right = arithmetic.rightOperand; - const leftStr = left.type === "static" ? left.value : `${left.type}.${left.field || left.resultField}`; - const rightStr = right.type === "static" ? right.value : `${right.type}.${right.field || right.resultField}`; - return `${leftStr} ${OPERATOR_LABELS[arithmetic.operator]} ${rightStr}`; + const leftStr = getOperandStr(arithmetic.leftOperand); + const rightStr = getOperandStr(arithmetic.rightOperand); + let formula = `${leftStr} ${OPERATOR_LABELS[arithmetic.operator]} ${rightStr}`; + + // 추가 연산 표시 + if (arithmetic.additionalOperations && arithmetic.additionalOperations.length > 0) { + for (const addOp of arithmetic.additionalOperations) { + const opStr = getOperandStr(addOp.operand); + formula += ` ${OPERATOR_LABELS[addOp.operator] || addOp.operator} ${opStr}`; + } + } + + return formula; } case "function": { if (!func) return "미설정"; diff --git a/frontend/components/dataflow/node-editor/panels/properties/FormulaTransformProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/FormulaTransformProperties.tsx index fc2fbdf8..d9a9a20c 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/FormulaTransformProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/FormulaTransformProperties.tsx @@ -797,6 +797,85 @@ export function FormulaTransformProperties({ nodeId, data }: FormulaTransformPro index, )} + + {/* 추가 연산 목록 */} + {trans.arithmetic.additionalOperations && trans.arithmetic.additionalOperations.length > 0 && ( +
+ + {trans.arithmetic.additionalOperations.map((addOp: any, addIndex: number) => ( +
+ +
+ {renderOperandSelector( + addOp.operand, + (updates) => { + const newAdditionalOps = [...(trans.arithmetic!.additionalOperations || [])]; + newAdditionalOps[addIndex] = { ...newAdditionalOps[addIndex], operand: updates }; + handleTransformationChange(index, { + arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps }, + }); + }, + index, + )} +
+ +
+ ))} +
+ )} + + {/* 추가 연산 버튼 */} + )} diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 8bc65cca..cc82e386 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4996,6 +4996,12 @@ export class ButtonActionExecutor { masterDetailRelation = relationResponse.data; // 버튼 설정에서 채번 규칙 등 추가 설정 가져오기 + // 업로드 후 제어: excelAfterUploadFlows를 우선 사용 (통합된 설정) + // masterDetailExcel.afterUploadFlows는 레거시 호환성을 위해 fallback으로만 사용 + const afterUploadFlows = config.excelAfterUploadFlows?.length > 0 + ? config.excelAfterUploadFlows + : config.masterDetailExcel?.afterUploadFlows; + if (config.masterDetailExcel) { masterDetailExcelConfig = { ...config.masterDetailExcel, @@ -5006,8 +5012,8 @@ export class ButtonActionExecutor { detailFkColumn: relationResponse.data.detailFkColumn, // 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑) numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId, - // 업로드 후 제어 설정 추가 - afterUploadFlows: config.masterDetailExcel.afterUploadFlows || config.excelAfterUploadFlows, + // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선) + afterUploadFlows, }; } else { // 버튼 설정이 없으면 분할 패널 정보만 사용 @@ -5019,8 +5025,8 @@ export class ButtonActionExecutor { simpleMode: true, // 기본값으로 간단 모드 사용 // 채번 규칙 ID 추가 (excelNumberingRuleId 사용) numberingRuleId: config.excelNumberingRuleId, - // 업로드 후 제어 설정 추가 - afterUploadFlows: config.excelAfterUploadFlows, + // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선) + afterUploadFlows, }; } diff --git a/frontend/types/node-editor.ts b/frontend/types/node-editor.ts index 9c7d5c5e..ef0d78ff 100644 --- a/frontend/types/node-editor.ts +++ b/frontend/types/node-editor.ts @@ -231,6 +231,17 @@ export interface FormulaTransformNodeData { value?: string | number; resultField?: string; }; + // 추가 연산 (다중 연산 지원: (left op right) op1 val1 op2 val2 ...) + additionalOperations?: Array<{ + operator: "+" | "-" | "*" | "/" | "%"; + operand: { + type: "source" | "target" | "static" | "result"; + field?: string; + fieldLabel?: string; + value?: string | number; + resultField?: string; + }; + }>; }; // 함수 (formulaType === "function")