From 5baf5842b427cd2ee196eb707da6e8886813aa1a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 12 Mar 2026 03:30:18 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260311182531-f443 round-1 --- .../V2TableSearchWidgetConfigPanel.tsx | 585 ++++++++++++++++-- 1 file changed, 540 insertions(+), 45 deletions(-) diff --git a/frontend/components/v2/config-panels/V2TableSearchWidgetConfigPanel.tsx b/frontend/components/v2/config-panels/V2TableSearchWidgetConfigPanel.tsx index 588d2d8e..d477a9f9 100644 --- a/frontend/components/v2/config-panels/V2TableSearchWidgetConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TableSearchWidgetConfigPanel.tsx @@ -2,66 +2,561 @@ /** * V2TableSearchWidget 설정 패널 - * 기존 TableSearchWidgetConfigPanel의 모든 로직(필터 모드, 대상 패널, 고정 필터 등)을 유지하면서 - * componentConfigChanged 이벤트를 추가하여 실시간 업데이트 지원 + * 토스식 단계별 UX: 대상 패널 카드 선택 -> 필터 모드 카드 선택 -> 고정 필터 목록 -> 고급 설정(접힘) */ -import React from "react"; -import { TableSearchWidgetConfigPanel } from "@/lib/registry/components/v2-table-search-widget/TableSearchWidgetConfigPanel"; +import React, { useState, useEffect, useCallback } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + PanelLeft, + PanelRight, + Layers, + Zap, + Lock, + Plus, + Trash2, + Settings, + ChevronDown, + Search, + Filter, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +// ─── 대상 패널 위치 카드 정의 ─── +const PANEL_POSITION_CARDS = [ + { + value: "left", + icon: PanelLeft, + title: "좌측 패널", + description: "카드 디스플레이 등", + }, + { + value: "right", + icon: PanelRight, + title: "우측 패널", + description: "테이블 리스트 등", + }, + { + value: "auto", + icon: Layers, + title: "자동", + description: "모든 테이블 대상", + }, +] as const; + +// ─── 필터 모드 카드 정의 ─── +const FILTER_MODE_CARDS = [ + { + value: "dynamic", + icon: Zap, + title: "동적 모드", + description: "사용자가 직접 필터를 선택해요", + }, + { + value: "preset", + icon: Lock, + title: "고정 모드", + description: "디자이너가 미리 필터를 지정해요", + }, +] as const; + +// ─── 필터 타입 옵션 ─── +const FILTER_TYPE_OPTIONS = [ + { value: "text", label: "텍스트" }, + { value: "number", label: "숫자" }, + { value: "date", label: "날짜" }, + { value: "select", label: "선택" }, +] as const; + +interface PresetFilter { + id: string; + columnName: string; + columnLabel: string; + filterType: "text" | "number" | "date" | "select"; + width?: number; + multiSelect?: boolean; +} + +// ─── 수평 Switch Row (토스 패턴) ─── +function SwitchRow({ + label, + description, + checked, + onCheckedChange, +}: { + label: string; + description?: string; + checked: boolean; + onCheckedChange: (checked: boolean) => void; +}) { + return ( +
+
+

{label}

+ {description && ( +

{description}

+ )} +
+ +
+ ); +} + +// ─── 섹션 헤더 컴포넌트 ─── +function SectionHeader({ + icon: Icon, + title, + description, +}: { + icon: React.ComponentType<{ className?: string }>; + title: string; + description?: string; +}) { + return ( +
+
+ +

{title}

+
+ {description && ( +

{description}

+ )} +
+ ); +} + +// ─── inputType에서 filterType 추출 헬퍼 ─── +function getFilterTypeFromInputType( + inputType: string +): "text" | "number" | "date" | "select" { + if ( + inputType.includes("number") || + inputType.includes("decimal") || + inputType.includes("integer") + ) { + return "number"; + } + if (inputType.includes("date") || inputType.includes("time")) { + return "date"; + } + if ( + inputType.includes("select") || + inputType.includes("dropdown") || + inputType.includes("code") || + inputType.includes("category") + ) { + return "select"; + } + return "text"; +} interface V2TableSearchWidgetConfigPanelProps { - component?: any; - config?: any; - onUpdateProperty?: (property: string, value: any) => void; - onChange?: (newConfig: any) => void; + config: Record; + onChange: (config: Record) => void; tables?: any[]; } -export function V2TableSearchWidgetConfigPanel({ - component, - config, - onUpdateProperty, - onChange, - tables, -}: V2TableSearchWidgetConfigPanelProps) { - const handleChange = (newConfig: any) => { - if (onChange) { +export const V2TableSearchWidgetConfigPanel: React.FC< + V2TableSearchWidgetConfigPanelProps +> = ({ config: configProp, onChange, tables = [] }) => { + const config = configProp || {}; + + // componentConfigChanged 이벤트 발행 래퍼 + const handleChange = useCallback( + (newConfig: Record) => { onChange(newConfig); - } + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("componentConfigChanged", { + detail: { config: newConfig }, + }) + ); + } + }, + [onChange] + ); - if (typeof window !== "undefined") { - window.dispatchEvent( - new CustomEvent("componentConfigChanged", { - detail: { config: newConfig }, - }) + // key-value 형태 업데이트 헬퍼 + const updateField = useCallback( + (key: string, value: any) => { + handleChange({ ...config, [key]: value }); + }, + [handleChange, config] + ); + + // 첫 번째 테이블의 컬럼 목록 + const availableColumns = + tables.length > 0 && tables[0].columns ? tables[0].columns : []; + + // ─── 로컬 상태 ─── + const [advancedOpen, setAdvancedOpen] = useState(false); + const [localPresetFilters, setLocalPresetFilters] = useState( + config.presetFilters ?? [] + ); + + // config 외부 변경 시 로컬 상태 동기화 + useEffect(() => { + setLocalPresetFilters(config.presetFilters ?? []); + }, [config.presetFilters]); + + // 현재 config 값들 + const targetPanelPosition = config.targetPanelPosition ?? "left"; + const filterMode = config.filterMode ?? "dynamic"; + const autoSelectFirstTable = config.autoSelectFirstTable ?? true; + const showTableSelector = config.showTableSelector ?? true; + + // ─── 고정 필터 CRUD ─── + const addFilter = useCallback(() => { + const newFilter: PresetFilter = { + id: `filter_${Date.now()}`, + columnName: "", + columnLabel: "", + filterType: "text", + width: 200, + }; + const updated = [...localPresetFilters, newFilter]; + setLocalPresetFilters(updated); + handleChange({ ...config, presetFilters: updated }); + }, [localPresetFilters, handleChange, config]); + + const removeFilter = useCallback( + (id: string) => { + const updated = localPresetFilters.filter((f) => f.id !== id); + setLocalPresetFilters(updated); + handleChange({ ...config, presetFilters: updated }); + }, + [localPresetFilters, handleChange, config] + ); + + const updateFilter = useCallback( + (id: string, field: keyof PresetFilter, value: any) => { + const updated = localPresetFilters.map((f) => + f.id === id ? { ...f, [field]: value } : f ); - } - }; + setLocalPresetFilters(updated); + handleChange({ ...config, presetFilters: updated }); + }, + [localPresetFilters, handleChange, config] + ); - const handleUpdateProperty = (property: string, value: any) => { - if (onUpdateProperty) { - onUpdateProperty(property, value); - } - - if (typeof window !== "undefined") { - window.dispatchEvent( - new CustomEvent("componentConfigChanged", { - detail: { property, value }, - }) + // 컬럼 선택 시 라벨+타입 자동 설정 + const handleColumnSelect = useCallback( + (filterId: string, columnName: string) => { + const selectedColumn = availableColumns.find( + (col: any) => col.columnName === columnName ); - } - }; + const updated = localPresetFilters.map((f) => + f.id === filterId + ? { + ...f, + columnName, + columnLabel: selectedColumn?.columnLabel || columnName, + filterType: getFilterTypeFromInputType( + selectedColumn?.inputType || "text" + ), + } + : f + ); + setLocalPresetFilters(updated); + handleChange({ ...config, presetFilters: updated }); + }, + [availableColumns, localPresetFilters, handleChange, config] + ); return ( - +
+ {/* ─── 1단계: 대상 패널 위치 선택 ─── */} +
+ + +

+ 어떤 패널의 테이블을 대상으로 하나요? +

+
+ {PANEL_POSITION_CARDS.map((card) => { + const Icon = card.icon; + const isSelected = targetPanelPosition === card.value; + return ( + + ); + })} +
+
+ + {/* ─── 2단계: 필터 모드 선택 ─── */} +
+

필터를 어떻게 구성할까요?

+
+ {FILTER_MODE_CARDS.map((card) => { + const Icon = card.icon; + const isSelected = filterMode === card.value; + return ( + + ); + })} +
+
+ + {/* ─── 3단계: 고정 모드 필터 목록 ─── */} + {filterMode === "preset" && ( +
+
+
+ + 고정 필터 목록 +
+ +
+ + {localPresetFilters.length === 0 ? ( +
+ +

아직 필터가 없어요

+

+ 위의 추가 버튼으로 필터를 만들어보세요 +

+
+ ) : ( +
+ {localPresetFilters.map((filter) => ( +
+ {/* 상단: 컬럼 선택 + 삭제 */} +
+
+ {availableColumns.length > 0 ? ( + + ) : ( + + updateFilter( + filter.id, + "columnName", + e.target.value + ) + } + placeholder="예: customer_name" + className="h-7 text-xs" + /> + )} +
+ +
+ + {/* 하단: 필터 타입 + 너비 */} +
+ +
+ + 너비 + + + updateFilter( + filter.id, + "width", + parseInt(e.target.value) || 200 + ) + } + className="h-7 w-16 text-xs" + min={100} + max={500} + /> +
+
+ + {/* 표시명 (컬럼 선택 시 자동 설정, 수동 변경 가능) */} + {filter.columnLabel && ( +

+ 표시명: {filter.columnLabel} +

+ )} +
+ ))} +
+ )} + +

+ 고정 모드에서는 설정 버튼이 숨겨지고 지정된 필터만 표시돼요 +

+
+ )} + + {/* 동적 모드 안내 */} + {filterMode === "dynamic" && ( +
+
+ + 동적 모드 안내 +
+

+ 사용자가 테이블 설정 버튼을 클릭하여 원하는 필터를 직접 선택할 수 + 있어요. 필터 설정은 브라우저에 저장되어 다음 접속 시에도 유지돼요. +

+
+ )} + + {/* ─── 4단계: 고급 설정 (기본 접혀있음) ─── */} + + + + + +
+ + updateField("autoSelectFirstTable", checked) + } + /> + + updateField("showTableSelector", checked) + } + /> +
+
+
+
); -} +}; V2TableSearchWidgetConfigPanel.displayName = "V2TableSearchWidgetConfigPanel";