# 플로우 단계별 버튼 표시/숨김 제어 시스템 구현 계획서 ## 📋 목차 1. [개요](#개요) 2. [요구사항 분석](#요구사항-분석) 3. [UI/UX 문제 및 해결방안](#uiux-문제-및-해결방안) 4. [시스템 아키텍처](#시스템-아키텍처) 5. [구현 상세](#구현-상세) 6. [데이터 구조](#데이터-구조) 7. [구현 단계](#구현-단계) --- ## 개요 ### 목적 플로우 위젯과 버튼 컴포넌트를 함께 사용할 때, 각 플로우 단계(Step)별로 특정 버튼만 표시하거나 숨기는 기능을 제공합니다. ### 핵심 시나리오 ``` 예시: 계약 프로세스 플로우 - Step 1 (구매 등록): [설치 자재 정보 수정] 버튼만 표시 - Step 2 (설치팀): [설치 자재 정보 수정], [수리증] 버튼 표시 - Step 3 (사용중): [설치 자재 정보 수정], [수리증], [폐기 처리] 버튼 모두 표시 - Step 4 (수리중): [수리증] 버튼만 표시 - Step 5 (폐기): 모든 버튼 숨김 ``` --- ## 요구사항 분석 ### 1. 기능적 요구사항 #### FR-1: 자동 감지 및 설정 UI 표시 - **설명**: 화면 편집기에 플로우 위젯과 버튼이 함께 배치되면, 버튼 설정 패널에 자동으로 "플로우 단계별 표시 설정" UI가 나타남 - **조건**: - 화면에 1개 이상의 플로우 위젯 존재 - 버튼 컴포넌트 선택 시 - **동작**: ``` 1. 화면의 모든 플로우 위젯 감지 2. 각 플로우의 단계 목록 조회 3. 버튼 속성 패널에 "플로우 단계별 표시 설정" 섹션 추가 ``` #### FR-2: 다중 단계 선택 - **설명**: 한 버튼이 여러 플로우 단계에서 동시에 표시될 수 있음 - **예시**: ``` [설치 자재 정보 수정] 버튼 - Step 1 ✅ 표시 - Step 2 ✅ 표시 - Step 3 ✅ 표시 - Step 4 ❌ 숨김 - Step 5 ❌ 숨김 ``` #### FR-3: 버튼별 독립 설정 - **설명**: 각 버튼은 서로 다른 플로우 단계 표시 설정을 가질 수 있음 - **예시**: ``` 화면에 3개 버튼: - [버튼 A]: Step 1, 2에서만 표시 - [버튼 B]: Step 2, 3, 4에서 표시 - [버튼 C]: 모든 단계에서 표시 ``` #### FR-4: 다중 플로우 위젯 지원 - **설명**: 화면에 2개 이상의 플로우 위젯이 있을 경우, 버튼이 어느 플로우에 반응할지 선택 가능 - **예시**: ``` 화면 구성: - 플로우 위젯 A (계약 프로세스) - 플로우 위젯 B (AS 프로세스) - [저장] 버튼: 플로우 A의 단계에 따라 표시/숨김 - [AS 접수] 버튼: 플로우 B의 단계에 따라 표시/숨김 ``` #### FR-5: 실시간 반응 - **설명**: 플로우 위젯에서 단계를 클릭하면, 버튼들이 즉시 표시/숨김 전환 - **성능**: 50ms 이내 반응 --- ### 2. UI/UX 요구사항 #### UX-1: 빈 공간 문제 해결 **문제**: 버튼이 여러 개 배치되어 있을 때, 중간 버튼이 숨겨지면 빈 공간이 생김 **해결방안 옵션**: ##### 옵션 A: Flexbox 자동 정렬 (권장) ```tsx // 버튼 컨테이너를 자동으로 감지하고 Flexbox로 변환
{visibleButtons.map(button =>
``` **장점**: - 자동으로 빈 공간 채움 - CSS 기반으로 성능 우수 - 반응형 대응 자동 **단점**: - 기존 절대 위치(absolute positioning) 레이아웃과 충돌 가능 ##### 옵션 B: 조건부 렌더링 (현재 구조 유지) ```tsx // 각 버튼을 원래 위치에 렌더링하되, 보이지 않는 버튼은 display: none {buttons.map(button => (
))} ``` **장점**: - 현재 레이아웃 시스템 유지 - 구현 간단 **단점**: - 빈 공간 그대로 남음 - 디자이너가 의도한 배치가 깨질 수 있음 ##### 옵션 C: 그룹 기반 재정렬 (하이브리드) ```tsx // 버튼들을 그룹으로 묶고, 그룹 내에서만 재정렬 ``` ##### 3-2. 버튼 컴포넌트 뱃지 ```tsx // 플로우 제어가 활성화된 버튼에 뱃지 표시 ``` --- ## 시스템 아키텍처 ### 1. 전역 상태 관리 (Zustand Store) #### FlowStepStore ```typescript // stores/flowStepStore.ts import create from 'zustand'; interface FlowStepState { // 현재 선택된 플로우 단계 (화면당 여러 플로우 가능) selectedSteps: Record; // key: flowId, value: stepId // 플로우 단계 선택 setSelectedStep: (flowId: string, stepId: number | null) => void; // 특정 플로우의 현재 단계 가져오기 getCurrentStep: (flowId: string) => number | null; // 초기화 reset: () => void; } export const useFlowStepStore = create((set, get) => ({ selectedSteps: {}, setSelectedStep: (flowId, stepId) => set((state) => ({ selectedSteps: { ...state.selectedSteps, [flowId]: stepId }, })), getCurrentStep: (flowId) => get().selectedSteps[flowId] || null, reset: () => set({ selectedSteps: {} }), })); ``` ### 2. 데이터 흐름 ``` ┌─────────────────┐ │ FlowWidget │ │ │ │ 단계 클릭 │ ─────┐ └─────────────────┘ │ │ setSelectedStep(flowId, stepId) ▼ ┌──────────────────┐ │ FlowStepStore │ │ (Zustand) │ │ │ │ selectedSteps │ └──────────────────┘ │ │ useFlowStepStore() │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Button A │ │ Button B │ │ Button C │ │ │ │ │ │ │ │ Step 1,2,3 │ │ Step 2,3,4 │ │ All Steps │ │ │ │ │ │ │ │ 현재: 표시 │ │ 현재: 표시 │ │ 현재: 표시 │ └──────────────┘ └──────────────┘ └──────────────┘ ``` ### 3. 컴포넌트 계층 구조 ``` ScreenDesigner ├── FlowWidget (컴포넌트 ID: "flow-1") │ ├── Step 1 카드 │ ├── Step 2 카드 ← 클릭됨 │ └── Step 3 카드 │ ├── ButtonGroup (레이아웃 컨테이너) │ ├── Button A (flowVisibilityConfig: { flowId: "flow-1", visibleSteps: [1,2] }) │ │ └── 현재 숨김 (Step 2 ∉ [1,2] ❌) │ │ │ ├── Button B (flowVisibilityConfig: { flowId: "flow-1", visibleSteps: [2,3,4] }) │ │ └── 현재 표시 (Step 2 ∈ [2,3,4] ✅) │ │ │ └── Button C (flowVisibilityConfig: null) │ └── 항상 표시 ✅ │ └── PropertiesPanel └── FlowVisibilityConfigPanel (버튼 선택 시) ├── 대상 플로우 선택 Dropdown ├── 단계 체크박스 목록 └── 레이아웃 옵션 ``` --- ## 데이터 구조 ### 1. ButtonTypeConfig 확장 ```typescript // types/screen-management.ts export interface ButtonTypeConfig { // 기존 설정 actionType: ButtonActionType; text?: string; variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"; size?: "sm" | "md" | "lg"; icon?: string; // ... 기존 필드들 ... // 🆕 플로우 단계별 표시 제어 flowVisibilityConfig?: FlowVisibilityConfig; } /** * 플로우 단계별 버튼 표시 설정 */ export interface FlowVisibilityConfig { // 활성화 여부 enabled: boolean; // 대상 플로우 (컴포넌트 ID) targetFlowComponentId: string; // 예: "component-123" targetFlowId?: number; // 플로우 정의 ID (선택사항, 검증용) targetFlowName?: string; // 플로우 이름 (표시용) // 표시 조건 mode: "whitelist" | "blacklist" | "all"; // - whitelist: visibleSteps에 포함된 단계에서만 표시 // - blacklist: hiddenSteps에 포함된 단계에서 숨김 // - all: 모든 단계에서 표시 (기본값) visibleSteps?: number[]; // mode="whitelist"일 때 사용 hiddenSteps?: number[]; // mode="blacklist"일 때 사용 // 레이아웃 옵션 layoutBehavior: "preserve-position" | "auto-compact"; // - preserve-position: 원래 위치 유지 (display: none) // - auto-compact: 빈 공간 자동 제거 (Flexbox) } ``` ### 2. ComponentData 확장 ```typescript // types/screen.ts export interface ComponentData { // 기존 필드들... // 🆕 버튼 그룹 정보 (옵션 C 구현 시) buttonGroupId?: string; // 버튼 그룹 ID buttonGroupLayout?: "horizontal" | "vertical" | "grid"; buttonGroupAlign?: "start" | "center" | "end" | "space-between"; } ``` ### 3. 데이터베이스 스키마 (선택사항) 플로우 설정을 DB에 저장하려면: ```sql -- screen_components 테이블에 컬럼 추가 ALTER TABLE screen_components ADD COLUMN flow_visibility_config JSONB NULL; -- 인덱스 추가 (쿼리 성능) CREATE INDEX idx_screen_components_flow_visibility ON screen_components USING GIN (flow_visibility_config); -- 예시 데이터 { "enabled": true, "targetFlowComponentId": "component-123", "targetFlowId": 5, "targetFlowName": "계약 관리 플로우", "mode": "whitelist", "visibleSteps": [1, 2, 3], "layoutBehavior": "auto-compact" } ``` --- ## 구현 상세 ### Phase 1: 전역 상태 관리 #### 파일: `frontend/stores/flowStepStore.ts` ```typescript "use client"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; /** * 플로우 단계 전역 상태 관리 */ interface FlowStepState { // 화면당 여러 플로우의 현재 선택된 단계 // key: flowComponentId (예: "component-123") selectedSteps: Record; // 플로우 단계 선택 setSelectedStep: (flowComponentId: string, stepId: number | null) => void; // 현재 단계 조회 getCurrentStep: (flowComponentId: string) => number | null; // 모든 플로우 초기화 reset: () => void; // 특정 플로우만 초기화 resetFlow: (flowComponentId: string) => void; } export const useFlowStepStore = create()( devtools( (set, get) => ({ selectedSteps: {}, setSelectedStep: (flowComponentId, stepId) => { console.log("🔄 플로우 단계 변경:", { flowComponentId, stepId }); set((state) => ({ selectedSteps: { ...state.selectedSteps, [flowComponentId]: stepId, }, })); }, getCurrentStep: (flowComponentId) => { return get().selectedSteps[flowComponentId] || null; }, reset: () => { console.log("🔄 모든 플로우 단계 초기화"); set({ selectedSteps: {} }); }, resetFlow: (flowComponentId) => { console.log("🔄 플로우 단계 초기화:", flowComponentId); set((state) => { const { [flowComponentId]: _, ...rest } = state.selectedSteps; return { selectedSteps: rest }; }); }, }), { name: "FlowStepStore" } ) ); ``` --- ### Phase 2: FlowWidget 수정 #### 파일: `frontend/components/screen/widgets/FlowWidget.tsx` ```typescript // 기존 imports... import { useFlowStepStore } from "@/stores/flowStepStore"; interface FlowWidgetProps { component: FlowComponent; onStepClick?: (stepId: number, stepName: string) => void; onSelectedDataChange?: (selectedData: any[], stepId: number | null) => void; flowRefreshKey?: number; onFlowRefresh?: () => void; } export function FlowWidget({ component, onStepClick, ... }: FlowWidgetProps) { // 🆕 전역 상태 관리 const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep); const resetFlow = useFlowStepStore((state) => state.resetFlow); // 기존 states... const [selectedStepId, setSelectedStepId] = useState(null); // componentId (플로우 컴포넌트 고유 ID) const flowComponentId = component.id; // 🆕 단계 클릭 핸들러 수정 const handleStepClick = async (stepId: number, stepName: string) => { // 외부 콜백 실행 if (onStepClick) { onStepClick(stepId, stepName); } // 같은 스텝 클릭 시 해제 if (selectedStepId === stepId) { setSelectedStepId(null); setSelectedStep(flowComponentId, null); // 🆕 전역 상태 업데이트 setStepData([]); setStepDataColumns([]); setSelectedRows(new Set()); onSelectedDataChange?.([], null); return; } // 새로운 스텝 선택 setSelectedStepId(stepId); setSelectedStep(flowComponentId, stepId); // 🆕 전역 상태 업데이트 // 기존 데이터 로드 로직... setStepDataLoading(true); setSelectedRows(new Set()); onSelectedDataChange?.([], stepId); try { // 데이터 로딩... } catch (err) { console.error("Failed to load step data:", err); toast.error(err.message || "데이터를 불러오는데 실패했습니다"); } finally { setStepDataLoading(false); } }; // 🆕 언마운트 시 상태 초기화 useEffect(() => { return () => { resetFlow(flowComponentId); }; }, [flowComponentId, resetFlow]); // 기존 렌더링 로직... return (
{/* ... */}
); } ``` --- ### Phase 3: 버튼 표시/숨김 로직 #### 파일: `frontend/components/screen/OptimizedButtonComponent.tsx` ```typescript // 기존 imports... import { useFlowStepStore } from "@/stores/flowStepStore"; import { useMemo } from "react"; export const OptimizedButtonComponent: React.FC = ({ component, ...props }) => { const config = component.webTypeConfig || {}; const flowConfig = config.flowVisibilityConfig; // 🆕 현재 플로우 단계 구독 const currentStep = useFlowStepStore((state) => { if (!flowConfig?.enabled || !flowConfig.targetFlowComponentId) { return null; } return state.getCurrentStep(flowConfig.targetFlowComponentId); }); // 🆕 버튼 표시 여부 계산 const shouldShowButton = useMemo(() => { // 플로우 제어 비활성화 시 항상 표시 if (!flowConfig?.enabled) { console.log("🔍 버튼 표시 체크 (플로우 제어 비활성):", component.id, "→ 항상 표시"); return true; } // 플로우 단계가 선택되지 않은 경우 if (currentStep === null) { console.log("🔍 버튼 표시 체크 (단계 미선택):", component.id, "→ 항상 표시"); 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("🔍 버튼 표시 체크:", { buttonId: component.id, currentStep, mode, visibleSteps, hiddenSteps, result: result ? "표시" : "숨김", }); return result; }, [flowConfig, currentStep, component.id]); // 🆕 숨김 처리 if (!shouldShowButton) { // 레이아웃 동작에 따라 다르게 처리 if (flowConfig?.layoutBehavior === "preserve-position") { // 위치 유지 (빈 공간) return
; } else { // 완전히 렌더링하지 않음 (auto-compact) return null; } } // 기존 버튼 렌더링 로직... return (
{/* 🆕 플로우 제어 활성화 표시 */} {flowConfig?.enabled && (
)}
); }; ``` --- ### Phase 4: 설정 UI #### 파일: `frontend/components/screen/config-panels/FlowVisibilityConfigPanel.tsx` ```typescript "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 } from "lucide-react"; import { ComponentData } from "@/types/screen"; import { FlowVisibilityConfig } from "@/types/screen-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 = ({ 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( currentConfig?.targetFlowComponentId || null ); const [mode, setMode] = useState<"whitelist" | "blacklist" | "all">(currentConfig?.mode || "whitelist"); const [visibleSteps, setVisibleSteps] = useState(currentConfig?.visibleSteps || []); const [hiddenSteps, setHiddenSteps] = useState(currentConfig?.hiddenSteps || []); const [layoutBehavior, setLayoutBehavior] = useState<"preserve-position" | "auto-compact">( currentConfig?.layoutBehavior || "auto-compact" ); // 선택된 플로우의 스텝 목록 const [flowSteps, setFlowSteps] = useState([]); const [flowInfo, setFlowInfo] = useState(null); const [loading, setLoading] = useState(false); // 플로우가 없을 때 if (flowWidgets.length === 0) { return ( 화면에 플로우 위젯을 추가하면 단계별 버튼 표시 제어가 가능합니다. ); } // 선택된 플로우의 스텝 로드 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 ( 플로우 단계별 표시 설정 플로우의 특정 단계에서만 이 버튼을 표시하거나 숨길 수 있습니다 {/* 활성화 체크박스 */}
setEnabled(!!checked)} />
{enabled && ( <> {/* 대상 플로우 선택 */}
{/* 플로우가 선택되면 스텝 목록 표시 */} {selectedFlowComponentId && flowSteps.length > 0 && ( <> {/* 모드 선택 */}
setMode(value)}>
{/* 단계 선택 (all 모드가 아닐 때만) */} {mode !== "all" && (
{/* 스텝 체크박스 목록 */}
{flowSteps.map((step) => { const isChecked = mode === "whitelist" ? visibleSteps.includes(step.id) : hiddenSteps.includes(step.id); return (
toggleStep(step.id)} />
); })}
)} {/* 레이아웃 옵션 */}
setLayoutBehavior(value)}>
{/* 미리보기 */} {mode === "whitelist" && visibleSteps.length > 0 && (

표시 단계:

{visibleSteps.map((stepId) => { const step = flowSteps.find((s) => s.id === stepId); return ( {step?.stepName || `Step ${stepId}`} ); })}
)} {mode === "blacklist" && hiddenSteps.length > 0 && (

숨김 단계:

{hiddenSteps.map((stepId) => { const step = flowSteps.find((s) => s.id === stepId); return ( {step?.stepName || `Step ${stepId}`} ); })}
)} {mode === "all" &&

이 버튼은 모든 단계에서 표시됩니다.

}
{/* 저장 버튼 */} )} {/* 플로우 선택 안내 */} {selectedFlowComponentId && flowSteps.length === 0 && !loading && ( 선택한 플로우에 단계가 없습니다. )} {loading && (
플로우 정보를 불러오는 중...
)} )}
); }; ``` --- ### Phase 5: PropertiesPanel에 통합 #### 파일: `frontend/components/screen/panels/PropertiesPanel.tsx` 기존 PropertiesPanel에 FlowVisibilityConfigPanel 추가: ```typescript // 기존 imports... import { FlowVisibilityConfigPanel } from "../config-panels/FlowVisibilityConfigPanel"; // PropertiesPanel 컴포넌트 내부 const renderButtonProperties = () => { return (
{/* 기존 버튼 속성 UI */} {/* ... */} {/* 🆕 플로우 단계별 표시 설정 */}
); }; ``` --- ## 구현 단계 ### Phase 1: 기반 구조 (1-2일) - [x] 요구사항 분석 및 계획 수립 - [ ] Zustand Store 생성 (`flowStepStore.ts`) - [ ] 타입 정의 추가 (`FlowVisibilityConfig`) - [ ] FlowWidget에 전역 상태 연동 ### Phase 2: 핵심 기능 (2-3일) - [ ] OptimizedButtonComponent 수정 (표시/숨김 로직) - [ ] 버튼 렌더링 테스트 (단계별) - [ ] 성능 최적화 (useMemo, useCallback) ### Phase 3: 설정 UI (2-3일) - [ ] FlowVisibilityConfigPanel 컴포넌트 생성 - [ ] PropertiesPanel에 통합 - [ ] UX 개선 (로딩, 에러 처리, 미리보기) ### Phase 4: 고급 기능 (선택사항, 2-3일) - [ ] ButtonGroup 컴포넌트 (auto-compact 레이아웃) - [ ] 드래그앤드롭으로 버튼 그룹화 - [ ] 다중 플로우 동시 제어 ### Phase 5: 테스트 및 문서화 (1-2일) - [ ] 단위 테스트 - [ ] 통합 테스트 - [ ] 사용자 가이드 작성 - [ ] 성능 벤치마크 --- ## 예상 이슈 및 해결방안 ### 이슈 1: 플로우 위젯이 여러 개일 때 어떤 플로우를 따를지 모호함 **해결**: 버튼 설정에서 명시적으로 대상 플로우 선택 (Dropdown) ### 이슈 2: 버튼이 숨겨질 때 레이아웃이 깨짐 **해결**: `layoutBehavior` 옵션으로 사용자가 선택 (preserve-position vs auto-compact) ### 이슈 3: 플로우 단계가 선택되지 않았을 때 버튼 표시 기준 **해결**: 기본값은 "모든 버튼 표시" (단계 선택 전까지는 정상 동작) ### 이슈 4: 성능 문제 (버튼이 많을 경우) **해결**: - `useMemo`로 표시 여부 캐싱 - Zustand의 selector로 필요한 데이터만 구독 - 가상화 (React Virtuoso) 고려 ### 이슈 5: 데이터베이스 저장 시 JSON 필드 크기 **해결**: - visibleSteps/hiddenSteps는 배열로 저장 (효율적) - 불필요한 메타데이터 제거 --- ## 테스트 시나리오 ### 시나리오 1: 기본 동작 1. 플로우 위젯 추가 (3단계) 2. 버튼 3개 추가 (A, B, C) 3. 버튼 A: Step 1, 2에서만 표시 4. 버튼 B: Step 2, 3에서 표시 5. 버튼 C: 모든 단계 표시 6. 플로우 Step 1 클릭 → A, C만 표시 7. 플로우 Step 2 클릭 → A, B, C 모두 표시 8. 플로우 Step 3 클릭 → B, C만 표시 ### 시나리오 2: 다중 플로우 1. 플로우 위젯 A 추가 (계약 프로세스) 2. 플로우 위젯 B 추가 (AS 프로세스) 3. [저장] 버튼: 플로우 A Step 1, 2에서만 4. [AS 접수] 버튼: 플로우 B Step 1에서만 5. 플로우 A Step 1 클릭 → [저장] 표시 6. 플로우 B Step 1 클릭 → [AS 접수] 표시 ### 시나리오 3: 레이아웃 동작 1. 버튼 5개 가로 배치 (A, B, C, D, E) 2. B, D만 Step 1에서 숨김 3. `preserve-position` 모드: A [빈공간] C [빈공간] E 4. `auto-compact` 모드: A C E (자동 정렬) --- ## 성능 고려사항 ### 최적화 포인트 1. **Zustand Selector**: 필요한 플로우만 구독 2. **useMemo**: 표시 여부 계산 결과 캐싱 3. **조건부 렌더링**: `display: none` vs `null` 선택 4. **Debounce**: 빠른 단계 전환 시 렌더링 throttle ### 예상 성능 - 버튼 10개 기준: < 10ms 반응 시간 - 버튼 100개 기준: < 50ms 반응 시간 --- ## 결론 이 계획서는 **플로우 단계별 버튼 표시/숨김 제어** 기능의 전체 구현 방향을 제시합니다. **핵심 설계 원칙**: 1. ✅ 사용자 편의성 (자동 감지, 직관적 UI) 2. ✅ 유연성 (다중 플로우, 다중 버튼 지원) 3. ✅ 성능 (전역 상태 관리, 최적화) 4. ✅ 확장성 (추후 조건부 표시 등 추가 가능) **다음 단계**: Phase 1부터 순차적으로 구현 시작