Files
wace_rps/frontend/app/(main)/COMPANY_16/project/progress/page.tsx
T
hjjeong a1ace22626 프로젝트관리>진행관리 메뉴 신설 — wace projectMgmtWbsList3 1:1 이식
· 그리드: 8그룹 18셀 평탄화 (운영판 projectMgmtWbsGridList SQL 1:1)
· 검색폼: 11필드 (년도/프로젝트번호/주문유형/고객사/제품구분/요청납기/국내해외/유무상/품번/품명/S/N)
· 영업관리 패턴 통일: PartSelect 단일 search_partObjId, customer_mng 단일 LEFT JOIN
· client_mng/supply_mng 분기 → customer_mng로 흡수, CODE_NAME() 함수 직접 사용
· 행 클릭 동작은 P1.5 별도 결정 (영업관리 OrderRegistDialog 재사용 후보)
· PMS_WBS_TASK/SETUP_WBS_TASK 의존 컬럼은 그리드 표시 컬럼에 없어 P2(WBS관리) 보류

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:22:06 +09:00

239 lines
9.9 KiB
TypeScript

"use client";
// 진행관리 (wace projectMgmtWbsList3.jsp + projectMgmtWbsGridList 1:1 이식)
// 원본:
// - JSP: /Users/jhj/wace_plm/WebContent/WEB-INF/view/project/projectMgmtWbsList3.jsp (349줄)
// - 매퍼: wace_plm/src/com/pms/mapper/project.xml:3854 projectMgmtWbsGridList
// GAP: docs/migration/project/00-gap.md
//
// 그리드: 8그룹 18셀 평탄화 (DataGrid가 그룹 헤더 미지원 → 라벨 prefix로 표현)
// 검색폼: 11필드 (1행 6 + 2행 5)
// 행 클릭: P1.5에서 영업관리 OrderRegistDialog 재사용 검토 — 현재 미연결
import React, { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Search, Loader2, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { PartSelect } from "@/components/common/PartSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt";
// wace projectMgmtWbsList3.jsp 컬럼 정의 1:1 (8그룹 → 평탄화, 그룹명은 라벨 prefix)
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: "customer_name", label: "고객사", width: "w-[140px]" },
{ key: "free_of_charge", label: "유/무상", width: "w-[80px]", 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: "ebom_status", label: "E-BOM", width: "w-[100px]", align: "center" },
// 생산관리
{ key: "mbom_status", label: "M-BOM", width: "w-[100px]", align: "center" },
// 구매
{ key: "order_date", label: "발주일", width: "w-[110px]", align: "center" },
{ key: "receiving_rate", label: "입고율", width: "w-[90px]", 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: "assembly", label: "조립", width: "w-[90px]", align: "center" },
{ key: "verification", label: "검증", width: "w-[90px]", align: "center" },
// 출하
{ key: "shipment_date", label: "출하일", width: "w-[110px]", align: "center" },
];
const CATEGORY_GROUP = "0000167"; // 주문유형
const PRODUCT_GROUP = "0000001"; // 제품구분
// wace L229: 시스템년도 ±4 (운영판은 sysYear-4 ~ sysYear). RPS는 sysYear±4로 여유.
const YEAR_OPTIONS = (() => {
const cur = new Date().getFullYear();
const arr: string[] = [];
for (let y = cur + 4; y >= cur - 4; y--) arr.push(String(y));
return arr;
})();
const EMPTY_FILTER: ProgressListFilter = {
Year: "", project_nos: "", category_cd: "", customer_objid: "", product: "",
contract_start_date: "", contract_end_date: "",
area_cd: "", free_of_charge: "",
search_partObjId: "", serial_no: "",
};
export default function ProjectProgressPage() {
const [rows, setRows] = useState<ProgressRow[]>([]);
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState<ProgressListFilter>(EMPTY_FILTER);
const [projectNoOptions, setProjectNoOptions] = useState<SmartSelectOption[]>([]);
const fetchList = useCallback(async () => {
setLoading(true);
try {
const data = await projectMgmtApi.list(filter);
setRows(data);
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
} finally { setLoading(false); }
}, [filter]);
useEffect(() => { fetchList(); /* eslint-disable-next-line */ }, []);
// 프로젝트번호 셀렉트 옵션 (wace common.getCusProjectNoList 대응)
useEffect(() => {
projectMgmtApi.projectNoOptions()
.then((opts) => setProjectNoOptions(opts.map((o) => ({ code: o.value, label: o.label }))))
.catch(() => {});
}, []);
const handleReset = () => {
setFilter(EMPTY_FILTER);
setTimeout(() => fetchList(), 0);
};
return (
<div className="flex flex-col h-full">
{/* 검색폼 — wace projectMgmtWbsList3.jsp:222-313 활성 11필드 */}
<div className="border-b bg-card px-4 py-3">
<div className="grid grid-cols-6 gap-3 text-sm">
{/* 1행 */}
<Field label="년도">
<select
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
value={filter.Year ?? ""}
onChange={(e) => setFilter({ ...filter, Year: e.target.value })}
>
<option value=""></option>
{YEAR_OPTIONS.map((y) => <option key={y} value={y}>{y}</option>)}
</select>
</Field>
<Field label="프로젝트번호">
<SmartSelect
options={projectNoOptions}
value={filter.project_nos ?? ""}
onValueChange={(v) => setFilter({ ...filter, project_nos: v })}
placeholder="전체"
/>
</Field>
<Field label="주문유형">
<CommCodeSelect
groupId={CATEGORY_GROUP}
value={filter.category_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, category_cd: v })}
/>
</Field>
<Field label="고객사">
<CustomerSelect
value={filter.customer_objid ?? ""}
onValueChange={(v) => setFilter({ ...filter, customer_objid: v })}
/>
</Field>
<Field label="제품구분">
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filter.product ?? ""}
onValueChange={(v) => setFilter({ ...filter, product: v })}
/>
</Field>
<Field label="요청납기일">
<div className="flex items-center gap-1">
<Input type="date" value={filter.contract_start_date ?? ""} onChange={(e) => setFilter({ ...filter, contract_start_date: e.target.value })} />
<span className="text-xs text-muted-foreground">~</span>
<Input type="date" value={filter.contract_end_date ?? ""} onChange={(e) => setFilter({ ...filter, contract_end_date: e.target.value })} />
</div>
</Field>
{/* 2행 */}
<Field label="국내/해외">
<select
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
value={filter.area_cd ?? ""}
onChange={(e) => setFilter({ ...filter, area_cd: e.target.value })}
>
<option value=""></option>
<option value="국내"></option>
<option value="해외"></option>
</select>
</Field>
<Field label="유/무상">
<select
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
value={filter.free_of_charge ?? ""}
onChange={(e) => setFilter({ ...filter, free_of_charge: e.target.value })}
>
<option value=""></option>
<option value="유상"></option>
<option value="무상"></option>
</select>
</Field>
<Field label="품번">
<PartSelect
mode="partNo"
value={filter.search_partObjId ?? ""}
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
/>
</Field>
<Field label="품명">
<PartSelect
mode="partName"
value={filter.search_partObjId ?? ""}
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
/>
</Field>
<Field label="S/N">
<Input
value={filter.serial_no ?? ""}
onChange={(e) => setFilter({ ...filter, serial_no: e.target.value })}
placeholder="S/N LIKE"
/>
</Field>
{/* 액션 */}
<div className="flex items-end justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleReset}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={fetchList} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
</div>
</div>
</div>
{/* 그리드 (8그룹 18셀 평탄화) */}
<div className="flex-1 min-h-0 p-2">
<DataGrid
columns={GRID_COLUMNS}
data={rows}
loading={loading}
showRowNumber
emptyMessage="조건에 맞는 프로젝트가 없습니다."
gridId="project-progress-wbslist3"
/>
</div>
</div>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>
{children}
</div>
);
}