247 lines
8.3 KiB
TypeScript
247 lines
8.3 KiB
TypeScript
|
|
/**
|
||
|
|
* 플로우 단계 설정 패널
|
||
|
|
* 선택된 단계의 속성 편집
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { X, Trash2, Save, Check, ChevronsUpDown } from "lucide-react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||
|
|
import { useToast } from "@/hooks/use-toast";
|
||
|
|
import { updateFlowStep, deleteFlowStep } from "@/lib/api/flow";
|
||
|
|
import { FlowStep } from "@/types/flow";
|
||
|
|
import { FlowConditionBuilder } from "./FlowConditionBuilder";
|
||
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
interface FlowStepPanelProps {
|
||
|
|
step: FlowStep;
|
||
|
|
flowId: number;
|
||
|
|
onClose: () => void;
|
||
|
|
onUpdate: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanelProps) {
|
||
|
|
const { toast } = useToast();
|
||
|
|
|
||
|
|
const [formData, setFormData] = useState({
|
||
|
|
stepName: step.stepName,
|
||
|
|
tableName: step.tableName || "",
|
||
|
|
conditionJson: step.conditionJson,
|
||
|
|
});
|
||
|
|
|
||
|
|
const [tableList, setTableList] = useState<any[]>([]);
|
||
|
|
const [loadingTables, setLoadingTables] = useState(true);
|
||
|
|
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
||
|
|
|
||
|
|
// 테이블 목록 조회
|
||
|
|
useEffect(() => {
|
||
|
|
const loadTables = async () => {
|
||
|
|
try {
|
||
|
|
setLoadingTables(true);
|
||
|
|
const response = await tableManagementApi.getTableList();
|
||
|
|
if (response.success && response.data) {
|
||
|
|
setTableList(response.data);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load tables:", error);
|
||
|
|
} finally {
|
||
|
|
setLoadingTables(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
loadTables();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setFormData({
|
||
|
|
stepName: step.stepName,
|
||
|
|
tableName: step.tableName || "",
|
||
|
|
conditionJson: step.conditionJson,
|
||
|
|
});
|
||
|
|
}, [step]);
|
||
|
|
|
||
|
|
// 저장
|
||
|
|
const handleSave = async () => {
|
||
|
|
try {
|
||
|
|
const response = await updateFlowStep(step.id, formData);
|
||
|
|
if (response.success) {
|
||
|
|
toast({
|
||
|
|
title: "저장 완료",
|
||
|
|
description: "단계가 수정되었습니다.",
|
||
|
|
});
|
||
|
|
onUpdate();
|
||
|
|
onClose();
|
||
|
|
} else {
|
||
|
|
toast({
|
||
|
|
title: "저장 실패",
|
||
|
|
description: response.error,
|
||
|
|
variant: "destructive",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
toast({
|
||
|
|
title: "오류 발생",
|
||
|
|
description: error.message,
|
||
|
|
variant: "destructive",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 삭제
|
||
|
|
const handleDelete = async () => {
|
||
|
|
if (!confirm(`"${step.stepName}" 단계를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await deleteFlowStep(step.id);
|
||
|
|
if (response.success) {
|
||
|
|
toast({
|
||
|
|
title: "삭제 완료",
|
||
|
|
description: "단계가 삭제되었습니다.",
|
||
|
|
});
|
||
|
|
onUpdate();
|
||
|
|
onClose();
|
||
|
|
} else {
|
||
|
|
toast({
|
||
|
|
title: "삭제 실패",
|
||
|
|
description: response.error,
|
||
|
|
variant: "destructive",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
toast({
|
||
|
|
title: "오류 발생",
|
||
|
|
description: error.message,
|
||
|
|
variant: "destructive",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed top-0 right-0 z-50 h-full w-96 overflow-y-auto border-l bg-white shadow-xl">
|
||
|
|
<div className="space-y-6 p-6">
|
||
|
|
{/* 헤더 */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<h2 className="text-xl font-bold">단계 설정</h2>
|
||
|
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||
|
|
<X className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 기본 정보 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>기본 정보</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<Label>단계 이름</Label>
|
||
|
|
<Input
|
||
|
|
value={formData.stepName}
|
||
|
|
onChange={(e) => setFormData({ ...formData, stepName: e.target.value })}
|
||
|
|
placeholder="단계 이름 입력"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label>단계 순서</Label>
|
||
|
|
<Input value={step.stepOrder} disabled />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label>조회할 테이블</Label>
|
||
|
|
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
||
|
|
<PopoverTrigger asChild>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
role="combobox"
|
||
|
|
aria-expanded={openTableCombobox}
|
||
|
|
className="w-full justify-between"
|
||
|
|
disabled={loadingTables}
|
||
|
|
>
|
||
|
|
{formData.tableName
|
||
|
|
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
||
|
|
formData.tableName
|
||
|
|
: loadingTables
|
||
|
|
? "로딩 중..."
|
||
|
|
: "테이블 선택"}
|
||
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||
|
|
</Button>
|
||
|
|
</PopoverTrigger>
|
||
|
|
<PopoverContent className="w-[400px] p-0">
|
||
|
|
<Command>
|
||
|
|
<CommandInput placeholder="테이블 검색..." />
|
||
|
|
<CommandList>
|
||
|
|
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
||
|
|
<CommandGroup>
|
||
|
|
{tableList.map((table) => (
|
||
|
|
<CommandItem
|
||
|
|
key={table.tableName}
|
||
|
|
value={table.tableName}
|
||
|
|
onSelect={(currentValue) => {
|
||
|
|
setFormData({ ...formData, tableName: currentValue });
|
||
|
|
setOpenTableCombobox(false);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Check
|
||
|
|
className={cn(
|
||
|
|
"mr-2 h-4 w-4",
|
||
|
|
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
<div className="flex flex-col">
|
||
|
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||
|
|
{table.description && <span className="text-xs text-gray-500">{table.description}</span>}
|
||
|
|
</div>
|
||
|
|
</CommandItem>
|
||
|
|
))}
|
||
|
|
</CommandGroup>
|
||
|
|
</CommandList>
|
||
|
|
</Command>
|
||
|
|
</PopoverContent>
|
||
|
|
</Popover>
|
||
|
|
<p className="mt-1 text-xs text-gray-500">이 단계에서 조건을 적용할 테이블을 선택합니다</p>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 조건 설정 */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>조건 설정</CardTitle>
|
||
|
|
<CardDescription>이 단계에 포함될 데이터의 조건을 설정합니다</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{!formData.tableName ? (
|
||
|
|
<div className="py-8 text-center text-gray-500">먼저 테이블을 선택해주세요</div>
|
||
|
|
) : (
|
||
|
|
<FlowConditionBuilder
|
||
|
|
flowId={flowId}
|
||
|
|
tableName={formData.tableName}
|
||
|
|
condition={formData.conditionJson}
|
||
|
|
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 액션 버튼 */}
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Button className="flex-1" onClick={handleSave}>
|
||
|
|
<Save className="mr-2 h-4 w-4" />
|
||
|
|
저장
|
||
|
|
</Button>
|
||
|
|
<Button variant="destructive" onClick={handleDelete}>
|
||
|
|
<Trash2 className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|