플로우 구현
This commit is contained in:
238
frontend/components/screen/widgets/FlowWidget.tsx
Normal file
238
frontend/components/screen/widgets/FlowWidget.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user