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

@@ -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 && <div className="flex-1" />}
{/* 오른쪽: 데이터 건수 + 설정 버튼들 */}
{/* 오른쪽: 데이터 건수 + 설정 버튼들 (고정 모드에서는 숨김) */}
<div className="flex flex-shrink-0 items-center gap-2">
{/* 데이터 건수 표시 */}
{currentTable?.dataCount !== undefined && (
@@ -371,38 +400,43 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
</div>
)}
<Button
variant="outline"
size="sm"
onClick={() => setColumnVisibilityOpen(true)}
disabled={!selectedTableId}
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
{/* 동적 모드일 때만 설정 버튼들 표시 */}
{filterMode === "dynamic" && (
<>
<Button
variant="outline"
size="sm"
onClick={() => setColumnVisibilityOpen(true)}
disabled={!selectedTableId}
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setFilterOpen(true)}
disabled={!selectedTableId}
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" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setFilterOpen(true)}
disabled={!selectedTableId}
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" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setGroupingOpen(true)}
disabled={!selectedTableId}
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" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setGroupingOpen(true)}
disabled={!selectedTableId}
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" />
</Button>
</>
)}
</div>
{/* 패널들 */}
@@ -411,6 +445,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
isOpen={filterOpen}
onClose={() => setFilterOpen(false)}
onFiltersApplied={(filters) => setActiveFilters(filters)}
screenId={screenId}
/>
<GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} />
</div>