From b4a5fb9aa3b896299097ced637bba6589b28f89d Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 16 Mar 2026 16:47:33 +0900 Subject: [PATCH] feat: enhance ButtonConfigPanel and V2ButtonConfigPanel with improved data handling - Updated the ButtonConfigPanel to fetch a larger set of screens by modifying the API call to include a size parameter. - Enhanced the V2ButtonConfigPanel with new state variables and effects for managing data transfer field mappings, including loading available tables and their columns. - Implemented multi-table mapping logic to support complex data transfer actions, improving the flexibility and usability of the component. - Added a dedicated section for field mapping in the UI, allowing users to configure data transfer settings more effectively. These updates aim to enhance the functionality and user experience of the button configuration panels within the ERP system, enabling better data management and transfer capabilities. Made-with: Cursor --- .../config-panels/ButtonConfigPanel-fixed.tsx | 2 +- .../v2/config-panels/V2ButtonConfigPanel.tsx | 675 +++++++++++++++++- frontend/lib/utils/buttonActions.ts | 31 +- 3 files changed, 703 insertions(+), 5 deletions(-) diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx index 51322c3e..30b93cb4 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel-fixed.tsx @@ -102,7 +102,7 @@ export const ButtonConfigPanel: React.FC = ({ component, const fetchScreens = async () => { try { setScreensLoading(true); - const response = await apiClient.get("/screen-management/screens"); + const response = await apiClient.get("/screen-management/screens?size=1000"); if (response.data.success && Array.isArray(response.data.data)) { const screenList = response.data.data.map((screen: any) => ({ diff --git a/frontend/components/v2/config-panels/V2ButtonConfigPanel.tsx b/frontend/components/v2/config-panels/V2ButtonConfigPanel.tsx index 777c705b..a36cd8c9 100644 --- a/frontend/components/v2/config-panels/V2ButtonConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2ButtonConfigPanel.tsx @@ -269,6 +269,13 @@ export const V2ButtonConfigPanel: React.FC = ({ const [modalScreenOpen, setModalScreenOpen] = useState(false); const [modalSearchTerm, setModalSearchTerm] = useState(""); + // 데이터 전달 필드 매핑 관련 + const [availableTables, setAvailableTables] = useState>([]); + const [mappingSourceColumnsMap, setMappingSourceColumnsMap] = useState>>({}); + const [mappingTargetColumns, setMappingTargetColumns] = useState>([]); + const [fieldMappingOpen, setFieldMappingOpen] = useState(false); + const [activeMappingGroupIndex, setActiveMappingGroupIndex] = useState(0); + const showIconSettings = displayMode === "icon" || displayMode === "icon-text"; const currentActionIcons = actionIconMap[actionType] || []; const isNoIconAction = noIconActions.has(actionType); @@ -330,6 +337,76 @@ export const V2ButtonConfigPanel: React.FC = ({ setIconSize(config.icon?.size || "보통"); }, [config.icon?.name, config.icon?.type, config.icon?.size]); + // 테이블 목록 로드 (데이터 전달 액션용) + useEffect(() => { + if (actionType !== "transferData") return; + if (availableTables.length > 0) return; + + const loadTables = async () => { + try { + const response = await apiClient.get("/table-management/tables"); + if (response.data.success && response.data.data) { + const tables = response.data.data.map((t: any) => ({ + name: t.tableName || t.name, + label: t.displayName || t.tableLabel || t.label || t.tableName || t.name, + })); + setAvailableTables(tables); + } + } catch { + setAvailableTables([]); + } + }; + loadTables(); + }, [actionType, availableTables.length]); + + // 테이블 컬럼 로드 헬퍼 + const loadTableColumns = useCallback(async (tableName: string): Promise> => { + try { + const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); + if (response.data.success) { + let columnData = response.data.data; + if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns; + if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data; + if (Array.isArray(columnData)) { + return columnData.map((col: any) => ({ + name: col.name || col.columnName, + label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, + })); + } + } + } catch { /* ignore */ } + return []; + }, []); + + // 멀티 테이블 매핑: 소스/타겟 테이블 컬럼 로드 + useEffect(() => { + if (actionType !== "transferData") return; + + const multiTableMappings: Array<{ sourceTable: string }> = config.action?.dataTransfer?.multiTableMappings || []; + const targetTable = config.action?.dataTransfer?.targetTable; + + const loadAll = async () => { + const sourceTableNames = multiTableMappings.map((m) => m.sourceTable).filter(Boolean); + const newMap: Record> = {}; + for (const tbl of sourceTableNames) { + if (!mappingSourceColumnsMap[tbl]) { + newMap[tbl] = await loadTableColumns(tbl); + } + } + if (Object.keys(newMap).length > 0) { + setMappingSourceColumnsMap((prev) => ({ ...prev, ...newMap })); + } + + if (targetTable) { + const cols = await loadTableColumns(targetTable); + setMappingTargetColumns(cols); + } else { + setMappingTargetColumns([]); + } + }; + loadAll(); + }, [actionType, config.action?.dataTransfer?.multiTableMappings, config.action?.dataTransfer?.targetTable, loadTableColumns]); + // 화면 목록 로드 (모달 액션용) useEffect(() => { if (actionType !== "modal" && actionType !== "navigate") return; @@ -338,7 +415,7 @@ export const V2ButtonConfigPanel: React.FC = ({ const loadScreens = async () => { setScreensLoading(true); try { - const response = await apiClient.get("/screen-management/screens"); + const response = await apiClient.get("/screen-management/screens?size=1000"); if (response.data.success && response.data.data) { const screenList = response.data.data.map((s: any) => ({ id: s.id || s.screenId, @@ -521,6 +598,8 @@ export const V2ButtonConfigPanel: React.FC = ({ modalSearchTerm={modalSearchTerm} setModalSearchTerm={setModalSearchTerm} currentTableName={effectiveTableName} + allComponents={allComponents} + handleUpdateProperty={handleUpdateProperty} /> {/* ─── 아이콘 설정 (접기) ─── */} @@ -657,6 +736,26 @@ export const V2ButtonConfigPanel: React.FC = ({ )} + {/* 데이터 전달 필드 매핑 (transferData 액션 전용) */} + {actionType === "transferData" && ( + <> + + + + )} + {/* 제어 기능 */} {actionType !== "excel_upload" && actionType !== "multi_table_excel_upload" && ( <> @@ -699,6 +798,8 @@ const ActionDetailSection: React.FC<{ modalSearchTerm: string; setModalSearchTerm: (term: string) => void; currentTableName?: string; + allComponents?: ComponentData[]; + handleUpdateProperty?: (path: string, value: any) => void; }> = ({ actionType, config, @@ -711,6 +812,8 @@ const ActionDetailSection: React.FC<{ modalSearchTerm, setModalSearchTerm, currentTableName, + allComponents = [], + handleUpdateProperty, }) => { const action = config.action || {}; @@ -800,7 +903,7 @@ const ActionDetailSection: React.FC<{ - + {screens .filter((s) => + !modalSearchTerm || s.name.toLowerCase().includes(modalSearchTerm.toLowerCase()) || - s.description?.toLowerCase().includes(modalSearchTerm.toLowerCase()) + s.description?.toLowerCase().includes(modalSearchTerm.toLowerCase()) || + String(s.id).includes(modalSearchTerm) ) .map((screen) => ( ); + case "transferData": + return ( +
+
+ + 데이터 전달 설정 +
+ + {/* 소스 컴포넌트 선택 */} +
+ + +
+ + {/* 타겟 타입 */} +
+ + +
+ + {/* 타겟 컴포넌트 선택 */} + {action.dataTransfer?.targetType === "component" && ( +
+ + +
+ )} + + {/* 데이터 전달 모드 */} +
+ + +
+ + {/* 전달 후 초기화 */} +
+
+

전달 후 소스 선택 초기화

+

데이터 전달 후 소스의 선택을 해제해요

+
+ { + const dt = { ...action.dataTransfer, clearAfterTransfer: checked }; + updateActionConfig("dataTransfer", dt); + }} + /> +
+ + {/* 전달 전 확인 */} +
+
+

전달 전 확인 메시지

+

전달 전 확인 다이얼로그를 표시해요

+
+ { + const dt = { ...action.dataTransfer, confirmBeforeTransfer: checked }; + updateActionConfig("dataTransfer", dt); + }} + /> +
+ + {action.dataTransfer?.confirmBeforeTransfer && ( +
+ + { + const dt = { ...action.dataTransfer, confirmMessage: e.target.value }; + updateActionConfig("dataTransfer", dt); + }} + placeholder="선택한 항목을 전달하시겠습니까?" + className="h-7 text-xs" + /> +
+ )} + + {commonMessageSection} +
+ ); + case "event": return (
@@ -1373,6 +1662,386 @@ const IconSettingsSection: React.FC<{ ); }; +// ─── 데이터 전달 필드 매핑 서브 컴포넌트 (고급 설정 내부) ─── +const TransferDataFieldMappingSection: React.FC<{ + config: Record; + onChange: (config: Record) => void; + availableTables: Array<{ name: string; label: string }>; + mappingSourceColumnsMap: Record>; + setMappingSourceColumnsMap: React.Dispatch>>>; + mappingTargetColumns: Array<{ name: string; label: string }>; + fieldMappingOpen: boolean; + setFieldMappingOpen: (open: boolean) => void; + activeMappingGroupIndex: number; + setActiveMappingGroupIndex: (index: number) => void; + loadTableColumns: (tableName: string) => Promise>; +}> = ({ + config, + onChange, + availableTables, + mappingSourceColumnsMap, + setMappingSourceColumnsMap, + mappingTargetColumns, + activeMappingGroupIndex, + setActiveMappingGroupIndex, + loadTableColumns, +}) => { + const [sourcePopoverOpen, setSourcePopoverOpen] = useState>({}); + const [targetPopoverOpen, setTargetPopoverOpen] = useState>({}); + + const dataTransfer = config.action?.dataTransfer || {}; + const multiTableMappings: Array<{ sourceTable: string; mappingRules: Array<{ sourceField: string; targetField: string }> }> = + dataTransfer.multiTableMappings || []; + + const updateDataTransfer = (field: string, value: any) => { + const currentAction = config.action || {}; + const currentDt = currentAction.dataTransfer || {}; + onChange({ + ...config, + action: { + ...currentAction, + dataTransfer: { ...currentDt, [field]: value }, + }, + }); + }; + + const activeGroup = multiTableMappings[activeMappingGroupIndex]; + const activeSourceTable = activeGroup?.sourceTable || ""; + const activeSourceColumns = mappingSourceColumnsMap[activeSourceTable] || []; + const activeRules = activeGroup?.mappingRules || []; + + const updateGroupField = (field: string, value: any) => { + const mappings = [...multiTableMappings]; + mappings[activeMappingGroupIndex] = { ...mappings[activeMappingGroupIndex], [field]: value }; + updateDataTransfer("multiTableMappings", mappings); + }; + + return ( +
+
+

필드 매핑

+

+ 레이어별로 소스 테이블이 다를 때 각각 매핑 규칙을 설정해요 +

+
+ + {/* 타겟 테이블 (공통) */} +
+ + + + + + + + + + 테이블을 찾을 수 없습니다 + + {availableTables.map((table) => ( + updateDataTransfer("targetTable", table.name)} + className="text-xs" + > + +
+ {table.label} + {table.label !== table.name && {table.name}} +
+
+ ))} +
+
+
+
+
+
+ + {/* 소스 테이블 그룹 탭 + 추가 버튼 */} +
+
+ + +
+ + {!dataTransfer.targetTable ? ( +
+

타겟 테이블을 먼저 선택하세요

+
+ ) : multiTableMappings.length === 0 ? ( +
+

소스 테이블을 추가하세요

+
+ ) : ( +
+ {/* 그룹 탭 */} +
+ {multiTableMappings.map((group, gIdx) => ( +
+ + +
+ ))} +
+ + {/* 활성 그룹 편집 */} + {activeGroup && ( +
+ {/* 소스 테이블 선택 */} +
+ + + + + + + + + + 테이블을 찾을 수 없습니다 + + {availableTables.map((table) => ( + { + updateGroupField("sourceTable", table.name); + if (!mappingSourceColumnsMap[table.name]) { + const cols = await loadTableColumns(table.name); + setMappingSourceColumnsMap((prev) => ({ ...prev, [table.name]: cols })); + } + }} + className="text-xs" + > + +
+ {table.label} + {table.label !== table.name && {table.name}} +
+
+ ))} +
+
+
+
+
+
+ + {/* 매핑 규칙 */} +
+
+ + +
+ + {!activeSourceTable ? ( +

소스 테이블을 먼저 선택하세요

+ ) : activeRules.length === 0 ? ( +

매핑 없음 (동일 필드명 자동 매핑)

+ ) : ( +
+ {activeRules.map((rule: any, rIdx: number) => { + const keyS = `${activeMappingGroupIndex}-${rIdx}-s`; + const keyT = `${activeMappingGroupIndex}-${rIdx}-t`; + return ( +
+ {/* 소스 필드 */} + setSourcePopoverOpen((prev) => ({ ...prev, [keyS]: open }))} + > + + + + + + + + 찾을 수 없습니다 + + {activeSourceColumns.map((col) => ( + { + const newRules = [...activeRules]; + newRules[rIdx] = { ...newRules[rIdx], sourceField: col.name }; + updateGroupField("mappingRules", newRules); + setSourcePopoverOpen((prev) => ({ ...prev, [keyS]: false })); + }} + className="text-xs" + > + + {col.label} + {col.label !== col.name && ({col.name})} + + ))} + + + + + + + + + {/* 타겟 필드 */} + setTargetPopoverOpen((prev) => ({ ...prev, [keyT]: open }))} + > + + + + + + + + 찾을 수 없습니다 + + {mappingTargetColumns.map((col) => ( + { + const newRules = [...activeRules]; + newRules[rIdx] = { ...newRules[rIdx], targetField: col.name }; + updateGroupField("mappingRules", newRules); + setTargetPopoverOpen((prev) => ({ ...prev, [keyT]: false })); + }} + className="text-xs" + > + + {col.label} + {col.label !== col.name && ({col.name})} + + ))} + + + + + + + {/* 삭제 */} + +
+ ); + })} +
+ )} +
+
+ )} +
+ )} +
+
+ ); +}; + V2ButtonConfigPanel.displayName = "V2ButtonConfigPanel"; export default V2ButtonConfigPanel; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 1d8a3197..5ea616e2 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -6566,7 +6566,36 @@ export class ButtonActionExecutor { } // dataTransfer 설정이 있는 경우 - const { targetType, targetComponentId, targetScreenId, mappingRules, receiveMode } = dataTransfer; + const { targetType, targetComponentId, targetScreenId, receiveMode } = dataTransfer; + + // multiTableMappings 우선: 소스 테이블에 맞는 매핑 규칙 선택 + let mappingRules = dataTransfer.mappingRules; + const multiTableMappings = (dataTransfer as any).multiTableMappings as Array<{ + sourceTable: string; + mappingRules: Array<{ sourceField: string; targetField: string }>; + }> | undefined; + + if (multiTableMappings && multiTableMappings.length > 0) { + const sourceTableName = context.tableName || (dataTransfer as any).sourceTable; + const matchedGroup = multiTableMappings.find((g) => g.sourceTable === sourceTableName); + if (matchedGroup && matchedGroup.mappingRules?.length > 0) { + mappingRules = matchedGroup.mappingRules; + console.log("📋 [transferData] multiTableMappings 매핑 적용:", { + sourceTable: sourceTableName, + rules: matchedGroup.mappingRules, + }); + } else if (!mappingRules || mappingRules.length === 0) { + // 매칭되는 그룹이 없고 기존 mappingRules도 없으면 첫 번째 그룹 사용 + const fallback = multiTableMappings[0]; + if (fallback?.mappingRules?.length > 0) { + mappingRules = fallback.mappingRules; + console.log("📋 [transferData] multiTableMappings 폴백 매핑 적용:", { + sourceTable: fallback.sourceTable, + rules: fallback.mappingRules, + }); + } + } + } if (targetType === "component" && targetComponentId) { // 같은 화면 내 컴포넌트로 전달 + 레이어 활성화 이벤트 병행