Compare commits

..

No commits in common. "6fe49721dbe62117ad943444ec0308a716001489" and "ea9ed488e845e9a15348a171929deabdbcb00f84" have entirely different histories.

11 changed files with 10 additions and 2005 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
"use client";
import React, { useState, useCallback, useEffect, useMemo } from "react";
import React, { useState, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { toast } from "react-hot-toast";
import { Loader2, CheckCircle2, AlertCircle, Clock, Workflow } from "lucide-react";
import { Loader2, CheckCircle2, AlertCircle, Clock } from "lucide-react";
import { ComponentData, ButtonActionType } from "@/types/screen";
import {
optimizedButtonDataflowService,
@ -14,7 +14,6 @@ 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;
@ -60,54 +59,6 @@ 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 () => {
@ -563,18 +514,6 @@ 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
@ -610,19 +549,10 @@ 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" title="제어관리 활성화">
<Badge variant="outline" className="h-4 bg-white px-1 text-xs">
🔧
</Badge>
</div>

View File

@ -4166,7 +4166,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
}}
currentResolution={screenResolution}
onResolutionChange={handleResolutionChange}
allComponents={layout.components} // 🆕 플로우 위젯 감지용
/>
</div>
</div>

View File

@ -13,12 +13,10 @@ 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 {
@ -27,11 +25,7 @@ interface ScreenOption {
description?: string;
}
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
component,
onUpdateProperty,
allComponents = [], // 🆕 기본값 빈 배열
}) => {
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
console.log("🎨 ButtonConfigPanel 렌더링:", {
componentId: component.id,
"component.componentConfig?.action?.type": component.componentConfig?.action?.type,
@ -577,15 +571,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
<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>
);
};

View File

@ -1,404 +0,0 @@
"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>
);
};

View File

@ -67,8 +67,6 @@ 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> = ({
@ -83,7 +81,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onStyleChange,
currentResolution,
onResolutionChange,
allComponents = [], // 🆕 기본값 빈 배열
}) => {
const { webTypes } = useWebTypes({ active: "Y" });
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
@ -156,7 +153,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} allComponents={allComponents} />;
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "card":
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;

View File

@ -27,7 +27,6 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { useFlowStepStore } from "@/stores/flowStepStore";
interface FlowWidgetProps {
component: FlowComponent;
@ -38,10 +37,6 @@ 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>>({});
@ -73,9 +68,6 @@ 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 () => {
@ -205,43 +197,31 @@ 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);

View File

@ -1,6 +1,6 @@
"use client";
import React, { useState, useRef, useEffect, useMemo } from "react";
import React, { useState, useRef, useEffect } from "react";
import { ComponentRendererProps } from "@/types/component";
import { ButtonPrimaryConfig } from "./types";
import {
@ -21,7 +21,6 @@ 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;
@ -86,54 +85,6 @@ 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<{
@ -620,18 +571,6 @@ 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}>

View File

@ -1,135 +0,0 @@
"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;
});
};

View File

@ -49,29 +49,12 @@ 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";
}
/**
* 🔥
*/

View File

@ -289,59 +289,6 @@ 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";
}
// ===== 데이터 테이블 관련 =====
/**