diff --git a/frontend/app/(main)/COMPANY_16/project/progress/page.tsx b/frontend/app/(main)/COMPANY_16/project/progress/page.tsx index bcc7fa37..207fd6a1 100644 --- a/frontend/app/(main)/COMPANY_16/project/progress/page.tsx +++ b/frontend/app/(main)/COMPANY_16/project/progress/page.tsx @@ -22,6 +22,7 @@ import { PageHeader } from "@/components/common/PageHeader"; import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar"; import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog"; import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt"; +import { exportToExcel } from "@/lib/utils/excelExport"; // 진행관리 row → 정규화된 ProjectInfoData 매핑 const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({ @@ -45,32 +46,32 @@ const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({ const GRID_COLUMNS: DataGridColumn[] = [ { key: "project_no", label: "프로젝트번호", width: "w-[160px]", frozen: true }, // 프로젝트정보 그룹 - { key: "category_name", label: "주문유형", width: "w-[90px]", align: "center" }, - { key: "product_name", label: "제품구분", width: "w-[100px]", align: "left" }, - { key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" }, - { key: "reg_date", label: "접수일", width: "w-[110px]", align: "center" }, + { key: "category_name", label: "주문유형", width: "w-[115px]", align: "center" }, + { key: "product_name", label: "제품구분", width: "w-[115px]", align: "left" }, + { key: "area_name", label: "국내/해외", width: "w-[115px]", align: "center" }, + { key: "reg_date", label: "접수일", width: "w-[115px]", align: "center" }, { key: "customer_name", label: "고객사", width: "w-[140px]" }, - { key: "free_of_charge", label: "유/무상", width: "w-[80px]", align: "center" }, + { key: "free_of_charge", label: "유/무상", width: "w-[100px]", align: "center" }, { key: "product_item_code", label: "품번", width: "w-[150px]" }, { key: "product_item_name", label: "품명", width: "w-[180px]" }, { key: "serial_no", label: "S/N", width: "w-[150px]" }, - { key: "contract_qty", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "req_del_date", label: "요청납기", width: "w-[110px]", align: "center" }, + { key: "contract_qty", label: "수주수량", width: "w-[115px]", align: "right", formatNumber: true }, + { key: "req_del_date", label: "요청납기", width: "w-[115px]", align: "center" }, // 설계 - { key: "ebom_status", label: "E-BOM", width: "w-[100px]", align: "center" }, + { key: "ebom_status", label: "E-BOM", width: "w-[115px]", align: "center" }, // 생산관리 - { key: "mbom_status", label: "M-BOM", width: "w-[100px]", align: "center" }, + { key: "mbom_status", label: "M-BOM", width: "w-[115px]", align: "center" }, // 구매 - { key: "order_date", label: "발주일", width: "w-[110px]", align: "center" }, - { key: "receiving_rate", label: "입고율", width: "w-[90px]", align: "right" }, + { key: "order_date", label: "발주일", width: "w-[115px]", align: "center" }, + { key: "receiving_rate", label: "입고율", width: "w-[100px]", align: "right" }, // 생산 - { key: "production_team_12", label: "제조1,2팀", width: "w-[100px]", align: "center" }, - { key: "production_team_3", label: "제조3팀", width: "w-[100px]", align: "center" }, + { key: "production_team_12", label: "제조1,2팀", width: "w-[120px]", align: "center" }, + { key: "production_team_3", label: "제조3팀", width: "w-[115px]", align: "center" }, // 장비 - { key: "assembly", label: "조립", width: "w-[90px]", align: "center" }, - { key: "verification", label: "검증", width: "w-[90px]", align: "center" }, + { key: "assembly", label: "조립", width: "w-[100px]", align: "center" }, + { key: "verification", label: "검증", width: "w-[100px]", align: "center" }, // 출하 - { key: "shipment_date", label: "출하일", width: "w-[110px]", align: "center" }, + { key: "shipment_date", label: "출하일", width: "w-[115px]", align: "center" }, ]; const CATEGORY_GROUP = "0000167"; // 주문유형 @@ -145,6 +146,24 @@ export default function ProjectProgressPage() { setTimeout(() => fetchList(), 0); }; + // ─── 하단 통계 ────────────────────────────────────────────── + // 프로젝트 건수 / 수주수량 합계 / 입고율 평균 + const progressSummary = useMemo(() => { + const count = rows.length; + const qtySum = rows.reduce((acc, r) => acc + Number(r.contract_qty || 0), 0); + // 입고율은 "85%" 같은 문자열 가능 → 숫자만 추출 + const rateNums = rows + .map((r) => parseFloat(String((r as any).receiving_rate ?? "").replace(/[^0-9.]/g, ""))) + .filter((n) => !Number.isNaN(n)); + const rateAvg = rateNums.length === 0 ? 0 : rateNums.reduce((a, b) => a + b, 0) / rateNums.length; + const intFmt = (n: number) => n.toLocaleString(); + return [ + { label: "프로젝트 건수", value: intFmt(count), suffix: "건" }, + { label: "수주수량 합계", value: intFmt(qtySum) }, + { label: "입고율 평균", value: rateAvg.toFixed(1), suffix: "%" }, + ]; + }, [rows]); + return (
{ + if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; } + const exportRows = rows.map((r) => { + const out: Record = {}; + GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; }); + return out; + }); + exportToExcel(exportRows, "진행관리.xlsx", "진행관리"); + }} + showChart />
diff --git a/frontend/app/(main)/COMPANY_16/project/wbs-template/page.tsx b/frontend/app/(main)/COMPANY_16/project/wbs-template/page.tsx index 6c5c6565..b0918928 100644 --- a/frontend/app/(main)/COMPANY_16/project/wbs-template/page.tsx +++ b/frontend/app/(main)/COMPANY_16/project/wbs-template/page.tsx @@ -10,7 +10,7 @@ // 검색: 제품구분 단일 // 등록/수정 통합 다이얼로그: WbsTemplateDialog (wace WBSExcelImportPopUp.jsp 1:1) -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Plus, Trash2 } from "lucide-react"; import { toast } from "sonner"; @@ -20,6 +20,7 @@ import { PageHeader } from "@/components/common/PageHeader"; import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar"; import { wbsTemplateApi, TemplateRow } from "@/lib/api/wbsTemplate"; import { WbsTemplateDialog } from "@/components/project/WbsTemplateDialog"; +import { exportToExcel } from "@/lib/utils/excelExport"; const PRODUCT_GROUP = "0000001"; // 제품구분 @@ -29,12 +30,12 @@ const GRID_COLUMNS: DataGridColumn[] = [ { key: "wbs_task_cnt", label: "WBS", - width: "w-[100px]", + width: "w-[115px]", align: "center", renderType: "folder", // wace fnc_getFolderIcon }, { key: "writer_title", label: "등록자", width: "w-[180px]" }, - { key: "reg_date_title", label: "등록일", width: "w-[130px]", align: "center" }, + { key: "reg_date_title", label: "등록일", width: "w-[140px]", align: "center" }, ]; export default function WbsTemplatePage() { @@ -110,6 +111,20 @@ export default function WbsTemplatePage() { c.key === "wbs_task_cnt" ? { ...c, onClick: handleOpenEdit } : c ); + // ─── 하단 통계 ────────────────────────────────────────────── + // 템플릿 건수 / WBS 작업 합계 / 평균 WBS 작업수 + const templateSummary = useMemo(() => { + const count = rows.length; + const taskSum = rows.reduce((acc, r) => acc + Number((r as any).wbs_task_cnt || 0), 0); + const taskAvg = count === 0 ? 0 : taskSum / count; + const intFmt = (n: number) => n.toLocaleString(); + return [ + { label: "템플릿 건수", value: intFmt(count), suffix: "건" }, + { label: "WBS 작업 합계", value: intFmt(taskSum) }, + { label: "평균 WBS 작업수", value: taskAvg.toFixed(1) }, + ]; + }, [rows]); + return (
fetchList(filterProduct)} + onDownload={() => { + if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; } + const exportRows = rows.map((r) => { + const out: Record = {}; + GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; }); + return out; + }); + exportToExcel(exportRows, "WBS_템플릿.xlsx", "WBS_템플릿"); + }} + showChart />