"use client"; import React, { useState, useCallback, useEffect } from "react"; import { ComponentRendererProps } from "../../types"; import { SplitPanelLayoutConfig } from "./types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Plus, Search, GripVertical, Loader2 } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { useToast } from "@/hooks/use-toast"; export interface SplitPanelLayoutComponentProps extends ComponentRendererProps { // 추가 props } /** * SplitPanelLayout 컴포넌트 * 마스터-디테일 패턴의 좌우 분할 레이아웃 */ export const SplitPanelLayoutComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, onClick, ...props }) => { const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig; // 기본 설정값 const splitRatio = componentConfig.splitRatio || 30; const resizable = componentConfig.resizable ?? true; const minLeftWidth = componentConfig.minLeftWidth || 200; const minRightWidth = componentConfig.minRightWidth || 300; // 데이터 상태 const [leftData, setLeftData] = useState([]); const [rightData, setRightData] = useState(null); const [selectedLeftItem, setSelectedLeftItem] = useState(null); const [leftSearchQuery, setLeftSearchQuery] = useState(""); const [rightSearchQuery, setRightSearchQuery] = useState(""); const [isLoadingLeft, setIsLoadingLeft] = useState(false); const [isLoadingRight, setIsLoadingRight] = useState(false); const { toast } = useToast(); // 리사이저 드래그 상태 const [isDragging, setIsDragging] = useState(false); const [leftWidth, setLeftWidth] = useState(splitRatio); // 컴포넌트 스타일 const componentStyle: React.CSSProperties = { position: "absolute", left: `${component.style?.positionX || 0}px`, top: `${component.style?.positionY || 0}px`, width: `${component.style?.width || 1000}px`, height: `${component.style?.height || 600}px`, zIndex: component.style?.positionZ || 1, cursor: isDesignMode ? "pointer" : "default", border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb", }; // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; if (!leftTableName || isDesignMode) return; setIsLoadingLeft(true); try { const result = await dataApi.getTableData(leftTableName, { page: 1, size: 100, searchTerm: leftSearchQuery || undefined, }); setLeftData(result.data); } catch (error) { console.error("좌측 데이터 로드 실패:", error); toast({ title: "데이터 로드 실패", description: "좌측 패널 데이터를 불러올 수 없습니다.", variant: "destructive", }); } finally { setIsLoadingLeft(false); } }, [componentConfig.leftPanel?.tableName, leftSearchQuery, isDesignMode, toast]); // 우측 데이터 로드 const loadRightData = useCallback( async (leftItem: any) => { const relationshipType = componentConfig.rightPanel?.relation?.type || "detail"; const rightTableName = componentConfig.rightPanel?.tableName; if (!rightTableName || isDesignMode) return; setIsLoadingRight(true); try { if (relationshipType === "detail") { // 상세 모드: 동일 테이블의 상세 정보 const primaryKey = leftItem.id || leftItem.ID || Object.values(leftItem)[0]; const detail = await dataApi.getRecordDetail(rightTableName, primaryKey); setRightData(detail); } else if (relationshipType === "join") { // 조인 모드: 다른 테이블의 관련 데이터 const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; const rightColumn = componentConfig.rightPanel?.relation?.foreignKey; const leftTable = componentConfig.leftPanel?.tableName; if (leftColumn && rightColumn && leftTable) { const leftValue = leftItem[leftColumn]; const joinedData = await dataApi.getJoinedData( leftTable, rightTableName, leftColumn, rightColumn, leftValue, ); setRightData(joinedData[0] || null); // 첫 번째 관련 레코드 } } else { // 커스텀 모드: 상세 정보로 폴백 const primaryKey = leftItem.id || leftItem.ID || Object.values(leftItem)[0]; const detail = await dataApi.getRecordDetail(rightTableName, primaryKey); setRightData(detail); } } catch (error) { console.error("우측 데이터 로드 실패:", error); toast({ title: "데이터 로드 실패", description: "우측 패널 데이터를 불러올 수 없습니다.", variant: "destructive", }); } finally { setIsLoadingRight(false); } }, [ componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.relation, componentConfig.leftPanel?.tableName, isDesignMode, toast, ], ); // 좌측 항목 선택 핸들러 const handleLeftItemSelect = useCallback( (item: any) => { setSelectedLeftItem(item); loadRightData(item); }, [loadRightData], ); // 초기 데이터 로드 useEffect(() => { if (!isDesignMode && componentConfig.autoLoad !== false) { loadLeftData(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDesignMode, componentConfig.autoLoad]); // 검색어 변경 시 재로드 useEffect(() => { if (!isDesignMode && leftSearchQuery) { const timer = setTimeout(() => { loadLeftData(); }, 300); // 디바운스 return () => clearTimeout(timer); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [leftSearchQuery, isDesignMode]); // 리사이저 드래그 핸들러 const handleMouseDown = (e: React.MouseEvent) => { if (!resizable) return; setIsDragging(true); e.preventDefault(); }; const handleMouseMove = useCallback( (e: MouseEvent) => { if (!isDragging) return; const containerWidth = (e.currentTarget as HTMLElement)?.offsetWidth || 1000; const newLeftWidth = (e.clientX / containerWidth) * 100; if (newLeftWidth > 20 && newLeftWidth < 80) { setLeftWidth(newLeftWidth); } }, [isDragging], ); const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); React.useEffect(() => { if (isDragging) { document.addEventListener("mousemove", handleMouseMove as any); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove as any); document.removeEventListener("mouseup", handleMouseUp); }; } }, [isDragging, handleMouseMove, handleMouseUp]); return (
{ if (isDesignMode) { e.stopPropagation(); onClick?.(e); } }} className="flex overflow-hidden rounded-lg bg-white shadow-sm" > {/* 좌측 패널 */}
{componentConfig.leftPanel?.title || "좌측 패널"} {componentConfig.leftPanel?.showAdd && ( )}
{componentConfig.leftPanel?.showSearch && (
setLeftSearchQuery(e.target.value)} className="pl-9" />
)}
{/* 좌측 데이터 목록 */}
{isDesignMode ? ( // 디자인 모드: 샘플 데이터 <>
handleLeftItemSelect({ id: 1, name: "항목 1" })} className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${ selectedLeftItem?.id === 1 ? "bg-blue-50 text-blue-700" : "text-gray-700" }`} >
항목 1
설명 텍스트
handleLeftItemSelect({ id: 2, name: "항목 2" })} className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${ selectedLeftItem?.id === 2 ? "bg-blue-50 text-blue-700" : "text-gray-700" }`} >
항목 2
설명 텍스트
handleLeftItemSelect({ id: 3, name: "항목 3" })} className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${ selectedLeftItem?.id === 3 ? "bg-blue-50 text-blue-700" : "text-gray-700" }`} >
항목 3
설명 텍스트
) : isLoadingLeft ? ( // 로딩 중
데이터를 불러오는 중...
) : leftData.length > 0 ? ( // 실제 데이터 표시 leftData.map((item, index) => { const itemId = item.id || item.ID || item[Object.keys(item)[0]] || index; const isSelected = selectedLeftItem && (selectedLeftItem.id === itemId || selectedLeftItem === item); // 첫 번째 2-3개 필드를 표시 const keys = Object.keys(item).filter((k) => k !== "id" && k !== "ID"); const displayTitle = item[keys[0]] || item.name || item.title || `항목 ${index + 1}`; const displaySubtitle = keys[1] ? item[keys[1]] : null; return (
handleLeftItemSelect(item)} className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${ isSelected ? "bg-blue-50 text-blue-700" : "text-gray-700" }`} >
{displayTitle}
{displaySubtitle &&
{displaySubtitle}
}
); }) ) : ( // 데이터 없음
데이터가 없습니다.
)}
{/* 리사이저 */} {resizable && (
)} {/* 우측 패널 */}
{componentConfig.rightPanel?.title || "우측 패널"} {componentConfig.rightPanel?.showAdd && ( )}
{componentConfig.rightPanel?.showSearch && (
setRightSearchQuery(e.target.value)} className="pl-9" />
)}
{/* 우측 상세 데이터 */} {isLoadingRight ? ( // 로딩 중

상세 정보를 불러오는 중...

) : rightData ? ( // 실제 데이터 표시
{Object.entries(rightData).map(([key, value]) => { // null, undefined, 빈 문자열 제외 if (value === null || value === undefined || value === "") return null; return (
{key}
{String(value)}
); })}
) : selectedLeftItem && isDesignMode ? ( // 디자인 모드: 샘플 데이터

{selectedLeftItem.name} 상세 정보

항목 1: 값 1
항목 2: 값 2
항목 3: 값 3
) : ( // 선택 없음

좌측에서 항목을 선택하세요

선택한 항목의 상세 정보가 여기에 표시됩니다

)}
); }; /** * SplitPanelLayout 래퍼 컴포넌트 */ export const SplitPanelLayoutWrapper: React.FC = (props) => { return ; };