148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
DialogDescription,
|
||
|
|
DialogFooter,
|
||
|
|
} from "@/components/ui/dialog";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Loader2 } from "lucide-react";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import apiClient from "@/lib/api/client";
|
||
|
|
|
||
|
|
interface BomHistoryItem {
|
||
|
|
id: string;
|
||
|
|
revision: string;
|
||
|
|
version: string;
|
||
|
|
change_type: string;
|
||
|
|
change_description: string;
|
||
|
|
changed_by: string;
|
||
|
|
changed_date: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface BomHistoryModalProps {
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
bomId: string | null;
|
||
|
|
tableName?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const CHANGE_TYPE_STYLE: Record<string, string> = {
|
||
|
|
"등록": "bg-blue-50 text-blue-600 ring-blue-200",
|
||
|
|
"수정": "bg-amber-50 text-amber-600 ring-amber-200",
|
||
|
|
"추가": "bg-emerald-50 text-emerald-600 ring-emerald-200",
|
||
|
|
"삭제": "bg-red-50 text-red-600 ring-red-200",
|
||
|
|
};
|
||
|
|
|
||
|
|
export function BomHistoryModal({ open, onOpenChange, bomId, tableName = "bom_history" }: BomHistoryModalProps) {
|
||
|
|
const [history, setHistory] = useState<BomHistoryItem[]>([]);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (open && bomId) {
|
||
|
|
loadHistory();
|
||
|
|
}
|
||
|
|
}, [open, bomId]);
|
||
|
|
|
||
|
|
const loadHistory = async () => {
|
||
|
|
if (!bomId) return;
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const res = await apiClient.get(`/bom/${bomId}/history`, { params: { tableName } });
|
||
|
|
if (res.data?.success) {
|
||
|
|
setHistory(res.data.data || []);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("[BomHistory] 로드 실패:", error);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatDate = (dateStr: string) => {
|
||
|
|
if (!dateStr) return "-";
|
||
|
|
try {
|
||
|
|
return new Date(dateStr).toLocaleString("ko-KR", {
|
||
|
|
year: "numeric",
|
||
|
|
month: "2-digit",
|
||
|
|
day: "2-digit",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
});
|
||
|
|
} catch {
|
||
|
|
return dateStr;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[650px]">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="text-base sm:text-lg">이력 관리</DialogTitle>
|
||
|
|
<DialogDescription className="text-xs sm:text-sm">
|
||
|
|
BOM 변경 이력을 확인합니다
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
<div className="max-h-[400px] overflow-auto">
|
||
|
|
{loading ? (
|
||
|
|
<div className="flex h-32 items-center justify-center">
|
||
|
|
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
||
|
|
</div>
|
||
|
|
) : history.length === 0 ? (
|
||
|
|
<div className="flex h-32 items-center justify-center">
|
||
|
|
<p className="text-sm text-gray-400">이력이 없습니다</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<table className="w-full text-xs">
|
||
|
|
<thead className="sticky top-0 bg-gray-50">
|
||
|
|
<tr className="border-b">
|
||
|
|
<th className="px-3 py-2.5 text-center text-[11px] font-semibold text-gray-500" style={{ width: "50px" }}>차수</th>
|
||
|
|
<th className="px-3 py-2.5 text-center text-[11px] font-semibold text-gray-500" style={{ width: "50px" }}>버전</th>
|
||
|
|
<th className="px-3 py-2.5 text-center text-[11px] font-semibold text-gray-500" style={{ width: "70px" }}>변경구분</th>
|
||
|
|
<th className="px-3 py-2.5 text-left text-[11px] font-semibold text-gray-500">변경내용</th>
|
||
|
|
<th className="px-3 py-2.5 text-center text-[11px] font-semibold text-gray-500" style={{ width: "70px" }}>변경자</th>
|
||
|
|
<th className="px-3 py-2.5 text-center text-[11px] font-semibold text-gray-500" style={{ width: "130px" }}>변경일시</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{history.map((item, idx) => (
|
||
|
|
<tr key={item.id} className={cn("border-b border-gray-100", idx % 2 === 0 ? "bg-white" : "bg-gray-50/30")}>
|
||
|
|
<td className="px-3 py-2.5 text-center tabular-nums">{item.revision || "-"}</td>
|
||
|
|
<td className="px-3 py-2.5 text-center tabular-nums">{item.version || "-"}</td>
|
||
|
|
<td className="px-3 py-2.5 text-center">
|
||
|
|
<span className={cn(
|
||
|
|
"inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset",
|
||
|
|
CHANGE_TYPE_STYLE[item.change_type] || "bg-gray-50 text-gray-500 ring-gray-200",
|
||
|
|
)}>
|
||
|
|
{item.change_type}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2.5 text-gray-700">{item.change_description || "-"}</td>
|
||
|
|
<td className="px-3 py-2.5 text-center text-gray-600">{item.changed_by || "-"}</td>
|
||
|
|
<td className="px-3 py-2.5 text-center text-gray-400">{formatDate(item.changed_date)}</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => onOpenChange(false)}
|
||
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||
|
|
>
|
||
|
|
닫기
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
);
|
||
|
|
}
|