254 lines
9.0 KiB
TypeScript
254 lines
9.0 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, Plus, Trash2, Download, ShieldCheck } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface BomVersion {
|
|
id: string;
|
|
version_name: string;
|
|
revision: number;
|
|
status: string;
|
|
created_by: string;
|
|
created_date: string;
|
|
}
|
|
|
|
interface BomVersionModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
bomId: string | null;
|
|
tableName?: string;
|
|
detailTable?: string;
|
|
onVersionLoaded?: () => void;
|
|
}
|
|
|
|
const STATUS_STYLE: Record<string, { label: string; className: string }> = {
|
|
developing: { label: "개발중", className: "bg-red-50 text-red-600 ring-red-200" },
|
|
active: { label: "사용", className: "bg-emerald-50 text-emerald-600 ring-emerald-200" },
|
|
inactive: { label: "사용중지", className: "bg-gray-100 text-gray-500 ring-gray-200" },
|
|
};
|
|
|
|
export function BomVersionModal({ open, onOpenChange, bomId, tableName = "bom_version", detailTable = "bom_detail", onVersionLoaded }: BomVersionModalProps) {
|
|
const [versions, setVersions] = useState<BomVersion[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [creating, setCreating] = useState(false);
|
|
const [actionId, setActionId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (open && bomId) loadVersions();
|
|
}, [open, bomId]);
|
|
|
|
const loadVersions = async () => {
|
|
if (!bomId) return;
|
|
setLoading(true);
|
|
try {
|
|
const res = await apiClient.get(`/bom/${bomId}/versions`, { params: { tableName } });
|
|
if (res.data?.success) setVersions(res.data.data || []);
|
|
} catch (error) {
|
|
console.error("[BomVersion] 로드 실패:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateVersion = async () => {
|
|
if (!bomId) return;
|
|
setCreating(true);
|
|
try {
|
|
const res = await apiClient.post(`/bom/${bomId}/versions`, { tableName, detailTable });
|
|
if (res.data?.success) loadVersions();
|
|
} catch (error) {
|
|
console.error("[BomVersion] 생성 실패:", error);
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleLoadVersion = async (versionId: string) => {
|
|
if (!bomId) return;
|
|
setActionId(versionId);
|
|
try {
|
|
const res = await apiClient.post(`/bom/${bomId}/versions/${versionId}/load`, { tableName, detailTable });
|
|
if (res.data?.success) {
|
|
loadVersions();
|
|
onVersionLoaded?.();
|
|
}
|
|
} catch (error) {
|
|
console.error("[BomVersion] 불러오기 실패:", error);
|
|
} finally {
|
|
setActionId(null);
|
|
}
|
|
};
|
|
|
|
const handleActivateVersion = async (versionId: string) => {
|
|
if (!bomId || !confirm("이 버전을 사용 확정하시겠습니까?\n기존 사용중 버전은 사용중지로 변경됩니다.")) return;
|
|
setActionId(versionId);
|
|
try {
|
|
const res = await apiClient.post(`/bom/${bomId}/versions/${versionId}/activate`, { tableName });
|
|
if (res.data?.success) {
|
|
loadVersions();
|
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
|
}
|
|
} catch (error) {
|
|
console.error("[BomVersion] 사용 확정 실패:", error);
|
|
} finally {
|
|
setActionId(null);
|
|
}
|
|
};
|
|
|
|
const handleDeleteVersion = async (versionId: string) => {
|
|
if (!bomId || !confirm("이 버전을 삭제하시겠습니까?")) return;
|
|
setActionId(versionId);
|
|
try {
|
|
const res = await apiClient.delete(`/bom/${bomId}/versions/${versionId}`, { params: { tableName } });
|
|
if (res.data?.success) loadVersions();
|
|
} catch (error) {
|
|
console.error("[BomVersion] 삭제 실패:", error);
|
|
} finally {
|
|
setActionId(null);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
if (!dateStr) return "-";
|
|
try {
|
|
return new Date(dateStr).toLocaleDateString("ko-KR", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
});
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
};
|
|
|
|
const getStatus = (status: string) => STATUS_STYLE[status] || STATUS_STYLE.inactive;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[550px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">버전 관리</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
BOM 버전을 관리합니다. 불러오기로 특정 버전을 복원할 수 있습니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="max-h-[400px] space-y-2 overflow-auto">
|
|
{loading ? (
|
|
<div className="flex h-32 items-center justify-center">
|
|
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
|
</div>
|
|
) : versions.length === 0 ? (
|
|
<div className="flex h-32 items-center justify-center">
|
|
<p className="text-sm text-gray-400">생성된 버전이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
versions.map((ver) => {
|
|
const st = getStatus(ver.status);
|
|
const isActing = actionId === ver.id;
|
|
return (
|
|
<div
|
|
key={ver.id}
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg border p-3 transition-colors",
|
|
ver.status === "active" ? "border-emerald-200 bg-emerald-50/30" : "border-gray-200",
|
|
)}
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-gray-900">
|
|
Version {ver.version_name}
|
|
</span>
|
|
<span className={cn(
|
|
"inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset",
|
|
st.className,
|
|
)}>
|
|
{st.label}
|
|
</span>
|
|
</div>
|
|
<div className="mt-0.5 flex gap-3 text-[11px] text-gray-400">
|
|
<span>차수: {ver.revision}</span>
|
|
<span>등록일: {formatDate(ver.created_date)}</span>
|
|
{ver.created_by && <span>등록자: {ver.created_by}</span>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleLoadVersion(ver.id)}
|
|
disabled={isActing}
|
|
className="h-7 gap-1 px-2 text-[10px]"
|
|
>
|
|
{isActing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Download className="h-3 w-3" />}
|
|
불러오기
|
|
</Button>
|
|
{ver.status === "active" ? (
|
|
<span className="flex h-7 items-center rounded-md bg-emerald-50 px-2 text-[10px] font-medium text-emerald-600 ring-1 ring-inset ring-emerald-200">
|
|
사용중
|
|
</span>
|
|
) : (
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleActivateVersion(ver.id)}
|
|
disabled={isActing}
|
|
className="h-7 gap-1 px-2 text-[10px] border-emerald-300 text-emerald-600 hover:bg-emerald-50"
|
|
>
|
|
<ShieldCheck className="h-3 w-3" />
|
|
사용 확정
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => handleDeleteVersion(ver.id)}
|
|
disabled={isActing}
|
|
className="h-7 gap-1 px-2 text-[10px]"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
삭제
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
onClick={handleCreateVersion}
|
|
disabled={creating}
|
|
className="h-8 flex-1 gap-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{creating ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : <Plus className="mr-1 h-4 w-4" />}
|
|
신규 버전 생성
|
|
</Button>
|
|
<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>
|
|
);
|
|
}
|