222 lines
7.3 KiB
TypeScript
222 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Loader2, AlertCircle, ArrowRight } from "lucide-react";
|
|
import { getStepDataList, moveDataToNextStep } from "@/lib/api/flow";
|
|
import { toast } from "sonner";
|
|
|
|
interface FlowDataListModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
flowId: number;
|
|
stepId: number;
|
|
stepName: string;
|
|
allowDataMove?: boolean;
|
|
onDataMoved?: () => void; // 데이터 이동 후 리프레시
|
|
}
|
|
|
|
export function FlowDataListModal({
|
|
open,
|
|
onOpenChange,
|
|
flowId,
|
|
stepId,
|
|
stepName,
|
|
allowDataMove = false,
|
|
onDataMoved,
|
|
}: FlowDataListModalProps) {
|
|
const [data, setData] = useState<any[]>([]);
|
|
const [columns, setColumns] = useState<string[]>([]);
|
|
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [movingData, setMovingData] = useState(false);
|
|
|
|
// 데이터 조회
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
setSelectedRows(new Set());
|
|
|
|
const response = await getStepDataList(flowId, stepId, 1, 100);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || "데이터를 불러올 수 없습니다");
|
|
}
|
|
|
|
const rows = response.data?.records || [];
|
|
setData(rows);
|
|
|
|
// 컬럼 추출 (첫 번째 행에서)
|
|
if (rows.length > 0) {
|
|
setColumns(Object.keys(rows[0]));
|
|
} else {
|
|
setColumns([]);
|
|
}
|
|
} catch (err: any) {
|
|
console.error("Failed to load flow data:", err);
|
|
setError(err.message || "데이터를 불러오는데 실패했습니다");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, [open, flowId, stepId]);
|
|
|
|
// 전체 선택/해제
|
|
const toggleAllSelection = () => {
|
|
if (selectedRows.size === data.length) {
|
|
setSelectedRows(new Set());
|
|
} else {
|
|
setSelectedRows(new Set(data.map((_, index) => index)));
|
|
}
|
|
};
|
|
|
|
// 개별 행 선택/해제
|
|
const toggleRowSelection = (index: number) => {
|
|
const newSelected = new Set(selectedRows);
|
|
if (newSelected.has(index)) {
|
|
newSelected.delete(index);
|
|
} else {
|
|
newSelected.add(index);
|
|
}
|
|
setSelectedRows(newSelected);
|
|
};
|
|
|
|
// 선택된 데이터 이동
|
|
const handleMoveData = async () => {
|
|
if (selectedRows.size === 0) {
|
|
toast.error("이동할 데이터를 선택해주세요");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setMovingData(true);
|
|
|
|
// 선택된 행의 ID 추출 (가정: 각 행에 'id' 필드가 있음)
|
|
const selectedDataIds = Array.from(selectedRows).map((index) => data[index].id);
|
|
|
|
// 데이터 이동 API 호출
|
|
for (const dataId of selectedDataIds) {
|
|
const response = await moveDataToNextStep(flowId, stepId, dataId);
|
|
if (!response.success) {
|
|
throw new Error(`데이터 이동 실패: ${response.message}`);
|
|
}
|
|
}
|
|
|
|
toast.success(`${selectedRows.size}건의 데이터를 다음 단계로 이동했습니다`);
|
|
|
|
// 모달 닫고 리프레시
|
|
onOpenChange(false);
|
|
onDataMoved?.();
|
|
} catch (err: any) {
|
|
console.error("Failed to move data:", err);
|
|
toast.error(err.message || "데이터 이동에 실패했습니다");
|
|
} finally {
|
|
setMovingData(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="flex max-h-[80vh] max-w-4xl flex-col overflow-hidden">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
{stepName}
|
|
<Badge variant="secondary">{data.length}건</Badge>
|
|
</DialogTitle>
|
|
<DialogDescription>이 단계에 해당하는 데이터 목록입니다</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 overflow-auto">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
<span className="text-muted-foreground ml-2 text-sm">데이터 로딩 중...</span>
|
|
</div>
|
|
) : error ? (
|
|
<div className="border-destructive/50 bg-destructive/10 flex items-center gap-2 rounded-lg border p-4">
|
|
<AlertCircle className="text-destructive h-5 w-5" />
|
|
<span className="text-destructive text-sm">{error}</span>
|
|
</div>
|
|
) : data.length === 0 ? (
|
|
<div className="text-muted-foreground flex items-center justify-center py-12 text-sm">
|
|
데이터가 없습니다
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
{allowDataMove && (
|
|
<TableHead className="w-12">
|
|
<Checkbox
|
|
checked={selectedRows.size === data.length && data.length > 0}
|
|
onCheckedChange={toggleAllSelection}
|
|
/>
|
|
</TableHead>
|
|
)}
|
|
{columns.map((col) => (
|
|
<TableHead key={col}>{col}</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((row, index) => (
|
|
<TableRow key={index}>
|
|
{allowDataMove && (
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedRows.has(index)}
|
|
onCheckedChange={() => toggleRowSelection(index)}
|
|
/>
|
|
</TableCell>
|
|
)}
|
|
{columns.map((col) => (
|
|
<TableCell key={col}>
|
|
{row[col] !== null && row[col] !== undefined ? String(row[col]) : "-"}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-between border-t pt-4">
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
닫기
|
|
</Button>
|
|
|
|
{allowDataMove && data.length > 0 && (
|
|
<Button onClick={handleMoveData} disabled={selectedRows.size === 0 || movingData}>
|
|
{movingData ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
이동 중...
|
|
</>
|
|
) : (
|
|
<>
|
|
<ArrowRight className="mr-2 h-4 w-4" />
|
|
다음 단계로 이동 ({selectedRows.size})
|
|
</>
|
|
)}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|