"use client"; import React, { useState, useRef, useCallback, useEffect, ReactNode } from "react"; import { GripVertical, ChevronDown, ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface ResponsiveSplitPanelProps { left: ReactNode; right: ReactNode; leftTitle?: string; leftWidth?: number; minLeftWidth?: number; maxLeftWidth?: number; showResizer?: boolean; collapsedOnMobile?: boolean; height?: string; className?: string; leftClassName?: string; rightClassName?: string; } type ViewMode = "desktop" | "tablet" | "mobile"; const DESKTOP_BREAKPOINT = 1280; const TABLET_BREAKPOINT = 768; function getViewMode(width: number): ViewMode { if (width >= DESKTOP_BREAKPOINT) return "desktop"; if (width >= TABLET_BREAKPOINT) return "tablet"; return "mobile"; } function getAdaptiveLeftWidth( baseWidth: number, containerWidth: number, minPx: number ): number { const currentPx = (baseWidth / 100) * containerWidth; if (currentPx < minPx) { return Math.min((minPx / containerWidth) * 100, 50); } return baseWidth; } export function ResponsiveSplitPanel({ left, right, leftTitle = "목록", leftWidth: initialLeftWidth = 25, minLeftWidth = 10, maxLeftWidth = 50, showResizer = true, collapsedOnMobile = true, height = "100%", className, leftClassName, rightClassName, }: ResponsiveSplitPanelProps) { const [userLeftWidth, setUserLeftWidth] = useState(initialLeftWidth); const [viewMode, setViewMode] = useState("desktop"); const [leftCollapsed, setLeftCollapsed] = useState(false); const [containerWidth, setContainerWidth] = useState(0); const containerRef = useRef(null); const isDraggingRef = useRef(false); const prevViewModeRef = useRef("desktop"); // 컨테이너 크기 + 뷰 모드 실시간 감지 useEffect(() => { const container = containerRef.current; if (!container) return; const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (!entry) return; const w = entry.contentRect.width; setContainerWidth(w); const newMode = getViewMode(window.innerWidth); setViewMode((prev) => { if (prev !== newMode) { if (newMode === "mobile" && collapsedOnMobile) { setLeftCollapsed(true); } else if (newMode === "tablet") { setLeftCollapsed(true); } else if (newMode === "desktop" && prev !== "desktop") { setLeftCollapsed(false); } } return newMode; }); }); observer.observe(container); setContainerWidth(container.offsetWidth); const initialMode = getViewMode(window.innerWidth); setViewMode(initialMode); if (initialMode === "mobile" && collapsedOnMobile) { setLeftCollapsed(true); } else if (initialMode === "tablet") { setLeftCollapsed(true); } return () => observer.disconnect(); }, [collapsedOnMobile]); // window resize도 감지 (viewport 변화) useEffect(() => { const handleResize = () => { const newMode = getViewMode(window.innerWidth); setViewMode((prev) => { if (prev !== newMode) { if (newMode === "mobile" && collapsedOnMobile) { setLeftCollapsed(true); } else if (newMode === "tablet") { setLeftCollapsed(true); } else if (newMode === "desktop") { setLeftCollapsed(false); } } return newMode; }); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [collapsedOnMobile]); // 리사이저 const handleMouseDown = useCallback(() => { isDraggingRef.current = true; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }, []); const handleMouseMove = useCallback( (e: MouseEvent) => { if (!isDraggingRef.current || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const pct = ((e.clientX - rect.left) / rect.width) * 100; if (pct >= minLeftWidth && pct <= maxLeftWidth) { setUserLeftWidth(pct); } }, [minLeftWidth, maxLeftWidth] ); const handleMouseUp = useCallback(() => { isDraggingRef.current = false; document.body.style.cursor = ""; document.body.style.userSelect = ""; }, []); useEffect(() => { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [handleMouseMove, handleMouseUp]); // 좌측 패널 최소 px 보장 (200px 이하로 줄어들지 않게) const MIN_LEFT_PX = 200; const effectiveLeftWidth = containerWidth > 0 ? getAdaptiveLeftWidth(userLeftWidth, containerWidth, MIN_LEFT_PX) : userLeftWidth; // --- 모바일: 세로 스택 --- if (viewMode === "mobile") { return (
{!leftCollapsed && (
{left}
)}
{right}
); } // --- 태블릿 + 데스크톱: 좌우 분할 --- return (
{/* 좌측 패널 */} {!leftCollapsed ? ( <>
{left}
{/* 리사이저 (데스크톱만) */} {showResizer && viewMode === "desktop" && (
)} ) : (
)} {/* 우측 패널 */}
{!leftCollapsed && (
)}
{right}
); } export default ResponsiveSplitPanel;