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) { // 같은 화면 내 컴포넌트로 전달 + 레이어 활성화 이벤트 병행