667 lines
21 KiB
TypeScript
667 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
|
import { Group, Database, Trash2, Copy, Clipboard } from "lucide-react";
|
|
import {
|
|
ScreenDefinition,
|
|
ComponentData,
|
|
LayoutData,
|
|
GroupState,
|
|
WebType,
|
|
TableInfo,
|
|
GroupComponent,
|
|
Position,
|
|
} from "@/types/screen";
|
|
import { generateComponentId } from "@/lib/utils/generateId";
|
|
import {
|
|
createGroupComponent,
|
|
calculateBoundingBox,
|
|
calculateRelativePositions,
|
|
restoreAbsolutePositions,
|
|
getGroupChildren,
|
|
} from "@/lib/utils/groupingUtils";
|
|
import {
|
|
calculateGridInfo,
|
|
snapToGrid,
|
|
snapSizeToGrid,
|
|
generateGridLines,
|
|
GridSettings as GridUtilSettings,
|
|
} from "@/lib/utils/gridUtils";
|
|
import { GroupingToolbar } from "./GroupingToolbar";
|
|
import { screenApi } from "@/lib/api/screen";
|
|
import { toast } from "sonner";
|
|
|
|
import StyleEditor from "./StyleEditor";
|
|
import { RealtimePreview } from "./RealtimePreview";
|
|
import FloatingPanel from "./FloatingPanel";
|
|
import DesignerToolbar from "./DesignerToolbar";
|
|
import TablesPanel from "./panels/TablesPanel";
|
|
import PropertiesPanel from "./panels/PropertiesPanel";
|
|
import GridPanel from "./panels/GridPanel";
|
|
import { usePanelState, PanelConfig } from "@/hooks/usePanelState";
|
|
|
|
interface ScreenDesignerProps {
|
|
selectedScreen: ScreenDefinition | null;
|
|
onBackToList: () => void;
|
|
}
|
|
|
|
// 패널 설정
|
|
const panelConfigs: PanelConfig[] = [
|
|
{
|
|
id: "tables",
|
|
title: "테이블 목록",
|
|
defaultPosition: "left",
|
|
defaultWidth: 320,
|
|
defaultHeight: 600,
|
|
shortcutKey: "t",
|
|
},
|
|
{
|
|
id: "properties",
|
|
title: "속성 편집",
|
|
defaultPosition: "right",
|
|
defaultWidth: 320,
|
|
defaultHeight: 500,
|
|
shortcutKey: "p",
|
|
},
|
|
{
|
|
id: "styles",
|
|
title: "스타일 편집",
|
|
defaultPosition: "right",
|
|
defaultWidth: 320,
|
|
defaultHeight: 400,
|
|
shortcutKey: "s",
|
|
},
|
|
{
|
|
id: "grid",
|
|
title: "격자 설정",
|
|
defaultPosition: "right",
|
|
defaultWidth: 280,
|
|
defaultHeight: 450,
|
|
shortcutKey: "g",
|
|
},
|
|
];
|
|
|
|
export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenDesignerProps) {
|
|
// 패널 상태 관리
|
|
const { panelStates, togglePanel, openPanel, closePanel, closeAllPanels } = usePanelState(panelConfigs);
|
|
|
|
const [layout, setLayout] = useState<LayoutData>({
|
|
components: [],
|
|
gridSettings: { columns: 12, gap: 16, padding: 16, snapToGrid: true, showGrid: true },
|
|
});
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
|
|
|
|
// 실행취소/다시실행을 위한 히스토리 상태
|
|
const [history, setHistory] = useState<LayoutData[]>([]);
|
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
|
|
// 그룹 상태
|
|
const [groupState, setGroupState] = useState<GroupState>({
|
|
selectedComponents: [],
|
|
isGrouping: false,
|
|
});
|
|
|
|
// 드래그 상태
|
|
const [dragState, setDragState] = useState({
|
|
isDragging: false,
|
|
draggedComponent: null as ComponentData | null,
|
|
originalPosition: { x: 0, y: 0 },
|
|
currentPosition: { x: 0, y: 0 },
|
|
grabOffset: { x: 0, y: 0 },
|
|
});
|
|
|
|
// 테이블 데이터
|
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
// 클립보드
|
|
const [clipboard, setClipboard] = useState<{
|
|
type: "single" | "multiple" | "group";
|
|
data: ComponentData[];
|
|
} | null>(null);
|
|
|
|
// 그룹 생성 다이얼로그
|
|
const [showGroupCreateDialog, setShowGroupCreateDialog] = useState(false);
|
|
|
|
const canvasRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 격자 정보 계산
|
|
const gridInfo = useMemo(() => {
|
|
if (!canvasRef.current || !layout.gridSettings) return null;
|
|
return calculateGridInfo(canvasRef.current, layout.gridSettings);
|
|
}, [layout.gridSettings]);
|
|
|
|
// 격자 라인 생성
|
|
const gridLines = useMemo(() => {
|
|
if (!gridInfo || !layout.gridSettings?.showGrid) return [];
|
|
return generateGridLines(gridInfo, layout.gridSettings);
|
|
}, [gridInfo, layout.gridSettings]);
|
|
|
|
// 필터된 테이블 목록
|
|
const filteredTables = useMemo(() => {
|
|
if (!searchTerm) return tables;
|
|
return tables.filter(
|
|
(table) =>
|
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
);
|
|
}, [tables, searchTerm]);
|
|
|
|
// 히스토리에 저장
|
|
const saveToHistory = useCallback(
|
|
(newLayout: LayoutData) => {
|
|
setHistory((prev) => {
|
|
const newHistory = prev.slice(0, historyIndex + 1);
|
|
newHistory.push(newLayout);
|
|
return newHistory.slice(-50); // 최대 50개까지만 저장
|
|
});
|
|
setHistoryIndex((prev) => Math.min(prev + 1, 49));
|
|
setHasUnsavedChanges(true);
|
|
},
|
|
[historyIndex],
|
|
);
|
|
|
|
// 실행취소
|
|
const undo = useCallback(() => {
|
|
if (historyIndex > 0) {
|
|
setHistoryIndex((prev) => prev - 1);
|
|
setLayout(history[historyIndex - 1]);
|
|
}
|
|
}, [history, historyIndex]);
|
|
|
|
// 다시실행
|
|
const redo = useCallback(() => {
|
|
if (historyIndex < history.length - 1) {
|
|
setHistoryIndex((prev) => prev + 1);
|
|
setLayout(history[historyIndex + 1]);
|
|
}
|
|
}, [history, historyIndex]);
|
|
|
|
// 컴포넌트 속성 업데이트
|
|
const updateComponentProperty = useCallback(
|
|
(componentId: string, path: string, value: any) => {
|
|
const pathParts = path.split(".");
|
|
const updatedComponents = layout.components.map((comp) => {
|
|
if (comp.id !== componentId) return comp;
|
|
|
|
const newComp = { ...comp };
|
|
let current: any = newComp;
|
|
|
|
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;
|
|
|
|
// 크기 변경 시 격자 스냅 적용
|
|
if ((path === "size.width" || path === "size.height") && layout.gridSettings?.snapToGrid && gridInfo) {
|
|
const snappedSize = snapSizeToGrid(newComp.size, gridInfo, layout.gridSettings as GridUtilSettings);
|
|
newComp.size = snappedSize;
|
|
}
|
|
|
|
return newComp;
|
|
});
|
|
|
|
const newLayout = { ...layout, components: updatedComponents };
|
|
setLayout(newLayout);
|
|
saveToHistory(newLayout);
|
|
},
|
|
[layout, gridInfo, saveToHistory],
|
|
);
|
|
|
|
// 테이블 데이터 로드
|
|
useEffect(() => {
|
|
if (selectedScreen?.tableName) {
|
|
const loadTables = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await screenApi.getTableInfo([selectedScreen.tableName]);
|
|
setTables(response.data || []);
|
|
} catch (error) {
|
|
console.error("테이블 정보 로드 실패:", error);
|
|
toast.error("테이블 정보를 불러오는데 실패했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}
|
|
}, [selectedScreen?.tableName]);
|
|
|
|
// 화면 레이아웃 로드
|
|
useEffect(() => {
|
|
if (selectedScreen?.screenId) {
|
|
const loadLayout = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await screenApi.getScreenLayout(selectedScreen.screenId);
|
|
if (response.success && response.data) {
|
|
setLayout(response.data);
|
|
setHistory([response.data]);
|
|
setHistoryIndex(0);
|
|
setHasUnsavedChanges(false);
|
|
}
|
|
} catch (error) {
|
|
console.error("레이아웃 로드 실패:", error);
|
|
toast.error("화면 레이아웃을 불러오는데 실패했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadLayout();
|
|
}
|
|
}, [selectedScreen?.screenId]);
|
|
|
|
// 저장
|
|
const handleSave = useCallback(async () => {
|
|
if (!selectedScreen?.screenId) return;
|
|
|
|
try {
|
|
setIsSaving(true);
|
|
const response = await screenApi.saveScreenLayout(selectedScreen.screenId, layout);
|
|
if (response.success) {
|
|
toast.success("화면이 저장되었습니다.");
|
|
setHasUnsavedChanges(false);
|
|
} else {
|
|
toast.error("저장에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("저장 실패:", error);
|
|
toast.error("저장 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}, [selectedScreen?.screenId, layout]);
|
|
|
|
// 드래그 앤 드롭 처리
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
}, []);
|
|
|
|
const handleDrop = useCallback(
|
|
(e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
|
|
const dragData = e.dataTransfer.getData("application/json");
|
|
if (!dragData) return;
|
|
|
|
try {
|
|
const { type, table, column } = JSON.parse(dragData);
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
let newComponent: ComponentData;
|
|
|
|
if (type === "table") {
|
|
// 테이블 컨테이너 생성
|
|
newComponent = {
|
|
id: generateComponentId(),
|
|
type: "container",
|
|
label: table.tableLabel || table.tableName, // 테이블 라벨 우선, 없으면 테이블명
|
|
tableName: table.tableName,
|
|
position: { x, y, z: 1 },
|
|
size: { width: 300, height: 200 },
|
|
};
|
|
} else if (type === "column") {
|
|
// 컬럼 위젯 생성
|
|
newComponent = {
|
|
id: generateComponentId(),
|
|
type: "widget",
|
|
label: column.columnLabel || column.columnName, // 컬럼 라벨 우선, 없으면 컬럼명
|
|
tableName: table.tableName,
|
|
columnName: column.columnName,
|
|
widgetType: column.widgetType,
|
|
dataType: column.dataType,
|
|
required: column.required,
|
|
position: { x, y, z: 1 },
|
|
size: { width: 200, height: 40 },
|
|
};
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// 격자 스냅 적용
|
|
if (layout.gridSettings?.snapToGrid && gridInfo) {
|
|
newComponent.position = snapToGrid(newComponent.position, gridInfo, layout.gridSettings as GridUtilSettings);
|
|
newComponent.size = snapSizeToGrid(newComponent.size, gridInfo, layout.gridSettings as GridUtilSettings);
|
|
}
|
|
|
|
const newLayout = {
|
|
...layout,
|
|
components: [...layout.components, newComponent],
|
|
};
|
|
|
|
setLayout(newLayout);
|
|
saveToHistory(newLayout);
|
|
setSelectedComponent(newComponent);
|
|
|
|
// 속성 패널 자동 열기
|
|
openPanel("properties");
|
|
} catch (error) {
|
|
console.error("드롭 처리 실패:", error);
|
|
}
|
|
},
|
|
[layout, gridInfo, saveToHistory, openPanel],
|
|
);
|
|
|
|
// 컴포넌트 클릭 처리
|
|
const handleComponentClick = useCallback(
|
|
(component: ComponentData, event?: React.MouseEvent) => {
|
|
event?.stopPropagation();
|
|
setSelectedComponent(component);
|
|
|
|
// 속성 패널 자동 열기
|
|
openPanel("properties");
|
|
},
|
|
[openPanel],
|
|
);
|
|
|
|
// 컴포넌트 삭제
|
|
const deleteComponent = useCallback(() => {
|
|
if (!selectedComponent) return;
|
|
|
|
const newComponents = layout.components.filter((comp) => comp.id !== selectedComponent.id);
|
|
const newLayout = { ...layout, components: newComponents };
|
|
|
|
setLayout(newLayout);
|
|
saveToHistory(newLayout);
|
|
setSelectedComponent(null);
|
|
}, [selectedComponent, layout, saveToHistory]);
|
|
|
|
// 컴포넌트 복사
|
|
const copyComponent = useCallback(() => {
|
|
if (!selectedComponent) return;
|
|
|
|
setClipboard({
|
|
type: "single",
|
|
data: [{ ...selectedComponent, id: generateComponentId() }],
|
|
});
|
|
|
|
toast.success("컴포넌트가 복사되었습니다.");
|
|
}, [selectedComponent]);
|
|
|
|
// 그룹 생성
|
|
const handleGroupCreate = useCallback(
|
|
(componentIds: string[], title: string, style?: any) => {
|
|
const selectedComponents = layout.components.filter((comp) => componentIds.includes(comp.id));
|
|
if (selectedComponents.length < 2) return;
|
|
|
|
// 경계 박스 계산
|
|
const boundingBox = calculateBoundingBox(selectedComponents);
|
|
|
|
// 그룹 컴포넌트 생성
|
|
const groupComponent = createGroupComponent(
|
|
componentIds,
|
|
title,
|
|
{ x: boundingBox.minX, y: boundingBox.minY },
|
|
{ width: boundingBox.width, height: boundingBox.height },
|
|
style,
|
|
);
|
|
|
|
// 자식 컴포넌트들의 상대 위치 계산
|
|
const relativeChildren = calculateRelativePositions(
|
|
selectedComponents,
|
|
{ x: boundingBox.minX, y: boundingBox.minY },
|
|
groupComponent.id,
|
|
);
|
|
|
|
const newLayout = {
|
|
...layout,
|
|
components: [
|
|
...layout.components.filter((comp) => !componentIds.includes(comp.id)),
|
|
groupComponent,
|
|
...relativeChildren,
|
|
],
|
|
};
|
|
|
|
setLayout(newLayout);
|
|
saveToHistory(newLayout);
|
|
setGroupState((prev) => ({ ...prev, selectedComponents: [] }));
|
|
},
|
|
[layout, saveToHistory],
|
|
);
|
|
|
|
// 키보드 이벤트 처리
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
// Delete 키로 컴포넌트 삭제
|
|
if (e.key === "Delete" && selectedComponent) {
|
|
deleteComponent();
|
|
}
|
|
|
|
// Ctrl+C로 복사
|
|
if (e.ctrlKey && e.key === "c" && selectedComponent) {
|
|
copyComponent();
|
|
}
|
|
|
|
// Ctrl+Z로 실행취소
|
|
if (e.ctrlKey && e.key === "z" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
undo();
|
|
}
|
|
|
|
// Ctrl+Y 또는 Ctrl+Shift+Z로 다시실행
|
|
if ((e.ctrlKey && e.key === "y") || (e.ctrlKey && e.shiftKey && e.key === "z")) {
|
|
e.preventDefault();
|
|
redo();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, [selectedComponent, deleteComponent, copyComponent, undo, redo]);
|
|
|
|
if (!selectedScreen) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center">
|
|
<div className="text-center">
|
|
<Database className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
|
<h3 className="text-lg font-medium text-gray-900">화면을 선택하세요</h3>
|
|
<p className="text-gray-500">설계할 화면을 먼저 선택해주세요.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen w-full flex-col bg-gray-100">
|
|
{/* 상단 툴바 */}
|
|
<DesignerToolbar
|
|
screenName={selectedScreen?.screenName}
|
|
tableName={selectedScreen?.tableName}
|
|
onBack={onBackToList}
|
|
onSave={handleSave}
|
|
onUndo={undo}
|
|
onRedo={redo}
|
|
onPreview={() => {
|
|
toast.info("미리보기 기능은 준비 중입니다.");
|
|
}}
|
|
onTogglePanel={togglePanel}
|
|
panelStates={panelStates}
|
|
canUndo={historyIndex > 0}
|
|
canRedo={historyIndex < history.length - 1}
|
|
isSaving={isSaving}
|
|
/>
|
|
|
|
{/* 메인 캔버스 영역 (전체 화면) */}
|
|
<div
|
|
ref={canvasRef}
|
|
className="relative flex-1 overflow-hidden bg-white"
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) {
|
|
setSelectedComponent(null);
|
|
setGroupState((prev) => ({ ...prev, selectedComponents: [] }));
|
|
}
|
|
}}
|
|
onDrop={handleDrop}
|
|
onDragOver={handleDragOver}
|
|
>
|
|
{/* 격자 라인 */}
|
|
{gridLines.map((line, index) => (
|
|
<div
|
|
key={index}
|
|
className="pointer-events-none absolute"
|
|
style={{
|
|
left: line.type === "vertical" ? `${line.position}px` : 0,
|
|
top: line.type === "horizontal" ? `${line.position}px` : 0,
|
|
width: line.type === "vertical" ? "1px" : "100%",
|
|
height: line.type === "horizontal" ? "1px" : "100%",
|
|
backgroundColor: layout.gridSettings?.gridColor || "#e5e7eb",
|
|
opacity: layout.gridSettings?.gridOpacity || 0.3,
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* 컴포넌트들 */}
|
|
{layout.components
|
|
.filter((component) => !component.parentId) // 최상위 컴포넌트만 렌더링
|
|
.map((component) => {
|
|
const children =
|
|
component.type === "group" ? layout.components.filter((child) => child.parentId === component.id) : [];
|
|
|
|
return (
|
|
<RealtimePreview
|
|
key={component.id}
|
|
component={component}
|
|
isSelected={selectedComponent?.id === component.id}
|
|
onClick={(e) => handleComponentClick(component, e)}
|
|
>
|
|
{children.map((child) => (
|
|
<RealtimePreview
|
|
key={child.id}
|
|
component={child}
|
|
isSelected={selectedComponent?.id === child.id}
|
|
onClick={(e) => handleComponentClick(child, e)}
|
|
/>
|
|
))}
|
|
</RealtimePreview>
|
|
);
|
|
})}
|
|
|
|
{/* 빈 캔버스 안내 */}
|
|
{layout.components.length === 0 && (
|
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
<div className="text-center text-gray-400">
|
|
<Database className="mx-auto mb-4 h-16 w-16" />
|
|
<h3 className="mb-2 text-xl font-medium">캔버스가 비어있습니다</h3>
|
|
<p className="text-sm">좌측 테이블 패널에서 테이블이나 컬럼을 드래그하여 화면을 설계하세요</p>
|
|
<p className="mt-2 text-xs">단축키: T(테이블), P(속성), S(스타일), G(격자)</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 플로팅 패널들 */}
|
|
<FloatingPanel
|
|
id="tables"
|
|
title="테이블 목록"
|
|
isOpen={panelStates.tables?.isOpen || false}
|
|
onClose={() => closePanel("tables")}
|
|
position="left"
|
|
width={320}
|
|
height={600}
|
|
>
|
|
<TablesPanel
|
|
tables={filteredTables}
|
|
searchTerm={searchTerm}
|
|
onSearchChange={setSearchTerm}
|
|
onDragStart={(e, table, column) => {
|
|
const dragData = {
|
|
type: column ? "column" : "table",
|
|
table,
|
|
column,
|
|
};
|
|
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
|
}}
|
|
selectedTableName={selectedScreen.tableName}
|
|
/>
|
|
</FloatingPanel>
|
|
|
|
<FloatingPanel
|
|
id="properties"
|
|
title="속성 편집"
|
|
isOpen={panelStates.properties?.isOpen || false}
|
|
onClose={() => closePanel("properties")}
|
|
position="right"
|
|
width={320}
|
|
height={500}
|
|
>
|
|
<PropertiesPanel
|
|
selectedComponent={selectedComponent}
|
|
onUpdateProperty={updateComponentProperty}
|
|
onDeleteComponent={deleteComponent}
|
|
onCopyComponent={copyComponent}
|
|
/>
|
|
</FloatingPanel>
|
|
|
|
<FloatingPanel
|
|
id="styles"
|
|
title="스타일 편집"
|
|
isOpen={panelStates.styles?.isOpen || false}
|
|
onClose={() => closePanel("styles")}
|
|
position="right"
|
|
width={320}
|
|
height={400}
|
|
>
|
|
{selectedComponent ? (
|
|
<div className="p-4">
|
|
<StyleEditor
|
|
style={selectedComponent.style || {}}
|
|
onStyleChange={(newStyle) => updateComponentProperty(selectedComponent.id, "style", newStyle)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex h-full items-center justify-center text-gray-500">
|
|
컴포넌트를 선택하여 스타일을 편집하세요
|
|
</div>
|
|
)}
|
|
</FloatingPanel>
|
|
|
|
<FloatingPanel
|
|
id="grid"
|
|
title="격자 설정"
|
|
isOpen={panelStates.grid?.isOpen || false}
|
|
onClose={() => closePanel("grid")}
|
|
position="right"
|
|
width={280}
|
|
height={450}
|
|
>
|
|
<GridPanel
|
|
gridSettings={layout.gridSettings || { columns: 12, gap: 16, padding: 16, snapToGrid: true, showGrid: true }}
|
|
onGridSettingsChange={(settings) => {
|
|
const newLayout = { ...layout, gridSettings: settings };
|
|
setLayout(newLayout);
|
|
saveToHistory(newLayout);
|
|
}}
|
|
onResetGrid={() => {
|
|
const defaultSettings = { columns: 12, gap: 16, padding: 16, snapToGrid: true, showGrid: true };
|
|
const newLayout = { ...layout, gridSettings: defaultSettings };
|
|
setLayout(newLayout);
|
|
saveToHistory(newLayout);
|
|
}}
|
|
/>
|
|
</FloatingPanel>
|
|
|
|
{/* 그룹 생성 툴바 (필요시) */}
|
|
{groupState.selectedComponents.length > 1 && (
|
|
<div className="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 transform">
|
|
<GroupingToolbar
|
|
selectedComponents={groupState.selectedComponents}
|
|
onGroupCreate={handleGroupCreate}
|
|
showCreateDialog={showGroupCreateDialog}
|
|
onShowCreateDialogChange={setShowGroupCreateDialog}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|