ERP-node/frontend/app/(main)/admin/batchmng/page.tsx

356 lines
12 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import {
Plus,
Search,
Play,
Pause,
Edit,
Trash2,
RefreshCw,
Clock,
Database,
ArrowRight
} from "lucide-react";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import {
BatchAPI,
BatchConfig,
BatchMapping,
} from "@/lib/api/batch";
export default function BatchManagementPage() {
const router = useRouter();
// 상태 관리
const [batchConfigs, setBatchConfigs] = useState<BatchConfig[]>([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [executingBatch, setExecutingBatch] = useState<number | null>(null);
// 페이지 로드 시 배치 목록 조회
useEffect(() => {
loadBatchConfigs();
}, [currentPage, searchTerm]);
// 배치 설정 목록 조회
const loadBatchConfigs = async () => {
setLoading(true);
try {
const response = await BatchAPI.getBatchConfigs({
page: currentPage,
limit: 10,
search: searchTerm || undefined,
});
if (response.success && response.data) {
setBatchConfigs(response.data);
if (response.pagination) {
setTotalPages(response.pagination.totalPages);
}
} else {
setBatchConfigs([]);
}
} catch (error) {
console.error("배치 목록 조회 실패:", error);
toast.error("배치 목록을 불러오는데 실패했습니다.");
setBatchConfigs([]);
} finally {
setLoading(false);
}
};
// 배치 수동 실행
const executeBatch = async (batchId: number) => {
setExecutingBatch(batchId);
try {
const response = await BatchAPI.executeBatchConfig(batchId);
if (response.success) {
toast.success(`배치가 성공적으로 실행되었습니다! (처리: ${response.data?.totalRecords}개, 성공: ${response.data?.successRecords}개)`);
} else {
toast.error("배치 실행에 실패했습니다.");
}
} catch (error) {
console.error("배치 실행 실패:", error);
toast.error("배치 실행 중 오류가 발생했습니다.");
} finally {
setExecutingBatch(null);
}
};
// 배치 활성화/비활성화 토글
const toggleBatchStatus = async (batchId: number, currentStatus: string) => {
try {
const newStatus = currentStatus === 'Y' ? 'N' : 'Y';
await BatchAPI.updateBatchConfig(batchId, { isActive: newStatus });
toast.success(`배치가 ${newStatus === 'Y' ? '활성화' : '비활성화'}되었습니다.`);
loadBatchConfigs(); // 목록 새로고침
} catch (error) {
console.error("배치 상태 변경 실패:", error);
toast.error("배치 상태 변경에 실패했습니다.");
}
};
// 배치 삭제
const deleteBatch = async (batchId: number, batchName: string) => {
if (!confirm(`'${batchName}' 배치를 삭제하시겠습니까?`)) {
return;
}
try {
await BatchAPI.deleteBatchConfig(batchId);
toast.success("배치가 삭제되었습니다.");
loadBatchConfigs(); // 목록 새로고침
} catch (error) {
console.error("배치 삭제 실패:", error);
toast.error("배치 삭제에 실패했습니다.");
}
};
// 검색 처리
const handleSearch = (value: string) => {
setSearchTerm(value);
setCurrentPage(1); // 검색 시 첫 페이지로 이동
};
// 매핑 정보 요약 생성
const getMappingSummary = (mappings: BatchMapping[]) => {
if (!mappings || mappings.length === 0) {
return "매핑 없음";
}
const tableGroups = new Map<string, number>();
mappings.forEach(mapping => {
const key = `${mapping.from_table_name}${mapping.to_table_name}`;
tableGroups.set(key, (tableGroups.get(key) || 0) + 1);
});
const summaries = Array.from(tableGroups.entries()).map(([key, count]) =>
`${key} (${count}개 컬럼)`
);
return summaries.join(", ");
};
return (
<div className="container mx-auto p-6 space-y-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold"> </h1>
<p className="text-muted-foreground"> .</p>
</div>
<Button
onClick={() => router.push("/admin/batchmng/create")}
className="flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span> </span>
</Button>
</div>
{/* 검색 및 필터 */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center space-x-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="배치명 또는 설명으로 검색..."
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
className="pl-10"
/>
</div>
<Button
variant="outline"
onClick={loadBatchConfigs}
disabled={loading}
className="flex items-center space-x-2"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
<span></span>
</Button>
</div>
</CardContent>
</Card>
{/* 배치 목록 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span> ({batchConfigs.length})</span>
{loading && <RefreshCw className="h-4 w-4 animate-spin" />}
</CardTitle>
</CardHeader>
<CardContent>
{batchConfigs.length === 0 ? (
<div className="text-center py-12">
<Database className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2"> </h3>
<p className="text-muted-foreground mb-4">
{searchTerm ? "검색 결과가 없습니다." : "새로운 배치를 추가해보세요."}
</p>
{!searchTerm && (
<Button
onClick={() => router.push("/admin/batchmng/create")}
className="flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span> </span>
</Button>
)}
</div>
) : (
<div className="space-y-4">
{batchConfigs.map((batch) => (
<div key={batch.id} className="border rounded-lg p-6 space-y-4">
{/* 배치 기본 정보 */}
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center space-x-3">
<h3 className="text-lg font-semibold">{batch.batch_name}</h3>
<Badge variant={batch.is_active === 'Y' ? 'default' : 'secondary'}>
{batch.is_active === 'Y' ? '활성' : '비활성'}
</Badge>
</div>
{batch.description && (
<p className="text-muted-foreground">{batch.description}</p>
)}
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
<div className="flex items-center space-x-1">
<Clock className="h-4 w-4" />
<span>{batch.cron_schedule}</span>
</div>
<div>
: {new Date(batch.created_date).toLocaleDateString()}
</div>
</div>
</div>
{/* 액션 버튼들 */}
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => executeBatch(batch.id)}
disabled={executingBatch === batch.id}
className="flex items-center space-x-1"
>
{executingBatch === batch.id ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
<span></span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toggleBatchStatus(batch.id, batch.is_active)}
className="flex items-center space-x-1"
>
{batch.is_active === 'Y' ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
<span>{batch.is_active === 'Y' ? '비활성화' : '활성화'}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/admin/batchmng/edit/${batch.id}`)}
className="flex items-center space-x-1"
>
<Edit className="h-4 w-4" />
<span></span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => deleteBatch(batch.id, batch.batch_name)}
className="flex items-center space-x-1 text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
<span></span>
</Button>
</div>
</div>
{/* 매핑 정보 */}
{batch.batch_mappings && batch.batch_mappings.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">
({batch.batch_mappings.length})
</h4>
<div className="text-sm">
{getMappingSummary(batch.batch_mappings)}
</div>
</div>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex justify-center space-x-2">
<Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = i + 1;
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(pageNum)}
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
>
</Button>
</div>
)}
</div>
);
}