"use client"; /** * V2TableSearchWidget 설정 패널 * 토스식 단계별 UX: 대상 패널 카드 선택 -> 필터 모드 카드 선택 -> 고정 필터 목록 -> 고급 설정(접힘) */ 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 { config: Record; onChange: (config: Record) => void; tables?: any[]; } 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] ); // 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 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"; export default V2TableSearchWidgetConfigPanel;