"use client"; import React, { useState, useRef, useEffect } from "react"; import { X, GripVertical } from "lucide-react"; import { cn } from "@/lib/utils"; interface FloatingPanelProps { id: string; title: string; children: React.ReactNode; isOpen: boolean; onClose: () => void; position?: "left" | "right" | "top" | "bottom"; width?: number; height?: number; minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number; resizable?: boolean; draggable?: boolean; autoHeight?: boolean; // 자동 높이 조정 옵션 className?: string; } export const FloatingPanel: React.FC = ({ id, title, children, isOpen, onClose, position = "right", width = 320, height = 400, minWidth = 280, minHeight = 300, maxWidth = 600, maxHeight = 1200, // 800 → 1200 (더 큰 패널 지원) resizable = true, draggable = true, autoHeight = true, // 자동 높이 조정 활성화 (컨텐츠 크기에 맞게) className, }) => { const [panelSize, setPanelSize] = useState({ width, height }); // props 변경 시 패널 크기 업데이트 useEffect(() => { setPanelSize({ width, height }); }, [width, height]); const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const panelRef = useRef(null); const dragHandleRef = useRef(null); const contentRef = useRef(null); // 초기 위치 설정 (패널이 처음 열릴 때만) const [hasInitialized, setHasInitialized] = useState(false); useEffect(() => { if (isOpen && !hasInitialized && panelRef.current) { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let initialX = 0; let initialY = 0; switch (position) { case "left": initialX = 20; initialY = 80; break; case "right": initialX = viewportWidth - panelSize.width - 20; initialY = 80; break; case "top": initialX = (viewportWidth - panelSize.width) / 2; initialY = 20; break; case "bottom": initialX = (viewportWidth - panelSize.width) / 2; initialY = viewportHeight - panelSize.height - 20; break; } setPanelPosition({ x: initialX, y: initialY }); setHasInitialized(true); } // 패널이 닫힐 때 초기화 상태 리셋 if (!isOpen) { setHasInitialized(false); } }, [isOpen, position, hasInitialized]); // 자동 높이 조정 기능 useEffect(() => { if (!autoHeight || !contentRef.current || isResizing) return; const updateHeight = () => { if (!contentRef.current) return; // 일시적으로 높이 제한을 해제하여 실제 컨텐츠 높이 측정 contentRef.current.style.maxHeight = "none"; // 컨텐츠의 실제 높이 측정 const contentHeight = contentRef.current.scrollHeight; const headerHeight = 60; // 헤더 높이 const padding = 30; // 여유 공간 (좀 더 넉넉하게) const newHeight = Math.min(Math.max(minHeight, contentHeight + headerHeight + padding), maxHeight); // console.log(`🔧 패널 높이 자동 조정:`, { // panelId: id, // contentHeight, // calculatedHeight: newHeight, // currentHeight: panelSize.height, // willUpdate: Math.abs(panelSize.height - newHeight) > 10, // }); // 현재 높이와 다르면 업데이트 if (Math.abs(panelSize.height - newHeight) > 10) { setPanelSize((prev) => ({ ...prev, height: newHeight })); } }; // 초기 높이 설정 updateHeight(); // ResizeObserver로 컨텐츠 크기 변화 감지 const resizeObserver = new ResizeObserver((entries) => { // DOM 업데이트가 완료된 후에 높이 측정 requestAnimationFrame(() => { setTimeout(updateHeight, 50); // 약간의 지연으로 렌더링 완료 후 측정 }); }); resizeObserver.observe(contentRef.current); return () => { resizeObserver.disconnect(); }; }, [autoHeight, minHeight, maxHeight, isResizing, panelSize.height, children]); // 드래그 시작 - 성능 최적화 const handleDragStart = (e: React.MouseEvent) => { if (!draggable) return; e.preventDefault(); // 기본 동작 방지로 딜레이 제거 e.stopPropagation(); // 이벤트 버블링 방지 setIsDragging(true); const rect = panelRef.current?.getBoundingClientRect(); if (rect) { setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top, }); } }; // 리사이즈 시작 const handleResizeStart = (e: React.MouseEvent) => { if (!resizable) return; e.preventDefault(); setIsResizing(true); }; // 마우스 이동 처리 - 초고속 최적화 useEffect(() => { if (!isDragging && !isResizing) return; const handleMouseMove = (e: MouseEvent) => { if (isDragging) { // 직접 DOM 조작으로 최고 성능 if (panelRef.current) { const newX = e.clientX - dragOffset.x; const newY = e.clientY - dragOffset.y; panelRef.current.style.left = `${newX}px`; panelRef.current.style.top = `${newY}px`; // 상태는 throttle로 업데이트 setPanelPosition({ x: newX, y: newY }); } } else if (isResizing) { const newWidth = Math.max(minWidth, Math.min(maxWidth, e.clientX - panelPosition.x)); const newHeight = Math.max(minHeight, Math.min(maxHeight, e.clientY - panelPosition.y)); if (panelRef.current) { panelRef.current.style.width = `${newWidth}px`; panelRef.current.style.height = `${newHeight}px`; } setPanelSize({ width: newWidth, height: newHeight }); } }; const handleMouseUp = () => { setIsDragging(false); setIsResizing(false); }; // 고성능 이벤트 리스너 document.addEventListener("mousemove", handleMouseMove, { passive: true, capture: false, }); document.addEventListener("mouseup", handleMouseUp, { passive: true, capture: false, }); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging, isResizing, dragOffset.x, dragOffset.y, panelPosition.x, minWidth, maxWidth, minHeight, maxHeight]); if (!isOpen) return null; return (
{/* 헤더 */}

{title}

{/* 컨텐츠 */}
{children}
{/* 리사이즈 핸들 */} {resizable && !autoHeight && (
)}
); }; export default FloatingPanel;