"use client";
/**
* V2Hierarchy
*
* 통합 계층 구조 컴포넌트
* - 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 { V2HierarchyProps, HierarchyNode } from "@/types/v2-components";
import {
ChevronRight,
ChevronDown,
Folder,
FolderOpen,
File,
Plus,
Minus,
GripVertical,
User,
Users,
Building,
} from "lucide-react";
/**
* 트리 노드 컴포넌트
*/
const TreeNode = forwardRef<
HTMLDivElement,
{
node: HierarchyNode;
level: number;
maxLevel?: number;
selectedNode?: HierarchyNode;
onSelect?: (node: HierarchyNode) => 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<
HTMLDivElement,
{
data: HierarchyNode[];
selectedNode?: HierarchyNode;
onNodeSelect?: (node: HierarchyNode) => 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<
HTMLDivElement,
{
data: HierarchyNode[];
selectedNode?: HierarchyNode;
onNodeSelect?: (node: HierarchyNode) => 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<
HTMLDivElement,
{
data: HierarchyNode[];
selectedNode?: HierarchyNode;
onNodeSelect?: (node: HierarchyNode) => 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<
HTMLDivElement,
{
data: HierarchyNode[];
selectedNode?: HierarchyNode;
onNodeSelect?: (node: HierarchyNode) => 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";
/**
* 메인 V2Hierarchy 컴포넌트
*/
export const V2Hierarchy = 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;
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
return (
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
{showLabel && (
)}
{renderHierarchy()}
);
});
V2Hierarchy.displayName = "V2Hierarchy";
export default V2Hierarchy;