diff --git a/backend-node/src/controllers/mbomController.ts b/backend-node/src/controllers/mbomController.ts index 7fd963b9..3dad8ed5 100644 --- a/backend-node/src/controllers/mbomController.ts +++ b/backend-node/src/controllers/mbomController.ts @@ -53,6 +53,19 @@ export async function getTree(req: AuthenticatedRequest, res: Response) { } } +// PR-B4 — 변경이력 조회 (운영 getMbomHistory.do 1:1) +export async function getHistory(req: AuthenticatedRequest, res: Response) { + try { + const projectObjid = String(req.params.projectObjid ?? "").trim(); + if (!projectObjid) return res.status(400).json({ success: false, message: "projectObjid 누락" }); + const data = await svc.getHistory(projectObjid); + return res.json({ success: true, data }); + } catch (e: any) { + logger.error("M-BOM 변경이력 조회 실패", { error: e.message }); + return res.status(500).json({ success: false, message: e.message }); + } +} + // PR-B1 — 본 편집 저장 (운영 saveMbom.do 1:1) export async function save(req: AuthenticatedRequest, res: Response) { try { diff --git a/backend-node/src/routes/productionMbomRoutes.ts b/backend-node/src/routes/productionMbomRoutes.ts index ee8e0892..58a98af4 100644 --- a/backend-node/src/routes/productionMbomRoutes.ts +++ b/backend-node/src/routes/productionMbomRoutes.ts @@ -14,5 +14,6 @@ router.get("/list", ctrl.getList); router.get("/detail/:objid", ctrl.getDetail); router.get("/tree/:objid", ctrl.getTree); router.post("/save", ctrl.save); // PR-B1 본 편집 저장 +router.get("/history/:projectObjid", ctrl.getHistory); // PR-B4 변경이력 조회 export default router; diff --git a/backend-node/src/services/mbomService.ts b/backend-node/src/services/mbomService.ts index 03aa0c1c..340fcc55 100644 --- a/backend-node/src/services/mbomService.ts +++ b/backend-node/src/services/mbomService.ts @@ -1208,6 +1208,48 @@ export async function save(payload: MbomSavePayload, sessionUserId: string): Pro } } +// ─── 변경이력 조회 (PR-B4) ────────────────────────────────── +// +// 매퍼 productionplanning.getMbomHistory (3448~3470) 1:1. +// project_objid 로 그 프로젝트의 모든 mbom_header 변경이력 시간순 (최신 우선). + +export interface MbomHistoryRow { + objid: string; + mbom_header_objid: string; + change_type: string; + change_description: string | null; + change_user: string | null; + change_user_name: string | null; + change_date: string; + mbom_part_no: string | null; + mbom_regdate: string | null; +} + +export async function getHistory(projectObjid: string): Promise { + const pool = getPool(); + const sql = ` + SELECT + MH.OBJID AS objid, + MH.MBOM_HEADER_OBJID AS mbom_header_objid, + MH.CHANGE_TYPE AS change_type, + MH.CHANGE_DESCRIPTION AS change_description, + MH.CHANGE_USER AS change_user, + COALESCE( + (SELECT USER_NAME FROM USER_INFO WHERE USER_ID = MH.CHANGE_USER LIMIT 1), + MH.CHANGE_USER + ) AS change_user_name, + TO_CHAR(MH.CHANGE_DATE, 'YYYY-MM-DD HH24:MI:SS') AS change_date, + MHD.MBOM_NO AS mbom_part_no, + TO_CHAR(MHD.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS mbom_regdate + FROM MBOM_HISTORY MH + INNER JOIN MBOM_HEADER MHD ON MH.MBOM_HEADER_OBJID = MHD.OBJID + WHERE MHD.PROJECT_OBJID = $1 + ORDER BY MH.CHANGE_DATE DESC + `; + const r = await pool.query(sql, [projectObjid]); + return r.rows; +} + async function insertHistory( client: any, mbomHeaderObjid: string, diff --git a/frontend/components/production/MbomDetailDialog.tsx b/frontend/components/production/MbomDetailDialog.tsx index 988077a9..bd0f430a 100644 --- a/frontend/components/production/MbomDetailDialog.tsx +++ b/frontend/components/production/MbomDetailDialog.tsx @@ -22,10 +22,11 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Loader2, Folder, Pencil, Save, X } from "lucide-react"; +import { Loader2, Folder, Pencil, Save, X, History } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { mbomApi, MbomDetail, MbomTreeResponse, MbomBomDataType, MbomTreeRow, MbomSaveRow } from "@/lib/api/mbom"; +import { MbomHistoryDialog } from "./MbomHistoryDialog"; interface Props { open: boolean; @@ -58,6 +59,7 @@ export function MbomDetailDialog({ open, onOpenChange, projectObjid, onSaved }: const [saving, setSaving] = useState(false); const [editableRows, setEditableRows] = useState([]); const [dirty, setDirty] = useState(false); + const [historyOpen, setHistoryOpen] = useState(false); const loadTree = (objid: string) => { setLoading(true); @@ -214,6 +216,11 @@ export function MbomDetailDialog({ open, onOpenChange, projectObjid, onSaved }: )}
+ {!editMode && ( + + )} {canEdit && !editMode && ( + + ); } diff --git a/frontend/components/production/MbomHistoryDialog.tsx b/frontend/components/production/MbomHistoryDialog.tsx new file mode 100644 index 00000000..d618e0a7 --- /dev/null +++ b/frontend/components/production/MbomHistoryDialog.tsx @@ -0,0 +1,104 @@ +"use client"; + +// 생산관리 > M-BOM 관리 — 변경이력 다이얼로그 (PR-B4). +// +// 운영판 매퍼 productionplanning.getMbomHistory (3448~3470) 1:1. +// 프로젝트의 모든 mbom_header 변경이력 시간순(최신 우선) 표시. +// CREATE / UPDATE + 설명 + 변경자 + 변경일 + M-BOM 품번. + +import React, { useEffect, useState } from "react"; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Loader2, History } from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { mbomApi, MbomHistoryRow } from "@/lib/api/mbom"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + projectObjid: string | null; +} + +const CHANGE_TYPE_BADGE: Record = { + CREATE: { text: "생성", color: "bg-blue-600" }, + UPDATE: { text: "수정", color: "bg-amber-600" }, +}; + +export function MbomHistoryDialog({ open, onOpenChange, projectObjid }: Props) { + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!open || !projectObjid) { setRows([]); return; } + let alive = true; + setLoading(true); + mbomApi.getHistory(projectObjid) + .then(data => { if (alive) setRows(data); }) + .catch((e: any) => toast.error(e?.response?.data?.message ?? e?.message ?? "변경이력 조회 실패")) + .finally(() => { if (alive) setLoading(false); }); + return () => { alive = false; }; + }, [open, projectObjid]); + + return ( + + + + + + M-BOM 변경이력 + 총 {rows.length.toLocaleString()}건 + + + +
+ {loading ? ( +
+ +
+ ) : rows.length === 0 ? ( +
+ 변경이력이 없습니다. +
+ ) : ( + + + + + + + + + + + + {rows.map((r, idx) => { + const badge = CHANGE_TYPE_BADGE[r.change_type] ?? { text: r.change_type, color: "bg-slate-500" }; + return ( + + + + + + + + ); + })} + +
변경일시유형변경 내용변경자M-BOM 품번
{r.change_date} + + {badge.text} + + {r.change_description ?? ""}{r.change_user_name ?? r.change_user ?? ""}{r.mbom_part_no ?? ""}
+ )} +
+ + + + +
+
+ ); +} diff --git a/frontend/lib/api/mbom.ts b/frontend/lib/api/mbom.ts index 12e077ab..2aa3b521 100644 --- a/frontend/lib/api/mbom.ts +++ b/frontend/lib/api/mbom.ts @@ -232,4 +232,22 @@ export const mbomApi = { const res = await apiClient.post("/production/mbom/save", payload); return res.data?.data as MbomSaveResult; }, + async getHistory(projectObjid: string): Promise { + const res = await apiClient.get(`/production/mbom/history/${encodeURIComponent(projectObjid)}`); + return (res.data?.data ?? []) as MbomHistoryRow[]; + }, }; + +// ─── 변경이력 (PR-B4) ─────────────────────────────────────── + +export interface MbomHistoryRow { + objid: string; + mbom_header_objid: string; + change_type: string; // CREATE | UPDATE + change_description: string | null; + change_user: string | null; + change_user_name: string | null; + change_date: string; + mbom_part_no: string | null; + mbom_regdate: string | null; +}