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

367 lines
13 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Plus,
Search,
RefreshCw,
Database
} from "lucide-react";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import {
BatchAPI,
BatchConfig,
BatchMapping,
} from "@/lib/api/batch";
import BatchCard from "@/components/admin/BatchCard";
import { ScrollToTop } from "@/components/common/ScrollToTop";
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);
const [isBatchTypeModalOpen, setIsBatchTypeModalOpen] = useState(false);
// 페이지 로드 시 배치 목록 조회
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) => {
console.log("🔄 배치 상태 변경 시작:", { batchId, currentStatus });
try {
const newStatus = currentStatus === 'Y' ? 'N' : 'Y';
console.log("📝 새로운 상태:", newStatus);
const result = await BatchAPI.updateBatchConfig(batchId, { isActive: newStatus });
console.log("✅ API 호출 성공:", result);
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(", ");
};
// 배치 추가 버튼 클릭 핸들러
const handleCreateBatch = () => {
setIsBatchTypeModalOpen(true);
};
// 배치 타입 선택 핸들러
const handleBatchTypeSelect = (type: 'db-to-db' | 'restapi-to-db') => {
console.log("배치 타입 선택:", type);
setIsBatchTypeModalOpen(false);
if (type === 'db-to-db') {
// 기존 DB → DB 배치 생성 페이지로 이동
console.log("DB → DB 페이지로 이동:", '/admin/batchmng/create');
router.push('/admin/batchmng/create');
} else if (type === 'restapi-to-db') {
// 새로운 REST API 배치 페이지로 이동
console.log("REST API → DB 페이지로 이동:", '/admin/batch-management-new');
try {
router.push('/admin/batch-management-new');
console.log("라우터 push 실행 완료");
} catch (error) {
console.error("라우터 push 오류:", error);
// 대안: window.location 사용
window.location.href = '/admin/batch-management-new';
}
}
};
return (
<div className="flex min-h-screen flex-col bg-background">
<div className="space-y-6 p-6">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground"> .</p>
</div>
{/* 검색 및 액션 영역 */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
{/* 검색 영역 */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
<div className="w-full sm:w-[400px]">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="배치명 또는 설명으로 검색..."
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
className="h-10 pl-10 text-sm"
/>
</div>
</div>
<Button
variant="outline"
onClick={loadBatchConfigs}
disabled={loading}
className="h-10 gap-2 text-sm font-medium"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* 액션 버튼 영역 */}
<div className="flex items-center gap-4">
<div className="text-sm text-muted-foreground">
{" "}
<span className="font-semibold text-foreground">
{batchConfigs.length.toLocaleString()}
</span>{" "}
</div>
<Button
onClick={handleCreateBatch}
className="h-10 gap-2 text-sm font-medium"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
{/* 배치 목록 */}
{batchConfigs.length === 0 ? (
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
<div className="flex flex-col items-center gap-4 text-center">
<Database className="h-12 w-12 text-muted-foreground" />
<div className="space-y-2">
<h3 className="text-lg font-semibold"> </h3>
<p className="text-sm text-muted-foreground">
{searchTerm ? "검색 결과가 없습니다." : "새로운 배치를 추가해보세요."}
</p>
</div>
{!searchTerm && (
<Button
onClick={handleCreateBatch}
className="h-10 gap-2 text-sm font-medium"
>
<Plus className="h-4 w-4" />
</Button>
)}
</div>
</div>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
{batchConfigs.map((batch) => (
<BatchCard
key={batch.id}
batch={batch}
executingBatch={executingBatch}
onExecute={executeBatch}
onToggleStatus={(batchId, currentStatus) => {
toggleBatchStatus(batchId, currentStatus);
}}
onEdit={(batchId) => router.push(`/admin/batchmng/edit/${batchId}`)}
onDelete={deleteBatch}
getMappingSummary={getMappingSummary}
/>
))}
</div>
)}
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="h-10 text-sm font-medium"
>
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = i + 1;
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
onClick={() => setCurrentPage(pageNum)}
className="h-10 min-w-[40px] text-sm"
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="h-10 text-sm font-medium"
>
</Button>
</div>
)}
{/* 배치 타입 선택 모달 */}
{isBatchTypeModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-2xl rounded-lg border bg-card p-6 shadow-lg">
<div className="space-y-6">
<h2 className="text-xl font-semibold text-center"> </h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{/* DB → DB */}
<button
className="flex flex-col items-center gap-4 rounded-lg border bg-card p-6 shadow-sm transition-all hover:border-primary hover:bg-accent"
onClick={() => handleBatchTypeSelect('db-to-db')}
>
<div className="flex items-center gap-2">
<Database className="h-8 w-8 text-primary" />
<span className="text-muted-foreground"></span>
<Database className="h-8 w-8 text-primary" />
</div>
<div className="space-y-1 text-center">
<div className="text-lg font-medium">DB DB</div>
<div className="text-sm text-muted-foreground"> </div>
</div>
</button>
{/* REST API → DB */}
<button
className="flex flex-col items-center gap-4 rounded-lg border bg-card p-6 shadow-sm transition-all hover:border-primary hover:bg-accent"
onClick={() => handleBatchTypeSelect('restapi-to-db')}
>
<div className="flex items-center gap-2">
<span className="text-2xl">🌐</span>
<span className="text-muted-foreground"></span>
<Database className="h-8 w-8 text-primary" />
</div>
<div className="space-y-1 text-center">
<div className="text-lg font-medium">REST API DB</div>
<div className="text-sm text-muted-foreground">REST API에서 </div>
</div>
</button>
</div>
<div className="flex justify-center pt-2">
<Button
variant="outline"
onClick={() => setIsBatchTypeModalOpen(false)}
className="h-10 text-sm font-medium"
>
</Button>
</div>
</div>
</div>
</div>
)}
</div>
{/* Scroll to Top 버튼 */}
<ScrollToTop />
</div>
);
}