개발관리 — 5메뉴(PART/E-BOM/EO이력)에 logicstudio 스타일 DataGrid 적용
영업관리/프로젝트관리 패턴(fc959d88·6a181371)을 개발관리 5메뉴 전체로 확장.
공통 변경:
- 부모 wrapper 영업관리 통일: flex h-full flex-col overflow-hidden p-2 gap-2
+ DataGrid 를 직접 자식으로 (불필요한 min-h-0 flex-1 wrapper 제거)
- DataGrid props 확장:
- showColumnSettings · paginationStyle="range" · pageSizeOptions=[10,15,20,50,100]
- onRefresh = fetchList(또는 runQuery) · onDownload = exportToExcel(GRID_COLUMNS 라벨 매핑)
- showChart
- 컬럼 폭: ⋮⋮ 드래그 핸들 추가로 좁아진 4글자 한국어/영문 라벨을 95~125px 로 보정
메뉴별 summaryStats:
- PART 등록(M1): 전체·페이지 건수 / 환산수량·개당수량·BOM 수량 합계
- PART 조회(M2): 전체·페이지 건수 / BOM 수량·환산수량·개당수량 합계
- E-BOM 등록(M3): 전체·페이지 건수 + 상태별(status_title) 분포 4종
- E-BOM 조회(M4): 모드(정/역전개) + 표시·원본 행 + MAX_LEVEL + 수량·항목수량 합계
- 설계변경 리스트(M5): 전체·페이지 건수 / 수량·변경수량 합계
systemColumnKeys 분리 (컬럼 설정의 데이터/시스템 그룹 구분):
- PART 등록: revision, status
- PART 조회: revision, eo_no
- E-BOM 등록: dept_user_name, reg_date, deploy_date, revision, status_title
- 설계변경: writer_name, his_reg_date_title
id 매핑 누락 보강 (350ddcd3 함정 재발 방지):
- ebom-search: gridData 에 child_objid 또는 인덱스 fallback id 부여
- change-list: rows 에 objid 또는 인덱스 fallback id 부여
- (PART/E-BOM 등록은 이미 gridRows 매핑 있음)
E-BOM 조회 특수:
- defaultPageSize=100, pageSizeOptions=[20,50,100,200,500] — BOM 트리 가독성
- onDownload 는 PageHeader 의 정/역전개 엑셀 버튼과 동일 동작(현재 direction)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import { PageHeader } from "@/components/common/PageHeader";
|
||||
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
|
||||
import { devEoHistoryApi, EoHistoryListFilter, EoHistoryRow } from "@/lib/api/devEoHistory";
|
||||
import { PartHisDetailDialog } from "@/components/development/PartHisDetailDialog";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// comm_code 그룹 (vexplor_rps)
|
||||
const GROUP_PART_TYPE = "0000062";
|
||||
@@ -29,22 +30,22 @@ const YEAR_OPTIONS: SmartSelectOption[] = (() => {
|
||||
})();
|
||||
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "eo_no", label: "EO No", width: "w-[100px]", frozen: true },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[120px]" },
|
||||
{ key: "eo_no", label: "EO No", width: "w-[110px]", frozen: true },
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]" },
|
||||
{ key: "project_name", label: "프로젝트명", width: "w-[180px]" },
|
||||
{ key: "unit_name", label: "유닛명", width: "w-[160px]" },
|
||||
{ key: "parent_part_info", label: "모품번", width: "w-[160px]" },
|
||||
{ key: "part_no_disp", label: "품번", width: "w-[160px]" },
|
||||
{ key: "part_name_disp", label: "품명", minWidth: "min-w-[180px]" },
|
||||
{ key: "qty", label: "수량", width: "w-[70px]", align: "right", formatNumber: true },
|
||||
{ key: "qty_temp", label: "변경수량", width: "w-[80px]", align: "right", formatNumber: true },
|
||||
{ key: "change_type_name", label: "EO구분", width: "w-[90px]", align: "center" },
|
||||
{ key: "change_option_name", label: "EO사유", width: "w-[100px]", align: "center" },
|
||||
{ key: "revision_disp", label: "Revision", width: "w-[90px]", align: "center" },
|
||||
{ key: "eo_date", label: "EO Date", width: "w-[100px]", align: "center" },
|
||||
{ key: "part_type_name", label: "PART구분", width: "w-[90px]", align: "center" },
|
||||
{ key: "writer_name", label: "담당자", width: "w-[90px]", align: "center" },
|
||||
{ key: "his_reg_date_title", label: "실행일", width: "w-[100px]", align: "center" },
|
||||
{ key: "qty", label: "수량", width: "w-[95px]", align: "right", formatNumber: true },
|
||||
{ key: "qty_temp", label: "변경수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "change_type_name", label: "EO구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "change_option_name", label: "EO사유", width: "w-[115px]", align: "center" },
|
||||
{ key: "revision_disp", label: "Revision", width: "w-[115px]", align: "center" },
|
||||
{ key: "eo_date", label: "EO Date", width: "w-[110px]", align: "center" },
|
||||
{ key: "part_type_name", label: "PART구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "writer_name", label: "담당자", width: "w-[115px]", align: "center" },
|
||||
{ key: "his_reg_date_title", label: "실행일", width: "w-[115px]", align: "center" },
|
||||
];
|
||||
|
||||
const EMPTY_FILTER: EoHistoryListFilter = {
|
||||
@@ -69,7 +70,9 @@ export default function EoHistoryPage() {
|
||||
try {
|
||||
const f = { ...filter, ...override };
|
||||
const res = await devEoHistoryApi.list(f);
|
||||
setRows(res.rows ?? []);
|
||||
// DataGrid row 키 — objid 없을 수 있어 인덱스 fallback (id 누락 시 모든 행이 selected 로 잡힘)
|
||||
const list = (res.rows ?? []).map((r, i) => ({ ...r, id: (r as any).objid ?? `eo-${i}` }));
|
||||
setRows(list as any);
|
||||
setTotal(res.total ?? 0);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
@@ -90,8 +93,23 @@ export default function EoHistoryPage() {
|
||||
[],
|
||||
);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// 이력 건수(총·페이지) / 수량·변경수량 합계
|
||||
const eoSummary = useMemo(() => {
|
||||
const pageCount = rows.length;
|
||||
const qtySum = rows.reduce((acc, r: any) => acc + Number(r.qty || 0), 0);
|
||||
const qtyTemp = rows.reduce((acc, r: any) => acc + Number(r.qty_temp || 0), 0);
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "전체 건수", value: intFmt(total), suffix: "건" },
|
||||
{ label: "페이지 건수", value: intFmt(pageCount), suffix: "건" },
|
||||
{ label: "수량 합계", value: intFmt(qtySum) },
|
||||
{ label: "변경수량 합계", value: intFmt(qtyTemp) },
|
||||
];
|
||||
}, [rows, total]);
|
||||
|
||||
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={() => fetchList()}
|
||||
@@ -145,16 +163,30 @@ export default function EoHistoryPage() {
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="설계변경 이력이 없습니다."
|
||||
gridId="development-eo-history"
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="설계변경 이력이 없습니다."
|
||||
gridId="development-eo-history"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={eoSummary}
|
||||
systemColumnKeys={["writer_name", "his_reg_date_title"]}
|
||||
onRefresh={() => fetchList()}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r: any) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "설계변경_리스트.xlsx", "설계변경_리스트");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<PartHisDetailDialog
|
||||
open={detailOpen}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { BomReportStatusDialog } from "@/components/development/BomReportStatusD
|
||||
import { DevPartSelect } from "@/components/development/DevPartSelect";
|
||||
import { BomReportExcelImportDialog } from "@/components/development/BomReportExcelImportDialog";
|
||||
import { BomReportTreeDialog } from "@/components/development/BomReportTreeDialog";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
const PRODUCT_GROUP = "0000001"; // 제품구분 (vexplor 공용)
|
||||
|
||||
@@ -33,11 +34,11 @@ const BASE_GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "part_no", label: "품번", width: "w-[210px]" },
|
||||
{ key: "part_name", label: "품명", minWidth: "min-w-[220px]" },
|
||||
// wace fnc_getFolderIcon 1:1 — 폴더 아이콘 클릭 시 BOM 구조 다이얼로그
|
||||
{ key: "bom_cnt", label: "E-BOM", width: "w-[100px]", align: "center", renderType: "folder" },
|
||||
{ key: "bom_cnt", label: "E-BOM", width: "w-[115px]", 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" },
|
||||
{ key: "revision", label: "Version", width: "w-[100px]", align: "center" },
|
||||
{ key: "reg_date", label: "등록일", width: "w-[125px]", align: "center" },
|
||||
{ key: "deploy_date", label: "확정일", width: "w-[125px]", align: "center" },
|
||||
{ key: "revision", label: "Version", width: "w-[115px]", align: "center" },
|
||||
{ key: "status_title", label: "상태", width: "w-[120px]", align: "center" },
|
||||
];
|
||||
|
||||
@@ -113,8 +114,29 @@ export default function EbomRegistPage() {
|
||||
// DataGrid 는 row.id 를 키로 사용 — backend 응답은 row.objid (lowercase) 이므로 매핑
|
||||
const gridRows = useMemo(() => rows.map((r) => ({ ...r, id: r.objid })), [rows]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// BOM 건수(총·페이지) / 상태별 분포 (간이 — 상태 라벨별 카운트)
|
||||
const bomSummary = useMemo(() => {
|
||||
const pageCount = gridRows.length;
|
||||
const byStatus = new Map<string, number>();
|
||||
gridRows.forEach((r: any) => {
|
||||
const k = String(r.status_title ?? r.status ?? "-");
|
||||
byStatus.set(k, (byStatus.get(k) ?? 0) + 1);
|
||||
});
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
const stats: Array<{ label: string; value: string; suffix?: string }> = [
|
||||
{ label: "전체 건수", value: intFmt(total), suffix: "건" },
|
||||
{ label: "페이지 건수", value: intFmt(pageCount), suffix: "건" },
|
||||
];
|
||||
// 상태별 카운트(많이 노출되는 라벨만)
|
||||
Array.from(byStatus.entries()).slice(0, 4).forEach(([k, v]) => {
|
||||
stats.push({ label: `상태(${k})`, value: intFmt(v), suffix: "건" });
|
||||
});
|
||||
return stats;
|
||||
}, [gridRows, total]);
|
||||
|
||||
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={() => fetchList()}
|
||||
@@ -172,19 +194,33 @@ export default function EbomRegistPage() {
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridRows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="등록된 E-BOM이 없습니다."
|
||||
gridId="development-ebom-regist"
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridRows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="등록된 E-BOM이 없습니다."
|
||||
gridId="development-ebom-regist"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={bomSummary}
|
||||
systemColumnKeys={["dept_user_name", "reg_date", "deploy_date", "revision", "status_title"]}
|
||||
onRefresh={() => fetchList()}
|
||||
onDownload={() => {
|
||||
if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = gridRows.map((r: any) => {
|
||||
const out: Record<string, any> = {};
|
||||
BASE_GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "E-BOM_등록.xlsx", "E-BOM_등록");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<BomReportStatusDialog
|
||||
open={statusOpen}
|
||||
|
||||
@@ -23,6 +23,7 @@ const LEVEL_OPTIONS: SmartSelectOption[] = [
|
||||
{ code: "5", label: "5레벨" },
|
||||
];
|
||||
import { PartDetailDialog } from "@/components/development/PartDetailDialog";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
type Direction = "ascending" | "descending";
|
||||
|
||||
@@ -170,21 +171,38 @@ export default function EbomSearchPage() {
|
||||
const ancestors = ancestorsByChildId.get(String(r.child_objid)) ?? [];
|
||||
return !ancestors.some((a) => collapsedChildIds.has(a));
|
||||
})
|
||||
.map((r) => {
|
||||
.map((r, i) => {
|
||||
const expanded: any = { ...r };
|
||||
const lev = Number(r.lev ?? 0);
|
||||
for (let i = 1; i <= Math.max(1, maxLevel); i++) {
|
||||
expanded[`__lev_${i}`] = lev === i ? "*" : "";
|
||||
for (let j = 1; j <= Math.max(1, maxLevel); j++) {
|
||||
expanded[`__lev_${j}`] = lev === j ? "*" : "";
|
||||
}
|
||||
const childId = r.child_objid ? String(r.child_objid) : "";
|
||||
const hasChild = childId && hasChildSet.has(childId);
|
||||
expanded.__toggle = hasChild ? (collapsedChildIds.has(childId) ? "+" : "−") : "";
|
||||
// DataGrid row key — child_objid 우선, 없으면 인덱스 fallback
|
||||
expanded.id = childId || `bom-${i}`;
|
||||
return expanded;
|
||||
});
|
||||
}, [rows, maxLevel, hasChildSet, ancestorsByChildId, collapsedChildIds]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
const treeSummary = useMemo(() => {
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
const qtySum = gridData.reduce((acc, r: any) => acc + Number(r.qty || 0), 0);
|
||||
const pQtySum = gridData.reduce((acc, r: any) => acc + Number(r.p_qty || 0), 0);
|
||||
return [
|
||||
{ label: "모드", value: direction === "ascending" ? "정전개" : "역전개" },
|
||||
{ label: "표시 행", value: intFmt(gridData.length), suffix: "행" },
|
||||
{ label: "원본 행", value: intFmt(rows.length), suffix: "행" },
|
||||
{ label: "MAX_LEVEL", value: String(maxLevel) },
|
||||
{ label: "수량 합계", value: intFmt(qtySum) },
|
||||
{ label: "항목수량 합계", value: intFmt(pQtySum) },
|
||||
];
|
||||
}, [gridData, rows.length, maxLevel, direction]);
|
||||
|
||||
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
|
||||
onReset={() => { setFilter(EMPTY_FILTER); setRows([]); setMaxLevel(0); }}
|
||||
actions={
|
||||
@@ -257,16 +275,22 @@ export default function EbomSearchPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridData}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="조건에 맞는 BOM이 없습니다."
|
||||
gridId={`development-ebom-search-${direction}`}
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridData}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="조건에 맞는 BOM이 없습니다."
|
||||
gridId={`development-ebom-search-${direction}`}
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[20, 50, 100, 200, 500]}
|
||||
defaultPageSize={100}
|
||||
summaryStats={treeSummary}
|
||||
onRefresh={() => runQuery(direction)}
|
||||
onDownload={() => downloadExcel(direction)}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<PartDetailDialog
|
||||
open={partDetailOpen}
|
||||
|
||||
@@ -20,39 +20,40 @@ import { PartDetailDialog } from "@/components/development/PartDetailDialog";
|
||||
import { PartExcelImportDialog } from "@/components/development/PartExcelImportDialog";
|
||||
import { PartDrawingMultiUploadButton } from "@/components/development/PartDrawingMultiUploadButton";
|
||||
import { DevPartSelect } from "@/components/development/DevPartSelect";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// wace 23셀 + 부속 (PARENT_PART_INFO/PARTNER_TITLE/Q_QTY)
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]", frozen: true },
|
||||
{ key: "part_name", label: "품명", minWidth: "min-w-[220px]" },
|
||||
{ key: "cu01_cnt", label: "3D", width: "w-[70px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu02_cnt", label: "2D", width: "w-[70px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu03_cnt", label: "PDF", width: "w-[70px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu01_cnt", label: "3D", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu02_cnt", label: "2D", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu03_cnt", label: "PDF", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "material", label: "재료", width: "w-[100px]" },
|
||||
{ key: "heat_treatment_hardness", label: "열처리경도", width: "w-[110px]" },
|
||||
{ key: "heat_treatment_method", label: "열처리방법", width: "w-[110px]" },
|
||||
{ key: "surface_treatment", label: "표면처리", width: "w-[100px]" },
|
||||
{ key: "heat_treatment_hardness", label: "열처리경도", width: "w-[125px]" },
|
||||
{ key: "heat_treatment_method", label: "열처리방법", width: "w-[125px]" },
|
||||
{ key: "surface_treatment", label: "표면처리", width: "w-[115px]" },
|
||||
{ key: "maker", label: "메이커", width: "w-[100px]" },
|
||||
{ key: "part_type_title", label: "범주", width: "w-[100px]" },
|
||||
{ key: "spec", label: "규격", width: "w-[140px]" },
|
||||
{ key: "acctfg_nm", label: "계정구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "odrfg_nm", label: "조달구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_dc_nm", label: "재고단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitmang_dc_nm", label: "관리단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitchng_nb", label: "환산수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "lot_fg_nm", label: "LOT구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "use_yn_nm", label: "사용여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "qc_fg_nm", label: "검사여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "setitem_fg_nm", label: "SET품여부", width: "w-[90px]", align: "center" },
|
||||
{ key: "req_fg_nm", label: "의뢰여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_length", label: "개당길이", width: "w-[90px]", align: "right" },
|
||||
{ key: "unit_qty", label: "개당수량", width: "w-[90px]", align: "right" },
|
||||
{ key: "acctfg_nm", label: "계정구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "odrfg_nm", label: "조달구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "unit_dc_nm", label: "재고단위", width: "w-[115px]", align: "center" },
|
||||
{ key: "unitmang_dc_nm", label: "관리단위", width: "w-[115px]", align: "center" },
|
||||
{ key: "unitchng_nb", label: "환산수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "lot_fg_nm", label: "LOT구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "use_yn_nm", label: "사용여부", width: "w-[115px]", align: "center" },
|
||||
{ key: "qc_fg_nm", label: "검사여부", width: "w-[115px]", align: "center" },
|
||||
{ key: "setitem_fg_nm", label: "SET품여부", width: "w-[120px]", align: "center" },
|
||||
{ key: "req_fg_nm", label: "의뢰여부", width: "w-[115px]", align: "center" },
|
||||
{ key: "unit_length", label: "개당길이", width: "w-[115px]", align: "right" },
|
||||
{ key: "unit_qty", label: "개당수량", width: "w-[115px]", align: "right" },
|
||||
// M1 부속
|
||||
{ key: "partner_title", label: "공급업체(시퀀스)", minWidth: "min-w-[180px]" },
|
||||
{ key: "parent_part_info", label: "상위 품번", width: "w-[120px]" },
|
||||
{ key: "q_qty", label: "수량(BOM)", width: "w-[90px]", align: "right" },
|
||||
{ key: "revision", label: "REV", width: "w-[60px]", align: "center" },
|
||||
{ key: "status", label: "상태", width: "w-[80px]", align: "center" },
|
||||
{ key: "q_qty", label: "수량(BOM)", width: "w-[115px]", align: "right" },
|
||||
{ key: "revision", label: "REV", width: "w-[80px]", align: "center" },
|
||||
{ key: "status", label: "상태", width: "w-[95px]", align: "center" },
|
||||
];
|
||||
|
||||
const EMPTY_FILTER: PartListFilter = {
|
||||
@@ -105,6 +106,23 @@ export default function PartRegistPage() {
|
||||
// DataGrid 는 row.id 를 키로 사용 — backend 응답은 row.objid 이므로 매핑
|
||||
const gridRows = useMemo(() => rows.map((r) => ({ ...r, id: r.objid })), [rows]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// PART 건수(총·페이지) / 환산수량·개당수량·BOM 수량 합계
|
||||
const partSummary = useMemo(() => {
|
||||
const pageCount = gridRows.length;
|
||||
const unitChng = gridRows.reduce((acc, r: any) => acc + Number(r.unitchng_nb || 0), 0);
|
||||
const unitQty = gridRows.reduce((acc, r: any) => acc + Number(r.unit_qty || 0), 0);
|
||||
const qQty = gridRows.reduce((acc, r: any) => acc + Number(r.q_qty || 0), 0);
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "전체 건수", value: intFmt(total), suffix: "건" },
|
||||
{ label: "페이지 건수", value: intFmt(pageCount), suffix: "건" },
|
||||
{ label: "환산수량 합계", value: intFmt(unitChng) },
|
||||
{ label: "개당수량 합계", value: intFmt(unitQty) },
|
||||
{ label: "BOM 수량 합계", value: intFmt(qQty) },
|
||||
];
|
||||
}, [gridRows, total]);
|
||||
|
||||
// 등록
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
@@ -155,7 +173,7 @@ export default function PartRegistPage() {
|
||||
};
|
||||
|
||||
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={() => fetchList()}
|
||||
@@ -209,19 +227,33 @@ export default function PartRegistPage() {
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridRows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="조건에 맞는 PART가 없습니다."
|
||||
gridId="development-part-regist"
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridRows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="조건에 맞는 PART가 없습니다."
|
||||
gridId="development-part-regist"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={partSummary}
|
||||
systemColumnKeys={["revision", "status"]}
|
||||
onRefresh={() => fetchList()}
|
||||
onDownload={() => {
|
||||
if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
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, "PART_등록.xlsx", "PART_등록");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<PartFormDialog
|
||||
open={formOpen}
|
||||
|
||||
@@ -20,35 +20,36 @@ import { PartDetailDialog } from "@/components/development/PartDetailDialog";
|
||||
import { PartExcelImportDialog } from "@/components/development/PartExcelImportDialog";
|
||||
import { PartDrawingMultiUploadButton } from "@/components/development/PartDrawingMultiUploadButton";
|
||||
import { DevPartSelect } from "@/components/development/DevPartSelect";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]", frozen: true },
|
||||
{ key: "part_name", label: "품명", minWidth: "min-w-[220px]" },
|
||||
{ key: "cu01_cnt", label: "3D", width: "w-[70px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu02_cnt", label: "2D", width: "w-[70px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu03_cnt", label: "PDF", width: "w-[70px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu01_cnt", label: "3D", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu02_cnt", label: "2D", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "cu03_cnt", label: "PDF", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "material", label: "재료", width: "w-[100px]" },
|
||||
{ key: "heat_treatment_hardness", label: "열처리경도", width: "w-[110px]" },
|
||||
{ key: "heat_treatment_method", label: "열처리방법", width: "w-[110px]" },
|
||||
{ key: "surface_treatment", label: "표면처리", width: "w-[100px]" },
|
||||
{ key: "heat_treatment_hardness", label: "열처리경도", width: "w-[125px]" },
|
||||
{ key: "heat_treatment_method", label: "열처리방법", width: "w-[125px]" },
|
||||
{ key: "surface_treatment", label: "표면처리", width: "w-[115px]" },
|
||||
{ key: "maker", label: "메이커", width: "w-[100px]" },
|
||||
{ key: "part_type_title", label: "범주", width: "w-[100px]" },
|
||||
{ key: "spec", label: "규격", width: "w-[140px]" },
|
||||
{ key: "acctfg_nm", label: "계정구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "odrfg_nm", label: "조달구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_dc_nm", label: "재고단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitmang_dc_nm", label: "관리단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitchng_nb", label: "환산수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "lot_fg_nm", label: "LOT구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "use_yn_nm", label: "사용여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "qc_fg_nm", label: "검사여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "setitem_fg_nm", label: "SET품여부", width: "w-[90px]", align: "center" },
|
||||
{ key: "req_fg_nm", label: "의뢰여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_length", label: "개당길이", width: "w-[90px]", align: "right" },
|
||||
{ key: "unit_qty", label: "개당수량", width: "w-[90px]", align: "right" },
|
||||
{ key: "acctfg_nm", label: "계정구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "odrfg_nm", label: "조달구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "unit_dc_nm", label: "재고단위", width: "w-[115px]", align: "center" },
|
||||
{ key: "unitmang_dc_nm", label: "관리단위", width: "w-[115px]", align: "center" },
|
||||
{ key: "unitchng_nb", label: "환산수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "lot_fg_nm", label: "LOT구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "use_yn_nm", label: "사용여부", width: "w-[115px]", align: "center" },
|
||||
{ key: "qc_fg_nm", label: "검사여부", width: "w-[115px]", align: "center" },
|
||||
{ key: "setitem_fg_nm", label: "SET품여부", width: "w-[120px]", align: "center" },
|
||||
{ key: "req_fg_nm", label: "의뢰여부", width: "w-[115px]", align: "center" },
|
||||
{ key: "unit_length", label: "개당길이", width: "w-[115px]", align: "right" },
|
||||
{ key: "unit_qty", label: "개당수량", width: "w-[115px]", align: "right" },
|
||||
// M2 추가
|
||||
{ key: "bom_qty", label: "BOM 수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "revision", label: "REV", width: "w-[60px]", align: "center" },
|
||||
{ key: "bom_qty", label: "BOM 수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "revision", label: "REV", width: "w-[80px]", align: "center" },
|
||||
{ key: "eo_no", label: "EO_NO", width: "w-[120px]" },
|
||||
];
|
||||
|
||||
@@ -100,6 +101,23 @@ export default function PartSearchPage() {
|
||||
// DataGrid 는 row.id 를 키로 사용 — backend 응답은 row.objid 이므로 매핑
|
||||
const gridRows = useMemo(() => rows.map((r) => ({ ...r, id: r.objid })), [rows]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// PART 건수(총·페이지) / BOM 수량·환산수량·개당수량 합계
|
||||
const partSummary = useMemo(() => {
|
||||
const pageCount = gridRows.length;
|
||||
const bomQty = gridRows.reduce((acc, r: any) => acc + Number(r.bom_qty || 0), 0);
|
||||
const unitChng = gridRows.reduce((acc, r: any) => acc + Number(r.unitchng_nb || 0), 0);
|
||||
const unitQty = gridRows.reduce((acc, r: any) => acc + Number(r.unit_qty || 0), 0);
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "전체 건수", value: intFmt(total), suffix: "건" },
|
||||
{ label: "페이지 건수", value: intFmt(pageCount), suffix: "건" },
|
||||
{ label: "BOM 수량 합계", value: intFmt(bomQty) },
|
||||
{ label: "환산수량 합계", value: intFmt(unitChng) },
|
||||
{ label: "개당수량 합계", value: intFmt(unitQty) },
|
||||
];
|
||||
}, [gridRows, total]);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create"); setFormObjid(null); setFormOpen(true);
|
||||
};
|
||||
@@ -124,7 +142,7 @@ export default function PartSearchPage() {
|
||||
};
|
||||
|
||||
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={() => fetchList()}
|
||||
@@ -171,19 +189,33 @@ export default function PartSearchPage() {
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridRows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="조건에 맞는 PART가 없습니다."
|
||||
gridId="development-part-search"
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={gridRows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="조건에 맞는 PART가 없습니다."
|
||||
gridId="development-part-search"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={partSummary}
|
||||
systemColumnKeys={["revision", "eo_no"]}
|
||||
onRefresh={() => fetchList()}
|
||||
onDownload={() => {
|
||||
if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
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, "PART_조회.xlsx", "PART_조회");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<PartFormDialog
|
||||
open={formOpen}
|
||||
|
||||
Reference in New Issue
Block a user