From e2cc09b2d6e0ad6ece634d0ec21d5a58e097dc46 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 20 Nov 2025 16:21:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=9C=84=EC=A0=AF=20=ED=99=94=EB=A9=B4=EB=B3=84=20=EB=8F=85?= =?UTF-8?q?=EB=A6=BD=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=20=EB=AA=A8=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색 필터 설정을 화면별로 독립적으로 저장하도록 개선 (screenId 포함) - FilterPanel, TableSearchWidget, TableListComponent에 화면 ID 기반 localStorage 키 적용 - 동적 모드(사용자 설정)와 고정 모드(디자이너 설정) 2가지 필터 방식 추가 - 고정 모드에서 컬럼 드롭다운 선택 기능 구현 - 컬럼 선택 시 라벨 및 필터 타입 자동 설정 - ConfigPanel 표시 문제 해결 (type='component' 지원) - UnifiedPropertiesPanel에서 독립 컴포넌트 ConfigPanel 조회 개선 주요 변경: - 같은 테이블을 사용하는 다른 화면에서 필터 설정이 독립적으로 관리됨 - 고정 모드에서는 설정 버튼이 숨겨지고 지정된 필터만 표시 - 테이블 정보가 있으면 컬럼을 드롭다운으로 선택 가능 - inputType에 따라 filterType 자동 추론 (number, date, select, text) --- .../screen/panels/UnifiedPropertiesPanel.tsx | 19 +- .../screen/table-options/FilterPanel.tsx | 20 +- .../table-list/TableListComponent.tsx | 19 +- .../table-search-widget/TableSearchWidget.tsx | 155 ++++++---- .../TableSearchWidgetConfigPanel.tsx | 285 +++++++++++++++++- .../TableSearchWidgetRenderer.tsx | 4 +- 6 files changed, 415 insertions(+), 87 deletions(-) diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 6c77b4f1..49f31679 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -286,7 +286,8 @@ export const UnifiedPropertiesPanel: React.FC = ({ const componentId = selectedComponent.componentType || // ⭐ section-card 등 selectedComponent.componentConfig?.type || - selectedComponent.componentConfig?.id; + selectedComponent.componentConfig?.id || + (selectedComponent.type === "component" ? selectedComponent.id : null); // 🆕 독립 컴포넌트 (table-search-widget 등) if (componentId) { const definition = ComponentRegistry.getComponent(componentId); @@ -318,7 +319,11 @@ export const UnifiedPropertiesPanel: React.FC = ({

{definition.name} 설정

- + ); }; @@ -994,6 +999,16 @@ export const UnifiedPropertiesPanel: React.FC = ({ ); } + // 🆕 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; diff --git a/frontend/components/screen/table-options/FilterPanel.tsx b/frontend/components/screen/table-options/FilterPanel.tsx index 4688bb18..69395942 100644 --- a/frontend/components/screen/table-options/FilterPanel.tsx +++ b/frontend/components/screen/table-options/FilterPanel.tsx @@ -26,6 +26,7 @@ interface Props { isOpen: boolean; onClose: () => void; onFiltersApplied?: (filters: TableFilter[]) => void; // 필터 적용 시 콜백 + screenId?: number; // 화면 ID 추가 } // 필터 타입별 연산자 @@ -69,7 +70,7 @@ interface ColumnFilterConfig { selectOptions?: Array<{ label: string; value: string }>; } -export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied }) => { +export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied, screenId }) => { const { getTable, selectedTableId } = useTableOptions(); const table = selectedTableId ? getTable(selectedTableId) : undefined; @@ -79,7 +80,10 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied // localStorage에서 저장된 필터 설정 불러오기 useEffect(() => { 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); let filters: ColumnFilterConfig[]; @@ -192,9 +196,11 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied width: cf.width || 200, // 너비 포함 (기본 200px) })); - // localStorage에 저장 + // localStorage에 저장 (화면별로 독립적) 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)); } @@ -216,9 +222,11 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied setColumnFilters(clearedFilters); setSelectAll(false); - // localStorage에서 제거 + // localStorage에서 제거 (화면별로 독립적) 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); } diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 74d81211..24b80975 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -148,7 +148,7 @@ export interface TableListComponentProps { tableName?: string; onRefresh?: () => void; onClose?: () => void; - screenId?: string; + screenId?: number | string; // 화면 ID (필터 설정 저장용) userId?: string; // 사용자 ID (컬럼 순서 저장용) onSelectedRowsChange?: ( selectedRows: any[], @@ -183,6 +183,7 @@ export const TableListComponent: React.FC = ({ refreshKey, tableName, userId, + screenId, // 화면 ID 추출 }) => { // ======================================== // 설정 및 스타일 @@ -1535,17 +1536,21 @@ export const TableListComponent: React.FC = ({ // useEffect 훅 // ======================================== - // 필터 설정 localStorage 키 생성 + // 필터 설정 localStorage 키 생성 (화면별로 독립적) const filterSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; - return `tableList_filterSettings_${tableConfig.selectedTable}`; - }, [tableConfig.selectedTable]); + return screenId + ? `tableList_filterSettings_${tableConfig.selectedTable}_screen_${screenId}` + : `tableList_filterSettings_${tableConfig.selectedTable}`; + }, [tableConfig.selectedTable, screenId]); - // 그룹 설정 localStorage 키 생성 + // 그룹 설정 localStorage 키 생성 (화면별로 독립적) const groupSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; - return `tableList_groupSettings_${tableConfig.selectedTable}`; - }, [tableConfig.selectedTable]); + return screenId + ? `tableList_groupSettings_${tableConfig.selectedTable}_screen_${screenId}` + : `tableList_groupSettings_${tableConfig.selectedTable}`; + }, [tableConfig.selectedTable, screenId]); // 저장된 필터 설정 불러오기 useEffect(() => { diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index 01906c21..0416c4b3 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -12,6 +12,14 @@ import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel"; import { TableFilter } from "@/types/table-options"; 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 { component: { id: string; @@ -25,6 +33,8 @@ interface TableSearchWidgetProps { componentConfig?: { autoSelectFirstTable?: boolean; // 첫 번째 테이블 자동 선택 여부 showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부 + filterMode?: "dynamic" | "preset"; // 필터 모드 + presetFilters?: PresetFilter[]; // 고정 필터 목록 }; }; screenId?: number; // 화면 ID @@ -63,6 +73,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true; const showTableSelector = component.componentConfig?.showTableSelector ?? true; + const filterMode = component.componentConfig?.filterMode ?? "dynamic"; + const presetFilters = component.componentConfig?.presetFilters ?? []; // Map을 배열로 변환 const tableList = Array.from(registeredTables.values()); @@ -77,41 +89,58 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table } }, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]); - // 현재 테이블의 저장된 필터 불러오기 + // 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드) useEffect(() => { - if (currentTable?.tableName) { - const storageKey = `table_filters_${currentTable.tableName}`; - const savedFilters = localStorage.getItem(storageKey); + if (!currentTable?.tableName) return; - if (savedFilters) { - try { - const parsed = JSON.parse(savedFilters) as Array<{ - columnName: string; - columnLabel: string; - inputType: string; - enabled: boolean; - filterType: "text" | "number" | "date" | "select"; - width?: number; - }>; + // 고정 모드: presetFilters를 activeFilters로 설정 + if (filterMode === "preset") { + const activeFiltersList: TableFilter[] = presetFilters.map((f) => ({ + columnName: f.columnName, + operator: "contains", + value: "", + filterType: f.filterType, + width: f.width || 200, + })); + setActiveFilters(activeFiltersList); + return; + } - // 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, // 저장된 너비 포함 - })); + // 동적 모드: 화면별로 독립적인 필터 설정 불러오기 + const storageKey = screenId + ? `table_filters_${currentTable.tableName}_screen_${screenId}` + : `table_filters_${currentTable.tableName}`; + const savedFilters = localStorage.getItem(storageKey); - setActiveFilters(activeFiltersList); - } catch (error) { - console.error("저장된 필터 불러오기 실패:", error); - } + if (savedFilters) { + try { + 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 옵션 초기 로드 (한 번만 실행, 이후 유지) useEffect(() => { @@ -362,7 +391,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table {/* 필터가 없을 때는 빈 공간 */} {activeFilters.length === 0 &&
} - {/* 오른쪽: 데이터 건수 + 설정 버튼들 */} + {/* 오른쪽: 데이터 건수 + 설정 버튼들 (고정 모드에서는 숨김) */}
{/* 데이터 건수 표시 */} {currentTable?.dataCount !== undefined && ( @@ -371,38 +400,43 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
)} - + {/* 동적 모드일 때만 설정 버튼들 표시 */} + {filterMode === "dynamic" && ( + <> + - + - + + + )}
{/* 패널들 */} @@ -411,6 +445,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table isOpen={filterOpen} onClose={() => setFilterOpen(false)} onFiltersApplied={(filters) => setActiveFilters(filters)} + screenId={screenId} /> setGroupingOpen(false)} /> diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx index 646fd3c4..8c4ab6a1 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx @@ -3,27 +3,126 @@ import React, { useState, useEffect } from "react"; import { Label } from "@/components/ui/label"; 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 { - component: any; - onUpdateProperty: (property: string, value: any) => void; + component?: any; // 레거시 지원 + 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({ component, + config, onUpdateProperty, + onChange, + tables = [], }: 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( - component.componentConfig?.autoSelectFirstTable ?? true + currentConfig.autoSelectFirstTable ?? true ); const [localShowSelector, setLocalShowSelector] = useState( - component.componentConfig?.showTableSelector ?? true + currentConfig.showTableSelector ?? true + ); + const [localFilterMode, setLocalFilterMode] = useState<"dynamic" | "preset">( + currentConfig.filterMode ?? "dynamic" + ); + const [localPresetFilters, setLocalPresetFilters] = useState( + currentConfig.presetFilters ?? [] ); useEffect(() => { - setLocalAutoSelect(component.componentConfig?.autoSelectFirstTable ?? true); - setLocalShowSelector(component.componentConfig?.showTableSelector ?? true); - }, [component.componentConfig]); + setLocalAutoSelect(currentConfig.autoSelectFirstTable ?? true); + setLocalShowSelector(currentConfig.showTableSelector ?? true); + 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 (
@@ -41,7 +140,7 @@ export function TableSearchWidgetConfigPanel({ checked={localAutoSelect} onCheckedChange={(checked) => { setLocalAutoSelect(checked as boolean); - onUpdateProperty("componentConfig.autoSelectFirstTable", checked); + handleUpdate("autoSelectFirstTable", checked); }} />
+ {/* 필터 모드 선택 */} +
+ + { + setLocalFilterMode(value); + handleUpdate("filterMode", value); + }} + > +
+ + +
+
+ + +
+
+
+ + {/* 고정 모드일 때만 필터 설정 UI 표시 */} + {localFilterMode === "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" + /> + )} + {filter.columnLabel && ( +

+ 표시명: {filter.columnLabel} +

+ )} +
+ + {/* 필터 타입 */} +
+ + +
+ + {/* 너비 */} +
+ + updateFilter(filter.id, "width", parseInt(e.target.value))} + placeholder="200" + className="h-7 text-xs" + min={100} + max={500} + /> +
+
+ ))} +
+ )} +
+ )} +

참고사항:

  • 테이블 리스트, 분할 패널, 플로우 위젯이 자동 감지됩니다
  • 여러 테이블이 있으면 드롭다운에서 선택할 수 있습니다
  • -
  • 선택한 테이블의 컬럼 정보가 자동으로 로드됩니다
  • + {localFilterMode === "dynamic" ? ( +
  • 사용자가 필터 설정 버튼을 클릭하여 원하는 필터를 선택합니다
  • + ) : ( +
  • 고정 모드에서는 설정 버튼이 숨겨지고 지정된 필터만 표시됩니다
  • + )}
diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetRenderer.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetRenderer.tsx index 6fe47cc7..21e589ef 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetRenderer.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetRenderer.tsx @@ -2,8 +2,8 @@ import React from "react"; import { TableSearchWidget } from "./TableSearchWidget"; export class TableSearchWidgetRenderer { - static render(component: any) { - return ; + static render(component: any, props?: any) { + return ; } }