ERP-node/frontend/components/screen/config-panels/FlowVisibilityConfigPanel.tsx

417 lines
16 KiB
TypeScript

"use client";
import React, { useState, useEffect, useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Input } from "@/components/ui/input";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Workflow, Info, CheckCircle, XCircle, Loader2, ArrowRight, ArrowDown } from "lucide-react";
import { ComponentData } from "@/types/screen";
import { FlowVisibilityConfig } from "@/types/control-management";
import { getFlowById, getFlowSteps } from "@/lib/api/flow";
import type { FlowDefinition, FlowStep } from "@/types/flow";
import { toast } from "sonner";
interface FlowVisibilityConfigPanelProps {
component: ComponentData; // 현재 선택된 버튼
allComponents: ComponentData[]; // 화면의 모든 컴포넌트
onUpdateProperty: (path: string, value: any) => void;
}
/**
* 플로우 단계별 버튼 표시 설정 패널
*
* 플로우 위젯이 화면에 있을 때, 버튼이 특정 플로우 단계에서만 표시되도록 설정할 수 있습니다.
*/
export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps> = ({
component,
allComponents,
onUpdateProperty,
}) => {
// 현재 설정
const currentConfig: FlowVisibilityConfig | undefined = (component as any).webTypeConfig?.flowVisibilityConfig;
// 화면의 모든 플로우 위젯 찾기
const flowWidgets = useMemo(() => {
return allComponents.filter((comp) => {
const isFlowWidget =
comp.type === "flow" || (comp.type === "component" && (comp as any).componentConfig?.type === "flow-widget");
return isFlowWidget;
});
}, [allComponents]);
// State
const [enabled, setEnabled] = useState(currentConfig?.enabled || false);
const [selectedFlowComponentId, setSelectedFlowComponentId] = useState<string | null>(
currentConfig?.targetFlowComponentId || null,
);
const [mode, setMode] = useState<"whitelist" | "blacklist" | "all">(currentConfig?.mode || "whitelist");
const [visibleSteps, setVisibleSteps] = useState<number[]>(currentConfig?.visibleSteps || []);
const [hiddenSteps, setHiddenSteps] = useState<number[]>(currentConfig?.hiddenSteps || []);
const [layoutBehavior, setLayoutBehavior] = useState<"preserve-position" | "auto-compact">(
currentConfig?.layoutBehavior || "auto-compact",
);
// 🆕 그룹 설정 (auto-compact 모드에서만 사용)
const [groupId, setGroupId] = useState<string>(currentConfig?.groupId || `group-${Date.now()}`);
const [groupDirection, setGroupDirection] = useState<"horizontal" | "vertical">(
currentConfig?.groupDirection || "horizontal",
);
const [groupGap, setGroupGap] = useState<number>(currentConfig?.groupGap ?? 8);
const [groupAlign, setGroupAlign] = useState<"start" | "center" | "end" | "space-between" | "space-around">(
currentConfig?.groupAlign || "start",
);
// 선택된 플로우의 스텝 목록
const [flowSteps, setFlowSteps] = useState<FlowStep[]>([]);
const [flowInfo, setFlowInfo] = useState<FlowDefinition | null>(null);
const [loading, setLoading] = useState(false);
// 플로우가 없을 때
if (flowWidgets.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Workflow className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
<Alert>
<Info className="h-4 w-4" />
<AlertDescription className="text-xs">
.
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}
// 선택된 플로우의 스텝 로드
useEffect(() => {
if (!selectedFlowComponentId) {
setFlowSteps([]);
setFlowInfo(null);
return;
}
const loadFlowSteps = async () => {
try {
setLoading(true);
// 선택된 플로우 위젯 찾기
const flowWidget = flowWidgets.find((fw) => fw.id === selectedFlowComponentId);
if (!flowWidget) return;
// flowId 추출
const flowConfig = (flowWidget as any).componentConfig || {};
const flowId = flowConfig.flowId;
if (!flowId) {
toast.error("플로우 ID를 찾을 수 없습니다");
return;
}
// 플로우 정보 조회
const flowResponse = await getFlowById(flowId);
if (!flowResponse.success || !flowResponse.data) {
throw new Error("플로우를 찾을 수 없습니다");
}
setFlowInfo(flowResponse.data);
// 스텝 목록 조회
const stepsResponse = await getFlowSteps(flowId);
if (!stepsResponse.success) {
throw new Error("스텝 목록을 불러올 수 없습니다");
}
if (stepsResponse.data) {
const sortedSteps = stepsResponse.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder);
setFlowSteps(sortedSteps);
}
} catch (error: any) {
console.error("플로우 스텝 로딩 실패:", error);
toast.error(error.message || "플로우 정보를 불러오는데 실패했습니다");
} finally {
setLoading(false);
}
};
loadFlowSteps();
}, [selectedFlowComponentId, flowWidgets]);
// 🆕 설정 자동 저장 (즉시 적용) - 오버라이드 가능한 파라미터 지원
const applyConfig = (overrides?: Partial<FlowVisibilityConfig>) => {
const config: FlowVisibilityConfig = {
enabled,
targetFlowComponentId: selectedFlowComponentId || "",
targetFlowId: flowInfo?.id,
targetFlowName: flowInfo?.name,
mode,
visibleSteps: mode === "whitelist" ? visibleSteps : undefined,
hiddenSteps: mode === "blacklist" ? hiddenSteps : undefined,
layoutBehavior,
// 🆕 그룹 설정 (auto-compact 모드일 때만)
...(layoutBehavior === "auto-compact" && {
groupId,
groupDirection,
groupGap,
groupAlign,
}),
// 오버라이드 적용
...overrides,
};
console.log("💾 [FlowVisibilityConfig] 설정 자동 저장:", {
componentId: component.id,
config,
timestamp: new Date().toISOString(),
});
// 현재 버튼에 설정 적용 (그룹 설정은 ScreenDesigner에서 자동으로 일괄 적용됨)
onUpdateProperty("webTypeConfig.flowVisibilityConfig", config);
};
// 체크박스 토글
const toggleStep = (stepId: number) => {
if (mode === "whitelist") {
const newSteps = visibleSteps.includes(stepId)
? visibleSteps.filter((id) => id !== stepId)
: [...visibleSteps, stepId];
setVisibleSteps(newSteps);
// 🆕 새 상태값을 직접 전달하여 즉시 저장
applyConfig({ visibleSteps: newSteps });
} else if (mode === "blacklist") {
const newSteps = hiddenSteps.includes(stepId)
? hiddenSteps.filter((id) => id !== stepId)
: [...hiddenSteps, stepId];
setHiddenSteps(newSteps);
// 🆕 새 상태값을 직접 전달하여 즉시 저장
applyConfig({ hiddenSteps: newSteps });
}
};
// 빠른 선택
const selectAll = () => {
if (mode === "whitelist") {
const newSteps = flowSteps.map((s) => s.id);
setVisibleSteps(newSteps);
applyConfig({ visibleSteps: newSteps });
} else if (mode === "blacklist") {
setHiddenSteps([]);
applyConfig({ hiddenSteps: [] });
}
};
const selectNone = () => {
if (mode === "whitelist") {
setVisibleSteps([]);
applyConfig({ visibleSteps: [] });
} else if (mode === "blacklist") {
const newSteps = flowSteps.map((s) => s.id);
setHiddenSteps(newSteps);
applyConfig({ hiddenSteps: newSteps });
}
};
const invertSelection = () => {
if (mode === "whitelist") {
const allStepIds = flowSteps.map((s) => s.id);
const newSteps = allStepIds.filter((id) => !visibleSteps.includes(id));
setVisibleSteps(newSteps);
applyConfig({ visibleSteps: newSteps });
} else if (mode === "blacklist") {
const allStepIds = flowSteps.map((s) => s.id);
const newSteps = allStepIds.filter((id) => !hiddenSteps.includes(id));
setHiddenSteps(newSteps);
applyConfig({ hiddenSteps: newSteps });
}
};
return (
<div className="space-y-4">
<div className="space-y-1">
<h4 className="flex items-center gap-2 text-xs font-medium">
<Workflow className="h-4 w-4" />
</h4>
<p className="text-muted-foreground text-xs">
</p>
</div>
<div className="space-y-4">
{/* 활성화 체크박스 */}
<div className="flex items-center gap-2">
<Checkbox
id="flow-control-enabled"
checked={enabled}
onCheckedChange={(checked) => {
setEnabled(!!checked);
setTimeout(() => applyConfig(), 0);
}}
/>
<Label htmlFor="flow-control-enabled" className="text-xs font-medium">
</Label>
</div>
{enabled && (
<>
{/* 대상 플로우 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium">
</Label>
<Select
value={selectedFlowComponentId || ""}
onValueChange={(value) => {
setSelectedFlowComponentId(value);
setTimeout(() => applyConfig(), 0);
}}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="플로우 위젯 선택" />
</SelectTrigger>
<SelectContent>
{flowWidgets.map((fw) => {
const flowConfig = (fw as any).componentConfig || {};
const flowName = flowConfig.flowName || `플로우 ${fw.id}`;
return (
<SelectItem key={fw.id} value={fw.id} className="text-xs">
{flowName}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
{/* 플로우가 선택되면 스텝 목록 표시 */}
{selectedFlowComponentId && flowSteps.length > 0 && (
<>
{/* 단계 선택 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium">
</Label>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={selectAll}
className="h-7 px-2 text-xs"
>
</Button>
<Button
variant="ghost"
size="sm"
onClick={selectNone}
className="h-7 px-2 text-xs"
>
</Button>
<Button
variant="ghost"
size="sm"
onClick={invertSelection}
className="h-7 px-2 text-xs"
>
</Button>
</div>
</div>
{/* 스텝 체크박스 목록 */}
<div className="bg-muted/30 space-y-2 rounded-lg border p-3">
{flowSteps.map((step) => {
const isChecked = visibleSteps.includes(step.id);
return (
<div key={step.id} className="flex items-center gap-2">
<Checkbox
id={`step-${step.id}`}
checked={isChecked}
onCheckedChange={() => toggleStep(step.id)}
/>
<Label
htmlFor={`step-${step.id}`}
className="flex flex-1 items-center gap-2 text-xs"
>
<Badge variant="outline" className="text-xs">
Step {step.stepOrder}
</Badge>
<span>{step.stepName}</span>
{isChecked && <CheckCircle className="ml-auto h-4 w-4 text-success" />}
</Label>
</div>
);
})}
</div>
</div>
{/* 정렬 방식 */}
<div className="space-y-2">
<Label htmlFor="group-align" className="text-xs font-medium">
</Label>
<Select
value={groupAlign}
onValueChange={(value: any) => {
setGroupAlign(value);
onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value);
}}
>
<SelectTrigger id="group-align" className="h-6 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="start" className="text-xs">
</SelectItem>
<SelectItem value="center" className="text-xs">
</SelectItem>
<SelectItem value="end" className="text-xs">
</SelectItem>
<SelectItem value="space-between" className="text-xs">
</SelectItem>
<SelectItem value="space-around" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
{/* 플로우 선택 안내 */}
{selectedFlowComponentId && flowSteps.length === 0 && !loading && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription className="text-xs"> .</AlertDescription>
</Alert>
)}
{loading && (
<div className="flex items-center justify-center py-4">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-xs"> ...</span>
</div>
)}
</>
)}
</div>
</div>
);
};