367 lines
13 KiB
TypeScript
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>
|
|
);
|
|
} |