ERP-node/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx

398 lines
13 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Settings, Filter, Layers, X } 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,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface TableSearchWidgetProps {
component: {
id: string;
title?: string;
style?: {
width?: string;
height?: string;
padding?: string;
backgroundColor?: string;
};
componentConfig?: {
autoSelectFirstTable?: boolean; // 첫 번째 테이블 자동 선택 여부
showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부
};
};
}
export function TableSearchWidget({ component }: TableSearchWidgetProps) {
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 [selectedLabels, setSelectedLabels] = useState<Record<string, 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(() => {
const tables = Array.from(registeredTables.values());
if (autoSelectFirstTable && tables.length > 0 && !selectedTableId) {
setSelectedTableId(tables[0].tableId);
}
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
// 현재 테이블의 저장된 필터 불러오기
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);
} catch (error) {
console.error("저장된 필터 불러오기 실패:", error);
}
}
}
}, [currentTable?.tableName]);
// select 옵션 초기 로드 (한 번만 실행, 이후 유지)
useEffect(() => {
if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) {
return;
}
const loadSelectOptions = async () => {
const selectFilters = activeFilters.filter(f => f.filterType === "select");
if (selectFilters.length === 0) {
return;
}
console.log("🔄 [TableSearchWidget] select 옵션 초기 로드:", {
activeFiltersCount: activeFilters.length,
selectFiltersCount: selectFilters.length,
dataCount: currentTable.dataCount,
});
const newOptions: Record<string, Array<{ label: string; value: string }>> = { ...selectOptions };
for (const filter of selectFilters) {
// 이미 로드된 옵션이 있으면 스킵 (초기값 유지)
if (newOptions[filter.columnName] && newOptions[filter.columnName].length > 0) {
console.log("⏭️ [TableSearchWidget] 이미 로드된 옵션 스킵:", filter.columnName);
continue;
}
try {
const options = await currentTable.getColumnUniqueValues(filter.columnName);
newOptions[filter.columnName] = options;
console.log("✅ [TableSearchWidget] select 옵션 로드:", {
columnName: filter.columnName,
optionCount: options.length,
options: options.slice(0, 5),
});
} catch (error) {
console.error("❌ [TableSearchWidget] select 옵션 로드 실패:", filter.columnName, error);
}
}
console.log("✅ [TableSearchWidget] 최종 selectOptions:", newOptions);
setSelectOptions(newOptions);
};
loadSelectOptions();
}, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // dataCount 제거, tableName으로 변경
// 디버깅: 현재 테이블 정보 로깅
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) => {
console.log("🔍 [TableSearchWidget] 필터 값 변경:", {
columnName,
value,
currentTable: currentTable?.tableId,
});
const newValues = {
...filterValues,
[columnName]: value,
};
setFilterValues(newValues);
// 실시간 검색: 값 변경 시 즉시 필터 적용
applyFilters(newValues);
};
// 필터 적용 함수
const applyFilters = (values: Record<string, string> = filterValues) => {
// 빈 값이 아닌 필터만 적용
const filtersWithValues = activeFilters.map((filter) => ({
...filter,
value: values[filter.columnName] || "",
})).filter((f) => f.value !== "");
console.log("🔍 [TableSearchWidget] 필터 적용:", {
activeFilters: activeFilters.length,
filtersWithValues: filtersWithValues.length,
filters: filtersWithValues,
hasOnFilterChange: !!currentTable?.onFilterChange,
});
currentTable?.onFilterChange(filtersWithValues);
};
// 필터 초기화
const handleResetFilters = () => {
setFilterValues({});
setSelectedLabels({});
currentTable?.onFilterChange([]);
};
// 필터 입력 필드 렌더링
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-9 text-xs sm:text-sm"
style={{ height: '36px', minHeight: '36px' }}
placeholder={column?.columnLabel}
/>
);
case "number":
return (
<Input
type="number"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-9 text-xs sm:text-sm"
style={{ height: '36px', minHeight: '36px' }}
placeholder={column?.columnLabel}
/>
);
case "select": {
let options = selectOptions[filter.columnName] || [];
// 현재 선택된 값이 옵션 목록에 없으면 추가 (데이터 없을 때도 선택값 유지)
if (value && !options.find(opt => opt.value === value)) {
const savedLabel = selectedLabels[filter.columnName] || value;
options = [{ value, label: savedLabel }, ...options];
}
return (
<Select
value={value}
onValueChange={(val) => {
// 선택한 값의 라벨 저장
const selectedOption = options.find(opt => opt.value === val);
if (selectedOption) {
setSelectedLabels(prev => ({
...prev,
[filter.columnName]: selectedOption.label,
}));
}
handleFilterChange(filter.columnName, val);
}}
>
<SelectTrigger className="h-9 min-h-9 text-xs sm:text-sm" style={{ height: '36px', minHeight: '36px' }}>
<SelectValue placeholder={column?.columnLabel || "선택"} />
</SelectTrigger>
<SelectContent>
{options.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
</div>
) : (
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-9 text-xs sm:text-sm"
placeholder={column?.columnLabel}
/>
);
}
};
return (
<div
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,
}}
>
{/* 필터 입력 필드들 */}
{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="outline"
size="sm"
onClick={handleResetFilters}
className="h-9 shrink-0 text-xs sm:text-sm"
>
<X className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
)}
{/* 필터가 없을 때는 빈 공간 */}
{activeFilters.length === 0 && <div className="flex-1" />}
{/* 오른쪽: 데이터 건수 + 설정 버튼들 */}
<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"
onClick={() => {
console.log("🔘 [TableSearchWidget] 테이블 옵션 버튼 클릭");
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={() => {
console.log("🔘 [TableSearchWidget] 필터 설정 버튼 클릭");
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={() => {
console.log("🔘 [TableSearchWidget] 그룹 설정 버튼 클릭");
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>
{/* 패널들 */}
<ColumnVisibilityPanel
isOpen={columnVisibilityOpen}
onClose={() => setColumnVisibilityOpen(false)}
/>
<FilterPanel
isOpen={filterOpen}
onClose={() => setFilterOpen(false)}
onFiltersApplied={(filters) => setActiveFilters(filters)}
/>
<GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} />
</div>
);
}