ERP-node/frontend/components/screen/widgets/FlowWidget.tsx

239 lines
8.2 KiB
TypeScript

"use client";
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 } from "lucide-react";
import { getFlowById, getAllStepCounts } from "@/lib/api/flow";
import type { FlowDefinition, FlowStep } from "@/types/flow";
import { FlowDataListModal } from "@/components/flow/FlowDataListModal";
interface FlowWidgetProps {
component: FlowComponent;
onStepClick?: (stepId: number, stepName: string) => void;
}
export function FlowWidget({ component, onStepClick }: FlowWidgetProps) {
const [flowData, setFlowData] = useState<FlowDefinition | null>(null);
const [steps, setSteps] = useState<FlowStep[]>([]);
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 모달 상태
const [modalOpen, setModalOpen] = useState(false);
const [selectedStep, setSelectedStep] = useState<{ id: number; name: string } | null>(null);
// componentConfig에서 플로우 설정 추출 (DynamicComponentRenderer에서 전달됨)
const config = (component as any).componentConfig || (component as any).config || {};
const flowId = config.flowId || component.flowId;
const flowName = config.flowName || component.flowName;
const displayMode = config.displayMode || component.displayMode || "horizontal";
const showStepCount = config.showStepCount !== false && component.showStepCount !== false; // 기본값 true
const allowDataMove = config.allowDataMove || component.allowDataMove || false;
console.log("🔍 FlowWidget 렌더링:", {
component,
componentConfig: config,
flowId,
flowName,
displayMode,
showStepCount,
allowDataMove,
});
useEffect(() => {
console.log("🔍 FlowWidget useEffect 실행:", {
flowId,
hasFlowId: !!flowId,
config,
});
if (!flowId) {
setLoading(false);
return;
}
const loadFlowData = async () => {
try {
setLoading(true);
setError(null);
// 플로우 정보 조회
const flowResponse = await getFlowById(flowId!);
if (!flowResponse.success || !flowResponse.data) {
throw new Error("플로우를 찾을 수 없습니다");
}
setFlowData(flowResponse.data);
// 스텝 목록 조회
const stepsResponse = await fetch(`/api/flow/definitions/${flowId}/steps`);
if (!stepsResponse.ok) {
throw new Error("스텝 목록을 불러올 수 없습니다");
}
const stepsData = await stepsResponse.json();
if (stepsData.success && stepsData.data) {
const sortedSteps = stepsData.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder);
setSteps(sortedSteps);
// 스텝별 데이터 건수 조회
if (showStepCount) {
const countsResponse = await getAllStepCounts(flowId!);
if (countsResponse.success && countsResponse.data) {
// 배열을 Record<number, number>로 변환
const countsMap: Record<number, number> = {};
countsResponse.data.forEach((item: any) => {
countsMap[item.stepId] = item.count;
});
setStepCounts(countsMap);
}
}
}
} catch (err: any) {
console.error("Failed to load flow data:", err);
setError(err.message || "플로우 데이터를 불러오는데 실패했습니다");
} finally {
setLoading(false);
}
};
loadFlowData();
}, [flowId, showStepCount]);
// 스텝 클릭 핸들러
const handleStepClick = (stepId: number, stepName: string) => {
if (onStepClick) {
onStepClick(stepId, stepName);
} else {
// 기본 동작: 모달 열기
setSelectedStep({ id: stepId, name: stepName });
setModalOpen(true);
}
};
// 데이터 이동 후 리프레시
const handleDataMoved = async () => {
if (!flowId) return;
try {
// 스텝별 데이터 건수 다시 조회
const countsResponse = await getAllStepCounts(flowId);
if (countsResponse.success && countsResponse.data) {
// 배열을 Record<number, number>로 변환
const countsMap: Record<number, number> = {};
countsResponse.data.forEach((item: any) => {
countsMap[item.stepId] = item.count;
});
setStepCounts(countsMap);
}
} catch (err) {
console.error("Failed to refresh step counts:", err);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
<span className="text-muted-foreground ml-2 text-sm"> ...</span>
</div>
);
}
if (error) {
return (
<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>
);
}
if (!flowId || !flowData) {
return (
<div className="border-muted-foreground/25 flex items-center justify-center rounded-lg border-2 border-dashed p-8">
<span className="text-muted-foreground text-sm"> </span>
</div>
);
}
if (steps.length === 0) {
return (
<div className="border-muted flex items-center justify-center rounded-lg border p-8">
<span className="text-muted-foreground text-sm"> </span>
</div>
);
}
const containerClass =
displayMode === "horizontal"
? "flex flex-wrap items-center justify-center gap-3"
: "flex flex-col items-center gap-4";
return (
<div className="min-h-full w-full p-4">
{/* 플로우 제목 */}
<div className="mb-4 text-center">
<h3 className="text-foreground text-lg font-semibold">{flowData.name}</h3>
{flowData.description && <p className="text-muted-foreground mt-1 text-sm">{flowData.description}</p>}
</div>
{/* 플로우 스텝 목록 */}
<div className={containerClass}>
{steps.map((step, index) => (
<React.Fragment key={step.id}>
{/* 스텝 카드 */}
<Button
variant="outline"
className="hover:border-primary hover:bg-accent flex shrink-0 flex-col items-start gap-3 p-5"
onClick={() => handleStepClick(step.id, step.stepName)}
>
<div className="flex w-full items-center justify-between gap-2">
<Badge variant="outline" className="text-sm">
{step.stepOrder}
</Badge>
{showStepCount && (
<Badge variant="secondary" className="text-sm font-semibold">
{stepCounts[step.id] || 0}
</Badge>
)}
</div>
<div className="w-full text-left">
<div className="text-foreground text-base font-semibold">{step.stepName}</div>
{step.tableName && (
<div className="text-muted-foreground mt-2 flex items-center gap-1 text-sm">
<span>📊</span>
<span>{step.tableName}</span>
</div>
)}
</div>
</Button>
{/* 화살표 (마지막 스텝 제외) */}
{index < steps.length - 1 && (
<div className="text-muted-foreground flex shrink-0 items-center justify-center text-2xl font-bold">
{displayMode === "horizontal" ? "→" : "↓"}
</div>
)}
</React.Fragment>
))}
</div>
{/* 데이터 목록 모달 */}
{selectedStep && flowId && (
<FlowDataListModal
open={modalOpen}
onOpenChange={setModalOpen}
flowId={flowId}
stepId={selectedStep.id}
stepName={selectedStep.name}
allowDataMove={allowDataMove}
onDataMoved={handleDataMoved}
/>
)}
</div>
);
}