ERP-node/frontend/app/(main)/admin/batch-management-new/page.tsx

508 lines
17 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 { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Trash2, Plus, ArrowRight, Save, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import {
BatchManagementAPI,
BatchConnectionInfo,
BatchColumnInfo,
} from "@/lib/api/batchManagement";
interface MappingState {
from: {
connection: BatchConnectionInfo | null;
table: string;
column: BatchColumnInfo | null;
} | null;
to: {
connection: BatchConnectionInfo | null;
table: string;
column: BatchColumnInfo | null;
} | null;
}
export default function BatchManagementNewPage() {
// 기본 상태
const [batchName, setBatchName] = useState("");
const [cronSchedule, setCronSchedule] = useState("0 12 * * *");
const [description, setDescription] = useState("");
// 커넥션 및 테이블 데이터
const [connections, setConnections] = useState<BatchConnectionInfo[]>([]);
const [fromTables, setFromTables] = useState<string[]>([]);
const [toTables, setToTables] = useState<string[]>([]);
const [fromColumns, setFromColumns] = useState<BatchColumnInfo[]>([]);
const [toColumns, setToColumns] = useState<BatchColumnInfo[]>([]);
// 선택된 상태
const [fromConnection, setFromConnection] = useState<BatchConnectionInfo | null>(null);
const [toConnection, setToConnection] = useState<BatchConnectionInfo | null>(null);
const [fromTable, setFromTable] = useState("");
const [toTable, setToTable] = useState("");
const [selectedFromColumn, setSelectedFromColumn] = useState<BatchColumnInfo | null>(null);
// 매핑 상태
const [mappings, setMappings] = useState<MappingState[]>([]);
// 초기 데이터 로드
useEffect(() => {
loadConnections();
}, []);
// 커넥션 목록 로드
const loadConnections = async () => {
try {
const data = await BatchManagementAPI.getAvailableConnections();
setConnections(Array.isArray(data) ? data : []);
} catch (error) {
console.error("커넥션 목록 로드 오류:", error);
toast.error("커넥션 목록을 불러오는데 실패했습니다.");
setConnections([]); // 오류 시 빈 배열로 설정
}
};
// FROM 커넥션 변경 시 테이블 로드
const handleFromConnectionChange = async (connectionId: string) => {
if (connectionId === 'unknown') return;
const connection = connections.find((c: BatchConnectionInfo) =>
c.type === 'internal' ? c.type === connectionId : c.id?.toString() === connectionId
);
if (!connection) return;
setFromConnection(connection);
setFromTable("");
setFromColumns([]);
setSelectedFromColumn(null);
try {
const tables = await BatchManagementAPI.getTablesFromConnection(
connection.type,
connection.id
);
setFromTables(Array.isArray(tables) ? tables : []);
} catch (error) {
console.error("FROM 테이블 목록 로드 오류:", error);
toast.error("테이블 목록을 불러오는데 실패했습니다.");
setFromTables([]); // 오류 시 빈 배열로 설정
}
};
// TO 커넥션 변경 시 테이블 로드
const handleToConnectionChange = async (connectionId: string) => {
if (connectionId === 'unknown') return;
const connection = connections.find((c: BatchConnectionInfo) =>
c.type === 'internal' ? c.type === connectionId : c.id?.toString() === connectionId
);
if (!connection) return;
setToConnection(connection);
setToTable("");
setToColumns([]);
try {
const tables = await BatchManagementAPI.getTablesFromConnection(
connection.type,
connection.id
);
setToTables(Array.isArray(tables) ? tables : []);
} catch (error) {
console.error("TO 테이블 목록 로드 오류:", error);
toast.error("테이블 목록을 불러오는데 실패했습니다.");
setToTables([]); // 오류 시 빈 배열로 설정
}
};
// FROM 테이블 변경 시 컬럼 로드
const handleFromTableChange = async (tableName: string) => {
if (!fromConnection) return;
setFromTable(tableName);
setSelectedFromColumn(null);
try {
const columns = await BatchManagementAPI.getTableColumns(
fromConnection.type,
tableName,
fromConnection.id
);
setFromColumns(Array.isArray(columns) ? columns : []);
} catch (error) {
console.error("FROM 컬럼 목록 로드 오류:", error);
toast.error("컬럼 목록을 불러오는데 실패했습니다.");
setFromColumns([]); // 오류 시 빈 배열로 설정
}
};
// TO 테이블 변경 시 컬럼 로드
const handleToTableChange = async (tableName: string) => {
if (!toConnection) return;
console.log("TO 테이블 변경:", {
tableName,
connectionType: toConnection.type,
connectionId: toConnection.id
});
setToTable(tableName);
try {
const columns = await BatchManagementAPI.getTableColumns(
toConnection.type,
tableName,
toConnection.id
);
console.log("TO 컬럼 목록 로드 성공:", columns);
setToColumns(Array.isArray(columns) ? columns : []);
} catch (error) {
console.error("TO 컬럼 목록 로드 오류:", error);
toast.error("컬럼 목록을 불러오는데 실패했습니다.");
setToColumns([]); // 오류 시 빈 배열로 설정
}
};
// FROM 컬럼 클릭
const handleFromColumnClick = (column: BatchColumnInfo) => {
setSelectedFromColumn(column);
};
// TO 컬럼 클릭 (매핑 생성)
const handleToColumnClick = (column: BatchColumnInfo) => {
if (!selectedFromColumn || !fromConnection || !toConnection) {
toast.error("FROM 컬럼을 먼저 선택해주세요.");
return;
}
// N:1 매핑 방지 (여러 FROM 컬럼이 같은 TO 컬럼에 매핑되는 것 방지)
const isAlreadyMapped = mappings.some(mapping =>
mapping.to?.connection?.type === toConnection.type &&
mapping.to?.connection?.id === toConnection.id &&
mapping.to?.table === toTable &&
mapping.to?.column?.column_name === column.column_name
);
if (isAlreadyMapped) {
toast.error("이미 매핑된 TO 컬럼입니다. N:1 매핑은 허용되지 않습니다.");
return;
}
// 새 매핑 추가
const newMapping: MappingState = {
from: {
connection: fromConnection,
table: fromTable,
column: selectedFromColumn
},
to: {
connection: toConnection,
table: toTable,
column: column
}
};
setMappings([...mappings, newMapping]);
setSelectedFromColumn(null);
toast.success("매핑이 추가되었습니다.");
};
// 매핑 삭제
const removeMapping = (index: number) => {
setMappings(mappings.filter((_, i) => i !== index));
toast.success("매핑이 삭제되었습니다.");
};
// 컬럼이 이미 매핑되었는지 확인
const isColumnMapped = (
connectionType: 'internal' | 'external',
connectionId: number | undefined,
tableName: string,
columnName: string
): boolean => {
return mappings.some(mapping =>
mapping.to?.connection?.type === connectionType &&
mapping.to?.connection?.id === connectionId &&
mapping.to?.table === tableName &&
mapping.to?.column?.column_name === columnName
);
};
// 배치 설정 저장
const handleSave = () => {
if (!batchName.trim()) {
toast.error("배치명을 입력해주세요.");
return;
}
if (mappings.length === 0) {
toast.error("최소 하나의 매핑을 설정해주세요.");
return;
}
// TODO: 실제 저장 로직 구현
console.log("배치 설정 저장:", {
batchName,
cronSchedule,
description,
mappings
});
toast.success("배치 설정이 저장되었습니다.");
};
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold"> ( )</h1>
<div className="flex gap-2">
<Button onClick={loadConnections} variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleSave}>
<Save className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 기본 정보 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="batchName"> *</Label>
<Input
id="batchName"
value={batchName}
onChange={(e) => setBatchName(e.target.value)}
placeholder="배치명을 입력하세요"
/>
</div>
<div>
<Label htmlFor="cronSchedule"> *</Label>
<Input
id="cronSchedule"
value={cronSchedule}
onChange={(e) => setCronSchedule(e.target.value)}
placeholder="0 12 * * *"
/>
</div>
</div>
<div>
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="배치에 대한 설명을 입력하세요"
rows={3}
/>
</div>
</CardContent>
</Card>
{/* FROM 섹션 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ArrowRight className="w-5 h-5" />
FROM ()
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* FROM 커넥션 선택 */}
<div>
<Label> </Label>
<Select onValueChange={handleFromConnectionChange}>
<SelectTrigger>
<SelectValue placeholder="커넥션을 선택하세요" />
</SelectTrigger>
<SelectContent>
{Array.isArray(connections) && connections.map((conn: BatchConnectionInfo) => (
<SelectItem
key={conn.type === 'internal' ? 'internal' : conn.id || 'unknown'}
value={conn.type === 'internal' ? 'internal' : (conn.id ? conn.id.toString() : 'unknown')}
>
{conn.name} ({conn.db_type?.toUpperCase()})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* FROM 테이블 선택 */}
{fromConnection && (
<div>
<Label> </Label>
<Select onValueChange={handleFromTableChange}>
<SelectTrigger>
<SelectValue placeholder="테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{Array.isArray(fromTables) && fromTables.map((table: string) => (
<SelectItem key={table} value={table}>
{table.toUpperCase()}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* FROM 컬럼 목록 */}
{fromTable && (
<div>
<Label> ( )</Label>
<div className="space-y-2 max-h-64 overflow-y-auto">
{Array.isArray(fromColumns) && fromColumns.map((column: BatchColumnInfo) => (
<div
key={column.column_name}
className={`p-3 border-2 rounded cursor-pointer transition-all ${
selectedFromColumn?.column_name === column.column_name
? 'border-blue-500 bg-blue-50 font-semibold'
: 'hover:bg-gray-100'
}`}
onClick={() => handleFromColumnClick(column)}
>
<p className="font-medium">{column.column_name}</p>
<p className="text-sm text-gray-500">{column.data_type}</p>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* TO 섹션 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ArrowRight className="w-5 h-5" />
TO ()
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* TO 커넥션 선택 */}
<div>
<Label> </Label>
<Select onValueChange={handleToConnectionChange}>
<SelectTrigger>
<SelectValue placeholder="커넥션을 선택하세요" />
</SelectTrigger>
<SelectContent>
{Array.isArray(connections) && connections.map((conn: BatchConnectionInfo) => (
<SelectItem
key={conn.type === 'internal' ? 'internal' : conn.id || 'unknown'}
value={conn.type === 'internal' ? 'internal' : (conn.id ? conn.id.toString() : 'unknown')}
>
{conn.name} ({conn.db_type?.toUpperCase()})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* TO 테이블 선택 */}
{toConnection && (
<div>
<Label> </Label>
<Select onValueChange={handleToTableChange}>
<SelectTrigger>
<SelectValue placeholder="테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{Array.isArray(toTables) && toTables.map((table: string) => (
<SelectItem key={table} value={table}>
{table.toUpperCase()}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* TO 컬럼 목록 */}
{toTable && (
<div>
<Label> ( )</Label>
<div className="space-y-2 max-h-64 overflow-y-auto">
{Array.isArray(toColumns) && toColumns.map((column: BatchColumnInfo) => (
<div
key={column.column_name}
className={`p-3 border-2 rounded cursor-pointer transition-all ${
isColumnMapped(
toConnection!.type,
toConnection!.id,
toTable,
column.column_name
)
? 'bg-red-100 text-red-700 cursor-not-allowed opacity-60'
: selectedFromColumn
? 'hover:bg-red-50'
: 'cursor-not-allowed opacity-60'
}`}
onClick={() => handleToColumnClick(column)}
>
<p className="font-medium">{column.column_name}</p>
<p className="text-sm text-gray-500">{column.data_type}</p>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* 매핑 목록 */}
{mappings.length > 0 && (
<Card>
<CardHeader>
<CardTitle> ({mappings.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{mappings.map((mapping, index) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
<Badge className="border">
{mapping.from?.connection?.name}
</Badge>
<span className="font-medium">{mapping.from?.table}.{mapping.from?.column?.column_name}</span>
<ArrowRight className="w-4 h-4" />
<Badge className="border">
{mapping.to?.connection?.name}
</Badge>
<span className="font-medium">{mapping.to?.table}.{mapping.to?.column?.column_name}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeMapping(index)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
}