Merge pull request 'feature/screen-management' (#355) from feature/screen-management into main

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/355
This commit is contained in:
kjs 2026-01-13 10:25:20 +09:00
commit 989b7e53a7
6 changed files with 200 additions and 24 deletions

View File

@ -744,7 +744,9 @@ class MasterDetailExcelService {
result.masterInserted = 1; result.masterInserted = 1;
logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`); logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`);
// 4. 디테일 레코드들 생성 // 4. 디테일 레코드들 생성 (삽입된 데이터 수집)
const insertedDetailRows: Record<string, any>[] = [];
for (const row of detailData) { for (const row of detailData) {
try { try {
const detailRowData: Record<string, any> = { const detailRowData: Record<string, any> = {
@ -764,11 +766,18 @@ class MasterDetailExcelService {
const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`); const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`);
const detailValues = detailCols.map(k => detailRowData[k]); 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) `INSERT INTO "${detailTable}" (${detailCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${detailPlaceholders.join(", ")}, NOW())`, VALUES (${detailPlaceholders.join(", ")}, NOW())
RETURNING *`,
detailValues detailValues
); );
if (insertResult.rows && insertResult.rows[0]) {
insertedDetailRows.push(insertResult.rows[0]);
}
result.detailInserted++; result.detailInserted++;
} catch (error: any) { } catch (error: any) {
result.errors.push(`디테일 행 처리 실패: ${error.message}`); result.errors.push(`디테일 행 처리 실패: ${error.message}`);
@ -776,6 +785,8 @@ class MasterDetailExcelService {
} }
} }
logger.info(`디테일 레코드 ${insertedDetailRows.length}건 삽입 완료`);
await client.query("COMMIT"); await client.query("COMMIT");
result.success = result.errors.length === 0 || result.detailInserted > 0; result.success = result.errors.length === 0 || result.detailInserted > 0;
@ -797,7 +808,7 @@ class MasterDetailExcelService {
try { try {
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService"); const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService");
// 마스터 데이터를 제어에 전달 // 마스터 데이터 구성
const masterData = { const masterData = {
...masterFieldValues, ...masterFieldValues,
[relation!.masterKeyColumn]: result.generatedKey, [relation!.masterKeyColumn]: result.generatedKey,
@ -809,17 +820,28 @@ class MasterDetailExcelService {
// 순서대로 제어 실행 // 순서대로 제어 실행
for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) { for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) {
logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`); logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`);
logger.info(` 전달 데이터: 마스터 1건, 디테일 ${insertedDetailRows.length}`);
// 🆕 삽입된 디테일 데이터를 sourceData로 전달 (성능 최적화)
// - 전체 테이블 조회 대신 방금 INSERT한 데이터만 처리
// - tableSource 노드가 context-data 모드일 때 이 데이터를 사용
const controlResult = await NodeFlowExecutionService.executeFlow( const controlResult = await NodeFlowExecutionService.executeFlow(
parseInt(flow.flowId), parseInt(flow.flowId),
{ {
sourceData: [masterData], sourceData: insertedDetailRows.length > 0 ? insertedDetailRows : [masterData],
dataSourceType: "formData", dataSourceType: "excelUpload", // 엑셀 업로드 데이터임을 명시
buttonId: "excel-upload-button", buttonId: "excel-upload-button",
screenId: screenId, screenId: screenId,
userId: userId, userId: userId,
companyCode: companyCode, companyCode: companyCode,
formData: masterData, formData: masterData,
// 추가 컨텍스트: 마스터/디테일 정보
masterData: masterData,
detailData: insertedDetailRows,
masterTable: relation!.masterTable,
detailTable: relation!.detailTable,
masterKeyColumn: relation!.masterKeyColumn,
detailFkColumn: relation!.detailFkColumn,
} }
); );

View File

@ -4446,6 +4446,8 @@ export class NodeFlowExecutionService {
/** /**
* *
* : (leftOperand operator rightOperand) additionalOperations
* : (width * height) / 1000000 * qty
*/ */
private static evaluateArithmetic( private static evaluateArithmetic(
arithmetic: any, arithmetic: any,
@ -4472,27 +4474,67 @@ export class NodeFlowExecutionService {
const leftNum = Number(left) || 0; const leftNum = Number(left) || 0;
const rightNum = Number(right) || 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 "+": case "+":
return leftNum + rightNum; return left + right;
case "-": case "-":
return leftNum - rightNum; return left - right;
case "*": case "*":
return leftNum * rightNum; return left * right;
case "/": case "/":
if (rightNum === 0) { if (right === 0) {
logger.warn(`⚠️ 0으로 나누기 시도`); logger.warn(`⚠️ 0으로 나누기 시도`);
return null; return null;
} }
return leftNum / rightNum; return left / right;
case "%": case "%":
if (rightNum === 0) { if (right === 0) {
logger.warn(`⚠️ 0으로 나머지 연산 시도`); logger.warn(`⚠️ 0으로 나머지 연산 시도`);
return null; return null;
} }
return leftNum % rightNum; return left % right;
default: default:
throw new Error(`지원하지 않는 연산자: ${arithmetic.operator}`); throw new Error(`지원하지 않는 연산자: ${operator}`);
} }
} }

View File

@ -28,6 +28,14 @@ const OPERATOR_LABELS: Record<string, string> = {
"%": "%", "%": "%",
}; };
// 피연산자를 문자열로 변환
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 { function getFormulaSummary(transformation: FormulaTransformNodeData["transformations"][0]): string {
const { formulaType, arithmetic, function: func, condition, staticValue } = transformation; const { formulaType, arithmetic, function: func, condition, staticValue } = transformation;
@ -35,11 +43,19 @@ function getFormulaSummary(transformation: FormulaTransformNodeData["transformat
switch (formulaType) { switch (formulaType) {
case "arithmetic": { case "arithmetic": {
if (!arithmetic) return "미설정"; if (!arithmetic) return "미설정";
const left = arithmetic.leftOperand; const leftStr = getOperandStr(arithmetic.leftOperand);
const right = arithmetic.rightOperand; const rightStr = getOperandStr(arithmetic.rightOperand);
const leftStr = left.type === "static" ? left.value : `${left.type}.${left.field || left.resultField}`; let formula = `${leftStr} ${OPERATOR_LABELS[arithmetic.operator]} ${rightStr}`;
const rightStr = right.type === "static" ? right.value : `${right.type}.${right.field || right.resultField}`;
return `${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": { case "function": {
if (!func) return "미설정"; if (!func) return "미설정";

View File

@ -797,6 +797,85 @@ export function FormulaTransformProperties({ nodeId, data }: FormulaTransformPro
index, index,
)} )}
</div> </div>
{/* 추가 연산 목록 */}
{trans.arithmetic.additionalOperations && trans.arithmetic.additionalOperations.length > 0 && (
<div className="space-y-2 border-t pt-2">
<Label className="text-xs text-gray-500"> </Label>
{trans.arithmetic.additionalOperations.map((addOp: any, addIndex: number) => (
<div key={addIndex} className="flex items-center gap-2 rounded bg-orange-50 p-2">
<Select
value={addOp.operator}
onValueChange={(value) => {
const newAdditionalOps = [...(trans.arithmetic!.additionalOperations || [])];
newAdditionalOps[addIndex] = { ...newAdditionalOps[addIndex], operator: value };
handleTransformationChange(index, {
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
});
}}
>
<SelectTrigger className="h-7 w-20 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ARITHMETIC_OPERATORS.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.value}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex-1">
{renderOperandSelector(
addOp.operand,
(updates) => {
const newAdditionalOps = [...(trans.arithmetic!.additionalOperations || [])];
newAdditionalOps[addIndex] = { ...newAdditionalOps[addIndex], operand: updates };
handleTransformationChange(index, {
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
});
},
index,
)}
</div>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-red-500 hover:text-red-700"
onClick={() => {
const newAdditionalOps = trans.arithmetic!.additionalOperations!.filter(
(_: any, i: number) => i !== addIndex
);
handleTransformationChange(index, {
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
});
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
{/* 추가 연산 버튼 */}
<Button
variant="outline"
size="sm"
className="h-7 w-full text-xs"
onClick={() => {
const newAdditionalOps = [
...(trans.arithmetic!.additionalOperations || []),
{ operator: "*", operand: { type: "static" as const, value: "" } },
];
handleTransformationChange(index, {
arithmetic: { ...trans.arithmetic!, additionalOperations: newAdditionalOps },
});
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div> </div>
)} )}

View File

@ -4996,6 +4996,12 @@ export class ButtonActionExecutor {
masterDetailRelation = relationResponse.data; masterDetailRelation = relationResponse.data;
// 버튼 설정에서 채번 규칙 등 추가 설정 가져오기 // 버튼 설정에서 채번 규칙 등 추가 설정 가져오기
// 업로드 후 제어: excelAfterUploadFlows를 우선 사용 (통합된 설정)
// masterDetailExcel.afterUploadFlows는 레거시 호환성을 위해 fallback으로만 사용
const afterUploadFlows = config.excelAfterUploadFlows?.length > 0
? config.excelAfterUploadFlows
: config.masterDetailExcel?.afterUploadFlows;
if (config.masterDetailExcel) { if (config.masterDetailExcel) {
masterDetailExcelConfig = { masterDetailExcelConfig = {
...config.masterDetailExcel, ...config.masterDetailExcel,
@ -5006,8 +5012,8 @@ export class ButtonActionExecutor {
detailFkColumn: relationResponse.data.detailFkColumn, detailFkColumn: relationResponse.data.detailFkColumn,
// 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑) // 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑)
numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId, numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId,
// 업로드 후 제어 설정 추가 // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
afterUploadFlows: config.masterDetailExcel.afterUploadFlows || config.excelAfterUploadFlows, afterUploadFlows,
}; };
} else { } else {
// 버튼 설정이 없으면 분할 패널 정보만 사용 // 버튼 설정이 없으면 분할 패널 정보만 사용
@ -5019,8 +5025,8 @@ export class ButtonActionExecutor {
simpleMode: true, // 기본값으로 간단 모드 사용 simpleMode: true, // 기본값으로 간단 모드 사용
// 채번 규칙 ID 추가 (excelNumberingRuleId 사용) // 채번 규칙 ID 추가 (excelNumberingRuleId 사용)
numberingRuleId: config.excelNumberingRuleId, numberingRuleId: config.excelNumberingRuleId,
// 업로드 후 제어 설정 추가 // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
afterUploadFlows: config.excelAfterUploadFlows, afterUploadFlows,
}; };
} }

View File

@ -231,6 +231,17 @@ export interface FormulaTransformNodeData {
value?: string | number; value?: string | number;
resultField?: string; 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") // 함수 (formulaType === "function")