49956f7afa
사용자 보고: "품명/3D 컬럼이 좁아서 겹쳐 보임 + 폴더 모양(파랑/투명)으로 표시해줘"
운영판 wace 견적현황·partMng 폴더 아이콘 패턴 1:1 적용:
- 값 > 0 → 파란 폴더 (fill-[#1a73e8])
- 값 = 0 → 흰색 폴더 (투명 효과, fill-white + muted text)
수정:
- app/(main)/COMPANY_16/development/part-regist/page.tsx — 3D/2D/PDF (60→70px, center, folder)
- app/(main)/COMPANY_16/development/part-search/page.tsx — 3D/2D/PDF (60→70px, center, folder)
- app/(main)/COMPANY_16/development/ebom-search/page.tsx — 3D/2D/PDF (60→70px width 통일)
- components/development/BomReportTreeDialog.tsx — "Y/공백" → FolderCell
- components/production/MbomDetailDialog.tsx — "Y/공백" → FolderCell
sales/{order,sale,revenue}.tsx 의 cu01_cnt 는 "주문서첨부" clip 아이콘이라 별 의미라 미수정.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
205 lines
9.3 KiB
TypeScript
205 lines
9.3 KiB
TypeScript
"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, Folder } 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"><FolderCell n={r.cu01_cnt} /></td>
|
|
<td className="border px-2 py-0.5 text-center"><FolderCell n={r.cu02_cnt} /></td>
|
|
<td className="border px-2 py-0.5 text-center"><FolderCell n={r.cu03_cnt} /></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 FolderCell({ n }: { n: any }) {
|
|
const has = Number(n ?? 0) > 0;
|
|
return (
|
|
<span className="inline-flex items-center justify-center">
|
|
<Folder className={cn("w-4 h-4",
|
|
has ? "fill-[#1a73e8] text-[#1a73e8]" : "fill-white text-muted-foreground/40")} />
|
|
</span>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|