개발관리>E-BOM 등록 — BOM 구조 다이얼로그 신설 + 트리 조인 버그 정정
사용자 검증 중 두 가지 문제 발견:
1) 트리가 1레벨만 표시되고 자식들이 안 풀림 — ascending/descending CTE 의 재귀
조인 조건 버그.
잘못된 조인: B.parent_objid = T.objid
올바른 조인: B.parent_objid = T.child_objid (ascending),
B.child_objid = T.parent_objid (descending)
wace relatePartInfo 1:1 패턴 — BOM_PART_QTY INSERT 시 OBJID 와 별도로
createObjId() 따로 발급되는 CHILD_OBJID 가 부모 식별자. 자식 행의 PARENT_OBJID
는 부모 행의 CHILD_OBJID 와 매칭됨 (PARENT_OBJID = T.OBJID 가 아님).
4 함수 일괄 정정 : ascending / descending / ascendingForExcel / descendingForExcel
검증 : test-20003-0082 BOM → Level 1 루트 + Level 2 자식 2건 정상 트리 출력.
2) E-BOM 컬럼이 숫자(bom_cnt)로 표시되어 어디를 눌러 BOM 구조를 봐야 할지 불명확.
운영판 wace 는 fnc_getFolderIcon 으로 폴더 아이콘 + 클릭 → setStructurePopupMainFS.
backend:
- GET /api/development/ebom-tree/full → ascendingForExcel 1:1, 풀 컬럼 JSON 노출
(HEAT_TREATMENT_HARDNESS/METHOD/SURFACE_TREATMENT/MAKER/PART_TYPE_TITLE/CU_파일 카운트 포함)
frontend:
- BomReportTreeDialog.tsx 신설 (wace 4-Frame 팝업 → 단일 다이얼로그 통합)
· 헤더 메타 8필드 (제품구분/품번/품명/Version/상태/등록자/등록일/확정일)
· 동적 LEVEL 컬럼 (L1..LMaxLevel, "*" 표시) + 운영판 14컬럼
· 노란 배경 헤더 (운영판 스타일 1:1)
· 엑셀 다운로드 버튼 (해당 BOM 만 ascendingForExcel 호출)
- lib/api/devBom.ts : treeFull() API + BomTreeFullRow 타입
- app/.../ebom-regist/page.tsx :
· bom_cnt 컬럼 formatNumber → renderType: "folder" (wace fnc_getFolderIcon 1:1)
· 클릭 핸들러를 품번 → E-BOM 폴더 셀로 이동 (운영판 fn_openSetStructure 동작)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,18 @@ export async function ascending(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── E-BOM 트리 보기 (M3 행 클릭 → 다이얼로그) ─────────────
|
||||
// GET /api/development/ebom-tree/full?bom_report_objid=... — ascendingForExcel 1:1, 풀 컬럼 JSON
|
||||
export async function treeFull(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.ascendingForExcel(req.query as svc.BomTreeFilter);
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("E-BOM 트리 조회 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M4 엑셀 다운로드 (정/역전개) ──────────────────────────
|
||||
// GET /api/development/ebom-tree/ascending/excel
|
||||
// GET /api/development/ebom-tree/descending/excel
|
||||
|
||||
@@ -21,6 +21,7 @@ router.get("/ebom-tree/ascending", ctrl.ascending);
|
||||
router.get("/ebom-tree/descending", ctrl.descending);
|
||||
router.get("/ebom-tree/ascending/excel", ctrl.excelAscending);
|
||||
router.get("/ebom-tree/descending/excel", ctrl.excelDescending);
|
||||
router.get("/ebom-tree/full", ctrl.treeFull);
|
||||
|
||||
// M3 Excel Import — /:objid 보다 위에 (라우트 충돌 방지)
|
||||
router.post("/ebom/excel-parse", excelUpload.single("file"), ctrl.excelParse);
|
||||
|
||||
@@ -251,7 +251,7 @@ export async function ascending(filter: BomTreeFilter) {
|
||||
B.part_no, B.qty, B.seq, B.status,
|
||||
T.lev + 1, T.path || B.objid::varchar, B.objid::varchar = ANY(T.path)
|
||||
FROM bom_part_qty B
|
||||
JOIN TREE T ON B.parent_objid = T.objid AND NOT T.cycle
|
||||
JOIN TREE T ON B.parent_objid = T.child_objid AND NOT T.cycle
|
||||
)
|
||||
SELECT T.bom_report_objid, T.objid, T.parent_objid, T.child_objid, T.part_no, T.qty, T.seq, T.status,
|
||||
T.lev, T.path,
|
||||
@@ -327,7 +327,7 @@ export async function ascendingForExcel(filter: BomTreeFilter) {
|
||||
B.part_no, B.qty, B.item_qty, B.seq, B.status,
|
||||
T.lev + 1, T.path || B.objid::varchar, B.objid::varchar = ANY(T.path)
|
||||
FROM bom_part_qty B
|
||||
JOIN TREE T ON B.parent_objid = T.objid AND NOT T.cycle
|
||||
JOIN TREE T ON B.parent_objid = T.child_objid AND NOT T.cycle
|
||||
)
|
||||
SELECT T.lev,
|
||||
PM.part_no AS pm_part_no,
|
||||
@@ -389,7 +389,7 @@ export async function descendingForExcel(filter: BomTreeFilter) {
|
||||
B.part_no, B.qty, B.item_qty, B.seq, B.status,
|
||||
T.lev + 1, T.path || B.objid::varchar, B.objid::varchar = ANY(T.path)
|
||||
FROM bom_part_qty B
|
||||
JOIN TREE T ON B.objid = T.parent_objid AND NOT T.cycle
|
||||
JOIN TREE T ON B.child_objid = T.parent_objid AND NOT T.cycle
|
||||
)
|
||||
SELECT T.lev,
|
||||
PM.part_no AS pm_part_no,
|
||||
@@ -457,7 +457,7 @@ export async function descending(filter: BomTreeFilter) {
|
||||
B.part_no, B.qty, B.seq, B.status,
|
||||
T.lev + 1, T.path || B.objid::varchar, B.objid::varchar = ANY(T.path)
|
||||
FROM bom_part_qty B
|
||||
JOIN TREE T ON B.objid = T.parent_objid AND NOT T.cycle
|
||||
JOIN TREE T ON B.child_objid = T.parent_objid AND NOT T.cycle
|
||||
)
|
||||
SELECT T.bom_report_objid, T.objid, T.parent_objid, T.child_objid, T.part_no, T.qty, T.seq, T.status,
|
||||
T.lev, T.path,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// 액션: 조회 / 삭제 / 상태변경 (E-BOM등록 Excel Import는 별 PR)
|
||||
// 참조: docs/migration/development/02-ebom.md
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -18,6 +18,7 @@ import { CommCodeSelect } from "@/components/common/CommCodeSelect";
|
||||
import { devBomApi, BomReportListFilter, BomReportRow } from "@/lib/api/devBom";
|
||||
import { BomReportStatusDialog } from "@/components/development/BomReportStatusDialog";
|
||||
import { BomReportExcelImportDialog } from "@/components/development/BomReportExcelImportDialog";
|
||||
import { BomReportTreeDialog } from "@/components/development/BomReportTreeDialog";
|
||||
|
||||
const PRODUCT_GROUP = "0000001"; // 제품구분 (vexplor 공용)
|
||||
|
||||
@@ -27,11 +28,12 @@ const STATUS_OPTIONS = [
|
||||
{ code: "deploy", label: "배포완료" },
|
||||
];
|
||||
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
const BASE_GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "product_name", label: "제품구분", width: "w-[160px]", align: "center", frozen: true },
|
||||
{ key: "part_no", label: "품번", width: "w-[210px]" },
|
||||
{ key: "part_name", label: "품명", minWidth: "min-w-[220px]" },
|
||||
{ key: "bom_cnt", label: "E-BOM", width: "w-[100px]", align: "right", formatNumber: true },
|
||||
// wace fnc_getFolderIcon 1:1 — 폴더 아이콘 클릭 시 BOM 구조 다이얼로그
|
||||
{ key: "bom_cnt", label: "E-BOM", width: "w-[100px]", align: "center", renderType: "folder" },
|
||||
{ key: "dept_user_name", label: "등록자", width: "w-[140px]", align: "center" },
|
||||
{ key: "reg_date", label: "등록일", width: "w-[120px]", align: "center" },
|
||||
{ key: "deploy_date", label: "확정일", width: "w-[120px]", align: "center" },
|
||||
@@ -55,6 +57,8 @@ export default function EbomRegistPage() {
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
const [statusObjid, setStatusObjid] = useState<string | null>(null);
|
||||
const [excelOpen, setExcelOpen] = useState(false);
|
||||
const [treeOpen, setTreeOpen] = useState(false);
|
||||
const [treeReport, setTreeReport] = useState<BomReportRow | null>(null);
|
||||
|
||||
const fetchList = useCallback(async (override?: Partial<BomReportListFilter>) => {
|
||||
setLoading(true);
|
||||
@@ -91,6 +95,21 @@ export default function EbomRegistPage() {
|
||||
setStatusOpen(true);
|
||||
};
|
||||
|
||||
// 품번 셀 클릭 → BOM 트리 다이얼로그 (wace fn_openSetStructure 1:1)
|
||||
const openTree = useCallback((row: BomReportRow) => {
|
||||
setTreeReport(row);
|
||||
setTreeOpen(true);
|
||||
}, []);
|
||||
|
||||
const columns: DataGridColumn[] = useMemo(
|
||||
() => BASE_GRID_COLUMNS.map((c) =>
|
||||
c.key === "bom_cnt"
|
||||
? { ...c, onClick: (row: any) => openTree(row as BomReportRow) }
|
||||
: c,
|
||||
),
|
||||
[openTree],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b bg-card px-4 py-3">
|
||||
@@ -157,7 +176,7 @@ export default function EbomRegistPage() {
|
||||
|
||||
<div className="min-h-0 flex-1 p-2">
|
||||
<DataGrid
|
||||
columns={GRID_COLUMNS}
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
@@ -181,6 +200,11 @@ export default function EbomRegistPage() {
|
||||
initialProductCd={filter.product_cd ?? ""}
|
||||
onSaved={fetchList}
|
||||
/>
|
||||
<BomReportTreeDialog
|
||||
open={treeOpen}
|
||||
onOpenChange={setTreeOpen}
|
||||
bomReport={treeReport}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
// 개발관리 E-BOM 트리 다이얼로그 — wace setStructurePopupMainFS 1:1 (단일 다이얼로그로 통합)
|
||||
//
|
||||
// 운영판은 frameset(헤더/좌측 트리/우측 디테일/하단 버튼) 4-Frame 팝업이지만 RPS 는 단일
|
||||
// 다이얼로그에 헤더(BOM Report 메타) + 동적 LEVEL 컬럼 트리 그리드 + 엑셀 다운로드 버튼.
|
||||
//
|
||||
// 컬럼 (운영 structureAscendingListExcel.jsp 1:1):
|
||||
// 동적 L1..LMaxLevel ("*" 표시) + 품번 / 품명 / 수량 / 항목수량 / 3D / 2D / PDF /
|
||||
// 재료 / 열처리경도 / 열처리방법 / 표면처리 / 메이커 / 범주 이름 / 비고
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, FileSpreadsheet } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { devBomApi, BomReportRow, BomTreeFullRow } from "@/lib/api/devBom";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
bomReport: BomReportRow | null; // 헤더 정보 표시용
|
||||
}
|
||||
|
||||
export function BomReportTreeDialog({ open, onOpenChange, bomReport }: Props) {
|
||||
const [rows, setRows] = useState<BomTreeFullRow[]>([]);
|
||||
const [maxLevel, setMaxLevel] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !bomReport?.objid) {
|
||||
setRows([]); setMaxLevel(0);
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
devBomApi.treeFull({ bom_report_objid: bomReport.objid })
|
||||
.then((data) => {
|
||||
if (!alive) return;
|
||||
setRows(data.rows ?? []);
|
||||
setMaxLevel(Number(data.max_level) || 0);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "트리 조회 실패");
|
||||
})
|
||||
.finally(() => { if (alive) setLoading(false); });
|
||||
return () => { alive = false; };
|
||||
}, [open, bomReport?.objid]);
|
||||
|
||||
const handleExcel = async () => {
|
||||
if (!bomReport?.objid) return;
|
||||
setExporting(true);
|
||||
try {
|
||||
const { blob, fileName } = await devBomApi.excelAscending({ bom_report_objid: bomReport.objid });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = fileName;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(`${fileName} 다운로드`);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "엑셀 다운로드 실패");
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const effectiveMax = Math.max(1, maxLevel);
|
||||
|
||||
// 동적 LEVEL 컬럼 헤더 (1..maxLevel)
|
||||
const levelHeaders = useMemo(() => {
|
||||
const h: number[] = [];
|
||||
for (let i = 1; i <= effectiveMax; i++) h.push(i);
|
||||
return h;
|
||||
}, [effectiveMax]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[1500px] w-[97vw] max-h-[92vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="bg-blue-600 px-4 py-3">
|
||||
<DialogTitle className="text-white">BOM 구조 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 헤더 메타 */}
|
||||
{bomReport && (
|
||||
<div className="grid grid-cols-4 gap-x-6 gap-y-1.5 border-b px-4 py-3 text-xs">
|
||||
<MetaRow label="제품구분" value={bomReport.product_name ?? bomReport.product_cd} />
|
||||
<MetaRow label="품번" value={bomReport.part_no} />
|
||||
<MetaRow label="품명" value={bomReport.part_name} />
|
||||
<MetaRow label="Version" value={bomReport.revision} />
|
||||
<MetaRow label="상태" value={bomReport.status_title} />
|
||||
<MetaRow label="등록자" value={bomReport.dept_user_name} />
|
||||
<MetaRow label="등록일" value={bomReport.reg_date} />
|
||||
<MetaRow label="확정일" value={bomReport.deploy_date} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
총 {rows.length.toLocaleString()}행 · MAX_LEVEL = {maxLevel}
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={handleExcel} disabled={exporting || rows.length === 0}>
|
||||
{exporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileSpreadsheet className="h-4 w-4" />}
|
||||
<span className="ml-1">엑셀 다운로드</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<table className="text-xs border-collapse w-max min-w-full">
|
||||
<thead className="bg-yellow-100 dark:bg-yellow-900/30 sticky top-0">
|
||||
<tr>
|
||||
{levelHeaders.map((i) => (
|
||||
<th key={`l${i}`} className="border px-2 py-1 w-[36px] text-center font-bold">{i}</th>
|
||||
))}
|
||||
<th className="border px-2 py-1 min-w-[150px] text-left">품번</th>
|
||||
<th className="border px-2 py-1 min-w-[180px] text-left">품명</th>
|
||||
<th className="border px-2 py-1 min-w-[60px] text-right">수량</th>
|
||||
<th className="border px-2 py-1 min-w-[70px] text-right">항목수량</th>
|
||||
<th className="border px-2 py-1 min-w-[40px] text-center">3D</th>
|
||||
<th className="border px-2 py-1 min-w-[40px] text-center">2D</th>
|
||||
<th className="border px-2 py-1 min-w-[40px] text-center">PDF</th>
|
||||
<th className="border px-2 py-1 min-w-[100px] text-left">재료</th>
|
||||
<th className="border px-2 py-1 min-w-[100px] text-left">열처리경도</th>
|
||||
<th className="border px-2 py-1 min-w-[100px] text-left">열처리방법</th>
|
||||
<th className="border px-2 py-1 min-w-[100px] text-left">표면처리</th>
|
||||
<th className="border px-2 py-1 min-w-[110px] text-left">메이커</th>
|
||||
<th className="border px-2 py-1 min-w-[100px] text-center">범주 이름</th>
|
||||
<th className="border px-2 py-1 min-w-[130px] text-left">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={levelHeaders.length + 14} className="py-8 text-center text-muted-foreground">
|
||||
BOM 구조가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{rows.map((r, idx) => {
|
||||
const lev = Number(r.lev ?? 1);
|
||||
return (
|
||||
<tr key={idx} className="hover:bg-muted/30">
|
||||
{levelHeaders.map((i) => (
|
||||
<td key={`lc${i}`} className={cn("border px-1 py-0.5 text-center", i === lev && "font-bold")}>
|
||||
{i === lev ? "*" : ""}
|
||||
</td>
|
||||
))}
|
||||
<td className="border px-2 py-0.5 whitespace-nowrap">{r.pm_part_no}</td>
|
||||
<td className="border px-2 py-0.5">{r.pm_part_name}</td>
|
||||
<td className="border px-2 py-0.5 text-right">{r.qty}</td>
|
||||
<td className="border px-2 py-0.5 text-right">{r.p_qty}</td>
|
||||
<td className="border px-2 py-0.5 text-center">{Number(r.cu01_cnt ?? 0) > 0 ? "Y" : ""}</td>
|
||||
<td className="border px-2 py-0.5 text-center">{Number(r.cu02_cnt ?? 0) > 0 ? "Y" : ""}</td>
|
||||
<td className="border px-2 py-0.5 text-center">{Number(r.cu03_cnt ?? 0) > 0 ? "Y" : ""}</td>
|
||||
<td className="border px-2 py-0.5">{r.material}</td>
|
||||
<td className="border px-2 py-0.5">{r.heat_treatment_hardness}</td>
|
||||
<td className="border px-2 py-0.5">{r.heat_treatment_method}</td>
|
||||
<td className="border px-2 py-0.5">{r.surface_treatment}</td>
|
||||
<td className="border px-2 py-0.5">{r.maker}</td>
|
||||
<td className="border px-2 py-0.5 text-center">{r.part_type_title}</td>
|
||||
<td className="border px-2 py-0.5">{r.remark}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t bg-muted/20 px-4 py-3 sm:justify-center">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>닫기</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function MetaRow({ label, value }: { label: string; value: any }) {
|
||||
return (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-muted-foreground w-[60px] shrink-0">{label}</span>
|
||||
<span className="font-medium">{value != null && value !== "" ? value : "—"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,6 +124,30 @@ export interface BomTreeResponse {
|
||||
max_level: number;
|
||||
}
|
||||
|
||||
// 트리 풀 컬럼 (ascendingForExcel 1:1) — BomReportTreeDialog 용
|
||||
export interface BomTreeFullRow {
|
||||
lev: number | string;
|
||||
pm_part_no: string | null;
|
||||
pm_part_name: string | null;
|
||||
qty: string | number | null;
|
||||
p_qty: string | number | null;
|
||||
material: string | null;
|
||||
remark: string | null;
|
||||
heat_treatment_hardness: string | null;
|
||||
heat_treatment_method: string | null;
|
||||
surface_treatment: string | null;
|
||||
maker: string | null;
|
||||
part_type: string | null;
|
||||
part_type_title: string | null;
|
||||
cu01_cnt: number | string | null;
|
||||
cu02_cnt: number | string | null;
|
||||
cu03_cnt: number | string | null;
|
||||
}
|
||||
export interface BomTreeFullResponse {
|
||||
rows: BomTreeFullRow[];
|
||||
max_level: number;
|
||||
}
|
||||
|
||||
// ─── API ─────────────────────────────────────────────────
|
||||
|
||||
export const devBomApi = {
|
||||
@@ -158,6 +182,12 @@ export const devBomApi = {
|
||||
return res.data?.data as BomTreeResponse;
|
||||
},
|
||||
|
||||
// E-BOM 트리 (풀 컬럼) — M3 그리드 행 클릭 → BomReportTreeDialog
|
||||
async treeFull(filter: BomTreeFilter): Promise<BomTreeFullResponse> {
|
||||
const res = await apiClient.get("/development/ebom-tree/full", { params: filter });
|
||||
return res.data?.data as BomTreeFullResponse;
|
||||
},
|
||||
|
||||
// M4 엑셀 다운로드 (정/역전개) — wace 1:1
|
||||
async excelAscending(filter: BomTreeFilter): Promise<{ blob: Blob; fileName: string }> {
|
||||
const res = await apiClient.get("/development/ebom-tree/ascending/excel", {
|
||||
|
||||
Reference in New Issue
Block a user