ERP-node/frontend/components/pop/designer/PopDesigner.tsx

801 lines
29 KiB
TypeScript

"use client";
import { useState, useCallback, useEffect } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { ArrowLeft, Save, Smartphone, Tablet, Undo2, Redo2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { toast } from "sonner";
import { PopCanvas } from "./PopCanvas";
import { PopCanvasV4 } from "./PopCanvasV4";
import { PopPanel } from "./panels/PopPanel";
import { ComponentPaletteV4 } from "./panels/ComponentPaletteV4";
import { ComponentEditorPanelV4 } from "./panels/ComponentEditorPanelV4";
import {
PopLayoutDataV3,
PopLayoutDataV4,
PopLayoutModeKey,
PopComponentType,
GridPosition,
PopComponentDefinition,
PopComponentDefinitionV4,
PopContainerV4,
PopSizeConstraintV4,
createEmptyPopLayoutV3,
createEmptyPopLayoutV4,
ensureV3Layout,
addComponentToV3Layout,
removeComponentFromV3Layout,
updateComponentPositionInModeV3,
addComponentToV4Layout,
removeComponentFromV4Layout,
updateComponentInV4Layout,
updateContainerV4,
findContainerV4,
isV3Layout,
isV4Layout,
} from "./types/pop-layout";
import { screenApi } from "@/lib/api/screen";
import { ScreenDefinition } from "@/types/screen";
// ========================================
// 레이아웃 모드 타입
// ========================================
type LayoutMode = "v3" | "v4";
type DeviceType = "mobile" | "tablet";
// ========================================
// Props
// ========================================
interface PopDesignerProps {
selectedScreen: ScreenDefinition;
onBackToList: () => void;
onScreenUpdate?: (updatedScreen: Partial<ScreenDefinition>) => void;
}
// ========================================
// 메인 컴포넌트 (v3/v4 통합)
// - 새 화면: v4로 시작
// - 기존 v3 화면: v3로 로드 (하위 호환)
// ========================================
export default function PopDesigner({
selectedScreen,
onBackToList,
onScreenUpdate,
}: PopDesignerProps) {
// ========================================
// 레이아웃 모드 (데이터에 따라 자동 결정)
// ========================================
const [layoutMode, setLayoutMode] = useState<LayoutMode>("v4");
// ========================================
// 레이아웃 상태 (데스크탑 모드와 동일한 방식)
// ========================================
const [layoutV4, setLayoutV4] = useState<PopLayoutDataV4>(createEmptyPopLayoutV4());
const [layoutV3, setLayoutV3] = useState<PopLayoutDataV3>(createEmptyPopLayoutV3());
// 히스토리 (v4용)
const [historyV4, setHistoryV4] = useState<PopLayoutDataV4[]>([]);
const [historyIndexV4, setHistoryIndexV4] = useState(-1);
// 히스토리 (v3용)
const [historyV3, setHistoryV3] = useState<PopLayoutDataV3[]>([]);
const [historyIndexV3, setHistoryIndexV3] = useState(-1);
const [idCounter, setIdCounter] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// ========================================
// 히스토리 저장 함수
// ========================================
const saveToHistoryV4 = useCallback((newLayout: PopLayoutDataV4) => {
setHistoryV4((prev) => {
const newHistory = prev.slice(0, historyIndexV4 + 1);
newHistory.push(JSON.parse(JSON.stringify(newLayout))); // 깊은 복사
return newHistory.slice(-50); // 최대 50개
});
setHistoryIndexV4((prev) => Math.min(prev + 1, 49));
}, [historyIndexV4]);
const saveToHistoryV3 = useCallback((newLayout: PopLayoutDataV3) => {
setHistoryV3((prev) => {
const newHistory = prev.slice(0, historyIndexV3 + 1);
newHistory.push(JSON.parse(JSON.stringify(newLayout)));
return newHistory.slice(-50);
});
setHistoryIndexV3((prev) => Math.min(prev + 1, 49));
}, [historyIndexV3]);
// ========================================
// Undo/Redo 함수
// ========================================
const undoV4 = useCallback(() => {
if (historyIndexV4 > 0) {
const newIndex = historyIndexV4 - 1;
const previousLayout = historyV4[newIndex];
if (previousLayout) {
setLayoutV4(JSON.parse(JSON.stringify(previousLayout)));
setHistoryIndexV4(newIndex);
console.log("[Undo V4] 복원됨, index:", newIndex);
}
}
}, [historyIndexV4, historyV4]);
const redoV4 = useCallback(() => {
if (historyIndexV4 < historyV4.length - 1) {
const newIndex = historyIndexV4 + 1;
const nextLayout = historyV4[newIndex];
if (nextLayout) {
setLayoutV4(JSON.parse(JSON.stringify(nextLayout)));
setHistoryIndexV4(newIndex);
console.log("[Redo V4] 복원됨, index:", newIndex);
}
}
}, [historyIndexV4, historyV4]);
const undoV3 = useCallback(() => {
if (historyIndexV3 > 0) {
const newIndex = historyIndexV3 - 1;
const previousLayout = historyV3[newIndex];
if (previousLayout) {
setLayoutV3(JSON.parse(JSON.stringify(previousLayout)));
setHistoryIndexV3(newIndex);
}
}
}, [historyIndexV3, historyV3]);
const redoV3 = useCallback(() => {
if (historyIndexV3 < historyV3.length - 1) {
const newIndex = historyIndexV3 + 1;
const nextLayout = historyV3[newIndex];
if (nextLayout) {
setLayoutV3(JSON.parse(JSON.stringify(nextLayout)));
setHistoryIndexV3(newIndex);
}
}
}, [historyIndexV3, historyV3]);
// 현재 모드의 Undo/Redo
const canUndo = layoutMode === "v4" ? historyIndexV4 > 0 : historyIndexV3 > 0;
const canRedo = layoutMode === "v4"
? historyIndexV4 < historyV4.length - 1
: historyIndexV3 < historyV3.length - 1;
const handleUndo = layoutMode === "v4" ? undoV4 : undoV3;
const handleRedo = layoutMode === "v4" ? redoV4 : redoV3;
// ========================================
// v3용 디바이스/모드 상태
// ========================================
const [activeDevice, setActiveDevice] = useState<DeviceType>("tablet");
const [activeModeKey, setActiveModeKey] = useState<PopLayoutModeKey>("tablet_landscape");
// ========================================
// v4용 뷰포트 모드 상태
// ========================================
type ViewportMode = "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
const [currentViewportMode, setCurrentViewportMode] = useState<ViewportMode>("tablet_landscape");
// v4: 임시 레이아웃 (고정 전 배치) - 다른 모드에서만 사용
const [tempLayout, setTempLayout] = useState<PopContainerV4 | null>(null);
// ========================================
// 선택 상태
// ========================================
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
const [selectedContainerId, setSelectedContainerId] = useState<string | null>(null);
// 선택된 컴포넌트/컨테이너
const selectedComponentV3: PopComponentDefinition | null = selectedComponentId
? layoutV3.components[selectedComponentId] || null
: null;
const selectedComponentV4: PopComponentDefinitionV4 | null = selectedComponentId
? layoutV4.components[selectedComponentId] || null
: null;
const selectedContainer: PopContainerV4 | null = selectedContainerId
? findContainerV4(layoutV4.root, selectedContainerId)
: null;
// ========================================
// 레이아웃 로드
// ========================================
useEffect(() => {
const loadLayout = async () => {
if (!selectedScreen?.screenId) return;
setIsLoading(true);
try {
const loadedLayout = await screenApi.getLayoutPop(selectedScreen.screenId);
// 유효한 레이아웃인지 확인:
// 1. version 필드 필수
// 2. 컴포넌트가 있어야 함 (빈 레이아웃은 새 화면 취급)
const hasValidLayout = loadedLayout && loadedLayout.version;
const hasComponents = loadedLayout?.components && Object.keys(loadedLayout.components).length > 0;
if (hasValidLayout && hasComponents) {
if (isV4Layout(loadedLayout)) {
// v4 레이아웃
setLayoutV4(loadedLayout);
setHistoryV4([loadedLayout]);
setHistoryIndexV4(0);
setLayoutMode("v4");
console.log(`POP v4 레이아웃 로드: ${Object.keys(loadedLayout.components).length}개 컴포넌트`);
} else {
// v1/v2/v3 → v3로 변환
const v3Layout = ensureV3Layout(loadedLayout);
setLayoutV3(v3Layout);
setHistoryV3([v3Layout]);
setHistoryIndexV3(0);
setLayoutMode("v3");
console.log(`POP v3 레이아웃 로드: ${Object.keys(v3Layout.components).length}개 컴포넌트`);
}
} else {
// 새 화면 또는 빈 레이아웃 → v4로 시작
const emptyLayout = createEmptyPopLayoutV4();
setLayoutV4(emptyLayout);
setHistoryV4([emptyLayout]);
setHistoryIndexV4(0);
setLayoutMode("v4");
}
} catch (error) {
console.error("레이아웃 로드 실패:", error);
toast.error("레이아웃을 불러오는데 실패했습니다");
const emptyLayout = createEmptyPopLayoutV4();
setLayoutV4(emptyLayout);
setHistoryV4([emptyLayout]);
setHistoryIndexV4(0);
setLayoutMode("v4");
} finally {
setIsLoading(false);
}
};
loadLayout();
}, [selectedScreen?.screenId]);
// ========================================
// 저장
// ========================================
const handleSave = useCallback(async () => {
if (!selectedScreen?.screenId) return;
setIsSaving(true);
try {
const layoutToSave = layoutMode === "v3" ? layoutV3 : layoutV4;
await screenApi.saveLayoutPop(selectedScreen.screenId, layoutToSave);
toast.success("저장되었습니다");
setHasChanges(false);
} catch (error) {
console.error("저장 실패:", error);
toast.error("저장에 실패했습니다");
} finally {
setIsSaving(false);
}
}, [selectedScreen?.screenId, layoutMode, layoutV3, layoutV4]);
// ========================================
// v3: 컴포넌트 핸들러
// ========================================
const handleDropComponentV3 = useCallback(
(type: PopComponentType, gridPosition: GridPosition) => {
const newId = `${type}-${Date.now()}`;
const newLayout = addComponentToV3Layout(layoutV3, newId, type, gridPosition);
setLayoutV3(newLayout);
saveToHistoryV3(newLayout);
setSelectedComponentId(newId);
setHasChanges(true);
},
[layoutV3, saveToHistoryV3]
);
const handleUpdateComponentDefinitionV3 = useCallback(
(componentId: string, updates: Partial<PopComponentDefinition>) => {
const newLayout = {
...layoutV3,
components: {
...layoutV3.components,
[componentId]: { ...layoutV3.components[componentId], ...updates },
},
};
setLayoutV3(newLayout);
saveToHistoryV3(newLayout);
setHasChanges(true);
},
[layoutV3, saveToHistoryV3]
);
const handleUpdateComponentPositionV3 = useCallback(
(componentId: string, position: GridPosition, modeKey?: PopLayoutModeKey) => {
const targetMode = modeKey || activeModeKey;
const newLayout = updateComponentPositionInModeV3(layoutV3, targetMode, componentId, position);
setLayoutV3(newLayout);
saveToHistoryV3(newLayout);
setHasChanges(true);
},
[layoutV3, activeModeKey, saveToHistoryV3]
);
const handleDeleteComponentV3 = useCallback((componentId: string) => {
const newLayout = removeComponentFromV3Layout(layoutV3, componentId);
setLayoutV3(newLayout);
saveToHistoryV3(newLayout);
setSelectedComponentId(null);
setHasChanges(true);
}, [layoutV3, saveToHistoryV3]);
// ========================================
// v4: 컴포넌트 핸들러
// ========================================
const handleDropComponentV4 = useCallback(
(type: PopComponentType, containerId: string) => {
const componentId = `comp_${idCounter}`;
setIdCounter((prev) => prev + 1);
const newLayout = addComponentToV4Layout(layoutV4, componentId, type, containerId, `${type} ${idCounter}`);
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setSelectedComponentId(componentId);
setSelectedContainerId(null);
setHasChanges(true);
console.log("[V4] 컴포넌트 추가, 히스토리 저장됨");
},
[idCounter, layoutV4, saveToHistoryV4]
);
const handleUpdateComponentV4 = useCallback(
(componentId: string, updates: Partial<PopComponentDefinitionV4>) => {
const newLayout = updateComponentInV4Layout(layoutV4, componentId, updates);
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setHasChanges(true);
},
[layoutV4, saveToHistoryV4]
);
const handleUpdateContainerV4 = useCallback(
(containerId: string, updates: Partial<PopContainerV4>) => {
if (currentViewportMode === "tablet_landscape") {
// 기본 모드 (태블릿 가로) → root 직접 수정 ✅
const newLayout = {
...layoutV4,
root: updateContainerV4(layoutV4.root, containerId, updates),
};
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setHasChanges(true);
console.log("[기본 모드] root 컨테이너 수정");
} else {
// 다른 모드 → 속성 패널에서 수정 차단됨 (UI에서 비활성화)
toast.warning("기본 모드(태블릿 가로)에서만 속성을 변경할 수 있습니다");
console.log("[다른 모드] 속성 수정 차단");
}
},
[layoutV4, currentViewportMode, saveToHistoryV4]
);
const handleDeleteComponentV4 = useCallback((componentId: string) => {
const newLayout = removeComponentFromV4Layout(layoutV4, componentId);
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setSelectedComponentId(null);
setHasChanges(true);
console.log("[V4] 컴포넌트 삭제, 히스토리 저장됨");
}, [layoutV4, saveToHistoryV4]);
// v4: 현재 모드 배치 고정 (오버라이드 저장) 🔥
const handleLockLayoutV4 = useCallback(() => {
if (currentViewportMode === "tablet_landscape") {
toast.info("기본 모드는 고정할 필요가 없습니다");
return;
}
if (!tempLayout) {
toast.info("변경사항이 없습니다");
return;
}
// 임시 레이아웃을 오버라이드에 저장 ✅
const newLayout = {
...layoutV4,
overrides: {
...layoutV4.overrides,
[currentViewportMode]: {
...layoutV4.overrides?.[currentViewportMode as keyof typeof layoutV4.overrides],
containers: {
root: {
direction: tempLayout.direction,
wrap: tempLayout.wrap,
gap: tempLayout.gap,
alignItems: tempLayout.alignItems,
justifyContent: tempLayout.justifyContent,
padding: tempLayout.padding,
children: tempLayout.children, // 순서 고정
}
}
}
}
};
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setTempLayout(null); // 임시 레이아웃 초기화
setHasChanges(true);
toast.success(`${currentViewportMode} 모드 배치가 고정되었습니다`);
console.log(`[V4] ${currentViewportMode} 배치 고정됨 (tempLayout → overrides)`);
}, [layoutV4, currentViewportMode, tempLayout, saveToHistoryV4]);
// v4: 오버라이드 초기화 (자동 계산으로 되돌리기)
const handleResetOverrideV4 = useCallback((mode: ViewportMode) => {
if (mode === "tablet_landscape") {
toast.info("기본 모드는 초기화할 수 없습니다");
return;
}
const newOverrides = { ...layoutV4.overrides };
delete newOverrides[mode as keyof typeof newOverrides];
const newLayout = {
...layoutV4,
overrides: Object.keys(newOverrides).length > 0 ? newOverrides : undefined
};
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setHasChanges(true);
toast.success(`${mode} 모드 오버라이드가 초기화되었습니다`);
console.log(`[V4] ${mode} 오버라이드 초기화됨`);
}, [layoutV4, saveToHistoryV4]);
// v4: 컴포넌트 크기 조정 (드래그) - 리사이즈 중에는 히스토리 저장 안 함
// 리사이즈 완료 시 별도로 저장해야 함 (TODO: 드래그 종료 시 저장)
const handleResizeComponentV4 = useCallback(
(componentId: string, sizeUpdates: Partial<PopSizeConstraintV4>) => {
const existingComponent = layoutV4.components[componentId];
if (!existingComponent) return;
const newLayout = {
...layoutV4,
components: {
...layoutV4.components,
[componentId]: {
...existingComponent,
size: {
...existingComponent.size,
...sizeUpdates,
},
},
},
};
setLayoutV4(newLayout);
// 리사이즈 중에는 히스토리 저장 안 함 (너무 많아짐)
// saveToHistoryV4(newLayout);
setHasChanges(true);
},
[layoutV4]
);
// v4: 컴포넌트 순서 변경 (드래그 앤 드롭)
const handleReorderComponentV4 = useCallback(
(containerId: string, fromIndex: number, toIndex: number) => {
// 컨테이너 찾기 (재귀)
const reorderInContainer = (container: PopContainerV4): PopContainerV4 => {
if (container.id === containerId) {
const newChildren = [...container.children];
const [movedItem] = newChildren.splice(fromIndex, 1);
newChildren.splice(toIndex, 0, movedItem);
return { ...container, children: newChildren };
}
// 자식 컨테이너에서도 찾기
return {
...container,
children: container.children.map(child => {
if (typeof child === "object") {
return reorderInContainer(child);
}
return child;
}),
};
};
if (currentViewportMode === "tablet_landscape") {
// 기본 모드 → root 직접 수정 ✅
const newLayout = {
...layoutV4,
root: reorderInContainer(layoutV4.root),
};
setLayoutV4(newLayout);
saveToHistoryV4(newLayout);
setHasChanges(true);
console.log("[기본 모드] 컴포넌트 순서 변경 (root 저장)");
} else {
// 다른 모드 → 임시 레이아웃에만 저장 (화면에만 표시, layoutV4는 안 건드림) 🔥
const reorderedRoot = reorderInContainer(layoutV4.root);
setTempLayout(reorderedRoot);
console.log(`[${currentViewportMode}] 컴포넌트 순서 변경 (임시, 고정 필요)`);
toast.info("배치 변경됨. '고정' 버튼을 클릭하여 저장하세요", { duration: 2000 });
}
},
[layoutV4, currentViewportMode, saveToHistoryV4]
);
// ========================================
// v3: 디바이스/모드 전환
// ========================================
const handleDeviceChange = useCallback((device: DeviceType) => {
setActiveDevice(device);
setActiveModeKey(device === "tablet" ? "tablet_landscape" : "mobile_landscape");
}, []);
const handleModeKeyChange = useCallback((modeKey: PopLayoutModeKey) => {
setActiveModeKey(modeKey);
}, []);
// ========================================
// 뒤로가기
// ========================================
const handleBack = useCallback(() => {
if (hasChanges) {
if (confirm("저장하지 않은 변경사항이 있습니다. 나가시겠습니까?")) {
onBackToList();
}
} else {
onBackToList();
}
}, [hasChanges, onBackToList]);
// ========================================
// 단축키 처리 (Delete, Undo, Redo)
// ========================================
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
return;
}
const key = e.key.toLowerCase();
const isCtrlOrCmd = e.ctrlKey || e.metaKey;
// Delete / Backspace: 컴포넌트 삭제
if (e.key === "Delete" || e.key === "Backspace") {
e.preventDefault();
if (selectedComponentId) {
layoutMode === "v3" ? handleDeleteComponentV3(selectedComponentId) : handleDeleteComponentV4(selectedComponentId);
}
}
// Ctrl+Z / Cmd+Z: Undo (Shift 안 눌림)
if (isCtrlOrCmd && key === "z" && !e.shiftKey) {
e.preventDefault();
console.log("Undo 시도:", { canUndo, layoutMode });
if (canUndo) {
handleUndo();
setHasChanges(true);
toast.success("실행 취소됨");
} else {
toast.info("실행 취소할 내용이 없습니다");
}
return;
}
// Ctrl+Shift+Z / Cmd+Shift+Z: Redo
if (isCtrlOrCmd && key === "z" && e.shiftKey) {
e.preventDefault();
console.log("Redo 시도:", { canRedo, layoutMode });
if (canRedo) {
handleRedo();
setHasChanges(true);
toast.success("다시 실행됨");
} else {
toast.info("다시 실행할 내용이 없습니다");
}
return;
}
// Ctrl+Y / Cmd+Y: Redo (대체)
if (isCtrlOrCmd && key === "y") {
e.preventDefault();
if (canRedo) {
handleRedo();
setHasChanges(true);
toast.success("다시 실행됨");
}
return;
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedComponentId, layoutMode, handleDeleteComponentV3, handleDeleteComponentV4, canUndo, canRedo, handleUndo, handleRedo]);
// ========================================
// 로딩
// ========================================
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-muted-foreground"> ...</div>
</div>
);
}
// ========================================
// 렌더링
// ========================================
return (
<DndProvider backend={HTML5Backend}>
<div className="flex h-screen flex-col bg-background">
{/* 툴바 */}
<div className="flex h-12 items-center justify-between border-b px-4">
{/* 왼쪽: 뒤로가기 + 화면명 */}
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="mr-1 h-4 w-4" />
</Button>
<span className="text-sm font-medium">
{selectedScreen?.screenName || "POP 화면"}
</span>
{hasChanges && <span className="text-xs text-orange-500">*</span>}
</div>
{/* 중앙: 레이아웃 버전 + v3 디바이스 전환 */}
<div className="flex items-center gap-4">
<span className="text-muted-foreground text-xs">
{layoutMode === "v4" ? "자동 레이아웃 (v4)" : "4모드 레이아웃 (v3)"}
</span>
{layoutMode === "v3" && (
<Tabs value={activeDevice} onValueChange={(v) => handleDeviceChange(v as DeviceType)}>
<TabsList className="h-8">
<TabsTrigger value="tablet" className="h-7 px-3 text-xs">
<Tablet className="mr-1 h-3.5 w-3.5" />
릿
</TabsTrigger>
<TabsTrigger value="mobile" className="h-7 px-3 text-xs">
<Smartphone className="mr-1 h-3.5 w-3.5" />
</TabsTrigger>
</TabsList>
</Tabs>
)}
</div>
{/* 오른쪽: Undo/Redo + 저장 */}
<div className="flex items-center gap-2">
{/* Undo/Redo 버튼 */}
<div className="flex items-center gap-1 border-r pr-2 mr-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {
handleUndo();
setHasChanges(true);
toast.success("실행 취소됨");
}}
disabled={!canUndo}
title="실행 취소 (Ctrl+Z)"
>
<Undo2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {
handleRedo();
setHasChanges(true);
toast.success("다시 실행됨");
}}
disabled={!canRedo}
title="다시 실행 (Ctrl+Shift+Z)"
>
<Redo2 className="h-4 w-4" />
</Button>
</div>
{/* 저장 버튼 */}
<Button size="sm" onClick={handleSave} disabled={isSaving || !hasChanges}>
<Save className="mr-1 h-4 w-4" />
{isSaving ? "저장 중..." : "저장"}
</Button>
</div>
</div>
{/* 메인 영역 */}
<ResizablePanelGroup direction="horizontal" className="flex-1">
{/* 왼쪽: 컴포넌트 패널 */}
<ResizablePanel defaultSize={20} minSize={15} maxSize={30} className="border-r">
{layoutMode === "v3" ? (
<PopPanel
layout={layoutV3}
activeModeKey={activeModeKey}
selectedComponentId={selectedComponentId}
selectedComponent={selectedComponentV3}
onUpdateComponentDefinition={handleUpdateComponentDefinitionV3}
onDeleteComponent={handleDeleteComponentV3}
activeDevice={activeDevice}
/>
) : (
<ComponentPaletteV4 />
)}
</ResizablePanel>
<ResizableHandle withHandle />
{/* 중앙: 캔버스 */}
<ResizablePanel defaultSize={layoutMode === "v3" ? 80 : 60}>
{layoutMode === "v3" ? (
<PopCanvas
layout={layoutV3}
activeDevice={activeDevice}
activeModeKey={activeModeKey}
onModeKeyChange={handleModeKeyChange}
selectedComponentId={selectedComponentId}
onSelectComponent={setSelectedComponentId}
onUpdateComponentPosition={handleUpdateComponentPositionV3}
onDropComponent={handleDropComponentV3}
onDeleteComponent={handleDeleteComponentV3}
/>
) : (
<PopCanvasV4
layout={layoutV4}
selectedComponentId={selectedComponentId}
selectedContainerId={selectedContainerId}
currentMode={currentViewportMode}
tempLayout={tempLayout}
onModeChange={setCurrentViewportMode}
onSelectComponent={setSelectedComponentId}
onSelectContainer={setSelectedContainerId}
onDropComponent={handleDropComponentV4}
onUpdateComponent={handleUpdateComponentV4}
onUpdateContainer={handleUpdateContainerV4}
onDeleteComponent={handleDeleteComponentV4}
onResizeComponent={handleResizeComponentV4}
onReorderComponent={handleReorderComponentV4}
onLockLayout={handleLockLayoutV4}
onResetOverride={handleResetOverrideV4}
/>
)}
</ResizablePanel>
{/* 오른쪽: 속성 패널 (v4만) */}
{layoutMode === "v4" && (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
<ComponentEditorPanelV4
component={selectedComponentV4}
container={selectedContainer}
currentViewportMode={currentViewportMode}
onUpdateComponent={
selectedComponentId
? (updates) => handleUpdateComponentV4(selectedComponentId, updates)
: undefined
}
onUpdateContainer={
selectedContainerId
? (updates) => handleUpdateContainerV4(selectedContainerId, updates)
: undefined
}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</div>
</DndProvider>
);
}