Files
hjjeong 49956f7afa 개발관리·생산관리 — 3D/2D/PDF 카운트 컬럼을 폴더 아이콘으로 통일
사용자 보고: "품명/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>
2026-05-13 17:40:25 +09:00

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>
);
}