diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 39f32a73..54315683 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -333,22 +333,72 @@ export const ButtonConfigPanel: React.FC = ({ const loadModalMappingColumns = async () => { // 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지 - // allComponents에서 split-panel-layout 또는 table-list 찾기 let sourceTableName: string | null = null; + console.log("[openModalWithData] 컬럼 로드 시작:", { + allComponentsCount: allComponents.length, + currentTableName, + targetScreenId: config.action?.targetScreenId, + }); + + // 모든 컴포넌트 타입 로그 + allComponents.forEach((comp, idx) => { + const compType = comp.componentType || (comp as any).componentConfig?.type; + console.log(` [${idx}] componentType: ${compType}, tableName: ${(comp as any).componentConfig?.tableName || (comp as any).componentConfig?.leftPanel?.tableName || 'N/A'}`); + }); + for (const comp of allComponents) { const compType = comp.componentType || (comp as any).componentConfig?.type; + const compConfig = (comp as any).componentConfig || {}; + + // 분할 패널 타입들 (다양한 경로에서 테이블명 추출) if (compType === "split-panel-layout" || compType === "screen-split-panel") { - // 분할 패널의 좌측 테이블명 - sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName || - (comp as any).componentConfig?.leftTableName; - break; + sourceTableName = compConfig?.leftPanel?.tableName || + compConfig?.leftTableName || + compConfig?.tableName; + if (sourceTableName) { + console.log(`✅ [openModalWithData] split-panel-layout에서 소스 테이블 감지: ${sourceTableName}`); + break; + } } + + // split-panel-layout2 타입 (새로운 분할 패널) + if (compType === "split-panel-layout2") { + sourceTableName = compConfig?.leftPanel?.tableName || + compConfig?.tableName || + compConfig?.leftTableName; + if (sourceTableName) { + console.log(`✅ [openModalWithData] split-panel-layout2에서 소스 테이블 감지: ${sourceTableName}`); + break; + } + } + + // 테이블 리스트 타입 if (compType === "table-list") { - sourceTableName = (comp as any).componentConfig?.tableName; + sourceTableName = compConfig?.tableName; + if (sourceTableName) { + console.log(`✅ [openModalWithData] table-list에서 소스 테이블 감지: ${sourceTableName}`); + break; + } + } + + // 🆕 모든 컴포넌트에서 tableName 찾기 (폴백) + if (!sourceTableName && compConfig?.tableName) { + sourceTableName = compConfig.tableName; + console.log(`✅ [openModalWithData] ${compType}에서 소스 테이블 감지 (폴백): ${sourceTableName}`); break; } } + + // 여전히 없으면 currentTableName 사용 (화면 레벨 테이블명) + if (!sourceTableName && currentTableName) { + sourceTableName = currentTableName; + console.log(`✅ [openModalWithData] currentTableName에서 소스 테이블 사용: ${sourceTableName}`); + } + + if (!sourceTableName) { + console.warn("[openModalWithData] 소스 테이블을 찾을 수 없습니다."); + } // 소스 테이블 컬럼 로드 if (sourceTableName) { @@ -361,11 +411,11 @@ export const ButtonConfigPanel: React.FC = ({ if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => ({ - name: col.name || col.columnName, - label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, + name: col.name || col.columnName || col.column_name, + label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name, })); setModalSourceColumns(columns); - console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드:`, columns.length); + console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드 완료:`, columns.length); } } } catch (error) { @@ -379,8 +429,12 @@ export const ButtonConfigPanel: React.FC = ({ try { // 타겟 화면 정보 가져오기 const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`); + console.log("[openModalWithData] 타겟 화면 응답:", screenResponse.data); + if (screenResponse.data.success && screenResponse.data.data) { const targetTableName = screenResponse.data.data.tableName; + console.log("[openModalWithData] 타겟 화면 테이블명:", targetTableName); + if (targetTableName) { const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`); if (columnResponse.data.success) { @@ -390,23 +444,27 @@ export const ButtonConfigPanel: React.FC = ({ if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => ({ - name: col.name || col.columnName, - label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, + name: col.name || col.columnName || col.column_name, + label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name, })); setModalTargetColumns(columns); - console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드:`, columns.length); + console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드 완료:`, columns.length); } } + } else { + console.warn("[openModalWithData] 타겟 화면에 테이블명이 없습니다."); } } } catch (error) { console.error("타겟 화면 테이블 컬럼 로드 실패:", error); } + } else { + console.warn("[openModalWithData] 타겟 화면 ID가 없습니다."); } }; loadModalMappingColumns(); - }, [config.action?.type, config.action?.targetScreenId, allComponents]); + }, [config.action?.type, config.action?.targetScreenId, allComponents, currentTableName]); // 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준) useEffect(() => { @@ -1158,11 +1216,12 @@ export const ButtonConfigPanel: React.FC = ({

) : ( -
+
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => ( -
- {/* 소스 필드 선택 (Combobox) */} -
+
+ {/* 소스 필드 선택 (Combobox) - 세로 배치 */} +
+ setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))} @@ -1171,15 +1230,17 @@ export const ButtonConfigPanel: React.FC = ({ - + = ({ value={modalSourceSearch[index] || ""} onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))} /> - + 컬럼을 찾을 수 없습니다 {modalSourceColumns.map((col) => ( @@ -1208,9 +1269,9 @@ export const ButtonConfigPanel: React.FC = ({ mapping.sourceField === col.name ? "opacity-100" : "opacity-0" )} /> - {col.label} + {col.label} {col.label !== col.name && ( - ({col.name}) + ({col.name}) )} ))} @@ -1221,10 +1282,14 @@ export const ButtonConfigPanel: React.FC = ({
- + {/* 화살표 표시 */} +
+ +
- {/* 타겟 필드 선택 (Combobox) */} -
+ {/* 타겟 필드 선택 (Combobox) - 세로 배치 */} +
+ setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))} @@ -1233,15 +1298,17 @@ export const ButtonConfigPanel: React.FC = ({ - + = ({ value={modalTargetSearch[index] || ""} onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))} /> - + 컬럼을 찾을 수 없습니다 {modalTargetColumns.map((col) => ( @@ -1270,9 +1337,9 @@ export const ButtonConfigPanel: React.FC = ({ mapping.targetField === col.name ? "opacity-100" : "opacity-0" )} /> - {col.label} + {col.label} {col.label !== col.name && ( - ({col.name}) + ({col.name}) )} ))} @@ -1284,19 +1351,22 @@ export const ButtonConfigPanel: React.FC = ({
{/* 삭제 버튼 */} - +
+ +
))}
diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 26bbd0c9..160591c6 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -880,6 +880,44 @@ export const ButtonPrimaryComponent: React.FC = ({ return; } + // 모달 액션인데 선택된 데이터가 있으면 경고 메시지 표시하고 중단 + // (신규 등록 모달에서 선택된 데이터가 초기값으로 전달되는 것을 방지) + if (processedConfig.action.type === "modal" && effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) { + toast.warning("신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요."); + return; + } + + // 수정(edit) 액션 검증 + if (processedConfig.action.type === "edit") { + // 선택된 데이터가 없으면 경고 + if (!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) { + toast.warning("수정할 항목을 선택해주세요."); + return; + } + + // groupByColumns 설정이 있으면 해당 컬럼 값이 유일한지 확인 + const groupByColumns = processedConfig.action.groupByColumns; + if (groupByColumns && groupByColumns.length > 0 && effectiveSelectedRowsData.length > 1) { + // 첫 번째 그룹핑 컬럼 기준으로 중복 체크 (예: order_no) + const groupByColumn = groupByColumns[0]; + const uniqueValues = new Set( + effectiveSelectedRowsData.map((row: any) => row[groupByColumn]).filter(Boolean) + ); + + if (uniqueValues.size > 1) { + // 컬럼명을 한글로 변환 (order_no -> 수주번호) + const columnLabels: Record = { + order_no: "수주번호", + shipment_no: "출하번호", + purchase_no: "구매번호", + }; + const columnLabel = columnLabels[groupByColumn] || groupByColumn; + toast.warning(`${columnLabel} 하나만 선택해주세요. (현재 ${uniqueValues.size}개 선택됨)`); + return; + } + } + } + // 🆕 모든 컴포넌트의 설정 수집 (parentDataMapping 등) const componentConfigs: Record = {}; if (allComponents && Array.isArray(allComponents)) { diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index cb21ac5a..394e15c2 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1780,16 +1780,15 @@ export class ButtonActionExecutor { /** * 모달 액션 처리 - * 🔧 modal 액션은 항상 신규 등록(INSERT) 모드로 동작 - * edit 액션만 수정(UPDATE) 모드로 동작해야 함 + * 선택된 데이터가 있으면 함께 전달 (출하계획 등에서 사용) */ private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise { // 모달 열기 로직 - console.log("모달 열기 (신규 등록 모드):", { + console.log("모달 열기:", { title: config.modalTitle, size: config.modalSize, targetScreenId: config.targetScreenId, - // 🔧 selectedRowsData는 modal 액션에서 사용하지 않음 (신규 등록이므로) + selectedRowsData: context.selectedRowsData, }); if (config.targetScreenId) { @@ -1806,11 +1805,10 @@ export class ButtonActionExecutor { } } - // 🔧 modal 액션은 신규 등록이므로 selectedData를 전달하지 않음 - // selectedData가 있으면 ScreenModal에서 originalData로 인식하여 UPDATE 모드로 동작하게 됨 - // edit 액션만 selectedData/editData를 사용하여 UPDATE 모드로 동작 - console.log("📦 [handleModal] 신규 등록 모드 - selectedData 전달하지 않음"); - console.log("📦 [handleModal] 분할 패널 부모 데이터 (초기값으로 사용):", context.splitPanelParentData); + // 선택된 행 데이터 수집 + const selectedData = context.selectedRowsData || []; + console.log("📦 [handleModal] 선택된 데이터:", selectedData); + console.log("📦 [handleModal] 분할 패널 부모 데이터:", context.splitPanelParentData); // 전역 모달 상태 업데이트를 위한 이벤트 발생 const modalEvent = new CustomEvent("openScreenModal", { @@ -1819,11 +1817,10 @@ export class ButtonActionExecutor { title: config.modalTitle || "화면", description: description, size: config.modalSize || "md", - // 🔧 신규 등록이므로 selectedData/selectedIds를 전달하지 않음 - // edit 액션에서만 이 데이터를 사용 - selectedData: [], - selectedIds: [], - // 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 초기값으로 사용) + // 선택된 행 데이터 전달 + selectedData: selectedData, + selectedIds: selectedData.map((row: any) => row.id).filter(Boolean), + // 분할 패널 부모 데이터 전달 (탭 안 모달에서 사용) splitPanelParentData: context.splitPanelParentData || {}, }, }); @@ -2023,11 +2020,18 @@ export class ButtonActionExecutor { }); } + // 🆕 modalDataStore에서 선택된 전체 데이터 가져오기 (RepeatScreenModal에서 사용) + const modalData = dataRegistry[dataSourceId] || []; + 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 파라미터 포함) @@ -2039,6 +2043,9 @@ export class ButtonActionExecutor { size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음) splitPanelParentData: parentData, // 🆕 부모 데이터 전달 (excludeFilter에서 사용) + // 🆕 선택된 데이터 전달 (RepeatScreenModal에서 groupedData로 사용) + selectedData: selectedData, + selectedIds: selectedIds, }, });