From 8cdb8a30477f323962df11b7a2bd7fd4e78757d7 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 20 Jan 2026 14:01:35 +0900 Subject: [PATCH] =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EC=95=A4=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20Unified=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=B6=94=EA=B0=80:=20ScreenDesigner,=20Ta?= =?UTF-8?q?bsWidget,=20DynamicComponentRenderer=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EC=95=A4=20=EB=93=9C=EB=A1=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=9D=98=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=EC=99=80=20=ED=81=AC=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=ED=95=98=EA=B3=A0,=20Unified=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=A7=A4=ED=95=91=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EA=B2=BD=ED=97=98=EC=9D=84=20=ED=96=A5=EC=83=81?= =?UTF-8?q?=EC=8B=9C=EC=BC=B0=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EB=98=90?= =?UTF-8?q?=ED=95=9C,=20ButtonConfigPanel=EC=97=90=EC=84=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EA=B0=80=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=B0=A9=EC=96=B4=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=95=88=EC=A0=95=EC=84=B1=EC=9D=84=20=EB=86=92?= =?UTF-8?q?=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenDesigner.tsx | 162 ++++++++++++------ .../config-panels/ButtonConfigPanel.tsx | 9 + .../components/screen/widgets/TabsWidget.tsx | 22 ++- .../lib/registry/DynamicComponentRenderer.tsx | 29 +++- .../v2-table-list/TableListConfigPanel.tsx | 5 +- .../v2-tabs-widget/tabs-component.tsx | 146 ++++++++-------- .../lib/utils/getComponentConfigPanel.tsx | 90 +++++++++- 7 files changed, 335 insertions(+), 128 deletions(-) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index ba4a39c2..e8561387 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -2413,10 +2413,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - // 새 컴포넌트 생성 + // 새 컴포넌트 생성 - 드롭된 컴포넌트의 id를 그대로 사용 + // component.id는 ComponentDefinition의 id (예: "v2-table-list", "v2-button-primary") + const componentType = component.id || component.componentType || "v2-text-display"; + + console.log("🎯 탭에 컴포넌트 드롭:", { + componentId: component.id, + componentType: componentType, + componentName: component.name, + defaultConfig: component.defaultConfig, + defaultSize: component.defaultSize, + }); + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - componentType: component.id || component.componentType || "text-display", + componentType: componentType, label: component.name || component.label || "새 컴포넌트", position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, size: component.defaultSize || { width: 200, height: 100 }, @@ -2858,16 +2869,55 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - // 새 컴포넌트 생성 (컬럼 기반) + // 🆕 Unified 컴포넌트 매핑 사용 (일반 캔버스와 동일) + const unifiedMapping = createUnifiedConfigFromColumn({ + widgetType: column.widgetType, + columnName: column.columnName, + columnLabel: column.columnLabel, + codeCategory: column.codeCategory, + inputType: column.inputType, + required: column.required, + detailSettings: column.detailSettings, + referenceTable: column.referenceTable, + referenceColumn: column.referenceColumn, + displayColumn: column.displayColumn, + }); + + // 웹타입별 기본 크기 계산 + const getTabComponentSize = (widgetType: string) => { + const sizeMap: Record = { + text: { width: 200, height: 36 }, + number: { width: 150, height: 36 }, + decimal: { width: 150, height: 36 }, + date: { width: 180, height: 36 }, + datetime: { width: 200, height: 36 }, + select: { width: 200, height: 36 }, + category: { width: 200, height: 36 }, + code: { width: 200, height: 36 }, + entity: { width: 220, height: 36 }, + boolean: { width: 120, height: 36 }, + checkbox: { width: 120, height: 36 }, + textarea: { width: 300, height: 100 }, + file: { width: 250, height: 80 }, + }; + return sizeMap[widgetType] || { width: 200, height: 36 }; + }; + + const componentSize = getTabComponentSize(column.widgetType); + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - componentType: column.widgetType || "unified-input", + componentType: unifiedMapping.componentType, // unified-input, unified-select 등 label: column.columnLabel || column.columnName, position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, - size: { width: 200, height: 60 }, + size: componentSize, + inputType: column.inputType || column.widgetType, // 🆕 inputType 저장 (설정 패널용) + widgetType: column.widgetType, // 🆕 widgetType 저장 componentConfig: { + ...unifiedMapping.componentConfig, // Unified 컴포넌트 기본 설정 columnName: column.columnName, tableName: column.tableName, + inputType: column.inputType || column.widgetType, // 🆕 componentConfig에도 저장 }, }; @@ -4888,6 +4938,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD size: tabComp.size, componentConfig: tabComp.componentConfig || {}, style: tabComp.style || {}, + inputType: tabComp.inputType || tabComp.componentConfig?.inputType, // 🆕 inputType 추가 + widgetType: tabComp.widgetType || tabComp.componentConfig?.widgetType, // 🆕 widgetType 추가 }; return ( @@ -4895,60 +4947,68 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD componentId={tabComp.componentType} component={componentForConfig} config={tabComp.componentConfig || {}} + screenTableName={selectedScreen?.tableName} + tableColumns={tables.length > 0 ? tables[0].columns : []} + menuObjid={selectedScreen?.menuObjid} + currentComponent={componentForConfig} onChange={(newConfig: any) => { - // componentConfig 전체 업데이트 + // componentConfig 전체 업데이트 - 함수형 업데이트로 클로저 문제 해결 const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo; - const tabsComponent = layout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return; + + setLayout((prevLayout) => { + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; - const currentConfig = (tabsComponent as any).componentConfig || {}; - const tabs = currentConfig.tabs || []; + const currentConfig = (tabsComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; - const updatedTabs = tabs.map((tab: any) => { - if (tab.id === tabId) { - return { - ...tab, - components: (tab.components || []).map((comp: any) => - comp.id === componentId - ? { ...comp, componentConfig: newConfig } - : comp - ), - }; - } - return tab; - }); - - const updatedComponent = { - ...tabsComponent, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; - - const newLayout = { - ...layout, - components: layout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; - - setLayout(newLayout); - saveToHistory(newLayout); - - // 선택된 컴포넌트 정보 업데이트 - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo({ - ...selectedTabComponentInfo, - component: updatedComp, + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === tabId) { + return { + ...tab, + components: (tab.components || []).map((comp: any) => + comp.id === componentId + ? { ...comp, componentConfig: newConfig } + : comp + ), + }; + } + return tab; }); - } + + const updatedComponent = { + ...tabsComponent, + componentConfig: { + ...currentConfig, + tabs: updatedTabs, + }, + }; + + const newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedComponent : c + ), + }; + + // 선택된 컴포넌트 정보 업데이트 + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null + ); + } + + return newLayout; + }); }} + screenTableName={selectedScreen?.tableName} + tableColumns={tables.length > 0 ? tables[0].columns : []} tables={tables} allComponents={layout.components} + menuObjid={selectedScreen?.menuObjid} /> ); })()} diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 7aa2d01e..fd35428a 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -49,6 +49,15 @@ export const ButtonConfigPanel: React.FC = ({ currentTableName, // 현재 화면의 테이블명 currentScreenCompanyCode, // 현재 편집 중인 화면의 회사 코드 }) => { + // 🔧 component가 없는 경우 방어 처리 + if (!component) { + return ( +
+ 컴포넌트 정보를 불러올 수 없습니다. +
+ ); + } + // 🔧 component에서 직접 읽기 (useMemo 제거) const config = component.componentConfig || {}; const currentAction = component.componentConfig?.action || {}; diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 5aa1a5b2..72dca523 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -137,8 +137,24 @@ export function TabsWidget({ ); } + // 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보 + const maxBottom = Math.max( + ...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)), + 300 // 최소 높이 + ); + const maxRight = Math.max( + ...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), + 400 // 최소 너비 + ); + return ( -
+
{components.map((comp: TabInlineComponent) => { const isSelected = selectedComponentId === comp.id; @@ -228,7 +244,7 @@ export function TabsWidget({
-
+
{visibleTabs.map((tab) => { const shouldRender = mountedTabs.has(tab.id); const isActive = selectedTab === tab.id; @@ -238,7 +254,7 @@ export function TabsWidget({ key={tab.id} value={tab.id} forceMount - className={cn("h-full", !isActive && "hidden")} + className={cn("h-full overflow-auto", !isActive && "hidden")} > {shouldRender && renderTabComponents(tab)} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 6dd9a839..727462a0 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -187,7 +187,34 @@ export const DynamicComponentRenderer: React.FC = ...props }) => { // 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용 - const componentType = (component as any).componentType || component.type; + const rawComponentType = (component as any).componentType || component.type; + + // 🆕 레거시 타입을 v2 컴포넌트로 매핑 (v2 컴포넌트가 없으면 원본 유지) + const mapToV2ComponentType = (type: string | undefined): string | undefined => { + if (!type) return type; + // 이미 v2- 또는 unified- 접두사가 있으면 그대로 반환 + if (type.startsWith("v2-") || type.startsWith("unified-")) return type; + // 레거시 타입을 v2로 매핑 시도 + const v2Type = `v2-${type}`; + // v2 버전이 등록되어 있는지 확인 + if (ComponentRegistry.hasComponent(v2Type)) { + return v2Type; + } + // v2 버전이 없으면 원본 유지 + return type; + }; + + const componentType = mapToV2ComponentType(rawComponentType); + + // 디버그: 컴포넌트 타입 확인 + if (rawComponentType && rawComponentType.includes("table")) { + console.log("🔍 DynamicComponentRenderer 타입 변환:", { + raw: rawComponentType, + mapped: componentType, + hasComponent: ComponentRegistry.hasComponent(componentType || ""), + componentConfig: (component as any).componentConfig, + }); + } // 🆕 Unified 폼 시스템 연동 (최상위에서 한 번만 호출) // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx index 0f05e042..ff76960e 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx @@ -28,11 +28,14 @@ export interface TableListConfigPanelProps { * 컴포넌트의 설정값들을 편집할 수 있는 UI 제공 */ export const TableListConfigPanel: React.FC = ({ - config, + config: configProp, onChange, screenTableName, tableColumns, }) => { + // config가 undefined인 경우 빈 객체로 초기화 + const config = configProp || {}; + // console.log("🔍 TableListConfigPanel props:", { // config, // configType: typeof config, diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index 16e275cd..6aa29cb9 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -18,8 +18,9 @@ const TabsDesignEditor: React.FC<{ }> = ({ component, tabs, onUpdateComponent, onSelectTabComponent, selectedTabComponentId }) => { const [activeTabId, setActiveTabId] = useState(tabs[0]?.id || ""); const [draggingCompId, setDraggingCompId] = useState(null); - const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null); const containerRef = useRef(null); + const rafRef = useRef(null); const activeTab = tabs.find((t) => t.id === activeTabId); @@ -65,64 +66,54 @@ const TabsDesignEditor: React.FC<{ e.stopPropagation(); e.preventDefault(); - if (!containerRef.current) return; - - const targetElement = (e.currentTarget as HTMLElement); - const targetRect = targetElement.getBoundingClientRect(); - const containerRect = containerRef.current.getBoundingClientRect(); - - // 스크롤 위치 고려 - const scrollLeft = containerRef.current.scrollLeft; - const scrollTop = containerRef.current.scrollTop; - - // 마우스 클릭 위치에서 컴포넌트의 좌상단까지의 오프셋 - const offsetX = e.clientX - targetRect.left; - const offsetY = e.clientY - targetRect.top; - - // 초기 컨테이너 위치 저장 - const initialContainerX = containerRect.left; - const initialContainerY = containerRect.top; + // 드래그 시작 시 마우스 위치와 컴포넌트의 현재 위치 저장 + const startMouseX = e.clientX; + const startMouseY = e.clientY; + const startLeft = comp.position?.x || 0; + const startTop = comp.position?.y || 0; setDraggingCompId(comp.id); - setDragOffset({ x: offsetX, y: offsetY }); + setDragPosition({ x: startLeft, y: startTop }); const handleMouseMove = (moveEvent: MouseEvent) => { - if (!containerRef.current) return; - - // 현재 컨테이너의 위치 가져오기 (스크롤/리사이즈 고려) - const currentContainerRect = containerRef.current.getBoundingClientRect(); - const currentScrollLeft = containerRef.current.scrollLeft; - const currentScrollTop = containerRef.current.scrollTop; - - // 컨테이너 내에서의 위치 계산 (스크롤 포함) - const newX = moveEvent.clientX - currentContainerRect.left - offsetX + currentScrollLeft; - const newY = moveEvent.clientY - currentContainerRect.top - offsetY + currentScrollTop; - - // 실시간 위치 업데이트 (시각적 피드백) - const draggedElement = document.querySelector( - `[data-tab-comp-id="${comp.id}"]` - ) as HTMLElement; - if (draggedElement) { - draggedElement.style.left = `${Math.max(0, newX)}px`; - draggedElement.style.top = `${Math.max(0, newY)}px`; + // requestAnimationFrame으로 성능 최적화 + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); } + + rafRef.current = requestAnimationFrame(() => { + // 마우스 이동량 계산 + const deltaX = moveEvent.clientX - startMouseX; + const deltaY = moveEvent.clientY - startMouseY; + + // 새 위치 = 시작 위치 + 이동량 + const newX = Math.max(0, startLeft + deltaX); + const newY = Math.max(0, startTop + deltaY); + + // React 상태로 위치 업데이트 (리렌더링 트리거) + setDragPosition({ x: newX, y: newY }); + }); }; const handleMouseUp = (upEvent: MouseEvent) => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); - setDraggingCompId(null); - if (!containerRef.current) { - return; + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; } - - const currentContainerRect = containerRef.current.getBoundingClientRect(); - const currentScrollLeft = containerRef.current.scrollLeft; - const currentScrollTop = containerRef.current.scrollTop; - const newX = upEvent.clientX - currentContainerRect.left - offsetX + currentScrollLeft; - const newY = upEvent.clientY - currentContainerRect.top - offsetY + currentScrollTop; + // 마우스 이동량 계산 + const deltaX = upEvent.clientX - startMouseX; + const deltaY = upEvent.clientY - startMouseY; + + // 새 위치 = 시작 위치 + 이동량 + const newX = Math.max(0, startLeft + deltaX); + const newY = Math.max(0, startTop + deltaY); + + setDraggingCompId(null); + setDragPosition(null); // 탭 컴포넌트 위치 업데이트 if (onUpdateComponent) { @@ -198,7 +189,6 @@ const TabsDesignEditor: React.FC<{ {/* 탭 컨텐츠 영역 - 드롭 영역 */}
onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)} > {activeTab ? ( -
+
{activeTab.components && activeTab.components.length > 0 ? ( -
+
{activeTab.components.map((comp: TabInlineComponent) => { const isSelected = selectedTabComponentId === comp.id; const isDragging = draggingCompId === comp.id; @@ -225,22 +218,18 @@ const TabsDesignEditor: React.FC<{ style: comp.style || {}, }; + // 드래그 중인 컴포넌트는 dragPosition 사용, 아니면 저장된 position 사용 + const displayX = isDragging && dragPosition ? dragPosition.x : (comp.position?.x || 0); + const displayY = isDragging && dragPosition ? dragPosition.y : (comp.position?.y || 0); + return (
{ @@ -248,18 +237,22 @@ const TabsDesignEditor: React.FC<{ onSelectTabComponent?.(activeTabId, comp.id, comp); }} > - {/* 드래그 핸들 - 상단 */} + {/* 드래그 핸들 - 컴포넌트 외부 상단 */}
handleDragStart(e, comp)} > -
- - +
+ + {comp.label || comp.componentType}
-
+
- {/* 실제 컴포넌트 렌더링 */} -
+ {/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */} +
Promise> = { + // ========== Unified 컴포넌트 ========== + "unified-input": () => import("@/components/unified/config-panels/UnifiedInputConfigPanel"), + "unified-select": () => import("@/components/unified/config-panels/UnifiedSelectConfigPanel"), + "unified-date": () => import("@/components/unified/config-panels/UnifiedDateConfigPanel"), + "unified-list": () => import("@/components/unified/config-panels/UnifiedListConfigPanel"), + "unified-media": () => import("@/components/unified/config-panels/UnifiedMediaConfigPanel"), + "unified-biz": () => import("@/components/unified/config-panels/UnifiedBizConfigPanel"), + "unified-group": () => import("@/components/unified/config-panels/UnifiedGroupConfigPanel"), + "unified-hierarchy": () => import("@/components/unified/config-panels/UnifiedHierarchyConfigPanel"), + "unified-layout": () => import("@/components/unified/config-panels/UnifiedLayoutConfigPanel"), + "unified-repeater": () => import("@/components/unified/config-panels/UnifiedRepeaterConfigPanel"), + // ========== 기본 입력 컴포넌트 ========== "text-input": () => import("@/lib/registry/components/text-input/TextInputConfigPanel"), "number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"), @@ -116,21 +128,37 @@ export async function getComponentConfigPanel(componentId: string): Promise TextInputConfigPanel) - // 2차: 특수 export명들 fallback - // 3차: default export + // 2차: v2- 접두사 제거 후 PascalCase 이름으로 찾기 (예: v2-table-list -> TableListConfigPanel) + // 3차: 특수 export명들 fallback + // 4차: default export const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`; + // v2- 접두사가 있는 경우 접두사를 제거한 이름도 시도 + const baseComponentId = componentId.startsWith("v2-") ? componentId.slice(3) : componentId; + const basePascalCaseName = `${toPascalCase(baseComponentId)}ConfigPanel`; + const ConfigPanelComponent = module[pascalCaseName] || + module[basePascalCaseName] || // 특수 export명들 module.RepeaterConfigPanel || module.FlowWidgetConfigPanel || module.CustomerItemMappingConfigPanel || module.SelectedItemsDetailInputConfigPanel || module.ButtonConfigPanel || + module.TableListConfigPanel || module.SectionCardConfigPanel || module.SectionPaperConfigPanel || module.TabsConfigPanel || module.UnifiedRepeaterConfigPanel || + module.UnifiedInputConfigPanel || + module.UnifiedSelectConfigPanel || + module.UnifiedDateConfigPanel || + module.UnifiedListConfigPanel || + module.UnifiedMediaConfigPanel || + module.UnifiedBizConfigPanel || + module.UnifiedGroupConfigPanel || + module.UnifiedHierarchyConfigPanel || + module.UnifiedLayoutConfigPanel || module.RepeatContainerConfigPanel || module.ScreenSplitPanelConfigPanel || module.SimpleRepeaterTableConfigPanel || @@ -491,6 +519,20 @@ export const DynamicComponentConfigPanel: React.FC = return ; } + // 🆕 Unified 컴포넌트들은 전용 props 사용 + if (componentId.startsWith("unified-")) { + return ( + + ); + } + // entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용) // 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용) if (componentId === "entity-search-input") { @@ -520,6 +562,50 @@ export const DynamicComponentConfigPanel: React.FC = ); } + // 🆕 ButtonConfigPanel은 component와 onUpdateProperty를 사용 + if (componentId === "button-primary" || componentId === "v2-button-primary") { + // currentComponent가 있으면 그것을 사용, 없으면 config에서 component 구조 생성 + const componentForButton = currentComponent || { + id: "temp", + type: "component", + componentType: componentId, + componentConfig: config, + }; + + return ( + { + // path가 componentConfig로 시작하면 내부 경로 추출 + if (path.startsWith("componentConfig.")) { + const configPath = path.replace("componentConfig.", ""); + const pathParts = configPath.split("."); + + // 중첩된 경로 처리 - 현재 config를 기반으로 새 config 생성 + const currentConfig = componentForButton.componentConfig || {}; + const newConfig = JSON.parse(JSON.stringify(currentConfig)); // deep clone + let current: any = newConfig; + for (let i = 0; i < pathParts.length - 1; i++) { + if (!current[pathParts[i]]) { + current[pathParts[i]] = {}; + } + current = current[pathParts[i]]; + } + current[pathParts[pathParts.length - 1]] = value; + + onChange(newConfig); + } else { + // 직접 config 속성 변경 + const currentConfig = componentForButton.componentConfig || {}; + onChange({ ...currentConfig, [path]: value }); + } + }} + allComponents={allComponents} + currentTableName={screenTableName} + /> + ); + } + return (