From 040c7463341d609827ba9c3b368497367007c624 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 11 Mar 2026 15:10:14 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260311052455-y968 round-6 --- .../button-config/AdvancedTab.tsx | 132 +++ .../config-panels/button-config/BasicTab.tsx | 1036 +++++++++++++++++ .../button-config/DataflowTab.tsx | 20 + .../config-panels/button-config/types.ts | 31 + 4 files changed, 1219 insertions(+) create mode 100644 frontend/components/screen/config-panels/button-config/AdvancedTab.tsx create mode 100644 frontend/components/screen/config-panels/button-config/BasicTab.tsx create mode 100644 frontend/components/screen/config-panels/button-config/DataflowTab.tsx create mode 100644 frontend/components/screen/config-panels/button-config/types.ts diff --git a/frontend/components/screen/config-panels/button-config/AdvancedTab.tsx b/frontend/components/screen/config-panels/button-config/AdvancedTab.tsx new file mode 100644 index 00000000..141ca8d6 --- /dev/null +++ b/frontend/components/screen/config-panels/button-config/AdvancedTab.tsx @@ -0,0 +1,132 @@ +"use client"; + +import React, { useMemo } from "react"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ImprovedButtonControlConfigPanel } from "../ImprovedButtonControlConfigPanel"; +import { FlowVisibilityConfigPanel } from "../FlowVisibilityConfigPanel"; +import type { ButtonTabProps } from "./types"; + +/** + * AdvancedTab - 행 선택 활성화, 제어 기능, 플로우 표시 제어 + */ +export const AdvancedTab: React.FC = ({ + component, + onUpdateProperty, + allComponents, +}) => { + // 플로우 위젯이 화면에 있는지 확인 + const hasFlowWidget = useMemo(() => { + return allComponents.some((comp: { componentType?: string; widgetType?: string }) => { + const compType = comp.componentType || comp.widgetType || ""; + return compType === "flow-widget" || compType?.toLowerCase().includes("flow"); + }); + }, [allComponents]); + + const actionType = component.componentConfig?.action?.type; + + return ( +
+ {/* 행 선택 시에만 활성화 설정 */} +
+

행 선택 활성화 조건

+

+ 테이블 리스트나 분할 패널에서 데이터가 선택되었을 때만 버튼을 활성화합니다. +

+ +
+
+ +

+ 체크하면 테이블에서 행을 선택해야만 버튼이 활성화됩니다. +

+
+ { + onUpdateProperty("componentConfig.action.requireRowSelection", checked); + }} + /> +
+ + {component.componentConfig?.action?.requireRowSelection && ( +
+
+ + +

+ 자동 감지: 테이블, 분할 패널, 플로우 위젯 중 선택된 항목이 있으면 활성화 +

+
+ +
+
+ +

+ 여러 행이 선택되어도 활성화 (기본: 1개 이상 선택 시 활성화) +

+
+ { + onUpdateProperty("componentConfig.action.allowMultiRowSelection", checked); + }} + /> +
+ + {!(component.componentConfig?.action?.allowMultiRowSelection ?? true) && ( +
+

+ 정확히 1개의 행만 선택되어야 버튼이 활성화됩니다. +

+
+ )} +
+ )} +
+ + {/* 제어 기능 섹션 - 엑셀 업로드 계열이 아닐 때만 표시 */} + {actionType !== "excel_upload" && actionType !== "multi_table_excel_upload" && ( +
+ +
+ )} + + {/* 플로우 단계별 표시 제어 (플로우 위젯이 있을 때만) */} + {hasFlowWidget && ( +
+ +
+ )} +
+ ); +}; diff --git a/frontend/components/screen/config-panels/button-config/BasicTab.tsx b/frontend/components/screen/config-panels/button-config/BasicTab.tsx new file mode 100644 index 00000000..152bc1c0 --- /dev/null +++ b/frontend/components/screen/config-panels/button-config/BasicTab.tsx @@ -0,0 +1,1036 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Check, Plus, X, Info, RotateCcw } from "lucide-react"; +import { icons as allLucideIcons } from "lucide-react"; +import DOMPurify from "isomorphic-dompurify"; +import { cn } from "@/lib/utils"; +import { ComponentData } from "@/types/screen"; +import { ColorPickerWithTransparent } from "../../common/ColorPickerWithTransparent"; +import { + actionIconMap, + noIconActions, + NO_ICON_MESSAGE, + iconSizePresets, + getLucideIcon, + addToIconMap, + getDefaultIconForAction, +} from "@/lib/button-icon-map"; +import type { ButtonTabProps } from "./types"; + +export const BasicTab: React.FC = ({ + component, + onUpdateProperty, +}) => { + const config = component.componentConfig || {}; + + // 표시 모드, 버튼 텍스트, 액션 타입 + const [displayMode, setDisplayMode] = useState<"text" | "icon" | "icon-text">( + config.displayMode || "text" + ); + const [localText, setLocalText] = useState( + config.text !== undefined ? config.text : "버튼" + ); + const [localActionType, setLocalActionType] = useState( + String(config.action?.type || "save") + ); + + // 아이콘 설정 상태 + const [selectedIcon, setSelectedIcon] = useState(config.icon?.name || ""); + const [selectedIconType, setSelectedIconType] = useState<"lucide" | "svg">( + config.icon?.type || "lucide" + ); + const [iconSize, setIconSize] = useState(config.icon?.size || "보통"); + const [iconColor, setIconColor] = useState(config.icon?.color || ""); + const [iconGap, setIconGap] = useState(config.iconGap ?? 6); + const [iconTextPosition, setIconTextPosition] = useState< + "right" | "left" | "bottom" | "top" + >(config.iconTextPosition || "right"); + + // 커스텀 아이콘 UI 상태 + const [lucideSearchOpen, setLucideSearchOpen] = useState(false); + const [lucideSearchTerm, setLucideSearchTerm] = useState(""); + const [svgPasteOpen, setSvgPasteOpen] = useState(false); + const [svgInput, setSvgInput] = useState(""); + const [svgName, setSvgName] = useState(""); + const [svgError, setSvgError] = useState(""); + + // 컴포넌트 prop 변경 시 로컬 상태 동기화 + useEffect(() => { + const latestConfig = component.componentConfig || {}; + const latestAction = latestConfig.action || {}; + + setLocalText(latestConfig.text !== undefined ? latestConfig.text : "버튼"); + setLocalActionType(String(latestAction.type || "save")); + setDisplayMode((latestConfig.displayMode as "text" | "icon" | "icon-text") || "text"); + setSelectedIcon(latestConfig.icon?.name || ""); + setSelectedIconType((latestConfig.icon?.type as "lucide" | "svg") || "lucide"); + setIconSize(latestConfig.icon?.size || "보통"); + setIconColor(latestConfig.icon?.color || ""); + setIconGap(latestConfig.iconGap ?? 6); + setIconTextPosition( + (latestConfig.iconTextPosition as "right" | "left" | "bottom" | "top") || "right" + ); + }, [component.id, component.componentConfig?.action?.type]); + + // 현재 액션의 추천 아이콘 목록 + const currentActionIcons = actionIconMap[localActionType] || []; + const isNoIconAction = noIconActions.has(localActionType); + const customIcons: string[] = config.customIcons || []; + const customSvgIcons: Array<{ name: string; svg: string }> = + config.customSvgIcons || []; + const showIconSettings = displayMode === "icon" || displayMode === "icon-text"; + + // 아이콘 선택 핸들러 + const handleSelectIcon = (iconName: string, iconType: "lucide" | "svg" = "lucide") => { + setSelectedIcon(iconName); + setSelectedIconType(iconType); + onUpdateProperty("componentConfig.icon", { + name: iconName, + type: iconType, + size: iconSize, + ...(iconColor ? { color: iconColor } : {}), + }); + }; + + // 선택 중인 아이콘이 삭제되었을 때 디폴트 아이콘으로 복귀 + const revertToDefaultIcon = () => { + const def = getDefaultIconForAction(localActionType); + setSelectedIcon(def.name); + setSelectedIconType(def.type); + handleSelectIcon(def.name, def.type); + }; + + // 표시 모드 변경 핸들러 + const handleDisplayModeChange = (mode: "text" | "icon" | "icon-text") => { + setDisplayMode(mode); + onUpdateProperty("componentConfig.displayMode", mode); + + if ((mode === "icon" || mode === "icon-text") && !selectedIcon) { + revertToDefaultIcon(); + } + }; + + // 아이콘 크기 프리셋 변경 + const handleIconSizePreset = (preset: string) => { + setIconSize(preset); + if (selectedIcon) { + onUpdateProperty("componentConfig.icon.size", preset); + } + }; + + // 아이콘 색상 변경 + const handleIconColorChange = (color: string | undefined) => { + const val = color || ""; + setIconColor(val); + if (selectedIcon) { + if (val) { + onUpdateProperty("componentConfig.icon.color", val); + } else { + onUpdateProperty("componentConfig.icon.color", undefined); + } + } + }; + + return ( +
+ {/* 표시 모드 선택 */} +
+ +
+ {( + [ + { value: "text", label: "텍스트" }, + { value: "icon", label: "아이콘" }, + { value: "icon-text", label: "아이콘+텍스트" }, + ] as const + ).map((opt) => ( + + ))} +
+
+ + {/* 아이콘 모드 레이아웃 안내 */} + {displayMode === "icon" && ( +
+ + + 아이콘만 표시할 때는 버튼 영역의 가로 폭을 줄여 정사각형에 가깝게 + 만들면 더 깔끔합니다. + +
+ )} + + {/* 버튼 텍스트 (텍스트 / 아이콘+텍스트 모드에서 표시) */} + {(displayMode === "text" || displayMode === "icon-text") && ( +
+ + { + const newValue = e.target.value; + setLocalText(newValue); + onUpdateProperty("componentConfig.text", newValue); + }} + placeholder="버튼 텍스트를 입력하세요" + /> +
+ )} + + {/* 버튼 액션 */} +
+ + +
+ + {/* 아이콘 설정 영역 */} + {showIconSettings && ( +
+ {/* 추천 아이콘 / 안내 문구 */} + {isNoIconAction ? ( +
+
+ {NO_ICON_MESSAGE} +
+ + {/* 커스텀 아이콘이 있으면 표시 */} + {(customIcons.length > 0 || customSvgIcons.length > 0) && ( + <> +
+
+ + 커스텀 아이콘 + +
+
+
+ {customIcons.map((iconName) => { + const Icon = getLucideIcon(iconName); + if (!Icon) return null; + return ( +
+ + +
+ ); + })} + {customSvgIcons.map((svgIcon) => ( +
+ + +
+ ))} +
+ + )} + + {/* 커스텀 아이콘 추가 버튼 */} +
+ + + + + + + + + + 아이콘을 찾을 수 없습니다. + + + {Object.keys(allLucideIcons) + .filter((name) => + name + .toLowerCase() + .includes(lucideSearchTerm.toLowerCase()) + ) + .slice(0, 30) + .map((iconName) => { + const Icon = + allLucideIcons[ + iconName as keyof typeof allLucideIcons + ]; + return ( + { + const next = [...customIcons]; + if (!next.includes(iconName)) { + next.push(iconName); + onUpdateProperty( + "componentConfig.customIcons", + next + ); + if (Icon) + addToIconMap(iconName, Icon); + } + setLucideSearchOpen(false); + setLucideSearchTerm(""); + }} + className="flex items-center gap-2 text-xs" + > + {Icon ? ( + + ) : ( + + )} + {iconName} + {customIcons.includes(iconName) && ( + + )} + + ); + })} + + + + + + + + + + + + + setSvgName(e.target.value)} + placeholder="예: 회사로고" + className="h-7 text-xs" + /> + +