239 lines
8.2 KiB
TypeScript
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>
|
|
);
|
|
}
|