"use client"; /** * UnifiedHierarchy * * 통합 계층 구조 컴포넌트 * - tree: 트리 뷰 * - org: 조직도 * - bom: BOM 구조 * - cascading: 연쇄 드롭다운 */ import React, { forwardRef, useCallback, useState } from "react"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { UnifiedHierarchyProps, HierarchyNode } from "@/types/unified-components"; import { ChevronRight, ChevronDown, Folder, FolderOpen, File, Plus, Minus, GripVertical, User, Users, Building } from "lucide-react"; /** * 트리 노드 컴포넌트 */ const TreeNode = forwardRef void; editable?: boolean; draggable?: boolean; showQty?: boolean; className?: string; }>(({ node, level, maxLevel, selectedNode, onSelect, editable, draggable, showQty, className }, ref) => { const [isOpen, setIsOpen] = useState(level < 2); const hasChildren = node.children && node.children.length > 0; const isSelected = selectedNode?.id === node.id; // 최대 레벨 제한 if (maxLevel && level >= maxLevel) { return null; } return (
onSelect?.(node)} > {/* 드래그 핸들 */} {draggable && ( )} {/* 확장/축소 아이콘 */} {hasChildren ? ( e.stopPropagation()}> ) : ( )} {/* 폴더/파일 아이콘 */} {hasChildren ? ( isOpen ? ( ) : ( ) ) : ( )} {/* 라벨 */} {node.label} {/* 수량 (BOM용) */} {showQty && node.data?.qty && ( x{String(node.data.qty)} )} {/* 편집 버튼 */} {editable && (
)}
{/* 자식 노드 */} {hasChildren && ( {node.children!.map((child) => ( ))} )}
); }); TreeNode.displayName = "TreeNode"; /** * 트리 뷰 컴포넌트 */ const TreeView = forwardRef void; editable?: boolean; draggable?: boolean; maxLevel?: number; className?: string; }>(({ data, selectedNode, onNodeSelect, editable, draggable, maxLevel, className }, ref) => { return (
{data.length === 0 ? (
데이터가 없습니다
) : ( data.map((node) => ( )) )}
); }); TreeView.displayName = "TreeView"; /** * 조직도 뷰 컴포넌트 */ const OrgView = forwardRef void; className?: string; }>(({ data, selectedNode, onNodeSelect, className }, ref) => { const renderOrgNode = (node: HierarchyNode, isRoot = false) => { const isSelected = selectedNode?.id === node.id; const hasChildren = node.children && node.children.length > 0; return (
{/* 노드 카드 */}
onNodeSelect?.(node)} >
{isRoot ? ( ) : hasChildren ? ( ) : ( )}
{node.label}
{node.data?.title && (
{String(node.data.title)}
)}
{/* 자식 노드 */} {hasChildren && ( <> {/* 연결선 */}
{node.children!.map((child, index) => ( {index > 0 &&
} {renderOrgNode(child)} ))}
)}
); }; return (
{data.length === 0 ? (
조직 데이터가 없습니다
) : (
{data.map((node) => renderOrgNode(node, true))}
)}
); }); OrgView.displayName = "OrgView"; /** * BOM 뷰 컴포넌트 (수량 포함 트리) */ const BomView = forwardRef void; editable?: boolean; className?: string; }>(({ data, selectedNode, onNodeSelect, editable, className }, ref) => { return (
{data.length === 0 ? (
BOM 데이터가 없습니다
) : ( data.map((node) => ( )) )}
); }); BomView.displayName = "BomView"; /** * 연쇄 드롭다운 컴포넌트 */ const CascadingView = forwardRef void; maxLevel?: number; className?: string; }>(({ data, selectedNode, onNodeSelect, maxLevel = 3, className }, ref) => { const [selections, setSelections] = useState([]); // 레벨별 옵션 가져오기 const getOptionsForLevel = (level: number): HierarchyNode[] => { if (level === 0) return data; let currentNodes = data; for (let i = 0; i < level; i++) { const selectedId = selections[i]; if (!selectedId) return []; const selectedNode = currentNodes.find((n) => n.id === selectedId); if (!selectedNode?.children) return []; currentNodes = selectedNode.children; } return currentNodes; }; // 선택 핸들러 const handleSelect = (level: number, nodeId: string) => { const newSelections = [...selections.slice(0, level), nodeId]; setSelections(newSelections); // 마지막 선택된 노드 찾기 let node = data.find((n) => n.id === newSelections[0]); for (let i = 1; i < newSelections.length; i++) { node = node?.children?.find((n) => n.id === newSelections[i]); } if (node) { onNodeSelect?.(node); } }; return (
{Array.from({ length: maxLevel }, (_, level) => { const options = getOptionsForLevel(level); const isDisabled = level > 0 && !selections[level - 1]; return ( ); })}
); }); CascadingView.displayName = "CascadingView"; /** * 메인 UnifiedHierarchy 컴포넌트 */ export const UnifiedHierarchy = forwardRef( (props, ref) => { const { id, label, required, style, size, config: configProp, data = [], selectedNode, onNodeSelect, } = props; // config가 없으면 기본값 사용 const config = configProp || { type: "tree" as const, viewMode: "tree" as const, dataSource: "static" as const }; // 뷰모드별 렌더링 const renderHierarchy = () => { const viewMode = config.viewMode || config.type || "tree"; switch (viewMode) { case "tree": return ( ); case "org": return ( ); case "bom": return ( ); case "dropdown": case "cascading": return ( ); default: return ( ); } }; const showLabel = label && style?.labelDisplay !== false; const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; return (
{showLabel && ( )}
{renderHierarchy()}
); } ); UnifiedHierarchy.displayName = "UnifiedHierarchy"; export default UnifiedHierarchy;