diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 013b2034..ed2576cd 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -50,6 +50,9 @@ export class EntityJoinController { // search가 문자열인 경우 JSON 파싱 searchConditions = typeof search === "string" ? JSON.parse(search) : search; + + // 🔍 디버그: 파싱된 검색 조건 로깅 + logger.info(`🔍 파싱된 검색 조건:`, JSON.stringify(searchConditions, null, 2)); } catch (error) { logger.warn("검색 조건 파싱 오류:", error); searchConditions = {}; 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/components/table-category/CategoryValueAddDialog.tsx b/frontend/components/table-category/CategoryValueAddDialog.tsx index 9d962b22..303022bd 100644 --- a/frontend/components/table-category/CategoryValueAddDialog.tsx +++ b/frontend/components/table-category/CategoryValueAddDialog.tsx @@ -14,6 +14,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { TableCategoryValue } from "@/types/tableCategoryValue"; // 기본 색상 팔레트 @@ -51,6 +52,7 @@ export const CategoryValueAddDialog: React.FC< const [valueLabel, setValueLabel] = useState(""); const [description, setDescription] = useState(""); const [color, setColor] = useState("none"); + const [continuousAdd, setContinuousAdd] = useState(false); // 연속 입력 체크박스 // 라벨에서 코드 자동 생성 (항상 고유한 코드 생성) const generateCode = (): string => { @@ -60,6 +62,12 @@ export const CategoryValueAddDialog: React.FC< return `CATEGORY_${timestamp}${random}`; }; + const resetForm = () => { + setValueLabel(""); + setDescription(""); + setColor("none"); + }; + const handleSubmit = () => { if (!valueLabel.trim()) { return; @@ -77,14 +85,28 @@ export const CategoryValueAddDialog: React.FC< isDefault: false, } as TableCategoryValue); - // 초기화 - setValueLabel(""); - setDescription(""); - setColor("none"); + // 연속 입력 체크되어 있으면 폼만 초기화하고 모달 유지 + if (continuousAdd) { + resetForm(); + } else { + // 연속 입력 아니면 모달 닫기 + resetForm(); + onOpenChange(false); + } + }; + + const handleClose = () => { + resetForm(); + onOpenChange(false); }; return ( - + { + if (!isOpen) { + resetForm(); + } + onOpenChange(isOpen); + }}> @@ -165,24 +187,42 @@ export const CategoryValueAddDialog: React.FC< - - - + + {/* 연속 입력 체크박스 */} +
+ setContinuousAdd(checked as boolean)} + /> + +
+ +
+ + +
); }; - diff --git a/frontend/components/table-category/CategoryValueManager.tsx b/frontend/components/table-category/CategoryValueManager.tsx index 98d23bae..aba04d6a 100644 --- a/frontend/components/table-category/CategoryValueManager.tsx +++ b/frontend/components/table-category/CategoryValueManager.tsx @@ -123,7 +123,7 @@ export const CategoryValueManager: React.FC = ({ if (response.success && response.data) { await loadCategoryValues(); - setIsAddDialogOpen(false); + // 모달 닫기는 CategoryValueAddDialog에서 연속 입력 체크박스로 제어 toast({ title: "성공", description: "카테고리 값이 추가되었습니다", @@ -142,7 +142,7 @@ export const CategoryValueManager: React.FC = ({ title: "오류", description: error.message || "카테고리 값 추가에 실패했습니다", variant: "destructive", - }); + }); } }; diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 9fc8c161..71503e91 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1147,8 +1147,28 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 추가 탭 데이터 로딩 함수 const loadTabData = useCallback( async (tabIndex: number, leftItem: any) => { + console.log(`📥 loadTabData 호출됨: tabIndex=${tabIndex}`, { + leftItem: leftItem ? Object.keys(leftItem) : null, + additionalTabs: componentConfig.rightPanel?.additionalTabs?.length, + isDesignMode, + }); + const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1]; - if (!tabConfig || !leftItem || isDesignMode) return; + + console.log(`📥 tabConfig:`, { + tabIndex, + configIndex: tabIndex - 1, + tabConfig: tabConfig ? { + tableName: tabConfig.tableName, + relation: tabConfig.relation, + dataFilter: tabConfig.dataFilter + } : null, + }); + + if (!tabConfig || !leftItem || isDesignMode) { + console.log(`⚠️ loadTabData 중단:`, { hasTabConfig: !!tabConfig, hasLeftItem: !!leftItem, isDesignMode }); + return; + } const tabTableName = tabConfig.tableName; if (!tabTableName) return; @@ -1160,6 +1180,14 @@ export const SplitPanelLayoutComponent: React.FC const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn; const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn; + console.log(`🔑 [추가탭 ${tabIndex}] 조인 키 분석:`, { + hasRelation: !!tabConfig.relation, + keys, + leftColumn, + rightColumn, + willUseJoin: !!(leftColumn && rightColumn), + }); + let resultData: any[] = []; if (leftColumn && rightColumn) { @@ -1171,14 +1199,22 @@ export const SplitPanelLayoutComponent: React.FC // 복합키 keys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { - searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + // operator: "equals"를 추가하여 정확한 값 매칭 (entity 타입 컬럼에서 코드값으로 검색) + searchConditions[key.rightColumn] = { + value: leftItem[key.leftColumn], + operator: "equals", + }; } }); } else { // 단일키 const leftValue = leftItem[leftColumn]; if (leftValue !== undefined) { - searchConditions[rightColumn] = leftValue; + // operator: "equals"를 추가하여 정확한 값 매칭 (entity 타입 컬럼에서 코드값으로 검색) + searchConditions[rightColumn] = { + value: leftValue, + operator: "equals", + }; } } @@ -1193,43 +1229,68 @@ export const SplitPanelLayoutComponent: React.FC resultData = result.data || []; } else { // 조인 조건이 없는 경우: 전체 데이터 조회 (독립 탭) + console.log(`📋 [추가탭 ${tabIndex}] 조인 없이 전체 데이터 조회: ${tabTableName}`); const { entityJoinApi } = await import("@/lib/api/entityJoin"); const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { enableEntityJoin: true, size: 1000, }); resultData = result.data || []; + console.log(`📋 [추가탭 ${tabIndex}] 전체 데이터 조회 결과:`, resultData.length); } // 데이터 필터 적용 const dataFilter = tabConfig.dataFilter; // filters 또는 conditions 배열 지원 (DataFilterConfigPanel은 filters 사용) const filterConditions = dataFilter?.filters || dataFilter?.conditions || []; + + console.log(`🔍 [추가탭 ${tabIndex}] 필터 설정:`, { + enabled: dataFilter?.enabled, + filterConditions, + dataBeforeFilter: resultData.length, + }); + if (dataFilter?.enabled && filterConditions.length > 0) { + const beforeCount = resultData.length; resultData = resultData.filter((item: any) => { return filterConditions.every((cond: any) => { // columnName 또는 column 지원 const columnName = cond.columnName || cond.column; const value = item[columnName]; const condValue = cond.value; + + let result = true; switch (cond.operator) { case "equals": - return value === condValue; + result = value === condValue; + break; case "notEquals": - return value !== condValue; + result = value !== condValue; + break; case "contains": - return String(value).includes(String(condValue)); + result = String(value).includes(String(condValue)); + break; case "is_null": case "NULL": - return value === null || value === undefined || value === ""; + result = value === null || value === undefined || value === ""; + break; case "is_not_null": case "NOT NULL": - return value !== null && value !== undefined && value !== ""; + result = value !== null && value !== undefined && value !== ""; + break; default: - return true; + result = true; } + + // 첫 5개 항목만 로그 출력 + if (resultData.indexOf(item) < 5) { + console.log(` 필터 체크: ${columnName}=${value}, operator=${cond.operator}, result=${result}`); + } + + return result; }); }); + console.log(`🔍 [추가탭 ${tabIndex}] 필터 적용 후: ${beforeCount} → ${resultData.length}`); } // 중복 제거 적용 @@ -1301,6 +1362,12 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 탭 변경 핸들러 const handleTabChange = useCallback( (newTabIndex: number) => { + console.log(`🔄 탭 변경: ${activeTabIndex} → ${newTabIndex}`, { + selectedLeftItem: !!selectedLeftItem, + tabsData: Object.keys(tabsData), + hasTabData: !!tabsData[newTabIndex], + }); + setActiveTabIndex(newTabIndex); // 선택된 좌측 항목이 있으면 해당 탭의 데이터 로드 @@ -1311,14 +1378,15 @@ export const SplitPanelLayoutComponent: React.FC loadRightData(selectedLeftItem); } } else { - // 추가 탭: 해당 탭 데이터가 없으면 로드 - if (!tabsData[newTabIndex]) { - loadTabData(newTabIndex, selectedLeftItem); - } + // 추가 탭: 항상 새로 로드 (필터 설정 변경 반영을 위해) + console.log(`🔄 추가 탭 ${newTabIndex} 데이터 로드 (항상 새로고침)`); + loadTabData(newTabIndex, selectedLeftItem); } + } else { + console.log(`⚠️ 좌측 항목이 선택되지 않아 탭 데이터를 로드하지 않음`); } }, - [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData], + [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData, activeTabIndex], ); // 우측 항목 확장/축소 토글 diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index 9810388f..048fb385 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -237,7 +237,12 @@ const AdditionalTabConfigPanel: React.FC = ({ // 탭 업데이트 헬퍼 const updateTab = (updates: Partial) => { const newTabs = [...(config.rightPanel?.additionalTabs || [])]; - newTabs[tabIndex] = { ...tab, ...updates }; + // undefined 값도 명시적으로 덮어쓰기 위해 Object.assign 대신 직접 처리 + const updatedTab = { ...tab }; + Object.keys(updates).forEach((key) => { + (updatedTab as any)[key] = (updates as any)[key]; + }); + newTabs[tabIndex] = updatedTab; updateRightPanel({ additionalTabs: newTabs }); }; @@ -393,21 +398,31 @@ const AdditionalTabConfigPanel: React.FC = ({
{ - updateTab({ - relation: { - ...tab.relation, - type: "join", - keys: [{ leftColumn: tab.relation?.keys?.[0]?.leftColumn || "", rightColumn: value }], - }, - }); + if (value === "__none__") { + // 선택 안 함 - 조인 키 제거 + updateTab({ + relation: undefined, + }); + } else { + updateTab({ + relation: { + ...tab.relation, + type: "join", + keys: [{ leftColumn: tab.relation?.keys?.[0]?.leftColumn || "", rightColumn: value }], + }, + }); + } }} > + + 선택 안 함 (전체 데이터) + {tabColumns.map((col) => ( {col.columnLabel || col.columnName} diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 662f6379..57c50c58 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -393,6 +393,14 @@ export function UniversalFormModalComponent({ console.log("[UniversalFormModal] beforeFormSave 이벤트 수신"); console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields)); + // 🆕 시스템 필드 병합: id는 설정 여부와 관계없이 항상 전달 (UPDATE/INSERT 판단용) + // - 신규 등록: formData.id가 없으므로 영향 없음 + // - 편집 모드: formData.id가 있으면 메인 테이블 UPDATE에 사용 + if (formData.id !== undefined && formData.id !== null && formData.id !== "") { + event.detail.formData.id = formData.id; + console.log(`[UniversalFormModal] 시스템 필드 병합: id =`, formData.id); + } + // UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함) // 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀 // (UniversalFormModal이 해당 필드의 주인이므로) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 8bc65cca..a54850ee 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1971,19 +1971,43 @@ export class ButtonActionExecutor { } }); - console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave); + // 🆕 메인 테이블 UPDATE/INSERT 판단 + // - formData.id가 있으면 편집 모드 → UPDATE + // - formData.id가 없으면 신규 등록 → INSERT + const existingMainId = formData.id; + const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== ""; - const mainSaveResult = await DynamicFormApi.saveFormData({ - screenId: screenId!, - tableName: tableName!, - data: mainRowToSave, + console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave); + console.log("📦 [handleUniversalFormModalTableSectionSave] UPDATE/INSERT 판단:", { + existingMainId, + isMainUpdate, }); + let mainSaveResult: { success: boolean; data?: any; message?: string }; + + if (isMainUpdate) { + // 🔄 편집 모드: UPDATE 실행 + console.log("🔄 [handleUniversalFormModalTableSectionSave] 메인 테이블 UPDATE 실행, ID:", existingMainId); + mainSaveResult = await DynamicFormApi.updateFormData(existingMainId, { + tableName: tableName!, + data: mainRowToSave, + }); + mainRecordId = existingMainId; + } else { + // ➕ 신규 등록: INSERT 실행 + console.log("➕ [handleUniversalFormModalTableSectionSave] 메인 테이블 INSERT 실행"); + mainSaveResult = await DynamicFormApi.saveFormData({ + screenId: screenId!, + tableName: tableName!, + data: mainRowToSave, + }); + mainRecordId = mainSaveResult.data?.id || null; + } + if (!mainSaveResult.success) { throw new Error(mainSaveResult.message || "메인 데이터 저장 실패"); } - mainRecordId = mainSaveResult.data?.id || null; console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId); } @@ -4996,6 +5020,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 +5036,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 +5049,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")