diff --git a/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx b/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx
index 18db5ddb..c7f1475e 100644
--- a/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx
+++ b/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx
@@ -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 (
-
+
fetchList()}
@@ -145,16 +163,30 @@ export default function EoHistoryPage() {
-
-
-
+ fetchList()}
+ onDownload={() => {
+ if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
+ const exportRows = rows.map((r: any) => {
+ const out: Record = {};
+ GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
+ return out;
+ });
+ exportToExcel(exportRows, "설계변경_리스트.xlsx", "설계변경_리스트");
+ }}
+ showChart
+ />
rows.map((r) => ({ ...r, id: r.objid })), [rows]);
+ // ─── 하단 통계 ──────────────────────────────────────────────
+ // BOM 건수(총·페이지) / 상태별 분포 (간이 — 상태 라벨별 카운트)
+ const bomSummary = useMemo(() => {
+ const pageCount = gridRows.length;
+ const byStatus = new Map();
+ 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 (
-
+
fetchList()}
@@ -172,19 +194,33 @@ export default function EbomRegistPage() {
-
-
-
+ fetchList()}
+ onDownload={() => {
+ if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
+ const exportRows = gridRows.map((r: any) => {
+ const out: Record = {};
+ BASE_GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
+ return out;
+ });
+ exportToExcel(exportRows, "E-BOM_등록.xlsx", "E-BOM_등록");
+ }}
+ showChart
+ />
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 (
-
+
{ setFilter(EMPTY_FILTER); setRows([]); setMaxLevel(0); }}
actions={
@@ -257,16 +275,22 @@ export default function EbomSearchPage() {
)}
-
-
-
+
runQuery(direction)}
+ onDownload={() => downloadExcel(direction)}
+ showChart
+ />
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 (
-
+
fetchList()}
@@ -209,19 +227,33 @@ export default function PartRegistPage() {
-
-
-
+ fetchList()}
+ onDownload={() => {
+ if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
+ const exportRows = gridRows.map((r: any) => {
+ const out: Record = {};
+ GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
+ return out;
+ });
+ exportToExcel(exportRows, "PART_등록.xlsx", "PART_등록");
+ }}
+ showChart
+ />
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 (
-
+
fetchList()}
@@ -171,19 +189,33 @@ export default function PartSearchPage() {
-
-
-
+ fetchList()}
+ onDownload={() => {
+ if (gridRows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
+ const exportRows = gridRows.map((r: any) => {
+ const out: Record = {};
+ GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
+ return out;
+ });
+ exportToExcel(exportRows, "PART_조회.xlsx", "PART_조회");
+ }}
+ showChart
+ />