제어관리 개선판

This commit is contained in:
kjs
2025-10-24 14:11:12 +09:00
parent 96252270d7
commit 8d1f0e7098
30 changed files with 2285 additions and 655 deletions

View File

@@ -4,13 +4,12 @@ import React, { useEffect, useState } from "react";
import { FlowComponent } from "@/types/screen-management";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { AlertCircle, Loader2, ChevronDown, ChevronUp, History } from "lucide-react";
import { getFlowById, getAllStepCounts, getStepDataList, moveBatchData, getFlowAuditLogs } from "@/lib/api/flow";
import { AlertCircle, Loader2, ChevronUp, History } from "lucide-react";
import { getFlowById, getAllStepCounts, getStepDataList, getFlowAuditLogs } from "@/lib/api/flow";
import type { FlowDefinition, FlowStep, FlowAuditLog } from "@/types/flow";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { toast } from "sonner";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Dialog,
DialogContent,
@@ -55,8 +54,6 @@ export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowR
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
const [stepDataLoading, setStepDataLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [movingData, setMovingData] = useState(false);
const [selectedNextStepId, setSelectedNextStepId] = useState<number | null>(null); // 선택된 다음 단계
// 오딧 로그 상태
const [auditLogs, setAuditLogs] = useState<FlowAuditLog[]>([]);
@@ -303,84 +300,6 @@ export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowR
onSelectedDataChange?.(selectedData, selectedStepId);
};
// 현재 단계에서 가능한 다음 단계들 찾기
const getNextSteps = (currentStepId: number) => {
return connections
.filter((conn) => conn.fromStepId === currentStepId)
.map((conn) => steps.find((s) => s.id === conn.toStepId))
.filter((step) => step !== undefined);
};
// 다음 단계로 이동
const handleMoveToNext = async (targetStepId?: number) => {
if (!flowId || !selectedStepId || selectedRows.size === 0) return;
// 다음 단계 결정
let nextStepId = targetStepId || selectedNextStepId;
if (!nextStepId) {
const nextSteps = getNextSteps(selectedStepId);
if (nextSteps.length === 0) {
toast.error("다음 단계가 없습니다");
return;
}
if (nextSteps.length === 1) {
nextStepId = nextSteps[0].id;
} else {
toast.error("다음 단계를 선택해주세요");
return;
}
}
const selectedData = Array.from(selectedRows).map((index) => stepData[index]);
try {
setMovingData(true);
// Primary Key 컬럼 추출 (첫 번째 컬럼 가정)
const primaryKeyColumn = stepDataColumns[0];
const dataIds = selectedData.map((data) => String(data[primaryKeyColumn]));
// 배치 이동 API 호출
const response = await moveBatchData({
flowId,
fromStepId: selectedStepId,
toStepId: nextStepId,
dataIds,
});
if (!response.success) {
throw new Error(response.message || "데이터 이동에 실패했습니다");
}
const nextStepName = steps.find((s) => s.id === nextStepId)?.stepName;
toast.success(`${selectedRows.size}건의 데이터를 "${nextStepName}"(으)로 이동했습니다`);
// 선택 초기화
setSelectedNextStepId(null);
setSelectedRows(new Set());
// 선택 초기화 전달
onSelectedDataChange?.([], selectedStepId);
// 데이터 새로고침
await handleStepClick(selectedStepId, steps.find((s) => s.id === selectedStepId)?.stepName || "");
// 건수 새로고침
const countsResponse = await getAllStepCounts(flowId);
if (countsResponse.success && countsResponse.data) {
const countsMap: Record<number, number> = {};
countsResponse.data.forEach((item: any) => {
countsMap[item.stepId] = item.count;
});
setStepCounts(countsMap);
}
} catch (err: any) {
console.error("Failed to move data:", err);
toast.error(err.message || "데이터 이동 중 오류가 발생했습니다");
} finally {
setMovingData(false);
}
};
// 오딧 로그 로드
const loadAuditLogs = async () => {
@@ -716,93 +635,18 @@ export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowR
{selectedStepId !== null && (
<div className="bg-muted/30 mt-4 w-full rounded-lg p-4 sm:mt-6 sm:rounded-xl sm:p-5 lg:mt-8 lg:p-6">
{/* 헤더 */}
<div className="mb-4 flex flex-col items-start justify-between gap-3 sm:mb-6 sm:flex-row sm:items-center">
<div className="mb-4 sm:mb-6">
<div className="flex-1">
<h4 className="text-foreground text-base font-semibold sm:text-lg">
{steps.find((s) => s.id === selectedStepId)?.stepName}
</h4>
<p className="text-muted-foreground mt-1 text-xs sm:text-sm"> {stepData.length} </p>
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
{stepData.length}
{selectedRows.size > 0 && (
<span className="text-primary ml-2 font-medium">({selectedRows.size} )</span>
)}
</p>
</div>
{allowDataMove &&
selectedRows.size > 0 &&
(() => {
const nextSteps = getNextSteps(selectedStepId);
return nextSteps.length > 1 ? (
// 다음 단계가 여러 개인 경우: 선택 UI 표시
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<Select
value={selectedNextStepId?.toString() || ""}
onValueChange={(value) => setSelectedNextStepId(Number(value))}
>
<SelectTrigger className="h-8 w-full text-xs sm:h-10 sm:w-[180px] sm:text-sm">
<SelectValue placeholder="이동할 단계 선택" />
</SelectTrigger>
<SelectContent>
{nextSteps.map((step) => (
<SelectItem key={step.id} value={step.id.toString()}>
{step.stepName}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={() => handleMoveToNext()}
disabled={movingData || !selectedNextStepId}
className="h-8 gap-1 px-3 text-xs sm:h-10 sm:gap-2 sm:px-4 sm:text-sm"
>
{movingData ? (
<>
<Loader2 className="h-3 w-3 animate-spin sm:h-4 sm:w-4" />
<span> ...</span>
</>
) : (
<>
<svg className="h-3 w-3 sm:h-4 sm:w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
<span> ({selectedRows.size})</span>
</>
)}
</Button>
</div>
) : (
// 다음 단계가 하나인 경우: 바로 이동 버튼만 표시
<Button
onClick={() => handleMoveToNext()}
disabled={movingData}
className="h-8 w-full gap-1 px-3 text-xs sm:h-10 sm:w-auto sm:gap-2 sm:px-4 sm:text-sm"
>
{movingData ? (
<>
<Loader2 className="h-3 w-3 animate-spin sm:h-4 sm:w-4" />
<span className="hidden sm:inline"> ...</span>
<span className="sm:hidden"></span>
</>
) : (
<>
<svg className="h-3 w-3 sm:h-4 sm:w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
<span className="hidden sm:inline">
{nextSteps.length > 0 ? `${nextSteps[0].stepName}(으)로 이동` : "다음 단계로 이동"} (
{selectedRows.size})
</span>
<span className="sm:hidden"> ({selectedRows.size})</span>
</>
)}
</Button>
);
})()}
</div>
{/* 데이터 테이블 */}