프로젝트관리 — 진행관리/WBS템플릿에 logicstudio 스타일 DataGrid 적용
영업관리(fc959d88)와 동일 패턴을 프로젝트관리 2개 메뉴로 확장:
공통 DataGrid props:
- gridId 는 기존(project-progress-wbslist3 / project-wbs-template) 그대로 유지
- showColumnSettings, paginationStyle="range", pageSizeOptions=[10,15,20,50,100]
- onRefresh = fetchList(혹은 fetchList(filterProduct)), onDownload = exportToExcel(라벨 매핑)
- showChart
도메인별 summaryStats:
- 진행관리: 프로젝트 건수 / 수주수량 합계 / 입고율 평균(% 문자열 파싱)
- WBS 템플릿: 템플릿 건수 / WBS 작업 합계 / 평균 WBS 작업수
WBS 템플릿 systemColumnKeys: writer_title, reg_date_title (등록자/등록일 시스템 영역)
컬럼 폭 보정:
- ⋮⋮ 드래그 핸들 추가로 좁아진 4글자 한국어/영문 라벨 95~120px 로 확대
- 진행관리: 주문유형·제품구분·국내/해외·요청납기·E-BOM·M-BOM·제조1,2팀·제조3팀 등
- WBS: WBS 컬럼 100→115, 등록일 130→140
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import { PageHeader } from "@/components/common/PageHeader";
|
|||||||
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
|
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
|
||||||
import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
|
import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
|
||||||
import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt";
|
import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt";
|
||||||
|
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||||
|
|
||||||
// 진행관리 row → 정규화된 ProjectInfoData 매핑
|
// 진행관리 row → 정규화된 ProjectInfoData 매핑
|
||||||
const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({
|
const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({
|
||||||
@@ -45,32 +46,32 @@ const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({
|
|||||||
const GRID_COLUMNS: DataGridColumn[] = [
|
const GRID_COLUMNS: DataGridColumn[] = [
|
||||||
{ key: "project_no", label: "프로젝트번호", width: "w-[160px]", frozen: true },
|
{ key: "project_no", label: "프로젝트번호", width: "w-[160px]", frozen: true },
|
||||||
// 프로젝트정보 그룹
|
// 프로젝트정보 그룹
|
||||||
{ key: "category_name", label: "주문유형", width: "w-[90px]", align: "center" },
|
{ key: "category_name", label: "주문유형", width: "w-[115px]", align: "center" },
|
||||||
{ key: "product_name", label: "제품구분", width: "w-[100px]", align: "left" },
|
{ key: "product_name", label: "제품구분", width: "w-[115px]", align: "left" },
|
||||||
{ key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" },
|
{ key: "area_name", label: "국내/해외", width: "w-[115px]", align: "center" },
|
||||||
{ key: "reg_date", label: "접수일", width: "w-[110px]", align: "center" },
|
{ key: "reg_date", label: "접수일", width: "w-[115px]", align: "center" },
|
||||||
{ key: "customer_name", label: "고객사", width: "w-[140px]" },
|
{ 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_code", label: "품번", width: "w-[150px]" },
|
||||||
{ key: "product_item_name", label: "품명", width: "w-[180px]" },
|
{ key: "product_item_name", label: "품명", width: "w-[180px]" },
|
||||||
{ key: "serial_no", label: "S/N", width: "w-[150px]" },
|
{ key: "serial_no", label: "S/N", width: "w-[150px]" },
|
||||||
{ key: "contract_qty", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
|
{ key: "contract_qty", label: "수주수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||||
{ key: "req_del_date", label: "요청납기", width: "w-[110px]", align: "center" },
|
{ 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: "order_date", label: "발주일", width: "w-[115px]", align: "center" },
|
||||||
{ key: "receiving_rate", label: "입고율", width: "w-[90px]", align: "right" },
|
{ key: "receiving_rate", label: "입고율", width: "w-[100px]", align: "right" },
|
||||||
// 생산
|
// 생산
|
||||||
{ key: "production_team_12", label: "제조1,2팀", 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-[100px]", align: "center" },
|
{ key: "production_team_3", label: "제조3팀", width: "w-[115px]", align: "center" },
|
||||||
// 장비
|
// 장비
|
||||||
{ key: "assembly", label: "조립", width: "w-[90px]", align: "center" },
|
{ key: "assembly", label: "조립", width: "w-[100px]", align: "center" },
|
||||||
{ key: "verification", label: "검증", width: "w-[90px]", 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"; // 주문유형
|
const CATEGORY_GROUP = "0000167"; // 주문유형
|
||||||
@@ -145,6 +146,24 @@ export default function ProjectProgressPage() {
|
|||||||
setTimeout(() => fetchList(), 0);
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full gap-2 p-2">
|
<div className="flex flex-col h-full gap-2 p-2">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -245,6 +264,21 @@ export default function ProjectProgressPage() {
|
|||||||
showRowNumber
|
showRowNumber
|
||||||
emptyMessage="조건에 맞는 프로젝트가 없습니다."
|
emptyMessage="조건에 맞는 프로젝트가 없습니다."
|
||||||
gridId="project-progress-wbslist3"
|
gridId="project-progress-wbslist3"
|
||||||
|
showColumnSettings
|
||||||
|
paginationStyle="range"
|
||||||
|
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||||
|
summaryStats={progressSummary}
|
||||||
|
onRefresh={fetchList}
|
||||||
|
onDownload={() => {
|
||||||
|
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||||
|
const exportRows = rows.map((r) => {
|
||||||
|
const out: Record<string, any> = {};
|
||||||
|
GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; });
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
exportToExcel(exportRows, "진행관리.xlsx", "진행관리");
|
||||||
|
}}
|
||||||
|
showChart
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// 검색: 제품구분 단일
|
// 검색: 제품구분 단일
|
||||||
// 등록/수정 통합 다이얼로그: WbsTemplateDialog (wace WBSExcelImportPopUp.jsp 1:1)
|
// 등록/수정 통합 다이얼로그: 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 { Button } from "@/components/ui/button";
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -20,6 +20,7 @@ import { PageHeader } from "@/components/common/PageHeader";
|
|||||||
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
|
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
|
||||||
import { wbsTemplateApi, TemplateRow } from "@/lib/api/wbsTemplate";
|
import { wbsTemplateApi, TemplateRow } from "@/lib/api/wbsTemplate";
|
||||||
import { WbsTemplateDialog } from "@/components/project/WbsTemplateDialog";
|
import { WbsTemplateDialog } from "@/components/project/WbsTemplateDialog";
|
||||||
|
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||||
|
|
||||||
const PRODUCT_GROUP = "0000001"; // 제품구분
|
const PRODUCT_GROUP = "0000001"; // 제품구분
|
||||||
|
|
||||||
@@ -29,12 +30,12 @@ const GRID_COLUMNS: DataGridColumn[] = [
|
|||||||
{
|
{
|
||||||
key: "wbs_task_cnt",
|
key: "wbs_task_cnt",
|
||||||
label: "WBS",
|
label: "WBS",
|
||||||
width: "w-[100px]",
|
width: "w-[115px]",
|
||||||
align: "center",
|
align: "center",
|
||||||
renderType: "folder", // wace fnc_getFolderIcon
|
renderType: "folder", // wace fnc_getFolderIcon
|
||||||
},
|
},
|
||||||
{ key: "writer_title", label: "등록자", width: "w-[180px]" },
|
{ 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() {
|
export default function WbsTemplatePage() {
|
||||||
@@ -110,6 +111,20 @@ export default function WbsTemplatePage() {
|
|||||||
c.key === "wbs_task_cnt" ? { ...c, onClick: handleOpenEdit } : c
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full gap-2 p-2">
|
<div className="flex flex-col h-full gap-2 p-2">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -148,6 +163,22 @@ export default function WbsTemplatePage() {
|
|||||||
onCheckedChange={setCheckedIds}
|
onCheckedChange={setCheckedIds}
|
||||||
emptyMessage="등록된 WBS 템플릿이 없습니다."
|
emptyMessage="등록된 WBS 템플릿이 없습니다."
|
||||||
gridId="project-wbs-template"
|
gridId="project-wbs-template"
|
||||||
|
showColumnSettings
|
||||||
|
paginationStyle="range"
|
||||||
|
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||||
|
summaryStats={templateSummary}
|
||||||
|
systemColumnKeys={["writer_title", "reg_date_title"]}
|
||||||
|
onRefresh={() => fetchList(filterProduct)}
|
||||||
|
onDownload={() => {
|
||||||
|
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||||
|
const exportRows = rows.map((r) => {
|
||||||
|
const out: Record<string, any> = {};
|
||||||
|
GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; });
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
exportToExcel(exportRows, "WBS_템플릿.xlsx", "WBS_템플릿");
|
||||||
|
}}
|
||||||
|
showChart
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user