diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 965d2833..4d33dc1c 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -496,16 +496,27 @@ export class DynamicFormService { for (const repeater of mergedRepeaterData) { for (const item of repeater.data) { // 헤더 + 품목을 병합 - const mergedData = { ...dataToInsert, ...item }; + const rawMergedData = { ...dataToInsert, ...item }; - // 타입 변환 - Object.keys(mergedData).forEach((columnName) => { + // 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외) + const validColumnNames = columnInfo.map((col) => col.column_name); + const mergedData: Record = {}; + + Object.keys(rawMergedData).forEach((columnName) => { + // 실제 테이블 컬럼인지 확인 + if (validColumnNames.includes(columnName)) { const column = columnInfo.find((col) => col.column_name === columnName); if (column) { + // 타입 변환 mergedData[columnName] = this.convertValueForPostgreSQL( - mergedData[columnName], + rawMergedData[columnName], column.data_type ); + } else { + mergedData[columnName] = rawMergedData[columnName]; + } + } else { + console.log(`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`); } }); diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 88798fa6..4e756600 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -24,6 +24,8 @@ interface EditModalState { modalSize: "sm" | "md" | "lg" | "xl"; editData: Record; onSave?: () => void; + groupByColumns?: string[]; // 🆕 그룹핑 컬럼 (예: ["order_no"]) + tableName?: string; // 🆕 테이블명 (그룹 조회용) } interface EditModalProps { @@ -40,6 +42,8 @@ export const EditModal: React.FC = ({ className }) => { modalSize: "md", editData: {}, onSave: undefined, + groupByColumns: undefined, + tableName: undefined, }); const [screenData, setScreenData] = useState<{ @@ -58,6 +62,10 @@ export const EditModal: React.FC = ({ className }) => { // 폼 데이터 상태 (편집 데이터로 초기화됨) const [formData, setFormData] = useState>({}); const [originalData, setOriginalData] = useState>({}); + + // 🆕 그룹 데이터 상태 (같은 order_no의 모든 품목) + const [groupData, setGroupData] = useState[]>([]); + const [originalGroupData, setOriginalGroupData] = useState[]>([]); // 화면의 실제 크기 계산 함수 (ScreenModal과 동일) const calculateScreenDimensions = (components: ComponentData[]) => { @@ -110,7 +118,7 @@ export const EditModal: React.FC = ({ className }) => { // 전역 모달 이벤트 리스너 useEffect(() => { const handleOpenEditModal = (event: CustomEvent) => { - const { screenId, title, description, modalSize, editData, onSave } = event.detail; + const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName } = event.detail; setModalState({ isOpen: true, @@ -120,6 +128,8 @@ export const EditModal: React.FC = ({ className }) => { modalSize: modalSize || "lg", editData: editData || {}, onSave, + groupByColumns, // 🆕 그룹핑 컬럼 + tableName, // 🆕 테이블명 }); // 편집 데이터로 폼 데이터 초기화 @@ -154,9 +164,78 @@ export const EditModal: React.FC = ({ className }) => { useEffect(() => { if (modalState.isOpen && modalState.screenId) { loadScreenData(modalState.screenId); + + // 🆕 그룹 데이터 조회 (groupByColumns가 있는 경우) + if (modalState.groupByColumns && modalState.groupByColumns.length > 0 && modalState.tableName) { + loadGroupData(); + } } }, [modalState.isOpen, modalState.screenId]); + // 🆕 그룹 데이터 조회 함수 + const loadGroupData = async () => { + if (!modalState.tableName || !modalState.groupByColumns || modalState.groupByColumns.length === 0) { + console.warn("테이블명 또는 그룹핑 컬럼이 없습니다."); + return; + } + + try { + console.log("🔍 그룹 데이터 조회 시작:", { + tableName: modalState.tableName, + groupByColumns: modalState.groupByColumns, + editData: modalState.editData, + }); + + // 그룹핑 컬럼 값 추출 (예: order_no = "ORD-20251124-001") + const groupValues: Record = {}; + modalState.groupByColumns.forEach((column) => { + if (modalState.editData[column]) { + groupValues[column] = modalState.editData[column]; + } + }); + + if (Object.keys(groupValues).length === 0) { + console.warn("그룹핑 컬럼 값이 없습니다:", modalState.groupByColumns); + return; + } + + console.log("🔍 그룹 조회 요청:", { + tableName: modalState.tableName, + groupValues, + }); + + // 같은 그룹의 모든 레코드 조회 (entityJoinApi 사용) + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const response = await entityJoinApi.getTableDataWithJoins(modalState.tableName, { + page: 1, + size: 1000, + search: groupValues, // search 파라미터로 전달 (백엔드에서 WHERE 조건으로 처리) + enableEntityJoin: true, + }); + + console.log("🔍 그룹 조회 응답:", response); + + // entityJoinApi는 배열 또는 { data: [] } 형식으로 반환 + const dataArray = Array.isArray(response) ? response : response?.data || []; + + if (dataArray.length > 0) { + console.log("✅ 그룹 데이터 조회 성공:", dataArray); + setGroupData(dataArray); + setOriginalGroupData(JSON.parse(JSON.stringify(dataArray))); // Deep copy + toast.info(`${dataArray.length}개의 관련 품목을 불러왔습니다.`); + } else { + console.warn("그룹 데이터가 없습니다:", response); + setGroupData([modalState.editData]); // 기본값: 선택된 행만 + setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]); + } + } catch (error: any) { + console.error("❌ 그룹 데이터 조회 오류:", error); + toast.error("관련 데이터를 불러오는 중 오류가 발생했습니다."); + setGroupData([modalState.editData]); // 기본값: 선택된 행만 + setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]); + } + }; + const loadScreenData = async (screenId: number) => { try { setLoading(true); @@ -208,10 +287,14 @@ export const EditModal: React.FC = ({ className }) => { modalSize: "md", editData: {}, onSave: undefined, + groupByColumns: undefined, + tableName: undefined, }); setScreenData(null); setFormData({}); setOriginalData({}); + setGroupData([]); // 🆕 + setOriginalGroupData([]); // 🆕 }; // 저장 버튼 클릭 시 - UPDATE 액션 실행 @@ -222,7 +305,104 @@ export const EditModal: React.FC = ({ className }) => { } try { + // 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정 + if (groupData.length > 0) { + console.log("🔄 그룹 데이터 일괄 수정 시작:", { + groupDataLength: groupData.length, + originalGroupDataLength: originalGroupData.length, + }); + + let updatedCount = 0; + + for (let i = 0; i < groupData.length; i++) { + const currentData = groupData[i]; + const originalItemData = originalGroupData[i]; + + if (!originalItemData) { + console.warn(`원본 데이터가 없습니다 (index: ${i})`); + continue; + } + // 변경된 필드만 추출 + 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; + } + + if (currentData[key] !== originalItemData[key]) { + changedData[key] = currentData[key]; + } + }); + + // 변경사항이 없으면 스킵 + 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}개의 품목이 수정되었습니다.`); + + // 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침) + if (modalState.onSave) { + try { + modalState.onSave(); + } catch (callbackError) { + console.error("⚠️ onSave 콜백 에러:", callbackError); + } + } + + handleClose(); + } else { + toast.info("변경된 내용이 없습니다."); + handleClose(); + } + + return; + } + + // 기존 로직: 단일 레코드 수정 const changedData: Record = {}; Object.keys(formData).forEach((key) => { if (formData[key] !== originalData[key]) { @@ -341,6 +521,13 @@ export const EditModal: React.FC = ({ className }) => { maxHeight: "100%", }} > + {/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */} + {groupData.length > 1 && ( +
+ {groupData.length}개의 관련 품목을 함께 수정합니다 +
+ )} + {screenData.components.map((component) => { // 컴포넌트 위치를 offset만큼 조정 const offsetX = screenDimensions?.offsetX || 0; @@ -355,23 +542,51 @@ export const EditModal: React.FC = ({ className }) => { }, }; + // 🔍 디버깅: 컴포넌트 렌더링 시점의 groupData 확인 + if (component.id === screenData.components[0]?.id) { + console.log("🔍 [EditModal] InteractiveScreenViewerDynamic props:", { + componentId: component.id, + groupDataLength: groupData.length, + groupData: groupData, + formData: groupData.length > 0 ? groupData[0] : formData, + }); + } + return ( 0 ? groupData[0] : formData} onFormDataChange={(fieldName, value) => { + // 🆕 그룹 데이터가 있으면 처리 + if (groupData.length > 0) { + // ModalRepeaterTable의 경우 배열 전체를 받음 + if (Array.isArray(value)) { + setGroupData(value); + } else { + // 일반 필드는 모든 항목에 동일하게 적용 + setGroupData((prev) => + prev.map((item) => ({ + ...item, + [fieldName]: value, + })) + ); + } + } else { setFormData((prev) => ({ ...prev, [fieldName]: value, })); + } }} screenInfo={{ id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} onSave={handleSave} + // 🆕 그룹 데이터를 ModalRepeaterTable에 전달 + groupedData={groupData.length > 0 ? groupData : undefined} /> ); })} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index df134685..d1cd2a5f 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -46,6 +46,8 @@ interface InteractiveScreenViewerProps { userId?: string; userName?: string; companyCode?: string; + // 🆕 그룹 데이터 (EditModal에서 전달) + groupedData?: Record[]; } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -61,6 +63,7 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName: authUserName, user: authUser } = useAuth(); @@ -332,6 +335,8 @@ export const InteractiveScreenViewerDynamic: React.FC { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 3792518e..92fd89e8 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -107,6 +107,8 @@ export interface DynamicComponentRendererProps { onClose?: () => void; // 테이블 선택된 행 정보 (다중 선택 액션용) selectedRows?: any[]; + // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable) + groupedData?: Record[]; selectedRowsData?: any[]; onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void; // 테이블 정렬 정보 (엑셀 다운로드용) @@ -279,7 +281,17 @@ export const DynamicComponentRenderer: React.FC = // modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화 let currentValue; if (componentType === "modal-repeater-table") { - currentValue = formData?.[fieldName] || []; + // 🆕 EditModal에서 전달된 groupedData가 있으면 우선 사용 + currentValue = props.groupedData || formData?.[fieldName] || []; + + // 디버깅 로그 + console.log("🔍 [DynamicComponentRenderer] ModalRepeaterTable value 설정:", { + hasGroupedData: !!props.groupedData, + groupedDataLength: props.groupedData?.length || 0, + fieldName, + formDataValue: formData?.[fieldName], + finalValueLength: Array.isArray(currentValue) ? currentValue.length : 0, + }); } else { currentValue = formData?.[fieldName] || ""; } @@ -380,6 +392,8 @@ export const DynamicComponentRenderer: React.FC = isPreview, // 디자인 모드 플래그 전달 - isPreview와 명확히 구분 isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false, + // 🆕 그룹 데이터 전달 (EditModal → ConditionalContainer → ModalRepeaterTable) + groupedData: props.groupedData, }; // 렌더러가 클래스인지 함수인지 확인 diff --git a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx index d1aff6de..2589026f 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx @@ -40,6 +40,7 @@ export function ConditionalContainerComponent({ componentId, style, className, + groupedData, // 🆕 그룹 데이터 }: ConditionalContainerProps) { console.log("🎯 ConditionalContainerComponent 렌더링!", { isDesignMode, @@ -177,6 +178,7 @@ export function ConditionalContainerComponent({ showBorder={showBorder} formData={formData} onFormDataChange={onFormDataChange} + groupedData={groupedData} /> ))} @@ -196,6 +198,7 @@ export function ConditionalContainerComponent({ showBorder={showBorder} formData={formData} onFormDataChange={onFormDataChange} + groupedData={groupedData} /> ) : null ) diff --git a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx index c660a0a8..f77dbcdb 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react"; import { ConditionalSectionViewerProps } from "./types"; import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic"; +import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { cn } from "@/lib/utils"; import { Loader2 } from "lucide-react"; import { screenApi } from "@/lib/api/screen"; @@ -24,6 +25,7 @@ export function ConditionalSectionViewer({ showBorder = true, formData, onFormDataChange, + groupedData, // 🆕 그룹 데이터 }: ConditionalSectionViewerProps) { const { userId, userName, user } = useAuth(); const [isLoading, setIsLoading] = useState(false); @@ -135,13 +137,24 @@ export function ConditionalSectionViewer({ minHeight: "200px", }} > - {components.map((component) => ( - { + const { position = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component; + + return ( +
+ {}} + isInteractive={true} screenId={screenInfo?.id} tableName={screenInfo?.tableName} userId={userId} @@ -149,8 +162,11 @@ export function ConditionalSectionViewer({ companyCode={user?.companyCode} formData={formData} onFormDataChange={onFormDataChange} + groupedData={groupedData} /> - ))} +
+ ); + })} )} diff --git a/frontend/lib/registry/components/conditional-container/types.ts b/frontend/lib/registry/components/conditional-container/types.ts index 6f1964e9..0cf741b2 100644 --- a/frontend/lib/registry/components/conditional-container/types.ts +++ b/frontend/lib/registry/components/conditional-container/types.ts @@ -45,6 +45,7 @@ export interface ConditionalContainerProps { onChange?: (value: string) => void; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; + groupedData?: Record[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable) // 화면 편집기 관련 isDesignMode?: boolean; // 디자인 모드 여부 @@ -75,5 +76,6 @@ export interface ConditionalSectionViewerProps { // 폼 데이터 전달 formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; + groupedData?: Record[]; // 🆕 그룹 데이터 } diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index d903cc9f..3941a89f 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -291,13 +291,47 @@ export function ModalRepeaterTableComponent({ return; } + // 🔥 sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거) + console.log("🔍 [ModalRepeaterTable] 필터링 전 데이터:", { + sourceColumns, + sourceTable, + targetTable, + sampleItem: value[0], + itemKeys: value[0] ? Object.keys(value[0]) : [], + }); + + const filteredData = value.map((item: any) => { + const filtered: Record = {}; + + Object.keys(item).forEach((key) => { + // sourceColumns에 포함된 컬럼은 제외 (item_info 테이블의 컬럼) + if (sourceColumns.includes(key)) { + console.log(` ⛔ ${key} 제외 (sourceColumn)`); + return; + } + // 메타데이터 필드도 제외 + if (key.startsWith("_")) { + console.log(` ⛔ ${key} 제외 (메타데이터)`); + return; + } + filtered[key] = item[key]; + }); + + return filtered; + }); + + console.log("✅ [ModalRepeaterTable] 필터링 후 데이터:", { + filteredItemKeys: filteredData[0] ? Object.keys(filteredData[0]) : [], + sampleFilteredItem: filteredData[0], + }); + // 🔥 targetTable 메타데이터를 배열 항목에 추가 const dataWithTargetTable = targetTable - ? value.map(item => ({ + ? filteredData.map((item: any) => ({ ...item, _targetTable: targetTable, // 백엔드가 인식할 메타데이터 })) - : value; + : filteredData; // ✅ CustomEvent의 detail에 데이터 추가 if (event instanceof CustomEvent && event.detail) { @@ -333,9 +367,10 @@ export function ModalRepeaterTableComponent({ const calculated = calculateAll(value); // 값이 실제로 변경된 경우만 업데이트 if (JSON.stringify(calculated) !== JSON.stringify(value)) { - onChange(calculated); + handleChange(calculated); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleAddItems = async (items: any[]) => { diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 5f825cdc..3b9b9d9e 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -41,7 +41,8 @@ export interface ButtonActionConfig { // 모달/팝업 관련 modalTitle?: string; - modalTitleBlocks?: Array<{ // 🆕 블록 기반 제목 (우선순위 높음) + modalTitleBlocks?: Array<{ + // 🆕 블록 기반 제목 (우선순위 높음) id: string; type: "text" | "field"; value: string; // type=text: 텍스트 내용, type=field: 컬럼명 @@ -88,6 +89,12 @@ export interface ButtonActionConfig { // 코드 병합 관련 mergeColumnName?: string; // 병합할 컬럼명 (예: "item_code") mergeShowPreview?: boolean; // 병합 전 미리보기 표시 여부 (기본: true) + + // 편집 관련 (수주관리 등 그룹별 다중 레코드 편집) + editMode?: "modal" | "navigate" | "inline"; // 편집 모드 + editModalTitle?: string; // 편집 모달 제목 + editModalDescription?: string; // 편집 모달 설명 + groupByColumns?: string[]; // 같은 그룹의 여러 행을 함께 편집 (예: ["order_no"]) } /** @@ -1256,14 +1263,6 @@ export class ButtonActionExecutor { // 플로우 선택 데이터 우선 사용 let dataToEdit = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; - console.log("🔍 handleEdit - 데이터 소스 확인:", { - hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0), - flowSelectedDataLength: flowSelectedData?.length || 0, - hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0), - selectedRowsDataLength: selectedRowsData?.length || 0, - dataToEditLength: dataToEdit?.length || 0, - }); - // 선택된 데이터가 없는 경우 if (!dataToEdit || dataToEdit.length === 0) { toast.error("수정할 항목을 선택해주세요."); @@ -1276,26 +1275,15 @@ export class ButtonActionExecutor { return false; } - console.log(`📝 편집 액션 실행: ${dataToEdit.length}개 항목`, { - dataToEdit, - targetScreenId: config.targetScreenId, - editMode: config.editMode, - }); - if (dataToEdit.length === 1) { // 단일 항목 편집 const rowData = dataToEdit[0]; - console.log("📝 단일 항목 편집:", rowData); await this.openEditForm(config, rowData, context); } else { // 다중 항목 편집 - 현재는 단일 편집만 지원 toast.error("현재 단일 항목 편집만 지원됩니다. 하나의 항목만 선택해주세요."); return false; - - // TODO: 향후 다중 편집 지원 - // console.log("📝 다중 항목 편집:", selectedRowsData); - // this.openBulkEditForm(config, selectedRowsData, context); } return true; @@ -1329,7 +1317,7 @@ export class ButtonActionExecutor { default: // 기본값: 모달 - this.openEditModal(config, rowData, context); + await this.openEditModal(config, rowData, context); } } @@ -1341,11 +1329,17 @@ export class ButtonActionExecutor { rowData: any, context: ButtonActionContext, ): Promise { - console.log("🎭 편집 모달 열기:", { - targetScreenId: config.targetScreenId, - modalSize: config.modalSize, - rowData, - }); + const { groupByColumns = [] } = config; + + // PK 값 추출 (우선순위: id > ID > 첫 번째 필드) + let primaryKeyValue: any; + if (rowData.id !== undefined && rowData.id !== null) { + primaryKeyValue = rowData.id; + } else if (rowData.ID !== undefined && rowData.ID !== null) { + primaryKeyValue = rowData.ID; + } else { + primaryKeyValue = Object.values(rowData)[0]; + } // 1. config에 editModalDescription이 있으면 우선 사용 let description = config.editModalDescription || ""; @@ -1360,7 +1354,7 @@ export class ButtonActionExecutor { } } - // 모달 열기 이벤트 발생 + // 🔧 항상 EditModal 사용 (groupByColumns는 EditModal에서 처리) const modalEvent = new CustomEvent("openEditModal", { detail: { screenId: config.targetScreenId, @@ -1368,16 +1362,15 @@ export class ButtonActionExecutor { description: description, modalSize: config.modalSize || "lg", editData: rowData, + groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달 + tableName: context.tableName, // 🆕 테이블명 전달 onSave: () => { - // 저장 후 테이블 새로고침 - console.log("💾 편집 저장 완료 - 테이블 새로고침"); context.onRefresh?.(); }, }, }); window.dispatchEvent(modalEvent); - // 편집 모달 열기는 조용히 처리 (토스트 없음) } /**