From 1d068e0a2046f75c81d0a452c6c6837a51b513c1 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 22 Jan 2026 14:23:38 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=94=BC=ED=84=B0=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditModal, InteractiveScreenViewer, SaveModal 컴포넌트에서 리피터 데이터(배열)를 마스터 저장에서 제외하고, 별도로 저장하는 로직을 추가하였습니다. - 리피터 데이터 저장 이벤트를 발생시켜 UnifiedRepeater 컴포넌트가 이를 리스닝하도록 개선하였습니다. - 각 컴포넌트에서 최종 저장 데이터 로그를 업데이트하여, 저장 과정에서의 데이터 흐름을 명확히 하였습니다. 이로 인해 데이터 저장의 효율성과 리피터 관리의 일관성이 향상되었습니다. --- frontend/components/screen/EditModal.tsx | 29 +- .../screen/InteractiveScreenViewer.tsx | 27 +- .../screen/InteractiveScreenViewerDynamic.tsx | 13 +- frontend/components/screen/SaveModal.tsx | 27 +- .../components/unified/UnifiedRepeater.tsx | 106 ++- .../unified/registerUnifiedComponents.ts | 1 - frontend/hooks/useMultiLang.ts | 1 - .../lib/registry/DynamicComponentRenderer.tsx | 56 +- .../modal-repeater-table/RepeaterTable.tsx | 11 - .../SimpleRepeaterTableComponent.tsx | 55 +- .../table-list/TableListComponent.tsx | 116 +-- .../UnifiedRepeaterRenderer.tsx | 14 +- .../v2-table-list/TableListComponent.tsx | 53 +- .../UnifiedRepeaterRenderer.tsx | 14 +- frontend/lib/utils/buttonActions.ts | 875 ++++-------------- 15 files changed, 441 insertions(+), 957 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 48bf898f..16c99629 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -811,15 +811,40 @@ export const EditModal: React.FC = ({ className }) => { } } - console.log("[EditModal] 최종 저장 데이터:", dataToSave); + // 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장) + const masterDataToSave: Record = {}; + Object.entries(dataToSave).forEach(([key, value]) => { + if (!Array.isArray(value)) { + masterDataToSave[key] = value; + } else { + console.log(`🔄 [EditModal] 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`); + } + }); + + console.log("[EditModal] 최종 저장 데이터:", masterDataToSave); const response = await dynamicFormApi.saveFormData({ screenId: modalState.screenId!, tableName: screenData.screenInfo.tableName, - data: dataToSave, + data: masterDataToSave, }); if (response.success) { + const masterRecordId = response.data?.id || formData.id; + + // 🆕 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝) + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: masterRecordId, + masterRecordId, + mainFormData: formData, + tableName: screenData.screenInfo.tableName, + }, + }), + ); + console.log("📋 [EditModal] repeaterSave 이벤트 발생:", { masterRecordId, tableName: screenData.screenInfo.tableName }); + toast.success("데이터가 생성되었습니다."); // 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침) diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 8d5a6562..6456ac17 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1655,10 +1655,20 @@ export const InteractiveScreenViewer: React.FC = ( company_code: mappedData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode }; + // 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장) + const masterDataWithUserInfo: Record = {}; + Object.entries(dataWithUserInfo).forEach(([key, value]) => { + if (!Array.isArray(value)) { + masterDataWithUserInfo[key] = value; + } else { + console.log(`🔄 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`); + } + }); + const saveData: DynamicFormData = { screenId: screenInfo.id, tableName: tableName, - data: dataWithUserInfo, + data: masterDataWithUserInfo, }; console.log("🚀 API 저장 요청:", saveData); @@ -1666,6 +1676,21 @@ export const InteractiveScreenViewer: React.FC = ( const result = await dynamicFormApi.saveFormData(saveData); if (result.success) { + const masterRecordId = result.data?.id || formData.id; + + // 🆕 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝) + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: masterRecordId, + masterRecordId, + mainFormData: formData, + tableName: tableName, + }, + }), + ); + console.log("📋 repeaterSave 이벤트 발생:", { masterRecordId, tableName }); + alert("저장되었습니다."); // console.log("✅ 저장 성공:", result.data); diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 15f620d6..5c80c81e 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -532,9 +532,20 @@ export const InteractiveScreenViewerDynamic: React.FC = {}; + Object.entries(formData).forEach(([key, value]) => { + // 배열 데이터는 리피터 데이터이므로 제외 + if (!Array.isArray(value)) { + masterFormData[key] = value; + } else { + console.log(`🔄 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`); + } + }); + const saveData: DynamicFormData = { tableName: screenInfo.tableName, - data: formData, + data: masterFormData, }; // console.log("💾 저장 액션 실행:", saveData); diff --git a/frontend/components/screen/SaveModal.tsx b/frontend/components/screen/SaveModal.tsx index 61b2e2c5..f2f82292 100644 --- a/frontend/components/screen/SaveModal.tsx +++ b/frontend/components/screen/SaveModal.tsx @@ -206,13 +206,23 @@ export const SaveModal: React.FC = ({ company_code: dataToSave.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode }; + // 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장) + const masterDataWithUserInfo: Record = {}; + Object.entries(dataWithUserInfo).forEach(([key, value]) => { + if (!Array.isArray(value)) { + masterDataWithUserInfo[key] = value; + } else { + console.log(`🔄 [SaveModal] 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`); + } + }); + // 테이블명 결정 const tableName = screenData.tableName || components.find((c) => c.columnName)?.tableName || "dynamic_form_data"; const saveData: DynamicFormData = { screenId: screenId, tableName: tableName, - data: dataWithUserInfo, + data: masterDataWithUserInfo, }; console.log("💾 저장 요청 데이터:", saveData); @@ -221,6 +231,21 @@ export const SaveModal: React.FC = ({ const result = await dynamicFormApi.saveFormData(saveData); if (result.success) { + const masterRecordId = result.data?.id || dataToSave.id; + + // 🆕 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝) + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: masterRecordId, + masterRecordId, + mainFormData: dataToSave, + tableName: tableName, + }, + }), + ); + console.log("📋 [SaveModal] repeaterSave 이벤트 발생:", { masterRecordId, tableName }); + // ✅ 저장 성공 toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!"); diff --git a/frontend/components/unified/UnifiedRepeater.tsx b/frontend/components/unified/UnifiedRepeater.tsx index 1437b5cf..487ad190 100644 --- a/frontend/components/unified/UnifiedRepeater.tsx +++ b/frontend/components/unified/UnifiedRepeater.tsx @@ -85,21 +85,25 @@ export const UnifiedRepeater: React.FC = ({ const isModalMode = config.renderMode === "modal"; // 전역 리피터 등록 + // 🆕 useCustomTable이 설정된 경우 mainTableName 사용 (실제 저장될 테이블) useEffect(() => { - const tableName = config.dataSource?.tableName; - if (tableName) { + const targetTableName = config.useCustomTable && config.mainTableName + ? config.mainTableName + : config.dataSource?.tableName; + + if (targetTableName) { if (!window.__unifiedRepeaterInstances) { window.__unifiedRepeaterInstances = new Set(); } - window.__unifiedRepeaterInstances.add(tableName); + window.__unifiedRepeaterInstances.add(targetTableName); } return () => { - if (tableName && window.__unifiedRepeaterInstances) { - window.__unifiedRepeaterInstances.delete(tableName); + if (targetTableName && window.__unifiedRepeaterInstances) { + window.__unifiedRepeaterInstances.delete(targetTableName); } }; - }, [config.dataSource?.tableName]); + }, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]); // 저장 이벤트 리스너 useEffect(() => { @@ -115,11 +119,11 @@ export const UnifiedRepeater: React.FC = ({ const masterRecordId = event.detail?.masterRecordId || mainFormData?.id; if (!tableName || data.length === 0) { - console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length }); return; } - console.log("📋 UnifiedRepeater 저장 시작:", { + // UnifiedRepeater 저장 시작 + const saveInfo = { tableName, useCustomTable: config.useCustomTable, mainTableName: config.mainTableName, @@ -152,10 +156,24 @@ export const UnifiedRepeater: React.FC = ({ // 커스텀 테이블: 리피터 데이터만 저장 mergedData = { ...cleanRow }; - // 🆕 FK 자동 연결 - if (config.foreignKeyColumn && masterRecordId) { - mergedData[config.foreignKeyColumn] = masterRecordId; - console.log(`📎 FK 자동 연결: ${config.foreignKeyColumn} = ${masterRecordId}`); + // 🆕 FK 자동 연결 - foreignKeySourceColumn이 설정된 경우 해당 컬럼 값 사용 + if (config.foreignKeyColumn) { + // foreignKeySourceColumn이 있으면 mainFormData에서 해당 컬럼 값 사용 + // 없으면 마스터 레코드 ID 사용 (기존 동작) + const sourceColumn = config.foreignKeySourceColumn; + let fkValue: any; + + if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) { + // mainFormData에서 참조 컬럼 값 가져오기 + fkValue = mainFormData[sourceColumn]; + } else { + // 기본: 마스터 레코드 ID 사용 + fkValue = masterRecordId; + } + + if (fkValue !== undefined && fkValue !== null) { + mergedData[config.foreignKeyColumn] = fkValue; + } } } else { // 기존 방식: 메인 폼 데이터 병합 @@ -177,7 +195,6 @@ export const UnifiedRepeater: React.FC = ({ await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } - console.log("✅ UnifiedRepeater 저장 완료:", data.length, "건 →", tableName); } catch (error) { console.error("❌ UnifiedRepeater 저장 실패:", error); throw error; @@ -252,13 +269,6 @@ export const UnifiedRepeater: React.FC = ({ config.dataSource?.referenceKey || "id"; - console.log("🔄 [UnifiedRepeater] 엔티티 참조 정보 조회:", { - foreignKey, - resolvedSourceTable: refTable, - resolvedReferenceKey: refKey, - configSourceTable: config.dataSource?.sourceTable, - }); - setResolvedSourceTable(refTable); setResolvedReferenceKey(refKey); } else { @@ -424,11 +434,29 @@ export const UnifiedRepeater: React.FC = ({ const handleDataChange = useCallback( (newData: any[]) => { setData(newData); - onDataChange?.(newData); + + // 🆕 _targetTable 메타데이터 포함하여 전달 (백엔드에서 테이블 분리용) + if (onDataChange) { + const targetTable = config.useCustomTable && config.mainTableName + ? config.mainTableName + : config.dataSource?.tableName; + + if (targetTable) { + // 각 행에 _targetTable 추가 + const dataWithTarget = newData.map(row => ({ + ...row, + _targetTable: targetTable, + })); + onDataChange(dataWithTarget); + } else { + onDataChange(newData); + } + } + // 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 setAutoWidthTrigger((prev) => prev + 1); }, - [onDataChange], + [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], ); // 행 변경 핸들러 @@ -436,11 +464,26 @@ export const UnifiedRepeater: React.FC = ({ (index: number, newRow: any) => { const newData = [...data]; newData[index] = newRow; - // 🆕 handleDataChange 대신 직접 호출 (행 변경마다 너비 조정은 불필요) setData(newData); - onDataChange?.(newData); + + // 🆕 _targetTable 메타데이터 포함 + if (onDataChange) { + const targetTable = config.useCustomTable && config.mainTableName + ? config.mainTableName + : config.dataSource?.tableName; + + if (targetTable) { + const dataWithTarget = newData.map(row => ({ + ...row, + _targetTable: targetTable, + })); + onDataChange(dataWithTarget); + } else { + onDataChange(newData); + } + } }, - [data, onDataChange], + [data, onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], ); // 행 삭제 핸들러 @@ -672,13 +715,6 @@ export const UnifiedRepeater: React.FC = ({ return; } - console.log("📥 [UnifiedRepeater] componentDataTransfer 수신:", { - targetComponentId, - dataCount: transferData?.length, - mode, - myId: parentId, - }); - if (!transferData || transferData.length === 0) { return; } @@ -719,12 +755,6 @@ export const UnifiedRepeater: React.FC = ({ const customEvent = event as CustomEvent; const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {}; - console.log("📥 [UnifiedRepeater] splitPanelDataTransfer 수신:", { - dataCount: transferData?.length, - mode, - sourcePosition, - }); - if (!transferData || transferData.length === 0) { return; } diff --git a/frontend/components/unified/registerUnifiedComponents.ts b/frontend/components/unified/registerUnifiedComponents.ts index e1065f7a..5675ccba 100644 --- a/frontend/components/unified/registerUnifiedComponents.ts +++ b/frontend/components/unified/registerUnifiedComponents.ts @@ -192,7 +192,6 @@ export function registerUnifiedComponents(): void { continue; } ComponentRegistry.registerComponent(definition); - console.log(`✅ Unified 컴포넌트 등록: ${definition.id}`); } catch (error) { console.error(`❌ Unified 컴포넌트 등록 실패: ${definition.id}`, error); } diff --git a/frontend/hooks/useMultiLang.ts b/frontend/hooks/useMultiLang.ts index 64e56876..652d6a77 100644 --- a/frontend/hooks/useMultiLang.ts +++ b/frontend/hooks/useMultiLang.ts @@ -66,7 +66,6 @@ export const useMultiLang = (options: { companyCode?: string } = {}) => { const storedLocaleLoaded = localStorage.getItem("userLocaleLoaded"); if (storedLocaleLoaded === "true" && storedLocale) { - console.log("🌐 localStorage에서 사용자 로케일 사용:", storedLocale); setUserLang(storedLocale); globalUserLang = storedLocale; diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index b137c884..2a7440f6 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -441,10 +441,20 @@ export const DynamicComponentRenderer: React.FC = ); case "unified-repeater": + // 🆕 저장 설정 추출 (useCustomTable, mainTableName, foreignKeyColumn) + const repeaterTargetTable = config.useCustomTable && config.mainTableName + ? config.mainTableName + : config.dataSource?.tableName; + return ( = }} parentId={props.formData?.[config.dataSource?.referenceKey] || props.formData?.id} onDataChange={(data) => { - console.log("UnifiedRepeater data changed:", data); + // 🆕 formData 업데이트 (부모로 데이터 전달) + if (props.onFormDataChange) { + // _targetTable 메타데이터 추가 + const dataWithTargetTable = data.map((item: any) => ({ + ...item, + _targetTable: repeaterTargetTable, + })); + props.onFormDataChange(component.id || "repeaterData", dataWithTargetTable); + } }} onRowClick={(row) => { - console.log("UnifiedRepeater row clicked:", row); }} onButtonClick={(action, row, buttonConfig) => { - console.log("UnifiedRepeater button clicked:", action, row, buttonConfig); }} /> ); @@ -793,17 +809,6 @@ export const DynamicComponentRenderer: React.FC = NewComponentRenderer.prototype && NewComponentRenderer.prototype.render; - if (componentType === "table-search-widget") { - console.log("🔍 [DynamicComponentRenderer] table-search-widget 렌더링 분기:", { - isClass, - hasPrototype: !!NewComponentRenderer.prototype, - hasRender: !!NewComponentRenderer.prototype?.render, - componentName: NewComponentRenderer.name, - componentProp: rendererProps.component, - screenId: rendererProps.screenId, - }); - } - if (isClass) { // 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속) const rendererInstance = new NewComponentRenderer(rendererProps); @@ -812,29 +817,6 @@ export const DynamicComponentRenderer: React.FC = // 함수형 컴포넌트 // refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제 - // 🔧 디버깅: table-search-widget인 경우 직접 호출 후 반환 - if (componentType === "table-search-widget") { - console.log("🔧🔧🔧 [DynamicComponentRenderer] TableSearchWidget 직접 호출 반환"); - console.log("🔧 [DynamicComponentRenderer] NewComponentRenderer 함수 확인:", { - name: NewComponentRenderer.name, - toString: NewComponentRenderer.toString().substring(0, 200), - }); - try { - const result = NewComponentRenderer(rendererProps); - console.log("🔧 [DynamicComponentRenderer] TableSearchWidget 결과 상세:", { - resultType: typeof result, - type: result?.type?.name || result?.type || "unknown", - propsKeys: result?.props ? Object.keys(result.props) : [], - propsStyle: result?.props?.style, - propsChildren: typeof result?.props?.children, - }); - // 직접 호출 결과를 반환 - return result; - } catch (directCallError) { - console.error("❌ [DynamicComponentRenderer] TableSearchWidget 직접 호출 실패:", directCallError); - } - } - return ; } } diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index e6876a49..4aa4d930 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -102,26 +102,18 @@ export function RepeaterTable({ const loadCategoryOptions = async () => { // category 타입이면서 categoryRef가 있는 컬럼들 찾기 const categoryColumns = visibleColumns.filter((col) => col.type === "category"); - console.log( - "🔍 [RepeaterTable] 카테고리 컬럼 확인:", - categoryColumns.map((col) => ({ field: col.field, type: col.type, categoryRef: col.categoryRef })), - ); const categoryRefs = categoryColumns .filter((col) => col.categoryRef) .map((col) => col.categoryRef!) .filter((ref, index, self) => self.indexOf(ref) === index); // 중복 제거 - console.log("🔍 [RepeaterTable] categoryRefs:", categoryRefs); - if (categoryRefs.length === 0) { - console.log("⚠️ [RepeaterTable] categoryRef가 있는 컬럼이 없음"); return; } for (const categoryRef of categoryRefs) { if (categoryOptionsMap[categoryRef]) { - console.log(`⏭️ [RepeaterTable] ${categoryRef} 이미 로드됨`); continue; } @@ -141,16 +133,13 @@ export function RepeaterTable({ continue; } - console.log(`🌐 [RepeaterTable] API 호출: /table-categories/${tableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); - console.log("📥 [RepeaterTable] API 응답:", response.data); if (response.data?.success && response.data.data) { const options = response.data.data.map((item: any) => ({ value: item.valueCode || item.value_code, label: item.valueLabel || item.value_label || item.displayValue || item.display_value || item.label, })); - console.log(`✅ [RepeaterTable] ${categoryRef} 옵션 로드 성공:`, options); setCategoryOptionsMap((prev) => ({ ...prev, [categoryRef]: options, diff --git a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx index add34d5f..564ba780 100644 --- a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx @@ -90,6 +90,10 @@ export function SimpleRepeaterTableComponent({ const newRowDefaults = componentConfig?.newRowDefaults || {}; const summaryConfig = componentConfig?.summaryConfig; const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px"; + + // 🆕 컴포넌트 레벨의 저장 테이블 설정 + const componentTargetTable = componentConfig?.targetTable || componentConfig?.saveTable; + const componentFkColumn = componentConfig?.fkColumn; // value는 formData[columnName] 우선, 없으면 prop 사용 const columnName = component?.columnName; @@ -289,7 +293,44 @@ export function SimpleRepeaterTableComponent({ return; } - // 🆕 테이블별로 데이터 그룹화 + // 🆕 컴포넌트 레벨의 targetTable이 설정되어 있으면 우선 사용 + if (componentTargetTable) { + console.log("✅ [SimpleRepeaterTable] 컴포넌트 레벨 저장 테이블 사용:", componentTargetTable); + + // 모든 행을 해당 테이블에 저장 + const dataToSave = value.map((row: any) => { + // 메타데이터 필드 제외 (_, _rowIndex 등) + const cleanRow: Record = {}; + Object.keys(row).forEach((key) => { + if (!key.startsWith("_")) { + cleanRow[key] = row[key]; + } + }); + return { + ...cleanRow, + _targetTable: componentTargetTable, + }; + }); + + // CustomEvent의 detail에 데이터 추가 + if (event instanceof CustomEvent && event.detail) { + const key = columnName || component?.id || "repeater_data"; + event.detail.formData[key] = dataToSave; + console.log("✅ [SimpleRepeaterTable] 저장 데이터 준비:", { + key, + targetTable: componentTargetTable, + itemCount: dataToSave.length, + }); + } + + // 기존 onFormDataChange도 호출 (호환성) + if (onFormDataChange && columnName) { + onFormDataChange(columnName, dataToSave); + } + return; + } + + // 🆕 컬럼별 targetConfig가 있는 경우 기존 로직 사용 const dataByTable: Record = {}; for (const row of value) { @@ -318,6 +359,16 @@ export function SimpleRepeaterTableComponent({ } } + // 컬럼별 설정도 없으면 기본 동작 (formData에 직접 추가) + if (Object.keys(dataByTable).length === 0) { + console.log("⚠️ [SimpleRepeaterTable] targetTable 설정 없음 - 기본 저장"); + if (event instanceof CustomEvent && event.detail) { + const key = columnName || component?.id || "repeater_data"; + event.detail.formData[key] = value; + } + return; + } + // _rowIndex 제거 Object.keys(dataByTable).forEach((tableName) => { dataByTable[tableName] = dataByTable[tableName].map((row: any) => { @@ -360,7 +411,7 @@ export function SimpleRepeaterTableComponent({ return () => { window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener); }; - }, [value, columns, columnName, component?.id, onFormDataChange]); + }, [value, columns, columnName, component?.id, onFormDataChange, componentTargetTable]); const handleCellEdit = (rowIndex: number, field: string, cellValue: any) => { const newRow = { ...value[rowIndex], [field]: cellValue }; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 3d6521c3..0c439ca9 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -260,9 +260,7 @@ export const TableListComponent: React.FC = ({ // 객체인 경우 tableName 속성 추출 시도 if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) { - console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable); finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName; - console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable); } tableConfig.selectedTable = finalSelectedTable; @@ -353,12 +351,6 @@ export const TableListComponent: React.FC = ({ } }); - console.log("🔍 [TableListComponent] filters → searchValues:", { - filtersCount: filters.length, - filters: filters.map((f) => ({ col: f.columnName, op: f.operator, val: f.value })), - searchValues: newSearchValues, - }); - setSearchValues(newSearchValues); setCurrentPage(1); // 필터 변경 시 첫 페이지로 }, [filters]); @@ -761,7 +753,6 @@ export const TableListComponent: React.FC = ({ }); if (hasChanges) { - console.log("🔗 [TableList] 연결된 필터 값 변경:", newFilterValues); setLinkedFilterValues(newFilterValues); // searchValues에 연결된 필터 값 병합 @@ -817,13 +808,6 @@ export const TableListComponent: React.FC = ({ componentType: "table", receiveData: async (receivedData: any[], config: DataReceiverConfig) => { - console.log("📥 TableList 데이터 수신:", { - componentId: component.id, - receivedDataCount: receivedData.length, - mode: config.mode, - currentDataCount: data.length, - }); - try { let newData: any[] = []; @@ -831,13 +815,11 @@ export const TableListComponent: React.FC = ({ case "append": // 기존 데이터에 추가 newData = [...data, ...receivedData]; - console.log("✅ Append 모드: 기존 데이터에 추가", { newDataCount: newData.length }); break; case "replace": // 기존 데이터를 완전히 교체 newData = receivedData; - console.log("✅ Replace 모드: 데이터 교체", { newDataCount: newData.length }); break; case "merge": @@ -853,7 +835,6 @@ export const TableListComponent: React.FC = ({ } }); newData = Array.from(existingMap.values()); - console.log("✅ Merge 모드: 데이터 병합", { newDataCount: newData.length }); break; } @@ -862,10 +843,7 @@ export const TableListComponent: React.FC = ({ // 총 아이템 수 업데이트 setTotalItems(newData.length); - - console.log("✅ 데이터 수신 완료:", { finalDataCount: newData.length }); } catch (error) { - console.error("❌ 데이터 수신 실패:", error); throw error; } }, @@ -899,7 +877,8 @@ export const TableListComponent: React.FC = ({ componentId: component.id, componentType: "table-list", receiveData: async (incomingData: any[], mode: "append" | "replace" | "merge") => { - console.log("📥 [TableListComponent] 분할 패널에서 데이터 수신:", { + // 분할 패널에서 데이터 수신 처리 + const receiveInfo = { count: incomingData.length, mode, position: currentSplitPosition, @@ -937,24 +916,12 @@ export const TableListComponent: React.FC = ({ // 컬럼의 고유 값 조회 함수 const getColumnUniqueValues = async (columnName: string) => { - console.log("🔍 [getColumnUniqueValues] 호출됨:", { - columnName, - dataLength: data.length, - columnMeta: columnMeta[columnName], - sampleData: data[0], - }); - const meta = columnMeta[columnName]; const inputType = meta?.inputType || "text"; // 카테고리 타입인 경우 전체 정의된 값 조회 (백엔드 API) if (inputType === "category") { try { - console.log("🔍 [getColumnUniqueValues] 카테고리 전체 값 조회:", { - tableName: tableConfig.selectedTable, - columnName, - }); - // API 클라이언트 사용 (쿠키 인증 자동 처리) const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); @@ -965,24 +932,9 @@ export const TableListComponent: React.FC = ({ label: item.valueLabel, // 카멜케이스 })); - console.log("✅ [getColumnUniqueValues] 카테고리 전체 값:", { - columnName, - count: categoryOptions.length, - options: categoryOptions, - }); - return categoryOptions; - } else { - console.warn("⚠️ [getColumnUniqueValues] 응답 형식 오류:", response.data); } - } catch (error: any) { - console.error("❌ [getColumnUniqueValues] 카테고리 조회 실패:", { - error: error.message, - response: error.response?.data, - status: error.response?.status, - columnName, - tableName: tableConfig.selectedTable, - }); + } catch { // 에러 시 현재 데이터 기반으로 fallback } } @@ -991,15 +943,6 @@ export const TableListComponent: React.FC = ({ const isLabelType = ["category", "entity", "code"].includes(inputType); const labelField = isLabelType ? `${columnName}_name` : columnName; - console.log("🔍 [getColumnUniqueValues] 데이터 기반 조회:", { - columnName, - inputType, - isLabelType, - labelField, - hasLabelField: data[0] && labelField in data[0], - sampleLabelValue: data[0] ? data[0][labelField] : undefined, - }); - // 현재 로드된 데이터에서 고유 값 추출 const uniqueValuesMap = new Map(); // value -> label @@ -1020,15 +963,6 @@ export const TableListComponent: React.FC = ({ })) .sort((a, b) => a.label.localeCompare(b.label)); - console.log("✅ [getColumnUniqueValues] 데이터 기반 결과:", { - columnName, - inputType, - isLabelType, - labelField, - uniqueCount: result.length, - values: result, - }); - return result; }; @@ -1105,10 +1039,9 @@ export const TableListComponent: React.FC = ({ setSortColumn(column); setSortDirection(direction); hasInitializedSort.current = true; - console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction }); } - } catch (error) { - console.error("❌ 정렬 상태 복원 실패:", error); + } catch { + // 정렬 상태 복원 실패 - 무시 } } }, [tableConfig.selectedTable, userId]); @@ -1124,12 +1057,10 @@ export const TableListComponent: React.FC = ({ if (savedOrder) { try { const parsedOrder = JSON.parse(savedOrder); - console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder }); setColumnOrder(parsedOrder); // 부모 컴포넌트에 초기 컬럼 순서 전달 if (onSelectedRowsChange && parsedOrder.length > 0) { - console.log("✅ 초기 컬럼 순서 전달:", parsedOrder); // 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬) const initialData = data.map((row: any) => { @@ -1177,8 +1108,8 @@ export const TableListComponent: React.FC = ({ onSelectedRowsChange([], [], sortColumn, sortDirection, parsedOrder, initialData); } - } catch (error) { - console.error("❌ 컬럼 순서 파싱 실패:", error); + } catch { + // 컬럼 순서 파싱 실패 - 무시 } } }, [tableConfig.selectedTable, userId, data.length]); // data.length 추가 (데이터 로드 후 실행) @@ -1336,11 +1267,6 @@ export const TableListComponent: React.FC = ({ const parts = columnName.split("."); targetTable = parts[0]; // 조인된 테이블명 (예: item_info) targetColumn = parts[1]; // 실제 컬럼명 (예: material) - console.log("🔗 [TableList] 엔티티 조인 컬럼 감지:", { - originalColumn: columnName, - targetTable, - targetColumn, - }); } const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); @@ -1438,8 +1364,6 @@ export const TableListComponent: React.FC = ({ // 조인 테이블의 컬럼 inputType 정보 가져오기 (이미 import된 tableTypeApi 사용) const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable); - console.log(`📡 [TableList] 조인 테이블 inputType 로드 [${joinedTable}]:`, inputTypes); - for (const col of columns) { const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn); @@ -1448,17 +1372,9 @@ export const TableListComponent: React.FC = ({ inputType: inputTypeInfo?.inputType, }; - console.log( - ` 🔗 [${col.columnName}] (실제: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`, - ); - // inputType이 category인 경우 카테고리 매핑 로드 if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) { try { - console.log(`📡 [TableList] 조인 테이블 카테고리 로드 시도 [${col.columnName}]:`, { - url: `/table-categories/${joinedTable}/${col.actualColumn}/values`, - }); - const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`); if (response.data.success && response.data.data && Array.isArray(response.data.data)) { @@ -1476,20 +1392,19 @@ export const TableListComponent: React.FC = ({ mappings[col.columnName] = mapping; } } - } catch (error) { - console.log(`ℹ️ [TableList] 조인 테이블 카테고리 없음 (${col.columnName})`); + } catch { + // 조인 테이블 카테고리 없음 - 무시 } } } } catch (error) { - console.error(`❌ [TableList] 조인 테이블 inputType 로드 실패 [${joinedTable}]:`, error); + console.error(`조인 테이블 inputType 로드 실패 [${joinedTable}]:`, error); } } // 조인 컬럼 메타데이터 상태 업데이트 if (Object.keys(newJoinedColumnMeta).length > 0) { setJoinedColumnMeta(newJoinedColumnMeta); - console.log("✅ [TableList] 조인 컬럼 메타데이터 설정:", newJoinedColumnMeta); } // 🆕 카테고리 연쇄관계 매핑 로드 (category_value_cascading_mapping) @@ -1515,26 +1430,17 @@ export const TableListComponent: React.FC = ({ }; } } - console.log("✅ [TableList] 카테고리 연쇄관계 매핑 로드 완료:", { - tableName: tableConfig.selectedTable, - cascadingColumns: Object.keys(cascadingMappings), - }); } } catch (cascadingError: any) { // 연쇄관계 매핑이 없는 경우 무시 (404 등) - if (cascadingError?.response?.status !== 404) { - console.warn("⚠️ [TableList] 카테고리 연쇄관계 매핑 로드 실패:", cascadingError?.message); - } } if (Object.keys(mappings).length > 0) { setCategoryMappings(mappings); setCategoryMappingsKey((prev) => prev + 1); - } else { - console.warn("⚠️ [TableList] 매핑이 비어있어 상태 업데이트 스킵"); } } catch (error) { - console.error("❌ [TableList] 카테고리 매핑 로드 실패:", error); + console.error("카테고리 매핑 로드 실패:", error); } }; diff --git a/frontend/lib/registry/components/unified-repeater/UnifiedRepeaterRenderer.tsx b/frontend/lib/registry/components/unified-repeater/UnifiedRepeaterRenderer.tsx index 9c4af25e..bf1aec8e 100644 --- a/frontend/lib/registry/components/unified-repeater/UnifiedRepeaterRenderer.tsx +++ b/frontend/lib/registry/components/unified-repeater/UnifiedRepeaterRenderer.tsx @@ -32,9 +32,19 @@ const UnifiedRepeaterRenderer: React.FC = ({ onButtonClick, parentId, }) => { - // component.config에서 UnifiedRepeaterConfig 추출 + // component.componentConfig 또는 component.config에서 UnifiedRepeaterConfig 추출 const config: UnifiedRepeaterConfig = React.useMemo(() => { - const componentConfig = component?.config || component?.props?.config || {}; + // 🆕 componentConfig 우선 (DB에서 properties.componentConfig로 저장됨) + const componentConfig = component?.componentConfig || component?.config || component?.props?.config || {}; + + console.log("📋 UnifiedRepeaterRenderer config 추출:", { + hasComponentConfig: !!component?.componentConfig, + hasConfig: !!component?.config, + useCustomTable: componentConfig.useCustomTable, + mainTableName: componentConfig.mainTableName, + foreignKeyColumn: componentConfig.foreignKeyColumn, + }); + return { ...DEFAULT_REPEATER_CONFIG, ...componentConfig, diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 93626b7b..e24f5a2f 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -1313,11 +1313,6 @@ export const TableListComponent: React.FC = ({ const parts = columnName.split("."); targetTable = parts[0]; // 조인된 테이블명 (예: item_info) targetColumn = parts[1]; // 실제 컬럼명 (예: material) - console.log("🔗 [TableList] 엔티티 조인 컬럼 감지:", { - originalColumn: columnName, - targetTable, - targetColumn, - }); } const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); @@ -1358,21 +1353,9 @@ export const TableListComponent: React.FC = ({ } else { // 매핑 데이터가 비어있음 - 해당 컬럼에 카테고리 값이 없음 } - } else { - console.warn(`⚠️ [TableList] 카테고리 값 없음 [${columnName}]:`, { - success: response.data.success, - hasData: !!response.data.data, - isArray: Array.isArray(response.data.data), - response: response.data, - }); } - } catch (error: any) { - console.error(`❌ [TableList] 카테고리 값 로드 실패 [${columnName}]:`, { - error: error.message, - stack: error.stack, - response: error.response?.data, - status: error.response?.status, - }); + } catch { + // 카테고리 값 로드 실패 - 무시 } } @@ -1433,8 +1416,6 @@ export const TableListComponent: React.FC = ({ // 조인 테이블의 컬럼 inputType 정보 가져오기 (이미 import된 tableTypeApi 사용) const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable); - console.log(`📡 [TableList] 조인 테이블 inputType 로드 [${joinedTable}]:`, inputTypes); - for (const col of columns) { const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn); @@ -1443,17 +1424,9 @@ export const TableListComponent: React.FC = ({ inputType: inputTypeInfo?.inputType, }; - console.log( - ` 🔗 [${col.columnName}] (실제: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`, - ); - // inputType이 category인 경우 카테고리 매핑 로드 if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) { try { - console.log(`📡 [TableList] 조인 테이블 카테고리 로드 시도 [${col.columnName}]:`, { - url: `/table-categories/${joinedTable}/${col.actualColumn}/values`, - }); - const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`); if (response.data.success && response.data.data && Array.isArray(response.data.data)) { @@ -1481,20 +1454,19 @@ export const TableListComponent: React.FC = ({ mappings[col.columnName] = mapping; } } - } catch (error) { - console.log(`ℹ️ [TableList] 조인 테이블 카테고리 없음 (${col.columnName})`); + } catch { + // 조인 테이블 카테고리 없음 - 무시 } } } - } catch (error) { - console.error(`❌ [TableList] 조인 테이블 inputType 로드 실패 [${joinedTable}]:`, error); + } catch { + // 조인 테이블 inputType 로드 실패 - 무시 } } // 조인 컬럼 메타데이터 상태 업데이트 if (Object.keys(newJoinedColumnMeta).length > 0) { setJoinedColumnMeta(newJoinedColumnMeta); - console.log("✅ [TableList] 조인 컬럼 메타데이터 설정:", newJoinedColumnMeta); } // 🆕 카테고리 연쇄관계 매핑 로드 (category_value_cascading_mapping) @@ -1522,21 +1494,16 @@ export const TableListComponent: React.FC = ({ } // 카테고리 연쇄관계 매핑 로드 완료 } - } catch (cascadingError: any) { - // 연쇄관계 매핑이 없는 경우 무시 (404 등) - if (cascadingError?.response?.status !== 404) { - console.warn("⚠️ [TableList] 카테고리 연쇄관계 매핑 로드 실패:", cascadingError?.message); - } + } catch { + // 연쇄관계 매핑이 없는 경우 무시 } if (Object.keys(mappings).length > 0) { setCategoryMappings(mappings); setCategoryMappingsKey((prev) => prev + 1); - } else { - console.warn("⚠️ [TableList] 매핑이 비어있어 상태 업데이트 스킵"); } - } catch (error) { - console.error("❌ [TableList] 카테고리 매핑 로드 실패:", error); + } catch { + // 카테고리 매핑 로드 실패 - 무시 } }; diff --git a/frontend/lib/registry/components/v2-unified-repeater/UnifiedRepeaterRenderer.tsx b/frontend/lib/registry/components/v2-unified-repeater/UnifiedRepeaterRenderer.tsx index 210f5bd0..480046d2 100644 --- a/frontend/lib/registry/components/v2-unified-repeater/UnifiedRepeaterRenderer.tsx +++ b/frontend/lib/registry/components/v2-unified-repeater/UnifiedRepeaterRenderer.tsx @@ -32,9 +32,19 @@ const UnifiedRepeaterRenderer: React.FC = ({ onButtonClick, parentId, }) => { - // component.config에서 UnifiedRepeaterConfig 추출 + // component.componentConfig 또는 component.config에서 UnifiedRepeaterConfig 추출 const config: UnifiedRepeaterConfig = React.useMemo(() => { - const componentConfig = component?.config || component?.props?.config || {}; + // 🆕 componentConfig 우선 (DB에서 properties.componentConfig로 저장됨) + const componentConfig = component?.componentConfig || component?.config || component?.props?.config || {}; + + console.log("📋 V2UnifiedRepeaterRenderer config 추출:", { + hasComponentConfig: !!component?.componentConfig, + hasConfig: !!component?.config, + useCustomTable: componentConfig.useCustomTable, + mainTableName: componentConfig.mainTableName, + foreignKeyColumn: componentConfig.foreignKeyColumn, + }); + return { ...DEFAULT_REPEATER_CONFIG, ...componentConfig, diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index cfc0fc31..29db571d 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -331,19 +331,19 @@ export function resolveSpecialKeyword(sourceField: string | undefined, context: // 특수 키워드 처리 switch (sourceField) { case "__userId__": - console.log("🔑 특수 키워드 변환: __userId__ →", context.userId); + return context.userId; case "__userName__": - console.log("🔑 특수 키워드 변환: __userName__ →", context.userName); + return context.userName; case "__companyCode__": - console.log("🔑 특수 키워드 변환: __companyCode__ →", context.companyCode); + return context.companyCode; case "__screenId__": - console.log("🔑 특수 키워드 변환: __screenId__ →", context.screenId); + return context.screenId; case "__tableName__": - console.log("🔑 특수 키워드 변환: __tableName__ →", context.tableName); + return context.tableName; default: // 일반 폼 데이터에서 가져오기 @@ -458,12 +458,7 @@ export class ButtonActionExecutor { const value = formData[columnName]; // 값이 비어있는지 확인 (null, undefined, 빈 문자열, 공백만 있는 문자열) if (value === null || value === undefined || (typeof value === "string" && value.trim() === "")) { - console.log("🔍 [validateRequiredFields] 필수 항목 누락:", { - columnName, - label, - value, - isRequired, - }); + missingFields.push(label || columnName); } } @@ -494,42 +489,25 @@ export class ButtonActionExecutor { const now = Date.now(); const timeDiff = now - lastCallTime; - console.log(`🔒 [handleSave #${callId}] 락 체크:`, { lockKey: lockKey.slice(0, 50), timeDiff, threshold: 2000 }); - if (timeDiff < 2000) { - console.log(`⏭️ [handleSave #${callId}] 중복 호출 무시 (2초 내 재호출):`, { - lockKey: lockKey.slice(0, 50), - timeDiff, - }); + return true; // 중복 호출은 성공으로 처리 } this.saveLock.set(lockKey, now); - console.log(`💾 [handleSave #${callId}] 저장 시작:`, { - callId, - formDataKeys: Object.keys(formData), - tableName, - screenId, - hasOnSave: !!onSave, - }); - // ✅ 필수 항목 검증 - console.log("🔍 [handleSave] 필수 항목 검증 시작:", { - hasAllComponents: !!context.allComponents, - allComponentsLength: context.allComponents?.length || 0, - }); + const requiredValidation = this.validateRequiredFields(context); if (!requiredValidation.isValid) { console.log("❌ [handleSave] 필수 항목 누락:", requiredValidation.missingFields); toast.error(`필수 항목을 입력해주세요: ${requiredValidation.missingFields.join(", ")}`); return false; } - console.log("✅ [handleSave] 필수 항목 검증 통과"); // 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 if (onSave) { - console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행"); + try { await onSave(); return true; @@ -559,12 +537,10 @@ export class ButtonActionExecutor { // 🔧 skipDefaultSave 플래그 확인 - SelectedItemsDetailInput 등에서 자체 UPSERT 처리 시 기본 저장 건너뛰기 if (beforeSaveEventDetail.skipDefaultSave) { - console.log("🚫 [handleSave] skipDefaultSave=true - 기본 저장 로직 건너뛰기 (컴포넌트에서 자체 처리)"); + return true; } - console.log("📦 [handleSave] beforeFormSave 이벤트 후 formData:", context.formData); - // 🆕 렉 구조 컴포넌트 일괄 저장 감지 let rackStructureLocations: any[] | undefined; let rackStructureFieldKey = "_rackStructureLocations"; @@ -588,7 +564,7 @@ export class ButtonActionExecutor { firstItem.levelNum !== undefined; if (isNewFormat || isOldFormat) { - console.log("🏗️ [handleSave] 렉 구조 데이터 감지 - 필드:", key); + rackStructureLocations = value; rackStructureFieldKey = key; break; @@ -601,7 +577,7 @@ export class ButtonActionExecutor { comp.type === "component" && comp.componentId === "rack-structure" && comp.columnName === key, ); if (rackStructureComponentInLayout) { - console.log("🏗️ [handleSave] 렉 구조 컴포넌트 감지 (미리보기 없음) - 필드:", key); + hasEmptyRackStructureField = true; rackStructureFieldKey = key; } @@ -624,7 +600,7 @@ export class ButtonActionExecutor { !rackStructureLocations; if (isRackStructureScreen) { - console.log("🏗️ [handleSave] 렉 구조 등록 화면 감지 - 미리보기 데이터 없음"); + alert( "렉 구조 등록 화면입니다.\n\n" + "미리보기를 먼저 생성해주세요.\n" + @@ -636,28 +612,11 @@ export class ButtonActionExecutor { // 렉 구조 데이터가 있으면 일괄 저장 if (rackStructureLocations && rackStructureLocations.length > 0) { - console.log("🏗️ [handleSave] 렉 구조 컴포넌트 감지 - 일괄 저장 시작:", rackStructureLocations.length, "개"); + return await this.handleRackStructureBatchSave(config, context, rackStructureLocations, rackStructureFieldKey); } // 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조) - console.log("🔍 [handleSave] formData 구조 확인:", { - isFormDataArray: Array.isArray(context.formData), - keys: Object.keys(context.formData), - values: Object.entries(context.formData).map(([key, value]) => ({ - key, - isArray: Array.isArray(value), - length: Array.isArray(value) ? value.length : 0, - firstItem: - Array.isArray(value) && value.length > 0 - ? { - hasOriginalData: !!value[0]?.originalData, - hasFieldGroups: !!value[0]?.fieldGroups, - keys: Object.keys(value[0] || {}), - } - : null, - })), - }); // 🔧 formData 자체가 배열인 경우 (ScreenModal의 그룹 레코드 수정) if (Array.isArray(context.formData)) { @@ -671,24 +630,12 @@ export class ButtonActionExecutor { const selectedItemsKeys = Object.keys(context.formData).filter((key) => { const value = context.formData[key]; - console.log(`🔍 [handleSave] 필터링 체크 - ${key}:`, { - isArray: Array.isArray(value), - length: Array.isArray(value) ? value.length : 0, - firstItem: - Array.isArray(value) && value.length > 0 - ? { - keys: Object.keys(value[0] || {}), - hasOriginalData: !!value[0]?.originalData, - hasFieldGroups: !!value[0]?.fieldGroups, - actualValue: value[0], - } - : null, - }); + return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups; }); if (selectedItemsKeys.length > 0) { - console.log("🔄 [handleSave] SelectedItemsDetailInput 배치 저장 감지:", selectedItemsKeys); + return await this.handleBatchSave(config, context, selectedItemsKeys); } else { console.log("⚠️ [handleSave] SelectedItemsDetailInput 데이터 감지 실패 - 일반 저장 진행"); @@ -711,18 +658,12 @@ export class ButtonActionExecutor { }); if (repeaterJsonKeys.length > 0) { - console.log("🔄 [handleSave] RepeaterFieldGroup JSON 문자열 감지:", repeaterJsonKeys); // 🎯 채번 규칙 할당 처리 (RepeaterFieldGroup 저장 전에 실행) // 🔧 수정 모드 체크: formData.id가 존재하면 UPDATE 모드이므로 채번 코드 재할당 금지 const isEditModeRepeater = context.formData.id !== undefined && context.formData.id !== null && context.formData.id !== ""; - console.log("🔍 [handleSave-RepeaterFieldGroup] 채번 규칙 할당 체크 시작", { - isEditMode: isEditModeRepeater, - formDataId: context.formData.id, - }); - const fieldsWithNumberingRepeater: Record = {}; // formData에서 채번 규칙이 설정된 필드 찾기 @@ -730,30 +671,24 @@ export class ButtonActionExecutor { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumberingRepeater[fieldName] = value as string; - console.log(`🎯 [handleSave-RepeaterFieldGroup] 채번 필드 발견: ${fieldName} → 규칙 ${value}`); + } } - console.log("📋 [handleSave-RepeaterFieldGroup] 채번 규칙이 설정된 필드:", fieldsWithNumberingRepeater); - // 🔧 수정 모드에서는 채번 코드 할당 건너뛰기 (기존 번호 유지) // 신규 등록 모드에서만 allocateCode 호출하여 새 번호 할당 if (Object.keys(fieldsWithNumberingRepeater).length > 0 && !isEditModeRepeater) { - console.log( - "🎯 [handleSave-RepeaterFieldGroup] 신규 등록 모드 - 채번 규칙 할당 시작 (allocateCode 호출)", - ); + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) { try { - console.log(`🔄 [handleSave-RepeaterFieldGroup] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`); + const allocateResult = await allocateNumberingCode(ruleId); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; - console.log( - `✅ [handleSave-RepeaterFieldGroup] ${fieldName} 새 코드 할당: ${context.formData[fieldName]} → ${newCode}`, - ); + context.formData[fieldName] = newCode; } else { console.warn(`⚠️ [handleSave-RepeaterFieldGroup] ${fieldName} 코드 할당 실패:`, allocateResult.error); @@ -763,11 +698,9 @@ export class ButtonActionExecutor { } } } else if (isEditModeRepeater) { - console.log("⏭️ [handleSave-RepeaterFieldGroup] 수정 모드 - 채번 코드 할당 건너뜀 (기존 번호 유지)"); + } - console.log("✅ [handleSave-RepeaterFieldGroup] 채번 규칙 할당 처리 완료"); - // 🆕 상단 폼 데이터(마스터 정보) 추출 // RepeaterFieldGroup JSON과 컴포넌트 키를 제외한 나머지가 마스터 정보 const masterFields: Record = {}; @@ -798,9 +731,7 @@ export class ButtonActionExecutor { masterFields[fieldKey] = value; } }); - - console.log("📋 [handleSave] 상단 마스터 정보 (모든 품목에 적용):", masterFields); - + for (const key of repeaterJsonKeys) { try { const parsedData = JSON.parse(context.formData[key]); @@ -811,14 +742,10 @@ export class ButtonActionExecutor { continue; } - console.log(`📦 [handleSave] RepeaterFieldGroup 저장 시작: ${repeaterTargetTable}, ${parsedData.length}건`); - // 🆕 품목 고유 필드 목록 (RepeaterFieldGroup 설정에서 가져옴) // 첫 번째 아이템의 _repeaterFields에서 추출 const repeaterFields: string[] = parsedData[0]?._repeaterFields || []; const itemOnlyFields = new Set([...repeaterFields, 'id']); // id는 항상 포함 - - console.log("📋 [handleSave] RepeaterFieldGroup 품목 필드:", repeaterFields); for (const item of parsedData) { // 메타 필드 제거 @@ -850,7 +777,7 @@ export class ButtonActionExecutor { if (sourceValue !== undefined && sourceValue !== null) { itemOnlyData[targetField] = sourceValue; - console.log(`📋 [handleSave] 하위 데이터 값 매핑: ${sourceColumn} → ${targetField} = ${sourceValue}`); + } } }); @@ -861,7 +788,7 @@ export class ButtonActionExecutor { const subDataValue = _subDataSelection[subDataKey]; if (subDataValue !== undefined && subDataValue !== null) { itemOnlyData[subDataKey] = subDataValue; - console.log(`📋 [handleSave] 하위 데이터 선택 값 추가 (레거시): ${subDataKey} = ${subDataValue}`); + } } }); @@ -889,11 +816,6 @@ export class ButtonActionExecutor { // 새 레코드 vs 기존 레코드 판단 const isNewRecord = _isNewItem || !item.id || item.id === "" || item.id === undefined; - console.log(`📦 [handleSave] 저장할 데이터 (${isNewRecord ? 'INSERT' : 'UPDATE'}):`, { - id: item.id, - dataWithMeta, - }); - if (isNewRecord) { // INSERT - DynamicFormApi 사용하여 제어관리 실행 delete dataWithMeta.id; @@ -903,7 +825,7 @@ export class ButtonActionExecutor { tableName: repeaterTargetTable, data: dataWithMeta as Record, }); - console.log("✅ [handleSave] RepeaterFieldGroup INSERT 완료:", insertResult.data); + } else if (item.id && _existingRecord === true) { // UPDATE - 기존 레코드 const originalData = { id: item.id }; @@ -913,7 +835,7 @@ export class ButtonActionExecutor { originalData, updatedData, }); - console.log("✅ [handleSave] RepeaterFieldGroup UPDATE 완료:", updateResult.data); + } } } catch (err) { @@ -922,7 +844,7 @@ export class ButtonActionExecutor { } // RepeaterFieldGroup 저장 완료 후 새로고침 - console.log("✅ [handleSave] RepeaterFieldGroup 저장 완료"); + context.onRefresh?.(); context.onFlowRefresh?.(); window.dispatchEvent(new CustomEvent("closeEditModal")); @@ -935,7 +857,7 @@ export class ButtonActionExecutor { // 범용_폼_모달 내부에 _tableSection_ 데이터가 있는 경우 공통 필드 + 개별 품목 병합 저장 const universalFormModalResult = await this.handleUniversalFormModalTableSectionSave(config, context, formData); if (universalFormModalResult.handled) { - console.log("✅ [handleSave] Universal Form Modal 테이블 섹션 저장 완료"); + return universalFormModalResult.success; } @@ -988,32 +910,16 @@ export class ButtonActionExecutor { const hasIdInFormData = formData.id !== undefined && formData.id !== null && formData.id !== ""; const isUpdate = (hasRealOriginalData || hasIdInFormData) && !!primaryKeyValue; - console.log("🔍 [handleSave] INSERT/UPDATE 판단:", { - hasOriginalData: !!originalData, - hasRealOriginalData, - hasIdInFormData, - originalDataKeys: originalData ? Object.keys(originalData) : [], - primaryKeyValue, - isUpdate, - primaryKeys, - }); - if (isUpdate) { // UPDATE 처리 - 부분 업데이트 사용 (원본 데이터가 있는 경우) - console.log("🔄 UPDATE 모드로 저장:", { - primaryKeyValue, - hasOriginalData: !!originalData, - hasIdInFormData, - updateReason: hasRealOriginalData ? "originalData 존재" : "formData.id 존재 (폴백)", - }); if (hasRealOriginalData) { // 부분 업데이트: 변경된 필드만 업데이트 - console.log("📝 부분 업데이트 실행 (변경된 필드만)"); + saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName); } else { // 전체 업데이트 (originalData 없이 id로 UPDATE 판단된 경우) - console.log("📝 전체 업데이트 실행 (originalData 없음 - 폴백 모드)"); + saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, { tableName, data: formData, @@ -1041,7 +947,6 @@ export class ButtonActionExecutor { // }); // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가) - console.log("🔍 채번 규칙 할당 체크 시작"); const fieldsWithNumbering: Record = {}; @@ -1050,16 +955,13 @@ export class ButtonActionExecutor { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; - console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`); + } } - console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering); - console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length); - // 🔥 저장 시점에 allocateCode 호출하여 실제 순번 증가 if (Object.keys(fieldsWithNumbering).length > 0) { - console.log("🎯 채번 규칙 할당 시작 (allocateCode 호출)"); + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); let hasAllocationFailure = false; @@ -1067,12 +969,12 @@ export class ButtonActionExecutor { for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { - console.log(`🔄 ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`); + const allocateResult = await allocateNumberingCode(ruleId); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; - console.log(`✅ ${fieldName} 새 코드 할당: ${formData[fieldName]} → ${newCode}`); + formData[fieldName] = newCode; } else { console.warn(`⚠️ ${fieldName} 코드 할당 실패:`, allocateResult.error); @@ -1102,9 +1004,6 @@ export class ButtonActionExecutor { } } - console.log("✅ 채번 규칙 할당 완료"); - console.log("📦 최종 formData:", JSON.stringify(formData, null, 2)); - // 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터) // 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 병합해야 함 // 모든 필드를 병합하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생 @@ -1141,13 +1040,12 @@ export class ButtonActionExecutor { const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern)); if (isLinkField) { cleanedSplitPanelData[key] = value; - console.log(`🔗 [handleSave] INSERT 모드 - 연결 필드만 병합: ${key} = ${value}`); + } } if (Object.keys(rawSplitPanelData).length > 0) { - console.log("🧹 [handleSave] 원본 분할 패널 부모 데이터:", Object.keys(rawSplitPanelData)); - console.log("🧹 [handleSave] 정리된 분할 패널 부모 데이터 (연결 필드만):", cleanedSplitPanelData); + } const dataWithUserInfo = { @@ -1161,7 +1059,7 @@ export class ButtonActionExecutor { // 🔧 formData에서도 id 제거 (신규 INSERT이므로) if ("id" in dataWithUserInfo && !formData.id) { - console.log("🗑️ [handleSave] INSERT 모드 - dataWithUserInfo에서 id 제거:", dataWithUserInfo.id); + delete dataWithUserInfo.id; } @@ -1172,17 +1070,19 @@ export class ButtonActionExecutor { } } + // 🆕 배열 데이터(리피터 데이터) 제거 - 마스터 테이블에는 배열 데이터를 저장하지 않음 + // 리피터 데이터는 별도의 RepeaterFieldGroup/UnifiedRepeater 저장 로직에서 처리됨 + for (const key of Object.keys(dataWithUserInfo)) { + if (Array.isArray(dataWithUserInfo[key])) { + + delete dataWithUserInfo[key]; + } + } + // 🆕 반복 필드 그룹에서 삭제된 항목 처리 // formData의 각 필드에서 _deletedItemIds가 있는지 확인 - console.log("🔍 [handleSave] 삭제 항목 검색 시작 - dataWithUserInfo 키:", Object.keys(dataWithUserInfo)); for (const [key, value] of Object.entries(dataWithUserInfo)) { - console.log(`🔍 [handleSave] 필드 검사: ${key}`, { - type: typeof value, - isArray: Array.isArray(value), - isString: typeof value === "string", - valuePreview: typeof value === "string" ? value.substring(0, 100) : value, - }); let parsedValue = value; @@ -1190,7 +1090,7 @@ export class ButtonActionExecutor { if (typeof value === "string" && value.startsWith("[")) { try { parsedValue = JSON.parse(value); - console.log(`🔍 [handleSave] JSON 파싱 성공: ${key}`, parsedValue); + } catch (e) { // 파싱 실패하면 원본 값 유지 } @@ -1201,26 +1101,15 @@ export class ButtonActionExecutor { const deletedItemIds = firstItem?._deletedItemIds; const targetTable = firstItem?._targetTable; - console.log(`🔍 [handleSave] 배열 필드 분석: ${key}`, { - firstItemKeys: firstItem ? Object.keys(firstItem) : [], - deletedItemIds, - targetTable, - }); - if (deletedItemIds && deletedItemIds.length > 0 && targetTable) { - console.log("🗑️ [handleSave] 삭제할 항목 발견:", { - fieldKey: key, - targetTable, - deletedItemIds, - }); // 삭제 API 호출 - screenId 전달하여 제어관리 실행 가능하도록 함 for (const itemId of deletedItemIds) { try { - console.log(`🗑️ [handleSave] 항목 삭제 중: ${itemId} from ${targetTable}`); + const deleteResult = await DynamicFormApi.deleteFormDataFromTable(itemId, targetTable, context.screenId); if (deleteResult.success) { - console.log(`✅ [handleSave] 항목 삭제 완료: ${itemId}`); + } else { console.warn(`⚠️ [handleSave] 항목 삭제 실패: ${itemId}`, deleteResult.message); } @@ -1234,18 +1123,12 @@ export class ButtonActionExecutor { // 🆕 RepeaterFieldGroup 데이터 저장 처리 (_targetTable이 있는 배열 데이터) // formData에서 _targetTable 메타데이터가 포함된 배열 필드 찾기 - console.log("🔎 [handleSave] formData 키 목록:", Object.keys(context.formData)); - console.log("🔎 [handleSave] formData 전체:", context.formData); // 🆕 마스터-디테일 저장: 테이블 간 조인 관계 캐시 const joinRelationshipCache: Record = {}; for (const [fieldKey, fieldValue] of Object.entries(context.formData)) { - console.log(`🔎 [handleSave] 필드 검사: ${fieldKey}`, { - type: typeof fieldValue, - isArray: Array.isArray(fieldValue), - valuePreview: typeof fieldValue === "string" ? fieldValue.slice(0, 100) : fieldValue, - }); + // JSON 문자열인 경우 파싱 let parsedData = fieldValue; if (typeof fieldValue === "string" && fieldValue.startsWith("[")) { @@ -1265,9 +1148,14 @@ export class ButtonActionExecutor { // _targetTable이 없거나, _repeatScreenModal_ 키면 스킵 (다른 로직에서 처리) if (!repeaterTargetTable || fieldKey.startsWith("_repeatScreenModal_")) continue; - console.log(`📦 [handleSave] RepeaterFieldGroup 데이터 저장: ${fieldKey} → ${repeaterTargetTable}`, { - itemCount: parsedData.length, - }); + // 🆕 UnifiedRepeater가 등록된 테이블이면 RepeaterFieldGroup 저장 스킵 + // UnifiedRepeater가 repeaterSave 이벤트로 저장 처리함 + // @ts-ignore - window에 동적 속성 사용 + const registeredUnifiedRepeaterTables = Array.from(window.__unifiedRepeaterInstances || []); + if (registeredUnifiedRepeaterTables.includes(repeaterTargetTable)) { + + continue; + } // 🆕 마스터-디테일 조인 관계 조회 (메인 테이블 → 리피터 테이블) let joinRelationship: { mainColumn: string; detailColumn: string } | null = null; @@ -1287,7 +1175,7 @@ export class ButtonActionExecutor { mainColumn: joinResponse.data.data.mainColumn, detailColumn: joinResponse.data.data.detailColumn, }; - console.log(`🔗 [handleSave] 조인 관계 발견: ${tableName}.${joinRelationship.mainColumn} → ${repeaterTargetTable}.${joinRelationship.detailColumn}`); + } } catch (joinError) { console.warn(`⚠️ [handleSave] 조인 관계 조회 실패:`, joinError); @@ -1308,7 +1196,7 @@ export class ButtonActionExecutor { commonFields[key] = value; } } - console.log("📋 [handleSave] 범용 폼 모달 공통 필드:", commonFields); + } // 🆕 루트 레벨 formData에서 RepeaterFieldGroup에 전달할 공통 필드 추출 @@ -1332,7 +1220,6 @@ export class ButtonActionExecutor { // 위 규칙에 해당하지 않는 단순 값(문자열, 숫자, 날짜 등)은 공통 필드로 전달 commonFields[fieldName] = value; } - console.log("📋 [handleSave] 최종 공통 필드 (규칙 기반 자동 추출):", commonFields); // 🆕 마스터-디테일 조인: 메인 테이블의 조인 컬럼 값을 commonFields에 추가 if (joinRelationship) { @@ -1340,7 +1227,7 @@ export class ButtonActionExecutor { if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") { // 리피터 테이블의 조인 컬럼에 메인 테이블의 값 주입 commonFields[joinRelationship.detailColumn] = mainColumnValue; - console.log(`🔗 [handleSave] 조인 컬럼 값 주입: ${joinRelationship.detailColumn} = ${mainColumnValue}`); + } else { console.warn(`⚠️ [handleSave] 조인 컬럼 값이 없음: ${joinRelationship.mainColumn}`); } @@ -1386,12 +1273,12 @@ export class ButtonActionExecutor { if ("id" in dataWithMeta && (dataWithMeta.id === "" || dataWithMeta.id === null)) { delete dataWithMeta.id; } - console.log(`📝 [handleSave] RepeaterFieldGroup INSERT (${repeaterTargetTable}):`, dataWithMeta); + const insertResult = await apiClient.post( `/table-management/tables/${repeaterTargetTable}/add`, dataWithMeta, ); - console.log("✅ [handleSave] RepeaterFieldGroup INSERT 완료:", insertResult.data); + // 무시된 컬럼이 있으면 경고 출력 if (insertResult.data?.data?.skippedColumns?.length > 0) { console.warn( @@ -1403,15 +1290,12 @@ export class ButtonActionExecutor { // UPDATE (기존 항목) const originalData = { id: item.id }; const updatedData = { ...dataWithMeta, id: item.id }; - console.log("📝 [handleSave] RepeaterFieldGroup UPDATE:", { - id: item.id, - table: repeaterTargetTable, - }); + const updateResult = await apiClient.put(`/table-management/tables/${repeaterTargetTable}/edit`, { originalData, updatedData, }); - console.log("✅ [handleSave] RepeaterFieldGroup UPDATE 완료:", updateResult.data); + } } catch (err) { const error = err as { response?: { data?: unknown; status?: number }; message?: string }; @@ -1465,11 +1349,7 @@ export class ButtonActionExecutor { unifiedRepeaterTables.includes(tableName); if (shouldSkipMainSave) { - console.log(`⏭️ [handleSave] ${tableName} 메인 저장 건너뜀`, { - repeatScreenModalTables, - repeaterFieldGroupTables, - unifiedRepeaterTables, - }); + saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/UnifiedRepeater에서 처리" }; } else { saveResult = await DynamicFormApi.saveFormData({ @@ -1480,7 +1360,6 @@ export class ButtonActionExecutor { } if (repeatScreenModalKeys.length > 0) { - console.log("📦 [handleSave] RepeatScreenModal 데이터 저장 시작:", repeatScreenModalKeys); // 🆕 formData에서 채번 규칙으로 생성된 값들 추출 (예: shipment_plan_no) const numberingFields: Record = {}; @@ -1490,7 +1369,6 @@ export class ButtonActionExecutor { numberingFields[fieldKey] = value; } } - console.log("📦 [handleSave] 채번 규칙 필드:", numberingFields); for (const key of repeatScreenModalKeys) { const targetTable = key.replace("_repeatScreenModal_", ""); @@ -1514,7 +1392,7 @@ export class ButtonActionExecutor { mainColumn: joinResponse.data.data.mainColumn, detailColumn: joinResponse.data.data.detailColumn, }; - console.log(`🔗 [handleSave] RepeatScreenModal 조인 관계 발견: ${tableName}.${joinRelationship.mainColumn} → ${targetTable}.${joinRelationship.detailColumn}`); + } } catch (joinError) { console.warn(`⚠️ [handleSave] RepeatScreenModal 조인 관계 조회 실패:`, joinError); @@ -1529,12 +1407,10 @@ export class ButtonActionExecutor { const mainColumnValue = context.formData[joinRelationship.mainColumn]; if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") { joinColumnData[joinRelationship.detailColumn] = mainColumnValue; - console.log(`🔗 [handleSave] RepeatScreenModal 조인 컬럼 값: ${joinRelationship.detailColumn} = ${mainColumnValue}`); + } } - console.log(`📦 [handleSave] ${targetTable} 테이블 저장:`, rows); - for (const row of rows) { const { _isNew, _targetTable, id, ...dataToSave } = row; @@ -1551,22 +1427,22 @@ export class ButtonActionExecutor { try { if (_isNew) { // INSERT - console.log(`📝 [handleSave] ${targetTable} INSERT:`, dataWithMeta); + const insertResult = await apiClient.post( `/table-management/tables/${targetTable}/add`, dataWithMeta, ); - console.log(`✅ [handleSave] ${targetTable} INSERT 완료:`, insertResult.data); + } else if (id) { // UPDATE const originalData = { id }; const updatedData = { ...dataWithMeta, id }; - console.log(`📝 [handleSave] ${targetTable} UPDATE:`, { originalData, updatedData }); + const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, { originalData, updatedData, }); - console.log(`✅ [handleSave] ${targetTable} UPDATE 완료:`, updateResult.data); + } } catch (error: any) { console.error(`❌ [handleSave] ${targetTable} 저장 실패:`, error.response?.data || error.message); @@ -1579,7 +1455,6 @@ export class ButtonActionExecutor { // 🆕 v2-repeat-container 데이터 저장 처리 (_repeatContainerTables에 그룹화된 데이터) const repeatContainerTables = context.formData._repeatContainerTables as Record | undefined; if (repeatContainerTables && Object.keys(repeatContainerTables).length > 0) { - console.log("📦 [handleSave] v2-RepeatContainer 데이터 저장 시작:", Object.keys(repeatContainerTables)); for (const [targetTable, rows] of Object.entries(repeatContainerTables)) { if (!Array.isArray(rows) || rows.length === 0) continue; @@ -1600,7 +1475,7 @@ export class ButtonActionExecutor { mainColumn: joinResponse.data.data.mainColumn, detailColumn: joinResponse.data.data.detailColumn, }; - console.log(`🔗 [handleSave] RepeatContainer 조인 관계 발견: ${tableName}.${joinRelationship.mainColumn} → ${targetTable}.${joinRelationship.detailColumn}`); + } } catch (joinError) { console.warn(`⚠️ [handleSave] RepeatContainer 조인 관계 조회 실패:`, joinError); @@ -1615,18 +1490,16 @@ export class ButtonActionExecutor { const mainColumnValue = context.formData[joinRelationship.mainColumn]; if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") { joinColumnData[joinRelationship.detailColumn] = mainColumnValue; - console.log(`🔗 [handleSave] RepeatContainer 조인 컬럼 값: ${joinRelationship.detailColumn} = ${mainColumnValue}`); + } } - console.log(`📦 [handleSave] ${targetTable} 테이블 저장 (RepeatContainer): ${rows.length}건`); - for (const row of rows) { const { _isDirty, _sectionIndex, _targetTable, id, ...dataToSave } = row; // 변경되지 않은 행은 건너뛰기 if (_isDirty === false) { - console.log(`⏭️ [handleSave] ${targetTable} 변경 없음 건너뜀 (index: ${_sectionIndex})`); + continue; } @@ -1642,22 +1515,22 @@ export class ButtonActionExecutor { try { if (!id) { // INSERT (id가 없으면 새 레코드) - console.log(`📝 [handleSave] ${targetTable} INSERT (RepeatContainer):`, dataWithMeta); + const insertResult = await apiClient.post( `/table-management/tables/${targetTable}/add`, dataWithMeta, ); - console.log(`✅ [handleSave] ${targetTable} INSERT 완료:`, insertResult.data); + } else { // UPDATE (id가 있으면 기존 레코드) const originalData = { id }; const updatedData = { ...dataWithMeta, id }; - console.log(`📝 [handleSave] ${targetTable} UPDATE (RepeatContainer):`, { originalData, updatedData }); + const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, { originalData, updatedData, }); - console.log(`✅ [handleSave] ${targetTable} UPDATE 완료:`, updateResult.data); + } } catch (error: any) { console.error(`❌ [handleSave] ${targetTable} 저장 실패 (RepeatContainer):`, error.response?.data || error.message); @@ -1677,7 +1550,6 @@ export class ButtonActionExecutor { }>; if (aggregationConfigs && aggregationConfigs.length > 0) { - console.log("📊 [handleSave] 집계 저장 시작:", aggregationConfigs); for (const config of aggregationConfigs) { const { targetTable, targetColumn, joinKey, aggregatedValue, sourceValue } = config; @@ -1689,15 +1561,11 @@ export class ButtonActionExecutor { [joinKey.targetField]: sourceValue, }; - console.log( - `📊 [handleSave] ${targetTable}.${targetColumn} = ${aggregatedValue} (조인: ${joinKey.sourceField} = ${sourceValue})`, - ); - const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, { originalData, updatedData, }); - console.log(`✅ [handleSave] ${targetTable} 집계 저장 완료:`, updateResult.data); + } catch (error: any) { console.error(`❌ [handleSave] ${targetTable} 집계 저장 실패:`, error.response?.data || error.message); } @@ -1711,8 +1579,7 @@ export class ButtonActionExecutor { // 🔥 저장 성공 후 연결된 제어 실행 (dataflowTiming이 'after'인 경우) if (config.enableDataflowControl && config.dataflowConfig) { - console.log("🎯 저장 후 제어 실행 시작:", config.dataflowConfig); - + // 테이블 섹션 데이터 파싱 (comp_로 시작하는 필드에 JSON 배열이 있는 경우) // 입고 화면 등에서 품목 목록이 comp_xxx 필드에 JSON 문자열로 저장됨 const formData: Record = (saveResult.data || context.formData || {}) as Record; @@ -1740,7 +1607,7 @@ export class ButtonActionExecutor { }); return { ...commonFields, ...cleanItem }; }); - console.log(`📦 [handleSave] 테이블 섹션 데이터 파싱 완료: ${parsedSectionData.length}건`, parsedSectionData[0]); + } } catch (parseError) { console.warn("⚠️ [handleSave] 테이블 섹션 데이터 파싱 실패:", parseError); @@ -1788,13 +1655,6 @@ export class ButtonActionExecutor { } } - console.log("🔗 [handleSave] repeaterSave 이벤트 발생:", { - savedId, - tableName: context.tableName, - mainFormDataKeys: Object.keys(mainFormData), - saveResultData: saveResult?.data, - masterRecordId: savedId, // 🆕 마스터 레코드 ID (FK 연결용) - }); window.dispatchEvent( new CustomEvent("repeaterSave", { detail: { @@ -1829,11 +1689,10 @@ export class ButtonActionExecutor { if (formData.hasOwnProperty(primaryKeyColumn)) { const value = formData[primaryKeyColumn]; - console.log(`🔑 DB 기본키 발견: ${primaryKeyColumn} = ${value}`); // 복합키인 경우 로그 출력 if (primaryKeys.length > 1) { - console.log("🔗 복합 기본키 감지:", primaryKeys); + console.log(`📍 첫 번째 키 (${primaryKeyColumn}) 값을 사용: ${value}`); } @@ -1868,7 +1727,7 @@ export class ButtonActionExecutor { for (const keyName of commonPrimaryKeys) { if (formData.hasOwnProperty(keyName)) { const value = formData[keyName]; - console.log(`🔑 추측 기반 기본 키 발견: ${keyName} = ${value}`); + return value; } } @@ -1897,13 +1756,6 @@ export class ButtonActionExecutor { ): Promise { const { tableName, screenId, userId, companyCode } = context; - console.log("🏗️ [handleRackStructureBatchSave] 렉 구조 일괄 저장 시작:", { - locationsCount: locations.length, - tableName, - screenId, - rackStructureFieldKey, - }); - if (!tableName) { throw new Error("테이블명이 지정되지 않았습니다."); } @@ -1912,8 +1764,6 @@ export class ButtonActionExecutor { throw new Error("저장할 위치 데이터가 없습니다. 먼저 미리보기를 생성해주세요."); } - console.log("🏗️ [handleRackStructureBatchSave] 렉 구조 데이터 예시:", locations[0]); - // 저장 전 중복 체크 const firstLocation = locations[0]; const warehouseCode = firstLocation.warehouse_code || firstLocation.warehouse_id || firstLocation.warehouseCode; @@ -1921,7 +1771,6 @@ export class ButtonActionExecutor { const zone = firstLocation.zone; if (warehouseCode && floor && zone) { - console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseCode, floor, zone }); try { // search 파라미터를 사용하여 백엔드에서 필터링 (filters는 백엔드에서 처리 안됨) @@ -1993,9 +1842,6 @@ export class ButtonActionExecutor { return record; }); - console.log("🏗️ [handleRackStructureBatchSave] 저장할 레코드 수:", recordsToInsert.length); - console.log("🏗️ [handleRackStructureBatchSave] 첫 번째 레코드 예시:", recordsToInsert[0]); - // 일괄 INSERT 실행 try { let successCount = 0; @@ -2005,7 +1851,6 @@ export class ButtonActionExecutor { for (let i = 0; i < recordsToInsert.length; i++) { const record = recordsToInsert[i]; try { - console.log(`🏗️ [handleRackStructureBatchSave] 저장 중 (${i + 1}/${recordsToInsert.length}):`, record); const result = await DynamicFormApi.saveFormData({ screenId, @@ -2013,8 +1858,6 @@ export class ButtonActionExecutor { data: record, }); - console.log(`🏗️ [handleRackStructureBatchSave] API 응답 (${i + 1}):`, result); - if (result.success) { successCount++; } else { @@ -2031,12 +1874,6 @@ export class ButtonActionExecutor { } } - console.log("🏗️ [handleRackStructureBatchSave] 저장 완료:", { - successCount, - errorCount, - errors: errors.slice(0, 5), // 처음 5개 오류만 로그 - }); - if (errorCount > 0) { if (successCount > 0) { alert(`${successCount}개 저장 완료, ${errorCount}개 저장 실패\n\n오류: ${errors.slice(0, 3).join("\n")}`); @@ -2089,8 +1926,6 @@ export class ButtonActionExecutor { return { handled: false, success: false }; } - console.log("🎯 [handleUniversalFormModalTableSectionSave] Universal Form Modal 감지:", universalFormModalKey); - const modalData = formData[universalFormModalKey]; // 🆕 universal-form-modal 컴포넌트 설정 가져오기 @@ -2107,14 +1942,14 @@ export class ButtonActionExecutor { ); if (modalComponent) { modalComponentConfig = modalComponent.componentConfig || modalComponent.properties?.componentConfig; - console.log("🎯 [handleUniversalFormModalTableSectionSave] allComponents에서 설정 찾음:", modalComponent.id); + } } // 🆕 아직도 설정을 찾지 못했으면 화면 레이아웃 API에서 가져오기 if (!modalComponentConfig && screenId) { try { - console.log("🔍 [handleUniversalFormModalTableSectionSave] 화면 레이아웃 API에서 설정 조회:", screenId); + const { screenApi } = await import("@/lib/api/screen"); const layoutData = await screenApi.getLayout(screenId); @@ -2126,10 +1961,7 @@ export class ButtonActionExecutor { ); if (modalLayout) { modalComponentConfig = modalLayout.properties?.componentConfig || modalLayout.componentConfig; - console.log( - "🎯 [handleUniversalFormModalTableSectionSave] 화면 레이아웃에서 설정 찾음:", - modalLayout.componentId, - ); + } } } catch (error) { @@ -2140,18 +1972,6 @@ export class ButtonActionExecutor { const sections: any[] = modalComponentConfig?.sections || []; const saveConfig = modalComponentConfig?.saveConfig || {}; - console.log("🎯 [handleUniversalFormModalTableSectionSave] 컴포넌트 설정:", { - hasComponentConfig: !!modalComponentConfig, - sectionsCount: sections.length, - mainTableName: saveConfig.tableName || tableName, - sectionSaveModes: saveConfig.sectionSaveModes, - sectionDetails: sections.map((s: any) => ({ - id: s.id, - type: s.type, - targetTable: s.tableConfig?.saveConfig?.targetTable, - })), - }); - // _tableSection_ 데이터 추출 const tableSectionData: Record = {}; const commonFieldsData: Record = {}; @@ -2170,14 +1990,6 @@ export class ButtonActionExecutor { } } - console.log("🎯 [handleUniversalFormModalTableSectionSave] 데이터 분리:", { - commonFields: Object.keys(commonFieldsData), - tableSections: Object.keys(tableSectionData), - tableSectionCounts: Object.entries(tableSectionData).map(([k, v]) => ({ [k]: v.length })), - originalGroupedDataCount: originalGroupedData.length, - isEditMode: originalGroupedData.length > 0, - }); - // 테이블 섹션 데이터가 없고 원본 데이터도 없으면 처리하지 않음 const hasTableSectionData = Object.values(tableSectionData).some((arr) => arr.length > 0); if (!hasTableSectionData && originalGroupedData.length === 0) { @@ -2191,12 +2003,6 @@ export class ButtonActionExecutor { (formData.id !== undefined && formData.id !== null && formData.id !== "") || originalGroupedData.length > 0; - console.log("🔍 [handleUniversalFormModalTableSectionSave] 채번 규칙 할당 체크 시작", { - isEditMode: isEditModeUniversal, - formDataId: formData.id, - originalGroupedDataCount: originalGroupedData.length, - }); - const fieldsWithNumbering: Record = {}; // commonFieldsData와 modalData에서 채번 규칙이 설정된 필드 찾기 @@ -2204,7 +2010,7 @@ export class ButtonActionExecutor { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; - console.log(`🎯 [handleUniversalFormModalTableSectionSave] 채번 필드 발견: ${fieldName} → 규칙 ${value}`); + } } @@ -2213,34 +2019,24 @@ export class ButtonActionExecutor { if (key.endsWith("_numberingRuleId") && value && !fieldsWithNumbering[key.replace("_numberingRuleId", "")]) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; - console.log( - `🎯 [handleUniversalFormModalTableSectionSave] 채번 필드 발견 (formData): ${fieldName} → 규칙 ${value}`, - ); + } } - console.log("📋 [handleUniversalFormModalTableSectionSave] 채번 규칙이 설정된 필드:", fieldsWithNumbering); - // 🔧 수정 모드에서는 채번 코드 할당 건너뛰기 (기존 번호 유지) // 신규 등록 모드에서만 allocateCode 호출하여 새 번호 할당 if (Object.keys(fieldsWithNumbering).length > 0 && !isEditModeUniversal) { - console.log( - "🎯 [handleUniversalFormModalTableSectionSave] 신규 등록 모드 - 채번 규칙 할당 시작 (allocateCode 호출)", - ); + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { - console.log( - `🔄 [handleUniversalFormModalTableSectionSave] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`, - ); + const allocateResult = await allocateNumberingCode(ruleId); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; - console.log( - `✅ [handleUniversalFormModalTableSectionSave] ${fieldName} 새 코드 할당: ${commonFieldsData[fieldName]} → ${newCode}`, - ); + commonFieldsData[fieldName] = newCode; } else { console.warn( @@ -2254,10 +2050,8 @@ export class ButtonActionExecutor { } } } else if (isEditModeUniversal) { - console.log("⏭️ [handleUniversalFormModalTableSectionSave] 수정 모드 - 채번 코드 할당 건너뜀 (기존 번호 유지)"); - } - console.log("✅ [handleUniversalFormModalTableSectionSave] 채번 규칙 할당 완료"); + } try { // 사용자 정보 추가 @@ -2286,7 +2080,6 @@ export class ButtonActionExecutor { ); if (hasSeparateTargetTable && Object.keys(commonFieldsData).length > 0) { - console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블에 공통 데이터 저장:", tableName); const mainRowToSave = { ...commonFieldsData, ...userInfo }; @@ -2303,17 +2096,11 @@ export class ButtonActionExecutor { const existingMainId = formData.id; const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== ""; - 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, @@ -2334,14 +2121,10 @@ export class ButtonActionExecutor { throw new Error(mainSaveResult.message || "메인 데이터 저장 실패"); } - console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId); } // 각 테이블 섹션 처리 for (const [sectionId, currentItems] of Object.entries(tableSectionData)) { - console.log( - `🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 처리 시작: ${currentItems.length}개 품목`, - ); // 🆕 해당 섹션의 설정 찾기 const sectionConfig = sections.find((s) => s.id === sectionId); @@ -2352,12 +2135,6 @@ export class ButtonActionExecutor { // - targetTable이 없으면 메인 테이블에 저장 const saveTableName = targetTableName || tableName!; - console.log(`📊 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 저장 테이블:`, { - targetTableName, - saveTableName, - isMainTable: saveTableName === tableName, - }); - // 1️⃣ 신규 품목 INSERT (id가 없는 항목) const newItems = currentItems.filter((item) => !item.id); for (const item of newItems) { @@ -2410,14 +2187,6 @@ export class ButtonActionExecutor { } }); - console.log("📝 [UPDATE 폴백] 저장할 데이터:", { - id: item.id, - tableName: saveTableName, - commonFieldsData, - itemFields: Object.keys(item).filter(k => !k.startsWith("_")), - rowToUpdate, - }); - // id를 유지하고 UPDATE 실행 const updateResult = await DynamicFormApi.updateFormData(item.id, { tableName: saveTableName, @@ -2438,7 +2207,6 @@ export class ButtonActionExecutor { const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon); if (hasChanges) { - console.log(`🔄 [UPDATE] 품목 수정: id=${item.id}, tableName=${saveTableName}`); // 변경된 필드만 추출하여 부분 업데이트 const updateResult = await DynamicFormApi.updateFormDataPartial( @@ -2454,7 +2222,7 @@ export class ButtonActionExecutor { updatedCount++; } else { - console.log(`⏭️ [SKIP] 변경 없음: id=${item.id}`); + } } @@ -2464,7 +2232,6 @@ export class ButtonActionExecutor { const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(String(orig.id))); for (const deletedItem of deletedItems) { - console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`); // screenId 전달하여 제어관리 실행 가능하도록 함 const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deletedItem.id, saveTableName, context.screenId); @@ -2486,21 +2253,17 @@ export class ButtonActionExecutor { const resultMessage = resultParts.length > 0 ? resultParts.join(", ") : "변경 사항 없음"; - console.log(`✅ [handleUniversalFormModalTableSectionSave] 완료: ${resultMessage}`); toast.success(`저장 완료: ${resultMessage}`); // 🆕 저장 성공 후 제어 관리 실행 (다중 테이블 저장 시 소스 테이블과 일치하는 섹션만 실행) if (config.enableDataflowControl && config.dataflowConfig?.flowConfig?.flowId) { const flowId = config.dataflowConfig.flowConfig.flowId; - console.log("🎯 [handleUniversalFormModalTableSectionSave] 제어 관리 실행 시작:", { flowId }); try { // 플로우 소스 테이블 조회 const { getFlowSourceTable } = await import("@/lib/api/nodeFlows"); const flowSourceInfo = await getFlowSourceTable(flowId); - console.log("📊 [handleUniversalFormModalTableSectionSave] 플로우 소스 테이블:", flowSourceInfo); - if (flowSourceInfo.sourceTable) { // 각 섹션 확인하여 소스 테이블과 일치하는 섹션 찾기 let controlExecuted = false; @@ -2509,17 +2272,8 @@ export class ButtonActionExecutor { const sectionConfig = sections.find((s: any) => s.id === sectionId); const sectionTargetTable = sectionConfig?.tableConfig?.saveConfig?.targetTable || tableName; - console.log(`🔍 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 테이블 비교:`, { - sectionTargetTable, - flowSourceTable: flowSourceInfo.sourceTable, - isMatch: sectionTargetTable === flowSourceInfo.sourceTable, - }); - // 소스 테이블과 일치하는 섹션만 제어 실행 if (sectionTargetTable === flowSourceInfo.sourceTable && sectionItems.length > 0) { - console.log( - `✅ [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} → 플로우 소스 테이블 일치! 제어 실행`, - ); // 공통 필드 + 해당 섹션 데이터 병합하여 sourceData 생성 const sourceData = sectionItems.map((item: any) => ({ @@ -2527,11 +2281,6 @@ export class ButtonActionExecutor { ...item, })); - console.log( - `📦 [handleUniversalFormModalTableSectionSave] 제어 전달 데이터: ${sourceData.length}건`, - sourceData[0], - ); - // 제어 관리용 컨텍스트 생성 const controlContext: ButtonActionContext = { ...context, @@ -2548,7 +2297,6 @@ export class ButtonActionExecutor { // 매칭되는 섹션이 없으면 메인 테이블 확인 if (!controlExecuted && tableName === flowSourceInfo.sourceTable) { - console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 일치! 공통 필드로 제어 실행"); const controlContext: ButtonActionContext = { ...context, @@ -2624,13 +2372,6 @@ export class ButtonActionExecutor { ): Promise { const { formData, tableName, screenId, selectedRowsData, originalData } = context; - console.log("🔍 [handleBatchSave] context 확인:", { - hasSelectedRowsData: !!selectedRowsData, - selectedRowsCount: selectedRowsData?.length || 0, - hasOriginalData: !!originalData, - originalDataKeys: originalData ? Object.keys(originalData) : [], - }); - if (!tableName || !screenId) { toast.error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)"); return false; @@ -2710,7 +2451,7 @@ export class ButtonActionExecutor { let combinations: any[][]; if (allGroupsEmpty) { // 🆕 모든 그룹이 비어있으면 빈 조합 하나 생성 (품목 기본 정보만으로 저장) - console.log("📝 [handleBatchSave] 모든 그룹이 비어있음 - 기본 레코드 생성"); + combinations = [[]]; } else { // 빈 그룹을 필터링하여 카티션 곱 계산 (빈 그룹은 무시) @@ -2857,14 +2598,6 @@ export class ButtonActionExecutor { // 플로우 선택 데이터 우선 사용 const dataToDelete = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; - console.log("🔍 handleDelete - 데이터 소스 확인:", { - hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0), - flowSelectedDataLength: flowSelectedData?.length || 0, - hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0), - selectedRowsDataLength: selectedRowsData?.length || 0, - dataToDeleteLength: dataToDelete?.length || 0, - }); - // 다중 선택된 데이터가 있는 경우 if (dataToDelete && dataToDelete.length > 0) { console.log(`다중 삭제 액션 실행: ${dataToDelete.length}개 항목`, dataToDelete); @@ -2876,7 +2609,7 @@ export class ButtonActionExecutor { const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(tableName); if (primaryKeysResult.success && primaryKeysResult.data) { primaryKeys = primaryKeysResult.data; - console.log(`🔑 테이블 ${tableName}의 기본키:`, primaryKeys); + } } catch (error) { console.warn("기본키 조회 실패, 폴백 방법 사용:", error); @@ -2891,7 +2624,7 @@ export class ButtonActionExecutor { if (primaryKeys.length > 0) { const primaryKey = primaryKeys[0]; // 첫 번째 기본키 사용 deleteId = rowData[primaryKey]; - console.log(`📊 기본키 ${primaryKey}로 ID 추출:`, deleteId); + } // 2순위: 폴백 - 일반적인 ID 필드명들 시도 @@ -2925,7 +2658,6 @@ export class ButtonActionExecutor { if (idField) deleteId = rowData[idField]; } - console.log("🔍 폴백 방법으로 ID 추출:", deleteId); } console.log("선택된 행 데이터:", rowData); @@ -2947,19 +2679,17 @@ export class ButtonActionExecutor { } } - console.log(`✅ 다중 삭제 성공: ${dataToDelete.length}개 항목`); - // 데이터 소스에 따라 적절한 새로고침 호출 if (flowSelectedData && flowSelectedData.length > 0) { - console.log("🔄 플로우 데이터 삭제 완료, 플로우 새로고침 호출"); + context.onFlowRefresh?.(); // 플로우 새로고침 } else { - console.log("🔄 테이블 데이터 삭제 완료, 테이블 새로고침 호출"); + context.onRefresh?.(); // 테이블 새로고침 // 🆕 분할 패널 등 전역 테이블 새로고침 이벤트 발생 window.dispatchEvent(new CustomEvent("refreshTable")); - console.log("🔄 refreshTable 전역 이벤트 발생"); + } toast.success(config.successMessage || `${dataToDelete.length}개 항목이 삭제되었습니다.`); @@ -2977,7 +2707,6 @@ export class ButtonActionExecutor { throw new Error(deleteResult.message || "삭제에 실패했습니다."); } - console.log("✅ 단일 삭제 성공:", deleteResult); } else { throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)"); } @@ -2986,7 +2715,6 @@ export class ButtonActionExecutor { // 🆕 분할 패널 등 전역 테이블 새로고침 이벤트 발생 window.dispatchEvent(new CustomEvent("refreshTable")); - console.log("🔄 refreshTable 전역 이벤트 발생 (단일 삭제)"); toast.success(config.successMessage || "삭제되었습니다."); return true; @@ -3054,13 +2782,6 @@ export class ButtonActionExecutor { // 버튼 설정에서 targetScreenId 가져오기 (여러 위치에서 확인) const targetScreenId = config.relatedModalConfig?.targetScreenId || config.targetScreenId; - console.log("🔍 [openRelatedModal] 설정 확인:", { - config, - relatedModalConfig: config.relatedModalConfig, - targetScreenId: config.targetScreenId, - finalTargetScreenId: targetScreenId, - }); - if (!targetScreenId) { console.error("❌ [openRelatedModal] targetScreenId가 설정되지 않았습니다."); toast.error("모달 화면 ID가 설정되지 않았습니다."); @@ -3070,12 +2791,6 @@ export class ButtonActionExecutor { // RelatedDataButtons에서 선택된 데이터 가져오기 const relatedData = window.__relatedButtonsSelectedData; - console.log("🔍 [openRelatedModal] RelatedDataButtons 데이터:", { - relatedData, - selectedItem: relatedData?.selectedItem, - config: relatedData?.config, - }); - if (!relatedData?.selectedItem) { console.warn("⚠️ [openRelatedModal] 선택된 버튼이 없습니다."); toast.warning("먼저 버튼을 선택해주세요."); @@ -3087,21 +2802,8 @@ export class ButtonActionExecutor { // 데이터 매핑 적용 const initialData: Record = {}; - console.log("🔍 [openRelatedModal] 매핑 설정:", { - modalLink: relatedConfig?.modalLink, - dataMapping: relatedConfig?.modalLink?.dataMapping, - }); - if (relatedConfig?.modalLink?.dataMapping && relatedConfig.modalLink.dataMapping.length > 0) { relatedConfig.modalLink.dataMapping.forEach((mapping) => { - console.log("🔍 [openRelatedModal] 매핑 처리:", { - mapping, - sourceField: mapping.sourceField, - targetField: mapping.targetField, - selectedItemValue: selectedItem.value, - selectedItemId: selectedItem.id, - rawDataValue: selectedItem.rawData[mapping.sourceField], - }); if (mapping.sourceField === "value") { initialData[mapping.targetField] = selectedItem.value; @@ -3113,16 +2815,10 @@ export class ButtonActionExecutor { }); } else { // 기본 매핑: id를 routing_version_id로 전달 - console.log("🔍 [openRelatedModal] 기본 매핑 사용"); + initialData["routing_version_id"] = selectedItem.value || selectedItem.id; } - console.log("📤 [openRelatedModal] 모달 열기:", { - targetScreenId, - selectedItem, - initialData, - }); - // 모달 열기 이벤트 발생 (ScreenModal은 editData를 사용) window.dispatchEvent( new CustomEvent("openScreenModal", { @@ -3366,12 +3062,6 @@ export class ButtonActionExecutor { config: ButtonActionConfig, context: ButtonActionContext, ): Promise { - console.log("📦 데이터와 함께 모달 열기:", { - title: config.modalTitle, - size: config.modalSize, - targetScreenId: config.targetScreenId, - dataSourceId: config.dataSourceId, - }); // 🆕 1. 현재 화면의 TableList 또는 SplitPanelLayout 자동 감지 let dataSourceId = config.dataSourceId; @@ -3416,24 +3106,12 @@ export class ButtonActionExecutor { const modalData = dataRegistry[dataSourceId] || []; - console.log("📊 현재 화면 데이터 확인:", { - dataSourceId, - count: modalData.length, - allKeys: Object.keys(dataRegistry), // 🆕 전체 데이터 키 확인 - }); - if (modalData.length === 0) { console.warn("⚠️ 선택된 데이터가 없습니다:", dataSourceId); toast.warning("선택된 데이터가 없습니다. 먼저 항목을 선택해주세요."); return false; } - console.log("✅ 모달 데이터 준비 완료:", { - currentData: { id: dataSourceId, count: modalData.length }, - previousData: Object.entries(dataRegistry) - .filter(([key]) => key !== dataSourceId) - .map(([key, data]: [string, any]) => ({ id: key, count: data.length })), - }); } catch (error) { console.error("❌ 데이터 확인 실패:", error); toast.error("데이터 확인 중 오류가 발생했습니다."); @@ -3481,7 +3159,7 @@ export class ButtonActionExecutor { }); finalTitle = titleParts.join(""); - console.log("📋 블록 기반 제목 생성:", finalTitle); + } // 기존 방식: {tableName.columnName} 패턴 (우선순위 2) else if (config.modalTitle) { @@ -3533,7 +3211,6 @@ export class ButtonActionExecutor { // 🆕 필드 매핑 적용 (소스 컬럼 → 타겟 컬럼) const parentData = { ...rawParentData }; if (config.fieldMappings && Array.isArray(config.fieldMappings) && config.fieldMappings.length > 0) { - console.log("🔄 [openModalWithData] 필드 매핑 적용:", config.fieldMappings); config.fieldMappings.forEach((mapping: { sourceField: string; targetField: string }) => { if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) { @@ -3549,15 +3226,6 @@ export class ButtonActionExecutor { const selectedData = modalData.map((item: any) => item.originalData || item); const selectedIds = selectedData.map((row: any) => row.id).filter(Boolean); - console.log("📦 [openModalWithData] 부모 데이터 전달:", { - dataSourceId, - rawParentData, - mappedParentData: parentData, - fieldMappings: config.fieldMappings, - selectedDataCount: selectedData.length, - selectedIds, - }); - // 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함) const modalEvent = new CustomEvent("openScreenModal", { detail: { @@ -3800,7 +3468,7 @@ export class ButtonActionExecutor { // 🆕 분할 패널 화면인 경우 ScreenModal 사용 (editData 전달) if (hasSplitPanel) { - console.log("📋 [openEditModal] 분할 패널 화면 - ScreenModal 사용"); + const screenModalEvent = new CustomEvent("openScreenModal", { detail: { screenId: config.targetScreenId, @@ -3848,7 +3516,6 @@ export class ButtonActionExecutor { } const editUrl = `/screens/${config.targetScreenId}?mode=edit&id=${rowId}`; - console.log("🔄 편집 화면으로 이동:", editUrl); window.location.href = editUrl; } @@ -3863,14 +3530,6 @@ export class ButtonActionExecutor { // 플로우 선택 데이터 우선 사용 const dataToCopy = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; - console.log("📋 handleCopy - 데이터 소스 확인:", { - hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0), - flowSelectedDataLength: flowSelectedData?.length || 0, - hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0), - selectedRowsDataLength: selectedRowsData?.length || 0, - dataToCopyLength: dataToCopy?.length || 0, - }); - // 선택된 데이터가 없는 경우 if (!dataToCopy || dataToCopy.length === 0) { toast.error("복사할 항목을 선택해주세요."); @@ -3883,17 +3542,9 @@ export class ButtonActionExecutor { return false; } - console.log(`📋 복사 액션 실행: ${dataToCopy.length}개 항목`, { - dataToCopy, - targetScreenId: config.targetScreenId, - editMode: config.editMode, - }); - if (dataToCopy.length === 1) { // 단일 항목 복사 const rowData = dataToCopy[0]; - console.log("📋 단일 항목 복사:", rowData); - console.log("📋 원본 데이터 키 목록:", Object.keys(rowData)); // 복사 시 제거할 필드들 const copiedData = { ...rowData }; @@ -3920,7 +3571,7 @@ export class ButtonActionExecutor { fieldsToRemove.forEach((field) => { if (copiedData[field] !== undefined) { delete copiedData[field]; - console.log(`🗑️ 필드 제거: ${field}`); + } }); @@ -3953,7 +3604,7 @@ export class ButtonActionExecutor { const columnName = compConfig.columnName || comp.columnName; if (columnName) { screenNumberingRules[columnName] = compConfig.autoGeneration.options.numberingRuleId; - console.log(`📋 화면 설정에서 채번 규칙 발견: ${columnName} → ${compConfig.autoGeneration.options.numberingRuleId}`); + } } // 중첩된 컴포넌트 확인 @@ -3966,7 +3617,7 @@ export class ButtonActionExecutor { if (layout?.components) { findNumberingRules(layout.components); } - console.log("📋 화면 설정에서 찾은 채번 규칙:", screenNumberingRules); + } catch (error) { console.warn("⚠️ 화면 레이아웃 조회 실패:", error); } @@ -3990,10 +3641,9 @@ export class ButtonActionExecutor { // 채번 규칙 ID가 있으면 복사 (저장 시 자동 생성) if (hasNumberingRule) { copiedData[ruleIdKey] = numberingRuleId; - console.log(`✅ 품목코드 초기화 (채번 규칙 있음): ${field} (기존값: ${originalValue})`); - console.log(`📋 채번 규칙 ID 설정: ${ruleIdKey} = ${numberingRuleId}`); + } else { - console.log(`✅ 품목코드 초기화 (수동 입력 필요): ${field} (기존값: ${originalValue})`); + } resetFieldName = field; @@ -4006,7 +3656,7 @@ export class ButtonActionExecutor { writerFields.forEach((field) => { if (copiedData[field] !== undefined && context.userId) { copiedData[field] = context.userId; - console.log(`👤 작성자 변경: ${field} = ${context.userId}`); + } }); @@ -4018,7 +3668,6 @@ export class ButtonActionExecutor { toast.info("복사본이 생성됩니다."); } - console.log("📋 복사된 데이터:", copiedData); await this.openCopyForm(config, copiedData, context); } else { // 다중 항목 복사 - 현재는 단일 복사만 지원 @@ -4044,24 +3693,23 @@ export class ButtonActionExecutor { ): Promise { try { const editMode = config.editMode || "modal"; - console.log("📋 openCopyForm 실행:", { editMode, targetScreenId: config.targetScreenId }); switch (editMode) { case "modal": // 모달로 복사 폼 열기 (편집 모달 재사용, INSERT 모드로) - console.log("📋 모달로 복사 폼 열기 (INSERT 모드)"); + await this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true break; case "navigate": // 새 페이지로 이동 - console.log("📋 새 페이지로 복사 화면 이동"); + this.navigateToCopyScreen(config, rowData, context); break; default: // 기본값: 모달 - console.log("📋 기본 모달로 복사 폼 열기 (INSERT 모드)"); + this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true } } catch (error: any) { @@ -4075,7 +3723,6 @@ export class ButtonActionExecutor { */ private static navigateToCopyScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void { const copyUrl = `/screens/${config.targetScreenId}?mode=copy`; - console.log("🔄 복사 화면으로 이동:", copyUrl); // 복사할 데이터를 sessionStorage에 저장 sessionStorage.setItem("copyData", JSON.stringify(rowData)); @@ -4096,22 +3743,8 @@ export class ButtonActionExecutor { * 제어 전용 액션 처리 (조건 체크만 수행) */ private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise { - console.log("🎯 ButtonActionExecutor.handleControl 실행:", { - formData: context.formData, - selectedRows: context.selectedRows, - selectedRowsData: context.selectedRowsData, - flowSelectedData: context.flowSelectedData, - flowSelectedStepId: context.flowSelectedStepId, - config, - }); // 🔥 제어 조건이 설정되어 있는지 확인 - console.log("🔍 제어관리 활성화 상태 확인:", { - enableDataflowControl: config.enableDataflowControl, - hasDataflowConfig: !!config.dataflowConfig, - dataflowConfig: config.dataflowConfig, - fullConfig: config, - }); if (!config.dataflowConfig || !config.enableDataflowControl) { console.warn("⚠️ 제어관리가 비활성화되어 있습니다:", { @@ -4133,26 +3766,19 @@ export class ButtonActionExecutor { // 설정이 없으면 자동 판단 (우선순위 순서대로) if (context.flowSelectedData && context.flowSelectedData.length > 0) { controlDataSource = "flow-selection"; - console.log("🔄 자동 판단: flow-selection 모드 사용"); + } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { controlDataSource = "table-selection"; - console.log("🔄 자동 판단: table-selection 모드 사용"); + } else if (context.formData && Object.keys(context.formData).length > 0) { controlDataSource = "form"; - console.log("🔄 자동 판단: form 모드 사용"); + } else { controlDataSource = "form"; // 기본값 - console.log("🔄 기본값: form 모드 사용"); + } } - console.log("📊 데이터 소스 모드:", { - controlDataSource, - hasFormData: !!(context.formData && Object.keys(context.formData).length > 0), - hasTableSelection: !!(context.selectedRowsData && context.selectedRowsData.length > 0), - hasFlowSelection: !!(context.flowSelectedData && context.flowSelectedData.length > 0), - }); - const extendedContext: ExtendedControlContext = { formData: context.formData || {}, selectedRows: context.selectedRows || [], @@ -4162,18 +3788,12 @@ export class ButtonActionExecutor { controlDataSource, }; - console.log("🔍 제어 조건 검증 시작:", { - dataflowConfig: config.dataflowConfig, - extendedContext, - }); - // 🔥 새로운 버튼 액션 실행 시스템 사용 // flowConfig가 있으면 controlMode가 명시되지 않아도 플로우 모드로 간주 const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId; const isFlowMode = config.dataflowConfig?.controlMode === "flow" || hasFlowConfig; if (isFlowMode && config.dataflowConfig?.flowConfig) { - console.log("🎯 노드 플로우 실행:", config.dataflowConfig.flowConfig); const { flowId, executionTiming } = config.dataflowConfig.flowConfig; @@ -4191,13 +3811,6 @@ export class ButtonActionExecutor { let sourceData: any = null; let dataSourceType: string = controlDataSource || "none"; - console.log("🔍 데이터 소스 결정:", { - controlDataSource, - hasFlowSelectedData: !!(context.flowSelectedData && context.flowSelectedData.length > 0), - hasSelectedRowsData: !!(context.selectedRowsData && context.selectedRowsData.length > 0), - hasFormData: !!(context.formData && Object.keys(context.formData).length > 0), - }); - // controlDataSource 설정에 따라 데이터 선택 switch (controlDataSource) { case "flow-selection": @@ -4218,10 +3831,7 @@ export class ButtonActionExecutor { case "table-selection": if (context.selectedRowsData && context.selectedRowsData.length > 0) { sourceData = context.selectedRowsData; - console.log("📊 테이블 선택 데이터 사용:", { - dataCount: sourceData.length, - sourceData, - }); + } else { console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다."); toast.error("테이블에서 처리할 항목을 먼저 선택해주세요."); @@ -4232,7 +3842,7 @@ export class ButtonActionExecutor { case "form": if (context.formData && Object.keys(context.formData).length > 0) { sourceData = [context.formData]; - console.log("📝 폼 데이터 사용:", sourceData); + } else { console.warn("⚠️ form 모드이지만 폼 데이터가 없습니다."); } @@ -4272,29 +3882,20 @@ export class ButtonActionExecutor { ...context.formData, })); dataSourceType = "both"; - console.log("📊 [자동] 테이블 선택 + 폼 데이터 병합 사용:", { - rowCount: context.selectedRowsData.length, - formDataKeys: Object.keys(context.formData), - }); + } else { sourceData = context.selectedRowsData; dataSourceType = "table-selection"; - console.log("📊 [자동] 테이블 선택 데이터 사용"); + } } else if (context.formData && Object.keys(context.formData).length > 0) { sourceData = [context.formData]; dataSourceType = "form"; - console.log("📝 [자동] 폼 데이터 사용"); + } break; } - console.log("📦 최종 전달 데이터:", { - dataSourceType, - sourceDataCount: sourceData ? (Array.isArray(sourceData) ? sourceData.length : 1) : 0, - sourceData, - }); - const result = await executeNodeFlow(flowId, { dataSourceType, sourceData, @@ -4302,18 +3903,18 @@ export class ButtonActionExecutor { }); if (result.success) { - console.log("✅ 노드 플로우 실행 완료:", result); + toast.success("플로우 실행이 완료되었습니다."); // 플로우 새로고침 (플로우 위젯용) if (context.onFlowRefresh) { - console.log("🔄 플로우 새로고침 호출"); + context.onFlowRefresh(); } // 테이블 새로고침 (일반 테이블용) if (context.onRefresh) { - console.log("🔄 테이블 새로고침 호출"); + context.onRefresh(); } @@ -4329,7 +3930,6 @@ export class ButtonActionExecutor { return false; } } else if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) { - console.log("🔗 관계 기반 제어 실행:", config.dataflowConfig.relationshipConfig); // 🔥 table-selection 모드일 때 선택된 행 데이터를 formData에 병합 let mergedFormData = { ...context.formData } || {}; @@ -4342,11 +3942,7 @@ export class ButtonActionExecutor { // 선택된 첫 번째 행의 데이터를 formData에 병합 const selectedRowData = context.selectedRowsData[0]; mergedFormData = { ...mergedFormData, ...selectedRowData }; - console.log("🔄 선택된 행 데이터를 formData에 병합:", { - originalFormData: context.formData, - selectedRowData, - mergedFormData, - }); + } // 새로운 ImprovedButtonActionExecutor 사용 @@ -4366,7 +3962,7 @@ export class ButtonActionExecutor { }); if (executionResult.success) { - console.log("✅ 관계 실행 완료:", executionResult); + toast.success(config.successMessage || "관계 실행이 완료되었습니다."); // 새로고침이 필요한 경우 @@ -4382,7 +3978,6 @@ export class ButtonActionExecutor { } } else { // 제어 없음 - 성공 처리 - console.log("⚡ 제어 없음 - 버튼 액션만 실행"); // 새로고침이 필요한 경우 if (context.onRefresh) { @@ -4404,11 +3999,6 @@ export class ButtonActionExecutor { * 다중 제어 순차 실행 지원 */ public static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise { - console.log("🎯 저장 후 제어 실행:", { - enableDataflowControl: config.enableDataflowControl, - dataflowConfig: config.dataflowConfig, - dataflowTiming: config.dataflowTiming, - }); // 제어 데이터 소스 결정 let controlDataSource = config.dataflowConfig?.controlDataSource; @@ -4426,7 +4016,6 @@ export class ButtonActionExecutor { // 🔥 다중 제어 지원 (flowControls 배열) const flowControls = config.dataflowConfig?.flowControls || []; if (flowControls.length > 0) { - console.log(`🎯 다중 제어 순차 실행 시작: ${flowControls.length}개`); // 순서대로 정렬 const sortedControls = [...flowControls].sort((a: any, b: any) => (a.order || 0) - (b.order || 0)); @@ -4442,11 +4031,11 @@ export class ButtonActionExecutor { let sourceData: any[]; if (context.selectedRowsData && context.selectedRowsData.length > 0) { sourceData = context.selectedRowsData; - console.log("📦 [다중제어] selectedRowsData 사용:", sourceData.length, "건"); + } else { const savedData = context.savedData || context.formData || {}; sourceData = Array.isArray(savedData) ? savedData : [savedData]; - console.log("📦 [다중제어] savedData/formData 사용:", sourceData.length, "건"); + } let allSuccess = true; @@ -4463,10 +4052,7 @@ export class ButtonActionExecutor { // executionTiming 체크 (after만 실행) if (control.executionTiming && control.executionTiming !== "after") { - console.log( - `⏭️ [${i + 1}/${sortedControls.length}] executionTiming이 'after'가 아님, 스킵:`, - control.executionTiming, - ); + continue; } @@ -4489,7 +4075,7 @@ export class ButtonActionExecutor { }); if (result.success) { - console.log(`✅ [${i + 1}/${sortedControls.length}] 제어 성공: ${control.flowName}`); + } else { console.error(`❌ [${i + 1}/${sortedControls.length}] 제어 실패: ${control.flowName} - ${result.message}`); allSuccess = false; @@ -4532,7 +4118,7 @@ export class ButtonActionExecutor { // 🔥 기존 단일 제어 실행 (하위 호환성) // dataflowTiming이 'after'가 아니면 실행하지 않음 if (config.dataflowTiming && config.dataflowTiming !== "after") { - console.log("⏭️ dataflowTiming이 'after'가 아니므로 제어 실행 건너뜀:", config.dataflowTiming); + return; } @@ -4542,12 +4128,10 @@ export class ButtonActionExecutor { // executionTiming 체크 const flowTiming = config.dataflowConfig.flowConfig.executionTiming; if (flowTiming && flowTiming !== "after") { - console.log("⏭️ flowConfig.executionTiming이 'after'가 아니므로 제어 실행 건너뜀:", flowTiming); + return; } - console.log("🎯 저장 후 노드 플로우 실행:", config.dataflowConfig.flowConfig); - const { flowId } = config.dataflowConfig.flowConfig; try { @@ -4562,11 +4146,11 @@ export class ButtonActionExecutor { let sourceData: any[]; if (context.selectedRowsData && context.selectedRowsData.length > 0) { sourceData = context.selectedRowsData; - console.log("📦 [단일제어] selectedRowsData 사용:", sourceData.length, "건"); + } else { const savedData = context.savedData || context.formData || {}; sourceData = Array.isArray(savedData) ? savedData : [savedData]; - console.log("📦 [단일제어] savedData/formData 사용:", sourceData.length, "건"); + } // repeat-screen-modal 데이터가 있으면 병합 @@ -4574,15 +4158,8 @@ export class ButtonActionExecutor { key.startsWith("_repeatScreenModal_"), ); if (repeatScreenModalKeys.length > 0) { - console.log("📦 repeat-screen-modal 데이터 발견:", repeatScreenModalKeys); - } - console.log("📦 노드 플로우에 전달할 데이터:", { - flowId, - dataSourceType: controlDataSource, - sourceDataCount: sourceData.length, - sourceDataSample: sourceData[0], - }); + } const result = await executeNodeFlow(flowId, { dataSourceType: controlDataSource, @@ -4591,7 +4168,7 @@ export class ButtonActionExecutor { }); if (result.success) { - console.log("✅ 저장 후 노드 플로우 실행 완료:", result); + toast.success("제어 로직 실행이 완료되었습니다."); } else { console.error("❌ 저장 후 노드 플로우 실행 실패:", result); @@ -4607,7 +4184,6 @@ export class ButtonActionExecutor { // 관계 기반 제어 실행 if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) { - console.log("🔗 저장 후 관계 기반 제어 실행:", config.dataflowConfig.relationshipConfig); const buttonConfig = { actionType: config.type, @@ -4629,7 +4205,7 @@ export class ButtonActionExecutor { ); if (executionResult.success) { - console.log("✅ 저장 후 제어 실행 완료:", executionResult); + // 성공 토스트는 save 액션에서 이미 표시했으므로 추가로 표시하지 않음 } else { console.error("❌ 저장 후 제어 실행 실패:", executionResult); @@ -4642,13 +4218,11 @@ export class ButtonActionExecutor { * 관계도에서 가져온 액션들을 실행 */ private static async executeRelationshipActions(actions: any[], context: ButtonActionContext): Promise { - console.log("🚀 관계도 액션 실행 시작:", actions); for (let i = 0; i < actions.length; i++) { const action = actions[i]; try { - console.log(`🔄 액션 ${i + 1}/${actions.length} 실행:`, action); const actionType = action.actionType || action.type; // actionType 우선, type 폴백 @@ -4675,8 +4249,6 @@ export class ButtonActionExecutor { throw new Error(`지원되지 않는 액션 타입: ${actionType}`); } - console.log(`✅ 액션 ${i + 1}/${actions.length} 완료:`, action.name); - // 성공 토스트 (개별 액션별) toast.success(`${action.name || `액션 ${i + 1}`} 완료`); } catch (error) { @@ -4695,7 +4267,6 @@ export class ButtonActionExecutor { } } - console.log("🎉 모든 액션 실행 완료!"); toast.success(`총 ${actions.length}개 액션이 모두 성공적으로 완료되었습니다.`); } @@ -4703,15 +4274,12 @@ export class ButtonActionExecutor { * 저장 액션 실행 */ private static async executeActionSave(action: any, context: ButtonActionContext): Promise { - console.log("💾 저장 액션 실행:", action); - console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2)); // 🎯 필드 매핑 정보 사용하여 저장 데이터 구성 let saveData: Record = {}; // 액션에 필드 매핑 정보가 있는지 확인 if (action.fieldMappings && Array.isArray(action.fieldMappings)) { - console.log("📋 필드 매핑 정보 발견:", action.fieldMappings); // 필드 매핑에 따라 데이터 구성 action.fieldMappings.forEach((mapping: any) => { @@ -4731,7 +4299,7 @@ export class ButtonActionExecutor { // 타겟 필드에 값 설정 if (targetField && value !== undefined) { saveData[targetField] = value; - console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`); + } }); } else { @@ -4743,8 +4311,6 @@ export class ButtonActionExecutor { }; } - console.log("📊 최종 저장할 데이터:", saveData); - try { // 🔥 실제 저장 API 호출 if (!context.tableName) { @@ -4758,7 +4324,7 @@ export class ButtonActionExecutor { }); if (result.success) { - console.log("✅ 저장 성공:", result); + toast.success("데이터가 저장되었습니다."); } else { throw new Error(result.message || "저장 실패"); @@ -4774,20 +4340,17 @@ export class ButtonActionExecutor { * 업데이트 액션 실행 */ private static async executeActionUpdate(action: any, context: ButtonActionContext): Promise { - console.log("🔄 업데이트 액션 실행:", action); - console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2)); // 🎯 필드 매핑 정보 사용하여 업데이트 데이터 구성 let updateData: Record = {}; // 액션에 필드 매핑 정보가 있는지 확인 if (action.fieldMappings && Array.isArray(action.fieldMappings)) { - console.log("📋 필드 매핑 정보 발견:", action.fieldMappings); // 🔑 먼저 선택된 데이터의 모든 필드를 기본으로 포함 (기본키 보존) if (context.selectedRowsData?.[0]) { updateData = { ...context.selectedRowsData[0] }; - console.log("🔑 선택된 데이터를 기본으로 설정 (기본키 보존):", updateData); + } // 필드 매핑에 따라 데이터 구성 (덮어쓰기) @@ -4808,7 +4371,7 @@ export class ButtonActionExecutor { // 타겟 필드에 값 설정 (덮어쓰기) if (targetField && value !== undefined) { updateData[targetField] = value; - console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`); + } }); } else { @@ -4820,8 +4383,6 @@ export class ButtonActionExecutor { }; } - console.log("📊 최종 업데이트할 데이터:", updateData); - try { // 🔥 실제 업데이트 API 호출 if (!context.tableName) { @@ -4857,7 +4418,7 @@ export class ButtonActionExecutor { }); if (result.success) { - console.log("✅ 업데이트 성공:", result); + toast.success("데이터가 업데이트되었습니다."); } else { throw new Error(result.message || "업데이트 실패"); @@ -4873,7 +4434,6 @@ export class ButtonActionExecutor { * 삭제 액션 실행 */ private static async executeActionDelete(action: any, context: ButtonActionContext): Promise { - console.log("🗑️ 삭제 액션 실행:", action); // 실제 삭제 로직 (기존 handleDelete와 유사) if (!context.selectedRowsData || context.selectedRowsData.length === 0) { @@ -4916,7 +4476,7 @@ export class ButtonActionExecutor { const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName, context.screenId); if (result.success) { - console.log("✅ 삭제 성공:", result); + toast.success("데이터가 삭제되었습니다."); } else { throw new Error(result.message || "삭제 실패"); @@ -4938,7 +4498,6 @@ export class ButtonActionExecutor { // 액션에 필드 매핑 정보가 있는지 확인 if (action.fieldMappings && Array.isArray(action.fieldMappings)) { - console.log("📋 삽입 액션 - 필드 매핑 정보:", action.fieldMappings); // 🎯 체크박스로 선택된 데이터가 있는지 확인 if (!context.selectedRowsData || context.selectedRowsData.length === 0) { @@ -4946,8 +4505,6 @@ export class ButtonActionExecutor { } const sourceData = context.selectedRowsData[0]; // 첫 번째 선택된 데이터 사용 - console.log("🎯 삽입 소스 데이터 (체크박스 선택):", sourceData); - console.log("🔍 소스 데이터 사용 가능한 키들:", Object.keys(sourceData)); // 필드 매핑에 따라 데이터 구성 action.fieldMappings.forEach((mapping: any) => { @@ -4957,24 +4514,22 @@ export class ButtonActionExecutor { let value: any; - console.log(`🔍 매핑 처리 중: ${sourceField} → ${targetField} (valueType: ${valueType})`); - // 값 소스에 따라 데이터 가져오기 if (valueType === "form" && context.formData && sourceField) { // 폼 데이터에서 가져오기 value = context.formData[sourceField]; - console.log(`📝 폼에서 매핑: ${sourceField} → ${targetField} = ${value}`); + } else if (valueType === "selection" && sourceField) { // 선택된 테이블 데이터에서 가져오기 (다양한 필드명 시도) value = sourceData[sourceField] || sourceData[sourceField + "_name"] || // 조인된 필드 (_name 접미사) sourceData[sourceField + "Name"]; // 카멜케이스 - console.log(`📊 테이블에서 매핑: ${sourceField} → ${targetField} = ${value} (소스필드: ${sourceField})`); + } else if (valueType === "default" || (defaultValue !== undefined && defaultValue !== "")) { // 기본값 사용 (valueType이 "default"이거나 defaultValue가 있을 때) value = defaultValue; - console.log(`🔧 기본값 매핑: ${targetField} = ${value}`); + } else { console.warn(`⚠️ 매핑 실패: ${sourceField} → ${targetField} (값을 찾을 수 없음)`); console.warn(` - valueType: ${valueType}, defaultValue: ${defaultValue}`); @@ -4989,11 +4544,10 @@ export class ButtonActionExecutor { } }); - console.log("🎯 최종 삽입 데이터 (필드매핑 적용):", insertData); } else { // 필드 매핑이 없으면 폼 데이터를 기본으로 사용 insertData = { ...context.formData }; - console.log("📝 기본 삽입 데이터 (폼 기반):", insertData); + } try { @@ -5007,15 +4561,10 @@ export class ButtonActionExecutor { data: insertData, }; - console.log("🎯 대상 테이블:", targetTable); - console.log("📋 삽입할 데이터:", insertData); - - console.log("💾 폼 데이터 저장 요청:", formDataPayload); - const result = await DynamicFormApi.saveFormData(formDataPayload); if (result.success) { - console.log("✅ 삽입 성공:", result); + toast.success("데이터가 타겟 테이블에 성공적으로 삽입되었습니다."); } else { throw new Error(result.message || "삽입 실패"); @@ -5090,12 +4639,6 @@ export class ButtonActionExecutor { } // recordId가 없어도 괜찮음 - 전체 테이블 이력 보기 - console.log("📋 이력 조회 대상:", { - tableName, - recordId: recordId || "전체", - recordLabel, - mode: recordId ? "단일 레코드" : "전체 테이블", - }); // 이력 모달 열기 (동적 import) try { @@ -5153,7 +4696,6 @@ export class ButtonActionExecutor { if (relationResponse.success && relationResponse.data) { // 마스터-디테일 구조인 경우 전용 API 사용 - console.log("📊 마스터-디테일 엑셀 다운로드:", relationResponse.data); const downloadResponse = await DynamicFormApi.getMasterDetailDownloadData( context.screenId, @@ -5170,7 +4712,6 @@ export class ButtonActionExecutor { columnLabels![col] = downloadResponse.data.headers[index] || col; }); - console.log(`✅ 마스터-디테일 데이터 조회 완료: ${dataToExport.length}행`); } else { toast.error("마스터-디테일 데이터 조회에 실패했습니다."); return false; @@ -5563,13 +5104,7 @@ export class ButtonActionExecutor { afterUploadFlows, }; } - - console.log("📊 마스터-디테일 구조 자동 감지:", { - masterTable: relationResponse.data.masterTable, - detailTable: relationResponse.data.detailTable, - masterKeyColumn: relationResponse.data.masterKeyColumn, - numberingRuleId: masterDetailExcelConfig?.numberingRuleId, - }); + } } @@ -5591,12 +5126,6 @@ export class ButtonActionExecutor { // localStorage 디버깅 const modalId = `excel-upload-${context.tableName || ""}`; const storageKey = `modal_size_${modalId}_${context.userId || "guest"}`; - console.log("🔍 엑셀 업로드 모달 localStorage 확인:", { - modalId, - userId: context.userId, - storageKey, - savedSize: localStorage.getItem(storageKey), - }); root.render( React.createElement(ExcelUploadModal, { @@ -5604,10 +5133,7 @@ export class ButtonActionExecutor { onOpenChange: (open: boolean) => { if (!open) { // 모달 닫을 때 localStorage 확인 - console.log("🔍 모달 닫을 때 localStorage:", { - storageKey, - savedSize: localStorage.getItem(storageKey), - }); + closeModal(); } }, @@ -5674,7 +5200,6 @@ export class ButtonActionExecutor { autoSubmit: config.barcodeAutoSubmit || false, userId: context.userId, onScanSuccess: (barcode: string) => { - console.log("✅ 바코드 스캔 성공:", barcode); // 대상 필드에 값 입력 if (config.barcodeTargetField && context.onFormDataChange) { @@ -5901,7 +5426,6 @@ export class ButtonActionExecutor { */ private static async handleTrackingStart(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { - console.log("🚀 [handleTrackingStart] 위치 추적 시작:", { config, context }); // 이미 추적 중인지 확인 if (this.trackingIntervalId) { @@ -5958,7 +5482,6 @@ export class ButtonActionExecutor { updateField: config.trackingStatusField, updateValue: config.trackingStatusOnStart, }); - console.log("✅ 상태 변경 완료:", config.trackingStatusOnStart); // 🆕 출발지/도착지도 vehicles 테이블에 저장 if (departure) { @@ -5970,7 +5493,7 @@ export class ButtonActionExecutor { updateField: "departure", updateValue: departure, }); - console.log("✅ 출발지 저장 완료:", departure); + } catch { // 컬럼이 없으면 무시 } @@ -5985,7 +5508,7 @@ export class ButtonActionExecutor { updateField: "arrival", updateValue: arrival, }); - console.log("✅ 도착지 저장 완료:", arrival); + } catch { // 컬럼이 없으면 무시 } @@ -6110,7 +5633,6 @@ export class ButtonActionExecutor { if (isTrackingActive && tripId) { try { const tripStats = await this.calculateTripStats(tripId); - console.log("📊 운행 통계:", tripStats); // 운행 통계를 두 테이블에 저장 if (tripStats) { @@ -6118,15 +5640,6 @@ export class ButtonActionExecutor { const timeMinutes = tripStats.totalTimeMinutes; const userId = this.trackingUserId || context.userId; - console.log("💾 운행 통계 DB 저장 시도:", { - tripId, - userId, - distanceMeters, - timeMinutes, - startTime: tripStats.startTime, - endTime: tripStats.endTime, - }); - const { apiClient } = await import("@/lib/api/client"); // 1️⃣ vehicle_location_history 마지막 레코드에 통계 저장 (이력용) @@ -6164,7 +5677,7 @@ export class ButtonActionExecutor { updateValue: update.value, }); } - console.log("✅ vehicle_location_history 통계 저장 완료"); + } else { console.warn("⚠️ trip_id에 해당하는 레코드를 찾을 수 없음:", tripId); } @@ -6191,7 +5704,7 @@ export class ButtonActionExecutor { updateValue: update.value, }); } - console.log("✅ vehicles 테이블 통계 업데이트 완료"); + } catch (vehicleError) { console.warn("⚠️ vehicles 테이블 저장 실패:", vehicleError); } @@ -6241,7 +5754,6 @@ export class ButtonActionExecutor { updateField: effectiveConfig.trackingStatusField, updateValue: effectiveConfig.trackingStatusOnStop, }); - console.log("✅ 상태 변경 완료:", effectiveConfig.trackingStatusOnStop); // 🆕 운행 종료 시 vehicles 테이블의 출발지/도착지/위도/경도를 null로 초기화 const fieldsToReset = ["departure", "arrival", "latitude", "longitude"]; @@ -6258,7 +5770,7 @@ export class ButtonActionExecutor { // 컬럼이 없으면 무시 } } - console.log("✅ 출발지/도착지/위도/경도 초기화 완료"); + } } catch (statusError) { console.warn("⚠️ 상태 변경 실패:", statusError); @@ -6312,7 +5824,7 @@ export class ButtonActionExecutor { }); if (!response.data?.success) { - console.log("📊 통계 계산: API 응답 실패"); + return null; } @@ -6320,12 +5832,11 @@ export class ButtonActionExecutor { const rows = response.data?.data?.data || response.data?.data?.rows || []; if (!rows.length) { - console.log("📊 통계 계산: 데이터 없음"); + return null; } const locations = rows; - console.log(`📊 통계 계산: ${locations.length}개 위치 데이터`); // 시간 계산 const startTime = locations[0].recorded_at; @@ -6352,15 +5863,6 @@ export class ButtonActionExecutor { const totalDistanceKm = totalDistanceM / 1000; - console.log("📊 운행 통계 결과:", { - tripId, - totalDistanceKm, - totalTimeMinutes, - startTime, - endTime, - pointCount: locations.length, - }); - return { totalDistanceKm, totalTimeMinutes, @@ -6431,7 +5933,7 @@ export class ButtonActionExecutor { const response = await apiClient.post("/dynamic-form/location-history", locationData); if (response.data?.success) { - console.log("✅ 위치 이력 저장 성공:", response.data.data); + } else { console.warn("⚠️ 위치 이력 저장 실패:", response.data); } @@ -6463,7 +5965,6 @@ export class ButtonActionExecutor { updateValue: longitude, }); - console.log(`✅ vehicles 테이블 위치 업데이트: (${latitude.toFixed(6)}, ${longitude.toFixed(6)})`); } catch (vehicleUpdateError) { // 컬럼이 없으면 조용히 무시 console.warn("⚠️ vehicles 테이블 위치 업데이트 실패 (무시):", vehicleUpdateError); @@ -6509,7 +6010,6 @@ export class ButtonActionExecutor { */ private static async handleTransferData(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { - console.log("📤 [handleTransferData] 데이터 전달 시작:", { config, context }); // 선택된 행 데이터 확인 const selectedRows = context.selectedRowsData || context.flowSelectedData || []; @@ -6519,14 +6019,11 @@ export class ButtonActionExecutor { return false; } - console.log("📤 [handleTransferData] 선택된 데이터:", selectedRows); - // dataTransfer 설정 확인 const dataTransfer = config.dataTransfer; if (!dataTransfer) { // dataTransfer 설정이 없으면 기본 동작: 전역 이벤트로 데이터 전달 - console.log("📤 [handleTransferData] dataTransfer 설정 없음 - 전역 이벤트 발생"); const transferEvent = new CustomEvent("splitPanelDataTransfer", { detail: { @@ -6546,7 +6043,6 @@ export class ButtonActionExecutor { if (targetType === "component" && targetComponentId) { // 같은 화면 내 컴포넌트로 전달 - console.log("📤 [handleTransferData] 컴포넌트로 전달:", targetComponentId); const transferEvent = new CustomEvent("componentDataTransfer", { detail: { @@ -6562,7 +6058,6 @@ export class ButtonActionExecutor { return true; } else if (targetType === "screen" && targetScreenId) { // 다른 화면으로 전달 (분할 패널 등) - console.log("📤 [handleTransferData] 화면으로 전달:", targetScreenId); const transferEvent = new CustomEvent("screenDataTransfer", { detail: { @@ -6578,7 +6073,6 @@ export class ButtonActionExecutor { return true; } else { // 기본: 분할 패널 데이터 전달 이벤트 - console.log("📤 [handleTransferData] 기본 분할 패널 전달"); const transferEvent = new CustomEvent("splitPanelDataTransfer", { detail: { @@ -6893,7 +6387,6 @@ export class ButtonActionExecutor { */ private static async handleSwapFields(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { - console.log("🔄 필드 값 교환 액션 실행:", { config, context }); const { formData, onFormDataChange } = context; @@ -6910,8 +6403,6 @@ export class ButtonActionExecutor { const valueA = formData?.[fieldA]; const valueB = formData?.[fieldB]; - console.log("🔄 교환 전:", { [fieldA]: valueA, [fieldB]: valueB }); - // 값 교환 if (onFormDataChange) { onFormDataChange(fieldA, valueB); @@ -6930,8 +6421,6 @@ export class ButtonActionExecutor { } } - console.log("🔄 교환 후:", { [fieldA]: valueB, [fieldB]: valueA }); - toast.success(config.successMessage || "값이 교환되었습니다."); return true; } catch (error) { @@ -6947,7 +6436,6 @@ export class ButtonActionExecutor { */ private static async handleQuickInsert(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { - console.log("⚡ Quick Insert 액션 실행:", { config, context }); const quickInsertConfig = config.quickInsertConfig; if (!quickInsertConfig?.targetTable) { @@ -6957,17 +6445,14 @@ export class ButtonActionExecutor { // ✅ allComponents가 있으면 기존 필수 항목 검증 수행 if (context.allComponents && context.allComponents.length > 0) { - console.log("🔍 [handleQuickInsert] 필수 항목 검증 시작:", { - hasAllComponents: !!context.allComponents, - allComponentsLength: context.allComponents?.length || 0, - }); + const requiredValidation = this.validateRequiredFields(context); if (!requiredValidation.isValid) { console.log("❌ [handleQuickInsert] 필수 항목 누락:", requiredValidation.missingFields); toast.error(`필수 항목을 입력해주세요: ${requiredValidation.missingFields.join(", ")}`); return false; } - console.log("✅ [handleQuickInsert] 필수 항목 검증 통과"); + } // ✅ quickInsert 전용 검증: component 타입 매핑에서 값이 비어있는지 확인 @@ -7016,23 +6501,9 @@ export class ButtonActionExecutor { toast.error(`다음 항목을 입력해주세요: ${missingMappingFields.join(", ")}`); return false; } - console.log("✅ [handleQuickInsert] quickInsert 매핑 검증 통과"); const { formData, splitPanelContext, userId, userName, companyCode } = context; - console.log("⚡ Quick Insert 상세 정보:", { - targetTable: quickInsertConfig.targetTable, - columnMappings: quickInsertConfig.columnMappings, - formData: formData, - formDataKeys: Object.keys(formData || {}), - splitPanelContext: splitPanelContext, - selectedLeftData: splitPanelContext?.selectedLeftData, - allComponents: context.allComponents, - userId, - userName, - companyCode, - }); - // 컬럼 매핑에 따라 저장할 데이터 구성 const insertData: Record = {}; const columnMappings = quickInsertConfig.columnMappings || []; @@ -7201,8 +6672,6 @@ export class ButtonActionExecutor { } } - console.log("⚡ Quick Insert 최종 데이터:", insertData, "키 개수:", Object.keys(insertData).length); - // 필수 데이터 검증 if (Object.keys(insertData).length === 0) { toast.error("저장할 데이터가 없습니다."); @@ -7268,7 +6737,6 @@ export class ButtonActionExecutor { ); if (response.data?.success) { - console.log("✅ Quick Insert 저장 성공"); // 저장 후 동작 설정 로그 console.log("📍 afterInsert 설정:", quickInsertConfig.afterInsert); @@ -7284,7 +6752,7 @@ export class ButtonActionExecutor { if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("refreshTable")); window.dispatchEvent(new CustomEvent("refreshCardDisplay")); - console.log("✅ refreshTable, refreshCardDisplay 이벤트 발송 완료"); + } } @@ -7330,7 +6798,6 @@ export class ButtonActionExecutor { context: ButtonActionContext, ): Promise { try { - console.log("🔄 운행알림/종료 액션 실행:", { config, context }); // 🆕 출발지/도착지 필수 체크 (운행 시작 모드일 때만) // updateTrackingMode가 "start"이거나 updateTargetValue가 "active"/"inactive"인 경우 @@ -7398,7 +6865,7 @@ export class ButtonActionExecutor { if (config.confirmMessage) { const confirmed = window.confirm(config.confirmMessage); if (!confirmed) { - console.log("🔄 필드 값 변경 취소됨 (사용자가 취소)"); + return false; } } @@ -7479,8 +6946,6 @@ export class ButtonActionExecutor { } } - console.log("🔄 변경할 필드들:", updates); - // formData 업데이트 if (onFormDataChange) { Object.entries(updates).forEach(([field, value]) => { @@ -7501,14 +6966,6 @@ export class ButtonActionExecutor { // 특수 키워드 변환 (예: __userId__ → 실제 사용자 ID) const keyValue = resolveSpecialKeyword(keySourceField, context); - console.log("🔄 필드 값 변경 - 키 필드 사용:", { - targetTable: targetTableName, - keyField, - keySourceField, - keyValue, - updates, - }); - if (!keyValue) { console.warn("⚠️ 키 값이 없어서 업데이트를 건너뜁니다:", { keySourceField, keyValue }); toast.error("레코드를 식별할 키 값이 없습니다."); @@ -7520,7 +6977,6 @@ export class ButtonActionExecutor { const { apiClient } = await import("@/lib/api/client"); for (const [field, value] of Object.entries(updates)) { - console.log(`🔄 DB UPDATE: ${targetTableName}.${field} = ${value} WHERE ${keyField} = ${keyValue}`); const response = await apiClient.put("/dynamic-form/update-field", { tableName: targetTableName, @@ -7537,7 +6993,6 @@ export class ButtonActionExecutor { } } - console.log("✅ 모든 필드 업데이트 성공"); toast.success(config.successMessage || "상태가 변경되었습니다."); // 테이블 새로고침 이벤트 발생 @@ -7557,7 +7012,7 @@ export class ButtonActionExecutor { // onSave 콜백이 있으면 사용 if (onSave) { - console.log("🔄 필드 값 변경 후 자동 저장 (onSave 콜백)"); + try { await onSave(); toast.success(config.successMessage || "상태가 변경되었습니다."); @@ -7571,7 +7026,7 @@ export class ButtonActionExecutor { // API를 통한 직접 저장 (기존 방식: formData에 PK가 있는 경우) if (tableName && formData) { - console.log("🔄 필드 값 변경 후 자동 저장 (API 직접 호출)"); + try { // PK 필드 찾기 (id 또는 테이블명_id) const pkField = formData.id !== undefined ? "id" : `${tableName}_id`;