"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; /** 좌측 패널 기본 너비 (%, 기본: 25) */ leftWidth?: number; /** 좌측 패널 최소 너비 (%, 기본: 10) */ minLeftWidth?: number; /** 좌측 패널 최대 너비 (%, 기본: 50) */ maxLeftWidth?: number; /** 리사이저 표시 여부 (기본: true) */ showResizer?: boolean; /** 모바일에서 좌측 패널 기본 접힘 여부 (기본: true) */ collapsedOnMobile?: boolean; /** 컨테이너 높이 (기본: "100%") */ height?: string; /** 추가 className */ className?: string; /** 좌측 패널 추가 className */ leftClassName?: string; /** 우측 패널 추가 className */ rightClassName?: string; } const MOBILE_BREAKPOINT = 1024; export function ResponsiveSplitPanel({ left, right, leftTitle = "목록", leftWidth: initialLeftWidth = 25, minLeftWidth = 10, maxLeftWidth = 50, showResizer = true, collapsedOnMobile = true, height = "100%", className, leftClassName, rightClassName, }: ResponsiveSplitPanelProps) { const [leftWidth, setLeftWidth] = useState(initialLeftWidth); const [isMobileView, setIsMobileView] = useState(false); const [leftCollapsed, setLeftCollapsed] = useState(false); const containerRef = useRef(null); const isDraggingRef = useRef(false); // 뷰포트 감지 useEffect(() => { const checkMobile = () => { const mobile = window.innerWidth < MOBILE_BREAKPOINT; setIsMobileView(mobile); if (mobile && collapsedOnMobile) { setLeftCollapsed(true); } }; checkMobile(); window.addEventListener("resize", checkMobile); return () => window.removeEventListener("resize", checkMobile); }, [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) { setLeftWidth(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]); // --- 모바일 레이아웃: 세로 스택 --- if (isMobileView) { return (
{/* 좌측 패널 토글 헤더 */} {/* 좌측 패널 (접기/펼치기) */} {!leftCollapsed && (
{left}
)} {/* 우측 패널 (항상 표시) */}
{right}
); } // --- 데스크톱 레이아웃: 좌우 분할 --- return (
{/* 좌측 패널 (접기 가능) */} {!leftCollapsed ? ( <>
{left}
{/* 리사이저 */} {showResizer && (
)} ) : (
)} {/* 우측 패널 */}
{/* 데스크톱 접기 버튼 */} {!leftCollapsed && (
)}
{right}
); } export default ResponsiveSplitPanel;