"use client"; import { useCallback, useMemo, useRef } from "react"; import { useDrop } from "react-dnd"; import GridLayout, { Layout } from "react-grid-layout"; import { cn } from "@/lib/utils"; import { PopLayoutData, PopSectionData, PopComponentData, PopComponentType, GridPosition, } from "./types/pop-layout"; import { DND_ITEM_TYPES, DragItemSection, DragItemComponent } from "./panels/PopPanel"; import { GripVertical, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SectionGrid } from "./SectionGrid"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; type DeviceType = "mobile" | "tablet"; // 디바이스별 캔버스 크기 (dp) const DEVICE_SIZES = { mobile: { portrait: { width: 360, height: 640 }, landscape: { width: 640, height: 360 }, }, tablet: { portrait: { width: 768, height: 1024 }, landscape: { width: 1024, height: 768 }, }, } as const; interface PopCanvasProps { layout: PopLayoutData; activeDevice: DeviceType; showBothDevices: boolean; isLandscape: boolean; selectedSectionId: string | null; selectedComponentId: string | null; onSelectSection: (id: string | null) => void; onSelectComponent: (id: string | null) => void; onUpdateSection: (id: string, updates: Partial) => void; onDeleteSection: (id: string) => void; onLayoutChange: (sections: PopSectionData[]) => void; onDropSection: (gridPosition: GridPosition) => void; onDropComponent: (sectionId: string, type: PopComponentType, gridPosition: GridPosition) => void; onUpdateComponent: (sectionId: string, componentId: string, updates: Partial) => void; onDeleteComponent: (sectionId: string, componentId: string) => void; } export function PopCanvas({ layout, activeDevice, showBothDevices, isLandscape, selectedSectionId, selectedComponentId, onSelectSection, onSelectComponent, onUpdateSection, onDeleteSection, onLayoutChange, onDropSection, onDropComponent, onUpdateComponent, onDeleteComponent, }: PopCanvasProps) { const { canvasGrid, sections } = layout; // GridLayout용 레이아웃 변환 const gridLayoutItems: Layout[] = useMemo(() => { return sections.map((section) => ({ i: section.id, x: section.grid.col - 1, y: section.grid.row - 1, w: section.grid.colSpan, h: section.grid.rowSpan, minW: 2, // 최소 너비 2칸 minH: 1, // 최소 높이 1행 (20px) - 헤더만 보임 })); }, [sections]); // 드래그/리사이즈 완료 핸들러 (onDragStop, onResizeStop 사용) const handleDragResizeStop = useCallback( (layout: Layout[], oldItem: Layout, newItem: Layout) => { const section = sections.find((s) => s.id === newItem.i); if (!section) return; const newGrid: GridPosition = { col: newItem.x + 1, row: newItem.y + 1, colSpan: newItem.w, rowSpan: newItem.h, }; // 변경된 경우에만 업데이트 if ( section.grid.col !== newGrid.col || section.grid.row !== newGrid.row || section.grid.colSpan !== newGrid.colSpan || section.grid.rowSpan !== newGrid.rowSpan ) { const updatedSections = sections.map((s) => s.id === newItem.i ? { ...s, grid: newGrid } : s ); onLayoutChange(updatedSections); } }, [sections, onLayoutChange] ); // 디바이스 프레임 렌더링 const renderDeviceFrame = (device: DeviceType) => { const orientation = isLandscape ? "landscape" : "portrait"; const size = DEVICE_SIZES[device][orientation]; const isActive = device === activeDevice; const cols = canvasGrid.columns; const rowHeight = canvasGrid.rowHeight; const margin: [number, number] = [canvasGrid.gap, canvasGrid.gap]; const sizeLabel = `${size.width}x${size.height}`; const deviceLabel = device === "mobile" ? `모바일 (${sizeLabel})` : `태블릿 (${sizeLabel})`; return (
{/* 디바이스 라벨 */}
{deviceLabel}
{/* 드롭 영역 */}
); }; return (
{showBothDevices ? ( <> {renderDeviceFrame("tablet")} {renderDeviceFrame("mobile")} ) : ( renderDeviceFrame(activeDevice) )}
); } // 캔버스 드롭 영역 interface CanvasDropZoneProps { device: DeviceType; isActive: boolean; size: { width: number; height: number }; cols: number; rowHeight: number; margin: [number, number]; sections: PopSectionData[]; gridLayoutItems: Layout[]; selectedSectionId: string | null; selectedComponentId: string | null; onSelectSection: (id: string | null) => void; onSelectComponent: (id: string | null) => void; onDragResizeStop: (layout: Layout[], oldItem: Layout, newItem: Layout) => void; onDropSection: (gridPosition: GridPosition) => void; onDropComponent: (sectionId: string, type: PopComponentType, gridPosition: GridPosition) => void; onDeleteSection: (id: string) => void; onUpdateComponent: (sectionId: string, componentId: string, updates: Partial) => void; onDeleteComponent: (sectionId: string, componentId: string) => void; } function CanvasDropZone({ device, isActive, size, cols, rowHeight, margin, sections, gridLayoutItems, selectedSectionId, selectedComponentId, onSelectSection, onSelectComponent, onDragResizeStop, onDropSection, onDropComponent, onDeleteSection, onUpdateComponent, onDeleteComponent, }: CanvasDropZoneProps) { const dropRef = useRef(null); // 섹션 드롭 핸들러 const [{ isOver, canDrop }, drop] = useDrop(() => ({ accept: DND_ITEM_TYPES.SECTION, drop: (item: DragItemSection, monitor) => { if (!isActive) return; // 드롭 위치 계산 const clientOffset = monitor.getClientOffset(); if (!clientOffset || !dropRef.current) return; const dropRect = dropRef.current.getBoundingClientRect(); const x = clientOffset.x - dropRect.left; const y = clientOffset.y - dropRect.top; // 그리드 위치 계산 const colWidth = (size.width - 16) / cols; const col = Math.max(1, Math.min(cols, Math.floor(x / colWidth) + 1)); const row = Math.max(1, Math.floor(y / rowHeight) + 1); onDropSection({ col, row, colSpan: 3, // 기본 너비 rowSpan: 4, // 기본 높이 (20px * 4 = 80px) }); }, canDrop: () => isActive, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }), [isActive, size, cols, rowHeight, onDropSection]); // ref 결합 drop(dropRef); return (
{ if (e.target === e.currentTarget) { onSelectSection(null); onSelectComponent(null); } }} > {sections.length > 0 ? ( {sections.map((section) => (
{ e.stopPropagation(); onSelectSection(section.id); }} > {/* 섹션 헤더 - 고정 높이 */}
{section.label || `섹션`}
{selectedSectionId === section.id && ( )}
{/* 섹션 내부 - 나머지 영역 전부 차지 */}
))}
) : (
{isOver && canDrop ? "여기에 섹션을 놓으세요" : "왼쪽 패널에서 섹션을 드래그하세요"}
)}
); }