feat: 선택(select) 타입 필터 동적 옵션 로드 기능 추가

- TableRegistration에 getColumnUniqueValues 콜백 함수 추가
- TableListComponent에서 현재 데이터의 고유 값 추출 함수 구현
- TableSearchWidget에서 select 타입 필터의 옵션을 자동으로 로드
- 테이블 데이터가 변경되면 필터 옵션도 자동 업데이트
- 데이터 건수 표시 기능도 함께 수정 (등록 순서 문제 해결)
This commit is contained in:
kjs 2025-11-12 12:06:58 +09:00
parent 33ba13b070
commit 58870237b6
5 changed files with 510 additions and 184 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import {
Dialog,
@ -18,197 +18,302 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Checkbox } from "@/components/ui/checkbox";
import { Plus, X } from "lucide-react";
import { TableFilter } from "@/types/table-options";
interface Props {
isOpen: boolean;
onClose: () => void;
onFiltersApplied?: (filters: TableFilter[]) => void; // 필터 적용 시 콜백
}
export const FilterPanel: React.FC<Props> = ({
isOpen,
onClose,
}) => {
const { getTable, selectedTableId } = useTableOptions();
const table = selectedTableId ? getTable(selectedTableId) : undefined;
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
const addFilter = () => {
setActiveFilters([
...activeFilters,
{ columnName: "", operator: "contains", value: "" },
]);
};
const removeFilter = (index: number) => {
setActiveFilters(activeFilters.filter((_, i) => i !== index));
};
const updateFilter = (
index: number,
field: keyof TableFilter,
value: any
) => {
setActiveFilters(
activeFilters.map((filter, i) =>
i === index ? { ...filter, [field]: value } : filter
)
);
};
const applyFilters = () => {
// 빈 필터 제거
const validFilters = activeFilters.filter(
(f) => f.columnName && f.value !== ""
);
table?.onFilterChange(validFilters);
onOpenChange(false);
};
const clearFilters = () => {
setActiveFilters([]);
table?.onFilterChange([]);
};
const operatorLabels: Record<string, string> = {
equals: "같음",
// 필터 타입별 연산자
const operatorsByType: Record<string, Record<string, string>> = {
text: {
contains: "포함",
equals: "같음",
startsWith: "시작",
endsWith: "끝",
notEquals: "같지 않음",
},
number: {
equals: "같음",
gt: "보다 큼",
lt: "보다 작음",
gte: "이상",
lte: "이하",
notEquals: "같지 않음",
},
date: {
equals: "같음",
gt: "이후",
lt: "이전",
gte: "이후 포함",
lte: "이전 포함",
},
select: {
equals: "같음",
notEquals: "같지 않음",
},
};
// 컬럼 필터 설정 인터페이스
interface ColumnFilterConfig {
columnName: string;
columnLabel: string;
inputType: string;
enabled: boolean;
filterType: "text" | "number" | "date" | "select";
selectOptions?: Array<{ label: string; value: string }>;
}
export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied }) => {
const { getTable, selectedTableId } = useTableOptions();
const table = selectedTableId ? getTable(selectedTableId) : undefined;
const [columnFilters, setColumnFilters] = useState<ColumnFilterConfig[]>([]);
const [selectAll, setSelectAll] = useState(false);
// localStorage에서 저장된 필터 설정 불러오기
useEffect(() => {
if (table?.columns && table?.tableName) {
const storageKey = `table_filters_${table.tableName}`;
const savedFilters = localStorage.getItem(storageKey);
let filters: ColumnFilterConfig[];
if (savedFilters) {
try {
const parsed = JSON.parse(savedFilters) as ColumnFilterConfig[];
// 저장된 설정과 현재 컬럼 병합
filters = table.columns
.filter((col) => col.filterable !== false)
.map((col) => {
const saved = parsed.find((f) => f.columnName === col.columnName);
return saved || {
columnName: col.columnName,
columnLabel: col.columnLabel,
inputType: col.inputType || "text",
enabled: false,
filterType: mapInputTypeToFilterType(col.inputType || "text"),
};
});
} catch (error) {
console.error("저장된 필터 설정 불러오기 실패:", error);
filters = table.columns
.filter((col) => col.filterable !== false)
.map((col) => ({
columnName: col.columnName,
columnLabel: col.columnLabel,
inputType: col.inputType || "text",
enabled: false,
filterType: mapInputTypeToFilterType(col.inputType || "text"),
}));
}
} else {
filters = table.columns
.filter((col) => col.filterable !== false)
.map((col) => ({
columnName: col.columnName,
columnLabel: col.columnLabel,
inputType: col.inputType || "text",
enabled: false,
filterType: mapInputTypeToFilterType(col.inputType || "text"),
}));
}
setColumnFilters(filters);
}
}, [table?.columns, table?.tableName]);
// inputType을 filterType으로 매핑
const mapInputTypeToFilterType = (
inputType: string
): "text" | "number" | "date" | "select" => {
if (inputType.includes("number") || inputType.includes("decimal")) {
return "number";
}
if (inputType.includes("date") || inputType.includes("time")) {
return "date";
}
if (
inputType.includes("select") ||
inputType.includes("code") ||
inputType.includes("category")
) {
return "select";
}
return "text";
};
// 전체 선택/해제
const toggleSelectAll = (checked: boolean) => {
setSelectAll(checked);
setColumnFilters((prev) =>
prev.map((filter) => ({ ...filter, enabled: checked }))
);
};
// 개별 필터 토글
const toggleFilter = (columnName: string) => {
setColumnFilters((prev) =>
prev.map((filter) =>
filter.columnName === columnName
? { ...filter, enabled: !filter.enabled }
: filter
)
);
};
// 필터 타입 변경
const updateFilterType = (
columnName: string,
filterType: "text" | "number" | "date" | "select"
) => {
setColumnFilters((prev) =>
prev.map((filter) =>
filter.columnName === columnName ? { ...filter, filterType } : filter
)
);
};
// 저장
const applyFilters = () => {
// enabled된 필터들만 TableFilter로 변환
const activeFilters: TableFilter[] = columnFilters
.filter((cf) => cf.enabled)
.map((cf) => ({
columnName: cf.columnName,
operator: "contains", // 기본 연산자
value: "",
filterType: cf.filterType,
}));
// localStorage에 저장
if (table?.tableName) {
const storageKey = `table_filters_${table.tableName}`;
localStorage.setItem(storageKey, JSON.stringify(columnFilters));
}
table?.onFilterChange(activeFilters);
// 콜백으로 활성화된 필터 정보 전달
onFiltersApplied?.(activeFilters);
onClose();
};
// 초기화
const clearFilters = () => {
setColumnFilters((prev) =>
prev.map((filter) => ({ ...filter, enabled: false }))
);
setSelectAll(false);
};
const enabledCount = columnFilters.filter((f) => f.enabled).length;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl">
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
.
.
. .
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 전체 선택/해제 */}
<div className="flex items-center justify-between">
<div className="text-xs text-muted-foreground sm:text-sm">
{activeFilters.length}
<div className="flex items-center justify-between rounded-lg border bg-muted/30 p-3">
<div className="flex items-center gap-3">
<Checkbox
checked={selectAll}
onCheckedChange={(checked) =>
toggleSelectAll(checked as boolean)
}
/>
<span className="text-sm font-medium"> /</span>
</div>
<div className="text-xs text-muted-foreground">
{enabledCount} / {columnFilters.length}
</div>
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-7 text-xs"
>
</Button>
</div>
{/* 필터 리스트 */}
<ScrollArea className="h-[300px] sm:h-[400px]">
<div className="space-y-3 pr-4">
{activeFilters.map((filter, index) => (
{/* 컬럼 필터 리스트 */}
<ScrollArea className="h-[400px] sm:h-[450px]">
<div className="space-y-2 pr-4">
{columnFilters.map((filter) => (
<div
key={index}
className="flex flex-col gap-2 rounded-lg border bg-background p-3 sm:flex-row sm:items-center"
key={filter.columnName}
className="flex items-center gap-3 rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50"
>
{/* 컬럼 선택 */}
<Select
value={filter.columnName}
onValueChange={(val) =>
updateFilter(index, "columnName", val)
}
>
<SelectTrigger className="h-8 text-xs sm:h-9 sm:w-40 sm:text-sm">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{table?.columns
.filter((col) => col.filterable !== false)
.map((col) => (
<SelectItem
key={col.columnName}
value={col.columnName}
>
{col.columnLabel}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 체크박스 */}
<Checkbox
checked={filter.enabled}
onCheckedChange={() => toggleFilter(filter.columnName)}
/>
{/* 연산자 선택 */}
{/* 컬럼 정보 */}
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">
{filter.columnLabel}
</div>
<div className="truncate text-xs text-muted-foreground">
{filter.columnName}
</div>
</div>
{/* 필터 타입 선택 */}
<Select
value={filter.operator}
onValueChange={(val) =>
updateFilter(index, "operator", val)
value={filter.filterType}
onValueChange={(val: any) =>
updateFilterType(filter.columnName, val)
}
disabled={!filter.enabled}
>
<SelectTrigger className="h-8 text-xs sm:h-9 sm:w-32 sm:text-sm">
<SelectTrigger className="h-8 w-[110px] text-xs sm:h-9 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(operatorLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="select"></SelectItem>
</SelectContent>
</Select>
{/* 값 입력 */}
<Input
value={filter.value as string}
onChange={(e) =>
updateFilter(index, "value", e.target.value)
}
placeholder="값 입력"
className="h-8 flex-1 text-xs sm:h-9 sm:text-sm"
/>
{/* 삭제 버튼 */}
<Button
variant="ghost"
size="icon"
onClick={() => removeFilter(index)}
className="h-8 w-8 shrink-0 sm:h-9 sm:w-9"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</ScrollArea>
{/* 필터 추가 버튼 */}
<Button
variant="outline"
onClick={addFilter}
className="h-8 w-full text-xs sm:h-9 sm:text-sm"
>
<Plus className="mr-2 h-4 w-4" />
</Button>
{/* 안내 메시지 */}
<div className="rounded-lg bg-muted/50 p-3 text-center text-xs text-muted-foreground">
1
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="ghost"
onClick={clearFilters}
className="h-8 text-xs sm:h-10 sm:text-sm"
>
</Button>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
onClick={onClose}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={applyFilters}
disabled={enabledCount === 0}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>

View File

@ -72,6 +72,29 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
[registeredTables]
);
/**
*
*/
const updateTableDataCount = useCallback((tableId: string, count: number) => {
setRegisteredTables((prev) => {
const table = prev.get(tableId);
if (table) {
// 기존 테이블 정보에 dataCount만 업데이트
const updatedTable = { ...table, dataCount: count };
const newMap = new Map(prev);
newMap.set(tableId, updatedTable);
console.log("🔄 [TableOptionsContext] 데이터 건수 업데이트:", {
tableId,
count,
updated: true,
});
return newMap;
}
console.warn("⚠️ [TableOptionsContext] 테이블을 찾을 수 없음:", tableId);
return prev;
});
}, []);
return (
<TableOptionsContext.Provider
value={{
@ -79,6 +102,7 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
registerTable,
unregisterTable,
getTable,
updateTableDataCount,
selectedTableId,
setSelectedTableId,
}}

View File

@ -246,19 +246,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 상태 관리
// ========================================
// 사용자 정보
const { userId } = useAuth();
// 사용자 정보 (props에서 받거나 useAuth에서 가져오기)
const { userId: authUserId } = useAuth();
const currentUserId = userId || authUserId;
// TableOptions Context
const { registerTable, unregisterTable } = useTableOptions();
const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions();
const [filters, setFilters] = useState<TableFilter[]>([]);
const [grouping, setGrouping] = useState<string[]>([]);
const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility[]>([]);
// 초기 로드 시 localStorage에서 저장된 설정 불러오기
useEffect(() => {
if (tableConfig.selectedTable && userId) {
const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${userId}`;
if (tableConfig.selectedTable && currentUserId) {
const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`;
const savedSettings = localStorage.getItem(storageKey);
if (savedSettings) {
@ -270,7 +271,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
}
}
}, [tableConfig.selectedTable, userId]);
}, [tableConfig.selectedTable, currentUserId]);
// columnVisibility 변경 시 컬럼 순서 및 가시성 적용
useEffect(() => {
@ -281,12 +282,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setColumnOrder(newOrder);
// localStorage에 저장 (사용자별)
if (tableConfig.selectedTable && userId) {
const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${userId}`;
if (tableConfig.selectedTable && currentUserId) {
const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`;
localStorage.setItem(storageKey, JSON.stringify(columnVisibility));
}
}
}, [columnVisibility, tableConfig.selectedTable, userId]);
}, [columnVisibility, tableConfig.selectedTable, currentUserId]);
const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false);
@ -345,10 +346,31 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return;
}
// 컬럼의 고유 값 조회 함수
const getColumnUniqueValues = async (columnName: string) => {
// 현재 로드된 데이터에서 고유 값 추출
const uniqueValues = new Set<string>();
data.forEach((row) => {
const value = row[columnName];
if (value !== null && value !== undefined && value !== "") {
uniqueValues.add(String(value));
}
});
// Set을 배열로 변환하고 정렬
const sortedValues = Array.from(uniqueValues).sort();
return sortedValues.map((value) => ({
label: value,
value: value,
}));
};
const registration = {
tableId,
label: tableLabel || tableConfig.selectedTable,
tableName: tableConfig.selectedTable,
dataCount: totalItems || data.length, // 초기 데이터 건수 포함
columns: columnsToRegister.map((col) => ({
columnName: col.columnName || col.field,
columnLabel: columnLabels[col.columnName] || col.displayName || col.label || col.columnName || col.field,
@ -361,6 +383,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onFilterChange: setFilters,
onGroupChange: setGrouping,
onColumnVisibilityChange: setColumnVisibility,
getColumnUniqueValues, // 고유 값 조회 함수 등록
};
registerTable(registration);
@ -376,6 +399,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
columnMeta,
columnWidths,
tableLabel,
data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용)
totalItems, // 전체 항목 수가 변경되면 재등록
registerTable,
unregisterTable,
]);

View File

@ -2,11 +2,13 @@
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Settings, Filter, Layers } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Settings, Filter, Layers, Search } from "lucide-react";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
import { TableFilter } from "@/types/table-options";
import {
Select,
SelectContent,
@ -33,16 +35,23 @@ interface TableSearchWidgetProps {
}
export function TableSearchWidget({ component }: TableSearchWidgetProps) {
const { registeredTables, selectedTableId, setSelectedTableId } = useTableOptions();
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
const [filterOpen, setFilterOpen] = useState(false);
const [groupingOpen, setGroupingOpen] = useState(false);
// 활성화된 필터 목록
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
// select 타입 필터의 옵션들
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true;
const showTableSelector = component.componentConfig?.showTableSelector ?? true;
// Map을 배열로 변환
const tableList = Array.from(registeredTables.values());
const currentTable = selectedTableId ? getTable(selectedTableId) : undefined;
// 첫 번째 테이블 자동 선택
useEffect(() => {
@ -53,58 +62,211 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
}
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
// 현재 테이블의 저장된 필터 불러오기 및 select 옵션 로드
useEffect(() => {
if (currentTable?.tableName) {
const storageKey = `table_filters_${currentTable.tableName}`;
const savedFilters = localStorage.getItem(storageKey);
if (savedFilters) {
try {
const parsed = JSON.parse(savedFilters) as Array<{
columnName: string;
columnLabel: string;
inputType: string;
enabled: boolean;
filterType: "text" | "number" | "date" | "select";
}>;
// enabled된 필터들만 activeFilters로 설정
const activeFiltersList: TableFilter[] = parsed
.filter((f) => f.enabled)
.map((f) => ({
columnName: f.columnName,
operator: "contains",
value: "",
filterType: f.filterType,
}));
setActiveFilters(activeFiltersList);
// select 타입 필터들의 옵션 로드
const loadSelectOptions = async () => {
const newOptions: Record<string, Array<{ label: string; value: string }>> = {};
for (const filter of activeFiltersList) {
if (filter.filterType === "select" && currentTable.getColumnUniqueValues) {
try {
const options = await currentTable.getColumnUniqueValues(filter.columnName);
newOptions[filter.columnName] = options;
console.log("✅ [TableSearchWidget] select 옵션 로드:", {
columnName: filter.columnName,
optionCount: options.length,
});
} catch (error) {
console.error("select 옵션 로드 실패:", filter.columnName, error);
}
}
}
setSelectOptions(newOptions);
};
loadSelectOptions();
} catch (error) {
console.error("저장된 필터 불러오기 실패:", error);
}
}
}
}, [currentTable?.tableName, currentTable?.getColumnUniqueValues]);
// 디버깅: 현재 테이블 정보 로깅
useEffect(() => {
if (currentTable) {
console.log("🔍 [TableSearchWidget] 현재 테이블 정보:", {
selectedTableId,
tableId: currentTable.tableId,
tableName: currentTable.tableName,
label: currentTable.label,
dataCount: currentTable.dataCount,
hasDataCount: currentTable.dataCount !== undefined,
});
} else {
console.log("🔍 [TableSearchWidget] 테이블 없음:", { selectedTableId });
}
}, [selectedTableId, currentTable?.dataCount]);
const hasMultipleTables = tableList.length > 1;
// 필터 값 변경 핸들러
const handleFilterChange = (columnName: string, value: string) => {
setFilterValues((prev) => ({
...prev,
[columnName]: value,
}));
};
// 검색 실행
const handleSearch = () => {
// 빈 값이 아닌 필터만 적용
const filtersWithValues = activeFilters.map((filter) => ({
...filter,
value: filterValues[filter.columnName] || "",
})).filter((f) => f.value !== "");
currentTable?.onFilterChange(filtersWithValues);
};
// 필터 입력 필드 렌더링
const renderFilterInput = (filter: TableFilter) => {
const column = currentTable?.columns.find((c) => c.columnName === filter.columnName);
const value = filterValues[filter.columnName] || "";
switch (filter.filterType) {
case "date":
return (
<Input
type="date"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-8 text-xs sm:h-9 sm:text-sm"
placeholder={column?.columnLabel}
/>
);
case "number":
return (
<Input
type="number"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-8 text-xs sm:h-9 sm:text-sm"
placeholder={column?.columnLabel}
/>
);
case "select": {
const options = selectOptions[filter.columnName] || [];
return (
<Select
value={value}
onValueChange={(val) => handleFilterChange(filter.columnName, val)}
>
<SelectTrigger className="h-8 text-xs sm:h-9 sm:text-sm">
<SelectValue placeholder={column?.columnLabel} />
</SelectTrigger>
<SelectContent>
{options.length === 0 ? (
<SelectItem value="" disabled>
</SelectItem>
) : (
options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
);
}
default: // text
return (
<Input
type="text"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-8 text-xs sm:h-9 sm:text-sm"
placeholder={column?.columnLabel}
/>
);
}
};
return (
<div
className="flex h-full w-full items-center justify-between gap-2 border-b bg-card"
className="flex h-full w-full items-center gap-2 border-b bg-card"
style={{
padding: component.style?.padding || "0.75rem",
backgroundColor: component.style?.backgroundColor,
}}
>
{/* 왼쪽: 제목 + 테이블 정보 */}
<div className="flex items-center gap-3">
{/* 제목 */}
{component.title && (
<div className="text-sm font-medium text-foreground">
{component.title}
</div>
)}
{/* 필터 입력 필드들 */}
{activeFilters.length > 0 && (
<div className="flex flex-1 items-center gap-2 overflow-x-auto">
{activeFilters.map((filter) => (
<div key={filter.columnName} className="min-w-[150px]">
{renderFilterInput(filter)}
</div>
))}
{/* 검색 버튼 */}
<Button
variant="default"
size="sm"
onClick={handleSearch}
className="h-8 shrink-0 text-xs sm:h-9 sm:text-sm"
>
<Search className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
)}
{/* 테이블 선택 드롭다운 (여러 테이블이 있고, showTableSelector가 true일 때만) */}
{showTableSelector && hasMultipleTables && (
<Select value={selectedTableId || ""} onValueChange={setSelectedTableId}>
<SelectTrigger className="h-8 w-[200px] text-xs sm:h-9 sm:text-sm">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tableList.map((table) => (
<SelectItem key={table.tableId} value={table.tableId} className="text-xs sm:text-sm">
{table.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* 필터가 없을 때는 빈 공간 */}
{activeFilters.length === 0 && <div className="flex-1" />}
{/* 테이블이 하나만 있을 때는 라벨만 표시 */}
{!hasMultipleTables && tableList.length === 1 && (
<div className="text-xs text-muted-foreground sm:text-sm">
{tableList[0].label}
</div>
)}
{/* 테이블이 없을 때 */}
{tableList.length === 0 && (
<div className="text-xs text-muted-foreground sm:text-sm">
</div>
)}
</div>
{/* 오른쪽: 버튼들 */}
{/* 오른쪽: 데이터 건수 + 설정 버튼들 */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* 데이터 건수 표시 */}
{currentTable?.dataCount !== undefined && (
<div className="rounded-md bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground sm:text-sm">
{currentTable.dataCount.toLocaleString()}
</div>
)}
<Button
variant="outline"
size="sm"
@ -153,7 +315,11 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
isOpen={columnVisibilityOpen}
onClose={() => setColumnVisibilityOpen(false)}
/>
<FilterPanel isOpen={filterOpen} onClose={() => setFilterOpen(false)} />
<FilterPanel
isOpen={filterOpen}
onClose={() => setFilterOpen(false)}
onFiltersApplied={(filters) => setActiveFilters(filters)}
/>
<GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} />
</div>
);

View File

@ -18,6 +18,7 @@ export interface TableFilter {
| "lte"
| "notEquals";
value: string | number | boolean;
filterType?: "text" | "number" | "date" | "select"; // 필터 입력 타입
}
/**
@ -52,11 +53,15 @@ export interface TableRegistration {
label: string; // 사용자에게 보이는 이름 (예: "품목 관리")
tableName: string; // 실제 DB 테이블명 (예: "item_info")
columns: TableColumn[];
dataCount?: number; // 현재 표시된 데이터 건수
// 콜백 함수들
onFilterChange: (filters: TableFilter[]) => void;
onGroupChange: (groups: string[]) => void;
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
// 데이터 조회 함수 (선택 타입 필터용)
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;
}
/**
@ -67,6 +72,7 @@ export interface TableOptionsContextValue {
registerTable: (registration: TableRegistration) => void;
unregisterTable: (tableId: string) => void;
getTable: (tableId: string) => TableRegistration | undefined;
updateTableDataCount: (tableId: string, count: number) => void; // 데이터 건수 업데이트
selectedTableId: string | null;
setSelectedTableId: (tableId: string | null) => void;
}