Files
wace_rps/frontend/components/production/MbomAssignDialog.tsx
T
hjjeong b38f5957f2 구매관리 7메뉴 신규 + M-BOM PR-B3·B5 + 발주관리 DataGrid 통일 + 생산계획&실적 라우트
구매관리 (wace 1:1)
- backend: services/purchaseService.ts (7 list + 옵션 3종) + controllers/purchaseController.ts + routes/purchaseRoutes.ts (/api/purchase 마운트)
- frontend: lib/api/purchase.ts + 7 page.tsx (list/quote-request/proposal/inbound/inbound-by-item/inbound-by-date/project-status)
- 영업관리 4메뉴 DataGrid 패턴 통일 — pageSizeOptions=[10,15,20,50,100], emptyMessage, showColumnSettings/summaryStats/onRefresh/onDownload/showChart
- 마스터단독 데이터(sales_request_master, project_mgmt+mbom_detail) 노출, detail/part 누락 테이블 의존은 빈 그리드 + UI

발주관리 (purchase/order/page.tsx)
- EDataTable → DataGrid 교체 + logicstudio 6종 props + 날짜/숫자 pre-format

M-BOM PR-B3 — 구매리스트 생성 (wace createPurchaseListFromMBom.do 1:1)
- mbomService.createSalesRequest + controller + route POST /api/production/mbom/sales-request
- 단건 체크 + 1:1 강제 + R-YYYYMMDD-NNN 채번 + sales_request_master 단건 INSERT
- production/mbom/page.tsx 에 [구매리스트 생성] 버튼

M-BOM PR-B5 — BOM 할당 (mBomEbomSelectPopup.do)
- mbomService.searchAssignableEboms/assignBom + controller + routes
- MbomAssignDialog 신규, MbomDetailDialog 통합

생산관리 4메뉴 라우트 (생산계획&실적, 소요량)
- prodPlanResultService/Controller + productionPlanResultRoutes (planResult/mbomReq)
- mbomRequirementService + 4 page.tsx (prod-plan-result, prod-plan-result-equip, raw-material-requirement, semi-product-requirement)
- lib/api/prodPlanResult.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:31:12 +09:00

191 lines
8.6 KiB
TypeScript

"use client";
// 생산관리 > M-BOM 관리 — BOM 할당 다이얼로그 (PR-B5).
//
// 운영판 mBomEbomSelectPopup.jsp + assignEbomToMbom.do 1:1.
// 프로젝트에 source E-BOM (part_bom_report) 을 지정 → project_mgmt.source_bom_type='EBOM'.
// 할당 후 M-BOM 본 다이얼로그 트리가 ASSIGNED_EBOM 분기로 자동 표시됨.
//
// (M-BOM 할당은 PR-B5 v2 예정 — 우선 E-BOM 만)
import React, { useEffect, useState } from "react";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Loader2, Search, Link as LinkIcon } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { mbomApi, AssignableEbomRow } from "@/lib/api/mbom";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
projectObjid: string | null;
currentSourceEbomObjid?: string | null; // 이미 할당돼있으면 그 행 하이라이트
onAssigned: () => void; // 할당 성공 후 부모가 트리 재조회
}
export function MbomAssignDialog({
open, onOpenChange, projectObjid, currentSourceEbomObjid, onAssigned,
}: Props) {
const [filter, setFilter] = useState({
search_part_no: "", search_part_name: "", search_material: "",
});
const [rows, setRows] = useState<AssignableEbomRow[]>([]);
const [loading, setLoading] = useState(false);
const [selectedObjid, setSelectedObjid] = useState<string | null>(null);
const [assigning, setAssigning] = useState(false);
const search = (override?: Partial<typeof filter>) => {
const f = { ...filter, ...override };
setLoading(true);
mbomApi.searchAssignableEboms(f)
.then(setRows)
.catch((e: any) => toast.error(e?.response?.data?.message ?? e?.message ?? "E-BOM 조회 실패"))
.finally(() => setLoading(false));
};
useEffect(() => {
if (!open) {
setRows([]); setSelectedObjid(null);
setFilter({ search_part_no: "", search_part_name: "", search_material: "" });
return;
}
setSelectedObjid(currentSourceEbomObjid ?? null);
search();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, currentSourceEbomObjid]);
const handleAssign = async () => {
if (!projectObjid) return;
if (!selectedObjid) { toast.error("E-BOM 한 건을 선택해주세요"); return; }
if (selectedObjid === currentSourceEbomObjid) {
toast.info("현재 할당된 E-BOM 과 동일합니다");
return;
}
setAssigning(true);
try {
await mbomApi.assignBom({
project_obj_id: projectObjid,
source_bom_type: "EBOM",
source_bom_obj_id: selectedObjid,
});
toast.success("E-BOM 이 할당되었습니다");
onAssigned();
onOpenChange(false);
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "할당 실패");
} finally {
setAssigning(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[1100px] w-[95vw] max-h-[85vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="bg-blue-600 px-4 py-3">
<DialogTitle className="text-white flex items-center gap-2">
<LinkIcon className="w-4 h-4" />
BOM E-BOM
{currentSourceEbomObjid && (
<span className="text-xs font-normal opacity-90">· : {currentSourceEbomObjid}</span>
)}
</DialogTitle>
</DialogHeader>
{/* 검색 */}
<div className="flex items-center gap-2 border-b bg-muted/30 px-4 py-2">
<span className="text-xs text-muted-foreground"></span>
<Input
className="h-7 text-xs w-[150px]"
value={filter.search_part_no}
onChange={(e) => setFilter({ ...filter, search_part_no: e.target.value })}
onKeyDown={(e) => { if (e.key === "Enter") search(); }}
/>
<span className="text-xs text-muted-foreground"></span>
<Input
className="h-7 text-xs w-[180px]"
value={filter.search_part_name}
onChange={(e) => setFilter({ ...filter, search_part_name: e.target.value })}
onKeyDown={(e) => { if (e.key === "Enter") search(); }}
/>
<span className="text-xs text-muted-foreground"></span>
<Input
className="h-7 text-xs w-[120px]"
value={filter.search_material}
onChange={(e) => setFilter({ ...filter, search_material: e.target.value })}
onKeyDown={(e) => { if (e.key === "Enter") search(); }}
/>
<Button size="sm" onClick={() => search()} disabled={loading}>
{loading ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <Search className="w-3 h-3 mr-1" />}
</Button>
<div className="ml-auto text-xs text-muted-foreground">
{rows.length.toLocaleString()} ( 100)
</div>
</div>
{/* 그리드 */}
<div className="flex-1 min-h-0 overflow-auto">
<table className="text-xs border-collapse w-full">
<thead className="bg-yellow-100 dark:bg-yellow-900/30 sticky top-0">
<tr>
<th className="border px-2 py-1.5 w-[40px] text-center"></th>
<th className="border px-2 py-1.5 w-[90px] text-center"></th>
<th className="border px-2 py-1.5 w-[150px] text-left"></th>
<th className="border px-2 py-1.5 text-left"></th>
<th className="border px-2 py-1.5 w-[100px] text-left"></th>
<th className="border px-2 py-1.5 w-[100px] text-left"></th>
<th className="border px-2 py-1.5 w-[80px] text-center"></th>
<th className="border px-2 py-1.5 w-[100px] text-center"></th>
<th className="border px-2 py-1.5 w-[80px] text-center"></th>
</tr>
</thead>
<tbody>
{loading && rows.length === 0 ? (
<tr><td colSpan={9} className="py-12 text-center"><Loader2 className="inline w-5 h-5 animate-spin" /></td></tr>
) : rows.length === 0 ? (
<tr><td colSpan={9} className="py-8 text-center text-muted-foreground"> E-BOM .</td></tr>
) : rows.map(r => {
const isSel = selectedObjid === r.objid;
const isCurrent = currentSourceEbomObjid === r.objid;
return (
<tr key={r.objid}
className={cn("cursor-pointer hover:bg-muted/30",
isSel && "bg-blue-100 dark:bg-blue-950/40",
!isSel && isCurrent && "bg-emerald-50 dark:bg-emerald-950/20")}
onClick={() => setSelectedObjid(r.objid)}>
<td className="border px-2 py-1 text-center">
<input type="radio" checked={isSel} onChange={() => setSelectedObjid(r.objid)} />
</td>
<td className="border px-2 py-1 text-center">{r.product_name ?? ""}</td>
<td className="border px-2 py-1 whitespace-nowrap">{r.part_no}
{isCurrent && <span className="ml-1 text-[10px] text-emerald-600">()</span>}
</td>
<td className="border px-2 py-1">{r.part_name}</td>
<td className="border px-2 py-1">{r.material}</td>
<td className="border px-2 py-1">{r.supplier}</td>
<td className="border px-2 py-1 text-center">{r.revision ?? ""}</td>
<td className="border px-2 py-1 text-center tabular-nums">{r.reg_date ?? ""}</td>
<td className="border px-2 py-1 text-center">{r.writer_name ?? ""}</td>
</tr>
);
})}
</tbody>
</table>
</div>
<DialogFooter className="border-t bg-muted/20 px-4 py-3 sm:justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}></Button>
<Button onClick={handleAssign} disabled={!selectedObjid || assigning}>
{assigning ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <LinkIcon className="w-3 h-3 mr-1" />}
{currentSourceEbomObjid ? "할당 변경" : "할당"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}