347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { 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 { Label } from "@/components/ui/label";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { Switch } from "@/components/ui/switch";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
import { CollectionAPI, DataCollectionConfig } from "@/lib/api/collection";
|
||
|
|
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||
|
|
|
||
|
|
interface CollectionConfigModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
onSave: () => void;
|
||
|
|
config?: DataCollectionConfig | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function CollectionConfigModal({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
onSave,
|
||
|
|
config,
|
||
|
|
}: CollectionConfigModalProps) {
|
||
|
|
const [formData, setFormData] = useState<Partial<DataCollectionConfig>>({
|
||
|
|
config_name: "",
|
||
|
|
description: "",
|
||
|
|
source_connection_id: 0,
|
||
|
|
source_table: "",
|
||
|
|
target_table: "",
|
||
|
|
collection_type: "full",
|
||
|
|
schedule_cron: "",
|
||
|
|
is_active: "Y",
|
||
|
|
collection_options: {},
|
||
|
|
});
|
||
|
|
const [isLoading, setIsLoading] = useState(false);
|
||
|
|
const [connections, setConnections] = useState<any[]>([]);
|
||
|
|
const [tables, setTables] = useState<string[]>([]);
|
||
|
|
|
||
|
|
const collectionTypeOptions = CollectionAPI.getCollectionTypeOptions();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOpen) {
|
||
|
|
loadConnections();
|
||
|
|
if (config) {
|
||
|
|
setFormData({
|
||
|
|
...config,
|
||
|
|
collection_options: config.collection_options || {},
|
||
|
|
});
|
||
|
|
if (config.source_connection_id) {
|
||
|
|
loadTables(config.source_connection_id);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
setFormData({
|
||
|
|
config_name: "",
|
||
|
|
description: "",
|
||
|
|
source_connection_id: 0,
|
||
|
|
source_table: "",
|
||
|
|
target_table: "",
|
||
|
|
collection_type: "full",
|
||
|
|
schedule_cron: "",
|
||
|
|
is_active: "Y",
|
||
|
|
collection_options: {},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [isOpen, config]);
|
||
|
|
|
||
|
|
const loadConnections = async () => {
|
||
|
|
try {
|
||
|
|
const connectionList = await ExternalDbConnectionAPI.getConnections({
|
||
|
|
is_active: "Y",
|
||
|
|
});
|
||
|
|
setConnections(connectionList);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("외부 연결 목록 조회 오류:", error);
|
||
|
|
toast.error("외부 연결 목록을 불러오는데 실패했습니다.");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const loadTables = async (connectionId: number) => {
|
||
|
|
try {
|
||
|
|
const result = await ExternalDbConnectionAPI.getTables(connectionId);
|
||
|
|
if (result.success && result.data) {
|
||
|
|
setTables(result.data);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("테이블 목록 조회 오류:", error);
|
||
|
|
toast.error("테이블 목록을 불러오는데 실패했습니다.");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConnectionChange = (connectionId: string) => {
|
||
|
|
const id = parseInt(connectionId);
|
||
|
|
setFormData(prev => ({
|
||
|
|
...prev,
|
||
|
|
source_connection_id: id,
|
||
|
|
source_table: "",
|
||
|
|
}));
|
||
|
|
if (id > 0) {
|
||
|
|
loadTables(id);
|
||
|
|
} else {
|
||
|
|
setTables([]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
if (!formData.config_name || !formData.source_connection_id || !formData.source_table) {
|
||
|
|
toast.error("필수 필드를 모두 입력해주세요.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true);
|
||
|
|
try {
|
||
|
|
if (config?.id) {
|
||
|
|
await CollectionAPI.updateCollectionConfig(config.id, formData);
|
||
|
|
toast.success("수집 설정이 수정되었습니다.");
|
||
|
|
} else {
|
||
|
|
await CollectionAPI.createCollectionConfig(formData as DataCollectionConfig);
|
||
|
|
toast.success("수집 설정이 생성되었습니다.");
|
||
|
|
}
|
||
|
|
onSave();
|
||
|
|
onClose();
|
||
|
|
} catch (error) {
|
||
|
|
console.error("수집 설정 저장 오류:", error);
|
||
|
|
toast.error(
|
||
|
|
error instanceof Error ? error.message : "수집 설정 저장에 실패했습니다."
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSchedulePresetSelect = (preset: string) => {
|
||
|
|
setFormData(prev => ({
|
||
|
|
...prev,
|
||
|
|
schedule_cron: preset,
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const schedulePresets = [
|
||
|
|
{ value: "0 */1 * * *", label: "매시간" },
|
||
|
|
{ value: "0 0 */6 * *", label: "6시간마다" },
|
||
|
|
{ value: "0 0 * * *", label: "매일 자정" },
|
||
|
|
{ value: "0 0 * * 0", label: "매주 일요일" },
|
||
|
|
{ value: "0 0 1 * *", label: "매월 1일" },
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||
|
|
<DialogContent className="max-w-2xl">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>
|
||
|
|
{config ? "수집 설정 수정" : "새 수집 설정"}
|
||
|
|
</DialogTitle>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||
|
|
{/* 기본 정보 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h3 className="text-lg font-medium">기본 정보</h3>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="config_name">설정명 *</Label>
|
||
|
|
<Input
|
||
|
|
id="config_name"
|
||
|
|
value={formData.config_name || ""}
|
||
|
|
onChange={(e) =>
|
||
|
|
setFormData(prev => ({ ...prev, config_name: e.target.value }))
|
||
|
|
}
|
||
|
|
placeholder="수집 설정명을 입력하세요"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="collection_type">수집 타입 *</Label>
|
||
|
|
<Select
|
||
|
|
value={formData.collection_type || "full"}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
setFormData(prev => ({ ...prev, collection_type: value as any }))
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{collectionTypeOptions.map((option) => (
|
||
|
|
<SelectItem key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="description">설명</Label>
|
||
|
|
<Textarea
|
||
|
|
id="description"
|
||
|
|
value={formData.description || ""}
|
||
|
|
onChange={(e) =>
|
||
|
|
setFormData(prev => ({ ...prev, description: e.target.value }))
|
||
|
|
}
|
||
|
|
placeholder="수집 설정에 대한 설명을 입력하세요"
|
||
|
|
rows={3}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 소스 설정 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h3 className="text-lg font-medium">소스 설정</h3>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="source_connection">소스 연결 *</Label>
|
||
|
|
<Select
|
||
|
|
value={formData.source_connection_id?.toString() || ""}
|
||
|
|
onValueChange={handleConnectionChange}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue placeholder="연결을 선택하세요" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{connections.map((conn) => (
|
||
|
|
<SelectItem key={conn.id} value={conn.id.toString()}>
|
||
|
|
{conn.connection_name} ({conn.db_type})
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="source_table">소스 테이블 *</Label>
|
||
|
|
<Select
|
||
|
|
value={formData.source_table || ""}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
setFormData(prev => ({ ...prev, source_table: value }))
|
||
|
|
}
|
||
|
|
disabled={!formData.source_connection_id}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue placeholder="테이블을 선택하세요" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{tables.map((table) => (
|
||
|
|
<SelectItem key={table} value={table}>
|
||
|
|
{table}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="target_table">대상 테이블</Label>
|
||
|
|
<Input
|
||
|
|
id="target_table"
|
||
|
|
value={formData.target_table || ""}
|
||
|
|
onChange={(e) =>
|
||
|
|
setFormData(prev => ({ ...prev, target_table: e.target.value }))
|
||
|
|
}
|
||
|
|
placeholder="대상 테이블명 (선택사항)"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 스케줄 설정 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h3 className="text-lg font-medium">스케줄 설정</h3>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="schedule_cron">Cron 표현식</Label>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Input
|
||
|
|
id="schedule_cron"
|
||
|
|
value={formData.schedule_cron || ""}
|
||
|
|
onChange={(e) =>
|
||
|
|
setFormData(prev => ({ ...prev, schedule_cron: e.target.value }))
|
||
|
|
}
|
||
|
|
placeholder="예: 0 0 * * * (매일 자정)"
|
||
|
|
className="flex-1"
|
||
|
|
/>
|
||
|
|
<Select onValueChange={handleSchedulePresetSelect}>
|
||
|
|
<SelectTrigger className="w-32">
|
||
|
|
<SelectValue placeholder="프리셋" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{schedulePresets.map((preset) => (
|
||
|
|
<SelectItem key={preset.value} value={preset.value}>
|
||
|
|
{preset.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 활성화 설정 */}
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Switch
|
||
|
|
id="is_active"
|
||
|
|
checked={formData.is_active === "Y"}
|
||
|
|
onCheckedChange={(checked) =>
|
||
|
|
setFormData(prev => ({ ...prev, is_active: checked ? "Y" : "N" }))
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="is_active">활성화</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter>
|
||
|
|
<Button type="button" variant="outline" onClick={onClose}>
|
||
|
|
취소
|
||
|
|
</Button>
|
||
|
|
<Button type="submit" disabled={isLoading}>
|
||
|
|
{isLoading ? "저장 중..." : "저장"}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</form>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
);
|
||
|
|
}
|