ERP-node/frontend/components/admin/CollectionConfigModal.tsx

347 lines
11 KiB
TypeScript
Raw Normal View History

2025-09-24 10:04:25 +09:00
"use client";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
2025-11-05 16:36:32 +09:00
} from "@/components/ui/resizable-dialog";
2025-09-24 10:04:25 +09:00
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 (
2025-11-05 16:36:32 +09:00
<ResizableDialog open={isOpen} onOpenChange={onClose}>
<ResizableDialogContent className="max-w-2xl">
<ResizableDialogHeader>
<ResizableDialogTitle>
2025-09-24 10:04:25 +09:00
{config ? "수집 설정 수정" : "새 수집 설정"}
2025-11-05 16:36:32 +09:00
</ResizableDialogTitle>
</ResizableDialogHeader>
2025-09-24 10:04:25 +09:00
<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>
2025-11-05 16:36:32 +09:00
<ResizableDialogFooter>
2025-09-24 10:04:25 +09:00
<Button type="button" variant="outline" onClick={onClose}>
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? "저장 중..." : "저장"}
</Button>
2025-11-05 16:36:32 +09:00
</ResizableDialogFooter>
2025-09-24 10:04:25 +09:00
</form>
2025-11-05 16:36:32 +09:00
</ResizableDialogContent>
</ResizableDialog>
2025-09-24 10:04:25 +09:00
);
}