From c955fe0dac675a07d4d803e18a5093ac4ae15d0a Mon Sep 17 00:00:00 2001 From: hjjeong Date: Thu, 14 May 2026 15:23:33 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=E2=80=94=205=EB=A9=94=EB=89=B4(PART/E-BOM/EO=EC=9D=B4=EB=A0=A5?= =?UTF-8?q?)=EC=97=90=20logicstudio=20=EC=8A=A4=ED=83=80=EC=9D=BC=20DataGr?= =?UTF-8?q?id=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 영업관리/프로젝트관리 패턴(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) --- .../development/change-list/page.tsx | 78 ++++++++++---- .../development/ebom-regist/page.tsx | 72 +++++++++---- .../development/ebom-search/page.tsx | 52 ++++++--- .../development/part-regist/page.tsx | 102 ++++++++++++------ .../development/part-search/page.tsx | 100 +++++++++++------ 5 files changed, 280 insertions(+), 124 deletions(-) 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 + />