ERP-node/frontend/components/admin/TableLogViewer.tsx

272 lines
9.5 KiB
TypeScript

"use client";
import { useState, useEffect } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { toast } from "sonner";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { History, RefreshCw, Filter, X } from "lucide-react";
interface TableLogViewerProps {
tableName: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}
interface LogData {
log_id: number;
operation_type: string;
original_id: string;
changed_column?: string;
old_value?: string;
new_value?: string;
changed_by?: string;
changed_at: string;
ip_address?: string;
}
export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewerProps) {
const [logs, setLogs] = useState<LogData[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(20);
const [totalPages, setTotalPages] = useState(0);
// 필터 상태
const [operationType, setOperationType] = useState<string>("");
const [startDate, setStartDate] = useState<string>("");
const [endDate, setEndDate] = useState<string>("");
const [changedBy, setChangedBy] = useState<string>("");
const [originalId, setOriginalId] = useState<string>("");
// 로그 데이터 로드
const loadLogs = async () => {
if (!tableName) return;
setLoading(true);
try {
const response = await tableManagementApi.getLogData(tableName, {
page,
size: pageSize,
operationType: operationType || undefined,
startDate: startDate || undefined,
endDate: endDate || undefined,
changedBy: changedBy || undefined,
originalId: originalId || undefined,
});
if (response.success && response.data) {
setLogs(response.data.data);
setTotal(response.data.total);
setTotalPages(response.data.totalPages);
} else {
toast.error(response.message || "로그 데이터를 불러올 수 없습니다.");
}
} catch (error) {
toast.error("로그 데이터 조회 중 오류가 발생했습니다.");
} finally {
setLoading(false);
}
};
// 다이얼로그가 열릴 때 로그 로드
useEffect(() => {
if (open && tableName) {
loadLogs();
}
}, [open, tableName, page]);
// 필터 초기화
const resetFilters = () => {
setOperationType("");
setStartDate("");
setEndDate("");
setChangedBy("");
setOriginalId("");
setPage(1);
};
// 작업 타입에 따른 뱃지 색상
const getOperationBadge = (type: string) => {
switch (type) {
case "INSERT":
return <Badge className="bg-green-500"></Badge>;
case "UPDATE":
return <Badge className="bg-blue-500"></Badge>;
case "DELETE":
return <Badge className="bg-red-500"></Badge>;
default:
return <Badge>{type}</Badge>;
}
};
// 날짜 포맷팅
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString("ko-KR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[90vh] max-w-6xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<History className="h-5 w-5" />
{tableName} -
</DialogTitle>
</DialogHeader>
{/* 필터 영역 */}
<div className="space-y-3 rounded-lg border p-4">
<div className="mb-2 flex items-center justify-between">
<h4 className="flex items-center gap-2 text-sm font-semibold">
<Filter className="h-4 w-4" />
</h4>
<Button variant="ghost" size="sm" onClick={resetFilters}>
<X className="mr-1 h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
<div>
<label className="mb-1 block text-sm text-gray-600"> </label>
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__all__" 사용 */}
<Select
value={operationType || "__all__"}
onValueChange={(value) => setOperationType(value === "__all__" ? "" : value)}
>
<SelectTrigger>
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__"></SelectItem>
<SelectItem value="INSERT"></SelectItem>
<SelectItem value="UPDATE"></SelectItem>
<SelectItem value="DELETE"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-1 block text-sm text-gray-600"> </label>
<Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
</div>
<div>
<label className="mb-1 block text-sm text-gray-600"> </label>
<Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
</div>
<div>
<label className="mb-1 block text-sm text-gray-600"></label>
<Input placeholder="사용자 ID" value={changedBy} onChange={(e) => setChangedBy(e.target.value)} />
</div>
<div>
<label className="mb-1 block text-sm text-gray-600"> ID</label>
<Input placeholder="레코드 ID" value={originalId} onChange={(e) => setOriginalId(e.target.value)} />
</div>
<div className="flex items-end">
<Button onClick={loadLogs} className="w-full">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* 로그 테이블 */}
<div className="flex-1 overflow-auto rounded-lg border">
{loading ? (
<div className="flex h-64 items-center justify-center">
<LoadingSpinner />
</div>
) : logs.length === 0 ? (
<div className="flex h-64 items-center justify-center text-gray-500"> .</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]"></TableHead>
<TableHead> ID</TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead>IP</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.map((log) => (
<TableRow key={log.log_id}>
<TableCell>{getOperationBadge(log.operation_type)}</TableCell>
<TableCell className="font-mono text-sm">{log.original_id}</TableCell>
<TableCell className="text-sm">{log.changed_column || "-"}</TableCell>
<TableCell className="max-w-[200px] truncate text-sm" title={log.old_value}>
{log.old_value || "-"}
</TableCell>
<TableCell className="max-w-[200px] truncate text-sm" title={log.new_value}>
{log.new_value || "-"}
</TableCell>
<TableCell className="text-sm">{log.changed_by || "system"}</TableCell>
<TableCell className="text-sm">{formatDate(log.changed_at)}</TableCell>
<TableCell className="font-mono text-xs">{log.ip_address || "-"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
{/* 페이지네이션 */}
<div className="flex items-center justify-between border-t pt-4">
<div className="text-sm text-gray-600">
{total} ( {page} / {totalPages})
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1 || loading}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages || loading}
>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}