플로우 단계별 버튼 표시 설정 #144
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Loader2, CheckCircle2, AlertCircle, Clock } from "lucide-react";
|
||||
import { Loader2, CheckCircle2, AlertCircle, Clock, Workflow } from "lucide-react";
|
||||
import { ComponentData, ButtonActionType } from "@/types/screen";
|
||||
import {
|
||||
optimizedButtonDataflowService,
|
||||
|
|
@ -14,6 +14,7 @@ import { dataflowJobQueue } from "@/lib/services/dataflowJobQueue";
|
|||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { executeButtonWithFlow, handleFlowExecutionResult } from "@/lib/utils/nodeFlowButtonExecutor";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
|
||||
interface OptimizedButtonProps {
|
||||
component: ComponentData;
|
||||
|
|
@ -59,6 +60,54 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
|
||||
const config = component.webTypeConfig;
|
||||
const buttonLabel = component.label || "버튼";
|
||||
const flowConfig = config?.flowVisibilityConfig;
|
||||
|
||||
// 🆕 현재 플로우 단계 구독
|
||||
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
||||
|
||||
// 🆕 버튼 표시 여부 계산
|
||||
const shouldShowButton = useMemo(() => {
|
||||
// 플로우 제어 비활성화 시 항상 표시
|
||||
if (!flowConfig?.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 플로우 단계가 선택되지 않은 경우 처리
|
||||
if (currentStep === null) {
|
||||
// 🔧 화이트리스트 모드일 때는 단계 미선택 시 숨김
|
||||
if (flowConfig.mode === "whitelist") {
|
||||
console.log("🔍 [OptimizedButton] 화이트리스트 모드 + 단계 미선택 → 숨김");
|
||||
return false;
|
||||
}
|
||||
// 블랙리스트나 all 모드는 표시
|
||||
return true;
|
||||
}
|
||||
|
||||
const { mode, visibleSteps = [], hiddenSteps = [] } = flowConfig;
|
||||
|
||||
let result = true;
|
||||
if (mode === "whitelist") {
|
||||
result = visibleSteps.includes(currentStep);
|
||||
} else if (mode === "blacklist") {
|
||||
result = !hiddenSteps.includes(currentStep);
|
||||
} else if (mode === "all") {
|
||||
result = true;
|
||||
}
|
||||
|
||||
// 항상 로그 출력 (개발 모드뿐만 아니라)
|
||||
console.log("🔍 [OptimizedButton] 표시 체크:", {
|
||||
buttonId: component.id,
|
||||
buttonLabel,
|
||||
flowComponentId: flowConfig.targetFlowComponentId,
|
||||
currentStep,
|
||||
mode,
|
||||
visibleSteps,
|
||||
hiddenSteps,
|
||||
result: result ? "표시 ✅" : "숨김 ❌",
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [flowConfig, currentStep, component.id, buttonLabel]);
|
||||
|
||||
// 🔥 디바운싱된 클릭 핸들러 (300ms)
|
||||
const handleClick = useCallback(async () => {
|
||||
|
|
@ -514,6 +563,18 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// 🆕 플로우 단계별 표시 제어
|
||||
if (!shouldShowButton) {
|
||||
// 레이아웃 동작에 따라 다르게 처리
|
||||
if (flowConfig?.layoutBehavior === "preserve-position") {
|
||||
// 위치 유지 (빈 공간, display: none)
|
||||
return <div style={{ display: "none" }} />;
|
||||
} else {
|
||||
// 완전히 렌더링하지 않음 (auto-compact, 빈 공간 제거)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
|
|
@ -549,10 +610,19 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
{/* 백그라운드 작업 상태 표시 */}
|
||||
{renderBackgroundStatus()}
|
||||
|
||||
{/* 🆕 플로우 제어 활성화 표시 */}
|
||||
{flowConfig?.enabled && (
|
||||
<div className="absolute -right-1 -top-1">
|
||||
<Badge variant="outline" className="h-4 bg-white px-1 text-xs" title="플로우 단계별 표시 제어 활성화">
|
||||
<Workflow className="h-3 w-3" />
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 제어관리 활성화 표시 */}
|
||||
{config?.enableDataflowControl && (
|
||||
<div className="absolute -right-1 -bottom-1">
|
||||
<Badge variant="outline" className="h-4 bg-white px-1 text-xs">
|
||||
<Badge variant="outline" className="h-4 bg-white px-1 text-xs" title="제어관리 활성화">
|
||||
🔧
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4166,6 +4166,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}}
|
||||
currentResolution={screenResolution}
|
||||
onResolutionChange={handleResolutionChange}
|
||||
allComponents={layout.components} // 🆕 플로우 위젯 감지용
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ import { ComponentData } from "@/types/screen";
|
|||
import { apiClient } from "@/lib/api/client";
|
||||
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
||||
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
||||
import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel";
|
||||
|
||||
interface ButtonConfigPanelProps {
|
||||
component: ComponentData;
|
||||
onUpdateProperty: (path: string, value: any) => void;
|
||||
allComponents?: ComponentData[]; // 🆕 플로우 위젯 감지용
|
||||
}
|
||||
|
||||
interface ScreenOption {
|
||||
|
|
@ -25,7 +27,11 @@ interface ScreenOption {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
component,
|
||||
onUpdateProperty,
|
||||
allComponents = [], // 🆕 기본값 빈 배열
|
||||
}) => {
|
||||
console.log("🎨 ButtonConfigPanel 렌더링:", {
|
||||
componentId: component.id,
|
||||
"component.componentConfig?.action?.type": component.componentConfig?.action?.type,
|
||||
|
|
@ -571,6 +577,15 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
|
||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||
</div>
|
||||
|
||||
{/* 🆕 플로우 단계별 표시 제어 섹션 */}
|
||||
<div className="mt-8 border-t border-gray-200 pt-6">
|
||||
<FlowVisibilityConfigPanel
|
||||
component={component}
|
||||
allComponents={allComponents}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,404 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 { Workflow, Info, CheckCircle, XCircle, Loader2 } from "lucide-react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||
import { getFlowById } 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"
|
||||
);
|
||||
|
||||
// 선택된 플로우의 스텝 목록
|
||||
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 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);
|
||||
setFlowSteps(sortedSteps);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("플로우 스텝 로딩 실패:", error);
|
||||
toast.error(error.message || "플로우 정보를 불러오는데 실패했습니다");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadFlowSteps();
|
||||
}, [selectedFlowComponentId, flowWidgets]);
|
||||
|
||||
// 설정 저장
|
||||
const handleSave = () => {
|
||||
const config: FlowVisibilityConfig = {
|
||||
enabled,
|
||||
targetFlowComponentId: selectedFlowComponentId || "",
|
||||
targetFlowId: flowInfo?.id,
|
||||
targetFlowName: flowInfo?.name,
|
||||
mode,
|
||||
visibleSteps: mode === "whitelist" ? visibleSteps : undefined,
|
||||
hiddenSteps: mode === "blacklist" ? hiddenSteps : undefined,
|
||||
layoutBehavior,
|
||||
};
|
||||
|
||||
onUpdateProperty("webTypeConfig.flowVisibilityConfig", config);
|
||||
toast.success("플로우 단계별 표시 설정이 저장되었습니다");
|
||||
};
|
||||
|
||||
// 체크박스 토글
|
||||
const toggleStep = (stepId: number) => {
|
||||
if (mode === "whitelist") {
|
||||
setVisibleSteps((prev) =>
|
||||
prev.includes(stepId) ? prev.filter((id) => id !== stepId) : [...prev, stepId]
|
||||
);
|
||||
} else if (mode === "blacklist") {
|
||||
setHiddenSteps((prev) =>
|
||||
prev.includes(stepId) ? prev.filter((id) => id !== stepId) : [...prev, stepId]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 빠른 선택
|
||||
const selectAll = () => {
|
||||
if (mode === "whitelist") {
|
||||
setVisibleSteps(flowSteps.map((s) => s.id));
|
||||
} else if (mode === "blacklist") {
|
||||
setHiddenSteps([]);
|
||||
}
|
||||
};
|
||||
|
||||
const selectNone = () => {
|
||||
if (mode === "whitelist") {
|
||||
setVisibleSteps([]);
|
||||
} else if (mode === "blacklist") {
|
||||
setHiddenSteps(flowSteps.map((s) => s.id));
|
||||
}
|
||||
};
|
||||
|
||||
const invertSelection = () => {
|
||||
if (mode === "whitelist") {
|
||||
const allStepIds = flowSteps.map((s) => s.id);
|
||||
setVisibleSteps(allStepIds.filter((id) => !visibleSteps.includes(id)));
|
||||
} else if (mode === "blacklist") {
|
||||
const allStepIds = flowSteps.map((s) => s.id);
|
||||
setHiddenSteps(allStepIds.filter((id) => !hiddenSteps.includes(id)));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Workflow className="h-4 w-4" />
|
||||
플로우 단계별 표시 설정
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
플로우의 특정 단계에서만 이 버튼을 표시하거나 숨길 수 있습니다
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{/* 활성화 체크박스 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="flow-control-enabled" checked={enabled} onCheckedChange={(checked) => setEnabled(!!checked)} />
|
||||
<Label htmlFor="flow-control-enabled" className="text-sm font-medium">
|
||||
플로우 단계에 따라 버튼 표시 제어
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<>
|
||||
{/* 대상 플로우 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">대상 플로우</Label>
|
||||
<Select value={selectedFlowComponentId || ""} onValueChange={setSelectedFlowComponentId}>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<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}>
|
||||
{flowName}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 플로우가 선택되면 스텝 목록 표시 */}
|
||||
{selectedFlowComponentId && flowSteps.length > 0 && (
|
||||
<>
|
||||
{/* 모드 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">표시 모드</Label>
|
||||
<RadioGroup value={mode} onValueChange={(value: any) => setMode(value)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="whitelist" id="mode-whitelist" />
|
||||
<Label htmlFor="mode-whitelist" className="text-sm font-normal">
|
||||
화이트리스트 (선택한 단계에서만 표시)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="blacklist" id="mode-blacklist" />
|
||||
<Label htmlFor="mode-blacklist" className="text-sm font-normal">
|
||||
블랙리스트 (선택한 단계에서 숨김)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="all" id="mode-all" />
|
||||
<Label htmlFor="mode-all" className="text-sm font-normal">
|
||||
모든 단계에서 표시
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 단계 선택 (all 모드가 아닐 때만) */}
|
||||
{mode !== "all" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">
|
||||
{mode === "whitelist" ? "표시할 단계" : "숨길 단계"}
|
||||
</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="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
{flowSteps.map((step) => {
|
||||
const isChecked =
|
||||
mode === "whitelist"
|
||||
? visibleSteps.includes(step.id)
|
||||
: hiddenSteps.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-sm">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Step {step.stepOrder}
|
||||
</Badge>
|
||||
<span>{step.stepName}</span>
|
||||
{isChecked && (
|
||||
<CheckCircle className={`ml-auto h-4 w-4 ${mode === "whitelist" ? "text-green-500" : "text-red-500"}`} />
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 레이아웃 옵션 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">레이아웃 동작</Label>
|
||||
<RadioGroup value={layoutBehavior} onValueChange={(value: any) => setLayoutBehavior(value)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="preserve-position" id="layout-preserve" />
|
||||
<Label htmlFor="layout-preserve" className="text-sm font-normal">
|
||||
원래 위치 유지 (빈 공간 가능)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="auto-compact" id="layout-compact" />
|
||||
<Label htmlFor="layout-compact" className="text-sm font-normal">
|
||||
자동 정렬 (빈 공간 제거) ⭐ 권장
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 미리보기 */}
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className="text-xs">
|
||||
{mode === "whitelist" && visibleSteps.length > 0 && (
|
||||
<div>
|
||||
<p className="font-medium">표시 단계:</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{visibleSteps.map((stepId) => {
|
||||
const step = flowSteps.find((s) => s.id === stepId);
|
||||
return (
|
||||
<Badge key={stepId} variant="secondary" className="text-xs">
|
||||
{step?.stepName || `Step ${stepId}`}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mode === "blacklist" && hiddenSteps.length > 0 && (
|
||||
<div>
|
||||
<p className="font-medium">숨김 단계:</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{hiddenSteps.map((stepId) => {
|
||||
const step = flowSteps.find((s) => s.id === stepId);
|
||||
return (
|
||||
<Badge key={stepId} variant="destructive" className="text-xs">
|
||||
{step?.stepName || `Step ${stepId}`}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mode === "all" && <p>이 버튼은 모든 단계에서 표시됩니다.</p>}
|
||||
{mode === "whitelist" && visibleSteps.length === 0 && <p>표시할 단계를 선택해주세요.</p>}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* 저장 버튼 */}
|
||||
<Button onClick={handleSave} className="w-full">
|
||||
설정 저장
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 플로우 선택 안내 */}
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -67,6 +67,8 @@ interface UnifiedPropertiesPanelProps {
|
|||
// 해상도 관련
|
||||
currentResolution?: { name: string; width: number; height: number };
|
||||
onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void;
|
||||
// 🆕 플로우 위젯 감지용
|
||||
allComponents?: ComponentData[];
|
||||
}
|
||||
|
||||
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
|
|
@ -81,6 +83,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
onStyleChange,
|
||||
currentResolution,
|
||||
onResolutionChange,
|
||||
allComponents = [], // 🆕 기본값 빈 배열
|
||||
}) => {
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
||||
|
|
@ -153,7 +156,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
case "button-primary":
|
||||
case "button-secondary":
|
||||
// 🔧 component.id만 key로 사용 (unmount 방지)
|
||||
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} allComponents={allComponents} />;
|
||||
|
||||
case "card":
|
||||
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { useFlowStepStore } from "@/stores/flowStepStore";
|
||||
|
||||
interface FlowWidgetProps {
|
||||
component: FlowComponent;
|
||||
|
|
@ -37,6 +38,10 @@ interface FlowWidgetProps {
|
|||
}
|
||||
|
||||
export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowRefreshKey, onFlowRefresh }: FlowWidgetProps) {
|
||||
// 🆕 전역 상태 관리
|
||||
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
||||
const resetFlow = useFlowStepStore((state) => state.resetFlow);
|
||||
|
||||
const [flowData, setFlowData] = useState<FlowDefinition | null>(null);
|
||||
const [steps, setSteps] = useState<FlowStep[]>([]);
|
||||
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
|
||||
|
|
@ -68,6 +73,9 @@ export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowR
|
|||
const showStepCount = config.showStepCount !== false && component.showStepCount !== false; // 기본값 true
|
||||
const allowDataMove = config.allowDataMove || component.allowDataMove || false;
|
||||
|
||||
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
||||
const flowComponentId = component.id;
|
||||
|
||||
|
||||
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
||||
const refreshStepData = async () => {
|
||||
|
|
@ -197,31 +205,43 @@ export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowR
|
|||
}
|
||||
}, [flowRefreshKey]);
|
||||
|
||||
// 스텝 클릭 핸들러
|
||||
// 🆕 언마운트 시 전역 상태 초기화
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.log("🧹 [FlowWidget] 언마운트 - 전역 상태 초기화:", flowComponentId);
|
||||
resetFlow(flowComponentId);
|
||||
};
|
||||
}, [flowComponentId, resetFlow]);
|
||||
|
||||
// 🆕 스텝 클릭 핸들러 (전역 상태 업데이트 추가)
|
||||
const handleStepClick = async (stepId: number, stepName: string) => {
|
||||
// 외부 콜백 실행
|
||||
if (onStepClick) {
|
||||
onStepClick(stepId, stepName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 같은 스텝을 다시 클릭하면 접기
|
||||
if (selectedStepId === stepId) {
|
||||
setSelectedStepId(null);
|
||||
setSelectedStep(flowComponentId, null); // 🆕 전역 상태 업데이트
|
||||
setStepData([]);
|
||||
setStepDataColumns([]);
|
||||
setSelectedRows(new Set());
|
||||
// 선택 초기화 전달
|
||||
onSelectedDataChange?.([], null);
|
||||
|
||||
console.log("🔄 [FlowWidget] 단계 선택 해제:", { flowComponentId, stepId });
|
||||
return;
|
||||
}
|
||||
|
||||
// 새로운 스텝 선택 - 데이터 로드
|
||||
setSelectedStepId(stepId);
|
||||
setSelectedStep(flowComponentId, stepId); // 🆕 전역 상태 업데이트
|
||||
setStepDataLoading(true);
|
||||
setSelectedRows(new Set());
|
||||
// 선택 초기화 전달
|
||||
onSelectedDataChange?.([], stepId);
|
||||
|
||||
console.log("✅ [FlowWidget] 단계 선택:", { flowComponentId, stepId, stepName });
|
||||
|
||||
try {
|
||||
const response = await getStepDataList(flowId!, stepId, 1, 100);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { ButtonPrimaryConfig } from "./types";
|
||||
import {
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "@/components/ui/alert-dialog";
|
||||
import { toast } from "sonner";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
|
|
@ -85,6 +86,54 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
screenId,
|
||||
});
|
||||
|
||||
// 🆕 플로우 단계별 표시 제어
|
||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
||||
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
||||
|
||||
// 🆕 버튼 표시 여부 계산
|
||||
const shouldShowButton = useMemo(() => {
|
||||
// 플로우 제어 비활성화 시 항상 표시
|
||||
if (!flowConfig?.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 플로우 단계가 선택되지 않은 경우 처리
|
||||
if (currentStep === null) {
|
||||
// 🔧 화이트리스트 모드일 때는 단계 미선택 시 숨김
|
||||
if (flowConfig.mode === "whitelist") {
|
||||
console.log("🔍 [ButtonPrimary] 화이트리스트 모드 + 단계 미선택 → 숨김");
|
||||
return false;
|
||||
}
|
||||
// 블랙리스트나 all 모드는 표시
|
||||
return true;
|
||||
}
|
||||
|
||||
const { mode, visibleSteps = [], hiddenSteps = [] } = flowConfig;
|
||||
|
||||
let result = true;
|
||||
if (mode === "whitelist") {
|
||||
result = visibleSteps.includes(currentStep);
|
||||
} else if (mode === "blacklist") {
|
||||
result = !hiddenSteps.includes(currentStep);
|
||||
} else if (mode === "all") {
|
||||
result = true;
|
||||
}
|
||||
|
||||
// 항상 로그 출력
|
||||
console.log("🔍 [ButtonPrimary] 표시 체크:", {
|
||||
buttonId: component.id,
|
||||
buttonLabel: component.label,
|
||||
flowComponentId: flowConfig.targetFlowComponentId,
|
||||
currentStep,
|
||||
mode,
|
||||
visibleSteps,
|
||||
hiddenSteps,
|
||||
result: result ? "표시 ✅" : "숨김 ❌",
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [flowConfig, currentStep, component.id, component.label]);
|
||||
|
||||
// 확인 다이얼로그 상태
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<{
|
||||
|
|
@ -571,6 +620,18 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
// DOM 안전한 props만 필터링
|
||||
const safeDomProps = filterDOMProps(domProps);
|
||||
|
||||
// 🆕 플로우 단계별 표시 제어
|
||||
if (!shouldShowButton) {
|
||||
// 레이아웃 동작에 따라 다르게 처리
|
||||
if (flowConfig?.layoutBehavior === "preserve-position") {
|
||||
// 위치 유지 (빈 공간, display: none)
|
||||
return <div style={{ display: "none" }} />;
|
||||
} else {
|
||||
// 완전히 렌더링하지 않음 (auto-compact, 빈 공간 제거)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={componentStyle} className={className} {...safeDomProps}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
/**
|
||||
* 플로우 단계 전역 상태 관리
|
||||
*
|
||||
* 플로우 위젯에서 단계를 선택하면 이 스토어에 저장되고,
|
||||
* 버튼 컴포넌트들이 현재 선택된 단계를 구독하여 표시/숨김을 결정합니다.
|
||||
*/
|
||||
interface FlowStepState {
|
||||
/**
|
||||
* 화면당 여러 플로우의 현재 선택된 단계
|
||||
* key: flowComponentId (예: "component-123")
|
||||
* value: stepId (플로우 단계 ID) 또는 null (선택 해제)
|
||||
*/
|
||||
selectedSteps: Record<string, number | null>;
|
||||
|
||||
/**
|
||||
* 플로우 단계 선택
|
||||
* @param flowComponentId 플로우 컴포넌트 고유 ID
|
||||
* @param stepId 선택된 단계 ID (null이면 선택 해제)
|
||||
*/
|
||||
setSelectedStep: (flowComponentId: string, stepId: number | null) => void;
|
||||
|
||||
/**
|
||||
* 현재 단계 조회
|
||||
* @param flowComponentId 플로우 컴포넌트 고유 ID
|
||||
* @returns 현재 선택된 단계 ID 또는 null
|
||||
*/
|
||||
getCurrentStep: (flowComponentId: string) => number | null;
|
||||
|
||||
/**
|
||||
* 모든 플로우 초기화 (화면 전환 시 사용)
|
||||
*/
|
||||
reset: () => void;
|
||||
|
||||
/**
|
||||
* 특정 플로우만 초기화 (플로우 위젯 언마운트 시 사용)
|
||||
* @param flowComponentId 플로우 컴포넌트 고유 ID
|
||||
*/
|
||||
resetFlow: (flowComponentId: string) => void;
|
||||
}
|
||||
|
||||
export const useFlowStepStore = create<FlowStepState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
selectedSteps: {},
|
||||
|
||||
setSelectedStep: (flowComponentId, stepId) => {
|
||||
console.log("🔄 [FlowStepStore] 플로우 단계 변경:", {
|
||||
flowComponentId,
|
||||
stepId,
|
||||
stepName: stepId ? `Step ${stepId}` : "선택 해제",
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
selectedSteps: {
|
||||
...state.selectedSteps,
|
||||
[flowComponentId]: stepId,
|
||||
},
|
||||
}));
|
||||
|
||||
// 개발 모드에서 현재 상태 출력
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const currentState = get().selectedSteps;
|
||||
console.log("📊 [FlowStepStore] 현재 상태:", currentState);
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentStep: (flowComponentId) => {
|
||||
const stepId = get().selectedSteps[flowComponentId] || null;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("🔍 [FlowStepStore] 현재 단계 조회:", {
|
||||
flowComponentId,
|
||||
stepId,
|
||||
});
|
||||
}
|
||||
|
||||
return stepId;
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
console.log("🔄 [FlowStepStore] 모든 플로우 단계 초기화");
|
||||
set({ selectedSteps: {} });
|
||||
},
|
||||
|
||||
resetFlow: (flowComponentId) => {
|
||||
console.log("🔄 [FlowStepStore] 플로우 단계 초기화:", flowComponentId);
|
||||
|
||||
set((state) => {
|
||||
const { [flowComponentId]: _, ...rest } = state.selectedSteps;
|
||||
return { selectedSteps: rest };
|
||||
});
|
||||
},
|
||||
}),
|
||||
{ name: "FlowStepStore" }
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 플로우의 현재 단계를 구독하는 Hook
|
||||
*
|
||||
* @example
|
||||
* const currentStep = useCurrentFlowStep("component-123");
|
||||
* if (currentStep === null) {
|
||||
* // 단계가 선택되지 않음
|
||||
* }
|
||||
*/
|
||||
export const useCurrentFlowStep = (flowComponentId: string | null | undefined) => {
|
||||
return useFlowStepStore((state) => {
|
||||
if (!flowComponentId) return null;
|
||||
return state.getCurrentStep(flowComponentId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 플로우의 현재 단계를 한 번에 구독하는 Hook
|
||||
*
|
||||
* @example
|
||||
* const steps = useMultipleFlowSteps(["component-123", "component-456"]);
|
||||
* // { "component-123": 1, "component-456": null }
|
||||
*/
|
||||
export const useMultipleFlowSteps = (flowComponentIds: string[]) => {
|
||||
return useFlowStepStore((state) => {
|
||||
const result: Record<string, number | null> = {};
|
||||
flowComponentIds.forEach((id) => {
|
||||
result[id] = state.getCurrentStep(id);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -49,12 +49,29 @@ export interface ExtendedButtonTypeConfig {
|
|||
dataflowConfig?: ButtonDataflowConfig;
|
||||
dataflowTiming?: "before" | "after" | "replace";
|
||||
|
||||
// 🆕 플로우 단계별 표시 제어
|
||||
flowVisibilityConfig?: FlowVisibilityConfig;
|
||||
|
||||
// 스타일 설정
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
borderColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 단계별 버튼 표시 설정
|
||||
*/
|
||||
export interface FlowVisibilityConfig {
|
||||
enabled: boolean;
|
||||
targetFlowComponentId: string;
|
||||
targetFlowId?: number;
|
||||
targetFlowName?: string;
|
||||
mode: "whitelist" | "blacklist" | "all";
|
||||
visibleSteps?: number[];
|
||||
hiddenSteps?: number[];
|
||||
layoutBehavior: "preserve-position" | "auto-compact";
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 단순화된 버튼 데이터플로우 설정
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -289,6 +289,59 @@ export interface ButtonTypeConfig {
|
|||
// ButtonActionType과 관련된 설정은 control-management.ts에서 정의
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 단계별 버튼 표시 설정
|
||||
*
|
||||
* 플로우 위젯과 버튼을 함께 사용할 때, 특정 플로우 단계에서만 버튼을 표시하거나 숨길 수 있습니다.
|
||||
*/
|
||||
export interface FlowVisibilityConfig {
|
||||
/**
|
||||
* 플로우 단계별 표시 제어 활성화 여부
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* 대상 플로우 컴포넌트 ID
|
||||
* 화면에 여러 플로우 위젯이 있을 경우, 어떤 플로우에 반응할지 지정
|
||||
*/
|
||||
targetFlowComponentId: string;
|
||||
|
||||
/**
|
||||
* 대상 플로우 정의 ID (선택사항, 검증용)
|
||||
*/
|
||||
targetFlowId?: number;
|
||||
|
||||
/**
|
||||
* 대상 플로우 이름 (표시용)
|
||||
*/
|
||||
targetFlowName?: string;
|
||||
|
||||
/**
|
||||
* 표시 조건 모드
|
||||
* - whitelist: visibleSteps에 포함된 단계에서만 표시
|
||||
* - blacklist: hiddenSteps에 포함된 단계에서 숨김
|
||||
* - all: 모든 단계에서 표시 (기본값)
|
||||
*/
|
||||
mode: "whitelist" | "blacklist" | "all";
|
||||
|
||||
/**
|
||||
* 표시할 단계 ID 목록 (mode="whitelist"일 때 사용)
|
||||
*/
|
||||
visibleSteps?: number[];
|
||||
|
||||
/**
|
||||
* 숨길 단계 ID 목록 (mode="blacklist"일 때 사용)
|
||||
*/
|
||||
hiddenSteps?: number[];
|
||||
|
||||
/**
|
||||
* 레이아웃 동작 방식
|
||||
* - preserve-position: 원래 위치 유지 (display: none, 빈 공간 유지)
|
||||
* - auto-compact: 빈 공간 자동 제거 (Flexbox, 렌더링하지 않음)
|
||||
*/
|
||||
layoutBehavior: "preserve-position" | "auto-compact";
|
||||
}
|
||||
|
||||
// ===== 데이터 테이블 관련 =====
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue