diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 7af50458..58f7124c 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -434,6 +434,7 @@ export const ButtonConfigPanel: React.FC = ({ 편집 복사 (품목코드 초기화) 페이지 이동 + 📦 데이터 전달 데이터 전달 + 모달 열기 🆕 모달 열기 제어 흐름 @@ -1601,6 +1602,166 @@ export const ButtonConfigPanel: React.FC = ({ )} + {/* 데이터 전달 액션 설정 */} + {(component.componentConfig?.action?.type || "save") === "transferData" && ( +
+

📦 데이터 전달 설정

+ +
+ + onUpdateProperty("componentConfig.action.dataTransfer.sourceComponentId", e.target.value)} + className="h-8 text-xs" + /> +

+ 데이터를 가져올 컴포넌트의 ID (테이블 등) +

+
+ +
+ + +
+ + {config.action?.dataTransfer?.targetType !== "screen" && ( +
+ + onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", e.target.value)} + className="h-8 text-xs" + /> +

+ 데이터를 받을 컴포넌트의 ID +

+
+ )} + +
+ + +

+ 기존 데이터를 어떻게 처리할지 선택 +

+
+ +
+
+ +

데이터 전달 후 소스의 선택을 해제합니다

+
+ onUpdateProperty("componentConfig.action.dataTransfer.clearAfterTransfer", checked)} + /> +
+ +
+
+ +

데이터 전달 전 확인 다이얼로그를 표시합니다

+
+ onUpdateProperty("componentConfig.action.dataTransfer.confirmBeforeTransfer", checked)} + /> +
+ + {config.action?.dataTransfer?.confirmBeforeTransfer && ( +
+ + onUpdateProperty("componentConfig.action.dataTransfer.confirmMessage", e.target.value)} + className="h-8 text-xs" + /> +
+ )} + +
+ +
+
+ + onUpdateProperty("componentConfig.action.dataTransfer.validation.minSelection", parseInt(e.target.value) || 0)} + className="h-8 w-20 text-xs" + /> +
+
+ + onUpdateProperty("componentConfig.action.dataTransfer.validation.maxSelection", parseInt(e.target.value) || undefined)} + className="h-8 w-20 text-xs" + /> +
+
+
+ +
+

+ 사용 방법: +
+ 1. 소스 컴포넌트에서 데이터를 선택합니다 +
+ 2. 이 버튼을 클릭하면 선택된 데이터가 타겟으로 전달됩니다 +
+ 3. 매핑 규칙은 추후 고급 설정에서 추가 예정입니다 +

+
+
+ )} + {/* 제어 기능 섹션 */}
diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index d2b69074..8cfec543 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -23,6 +23,8 @@ import { toast } from "sonner"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { useCurrentFlowStep } from "@/stores/flowStepStore"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; +import { applyMappingRules } from "@/lib/utils/dataMapping"; export interface ButtonPrimaryComponentProps extends ComponentRendererProps { config?: ButtonPrimaryConfig; @@ -97,6 +99,7 @@ export const ButtonPrimaryComponent: React.FC = ({ ...props }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 + const screenContext = useScreenContextOptional(); // 화면 컨텍스트 // 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출) const propsOnSave = (props as any).onSave as (() => Promise) | undefined; @@ -374,6 +377,106 @@ export const ButtonPrimaryComponent: React.FC = ({ }; // 이벤트 핸들러 + /** + * transferData 액션 처리 + */ + const handleTransferDataAction = async (actionConfig: any) => { + const dataTransferConfig = actionConfig.dataTransfer; + + if (!dataTransferConfig) { + toast.error("데이터 전달 설정이 없습니다."); + return; + } + + if (!screenContext) { + toast.error("화면 컨텍스트를 찾을 수 없습니다."); + return; + } + + try { + // 1. 소스 컴포넌트에서 데이터 가져오기 + const sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId); + + if (!sourceProvider) { + toast.error(`소스 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.sourceComponentId}`); + return; + } + + const sourceData = sourceProvider.getSelectedData(); + + if (!sourceData || sourceData.length === 0) { + toast.warning("선택된 데이터가 없습니다."); + return; + } + + // 2. 검증 + const validation = dataTransferConfig.validation; + if (validation) { + if (validation.minSelection && sourceData.length < validation.minSelection) { + toast.error(`최소 ${validation.minSelection}개 이상 선택해야 합니다.`); + return; + } + if (validation.maxSelection && sourceData.length > validation.maxSelection) { + toast.error(`최대 ${validation.maxSelection}개까지 선택할 수 있습니다.`); + return; + } + } + + // 3. 확인 메시지 + if (dataTransferConfig.confirmBeforeTransfer) { + const confirmMessage = dataTransferConfig.confirmMessage || `${sourceData.length}개 항목을 전달하시겠습니까?`; + if (!window.confirm(confirmMessage)) { + return; + } + } + + // 4. 매핑 규칙 적용 + const mappedData = sourceData.map((row) => { + return applyMappingRules(row, dataTransferConfig.mappingRules || []); + }); + + console.log("📦 데이터 전달:", { + sourceData, + mappedData, + targetType: dataTransferConfig.targetType, + targetComponentId: dataTransferConfig.targetComponentId, + targetScreenId: dataTransferConfig.targetScreenId, + }); + + // 5. 타겟으로 데이터 전달 + if (dataTransferConfig.targetType === "component") { + // 같은 화면의 컴포넌트로 전달 + const targetReceiver = screenContext.getDataReceiver(dataTransferConfig.targetComponentId); + + if (!targetReceiver) { + toast.error(`타겟 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.targetComponentId}`); + return; + } + + await targetReceiver.receiveData(mappedData, { + targetComponentId: dataTransferConfig.targetComponentId, + targetComponentType: targetReceiver.componentType, + mode: dataTransferConfig.mode || "append", + mappingRules: dataTransferConfig.mappingRules || [], + }); + } else if (dataTransferConfig.targetType === "screen") { + // 다른 화면으로 전달 (구현 예정) + toast.info("다른 화면으로의 데이터 전달은 추후 구현 예정입니다."); + } + + toast.success(`${sourceData.length}개 항목이 전달되었습니다.`); + + // 6. 전달 후 정리 + if (dataTransferConfig.clearAfterTransfer) { + sourceProvider.clearSelection(); + } + + } catch (error: any) { + console.error("❌ 데이터 전달 실패:", error); + toast.error(error.message || "데이터 전달 중 오류가 발생했습니다."); + } + }; + const handleClick = async (e: React.MouseEvent) => { e.stopPropagation(); @@ -390,6 +493,12 @@ export const ButtonPrimaryComponent: React.FC = ({ // 인터랙티브 모드에서 액션 실행 if (isInteractive && processedConfig.action) { + // transferData 액션 처리 (화면 컨텍스트 필요) + if (processedConfig.action.type === "transferData") { + await handleTransferDataAction(processedConfig.action); + return; + } + // 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단 const hasDataToDelete = (selectedRowsData && selectedRowsData.length > 0) || (flowSelectedData && flowSelectedData.length > 0); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 76556ecb..1dc8f127 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -48,6 +48,8 @@ import { TableOptionsModal } from "@/components/common/TableOptionsModal"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; +import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; // ======================================== // 인터페이스 @@ -251,6 +253,9 @@ export const TableListComponent: React.FC = ({ const { userId: authUserId } = useAuth(); const currentUserId = userId || authUserId; + // 화면 컨텍스트 (데이터 제공자로 등록) + const screenContext = useScreenContextOptional(); + // TableOptions Context const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions(); const [filters, setFilters] = useState([]); @@ -359,6 +364,107 @@ export const TableListComponent: React.FC = ({ const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table"); const [frozenColumns, setFrozenColumns] = useState([]); + // DataProvidable 인터페이스 구현 + const dataProvider: DataProvidable = { + componentId: component.id, + componentType: "table-list", + + getSelectedData: () => { + // 선택된 행의 실제 데이터 반환 + const selectedData = data.filter((row) => { + const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || ""); + return selectedRows.has(rowId); + }); + return selectedData; + }, + + getAllData: () => { + return data; + }, + + clearSelection: () => { + setSelectedRows(new Set()); + setIsAllSelected(false); + }, + }; + + // DataReceivable 인터페이스 구현 + const dataReceiver: DataReceivable = { + componentId: component.id, + componentType: "table", + + receiveData: async (receivedData: any[], config: DataReceiverConfig) => { + console.log("📥 TableList 데이터 수신:", { + componentId: component.id, + receivedDataCount: receivedData.length, + mode: config.mode, + currentDataCount: data.length, + }); + + try { + let newData: any[] = []; + + switch (config.mode) { + case "append": + // 기존 데이터에 추가 + newData = [...data, ...receivedData]; + console.log("✅ Append 모드: 기존 데이터에 추가", { newDataCount: newData.length }); + break; + + case "replace": + // 기존 데이터를 완전히 교체 + newData = receivedData; + console.log("✅ Replace 모드: 데이터 교체", { newDataCount: newData.length }); + break; + + case "merge": + // 기존 데이터와 병합 (ID 기반) + const existingMap = new Map(data.map(item => [item.id, item])); + receivedData.forEach(item => { + if (item.id && existingMap.has(item.id)) { + // 기존 데이터 업데이트 + existingMap.set(item.id, { ...existingMap.get(item.id), ...item }); + } else { + // 새 데이터 추가 + existingMap.set(item.id || Date.now() + Math.random(), item); + } + }); + newData = Array.from(existingMap.values()); + console.log("✅ Merge 모드: 데이터 병합", { newDataCount: newData.length }); + break; + } + + // 상태 업데이트 + setData(newData); + + // 총 아이템 수 업데이트 + setTotalItems(newData.length); + + console.log("✅ 데이터 수신 완료:", { finalDataCount: newData.length }); + } catch (error) { + console.error("❌ 데이터 수신 실패:", error); + throw error; + } + }, + + getData: () => { + return data; + }, + }; + + // 화면 컨텍스트에 데이터 제공자/수신자로 등록 + useEffect(() => { + if (screenContext && component.id) { + screenContext.registerDataProvider(component.id, dataProvider); + screenContext.registerDataReceiver(component.id, dataReceiver); + + return () => { + screenContext.unregisterDataProvider(component.id); + screenContext.unregisterDataReceiver(component.id); + }; + } + }, [screenContext, component.id, data, selectedRows]); + // 테이블 등록 (Context에 등록) const tableId = `table-list-${component.id}`; diff --git a/frontend/lib/utils/improvedButtonActionExecutor.ts b/frontend/lib/utils/improvedButtonActionExecutor.ts index ddad52d5..6f6d5798 100644 --- a/frontend/lib/utils/improvedButtonActionExecutor.ts +++ b/frontend/lib/utils/improvedButtonActionExecutor.ts @@ -864,11 +864,14 @@ export class ImprovedButtonActionExecutor { context: ButtonExecutionContext, ): Promise { try { - // 기존 ButtonActionExecutor 로직을 여기서 호출하거나 - // 간단한 액션들을 직접 구현 const startTime = performance.now(); - // 임시 구현 - 실제로는 기존 ButtonActionExecutor를 호출해야 함 + // transferData 액션 처리 + if (buttonConfig.actionType === "transferData") { + return await this.executeTransferDataAction(buttonConfig, formData, context); + } + + // 기존 액션들 (임시 구현) const result = { success: true, message: `${buttonConfig.actionType} 액션 실행 완료`, @@ -889,6 +892,43 @@ export class ImprovedButtonActionExecutor { } } + /** + * 데이터 전달 액션 실행 + */ + private static async executeTransferDataAction( + buttonConfig: ExtendedButtonTypeConfig, + formData: Record, + context: ButtonExecutionContext, + ): Promise { + const startTime = performance.now(); + + try { + const dataTransferConfig = buttonConfig.dataTransfer; + + if (!dataTransferConfig) { + throw new Error("데이터 전달 설정이 없습니다."); + } + + console.log("📦 데이터 전달 시작:", dataTransferConfig); + + // 1. 화면 컨텍스트에서 소스 컴포넌트 찾기 + const { ScreenContextProvider } = await import("@/contexts/ScreenContext"); + // 실제로는 현재 화면의 컨텍스트를 사용해야 하지만, 여기서는 전역적으로 접근할 수 없음 + // 대신 context에 screenContext를 전달하도록 수정 필요 + + throw new Error("데이터 전달 기능은 버튼 컴포넌트에서 직접 구현되어야 합니다."); + + } catch (error) { + console.error("❌ 데이터 전달 실패:", error); + return { + success: false, + message: `데이터 전달 실패: ${error.message}`, + executionTime: performance.now() - startTime, + error: error.message, + }; + } + } + /** * 🔥 실행 오류 처리 및 롤백 */