"use client"; import { useCallback, useMemo, useRef, useState, useEffect } from "react"; import { useDrop } from "react-dnd"; import GridLayout, { Layout } from "react-grid-layout"; import { cn } from "@/lib/utils"; import { PopSectionData, PopComponentData, PopComponentType, GridPosition, } from "./types/pop-layout"; import { DND_ITEM_TYPES, DragItemComponent } from "./panels/PopPanel"; import { Trash2, Move } from "lucide-react"; import { Button } from "@/components/ui/button"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; interface SectionGridProps { section: PopSectionData; isActive: boolean; selectedComponentId: string | null; onSelectComponent: (id: string | null) => 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 SectionGrid({ section, isActive, selectedComponentId, onSelectComponent, onDropComponent, onUpdateComponent, onDeleteComponent, }: SectionGridProps) { const containerRef = useRef(null); const { components } = section; // 컨테이너 크기 측정 const [containerSize, setContainerSize] = useState({ width: 300, height: 200 }); useEffect(() => { const updateSize = () => { if (containerRef.current) { setContainerSize({ width: containerRef.current.offsetWidth, height: containerRef.current.offsetHeight, }); } }; updateSize(); const resizeObserver = new ResizeObserver(updateSize); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => resizeObserver.disconnect(); }, []); // 셀 크기 계산 - 고정 셀 크기 기반으로 자동 계산 const padding = 8; // p-2 = 8px const gap = 4; // 고정 간격 const availableWidth = containerSize.width - padding * 2; const availableHeight = containerSize.height - padding * 2; // 고정 셀 크기 (40px) 기반으로 열/행 수 자동 계산 const CELL_SIZE = 40; const cols = Math.max(1, Math.floor((availableWidth + gap) / (CELL_SIZE + gap))); const rows = Math.max(1, Math.floor((availableHeight + gap) / (CELL_SIZE + gap))); const cellHeight = CELL_SIZE; // GridLayout용 레이아웃 변환 (자동 계산된 cols/rows 사용) const gridLayoutItems: Layout[] = useMemo(() => { return components.map((comp) => { // 컴포넌트 위치가 그리드 범위를 벗어나지 않도록 조정 const x = Math.min(Math.max(0, comp.grid.col - 1), Math.max(0, cols - 1)); const y = Math.min(Math.max(0, comp.grid.row - 1), Math.max(0, rows - 1)); // colSpan/rowSpan도 범위 제한 const w = Math.min(Math.max(1, comp.grid.colSpan), Math.max(1, cols - x)); const h = Math.min(Math.max(1, comp.grid.rowSpan), Math.max(1, rows - y)); return { i: comp.id, x, y, w, h, minW: 1, minH: 1, }; }); }, [components, cols, rows]); // 드래그/리사이즈 완료 핸들러 (onDragStop, onResizeStop 사용) const handleDragStop = useCallback( (layout: Layout[], oldItem: Layout, newItem: Layout) => { const comp = components.find((c) => c.id === newItem.i); if (!comp) return; const newGrid: GridPosition = { col: newItem.x + 1, row: newItem.y + 1, colSpan: newItem.w, rowSpan: newItem.h, }; if ( comp.grid.col !== newGrid.col || comp.grid.row !== newGrid.row ) { onUpdateComponent(section.id, comp.id, { grid: newGrid }); } }, [components, section.id, onUpdateComponent] ); const handleResizeStop = useCallback( (layout: Layout[], oldItem: Layout, newItem: Layout) => { const comp = components.find((c) => c.id === newItem.i); if (!comp) return; const newGrid: GridPosition = { col: newItem.x + 1, row: newItem.y + 1, colSpan: newItem.w, rowSpan: newItem.h, }; if ( comp.grid.colSpan !== newGrid.colSpan || comp.grid.rowSpan !== newGrid.rowSpan ) { onUpdateComponent(section.id, comp.id, { grid: newGrid }); } }, [components, section.id, onUpdateComponent] ); // 빈 셀 찾기 (드롭 위치용) - 자동 계산된 cols/rows 사용 const findEmptyCell = useCallback((): GridPosition => { const occupied = new Set(); components.forEach((comp) => { for (let c = comp.grid.col; c < comp.grid.col + comp.grid.colSpan; c++) { for (let r = comp.grid.row; r < comp.grid.row + comp.grid.rowSpan; r++) { occupied.add(`${c}-${r}`); } } }); // 빈 셀 찾기 for (let r = 1; r <= rows; r++) { for (let c = 1; c <= cols; c++) { if (!occupied.has(`${c}-${r}`)) { return { col: c, row: r, colSpan: 1, rowSpan: 1 }; } } } // 빈 셀 없으면 첫 번째 위치에 return { col: 1, row: 1, colSpan: 1, rowSpan: 1 }; }, [components, cols, rows]); // 컴포넌트 드롭 핸들러 const [{ isOver, canDrop }, drop] = useDrop(() => ({ accept: DND_ITEM_TYPES.COMPONENT, drop: (item: DragItemComponent) => { if (!isActive) return; const emptyCell = findEmptyCell(); onDropComponent(section.id, item.componentType, emptyCell); return { dropped: true }; }, canDrop: () => isActive, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }), [isActive, section.id, findEmptyCell, onDropComponent]); return (
{ containerRef.current = node; drop(node); }} className={cn( "relative h-full w-full p-2 transition-colors", isOver && canDrop && "bg-blue-50" )} onClick={(e) => { e.stopPropagation(); onSelectComponent(null); }} onMouseDown={(e) => { e.stopPropagation(); }} > {/* 빈 상태 안내 텍스트 */} {components.length === 0 && (
{isOver && canDrop ? "여기에 놓으세요" : "컴포넌트를 드래그하세요"}
)} {/* 컴포넌트 GridLayout */} {components.length > 0 && availableWidth > 0 && cols > 0 && ( {components.map((comp) => (
{ e.stopPropagation(); onSelectComponent(comp.id); }} onMouseDown={(e) => e.stopPropagation()} > {/* 드래그 핸들 바 */}
{/* 컴포넌트 내용 */}
{/* 삭제 버튼 */} {selectedComponentId === comp.id && ( )}
))}
)}
); } // 컴포넌트 미리보기 interface ComponentPreviewProps { component: PopComponentData; } function ComponentPreview({ component }: ComponentPreviewProps) { const { type, label } = component; // 타입별 미리보기 렌더링 const renderPreview = () => { switch (type) { case "pop-field": return (
{label || "필드"}
); case "pop-button": return (
{label || "버튼"}
); case "pop-list": return (
{label || "리스트"}
); case "pop-indicator": return (
{label || "KPI"} 0
); case "pop-scanner": return (
QR
{label || "스캐너"}
); case "pop-numpad": return (
{[1, 2, 3, 4, 5, 6, 7, 8, 9, "C", 0, "OK"].map((key) => (
{key}
))}
); default: return {label || type}; } }; return
{renderPreview()}
; }