생산관리 — M-BOM 관리에 logicstudio 스타일 DataGrid 적용

PR-B1(7a7f4f03)에 이미 들어간 서버 페이지네이션 + folder 컬럼은 유지하고,
영업관리/개발관리 패턴의 toolbar + 통계만 얹는다.
구매관리 mbom 메뉴는 production/mbom 페이지 re-export 이므로 자동 적용.

추가 DataGrid props:
- showColumnSettings · summaryStats · showChart
- onRefresh = fetchList · onDownload = exportToExcel(현재 페이지 행만, 라벨 매핑)
- systemColumnKeys=["writer_name","mbom_regdate"]

summaryStats (페이지 기준 + 서버 total):
- 전체 건수(서버 total) / 페이지 건수 / 수주수량 합계 / M-BOM 저장 비율(N/M + %)

부수 정리:
- 부모 wrapper 영업관리 통일: flex h-full flex-col overflow-hidden p-2 gap-2
  + DataGrid 직접 자식으로 (min-h-0 flex-1 wrapper 제거)
- 컬럼 폭: ⋮⋮ 핸들 추가로 좁아진 4글자 한국어 라벨을 100~125px 로 보정

서버 페이지네이션 모드라 onDownload 는 현재 페이지만 export.
전체 export 가 필요하면 별 endpoint(서버에서 페이징 없는 전체 응답)와 함께 추가 가능.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-14 16:30:51 +09:00
parent 430a511605
commit a6692c4e21
@@ -19,6 +19,7 @@ import { PageHeader } from "@/components/common/PageHeader";
import { apiClient } from "@/lib/api/client";
import { mbomApi, MbomListFilter, MbomRow } from "@/lib/api/mbom";
import { MbomDetailDialog } from "@/components/production/MbomDetailDialog";
import { exportToExcel } from "@/lib/utils/excelExport";
const PARENT_CATEGORY = "0000167"; // 주문유형 comm_code parent_code_id
const PARENT_PRODUCT = "0000001"; // 제품구분 comm_code parent_code_id
@@ -125,25 +126,41 @@ export default function MbomMgmtPage() {
const GRID_COLUMNS: DataGridColumn[] = useMemo(() => ([
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]" },
{ key: "category_name", label: "주문유형", width: "w-[100px]", align: "center" },
{ key: "product_name", label: "제품구분", width: "w-[90px]", align: "center" },
{ key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" },
{ key: "receipt_date", label: "접수일", width: "w-[100px]", align: "center" },
{ key: "writer_name", label: "작성자", width: "w-[90px]", align: "center" },
{ key: "category_name", label: "주문유형", width: "w-[115px]", align: "center" },
{ key: "product_name", label: "제품구분", width: "w-[115px]", align: "center" },
{ key: "area_name", label: "국내/해외", width: "w-[115px]", align: "center" },
{ key: "receipt_date", label: "접수일", width: "w-[115px]", align: "center" },
{ key: "writer_name", label: "작성자", width: "w-[115px]", align: "center" },
{ key: "customer_name", label: "고객사", minWidth: "min-w-[160px]" },
{ key: "paid_type_name", label: "유/무상", width: "w-[80px]", align: "center" },
{ key: "paid_type_name", label: "유/무상", width: "w-[100px]", align: "center" },
{ key: "part_no", label: "품번", width: "w-[150px]" },
{ key: "part_name", label: "품명", minWidth: "min-w-[180px]" },
{ key: "serial_no", label: "S/N", width: "w-[110px]", align: "center" },
{ key: "quantity", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
{ key: "req_del_date", label: "요청납기", width: "w-[100px]", align: "center" },
{ key: "serial_no", label: "S/N", width: "w-[115px]", align: "center" },
{ key: "quantity", label: "수주수량", width: "w-[115px]", align: "right", formatNumber: true },
{ key: "req_del_date", label: "요청납기", width: "w-[115px]", align: "center" },
{ key: "customer_request", label: "고객사요청사항", minWidth: "min-w-[200px]" },
// M-BOM 컬럼 — 폴더 아이콘 (저장됨=파랑, 미저장=흰색). 클릭 시 본 편집 다이얼로그.
{ key: "mbom_has", label: "M-BOM", width: "w-[80px]", align: "center",
{ key: "mbom_has", label: "M-BOM", width: "w-[100px]", align: "center",
renderType: "folder", onClick: openMbomDialog },
{ key: "mbom_regdate", label: "최종저장일", width: "w-[100px]", align: "center" },
{ key: "mbom_regdate", label: "최종저장일", width: "w-[125px]", align: "center" },
]), [openMbomDialog]);
// ─── 하단 통계 ──────────────────────────────────────────────
// 전체 건수(서버 total) / 현재 페이지 건수 / 수주수량 합계(페이지) / M-BOM 저장 비율(페이지)
const mbomSummary = useMemo(() => {
const pageCount = gridRows.length;
const qtySum = gridRows.reduce((acc, r: any) => acc + Number(r.quantity || 0), 0);
const hasMbom = gridRows.reduce((acc, r: any) => acc + (Number(r.mbom_has || 0) > 0 ? 1 : 0), 0);
const rate = pageCount === 0 ? 0 : (hasMbom / pageCount) * 100;
const intFmt = (n: number) => n.toLocaleString();
return [
{ label: "전체 건수", value: intFmt(total), suffix: "건" },
{ label: "페이지 건수", value: intFmt(pageCount), suffix: "건" },
{ label: "수주수량 합계", value: intFmt(qtySum) },
{ label: "M-BOM 저장", value: `${intFmt(hasMbom)} / ${intFmt(pageCount)}`, suffix: `(${rate.toFixed(1)}%)` },
];
}, [gridRows, total]);
const handleSearch = () => {
setFilter((f) => ({ ...f, page: 1 }));
fetchList({ page: 1 });
@@ -155,7 +172,7 @@ export default function MbomMgmtPage() {
};
return (
<div className="flex h-full flex-col gap-2 p-2">
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
<PageHeader
loading={loading}
onSearch={handleSearch}
@@ -234,29 +251,42 @@ export default function MbomMgmtPage() {
</CompactFilterField>
</CompactFilterBar>
<div className="min-h-0 flex-1">
<DataGrid
columns={GRID_COLUMNS}
data={gridRows}
loading={loading}
showRowNumber
emptyMessage="조건에 맞는 프로젝트가 없습니다."
gridId="production-mbom-mgmt"
pageSizeOptions={[25, 50, 100, 200, 500]}
paginationStyle="range"
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
onRowDoubleClick={(row: any) => {
if (!row?.objid) return;
setDialogObjid(String(row.objid));
setDialogOpen(true);
}}
/>
</div>
<DataGrid
columns={GRID_COLUMNS}
data={gridRows}
loading={loading}
showRowNumber
emptyMessage="조건에 맞는 프로젝트가 없습니다."
gridId="production-mbom-mgmt"
pageSizeOptions={[25, 50, 100, 200, 500]}
paginationStyle="range"
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
onRowDoubleClick={(row: any) => {
if (!row?.objid) return;
setDialogObjid(String(row.objid));
setDialogOpen(true);
}}
showColumnSettings
summaryStats={mbomSummary}
systemColumnKeys={["writer_name", "mbom_regdate"]}
onRefresh={() => fetchList()}
onDownload={() => {
if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
// 서버 페이지네이션 — 현재 페이지 행만 export. 전체 export 는 별 endpoint 필요시 추가.
const exportRows = gridRows.map((r: any) => {
const out: Record<string, any> = {};
GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
return out;
});
exportToExcel(exportRows, "M-BOM_관리.xlsx", "M-BOM_관리");
}}
showChart
/>
<MbomDetailDialog
open={dialogOpen}