From 1139cea838bf5e42a139ed3701db6a61ee229312 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 24 Nov 2025 16:54:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(table-list):=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EB=84=88=EB=B9=84=20=EC=9E=90=EB=8F=99=20=EC=A1=B0=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=95=EB=A0=AC=20=EC=83=81=ED=83=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 내용 기반 컬럼 너비 자동 계산 (상위 50개 샘플링) - 사용자가 조정한 컬럼 너비를 localStorage에 저장/복원 - 정렬 상태(컬럼, 방향)를 localStorage에 저장/복원 - 사용자별, 테이블별 독립적인 설정 관리 변경: - TableListComponent.tsx: calculateOptimalColumnWidth 추가, 정렬 상태 저장/복원 로직 추가 - README.md: 새로운 기능 문서화 저장 키: - table_column_widths_{테이블}_{사용자}: 컬럼 너비 - table_sort_state_{테이블}_{사용자}: 정렬 상태 Fixes: 수주관리 화면에서 컬럼 너비 수동 조정 번거로움, 정렬 설정 미유지 문제 --- .../table-list/TableListComponent.tsx | 130 ++++++++++++++++-- 1 file changed, 117 insertions(+), 13 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 12bdb7d1..a8356721 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -322,6 +322,7 @@ export const TableListComponent: React.FC = ({ const [searchTerm, setSearchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const hasInitializedSort = useRef(false); const [columnLabels, setColumnLabels] = useState>({}); const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); @@ -508,6 +509,28 @@ export const TableListComponent: React.FC = ({ unregisterTable, ]); + // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 + useEffect(() => { + if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return; + + const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; + const savedSort = localStorage.getItem(storageKey); + + if (savedSort) { + try { + const { column, direction } = JSON.parse(savedSort); + if (column && direction) { + setSortColumn(column); + setSortDirection(direction); + hasInitializedSort.current = true; + console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction }); + } + } catch (error) { + console.error("❌ 정렬 상태 복원 실패:", error); + } + } + }, [tableConfig.selectedTable, userId]); + // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기 useEffect(() => { if (!tableConfig.selectedTable || !userId) return; @@ -955,6 +978,20 @@ export const TableListComponent: React.FC = ({ newSortDirection = "asc"; } + // 🎯 정렬 상태를 localStorage에 저장 (사용자별) + if (tableConfig.selectedTable && userId) { + const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; + try { + localStorage.setItem(storageKey, JSON.stringify({ + column: newSortColumn, + direction: newSortDirection + })); + console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection }); + } catch (error) { + console.error("❌ 정렬 상태 저장 실패:", error); + } + } + console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection }); console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange); @@ -1876,11 +1913,59 @@ export const TableListComponent: React.FC = ({ }; }, [tableConfig.selectedTable, isDesignMode]); - // 초기 컬럼 너비 측정 (한 번만) + // 🎯 컬럼 너비 자동 계산 (내용 기반) + const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => { + // 기본 너비 설정 + const MIN_WIDTH = 100; + const MAX_WIDTH = 400; + const PADDING = 48; // 좌우 패딩 + 여유 공간 + const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등) + + // 헤더 텍스트 너비 계산 (대략 8px per character) + const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING; + + // 데이터 셀 너비 계산 (상위 50개 샘플링) + const sampleSize = Math.min(50, data.length); + let maxDataWidth = headerWidth; + + for (let i = 0; i < sampleSize; i++) { + const cellValue = data[i]?.[columnName]; + if (cellValue !== null && cellValue !== undefined) { + const cellText = String(cellValue); + // 숫자는 좁게, 텍스트는 넓게 계산 + const isNumber = !isNaN(Number(cellValue)) && cellValue !== ""; + const charWidth = isNumber ? 8 : 9; + const cellWidth = cellText.length * charWidth + PADDING; + maxDataWidth = Math.max(maxDataWidth, cellWidth); + } + } + + // 최소/최대 범위 내로 제한 + return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth))); + }, [data]); + + // 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산 useEffect(() => { - if (!hasInitializedWidths.current && visibleColumns.length > 0) { - // 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정 + if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) { const timer = setTimeout(() => { + const storageKey = tableConfig.selectedTable && userId + ? `table_column_widths_${tableConfig.selectedTable}_${userId}` + : null; + + // 1. localStorage에서 저장된 너비 불러오기 + let savedWidths: Record = {}; + if (storageKey) { + try { + const saved = localStorage.getItem(storageKey); + if (saved) { + savedWidths = JSON.parse(saved); + } + } catch (error) { + console.error("컬럼 너비 불러오기 실패:", error); + } + } + + // 2. 자동 계산 또는 저장된 너비 적용 const newWidths: Record = {}; let hasAnyWidth = false; @@ -1888,13 +1973,18 @@ export const TableListComponent: React.FC = ({ // 체크박스 컬럼은 제외 (고정 48px) if (column.columnName === "__checkbox__") return; - const thElement = columnRefs.current[column.columnName]; - if (thElement) { - const measuredWidth = thElement.offsetWidth; - if (measuredWidth > 0) { - newWidths[column.columnName] = measuredWidth; - hasAnyWidth = true; - } + // 저장된 너비가 있으면 우선 사용 + if (savedWidths[column.columnName]) { + newWidths[column.columnName] = savedWidths[column.columnName]; + hasAnyWidth = true; + } else { + // 저장된 너비가 없으면 자동 계산 + const optimalWidth = calculateOptimalColumnWidth( + column.columnName, + columnLabels[column.columnName] || column.displayName + ); + newWidths[column.columnName] = optimalWidth; + hasAnyWidth = true; } }); @@ -1902,11 +1992,11 @@ export const TableListComponent: React.FC = ({ setColumnWidths(newWidths); hasInitializedWidths.current = true; } - }, 100); + }, 150); // DOM 렌더링 대기 return () => clearTimeout(timer); } - }, [visibleColumns]); + }, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]); // ======================================== // 페이지네이션 JSX @@ -2241,7 +2331,21 @@ export const TableListComponent: React.FC = ({ // 최종 너비를 state에 저장 if (thElement) { const finalWidth = Math.max(80, thElement.offsetWidth); - setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth })); + setColumnWidths((prev) => { + const newWidths = { ...prev, [column.columnName]: finalWidth }; + + // 🎯 localStorage에 컬럼 너비 저장 (사용자별) + if (tableConfig.selectedTable && userId) { + const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`; + try { + localStorage.setItem(storageKey, JSON.stringify(newWidths)); + } catch (error) { + console.error("컬럼 너비 저장 실패:", error); + } + } + + return newWidths; + }); } // 텍스트 선택 복원 From a9f57add62f2256f86a08ea21d12bdd8271e7d1d Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 25 Nov 2025 12:07:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EC=88=98=EC=A3=BC=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=92=88=EB=AA=A9=20=EC=B6=94=EA=B0=80/=EC=88=98?= =?UTF-8?q?=EC=A0=95/=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditModal의 handleSave가 button-primary까지 전달되도록 수정 - ConditionalContainer/ConditionalSectionViewer에 onSave prop 추가 - DynamicComponentRenderer와 InteractiveScreenViewerDynamic에 onSave 전달 로직 추가 - ButtonActionExecutor에서 context.onSave 콜백 우선 실행 로직 구현 - 신규 품목 추가 시 groupByColumns 값 자동 포함 처리 기능: - 품목 추가: order_no 자동 설정 - 품목 수정: 변경 필드만 부분 업데이트 - 품목 삭제: originalGroupData 비교 후 제거 --- frontend/components/screen/EditModal.tsx | 220 ++++++++++++------ .../screen/InteractiveScreenViewerDynamic.tsx | 6 + .../lib/registry/DynamicComponentRenderer.tsx | 3 + .../button-primary/ButtonPrimaryComponent.tsx | 7 + .../ConditionalContainerComponent.tsx | 3 + .../ConditionalSectionViewer.tsx | 20 +- .../components/conditional-container/types.ts | 2 + frontend/lib/utils/buttonActions.ts | 19 +- 8 files changed, 204 insertions(+), 76 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 4e756600..3280891f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -305,84 +305,173 @@ export const EditModal: React.FC = ({ className }) => { } try { - // 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정 - if (groupData.length > 0) { - console.log("🔄 그룹 데이터 일괄 수정 시작:", { + // 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 처리 (추가/수정/삭제) + if (groupData.length > 0 || originalGroupData.length > 0) { + console.log("🔄 그룹 데이터 일괄 처리 시작:", { groupDataLength: groupData.length, originalGroupDataLength: originalGroupData.length, + groupData, + originalGroupData, + tableName: screenData.screenInfo.tableName, + screenId: modalState.screenId, }); + let insertedCount = 0; let updatedCount = 0; + let deletedCount = 0; - for (let i = 0; i < groupData.length; i++) { - const currentData = groupData[i]; - const originalItemData = originalGroupData[i]; + // 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외) + const salesOrderColumns = [ + "id", + "order_no", + "customer_code", + "customer_name", + "order_date", + "delivery_date", + "item_code", + "quantity", + "unit_price", + "amount", + "status", + "notes", + "created_at", + "updated_at", + "company_code", + ]; - if (!originalItemData) { - console.warn(`원본 데이터가 없습니다 (index: ${i})`); - continue; - } + // 1️⃣ 신규 품목 추가 (id가 없는 항목) + for (const currentData of groupData) { + if (!currentData.id) { + console.log("➕ 신규 품목 추가:", currentData); - // 변경된 필드만 추출 - const changedData: Record = {}; - - // 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외) - const salesOrderColumns = [ - "id", - "order_no", - "customer_code", - "customer_name", - "order_date", - "delivery_date", - "item_code", - "quantity", - "unit_price", - "amount", - "status", - "notes", - "created_at", - "updated_at", - "company_code", - ]; - - Object.keys(currentData).forEach((key) => { - // sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외) - if (!salesOrderColumns.includes(key)) { - return; + // 실제 테이블 컬럼만 추출 + const insertData: Record = {}; + Object.keys(currentData).forEach((key) => { + if (salesOrderColumns.includes(key) && key !== "id") { + insertData[key] = currentData[key]; + } + }); + + // 🆕 groupByColumns의 값을 강제로 포함 (order_no 등) + if (modalState.groupByColumns && modalState.groupByColumns.length > 0) { + modalState.groupByColumns.forEach((colName) => { + // 기존 품목(groupData[0])에서 groupByColumns 값 가져오기 + const referenceData = originalGroupData[0] || groupData.find(item => item.id); + if (referenceData && referenceData[colName]) { + insertData[colName] = referenceData[colName]; + console.log(`🔑 [신규 품목] ${colName} 값 추가:`, referenceData[colName]); + } + }); } - - if (currentData[key] !== originalItemData[key]) { - changedData[key] = currentData[key]; + + console.log("📦 [신규 품목] 최종 insertData:", insertData); + + try { + const response = await dynamicFormApi.saveFormData({ + screenId: modalState.screenId || 0, + tableName: screenData.screenInfo.tableName, + data: insertData, + }); + + if (response.success) { + insertedCount++; + console.log("✅ 신규 품목 추가 성공:", response.data); + } else { + console.error("❌ 신규 품목 추가 실패:", response.message); + } + } catch (error: any) { + console.error("❌ 신규 품목 추가 오류:", error); } - }); - - // 변경사항이 없으면 스킵 - if (Object.keys(changedData).length === 0) { - console.log(`변경사항 없음 (index: ${i})`); - continue; - } - - // 기본키 확인 - const recordId = originalItemData.id || Object.values(originalItemData)[0]; - - // UPDATE 실행 - const response = await dynamicFormApi.updateFormDataPartial( - recordId, - originalItemData, - changedData, - screenData.screenInfo.tableName, - ); - - if (response.success) { - updatedCount++; - console.log(`✅ 품목 ${i + 1} 수정 성공 (id: ${recordId})`); - } else { - console.error(`❌ 품목 ${i + 1} 수정 실패 (id: ${recordId}):`, response.message); } } - if (updatedCount > 0) { - toast.success(`${updatedCount}개의 품목이 수정되었습니다.`); + // 2️⃣ 기존 품목 수정 (id가 있는 항목) + for (const currentData of groupData) { + if (currentData.id) { + // id 기반 매칭 (인덱스 기반 X) + const originalItemData = originalGroupData.find( + (orig) => orig.id === currentData.id + ); + + if (!originalItemData) { + console.warn(`원본 데이터를 찾을 수 없습니다 (id: ${currentData.id})`); + continue; + } + + // 변경된 필드만 추출 + const changedData: Record = {}; + Object.keys(currentData).forEach((key) => { + // sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외) + if (!salesOrderColumns.includes(key)) { + return; + } + + if (currentData[key] !== originalItemData[key]) { + changedData[key] = currentData[key]; + } + }); + + // 변경사항이 없으면 스킵 + if (Object.keys(changedData).length === 0) { + console.log(`변경사항 없음 (id: ${currentData.id})`); + continue; + } + + // UPDATE 실행 + try { + const response = await dynamicFormApi.updateFormDataPartial( + currentData.id, + originalItemData, + changedData, + screenData.screenInfo.tableName, + ); + + if (response.success) { + updatedCount++; + console.log(`✅ 품목 수정 성공 (id: ${currentData.id})`); + } else { + console.error(`❌ 품목 수정 실패 (id: ${currentData.id}):`, response.message); + } + } catch (error: any) { + console.error(`❌ 품목 수정 오류 (id: ${currentData.id}):`, error); + } + } + } + + // 3️⃣ 삭제된 품목 제거 (원본에는 있지만 현재 데이터에는 없는 항목) + const currentIds = new Set(groupData.map((item) => item.id).filter(Boolean)); + const deletedItems = originalGroupData.filter( + (orig) => orig.id && !currentIds.has(orig.id) + ); + + for (const deletedItem of deletedItems) { + console.log("🗑️ 품목 삭제:", deletedItem); + + try { + const response = await dynamicFormApi.deleteFormDataFromTable( + deletedItem.id, + screenData.screenInfo.tableName + ); + + if (response.success) { + deletedCount++; + console.log(`✅ 품목 삭제 성공 (id: ${deletedItem.id})`); + } else { + console.error(`❌ 품목 삭제 실패 (id: ${deletedItem.id}):`, response.message); + } + } catch (error: any) { + console.error(`❌ 품목 삭제 오류 (id: ${deletedItem.id}):`, error); + } + } + + // 결과 메시지 + const messages: string[] = []; + if (insertedCount > 0) messages.push(`${insertedCount}개 추가`); + if (updatedCount > 0) messages.push(`${updatedCount}개 수정`); + if (deletedCount > 0) messages.push(`${deletedCount}개 삭제`); + + if (messages.length > 0) { + toast.success(`품목이 저장되었습니다 (${messages.join(", ")})`); // 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침) if (modalState.onSave) { @@ -585,6 +674,7 @@ export const EditModal: React.FC = ({ className }) => { tableName: screenData.screenInfo?.tableName, }} onSave={handleSave} + isInModal={true} // 🆕 그룹 데이터를 ModalRepeaterTable에 전달 groupedData={groupData.length > 0 ? groupData : undefined} /> diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index d1cd2a5f..fb5046c3 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -48,6 +48,8 @@ interface InteractiveScreenViewerProps { companyCode?: string; // 🆕 그룹 데이터 (EditModal에서 전달) groupedData?: Record[]; + // 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록) + isInModal?: boolean; } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -64,6 +66,7 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName: authUserName, user: authUser } = useAuth(); @@ -329,6 +332,7 @@ export const InteractiveScreenViewerDynamic: React.FC { @@ -401,6 +405,8 @@ export const InteractiveScreenViewerDynamic: React.FC { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 92fd89e8..bf2b6ecb 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -105,6 +105,7 @@ export interface DynamicComponentRendererProps { companyCode?: string; // 🆕 현재 사용자의 회사 코드 onRefresh?: () => void; onClose?: () => void; + onSave?: () => Promise; // 🆕 EditModal의 handleSave 콜백 // 테이블 선택된 행 정보 (다중 선택 액션용) selectedRows?: any[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable) @@ -244,6 +245,7 @@ export const DynamicComponentRenderer: React.FC = selectedScreen, // 🆕 화면 정보 onRefresh, onClose, + onSave, // 🆕 EditModal의 handleSave 콜백 screenId, userId, // 🆕 사용자 ID userName, // 🆕 사용자 이름 @@ -358,6 +360,7 @@ export const DynamicComponentRenderer: React.FC = selectedScreen, // 🆕 화면 정보 onRefresh, onClose, + onSave, // 🆕 EditModal의 handleSave 콜백 screenId, userId, // 🆕 사용자 ID userName, // 🆕 사용자 이름 diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 112a285c..d2b69074 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -35,6 +35,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps { onRefresh?: () => void; onClose?: () => void; onFlowRefresh?: () => void; + onSave?: () => Promise; // 🆕 EditModal의 handleSave 콜백 // 폼 데이터 관련 originalData?: Record; // 부분 업데이트용 원본 데이터 @@ -83,6 +84,7 @@ export const ButtonPrimaryComponent: React.FC = ({ onRefresh, onClose, onFlowRefresh, + onSave, // 🆕 EditModal의 handleSave 콜백 sortBy, // 🆕 정렬 컬럼 sortOrder, // 🆕 정렬 방향 columnOrder, // 🆕 컬럼 순서 @@ -95,6 +97,10 @@ export const ButtonPrimaryComponent: React.FC = ({ ...props }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 + + // 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출) + const propsOnSave = (props as any).onSave as (() => Promise) | undefined; + const finalOnSave = onSave || propsOnSave; // 🆕 플로우 단계별 표시 제어 const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig; @@ -415,6 +421,7 @@ export const ButtonPrimaryComponent: React.FC = ({ onRefresh, onClose, onFlowRefresh, // 플로우 새로고침 콜백 추가 + onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출) // 테이블 선택된 행 정보 추가 selectedRows, selectedRowsData, diff --git a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx index 2589026f..626ee137 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx @@ -41,6 +41,7 @@ export function ConditionalContainerComponent({ style, className, groupedData, // 🆕 그룹 데이터 + onSave, // 🆕 EditModal의 handleSave 콜백 }: ConditionalContainerProps) { console.log("🎯 ConditionalContainerComponent 렌더링!", { isDesignMode, @@ -179,6 +180,7 @@ export function ConditionalContainerComponent({ formData={formData} onFormDataChange={onFormDataChange} groupedData={groupedData} + onSave={onSave} /> ))} @@ -199,6 +201,7 @@ export function ConditionalContainerComponent({ formData={formData} onFormDataChange={onFormDataChange} groupedData={groupedData} + onSave={onSave} /> ) : null ) diff --git a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx index f77dbcdb..9709b620 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx @@ -26,6 +26,7 @@ export function ConditionalSectionViewer({ formData, onFormDataChange, groupedData, // 🆕 그룹 데이터 + onSave, // 🆕 EditModal의 handleSave 콜백 }: ConditionalSectionViewerProps) { const { userId, userName, user } = useAuth(); const [isLoading, setIsLoading] = useState(false); @@ -153,17 +154,18 @@ export function ConditionalSectionViewer({ }} > + onSave={onSave} + /> ); })} diff --git a/frontend/lib/registry/components/conditional-container/types.ts b/frontend/lib/registry/components/conditional-container/types.ts index 0cf741b2..bcd701ef 100644 --- a/frontend/lib/registry/components/conditional-container/types.ts +++ b/frontend/lib/registry/components/conditional-container/types.ts @@ -46,6 +46,7 @@ export interface ConditionalContainerProps { formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; groupedData?: Record[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable) + onSave?: () => Promise; // 🆕 EditModal의 handleSave 콜백 // 화면 편집기 관련 isDesignMode?: boolean; // 디자인 모드 여부 @@ -77,5 +78,6 @@ export interface ConditionalSectionViewerProps { formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; groupedData?: Record[]; // 🆕 그룹 데이터 + onSave?: () => Promise; // 🆕 EditModal의 handleSave 콜백 } diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 3b9b9d9e..ddcf0f18 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -112,6 +112,7 @@ export interface ButtonActionContext { onClose?: () => void; onRefresh?: () => void; onFlowRefresh?: () => void; // 플로우 새로고침 콜백 + onSave?: () => Promise; // 🆕 EditModal의 handleSave 콜백 // 테이블 선택된 행 정보 (다중 선택 액션용) selectedRows?: any[]; @@ -213,9 +214,23 @@ export class ButtonActionExecutor { * 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반) */ private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise { - const { formData, originalData, tableName, screenId } = context; + const { formData, originalData, tableName, screenId, onSave } = context; - console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId }); + console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId, hasOnSave: !!onSave }); + + // 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 + if (onSave) { + console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행"); + try { + await onSave(); + return true; + } catch (error) { + console.error("❌ [handleSave] onSave 콜백 실행 오류:", error); + throw error; + } + } + + console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행"); // 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집) // context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함