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

369 lines
12 KiB
TypeScript
Raw Normal View History

"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";
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 { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { BatchAPI, BatchJob } from "@/lib/api/batch";
2025-09-26 17:29:20 +09:00
// import { CollectionAPI } from "@/lib/api/collection"; // 사용하지 않는 import 제거
interface BatchJobModalProps {
isOpen: boolean;
onClose: () => void;
onSave: () => void;
job?: BatchJob | null;
}
export default function BatchJobModal({
isOpen,
onClose,
onSave,
job,
}: BatchJobModalProps) {
const [formData, setFormData] = useState<Partial<BatchJob>>({
job_name: "",
description: "",
job_type: "collection",
schedule_cron: "",
is_active: "Y",
config_json: {},
execution_count: 0,
success_count: 0,
failure_count: 0,
});
const [isLoading, setIsLoading] = useState(false);
const [jobTypes, setJobTypes] = useState<Array<{ value: string; label: string }>>([]);
const [schedulePresets, setSchedulePresets] = useState<Array<{ value: string; label: string }>>([]);
const [collectionConfigs, setCollectionConfigs] = useState<any[]>([]);
useEffect(() => {
if (isOpen) {
loadJobTypes();
loadSchedulePresets();
loadCollectionConfigs();
if (job) {
setFormData({
...job,
config_json: job.config_json || {},
});
} else {
setFormData({
job_name: "",
description: "",
job_type: "collection",
schedule_cron: "",
is_active: "Y",
config_json: {},
execution_count: 0,
success_count: 0,
failure_count: 0,
});
}
}
}, [isOpen, job]);
const loadJobTypes = async () => {
try {
const types = await BatchAPI.getSupportedJobTypes();
setJobTypes(types);
} catch (error) {
console.error("작업 타입 조회 오류:", error);
}
};
const loadSchedulePresets = async () => {
try {
const presets = await BatchAPI.getSchedulePresets();
setSchedulePresets(presets);
} catch (error) {
console.error("스케줄 프리셋 조회 오류:", error);
}
};
const loadCollectionConfigs = async () => {
try {
2025-09-26 17:29:20 +09:00
// 배치 설정 조회로 대체
const configs = await BatchAPI.getBatchConfigs({
is_active: "Y",
});
2025-09-26 17:29:20 +09:00
setCollectionConfigs(configs.data || []);
} catch (error) {
2025-09-26 17:29:20 +09:00
console.error("배치 설정 조회 오류:", error);
setCollectionConfigs([]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.job_name || !formData.job_type) {
toast.error("필수 필드를 모두 입력해주세요.");
return;
}
setIsLoading(true);
try {
if (job?.id) {
await BatchAPI.updateBatchJob(job.id, formData);
toast.success("배치 작업이 수정되었습니다.");
} else {
await BatchAPI.createBatchJob(formData as BatchJob);
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 handleJobTypeChange = (jobType: string) => {
setFormData(prev => ({
...prev,
job_type: jobType as any,
config_json: {},
}));
};
const handleCollectionConfigChange = (configId: string) => {
setFormData(prev => ({
...prev,
config_json: {
...prev.config_json,
collectionConfigId: parseInt(configId),
},
}));
};
2025-10-22 14:52:13 +09:00
// 상태 제거 - 필요없음
return (
2025-11-05 16:36:32 +09:00
<ResizableDialog open={isOpen} onOpenChange={onClose}>
<ResizableDialogContent className="max-w-[95vw] sm:max-w-[600px]">
<ResizableDialogHeader>
<ResizableDialogTitle className="text-base sm:text-lg">
{job ? "배치 작업 수정" : "새 배치 작업"}
2025-11-05 16:36:32 +09:00
</ResizableDialogTitle>
</ResizableDialogHeader>
2025-10-22 14:52:13 +09:00
<form onSubmit={handleSubmit} className="space-y-3 sm:space-y-4">
{/* 기본 정보 */}
2025-10-22 14:52:13 +09:00
<div className="space-y-3 sm:space-y-4">
<h3 className="text-sm font-semibold sm:text-base"> </h3>
2025-10-22 14:52:13 +09:00
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div>
<Label htmlFor="job_name" className="text-xs sm:text-sm"> *</Label>
<Input
id="job_name"
value={formData.job_name || ""}
onChange={(e) =>
setFormData(prev => ({ ...prev, job_name: e.target.value }))
}
placeholder="배치 작업명을 입력하세요"
2025-10-22 14:52:13 +09:00
className="h-8 text-xs sm:h-10 sm:text-sm"
required
/>
</div>
2025-10-22 14:52:13 +09:00
<div>
<Label htmlFor="job_type" className="text-xs sm:text-sm"> *</Label>
<Select
value={formData.job_type || "collection"}
onValueChange={handleJobTypeChange}
>
2025-10-22 14:52:13 +09:00
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{jobTypes.map((type) => (
2025-10-22 14:52:13 +09:00
<SelectItem key={type.value} value={type.value} className="text-xs sm:text-sm">
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
2025-10-22 14:52:13 +09:00
<div>
<Label htmlFor="description" className="text-xs sm:text-sm"></Label>
<Textarea
id="description"
value={formData.description || ""}
onChange={(e) =>
setFormData(prev => ({ ...prev, description: e.target.value }))
}
placeholder="배치 작업에 대한 설명을 입력하세요"
2025-10-22 14:52:13 +09:00
className="min-h-[60px] text-xs sm:min-h-[80px] sm:text-sm"
rows={3}
/>
</div>
</div>
{/* 작업 설정 */}
{formData.job_type === 'collection' && (
2025-10-22 14:52:13 +09:00
<div className="space-y-3 sm:space-y-4">
<h3 className="text-sm font-semibold sm:text-base"> </h3>
2025-10-22 14:52:13 +09:00
<div>
<Label htmlFor="collection_config" className="text-xs sm:text-sm"> </Label>
<Select
value={formData.config_json?.collectionConfigId?.toString() || ""}
onValueChange={handleCollectionConfigChange}
>
2025-10-22 14:52:13 +09:00
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="수집 설정을 선택하세요" />
</SelectTrigger>
<SelectContent>
{collectionConfigs.map((config) => (
2025-10-22 14:52:13 +09:00
<SelectItem key={config.id} value={config.id.toString()} className="text-xs sm:text-sm">
{config.config_name} - {config.source_table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
{/* 스케줄 설정 */}
2025-10-22 14:52:13 +09:00
<div className="space-y-3 sm:space-y-4">
<h3 className="text-sm font-semibold sm:text-base"> </h3>
2025-10-22 14:52:13 +09:00
<div>
<Label htmlFor="schedule_cron" className="text-xs sm:text-sm">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 * * * (매일 자정)"
2025-10-22 14:52:13 +09:00
className="h-8 flex-1 text-xs sm:h-10 sm:text-sm"
/>
<Select onValueChange={handleSchedulePresetSelect}>
2025-10-22 14:52:13 +09:00
<SelectTrigger className="h-8 w-24 text-xs sm:h-10 sm:w-32 sm:text-sm">
<SelectValue placeholder="프리셋" />
</SelectTrigger>
<SelectContent>
{schedulePresets.map((preset) => (
2025-10-22 14:52:13 +09:00
<SelectItem key={preset.value} value={preset.value} className="text-xs sm:text-sm">
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 실행 통계 (수정 모드일 때만) */}
{job?.id && (
2025-10-22 14:52:13 +09:00
<div className="space-y-3 sm:space-y-4">
<h3 className="text-sm font-semibold sm:text-base"> </h3>
2025-10-22 14:52:13 +09:00
<div className="grid grid-cols-3 gap-2 sm:gap-4">
<div className="rounded-lg border bg-card p-3 sm:p-4">
<div className="text-xl font-bold text-primary sm:text-2xl">
{formData.execution_count || 0}
</div>
2025-10-22 14:52:13 +09:00
<div className="text-xs text-muted-foreground sm:text-sm"> </div>
</div>
2025-10-22 14:52:13 +09:00
<div className="rounded-lg border bg-card p-3 sm:p-4">
<div className="text-xl font-bold text-primary sm:text-2xl">
{formData.success_count || 0}
</div>
2025-10-22 14:52:13 +09:00
<div className="text-xs text-muted-foreground sm:text-sm"></div>
</div>
2025-10-22 14:52:13 +09:00
<div className="rounded-lg border bg-card p-3 sm:p-4">
<div className="text-xl font-bold text-destructive sm:text-2xl">
{formData.failure_count || 0}
</div>
2025-10-22 14:52:13 +09:00
<div className="text-xs text-muted-foreground sm:text-sm"></div>
</div>
</div>
{formData.last_executed_at && (
2025-10-22 14:52:13 +09:00
<p className="text-xs text-muted-foreground sm:text-sm">
: {new Date(formData.last_executed_at).toLocaleString()}
2025-10-22 14:52:13 +09:00
</p>
)}
</div>
)}
{/* 활성화 설정 */}
<div className="flex items-center justify-between">
2025-10-22 14:52:13 +09:00
<div className="flex items-center gap-2">
<Switch
id="is_active"
checked={formData.is_active === "Y"}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, is_active: checked ? "Y" : "N" }))
}
/>
2025-10-22 14:52:13 +09:00
<Label htmlFor="is_active" className="text-xs sm:text-sm"></Label>
</div>
2025-10-22 14:52:13 +09:00
<Badge variant={formData.is_active === "Y" ? "default" : "secondary"}>
{formData.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</div>
2025-10-22 14:52:13 +09:00
<DialogFooter className="gap-2 sm:gap-0">
<Button
type="button"
variant="outline"
onClick={onClose}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
2025-10-22 14:52:13 +09:00
<Button
type="submit"
disabled={isLoading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{isLoading ? "저장 중..." : "저장"}
</Button>
2025-11-05 16:36:32 +09:00
</ResizableDialogFooter>
</form>
2025-11-05 16:36:32 +09:00
</ResizableDialogContent>
</ResizableDialog>
);
}