feat: 검색 필터 위젯 화면별 독립 설정 및 고정 모드 추가

- 검색 필터 설정을 화면별로 독립적으로 저장하도록 개선 (screenId 포함)
- FilterPanel, TableSearchWidget, TableListComponent에 화면 ID 기반 localStorage 키 적용
- 동적 모드(사용자 설정)와 고정 모드(디자이너 설정) 2가지 필터 방식 추가
- 고정 모드에서 컬럼 드롭다운 선택 기능 구현
- 컬럼 선택 시 라벨 및 필터 타입 자동 설정
- ConfigPanel 표시 문제 해결 (type='component' 지원)
- UnifiedPropertiesPanel에서 독립 컴포넌트 ConfigPanel 조회 개선

주요 변경:
- 같은 테이블을 사용하는 다른 화면에서 필터 설정이 독립적으로 관리됨
- 고정 모드에서는 설정 버튼이 숨겨지고 지정된 필터만 표시
- 테이블 정보가 있으면 컬럼을 드롭다운으로 선택 가능
- inputType에 따라 filterType 자동 추론 (number, date, select, text)
This commit is contained in:
kjs 2025-11-20 16:21:18 +09:00
parent 45ac397417
commit e2cc09b2d6
6 changed files with 415 additions and 87 deletions

View File

@ -286,7 +286,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
const componentId = const componentId =
selectedComponent.componentType || // ⭐ section-card 등 selectedComponent.componentType || // ⭐ section-card 등
selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.type ||
selectedComponent.componentConfig?.id; selectedComponent.componentConfig?.id ||
(selectedComponent.type === "component" ? selectedComponent.id : null); // 🆕 독립 컴포넌트 (table-search-widget 등)
if (componentId) { if (componentId) {
const definition = ComponentRegistry.getComponent(componentId); const definition = ComponentRegistry.getComponent(componentId);
@ -318,7 +319,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<Settings className="h-4 w-4 text-primary" /> <Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3> <h3 className="text-sm font-semibold">{definition.name} </h3>
</div> </div>
<ConfigPanelComponent config={config} onChange={handleConfigChange} /> <ConfigPanelComponent
config={config}
onChange={handleConfigChange}
tables={tables} // 테이블 정보 전달
/>
</div> </div>
); );
}; };
@ -994,6 +999,16 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
); );
} }
// 🆕 ComponentRegistry에서 전용 ConfigPanel이 있는지 먼저 확인
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
// 전용 ConfigPanel이 있으면 renderComponentConfigPanel 호출
const configPanelContent = renderComponentConfigPanel();
if (configPanelContent) {
return configPanelContent;
}
}
// 현재 웹타입의 기본 입력 타입 추출 // 현재 웹타입의 기본 입력 타입 추출
const currentBaseInputType = webType ? getBaseInputType(webType as any) : null; const currentBaseInputType = webType ? getBaseInputType(webType as any) : null;

View File

@ -26,6 +26,7 @@ interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onFiltersApplied?: (filters: TableFilter[]) => void; // 필터 적용 시 콜백 onFiltersApplied?: (filters: TableFilter[]) => void; // 필터 적용 시 콜백
screenId?: number; // 화면 ID 추가
} }
// 필터 타입별 연산자 // 필터 타입별 연산자
@ -69,7 +70,7 @@ interface ColumnFilterConfig {
selectOptions?: Array<{ label: string; value: string }>; selectOptions?: Array<{ label: string; value: string }>;
} }
export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied }) => { export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied, screenId }) => {
const { getTable, selectedTableId } = useTableOptions(); const { getTable, selectedTableId } = useTableOptions();
const table = selectedTableId ? getTable(selectedTableId) : undefined; const table = selectedTableId ? getTable(selectedTableId) : undefined;
@ -79,7 +80,10 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
// localStorage에서 저장된 필터 설정 불러오기 // localStorage에서 저장된 필터 설정 불러오기
useEffect(() => { useEffect(() => {
if (table?.columns && table?.tableName) { if (table?.columns && table?.tableName) {
const storageKey = `table_filters_${table.tableName}`; // 화면별로 독립적인 필터 설정 저장
const storageKey = screenId
? `table_filters_${table.tableName}_screen_${screenId}`
: `table_filters_${table.tableName}`;
const savedFilters = localStorage.getItem(storageKey); const savedFilters = localStorage.getItem(storageKey);
let filters: ColumnFilterConfig[]; let filters: ColumnFilterConfig[];
@ -192,9 +196,11 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
width: cf.width || 200, // 너비 포함 (기본 200px) width: cf.width || 200, // 너비 포함 (기본 200px)
})); }));
// localStorage에 저장 // localStorage에 저장 (화면별로 독립적)
if (table?.tableName) { if (table?.tableName) {
const storageKey = `table_filters_${table.tableName}`; const storageKey = screenId
? `table_filters_${table.tableName}_screen_${screenId}`
: `table_filters_${table.tableName}`;
localStorage.setItem(storageKey, JSON.stringify(columnFilters)); localStorage.setItem(storageKey, JSON.stringify(columnFilters));
} }
@ -216,9 +222,11 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
setColumnFilters(clearedFilters); setColumnFilters(clearedFilters);
setSelectAll(false); setSelectAll(false);
// localStorage에서 제거 // localStorage에서 제거 (화면별로 독립적)
if (table?.tableName) { if (table?.tableName) {
const storageKey = `table_filters_${table.tableName}`; const storageKey = screenId
? `table_filters_${table.tableName}_screen_${screenId}`
: `table_filters_${table.tableName}`;
localStorage.removeItem(storageKey); localStorage.removeItem(storageKey);
} }

View File

@ -148,7 +148,7 @@ export interface TableListComponentProps {
tableName?: string; tableName?: string;
onRefresh?: () => void; onRefresh?: () => void;
onClose?: () => void; onClose?: () => void;
screenId?: string; screenId?: number | string; // 화면 ID (필터 설정 저장용)
userId?: string; // 사용자 ID (컬럼 순서 저장용) userId?: string; // 사용자 ID (컬럼 순서 저장용)
onSelectedRowsChange?: ( onSelectedRowsChange?: (
selectedRows: any[], selectedRows: any[],
@ -183,6 +183,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
refreshKey, refreshKey,
tableName, tableName,
userId, userId,
screenId, // 화면 ID 추출
}) => { }) => {
// ======================================== // ========================================
// 설정 및 스타일 // 설정 및 스타일
@ -1535,17 +1536,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// useEffect 훅 // useEffect 훅
// ======================================== // ========================================
// 필터 설정 localStorage 키 생성 // 필터 설정 localStorage 키 생성 (화면별로 독립적)
const filterSettingKey = useMemo(() => { const filterSettingKey = useMemo(() => {
if (!tableConfig.selectedTable) return null; if (!tableConfig.selectedTable) return null;
return `tableList_filterSettings_${tableConfig.selectedTable}`; return screenId
}, [tableConfig.selectedTable]); ? `tableList_filterSettings_${tableConfig.selectedTable}_screen_${screenId}`
: `tableList_filterSettings_${tableConfig.selectedTable}`;
}, [tableConfig.selectedTable, screenId]);
// 그룹 설정 localStorage 키 생성 // 그룹 설정 localStorage 키 생성 (화면별로 독립적)
const groupSettingKey = useMemo(() => { const groupSettingKey = useMemo(() => {
if (!tableConfig.selectedTable) return null; if (!tableConfig.selectedTable) return null;
return `tableList_groupSettings_${tableConfig.selectedTable}`; return screenId
}, [tableConfig.selectedTable]); ? `tableList_groupSettings_${tableConfig.selectedTable}_screen_${screenId}`
: `tableList_groupSettings_${tableConfig.selectedTable}`;
}, [tableConfig.selectedTable, screenId]);
// 저장된 필터 설정 불러오기 // 저장된 필터 설정 불러오기
useEffect(() => { useEffect(() => {

View File

@ -12,6 +12,14 @@ import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
import { TableFilter } from "@/types/table-options"; import { TableFilter } from "@/types/table-options";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface PresetFilter {
id: string;
columnName: string;
columnLabel: string;
filterType: "text" | "number" | "date" | "select";
width?: number;
}
interface TableSearchWidgetProps { interface TableSearchWidgetProps {
component: { component: {
id: string; id: string;
@ -25,6 +33,8 @@ interface TableSearchWidgetProps {
componentConfig?: { componentConfig?: {
autoSelectFirstTable?: boolean; // 첫 번째 테이블 자동 선택 여부 autoSelectFirstTable?: boolean; // 첫 번째 테이블 자동 선택 여부
showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부 showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부
filterMode?: "dynamic" | "preset"; // 필터 모드
presetFilters?: PresetFilter[]; // 고정 필터 목록
}; };
}; };
screenId?: number; // 화면 ID screenId?: number; // 화면 ID
@ -63,6 +73,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true; const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true;
const showTableSelector = component.componentConfig?.showTableSelector ?? true; const showTableSelector = component.componentConfig?.showTableSelector ?? true;
const filterMode = component.componentConfig?.filterMode ?? "dynamic";
const presetFilters = component.componentConfig?.presetFilters ?? [];
// Map을 배열로 변환 // Map을 배열로 변환
const tableList = Array.from(registeredTables.values()); const tableList = Array.from(registeredTables.values());
@ -77,41 +89,58 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
} }
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]); }, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
// 현재 테이블의 저장된 필터 불러오기 // 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
useEffect(() => { useEffect(() => {
if (currentTable?.tableName) { if (!currentTable?.tableName) return;
const storageKey = `table_filters_${currentTable.tableName}`;
const savedFilters = localStorage.getItem(storageKey);
if (savedFilters) { // 고정 모드: presetFilters를 activeFilters로 설정
try { if (filterMode === "preset") {
const parsed = JSON.parse(savedFilters) as Array<{ const activeFiltersList: TableFilter[] = presetFilters.map((f) => ({
columnName: string; columnName: f.columnName,
columnLabel: string; operator: "contains",
inputType: string; value: "",
enabled: boolean; filterType: f.filterType,
filterType: "text" | "number" | "date" | "select"; width: f.width || 200,
width?: number; }));
}>; setActiveFilters(activeFiltersList);
return;
}
// enabled된 필터들만 activeFilters로 설정 // 동적 모드: 화면별로 독립적인 필터 설정 불러오기
const activeFiltersList: TableFilter[] = parsed const storageKey = screenId
.filter((f) => f.enabled) ? `table_filters_${currentTable.tableName}_screen_${screenId}`
.map((f) => ({ : `table_filters_${currentTable.tableName}`;
columnName: f.columnName, const savedFilters = localStorage.getItem(storageKey);
operator: "contains",
value: "",
filterType: f.filterType,
width: f.width || 200, // 저장된 너비 포함
}));
setActiveFilters(activeFiltersList); if (savedFilters) {
} catch (error) { try {
console.error("저장된 필터 불러오기 실패:", error); const parsed = JSON.parse(savedFilters) as Array<{
} columnName: string;
columnLabel: string;
inputType: string;
enabled: boolean;
filterType: "text" | "number" | "date" | "select";
width?: number;
}>;
// enabled된 필터들만 activeFilters로 설정
const activeFiltersList: TableFilter[] = parsed
.filter((f) => f.enabled)
.map((f) => ({
columnName: f.columnName,
operator: "contains",
value: "",
filterType: f.filterType,
width: f.width || 200, // 저장된 너비 포함
}));
setActiveFilters(activeFiltersList);
} catch (error) {
console.error("저장된 필터 불러오기 실패:", error);
} }
} }
}, [currentTable?.tableName]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTable?.tableName, filterMode, screenId, JSON.stringify(presetFilters)]);
// select 옵션 초기 로드 (한 번만 실행, 이후 유지) // select 옵션 초기 로드 (한 번만 실행, 이후 유지)
useEffect(() => { useEffect(() => {
@ -362,7 +391,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
{/* 필터가 없을 때는 빈 공간 */} {/* 필터가 없을 때는 빈 공간 */}
{activeFilters.length === 0 && <div className="flex-1" />} {activeFilters.length === 0 && <div className="flex-1" />}
{/* 오른쪽: 데이터 건수 + 설정 버튼들 */} {/* 오른쪽: 데이터 건수 + 설정 버튼들 (고정 모드에서는 숨김) */}
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">
{/* 데이터 건수 표시 */} {/* 데이터 건수 표시 */}
{currentTable?.dataCount !== undefined && ( {currentTable?.dataCount !== undefined && (
@ -371,38 +400,43 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
</div> </div>
)} )}
<Button {/* 동적 모드일 때만 설정 버튼들 표시 */}
variant="outline" {filterMode === "dynamic" && (
size="sm" <>
onClick={() => setColumnVisibilityOpen(true)} <Button
disabled={!selectedTableId} variant="outline"
className="h-8 text-xs sm:h-9 sm:text-sm" size="sm"
> onClick={() => setColumnVisibilityOpen(true)}
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" /> disabled={!selectedTableId}
className="h-8 text-xs sm:h-9 sm:text-sm"
</Button> >
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFilterOpen(true)} onClick={() => setFilterOpen(true)}
disabled={!selectedTableId} disabled={!selectedTableId}
className="h-8 text-xs sm:h-9 sm:text-sm" className="h-8 text-xs sm:h-9 sm:text-sm"
> >
<Filter className="mr-1 h-3 w-3 sm:h-4 sm:w-4" /> <Filter className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setGroupingOpen(true)} onClick={() => setGroupingOpen(true)}
disabled={!selectedTableId} disabled={!selectedTableId}
className="h-8 text-xs sm:h-9 sm:text-sm" className="h-8 text-xs sm:h-9 sm:text-sm"
> >
<Layers className="mr-1 h-3 w-3 sm:h-4 sm:w-4" /> <Layers className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button> </Button>
</>
)}
</div> </div>
{/* 패널들 */} {/* 패널들 */}
@ -411,6 +445,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
isOpen={filterOpen} isOpen={filterOpen}
onClose={() => setFilterOpen(false)} onClose={() => setFilterOpen(false)}
onFiltersApplied={(filters) => setActiveFilters(filters)} onFiltersApplied={(filters) => setActiveFilters(filters)}
screenId={screenId}
/> />
<GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} /> <GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} />
</div> </div>

View File

@ -3,27 +3,126 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Plus, X } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface TableSearchWidgetConfigPanelProps { interface TableSearchWidgetConfigPanelProps {
component: any; component?: any; // 레거시 지원
onUpdateProperty: (property: string, value: any) => void; config?: any; // 새 인터페이스
onUpdateProperty?: (property: string, value: any) => void; // 레거시 지원
onChange?: (newConfig: any) => void; // 새 인터페이스
tables?: any[]; // 화면의 테이블 정보
}
interface PresetFilter {
id: string;
columnName: string;
columnLabel: string;
filterType: "text" | "number" | "date" | "select";
width?: number;
} }
export function TableSearchWidgetConfigPanel({ export function TableSearchWidgetConfigPanel({
component, component,
config,
onUpdateProperty, onUpdateProperty,
onChange,
tables = [],
}: TableSearchWidgetConfigPanelProps) { }: TableSearchWidgetConfigPanelProps) {
// 레거시와 새 인터페이스 모두 지원
const currentConfig = config || component?.componentConfig || {};
const updateConfig = onChange || ((key: string, value: any) => {
if (onUpdateProperty) {
onUpdateProperty(`componentConfig.${key}`, value);
}
});
// 첫 번째 테이블의 컬럼 목록 가져오기
const availableColumns = tables.length > 0 && tables[0].columns ? tables[0].columns : [];
// inputType에서 filterType 추출 헬퍼 함수
const 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";
};
const [localAutoSelect, setLocalAutoSelect] = useState( const [localAutoSelect, setLocalAutoSelect] = useState(
component.componentConfig?.autoSelectFirstTable ?? true currentConfig.autoSelectFirstTable ?? true
); );
const [localShowSelector, setLocalShowSelector] = useState( const [localShowSelector, setLocalShowSelector] = useState(
component.componentConfig?.showTableSelector ?? true currentConfig.showTableSelector ?? true
);
const [localFilterMode, setLocalFilterMode] = useState<"dynamic" | "preset">(
currentConfig.filterMode ?? "dynamic"
);
const [localPresetFilters, setLocalPresetFilters] = useState<PresetFilter[]>(
currentConfig.presetFilters ?? []
); );
useEffect(() => { useEffect(() => {
setLocalAutoSelect(component.componentConfig?.autoSelectFirstTable ?? true); setLocalAutoSelect(currentConfig.autoSelectFirstTable ?? true);
setLocalShowSelector(component.componentConfig?.showTableSelector ?? true); setLocalShowSelector(currentConfig.showTableSelector ?? true);
}, [component.componentConfig]); setLocalFilterMode(currentConfig.filterMode ?? "dynamic");
setLocalPresetFilters(currentConfig.presetFilters ?? []);
}, [currentConfig]);
// 설정 업데이트 헬퍼
const handleUpdate = (key: string, value: any) => {
if (onChange) {
// 새 인터페이스: 전체 config 업데이트
onChange({ ...currentConfig, [key]: value });
} else if (onUpdateProperty) {
// 레거시: 개별 속성 업데이트
onUpdateProperty(`componentConfig.${key}`, value);
}
};
// 필터 추가
const addFilter = () => {
const newFilter: PresetFilter = {
id: `filter_${Date.now()}`,
columnName: "",
columnLabel: "",
filterType: "text",
width: 200,
};
const updatedFilters = [...localPresetFilters, newFilter];
setLocalPresetFilters(updatedFilters);
handleUpdate("presetFilters", updatedFilters);
};
// 필터 삭제
const removeFilter = (id: string) => {
const updatedFilters = localPresetFilters.filter((f) => f.id !== id);
setLocalPresetFilters(updatedFilters);
handleUpdate("presetFilters", updatedFilters);
};
// 필터 업데이트
const updateFilter = (id: string, field: keyof PresetFilter, value: any) => {
const updatedFilters = localPresetFilters.map((f) =>
f.id === id ? { ...f, [field]: value } : f
);
setLocalPresetFilters(updatedFilters);
handleUpdate("presetFilters", updatedFilters);
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -41,7 +140,7 @@ export function TableSearchWidgetConfigPanel({
checked={localAutoSelect} checked={localAutoSelect}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setLocalAutoSelect(checked as boolean); setLocalAutoSelect(checked as boolean);
onUpdateProperty("componentConfig.autoSelectFirstTable", checked); handleUpdate("autoSelectFirstTable", checked);
}} }}
/> />
<Label htmlFor="autoSelectFirstTable" className="text-xs sm:text-sm cursor-pointer"> <Label htmlFor="autoSelectFirstTable" className="text-xs sm:text-sm cursor-pointer">
@ -56,7 +155,7 @@ export function TableSearchWidgetConfigPanel({
checked={localShowSelector} checked={localShowSelector}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setLocalShowSelector(checked as boolean); setLocalShowSelector(checked as boolean);
onUpdateProperty("componentConfig.showTableSelector", checked); handleUpdate("showTableSelector", checked);
}} }}
/> />
<Label htmlFor="showTableSelector" className="text-xs sm:text-sm cursor-pointer"> <Label htmlFor="showTableSelector" className="text-xs sm:text-sm cursor-pointer">
@ -64,12 +163,178 @@ export function TableSearchWidgetConfigPanel({
</Label> </Label>
</div> </div>
{/* 필터 모드 선택 */}
<div className="space-y-2 border-t pt-4">
<Label className="text-xs sm:text-sm font-medium"> </Label>
<RadioGroup
value={localFilterMode}
onValueChange={(value: "dynamic" | "preset") => {
setLocalFilterMode(value);
handleUpdate("filterMode", value);
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dynamic" id="mode-dynamic" />
<Label htmlFor="mode-dynamic" className="text-xs sm:text-sm cursor-pointer font-normal">
( )
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="preset" id="mode-preset" />
<Label htmlFor="mode-preset" className="text-xs sm:text-sm cursor-pointer font-normal">
( )
</Label>
</div>
</RadioGroup>
</div>
{/* 고정 모드일 때만 필터 설정 UI 표시 */}
{localFilterMode === "preset" && (
<div className="space-y-3 border-t pt-4">
<div className="flex items-center justify-between">
<Label className="text-xs sm:text-sm font-medium"> </Label>
<Button
variant="outline"
size="sm"
onClick={addFilter}
className="h-7 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{localPresetFilters.length === 0 ? (
<div className="rounded-md bg-muted p-3 text-center text-xs text-muted-foreground">
. .
</div>
) : (
<div className="space-y-2">
{localPresetFilters.map((filter) => (
<div
key={filter.id}
className="rounded-md border bg-card p-3 space-y-2"
>
<div className="flex items-center justify-between">
<Label className="text-xs font-medium"> </Label>
<Button
variant="ghost"
size="sm"
onClick={() => removeFilter(filter.id)}
className="h-6 w-6 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
{/* 컬럼 선택 */}
<div>
<Label className="text-[10px] sm:text-xs mb-1"> </Label>
{availableColumns.length > 0 ? (
<Select
value={filter.columnName}
onValueChange={(value) => {
// 선택된 컬럼 정보 가져오기
const selectedColumn = availableColumns.find(
(col: any) => col.columnName === value
);
// 컬럼명과 라벨 동시 업데이트
const updatedFilters = localPresetFilters.map((f) =>
f.id === filter.id
? {
...f,
columnName: value,
columnLabel: selectedColumn?.columnLabel || value,
filterType: getFilterTypeFromInputType(selectedColumn?.inputType || "text"),
}
: f
);
setLocalPresetFilters(updatedFilters);
handleUpdate("presetFilters", updatedFilters);
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col: any) => (
<SelectItem key={col.columnName} value={col.columnName}>
<div className="flex items-center gap-2">
<span className="font-medium">{col.columnLabel}</span>
<span className="text-muted-foreground text-[10px]">
({col.columnName})
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={filter.columnName}
onChange={(e) => updateFilter(filter.id, "columnName", e.target.value)}
placeholder="예: customer_name"
className="h-7 text-xs"
/>
)}
{filter.columnLabel && (
<p className="text-muted-foreground mt-1 text-[10px]">
: {filter.columnLabel}
</p>
)}
</div>
{/* 필터 타입 */}
<div>
<Label className="text-[10px] sm:text-xs mb-1"> </Label>
<Select
value={filter.filterType}
onValueChange={(value: "text" | "number" | "date" | "select") =>
updateFilter(filter.id, "filterType", value)
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="select"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 너비 */}
<div>
<Label className="text-[10px] sm:text-xs mb-1"> (px)</Label>
<Input
type="number"
value={filter.width || 200}
onChange={(e) => updateFilter(filter.id, "width", parseInt(e.target.value))}
placeholder="200"
className="h-7 text-xs"
min={100}
max={500}
/>
</div>
</div>
))}
</div>
)}
</div>
)}
<div className="rounded-md bg-muted p-3 text-xs"> <div className="rounded-md bg-muted p-3 text-xs">
<p className="font-medium mb-1">:</p> <p className="font-medium mb-1">:</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground"> <ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li> , , </li> <li> , , </li>
<li> </li> <li> </li>
<li> </li> {localFilterMode === "dynamic" ? (
<li> </li>
) : (
<li> </li>
)}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -2,8 +2,8 @@ import React from "react";
import { TableSearchWidget } from "./TableSearchWidget"; import { TableSearchWidget } from "./TableSearchWidget";
export class TableSearchWidgetRenderer { export class TableSearchWidgetRenderer {
static render(component: any) { static render(component: any, props?: any) {
return <TableSearchWidget component={component} />; return <TableSearchWidget component={component} screenId={props?.screenId} />;
} }
} }