"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { ChevronRight, ChevronDown, Package, Layers, Box, AlertCircle } from "lucide-react"; import { cn } from "@/lib/utils"; import { entityJoinApi } from "@/lib/api/entityJoin"; /** * BOM 트리 노드 데이터 */ interface BomTreeNode { id: string; bom_id: string; parent_detail_id: string | null; seq_no: string; level: string; child_item_id: string; child_item_code: string; child_item_name: string; child_item_type: string; quantity: string; unit: string; loss_rate: string; remark: string; children: BomTreeNode[]; } /** * BOM 헤더 정보 */ interface BomHeaderInfo { id: string; bom_number: string; item_code: string; item_name: string; item_type: string; base_qty: string; unit: string; version: string; revision: string; status: string; effective_date: string; expired_date: string; remark: string; } interface BomTreeComponentProps { component?: any; formData?: Record; tableName?: string; companyCode?: string; isDesignMode?: boolean; selectedRowsData?: any[]; [key: string]: any; } /** * BOM 트리 컴포넌트 * 좌측 패널에서 BOM 헤더 선택 시 계층 구조로 BOM 디테일을 표시 */ export function BomTreeComponent({ component, formData, companyCode, isDesignMode = false, selectedRowsData, ...props }: BomTreeComponentProps) { const [headerInfo, setHeaderInfo] = useState(null); const [treeData, setTreeData] = useState([]); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [loading, setLoading] = useState(false); const [selectedNodeId, setSelectedNodeId] = useState(null); const config = component?.componentConfig || {}; // 선택된 BOM 헤더에서 bom_id 추출 const selectedBomId = useMemo(() => { // SplitPanel에서 좌측 선택 시 formData나 selectedRowsData로 전달됨 if (selectedRowsData && selectedRowsData.length > 0) { return selectedRowsData[0]?.id; } if (formData?.id) return formData.id; return null; }, [formData, selectedRowsData]); // 선택된 BOM 헤더 정보 추출 const selectedHeaderData = useMemo(() => { if (selectedRowsData && selectedRowsData.length > 0) { return selectedRowsData[0] as BomHeaderInfo; } if (formData?.id) return formData as unknown as BomHeaderInfo; return null; }, [formData, selectedRowsData]); // BOM 디테일 데이터 로드 const loadBomDetails = useCallback(async (bomId: string) => { if (!bomId) return; setLoading(true); try { const result = await entityJoinApi.getTableDataWithJoins("bom_detail", { page: 1, size: 500, search: { bom_id: bomId }, sortBy: "seq_no", sortOrder: "asc", enableEntityJoin: true, }); const rows = result.data || []; const tree = buildTree(rows); setTreeData(tree); const firstLevelIds = new Set(tree.map((n: BomTreeNode) => n.id)); setExpandedNodes(firstLevelIds); } catch (error) { console.error("[BomTree] 데이터 로드 실패:", error); } finally { setLoading(false); } }, []); // 평면 데이터 -> 트리 구조 변환 const buildTree = (flatData: any[]): BomTreeNode[] => { const nodeMap = new Map(); const roots: BomTreeNode[] = []; // 모든 노드를 맵에 등록 flatData.forEach((item) => { nodeMap.set(item.id, { ...item, children: [] }); }); // 부모-자식 관계 설정 flatData.forEach((item) => { const node = nodeMap.get(item.id)!; if (item.parent_detail_id && nodeMap.has(item.parent_detail_id)) { nodeMap.get(item.parent_detail_id)!.children.push(node); } else { roots.push(node); } }); return roots; }; // 선택된 BOM 변경 시 데이터 로드 useEffect(() => { if (selectedBomId) { setHeaderInfo(selectedHeaderData); loadBomDetails(selectedBomId); } else { setHeaderInfo(null); setTreeData([]); } }, [selectedBomId, selectedHeaderData, loadBomDetails]); // 노드 펼치기/접기 토글 const toggleNode = useCallback((nodeId: string) => { setExpandedNodes((prev) => { const next = new Set(prev); if (next.has(nodeId)) { next.delete(nodeId); } else { next.add(nodeId); } return next; }); }, []); // 전체 펼치기 const expandAll = useCallback(() => { const allIds = new Set(); const collectIds = (nodes: BomTreeNode[]) => { nodes.forEach((n) => { allIds.add(n.id); if (n.children.length > 0) collectIds(n.children); }); }; collectIds(treeData); setExpandedNodes(allIds); }, [treeData]); // 전체 접기 const collapseAll = useCallback(() => { setExpandedNodes(new Set()); }, []); // 품목 구분 라벨 const getItemTypeLabel = (type: string) => { switch (type) { case "product": return "제품"; case "semi": return "반제품"; case "material": return "원자재"; case "part": return "부품"; default: return type || "-"; } }; // 품목 구분 아이콘 & 색상 const getItemTypeStyle = (type: string) => { switch (type) { case "product": return { icon: Package, color: "text-blue-600", bg: "bg-blue-50" }; case "semi": return { icon: Layers, color: "text-amber-600", bg: "bg-amber-50" }; case "material": return { icon: Box, color: "text-emerald-600", bg: "bg-emerald-50" }; default: return { icon: Box, color: "text-gray-500", bg: "bg-gray-50" }; } }; // 디자인 모드 미리보기 if (isDesignMode) { return (
BOM 트리 뷰
완제품 A (제품) 수량: 1
반제품 B (반제품) 수량: 2
원자재 C (원자재) 수량: 5
); } // 선택 안 된 상태 if (!selectedBomId) { return (

좌측에서 BOM을 선택하세요

선택한 BOM의 구성 정보가 트리로 표시됩니다

); } return (
{/* 헤더 정보 */} {headerInfo && (

{headerInfo.item_name || "-"}

{headerInfo.bom_number || "-"} {headerInfo.status === "active" ? "사용" : "미사용"}
품목코드: {headerInfo.item_code || "-"} 구분: {getItemTypeLabel(headerInfo.item_type)} 기준수량: {headerInfo.base_qty || "1"} {headerInfo.unit || ""} 버전: v{headerInfo.version || "1.0"} (차수 {headerInfo.revision || "1"})
)} {/* 트리 툴바 */}
BOM 구성 {treeData.length}건
{/* 트리 컨텐츠 */}
{loading ? (
로딩 중...
) : treeData.length === 0 ? (

등록된 하위 품목이 없습니다

) : (
{treeData.map((node) => ( ))}
)}
); } /** * 트리 노드 행 (재귀 렌더링) */ interface TreeNodeRowProps { node: BomTreeNode; depth: number; expandedNodes: Set; selectedNodeId: string | null; onToggle: (id: string) => void; onSelect: (id: string) => void; getItemTypeLabel: (type: string) => string; getItemTypeStyle: (type: string) => { icon: any; color: string; bg: string }; } function TreeNodeRow({ node, depth, expandedNodes, selectedNodeId, onToggle, onSelect, getItemTypeLabel, getItemTypeStyle, }: TreeNodeRowProps) { const isExpanded = expandedNodes.has(node.id); const hasChildren = node.children.length > 0; const isSelected = selectedNodeId === node.id; const style = getItemTypeStyle(node.child_item_type); const ItemIcon = style.icon; return ( <>
{ onSelect(node.id); if (hasChildren) onToggle(node.id); }} > {/* 펼치기/접기 화살표 */} {hasChildren ? ( isExpanded ? ( ) : ( ) ) : ( )} {/* 품목 타입 아이콘 */} {/* 품목 정보 */}
{node.child_item_name || "-"} {node.child_item_code || ""} {getItemTypeLabel(node.child_item_type)}
{/* 수량/단위 */}
수량: {node.quantity || "0"} {node.unit || ""} {node.loss_rate && node.loss_rate !== "0" && ( 로스: {node.loss_rate}% )}
{/* 하위 노드 재귀 렌더링 */} {hasChildren && isExpanded && (
{node.children.map((child) => ( ))}
)} ); }