feat: 분할 패널 내부 컴포넌트 선택 기능 추가
- RealtimePreviewDynamic, ScreenDesigner, DynamicComponentRenderer, SplitPanelLayoutComponent 및 관련 파일에서 분할 패널 내부 컴포넌트 선택 콜백 및 상태 관리 기능을 추가하였습니다. - 커스텀 모드에서 패널 내부에 컴포넌트를 자유롭게 배치할 수 있는 기능을 구현하였습니다. - 선택된 패널 컴포넌트의 상태를 관리하고, 관련 UI 요소를 업데이트하여 사용자 경험을 향상시켰습니다. - 패널의 표시 모드에 'custom' 옵션을 추가하여 사용자 정의 배치 기능을 지원합니다.
This commit is contained in:
@@ -39,6 +39,8 @@ interface RealtimePreviewProps {
|
||||
onUpdateComponent?: (updatedComponent: any) => void; // 🆕 컴포넌트 업데이트 콜백
|
||||
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; // 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
selectedTabComponentId?: string; // 🆕 선택된 탭 컴포넌트 ID
|
||||
onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: any) => void; // 🆕 분할 패널 내부 컴포넌트 선택 콜백
|
||||
selectedPanelComponentId?: string; // 🆕 선택된 분할 패널 컴포넌트 ID
|
||||
onResize?: (componentId: string, newSize: { width: number; height: number }) => void; // 🆕 리사이즈 콜백
|
||||
|
||||
// 버튼 액션을 위한 props
|
||||
@@ -140,6 +142,8 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
||||
onUpdateComponent, // 🆕 컴포넌트 업데이트 콜백
|
||||
onSelectTabComponent, // 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
selectedTabComponentId, // 🆕 선택된 탭 컴포넌트 ID
|
||||
onSelectPanelComponent, // 🆕 분할 패널 내부 컴포넌트 선택 콜백
|
||||
selectedPanelComponentId, // 🆕 선택된 분할 패널 컴포넌트 ID
|
||||
onResize, // 🆕 리사이즈 콜백
|
||||
}) => {
|
||||
// 🆕 화면 다국어 컨텍스트
|
||||
@@ -640,6 +644,8 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
||||
onUpdateComponent={onUpdateComponent}
|
||||
onSelectTabComponent={onSelectTabComponent}
|
||||
selectedTabComponentId={selectedTabComponentId}
|
||||
onSelectPanelComponent={onSelectPanelComponent}
|
||||
selectedPanelComponentId={selectedPanelComponentId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -177,13 +177,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
component: any; // 탭 내부 컴포넌트 데이터
|
||||
} | null>(null);
|
||||
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 상태
|
||||
const [selectedPanelComponentInfo, setSelectedPanelComponentInfo] = useState<{
|
||||
splitPanelId: string; // 분할 패널 컴포넌트 ID
|
||||
panelSide: "left" | "right"; // 좌측/우측 패널
|
||||
componentId: string; // 패널 내부 컴포넌트 ID
|
||||
component: any; // 패널 내부 컴포넌트 데이터
|
||||
} | null>(null);
|
||||
|
||||
// 컴포넌트 선택 시 통합 패널 자동 열기
|
||||
const handleComponentSelect = useCallback(
|
||||
(component: ComponentData | null) => {
|
||||
setSelectedComponent(component);
|
||||
// 일반 컴포넌트 선택 시 탭 내부 컴포넌트 선택 해제
|
||||
// 일반 컴포넌트 선택 시 탭 내부 컴포넌트/분할 패널 내부 컴포넌트 선택 해제
|
||||
if (component) {
|
||||
setSelectedTabComponentInfo(null);
|
||||
setSelectedPanelComponentInfo(null);
|
||||
}
|
||||
|
||||
// 컴포넌트가 선택되면 통합 패널 자동 열기
|
||||
@@ -209,8 +218,32 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
componentId: compId,
|
||||
component: comp,
|
||||
});
|
||||
// 탭 내부 컴포넌트 선택 시 일반 컴포넌트 선택 해제
|
||||
// 탭 내부 컴포넌트 선택 시 일반 컴포넌트/분할 패널 내부 컴포넌트 선택 해제
|
||||
setSelectedComponent(null);
|
||||
setSelectedPanelComponentInfo(null);
|
||||
openPanel("v2");
|
||||
},
|
||||
[openPanel],
|
||||
);
|
||||
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 핸들러
|
||||
const handleSelectPanelComponent = useCallback(
|
||||
(splitPanelId: string, panelSide: "left" | "right", compId: string, comp: any) => {
|
||||
if (!compId) {
|
||||
// 패널 영역 빈 공간 클릭 시 선택 해제
|
||||
setSelectedPanelComponentInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPanelComponentInfo({
|
||||
splitPanelId,
|
||||
panelSide,
|
||||
componentId: compId,
|
||||
component: comp,
|
||||
});
|
||||
// 분할 패널 내부 컴포넌트 선택 시 일반 컴포넌트/탭 내부 컴포넌트 선택 해제
|
||||
setSelectedComponent(null);
|
||||
setSelectedTabComponentInfo(null);
|
||||
openPanel("v2");
|
||||
},
|
||||
[openPanel],
|
||||
@@ -2509,6 +2542,72 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 분할 패널 커스텀 모드 컨테이너 내부 드롭 처리
|
||||
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
||||
if (splitPanelContainer) {
|
||||
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
||||
if (containerId && panelSide) {
|
||||
const targetComponent = layout.components.find((c) => c.id === containerId);
|
||||
const compType = (targetComponent as any)?.componentType;
|
||||
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
||||
const currentConfig = (targetComponent as any).componentConfig || {};
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const currentComponents = panelConfig.components || [];
|
||||
|
||||
// 드롭 위치 계산
|
||||
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
||||
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
||||
|
||||
// 새 컴포넌트 생성
|
||||
const componentType = component.id || component.componentType || "v2-text-display";
|
||||
|
||||
console.log("🎯 분할 패널에 컴포넌트 드롭:", {
|
||||
componentId: component.id,
|
||||
componentType: componentType,
|
||||
panelSide: panelSide,
|
||||
dropPosition: { x: dropX, y: dropY },
|
||||
});
|
||||
|
||||
const newPanelComponent = {
|
||||
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
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 },
|
||||
componentConfig: component.defaultConfig || {},
|
||||
};
|
||||
|
||||
const updatedPanelConfig = {
|
||||
...panelConfig,
|
||||
components: [...currentComponents, newPanelComponent],
|
||||
};
|
||||
|
||||
const updatedComponent = {
|
||||
...targetComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: updatedPanelConfig,
|
||||
},
|
||||
};
|
||||
|
||||
const newLayout = {
|
||||
...layout,
|
||||
components: layout.components.map((c) =>
|
||||
c.id === containerId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
toast.success(`컴포넌트가 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
||||
return; // 분할 패널 처리 완료
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
@@ -2996,6 +3095,105 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 분할 패널 커스텀 모드 컨테이너 내부에 컬럼 드롭 시 처리
|
||||
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
||||
if (splitPanelContainer && type === "column" && column) {
|
||||
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
||||
if (containerId && panelSide) {
|
||||
const targetComponent = layout.components.find((c) => c.id === containerId);
|
||||
const compType = (targetComponent as any)?.componentType;
|
||||
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
||||
const currentConfig = (targetComponent as any).componentConfig || {};
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const currentComponents = panelConfig.components || [];
|
||||
|
||||
// 드롭 위치 계산
|
||||
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
||||
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
||||
|
||||
// V2 컴포넌트 매핑 사용
|
||||
const v2Mapping = createV2ConfigFromColumn({
|
||||
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 getPanelComponentSize = (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 = getPanelComponentSize(column.widgetType);
|
||||
|
||||
const newPanelComponent = {
|
||||
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
componentType: v2Mapping.componentType,
|
||||
label: column.columnLabel || column.columnName,
|
||||
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
||||
size: componentSize,
|
||||
inputType: column.inputType || column.widgetType,
|
||||
widgetType: column.widgetType,
|
||||
componentConfig: {
|
||||
...v2Mapping.componentConfig,
|
||||
columnName: column.columnName,
|
||||
tableName: column.tableName,
|
||||
inputType: column.inputType || column.widgetType,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedPanelConfig = {
|
||||
...panelConfig,
|
||||
components: [...currentComponents, newPanelComponent],
|
||||
};
|
||||
|
||||
const updatedComponent = {
|
||||
...targetComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: updatedPanelConfig,
|
||||
},
|
||||
};
|
||||
|
||||
const newLayout = {
|
||||
...layout,
|
||||
components: layout.components.map((c) =>
|
||||
c.id === containerId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
toast.success(`컬럼이 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
@@ -5123,6 +5321,158 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : selectedPanelComponentInfo ? (
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 시 V2PropertiesPanel 사용
|
||||
(() => {
|
||||
const panelComp = selectedPanelComponentInfo.component;
|
||||
|
||||
// 분할 패널 내부 컴포넌트를 ComponentData 형식으로 변환
|
||||
const panelComponentAsComponentData: ComponentData = {
|
||||
id: panelComp.id,
|
||||
type: "component",
|
||||
componentType: panelComp.componentType,
|
||||
label: panelComp.label,
|
||||
position: panelComp.position || { x: 0, y: 0 },
|
||||
size: panelComp.size || { width: 200, height: 100 },
|
||||
componentConfig: panelComp.componentConfig || {},
|
||||
style: panelComp.style || {},
|
||||
} as ComponentData;
|
||||
|
||||
// 분할 패널 내부 컴포넌트용 속성 업데이트 핸들러
|
||||
const updatePanelComponentProperty = (componentId: string, path: string, value: any) => {
|
||||
const { splitPanelId, panelSide } = selectedPanelComponentInfo;
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
|
||||
setLayout((prevLayout) => {
|
||||
const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId);
|
||||
if (!splitPanelComponent) return prevLayout;
|
||||
|
||||
const currentConfig = (splitPanelComponent as any).componentConfig || {};
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const components = panelConfig.components || [];
|
||||
|
||||
// 해당 컴포넌트 찾기
|
||||
const targetCompIndex = components.findIndex((c: any) => c.id === componentId);
|
||||
if (targetCompIndex === -1) return prevLayout;
|
||||
|
||||
// 컴포넌트 속성 업데이트
|
||||
const targetComp = components[targetCompIndex];
|
||||
const updatedComp = path === "style"
|
||||
? { ...targetComp, style: value }
|
||||
: path.includes(".")
|
||||
? (() => {
|
||||
const parts = path.split(".");
|
||||
let obj = { ...targetComp };
|
||||
let current: any = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
current[parts[i]] = { ...current[parts[i]] };
|
||||
current = current[parts[i]];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
return obj;
|
||||
})()
|
||||
: { ...targetComp, [path]: value };
|
||||
|
||||
const updatedComponents = [
|
||||
...components.slice(0, targetCompIndex),
|
||||
updatedComp,
|
||||
...components.slice(targetCompIndex + 1),
|
||||
];
|
||||
|
||||
const updatedComponent = {
|
||||
...splitPanelComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// selectedPanelComponentInfo 업데이트
|
||||
setSelectedPanelComponentInfo(prev =>
|
||||
prev ? { ...prev, component: updatedComp } : null
|
||||
);
|
||||
|
||||
return {
|
||||
...prevLayout,
|
||||
components: prevLayout.components.map((c) =>
|
||||
c.id === splitPanelId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 분할 패널 내부 컴포넌트 삭제 핸들러
|
||||
const deletePanelComponent = (componentId: string) => {
|
||||
const { splitPanelId, panelSide } = selectedPanelComponentInfo;
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
|
||||
setLayout((prevLayout) => {
|
||||
const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId);
|
||||
if (!splitPanelComponent) return prevLayout;
|
||||
|
||||
const currentConfig = (splitPanelComponent as any).componentConfig || {};
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const components = panelConfig.components || [];
|
||||
|
||||
const updatedComponents = components.filter((c: any) => c.id !== componentId);
|
||||
|
||||
const updatedComponent = {
|
||||
...splitPanelComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setSelectedPanelComponentInfo(null);
|
||||
|
||||
return {
|
||||
...prevLayout,
|
||||
components: prevLayout.components.map((c) =>
|
||||
c.id === splitPanelId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
분할 패널 ({selectedPanelComponentInfo.panelSide === "left" ? "좌측" : "우측"}) 컴포넌트
|
||||
</span>
|
||||
<button
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setSelectedPanelComponentInfo(null)}
|
||||
>
|
||||
선택 해제
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<V2PropertiesPanel
|
||||
selectedComponent={panelComponentAsComponentData}
|
||||
tables={tables}
|
||||
onUpdateProperty={updatePanelComponentProperty}
|
||||
onDeleteComponent={deletePanelComponent}
|
||||
currentTable={tables.length > 0 ? tables[0] : undefined}
|
||||
currentTableName={selectedScreen?.tableName}
|
||||
currentScreenCompanyCode={selectedScreen?.companyCode}
|
||||
onStyleChange={(style) => {
|
||||
updatePanelComponentProperty(panelComp.id, "style", style);
|
||||
}}
|
||||
allComponents={layout.components}
|
||||
menuObjid={menuObjid}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<V2PropertiesPanel
|
||||
selectedComponent={selectedComponent || undefined}
|
||||
@@ -5514,6 +5864,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
? selectedTabComponentInfo.componentId
|
||||
: undefined
|
||||
}
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 핸들러
|
||||
onSelectPanelComponent={(panelSide, compId, comp) =>
|
||||
handleSelectPanelComponent(component.id, panelSide, compId, comp)
|
||||
}
|
||||
selectedPanelComponentId={
|
||||
selectedPanelComponentInfo?.splitPanelId === component.id
|
||||
? selectedPanelComponentInfo.componentId
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{/* 컨테이너, 그룹, 영역, 컴포넌트의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
|
||||
{(component.type === "group" ||
|
||||
|
||||
Reference in New Issue
Block a user