"use client"; import { useState, useCallback, useEffect } from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { ArrowLeft, Save, Undo2, Redo2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { toast } from "sonner"; import PopCanvas from "./PopCanvas"; import ComponentEditorPanel from "./panels/ComponentEditorPanel"; import ComponentPalette from "./panels/ComponentPalette"; import { PopLayoutDataV5, PopComponentType, PopComponentDefinitionV5, PopGridPosition, GridMode, createEmptyPopLayoutV5, isV5Layout, addComponentToV5Layout, } from "./types/pop-layout"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition } from "@/types/screen"; // ======================================== // Props // ======================================== interface PopDesignerProps { selectedScreen: ScreenDefinition; onBackToList: () => void; onScreenUpdate?: (updatedScreen: Partial) => void; } // ======================================== // 메인 컴포넌트 (v5 그리드 시스템 전용) // ======================================== export default function PopDesigner({ selectedScreen, onBackToList, onScreenUpdate, }: PopDesignerProps) { // ======================================== // 레이아웃 상태 // ======================================== const [layout, setLayout] = useState(createEmptyPopLayoutV5()); // 히스토리 const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // UI 상태 const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [idCounter, setIdCounter] = useState(1); // 선택 상태 const [selectedComponentId, setSelectedComponentId] = useState(null); // 그리드 모드 (4개 프리셋) const [currentMode, setCurrentMode] = useState("tablet_landscape"); // 선택된 컴포넌트 const selectedComponent: PopComponentDefinitionV5 | null = selectedComponentId ? layout.components[selectedComponentId] || null : null; // ======================================== // 히스토리 관리 // ======================================== const saveToHistory = useCallback((newLayout: PopLayoutDataV5) => { setHistory((prev) => { const newHistory = prev.slice(0, historyIndex + 1); newHistory.push(JSON.parse(JSON.stringify(newLayout))); // 최대 50개 유지 if (newHistory.length > 50) { newHistory.shift(); return newHistory; } return newHistory; }); setHistoryIndex((prev) => Math.min(prev + 1, 49)); }, [historyIndex]); const undo = useCallback(() => { if (historyIndex > 0) { const newIndex = historyIndex - 1; const previousLayout = history[newIndex]; if (previousLayout) { setLayout(JSON.parse(JSON.stringify(previousLayout))); setHistoryIndex(newIndex); setHasChanges(true); toast.success("실행 취소됨"); } } }, [historyIndex, history]); const redo = useCallback(() => { if (historyIndex < history.length - 1) { const newIndex = historyIndex + 1; const nextLayout = history[newIndex]; if (nextLayout) { setLayout(JSON.parse(JSON.stringify(nextLayout))); setHistoryIndex(newIndex); setHasChanges(true); toast.success("다시 실행됨"); } } }, [historyIndex, history]); const canUndo = historyIndex > 0; const canRedo = historyIndex < history.length - 1; // ======================================== // 레이아웃 로드 // ======================================== useEffect(() => { const loadLayout = async () => { if (!selectedScreen?.screenId) return; setIsLoading(true); try { const loadedLayout = await screenApi.getLayoutPop(selectedScreen.screenId); if (loadedLayout && isV5Layout(loadedLayout) && Object.keys(loadedLayout.components).length > 0) { // v5 레이아웃 로드 setLayout(loadedLayout); setHistory([loadedLayout]); setHistoryIndex(0); console.log(`POP 레이아웃 로드: ${Object.keys(loadedLayout.components).length}개 컴포넌트`); } else { // 새 화면 또는 빈 레이아웃 const emptyLayout = createEmptyPopLayoutV5(); setLayout(emptyLayout); setHistory([emptyLayout]); setHistoryIndex(0); console.log("새 POP 화면 생성 (v5 그리드)"); } } catch (error) { console.error("레이아웃 로드 실패:", error); toast.error("레이아웃을 불러오는데 실패했습니다"); const emptyLayout = createEmptyPopLayoutV5(); setLayout(emptyLayout); setHistory([emptyLayout]); setHistoryIndex(0); } finally { setIsLoading(false); } }; loadLayout(); }, [selectedScreen?.screenId]); // ======================================== // 저장 // ======================================== const handleSave = useCallback(async () => { if (!selectedScreen?.screenId) return; setIsSaving(true); try { await screenApi.saveLayoutPop(selectedScreen.screenId, layout); toast.success("저장되었습니다"); setHasChanges(false); } catch (error) { console.error("저장 실패:", error); toast.error("저장에 실패했습니다"); } finally { setIsSaving(false); } }, [selectedScreen?.screenId, layout]); // ======================================== // 컴포넌트 핸들러 // ======================================== const handleDropComponent = useCallback( (type: PopComponentType, position: PopGridPosition) => { const componentId = `comp_${idCounter}`; setIdCounter((prev) => prev + 1); const newLayout = addComponentToV5Layout(layout, componentId, type, position, `${type} ${idCounter}`); setLayout(newLayout); saveToHistory(newLayout); setSelectedComponentId(componentId); setHasChanges(true); }, [idCounter, layout, saveToHistory] ); const handleUpdateComponent = useCallback( (componentId: string, updates: Partial) => { const existingComponent = layout.components[componentId]; if (!existingComponent) return; const newLayout = { ...layout, components: { ...layout.components, [componentId]: { ...existingComponent, ...updates, }, }, }; setLayout(newLayout); saveToHistory(newLayout); setHasChanges(true); }, [layout, saveToHistory] ); const handleDeleteComponent = useCallback( (componentId: string) => { const newComponents = { ...layout.components }; delete newComponents[componentId]; const newLayout = { ...layout, components: newComponents, }; setLayout(newLayout); saveToHistory(newLayout); setSelectedComponentId(null); setHasChanges(true); }, [layout, saveToHistory] ); // ======================================== // 뒤로가기 // ======================================== const handleBack = useCallback(() => { if (hasChanges) { if (confirm("저장하지 않은 변경사항이 있습니다. 정말 나가시겠습니까?")) { onBackToList(); } } else { onBackToList(); } }, [hasChanges, onBackToList]); // ======================================== // 단축키 처리 // ======================================== 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) { handleDeleteComponent(selectedComponentId); } } // Ctrl+Z: Undo if (isCtrlOrCmd && key === "z" && !e.shiftKey) { e.preventDefault(); if (canUndo) undo(); return; } // Ctrl+Shift+Z or Ctrl+Y: Redo if ((isCtrlOrCmd && key === "z" && e.shiftKey) || (isCtrlOrCmd && key === "y")) { e.preventDefault(); if (canRedo) redo(); return; } // Ctrl+S: 저장 if (isCtrlOrCmd && key === "s") { e.preventDefault(); handleSave(); return; } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedComponentId, handleDeleteComponent, canUndo, canRedo, undo, redo, handleSave]); // ======================================== // 로딩 // ======================================== if (isLoading) { return (
로딩 중...
); } // ======================================== // 렌더링 // ======================================== return (
{/* 헤더 */}
{/* 왼쪽: 뒤로가기 + 화면명 */}

{selectedScreen?.screenName}

그리드 레이아웃 (v5)

{/* 오른쪽: Undo/Redo + 저장 */}
{/* Undo/Redo 버튼 */}
{/* 저장 버튼 */}
{/* 메인 영역 */} {/* 왼쪽: 컴포넌트 팔레트 */} {/* 중앙: 캔버스 */} {/* 오른쪽: 속성 패널 */} handleUpdateComponent(selectedComponentId, updates) : undefined } />
); }