드래그 앤 드롭 기능 개선 및 Unified 컴포넌트 매핑 추가: ScreenDesigner, TabsWidget, DynamicComponentRenderer에서 드래그 앤 드롭 시 컴포넌트의 위치와 크기를 최적화하고, Unified 컴포넌트에 대한 매핑 로직을 추가하여 사용자 경험을 향상시켰습니다. 또한, ButtonConfigPanel에서 컴포넌트가 없는 경우 방어 처리 로직을 추가하여 안정성을 높였습니다.
This commit is contained in:
parent
58d658e638
commit
8cdb8a3047
|
|
@ -2413,10 +2413,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const dropX = (e.clientX - tabContentRect.left) / zoomLevel;
|
const dropX = (e.clientX - tabContentRect.left) / zoomLevel;
|
||||||
const dropY = (e.clientY - tabContentRect.top) / 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 = {
|
const newTabComponent = {
|
||||||
id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
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 || "새 컴포넌트",
|
label: component.name || component.label || "새 컴포넌트",
|
||||||
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
||||||
size: component.defaultSize || { width: 200, height: 100 },
|
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 dropX = (e.clientX - tabContentRect.left) / zoomLevel;
|
||||||
const dropY = (e.clientY - tabContentRect.top) / 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<string, { width: number; height: number }> = {
|
||||||
|
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 = {
|
const newTabComponent = {
|
||||||
id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
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,
|
label: column.columnLabel || column.columnName,
|
||||||
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
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: {
|
componentConfig: {
|
||||||
|
...unifiedMapping.componentConfig, // Unified 컴포넌트 기본 설정
|
||||||
columnName: column.columnName,
|
columnName: column.columnName,
|
||||||
tableName: column.tableName,
|
tableName: column.tableName,
|
||||||
|
inputType: column.inputType || column.widgetType, // 🆕 componentConfig에도 저장
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -4888,6 +4938,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
size: tabComp.size,
|
size: tabComp.size,
|
||||||
componentConfig: tabComp.componentConfig || {},
|
componentConfig: tabComp.componentConfig || {},
|
||||||
style: tabComp.style || {},
|
style: tabComp.style || {},
|
||||||
|
inputType: tabComp.inputType || tabComp.componentConfig?.inputType, // 🆕 inputType 추가
|
||||||
|
widgetType: tabComp.widgetType || tabComp.componentConfig?.widgetType, // 🆕 widgetType 추가
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -4895,60 +4947,68 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
componentId={tabComp.componentType}
|
componentId={tabComp.componentType}
|
||||||
component={componentForConfig}
|
component={componentForConfig}
|
||||||
config={tabComp.componentConfig || {}}
|
config={tabComp.componentConfig || {}}
|
||||||
|
screenTableName={selectedScreen?.tableName}
|
||||||
|
tableColumns={tables.length > 0 ? tables[0].columns : []}
|
||||||
|
menuObjid={selectedScreen?.menuObjid}
|
||||||
|
currentComponent={componentForConfig}
|
||||||
onChange={(newConfig: any) => {
|
onChange={(newConfig: any) => {
|
||||||
// componentConfig 전체 업데이트
|
// componentConfig 전체 업데이트 - 함수형 업데이트로 클로저 문제 해결
|
||||||
const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo;
|
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 currentConfig = (tabsComponent as any).componentConfig || {};
|
||||||
const tabs = currentConfig.tabs || [];
|
const tabs = currentConfig.tabs || [];
|
||||||
|
|
||||||
const updatedTabs = tabs.map((tab: any) => {
|
const updatedTabs = tabs.map((tab: any) => {
|
||||||
if (tab.id === tabId) {
|
if (tab.id === tabId) {
|
||||||
return {
|
return {
|
||||||
...tab,
|
...tab,
|
||||||
components: (tab.components || []).map((comp: any) =>
|
components: (tab.components || []).map((comp: any) =>
|
||||||
comp.id === componentId
|
comp.id === componentId
|
||||||
? { ...comp, componentConfig: newConfig }
|
? { ...comp, componentConfig: newConfig }
|
||||||
: comp
|
: comp
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return tab;
|
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 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}
|
tables={tables}
|
||||||
allComponents={layout.components}
|
allComponents={layout.components}
|
||||||
|
menuObjid={selectedScreen?.menuObjid}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,15 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
currentTableName, // 현재 화면의 테이블명
|
currentTableName, // 현재 화면의 테이블명
|
||||||
currentScreenCompanyCode, // 현재 편집 중인 화면의 회사 코드
|
currentScreenCompanyCode, // 현재 편집 중인 화면의 회사 코드
|
||||||
}) => {
|
}) => {
|
||||||
|
// 🔧 component가 없는 경우 방어 처리
|
||||||
|
if (!component) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-sm text-muted-foreground">
|
||||||
|
컴포넌트 정보를 불러올 수 없습니다.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 🔧 component에서 직접 읽기 (useMemo 제거)
|
// 🔧 component에서 직접 읽기 (useMemo 제거)
|
||||||
const config = component.componentConfig || {};
|
const config = component.componentConfig || {};
|
||||||
const currentAction = component.componentConfig?.action || {};
|
const currentAction = component.componentConfig?.action || {};
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{
|
||||||
|
minHeight: maxBottom + 20,
|
||||||
|
minWidth: maxRight + 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{components.map((comp: TabInlineComponent) => {
|
{components.map((comp: TabInlineComponent) => {
|
||||||
const isSelected = selectedComponentId === comp.id;
|
const isSelected = selectedComponentId === comp.id;
|
||||||
|
|
||||||
|
|
@ -228,7 +244,7 @@ export function TabsWidget({
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex-1 overflow-hidden">
|
<div className="relative flex-1 overflow-auto">
|
||||||
{visibleTabs.map((tab) => {
|
{visibleTabs.map((tab) => {
|
||||||
const shouldRender = mountedTabs.has(tab.id);
|
const shouldRender = mountedTabs.has(tab.id);
|
||||||
const isActive = selectedTab === tab.id;
|
const isActive = selectedTab === tab.id;
|
||||||
|
|
@ -238,7 +254,7 @@ export function TabsWidget({
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
value={tab.id}
|
value={tab.id}
|
||||||
forceMount
|
forceMount
|
||||||
className={cn("h-full", !isActive && "hidden")}
|
className={cn("h-full overflow-auto", !isActive && "hidden")}
|
||||||
>
|
>
|
||||||
{shouldRender && renderTabComponents(tab)}
|
{shouldRender && renderTabComponents(tab)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,34 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용
|
// 컴포넌트 타입 추출 - 새 시스템에서는 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 폼 시스템 연동 (최상위에서 한 번만 호출)
|
// 🆕 Unified 폼 시스템 연동 (최상위에서 한 번만 호출)
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,14 @@ export interface TableListConfigPanelProps {
|
||||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||||
*/
|
*/
|
||||||
export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
config,
|
config: configProp,
|
||||||
onChange,
|
onChange,
|
||||||
screenTableName,
|
screenTableName,
|
||||||
tableColumns,
|
tableColumns,
|
||||||
}) => {
|
}) => {
|
||||||
|
// config가 undefined인 경우 빈 객체로 초기화
|
||||||
|
const config = configProp || {};
|
||||||
|
|
||||||
// console.log("🔍 TableListConfigPanel props:", {
|
// console.log("🔍 TableListConfigPanel props:", {
|
||||||
// config,
|
// config,
|
||||||
// configType: typeof config,
|
// configType: typeof config,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ const TabsDesignEditor: React.FC<{
|
||||||
}> = ({ component, tabs, onUpdateComponent, onSelectTabComponent, selectedTabComponentId }) => {
|
}> = ({ component, tabs, onUpdateComponent, onSelectTabComponent, selectedTabComponentId }) => {
|
||||||
const [activeTabId, setActiveTabId] = useState<string>(tabs[0]?.id || "");
|
const [activeTabId, setActiveTabId] = useState<string>(tabs[0]?.id || "");
|
||||||
const [draggingCompId, setDraggingCompId] = useState<string | null>(null);
|
const [draggingCompId, setDraggingCompId] = useState<string | null>(null);
|
||||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const rafRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
const activeTab = tabs.find((t) => t.id === activeTabId);
|
||||||
|
|
||||||
|
|
@ -65,64 +66,54 @@ const TabsDesignEditor: React.FC<{
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!containerRef.current) return;
|
// 드래그 시작 시 마우스 위치와 컴포넌트의 현재 위치 저장
|
||||||
|
const startMouseX = e.clientX;
|
||||||
const targetElement = (e.currentTarget as HTMLElement);
|
const startMouseY = e.clientY;
|
||||||
const targetRect = targetElement.getBoundingClientRect();
|
const startLeft = comp.position?.x || 0;
|
||||||
const containerRect = containerRef.current.getBoundingClientRect();
|
const startTop = comp.position?.y || 0;
|
||||||
|
|
||||||
// 스크롤 위치 고려
|
|
||||||
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;
|
|
||||||
|
|
||||||
setDraggingCompId(comp.id);
|
setDraggingCompId(comp.id);
|
||||||
setDragOffset({ x: offsetX, y: offsetY });
|
setDragPosition({ x: startLeft, y: startTop });
|
||||||
|
|
||||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
if (!containerRef.current) return;
|
// requestAnimationFrame으로 성능 최적화
|
||||||
|
if (rafRef.current) {
|
||||||
// 현재 컨테이너의 위치 가져오기 (스크롤/리사이즈 고려)
|
cancelAnimationFrame(rafRef.current);
|
||||||
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`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
const handleMouseUp = (upEvent: MouseEvent) => {
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
setDraggingCompId(null);
|
|
||||||
|
|
||||||
if (!containerRef.current) {
|
if (rafRef.current) {
|
||||||
return;
|
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) {
|
if (onUpdateComponent) {
|
||||||
|
|
@ -198,7 +189,6 @@ const TabsDesignEditor: React.FC<{
|
||||||
|
|
||||||
{/* 탭 컨텐츠 영역 - 드롭 영역 */}
|
{/* 탭 컨텐츠 영역 - 드롭 영역 */}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
|
||||||
className="relative flex-1 overflow-hidden"
|
className="relative flex-1 overflow-hidden"
|
||||||
data-tabs-container="true"
|
data-tabs-container="true"
|
||||||
data-component-id={component.id}
|
data-component-id={component.id}
|
||||||
|
|
@ -206,9 +196,12 @@ const TabsDesignEditor: React.FC<{
|
||||||
onClick={() => onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)}
|
onClick={() => onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)}
|
||||||
>
|
>
|
||||||
{activeTab ? (
|
{activeTab ? (
|
||||||
<div className="absolute inset-0 overflow-auto p-2">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="absolute inset-0 overflow-auto p-2"
|
||||||
|
>
|
||||||
{activeTab.components && activeTab.components.length > 0 ? (
|
{activeTab.components && activeTab.components.length > 0 ? (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||||
{activeTab.components.map((comp: TabInlineComponent) => {
|
{activeTab.components.map((comp: TabInlineComponent) => {
|
||||||
const isSelected = selectedTabComponentId === comp.id;
|
const isSelected = selectedTabComponentId === comp.id;
|
||||||
const isDragging = draggingCompId === comp.id;
|
const isDragging = draggingCompId === comp.id;
|
||||||
|
|
@ -225,22 +218,18 @@ const TabsDesignEditor: React.FC<{
|
||||||
style: comp.style || {},
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={comp.id}
|
key={comp.id}
|
||||||
data-tab-comp-id={comp.id}
|
data-tab-comp-id={comp.id}
|
||||||
className={cn(
|
className="absolute"
|
||||||
"absolute rounded border bg-white shadow-sm transition-all",
|
|
||||||
isSelected
|
|
||||||
? "border-primary ring-2 ring-primary/30"
|
|
||||||
: "border-gray-200 hover:border-primary/50",
|
|
||||||
isDragging && "opacity-80 shadow-lg"
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
left: comp.position?.x || 0,
|
left: displayX,
|
||||||
top: comp.position?.y || 0,
|
top: displayY,
|
||||||
width: comp.size?.width || 200,
|
|
||||||
height: comp.size?.height || 100,
|
|
||||||
zIndex: isDragging ? 100 : isSelected ? 10 : 1,
|
zIndex: isDragging ? 100 : isSelected ? 10 : 1,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -248,18 +237,22 @@ const TabsDesignEditor: React.FC<{
|
||||||
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
onSelectTabComponent?.(activeTabId, comp.id, comp);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 드래그 핸들 - 상단 */}
|
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 top-0 z-10 flex h-5 cursor-move items-center justify-between bg-gray-100/80 px-1"
|
className={cn(
|
||||||
|
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
|
||||||
|
isSelected ? "border-primary" : "border-gray-200"
|
||||||
|
)}
|
||||||
|
style={{ width: comp.size?.width || 200 }}
|
||||||
onMouseDown={(e) => handleDragStart(e, comp)}
|
onMouseDown={(e) => handleDragStart(e, comp)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-0.5">
|
||||||
<Move className="h-3 w-3 text-gray-400" />
|
<Move className="h-2.5 w-2.5 text-gray-400" />
|
||||||
<span className="text-[10px] text-gray-500 truncate max-w-[120px]">
|
<span className="text-[9px] text-gray-500 truncate max-w-[100px]">
|
||||||
{comp.label || comp.componentType}
|
{comp.label || comp.componentType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className="rounded p-0.5 hover:bg-gray-200"
|
className="rounded p-0.5 hover:bg-gray-200"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -268,7 +261,7 @@ const TabsDesignEditor: React.FC<{
|
||||||
}}
|
}}
|
||||||
title="설정"
|
title="설정"
|
||||||
>
|
>
|
||||||
<Settings className="h-3 w-3 text-gray-500" />
|
<Settings className="h-2.5 w-2.5 text-gray-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="rounded p-0.5 hover:bg-red-100"
|
className="rounded p-0.5 hover:bg-red-100"
|
||||||
|
|
@ -278,13 +271,26 @@ const TabsDesignEditor: React.FC<{
|
||||||
}}
|
}}
|
||||||
title="삭제"
|
title="삭제"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3 text-red-500" />
|
<Trash2 className="h-2.5 w-2.5 text-red-500" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 실제 컴포넌트 렌더링 */}
|
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
|
||||||
<div className="h-full w-full pt-5 overflow-hidden pointer-events-none">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-b border bg-white shadow-sm overflow-hidden pointer-events-none",
|
||||||
|
isSelected
|
||||||
|
? "border-primary ring-2 ring-primary/30"
|
||||||
|
: "border-gray-200",
|
||||||
|
isDragging && "opacity-80 shadow-lg",
|
||||||
|
!isDragging && "transition-all"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: comp.size?.width || 200,
|
||||||
|
height: comp.size?.height || 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={componentData as any}
|
component={componentData as any}
|
||||||
isDesignMode={true}
|
isDesignMode={true}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,18 @@ import React from "react";
|
||||||
// 컴포넌트별 ConfigPanel 동적 import 맵
|
// 컴포넌트별 ConfigPanel 동적 import 맵
|
||||||
// 모든 ConfigPanel이 있는 컴포넌트를 여기에 등록해야 슬롯/중첩 컴포넌트에서 전용 설정 패널이 표시됨
|
// 모든 ConfigPanel이 있는 컴포넌트를 여기에 등록해야 슬롯/중첩 컴포넌트에서 전용 설정 패널이 표시됨
|
||||||
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||||
|
// ========== 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"),
|
"text-input": () => import("@/lib/registry/components/text-input/TextInputConfigPanel"),
|
||||||
"number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"),
|
"number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"),
|
||||||
|
|
@ -116,21 +128,37 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
||||||
|
|
||||||
// 모듈에서 ConfigPanel 컴포넌트 추출
|
// 모듈에서 ConfigPanel 컴포넌트 추출
|
||||||
// 1차: PascalCase 변환된 이름으로 찾기 (예: text-input -> TextInputConfigPanel)
|
// 1차: PascalCase 변환된 이름으로 찾기 (예: text-input -> TextInputConfigPanel)
|
||||||
// 2차: 특수 export명들 fallback
|
// 2차: v2- 접두사 제거 후 PascalCase 이름으로 찾기 (예: v2-table-list -> TableListConfigPanel)
|
||||||
// 3차: default export
|
// 3차: 특수 export명들 fallback
|
||||||
|
// 4차: default export
|
||||||
const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`;
|
const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`;
|
||||||
|
// v2- 접두사가 있는 경우 접두사를 제거한 이름도 시도
|
||||||
|
const baseComponentId = componentId.startsWith("v2-") ? componentId.slice(3) : componentId;
|
||||||
|
const basePascalCaseName = `${toPascalCase(baseComponentId)}ConfigPanel`;
|
||||||
|
|
||||||
const ConfigPanelComponent =
|
const ConfigPanelComponent =
|
||||||
module[pascalCaseName] ||
|
module[pascalCaseName] ||
|
||||||
|
module[basePascalCaseName] ||
|
||||||
// 특수 export명들
|
// 특수 export명들
|
||||||
module.RepeaterConfigPanel ||
|
module.RepeaterConfigPanel ||
|
||||||
module.FlowWidgetConfigPanel ||
|
module.FlowWidgetConfigPanel ||
|
||||||
module.CustomerItemMappingConfigPanel ||
|
module.CustomerItemMappingConfigPanel ||
|
||||||
module.SelectedItemsDetailInputConfigPanel ||
|
module.SelectedItemsDetailInputConfigPanel ||
|
||||||
module.ButtonConfigPanel ||
|
module.ButtonConfigPanel ||
|
||||||
|
module.TableListConfigPanel ||
|
||||||
module.SectionCardConfigPanel ||
|
module.SectionCardConfigPanel ||
|
||||||
module.SectionPaperConfigPanel ||
|
module.SectionPaperConfigPanel ||
|
||||||
module.TabsConfigPanel ||
|
module.TabsConfigPanel ||
|
||||||
module.UnifiedRepeaterConfigPanel ||
|
module.UnifiedRepeaterConfigPanel ||
|
||||||
|
module.UnifiedInputConfigPanel ||
|
||||||
|
module.UnifiedSelectConfigPanel ||
|
||||||
|
module.UnifiedDateConfigPanel ||
|
||||||
|
module.UnifiedListConfigPanel ||
|
||||||
|
module.UnifiedMediaConfigPanel ||
|
||||||
|
module.UnifiedBizConfigPanel ||
|
||||||
|
module.UnifiedGroupConfigPanel ||
|
||||||
|
module.UnifiedHierarchyConfigPanel ||
|
||||||
|
module.UnifiedLayoutConfigPanel ||
|
||||||
module.RepeatContainerConfigPanel ||
|
module.RepeatContainerConfigPanel ||
|
||||||
module.ScreenSplitPanelConfigPanel ||
|
module.ScreenSplitPanelConfigPanel ||
|
||||||
module.SimpleRepeaterTableConfigPanel ||
|
module.SimpleRepeaterTableConfigPanel ||
|
||||||
|
|
@ -491,6 +519,20 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 Unified 컴포넌트들은 전용 props 사용
|
||||||
|
if (componentId.startsWith("unified-")) {
|
||||||
|
return (
|
||||||
|
<ConfigPanelComponent
|
||||||
|
config={config}
|
||||||
|
onChange={onChange}
|
||||||
|
menuObjid={menuObjid}
|
||||||
|
inputType={currentComponent?.inputType || config?.inputType}
|
||||||
|
screenTableName={screenTableName}
|
||||||
|
tableColumns={selectedTableColumns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용)
|
// entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용)
|
||||||
// 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용)
|
// 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용)
|
||||||
if (componentId === "entity-search-input") {
|
if (componentId === "entity-search-input") {
|
||||||
|
|
@ -520,6 +562,50 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 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 (
|
||||||
|
<ConfigPanelComponent
|
||||||
|
component={componentForButton}
|
||||||
|
onUpdateProperty={(path: string, value: any) => {
|
||||||
|
// 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 (
|
return (
|
||||||
<ConfigPanelComponent
|
<ConfigPanelComponent
|
||||||
config={config}
|
config={config}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue