생산관리 — 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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user