"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { ChevronRight, ChevronDown, Package, Layers, Box, AlertCircle, Expand, Shrink, Loader2, History, GitBranch, Check, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { BomDetailEditModal } from "./BomDetailEditModal"; import { BomHistoryModal } from "./BomHistoryModal"; import { BomVersionModal } from "./BomVersionModal"; interface BomTreeNode { id: string; [key: string]: any; children: BomTreeNode[]; } interface BomHeaderInfo { id: string; [key: string]: any; } interface TreeColumnDef { key: string; title: string; width?: string; visible?: boolean; hidden?: boolean; isSourceDisplay?: boolean; } interface BomTreeComponentProps { component?: any; formData?: Record; tableName?: string; companyCode?: string; isDesignMode?: boolean; selectedRowsData?: any[]; [key: string]: any; } // 컬럼은 설정 패널에서만 추가 (하드코딩 금지) const EMPTY_COLUMNS: TreeColumnDef[] = []; const INDENT_PX = 16; 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 [viewMode, setViewMode] = useState<"tree" | "level">("tree"); const [editModalOpen, setEditModalOpen] = useState(false); const [editTargetNode, setEditTargetNode] = useState(null); const [historyModalOpen, setHistoryModalOpen] = useState(false); const [versionModalOpen, setVersionModalOpen] = useState(false); const [colWidths, setColWidths] = useState>({}); const handleResizeStart = useCallback((colKey: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const startX = e.clientX; const th = (e.target as HTMLElement).closest("th"); const startWidth = th?.offsetWidth || 100; const onMove = (ev: MouseEvent) => { setColWidths((prev) => ({ ...prev, [colKey]: Math.max(40, startWidth + (ev.clientX - startX)) })); }; const onUp = () => { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); }, []); const config = component?.componentConfig || {}; const overrides = component?.overrides || {}; const selectedBomId = useMemo(() => { if (selectedRowsData && selectedRowsData.length > 0) return selectedRowsData[0]?.id; if (formData?.id) return formData.id; return null; }, [formData, selectedRowsData]); const selectedHeaderData = useMemo(() => { const raw = selectedRowsData?.[0] || (formData?.id ? formData : null); if (!raw) return null; return { ...raw, item_name: raw.item_id_item_name || raw.item_name || "", item_code: raw.item_id_item_number || raw.item_code || "", item_type: raw.item_id_division || raw.item_id_type || raw.item_type || "", } as BomHeaderInfo; }, [formData, selectedRowsData]); const detailTable = overrides.detailTable || config.detailTable || "bom_detail"; const foreignKey = overrides.foreignKey || config.foreignKey || "bom_id"; const parentKey = overrides.parentKey || config.parentKey || "parent_detail_id"; const sourceFk = config.dataSource?.foreignKey || "child_item_id"; const historyTable = config.historyTable || "bom_history"; const versionTable = config.versionTable || "bom_version"; const displayColumns = useMemo(() => { const configured = config.columns as TreeColumnDef[] | undefined; if (configured && configured.length > 0) return configured.filter((c) => !c.hidden); return EMPTY_COLUMNS; }, [config.columns]); const features = config.features || {}; const showHistory = features.showHistory !== false; const showVersion = features.showVersion !== false; // 카테고리 라벨 캐시 (process_type 등) const [categoryLabels, setCategoryLabels] = useState>>({}); useEffect(() => { const loadLabels = async () => { try { const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values`); const vals = res.data?.data || []; if (vals.length > 0) { const map: Record = {}; vals.forEach((v: any) => { map[v.value_code] = v.value_label; }); setCategoryLabels((prev) => ({ ...prev, process_type: map })); } } catch { /* 무시 */ } }; loadLabels(); }, [detailTable]); // ─── 데이터 로드 ─── // BOM 헤더 데이터로 가상 0레벨 루트 노드 생성 const buildVirtualRoot = useCallback((headerData: BomHeaderInfo | null, children: BomTreeNode[]): BomTreeNode | null => { if (!headerData) return null; return { id: `__root_${headerData.id}`, _isVirtualRoot: true, level: "0", child_item_name: headerData.item_name || "", child_item_code: headerData.item_code || headerData.bom_number || "", child_item_type: headerData.item_type || "", item_name: headerData.item_name || "", item_number: headerData.item_code || "", quantity: "-", base_qty: headerData.base_qty || "", unit: headerData.unit || "", revision: headerData.revision || "", loss_rate: "", process_type: "", remark: headerData.remark || "", children, }; }, []); const loadBomDetails = useCallback(async (bomId: string, headerData: BomHeaderInfo | null) => { if (!bomId) return; setLoading(true); try { const searchFilter: Record = { [foreignKey]: bomId }; let versionId = headerData?.current_version_id; // version_id가 없으면 서버에서 자동 초기화 if (!versionId) { try { const initRes = await apiClient.post(`/bom/${bomId}/initialize-version`); if (initRes.data?.success && initRes.data.data?.versionId) { versionId = initRes.data.data.versionId; } } catch { /* 무시 */ } } if (versionId) { searchFilter.version_id = versionId; } // autoFilter 비활성화: BOM 전용 API로 company_code 관리하므로 autoFilter 불필요 const res = await apiClient.get(`/table-management/tables/${detailTable}/data-with-joins`, { params: { page: 1, size: 500, search: JSON.stringify(searchFilter), sortBy: "seq_no", sortOrder: "asc", enableEntityJoin: true, autoFilter: JSON.stringify({ enabled: false }), }, }); const rawData = res.data?.data?.data || res.data?.data || []; const rows = (Array.isArray(rawData) ? rawData : []).map((row: Record) => { const mapped = { ...row }; for (const key of Object.keys(row)) { if (key.startsWith(`${sourceFk}_`)) { const shortKey = key.replace(`${sourceFk}_`, ""); const aliasKey = `child_${shortKey}`; if (!mapped[aliasKey]) mapped[aliasKey] = row[key]; if (!mapped[shortKey]) mapped[shortKey] = row[key]; } } mapped.child_item_name = row[`${sourceFk}_item_name`] || row.child_item_name || ""; mapped.child_item_code = row[`${sourceFk}_item_number`] || row.child_item_code || ""; mapped.child_item_type = row[`${sourceFk}_type`] || row[`${sourceFk}_division`] || row.child_item_type || ""; return mapped; }); const detailTree = buildTree(rows); const virtualRoot = buildVirtualRoot(headerData, detailTree); if (virtualRoot) { setTreeData([virtualRoot]); setExpandedNodes(new Set([virtualRoot.id])); } else { setTreeData(detailTree); const firstLevelIds = new Set(detailTree.map((n: BomTreeNode) => n.id)); setExpandedNodes(firstLevelIds); } } catch (error) { console.error("[BomTree] 데이터 로드 실패:", error); } finally { setLoading(false); } }, [detailTable, foreignKey, sourceFk, buildVirtualRoot]); 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[parentKey] && nodeMap.has(item[parentKey])) { nodeMap.get(item[parentKey])!.children.push(node); } else { roots.push(node); } }); return roots; }; // BOM 전용 API로 최신 current_version_id 조회 (company_code 필터 무관) const fetchCurrentVersionId = useCallback(async (bomId: string): Promise => { try { const res = await apiClient.get(`/bom/${bomId}/versions`); if (res.data?.success) { if (res.data.currentVersionId) return res.data.currentVersionId; const activeVersion = res.data.data?.find((v: any) => v.status === "active"); return activeVersion?.id || null; } } catch (e) { console.error("[BomTree] active 버전 조회 실패:", e); } return null; }, []); // BOM 전용 헤더 API로 최신 데이터 조회 (autoFilter 영향 없음) const fetchBomHeader = useCallback(async (bomId: string): Promise => { try { const res = await apiClient.get(`/bom/${bomId}/header`); if (res.data?.success && res.data.data) { const raw = res.data.data; return { ...raw, id: raw.id, item_name: raw.item_name || "", item_code: raw.item_number || raw.item_code || "", item_type: raw.item_type || raw.division || "", unit: raw.unit || raw.item_unit || "", } as BomHeaderInfo; } } catch (e) { console.error("[BomTree] BOM 헤더 API 조회 실패:", e); } return null; }, []); // BOM 선택 시 전용 API로 헤더 + 디테일 로드 const loadingBomIdRef = React.useRef(null); useEffect(() => { if (!selectedBomId) { setHeaderInfo(null); setTreeData([]); loadingBomIdRef.current = null; return; } // 현재 요청 ID로 stale 응답 필터링 (React StrictMode 호환) const requestId = selectedBomId; loadingBomIdRef.current = requestId; const load = async () => { let header = await fetchBomHeader(requestId); if (!header && selectedHeaderData) { header = { ...selectedHeaderData, id: requestId } as BomHeaderInfo; const freshVersionId = await fetchCurrentVersionId(requestId); if (freshVersionId) header.current_version_id = freshVersionId; } // stale 응답 무시: 다른 BOM이 선택됐거나 useEffect가 다시 실행된 경우 if (loadingBomIdRef.current !== requestId || !header) return; setHeaderInfo(header); loadBomDetails(requestId, header); }; load(); }, [selectedBomId, selectedHeaderData, loadBomDetails, fetchBomHeader, fetchCurrentVersionId]); // refreshTable 이벤트 수신 시 BOM 헤더 + 디테일 최신 데이터로 갱신 useEffect(() => { const handleRefresh = async () => { if (!selectedBomId) return; try { let header = await fetchBomHeader(selectedBomId); if (!header && headerInfo) { // API 실패 시 현재 headerInfo + 최신 version_id로 fallback const freshVersionId = await fetchCurrentVersionId(selectedBomId); header = { ...headerInfo, current_version_id: freshVersionId || headerInfo.current_version_id }; } if (header) { setHeaderInfo(header); loadBomDetails(selectedBomId, header); } } catch (e) { console.error("[BomTree] refreshTable 헤더 갱신 실패:", e); } }; window.addEventListener("refreshTable", handleRefresh); return () => window.removeEventListener("refreshTable", handleRefresh); }, [selectedBomId, loadBomDetails, fetchBomHeader, fetchCurrentVersionId, headerInfo]); // EditModal 열릴 때 editData를 최신 headerInfo로 보정 (버전/마스터 데이터 stale 방지) useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail; if (!detail?.editData || !headerInfo) return; const editId = String(detail.editData.id || ""); const bomId = String(selectedBomId || ""); if (editId !== bomId) return; console.log("[BomTree] openEditModal 가로채기 - editData 보정", { oldVersion: detail.editData.version, newVersion: headerInfo.version, oldCurrentVersionId: detail.editData.current_version_id, newCurrentVersionId: headerInfo.current_version_id, }); // headerInfo의 모든 필드를 editData에 덮어쓰기 (최신 서버 데이터 보장) Object.keys(headerInfo).forEach((key) => { if ((headerInfo as any)[key] !== undefined && (headerInfo as any)[key] !== null) { detail.editData[key] = (headerInfo as any)[key]; } }); // entity join된 필드를 dot notation으로도 매핑 (item_info.xxx 형식) const h = headerInfo as Record; if (h.item_name) detail.editData["item_info.item_name"] = h.item_name; if (h.item_type) detail.editData["item_info.division"] = h.item_type; if (h.item_code || h.item_number) detail.editData["item_info.item_number"] = h.item_code || h.item_number; if (h.unit) detail.editData["item_info.unit"] = h.unit; // entity join alias 형식도 매핑 if (h.item_name) detail.editData["item_id_item_name"] = h.item_name; if (h.item_type) detail.editData["item_id_division"] = h.item_type; if (h.item_code || h.item_number) detail.editData["item_id_item_number"] = h.item_code || h.item_number; if (h.unit) detail.editData["item_id_unit"] = h.unit; }; // capture: true → EditModal 리스너(bubble)보다 반드시 먼저 실행 window.addEventListener("openEditModal", handler, true); return () => window.removeEventListener("openEditModal", handler, true); }, [selectedBomId, headerInfo]); // EditModal 저장 시 version 값을 현재 headerInfo 기준으로 보정 useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail; if (detail?.formData && detail.formData.id === selectedBomId && headerInfo) { if (headerInfo.version && detail.formData.version !== headerInfo.version) { console.log("[BomTree] formData.version 보정:", detail.formData.version, "→", headerInfo.version); detail.formData.version = headerInfo.version; } if (headerInfo.revision && detail.formData.revision !== headerInfo.revision) { detail.formData.revision = headerInfo.revision; } } }; window.addEventListener("beforeFormSave", handler); return () => window.removeEventListener("beforeFormSave", handler); }, [selectedBomId, headerInfo]); 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) => { const map: Record = { product: "제품", semi: "반제품", material: "원자재", part: "부품" }; return map[type] || type || "-"; }; const getItemTypeBadge = (type: string) => { const map: Record = { product: "bg-blue-50 text-blue-600 ring-blue-200", semi: "bg-amber-50 text-amber-600 ring-amber-200", material: "bg-emerald-50 text-emerald-600 ring-emerald-200", part: "bg-purple-50 text-purple-600 ring-purple-200", }; return map[type] || "bg-gray-50 text-gray-500 ring-gray-200"; }; const getItemIcon = (type: string) => { const map: Record = { product: Package, semi: Layers }; return map[type] || Box; }; const getItemIconColor = (type: string) => { const map: Record = { product: "text-blue-500", semi: "text-amber-500", material: "text-emerald-500", part: "text-purple-500", }; return map[type] || "text-gray-400"; }; // ─── 셀 렌더링 ─── const renderCellValue = (node: BomTreeNode, col: TreeColumnDef, depth: number) => { const value = node[col.key]; if (col.key === "child_item_type" || col.key === "item_type") { const label = getItemTypeLabel(String(value || "")); return ( {label} ); } if (col.key === "level") { const displayLevel = node._isVirtualRoot ? 0 : depth; return ( {displayLevel} ); } if (col.key === "child_item_code") { return {value || "-"}; } if (col.key === "child_item_name") { return {value || "-"}; } if (col.key === "status") { const statusMap: Record = { active: "사용", inactive: "미사용", developing: "개발중" }; return {statusMap[String(value)] || value || "-"}; } if (col.key === "quantity" || col.key === "base_qty") { return ( {value != null && value !== "" && value !== "0" ? value : "-"} ); } if (col.key === "process_type" && value) { const label = categoryLabels.process_type?.[String(value)] || String(value); return {label}; } if (col.key === "loss_rate") { const num = Number(value); if (!num) return -; return {value}%; } if (col.key === "revision") { return ( {value != null && value !== "" && value !== "0" ? value : "-"} ); } if (col.key === "unit") { return {value || "-"}; } return {value ?? "-"}; }; // ─── 디자인 모드 ─── if (isDesignMode) { const configuredColumns = (config.columns || []).filter((c: TreeColumnDef) => !c.hidden); const previewSampleValue = (col: TreeColumnDef, rowIdx: number): React.ReactNode => { if (col.key === "level") return rowIdx === 0 ? "0" : "1"; if (col.key.includes("type") || col.key.includes("division")) { const badge = rowIdx === 0 ? { bg: "bg-blue-50 text-blue-500 ring-blue-200", label: "제품" } : { bg: "bg-amber-50 text-amber-500 ring-amber-200", label: "반제품" }; return ( {badge.label} ); } if (col.key.includes("quantity") || col.key.includes("qty")) return rowIdx === 0 ? "30" : "3"; if (col.key.includes("unit")) return "EA"; if (col.key.includes("process")) { const badge = rowIdx === 0 ? { bg: "bg-emerald-50 text-emerald-600 ring-emerald-200", label: "제조" } : { bg: "bg-purple-50 text-purple-600 ring-purple-200", label: "외주" }; return ( {badge.label} ); } return `예시${rowIdx + 1}`; }; return (
{/* 헤더 (실제 화면과 동일 구조) */}

BOM 상세정보

제품
품목코드 SAMPLE-001 기준수량 1
{/* 툴바 (실제 화면과 동일 구조) */}
BOM 구성 2
{showHistory && ( )} {showVersion && ( )}
트리 레벨
{features.showExpandAll !== false && (
)}
{/* 테이블 */} {configuredColumns.length === 0 ? (

컬럼 미설정

설정 패널 > 컬럼 탭에서 표시할 컬럼을 선택하세요

) : (
{configuredColumns.map((col: TreeColumnDef) => { const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no", "unit", "level"].includes(col.key) || col.key.includes("qty") || col.key.includes("quantity"); return ( ); })} {/* 0레벨 루트 */} {configuredColumns.map((col: TreeColumnDef) => { const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no", "unit", "level"].includes(col.key) || col.key.includes("qty") || col.key.includes("quantity"); return ( ); })} {/* 1레벨 자식 */} {configuredColumns.map((col: TreeColumnDef) => { const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no", "unit", "level"].includes(col.key) || col.key.includes("qty") || col.key.includes("quantity"); return ( ); })}
{col.title || col.key}
{previewSampleValue(col, 0)}
{previewSampleValue(col, 1)}
)}
); } // ─── 미선택 상태 ─── if (!selectedBomId) { return (

BOM을 선택해주세요

좌측 목록에서 BOM을 선택하면 구성이 표시됩니다

); } // ─── 트리 평탄화 ─── const flattenedRows = useMemo(() => { const rows: { node: BomTreeNode; depth: number }[] = []; const traverse = (nodes: BomTreeNode[], depth: number) => { for (const node of nodes) { rows.push({ node, depth }); if (node.children.length > 0 && expandedNodes.has(node.id)) { traverse(node.children, depth + 1); } } }; traverse(treeData, 0); return rows; }, [treeData, expandedNodes]); // 레벨 뷰용: 전체 노드 평탄화 (expand 상태 무관) const allFlattenedRows = useMemo(() => { const rows: { node: BomTreeNode; depth: number }[] = []; const traverse = (nodes: BomTreeNode[], depth: number) => { for (const node of nodes) { rows.push({ node, depth }); if (node.children.length > 0) traverse(node.children, depth + 1); } }; traverse(treeData, 0); return rows; }, [treeData]); const maxDepth = useMemo(() => { return allFlattenedRows.reduce((max, r) => Math.max(max, r.depth), 0); }, [allFlattenedRows]); const visibleRows = viewMode === "level" ? allFlattenedRows : flattenedRows; const levelColumnsForView = useMemo(() => { return Array.from({ length: maxDepth + 1 }, (_, i) => i); }, [maxDepth]); // 레벨 뷰에서 "level" 컬럼을 제외한 데이터 컬럼 const dataColumnsForLevelView = useMemo(() => { return displayColumns.filter((c) => c.key !== "level"); }, [displayColumns]); // 트리/레벨 뷰 전환 시 데이터 열 위치 고정을 위한 공통 접두 영역 너비 const prefixAreaWidth = useMemo(() => { const treeIconWidth = Math.max(52, maxDepth * INDENT_PX + 44); const levelColsWidth = (maxDepth + 1) * 30; return Math.max(treeIconWidth, levelColsWidth); }, [maxDepth]); // ─── 메인 렌더링 ─── return (
{/* 헤더 정보 */} {features.showHeader !== false && headerInfo && (

{headerInfo.item_name || "-"}

{getItemTypeLabel(headerInfo.item_type)} {headerInfo.status === "active" ? "사용" : "미사용"}
품목코드 {headerInfo.item_code || "-"} 기준수량 {headerInfo.base_qty || "1"} 버전 v{headerInfo.version || "1"} (차수 {headerInfo.revision || "1"})
)} {/* 툴바 */}
BOM 구성 {allFlattenedRows.length}
{showHistory && ( )} {showVersion && ( )}
{features.showExpandAll !== false && (
)}
{/* 테이블 */}
{loading ? (
) : displayColumns.length === 0 ? (

표시할 컬럼이 설정되지 않았습니다

디자인 모드에서 컬럼을 추가하세요

) : treeData.length === 0 ? (

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

) : viewMode === "level" ? ( /* ═══ 레벨 뷰 ═══ */ {levelColumnsForView.map((lvl) => { const eachWidth = Math.floor(prefixAreaWidth / levelColumnsForView.length); return ; })} {dataColumnsForLevelView.map((col) => ( ))} {levelColumnsForView.map((lvl) => ( ))} {dataColumnsForLevelView.map((col) => { const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no"].includes(col.key); const w = colWidths[col.key]; return ( ); })} {allFlattenedRows.map(({ node, depth }, rowIdx) => { const isRoot = !!node._isVirtualRoot; const displayDepth = isRoot ? 0 : depth; const lvlDepthBg = isRoot ? "border-gray-200 bg-blue-50/50 font-medium hover:bg-blue-50/70" : selectedNodeId === node.id ? "border-gray-100 bg-primary/5" : depth === 1 ? "border-gray-100 bg-white hover:bg-gray-50/60" : depth === 2 ? "border-gray-100 bg-gray-50/40 hover:bg-gray-100/50" : depth >= 3 ? "border-gray-100 bg-gray-100/40 hover:bg-gray-100/60" : "border-gray-100 bg-white hover:bg-gray-50/60"; return ( setSelectedNodeId(node.id)} onDoubleClick={() => { setEditTargetNode(node); setEditModalOpen(true); }} > {levelColumnsForView.map((lvl) => ( ))} {dataColumnsForLevelView.map((col) => { const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no"].includes(col.key); return ( ); })} ); })}
{lvl} {col.title}
handleResizeStart(col.key, e)} />
{displayDepth === lvl ? ( ) : null} {renderCellValue(node, col, depth)}
) : ( /* ═══ 트리 뷰 ═══ */ {displayColumns.map((col) => ( ))} {displayColumns.map((col) => { const centered = ["quantity", "loss_rate", "level", "base_qty", "revision", "seq_no"].includes(col.key); const w = colWidths[col.key]; return ( ); })} {flattenedRows.map(({ node, depth }, rowIdx) => { const hasChildren = node.children.length > 0; const isExpanded = expandedNodes.has(node.id); const isSelected = selectedNodeId === node.id; const isRoot = !!node._isVirtualRoot; const itemType = node.child_item_type || node.item_type || ""; const ItemIcon = getItemIcon(itemType); const depthBg = isRoot ? "border-gray-200 bg-blue-50/50 font-medium hover:bg-blue-50/70" : isSelected ? "border-gray-100 bg-primary/5" : depth === 1 ? "border-gray-100 bg-white hover:bg-gray-50/60" : depth === 2 ? "border-gray-100 bg-gray-50/40 hover:bg-gray-100/50" : depth >= 3 ? "border-gray-100 bg-gray-100/40 hover:bg-gray-100/60" : "border-gray-100 bg-white hover:bg-gray-50/60"; const depthBarColor = isRoot ? "bg-blue-400" : depth === 1 ? "bg-emerald-400" : depth === 2 ? "bg-amber-400" : depth >= 3 ? "bg-purple-400" : "bg-gray-300"; return ( { setSelectedNodeId(node.id); if (hasChildren) toggleNode(node.id); }} onDoubleClick={() => { setEditTargetNode(node); setEditModalOpen(true); }} > {displayColumns.map((col) => { const centered = ["quantity", "loss_rate", "level", "base_qty", "revision", "seq_no"].includes(col.key); return ( ); })} ); })}
{col.title}
handleResizeStart(col.key, e)} />
{hasChildren ? ( isExpanded ? ( ) : ( ) ) : ( )}
{renderCellValue(node, col, depth)}
)}
{/* 품목 수정 모달 */} { if (selectedBomId) loadBomDetails(selectedBomId, headerInfo); }} /> {showHistory && ( )} {showVersion && ( { window.dispatchEvent(new CustomEvent("refreshTable")); }} /> )}
); } export default BomTreeComponent;