검색 필터기능 수정사항

This commit is contained in:
kjs
2025-09-23 14:26:18 +09:00
parent e653effac0
commit da9985cd24
11 changed files with 1537 additions and 318 deletions

View File

@@ -7,14 +7,12 @@ import { entityJoinApi } from "@/lib/api/entityJoin";
import { codeCache } from "@/lib/caching/codeCache";
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Search,
RefreshCw,
ArrowUpDown,
ArrowUp,
@@ -23,6 +21,8 @@ import {
} from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters";
import { Separator } from "@/components/ui/separator";
export interface TableListComponentProps {
component: any;
@@ -96,10 +96,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [tableLabel, setTableLabel] = useState<string>("");
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태
const [selectedSearchColumn, setSelectedSearchColumn] = useState<string>(""); // 선택된 검색 컬럼
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
// 고급 필터 관련 state
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
// 체크박스 상태 관리
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set()); // 선택된 행들의 키 집합
const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태
@@ -234,56 +236,66 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
page: currentPage,
size: localPageSize,
search: searchTerm?.trim()
? (() => {
// 스마트한 단일 컬럼 선택 로직 (서버가 OR 검색을 지원하지 않음)
let searchColumn = selectedSearchColumn || sortColumn; // 사용자 선택 컬럼 최우선, 그 다음 정렬된 컬럼
search: (() => {
// 고급 필터 값이 있으면 우선 사용
const hasAdvancedFilters = Object.values(searchValues).some((value) => {
if (typeof value === "string") return value.trim() !== "";
if (typeof value === "object" && value !== null) {
return Object.values(value).some((v) => v !== "" && v !== null && v !== undefined);
}
return value !== null && value !== undefined && value !== "";
});
if (!searchColumn) {
// 1순위: name 관련 컬럼 (가장 검색에 적합)
const nameColumns = visibleColumns.filter(
(col) =>
col.columnName.toLowerCase().includes("name") ||
col.columnName.toLowerCase().includes("title") ||
col.columnName.toLowerCase().includes("subject"),
);
if (hasAdvancedFilters) {
console.log("🔍 고급 검색 필터 사용:", searchValues);
console.log("🔍 고급 검색 필터 상세:", JSON.stringify(searchValues, null, 2));
return searchValues;
}
// 2순위: text/varchar 타입 컬럼
const textColumns = visibleColumns.filter(
(col) => col.dataType === "text" || col.dataType === "varchar",
);
// 고급 필터가 없으면 기존 단순 검색 사용
if (searchTerm?.trim()) {
// 스마트한 단일 컬럼 선택 로직 (서버가 OR 검색을 지원하지 않음)
let searchColumn = sortColumn; // 정렬된 컬럼 우선
// 3순위: description 관련 컬럼
const descColumns = visibleColumns.filter(
(col) =>
col.columnName.toLowerCase().includes("desc") ||
col.columnName.toLowerCase().includes("comment") ||
col.columnName.toLowerCase().includes("memo"),
);
// 우선순위에 따라 선택
if (nameColumns.length > 0) {
searchColumn = nameColumns[0].columnName;
} else if (textColumns.length > 0) {
searchColumn = textColumns[0].columnName;
} else if (descColumns.length > 0) {
searchColumn = descColumns[0].columnName;
} else {
// 마지막 대안: 첫 번째 컬럼
searchColumn = visibleColumns[0]?.columnName || "id";
}
}
console.log("🔍 선택된 검색 컬럼:", searchColumn);
console.log("🔍 검색어:", searchTerm);
console.log(
"🔍 사용 가능한 컬럼들:",
visibleColumns.map((col) => `${col.columnName}(${col.dataType || "unknown"})`),
if (!searchColumn) {
// 1순위: name 관련 컬럼 (가장 검색에 적합)
const nameColumns = visibleColumns.filter(
(col) =>
col.columnName.toLowerCase().includes("name") ||
col.columnName.toLowerCase().includes("title") ||
col.columnName.toLowerCase().includes("subject"),
);
return { [searchColumn]: searchTerm };
})()
: undefined,
// 2순위: text/varchar 타입 컬럼
const textColumns = visibleColumns.filter((col) => col.dataType === "text" || col.dataType === "varchar");
// 3순위: description 관련 컬럼
const descColumns = visibleColumns.filter(
(col) =>
col.columnName.toLowerCase().includes("desc") ||
col.columnName.toLowerCase().includes("comment") ||
col.columnName.toLowerCase().includes("memo"),
);
// 우선순위에 따라 선택
if (nameColumns.length > 0) {
searchColumn = nameColumns[0].columnName;
} else if (textColumns.length > 0) {
searchColumn = textColumns[0].columnName;
} else if (descColumns.length > 0) {
searchColumn = descColumns[0].columnName;
} else {
// 마지막 대안: 첫 번째 컬럼
searchColumn = visibleColumns[0]?.columnName || "id";
}
}
console.log("🔍 기존 검색 방식 사용:", { [searchColumn]: searchTerm });
return { [searchColumn]: searchTerm };
}
return undefined;
})(),
sortBy: sortColumn || undefined,
sortOrder: sortDirection,
enableEntityJoin: true, // 🎯 Entity 조인 활성화
@@ -410,10 +422,23 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
};
// 검색
const handleSearch = (term: string) => {
setSearchTerm(term);
setCurrentPage(1); // 검색 시 첫 페이지로 이동
// 고급 필터 핸들러
const handleSearchValueChange = (columnName: string, value: any) => {
setSearchValues((prev) => ({
...prev,
[columnName]: value,
}));
};
const handleAdvancedSearch = () => {
setCurrentPage(1);
fetchTableData();
};
const handleClearAdvancedFilters = () => {
setSearchValues({});
setCurrentPage(1);
fetchTableData();
};
// 새로고침
@@ -522,7 +547,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (tableConfig.autoLoad && !isDesignMode) {
fetchTableData();
}
}, [tableConfig.selectedTable, localPageSize, currentPage, searchTerm, sortColumn, sortDirection, columnLabels]);
}, [
tableConfig.selectedTable,
localPageSize,
currentPage,
searchTerm,
sortColumn,
sortDirection,
columnLabels,
searchValues,
]);
// refreshKey 변경 시 테이블 데이터 새로고침
useEffect(() => {
@@ -577,8 +611,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
.sort((a, b) => a.order - b.order);
}
// 체크박스가 활성화 경우 체크박스 컬럼을 추가
if (checkboxConfig.enabled) {
// 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가
if (checkboxConfig.enabled && columns.length > 0) {
const checkboxColumn: ColumnConfig = {
columnName: "__checkbox__",
displayName: "",
@@ -819,8 +853,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</div>
)}
{/* 검색 */}
{tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && (
{/* 검색 - 기존 방식은 주석처리 */}
{/* {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && (
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
@@ -831,7 +865,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
className="w-64 pl-8"
/>
</div>
{/* 검색 컬럼 선택 드롭다운 */}
{tableConfig.filter?.showColumnSelector && (
<select
value={selectedSearchColumn}
@@ -847,7 +880,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</select>
)}
</div>
)}
)} */}
{/* 새로고침 */}
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
@@ -857,6 +890,33 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</div>
)}
{/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */}
{tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && (
<>
<Separator className="my-2" />
<AdvancedSearchFilters
filters={tableConfig.filter?.filters || []} // 설정된 필터 사용, 없으면 자동 생성
searchValues={searchValues}
onSearchValueChange={handleSearchValueChange}
onSearch={handleAdvancedSearch}
onClearFilters={handleClearAdvancedFilters}
tableColumns={visibleColumns.map((col) => ({
columnName: col.columnName,
webType: columnMeta[col.columnName]?.webType || "text",
displayName: columnLabels[col.columnName] || col.displayName || col.columnName,
codeCategory: columnMeta[col.columnName]?.codeCategory,
isVisible: col.visible,
// 추가 메타데이터 전달 (필터 자동 생성용)
web_type: columnMeta[col.columnName]?.webType || "text",
column_name: col.columnName,
column_label: columnLabels[col.columnName] || col.displayName || col.columnName,
code_category: columnMeta[col.columnName]?.codeCategory,
}))}
tableName={tableConfig.selectedTable}
/>
</>
)}
{/* 테이블 컨텐츠 */}
<div className={`flex-1 ${localPageSize >= 50 ? "overflow-auto" : "overflow-hidden"}`}>
{loading ? (

View File

@@ -95,20 +95,33 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
fetchTables();
}, []);
// 선택된 테이블의 컬럼 목록 설정 (tableColumns prop 우선 사용)
// 선택된 테이블의 컬럼 목록 설정
useEffect(() => {
console.log("🔍 useEffect 실행됨 - tableColumns:", tableColumns, "length:", tableColumns?.length);
if (tableColumns && tableColumns.length > 0) {
// tableColumns prop이 있으면 사용
console.log(
"🔍 useEffect 실행됨 - config.selectedTable:",
config.selectedTable,
"screenTableName:",
screenTableName,
);
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우에만 컬럼 목록 표시
const shouldShowColumns = config.selectedTable || (screenTableName && config.columns && config.columns.length > 0);
if (!shouldShowColumns) {
console.log("🔧 컬럼 목록 숨김 - 명시적 테이블 선택 또는 설정된 컬럼이 없음");
setAvailableColumns([]);
return;
}
// tableColumns prop을 우선 사용하되, 컴포넌트가 명시적으로 설정되었을 때만
if (tableColumns && tableColumns.length > 0 && (config.selectedTable || config.columns?.length > 0)) {
console.log("🔧 tableColumns prop 사용:", tableColumns);
console.log("🔧 첫 번째 컬럼 상세:", tableColumns[0]);
const mappedColumns = tableColumns.map((column: any) => ({
columnName: column.columnName || column.name,
dataType: column.dataType || column.type || "text",
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
}));
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
console.log("🏷️ 첫 번째 mappedColumn:", mappedColumns[0]);
setAvailableColumns(mappedColumns);
} else if (config.selectedTable || screenTableName) {
// API에서 컬럼 정보 가져오기
@@ -144,7 +157,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
} else {
setAvailableColumns([]);
}
}, [config.selectedTable, screenTableName, tableColumns]);
}, [config.selectedTable, screenTableName, tableColumns, config.columns]);
// Entity 조인 컬럼 정보 가져오기
useEffect(() => {
@@ -274,6 +287,72 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
handleChange("columns", columns);
};
// 필터 추가
const addFilter = (columnName: string) => {
const existingFilter = config.filter?.filters?.find((f) => f.columnName === columnName);
if (existingFilter) return;
const column = availableColumns.find((col) => col.columnName === columnName);
if (!column) return;
// tableColumns에서 해당 컬럼의 메타정보 찾기
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName || tc.column_name === columnName);
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
const inferWidgetType = (dataType: string, webType?: string): string => {
// 웹타입이 있으면 우선 사용
if (webType) {
return webType;
}
// 데이터 타입으로 추론
const type = dataType.toLowerCase();
if (type.includes("int") || type.includes("numeric") || type.includes("decimal")) return "number";
if (type.includes("date") || type.includes("timestamp")) return "date";
if (type.includes("bool")) return "boolean";
return "text";
};
const widgetType = inferWidgetType(column.dataType, tableColumn?.webType || tableColumn?.web_type);
const newFilter = {
columnName,
widgetType,
label: column.label || column.columnName,
gridColumns: 3,
numberFilterMode: "range" as const,
// 코드 타입인 경우 코드 카테고리 추가
...(widgetType === "code" && {
codeCategory: tableColumn?.codeCategory || tableColumn?.code_category,
}),
// 엔티티 타입인 경우 참조 정보 추가
...(widgetType === "entity" && {
referenceTable: tableColumn?.referenceTable || tableColumn?.reference_table,
referenceColumn: tableColumn?.referenceColumn || tableColumn?.reference_column,
displayColumn: tableColumn?.displayColumn || tableColumn?.display_column,
}),
};
console.log("🔍 필터 추가:", newFilter);
const currentFilters = config.filter?.filters || [];
handleNestedChange("filter", "filters", [...currentFilters, newFilter]);
};
// 필터 제거
const removeFilter = (index: number) => {
const currentFilters = config.filter?.filters || [];
const updatedFilters = currentFilters.filter((_, i) => i !== index);
handleNestedChange("filter", "filters", updatedFilters);
};
// 필터 업데이트
const updateFilter = (index: number, key: string, value: any) => {
const currentFilters = config.filter?.filters || [];
const updatedFilters = currentFilters.map((filter, i) => (i === index ? { ...filter, [key]: value } : filter));
handleNestedChange("filter", "filters", updatedFilters);
};
return (
<div className="space-y-4">
<div className="text-sm font-medium"> </div>
@@ -620,6 +699,16 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
</div>
</CardContent>
</Card>
) : availableColumns.length === 0 ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-gray-500">
<p> </p>
<p className="text-sm"> .</p>
<p className="mt-2 text-xs text-blue-600"> : {screenTableName}</p>
</div>
</CardContent>
</Card>
) : (
<>
<Card>
@@ -990,54 +1079,141 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
{/* 필터 설정 탭 */}
<TabsContent value="filter" className="space-y-4">
<ScrollArea className="h-[600px] pr-4">
{/* 필터 기능 활성화 */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="filterEnabled"
checked={config.filter?.enabled}
checked={config.filter?.enabled || false}
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
/>
<Label htmlFor="filterEnabled"> </Label>
</div>
{config.filter?.enabled && (
<>
<div className="flex items-center space-x-2">
<Checkbox
id="quickSearch"
checked={config.filter?.quickSearch}
onCheckedChange={(checked) => handleNestedChange("filter", "quickSearch", checked)}
/>
<Label htmlFor="quickSearch"> </Label>
</div>
{config.filter?.quickSearch && (
<div className="ml-6 flex items-center space-x-2">
<Checkbox
id="showColumnSelector"
checked={config.filter?.showColumnSelector}
onCheckedChange={(checked) => handleNestedChange("filter", "showColumnSelector", checked)}
/>
<Label htmlFor="showColumnSelector"> </Label>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="advancedFilter"
checked={config.filter?.advancedFilter}
onCheckedChange={(checked) => handleNestedChange("filter", "advancedFilter", checked)}
/>
<Label htmlFor="advancedFilter"> </Label>
</div>
</>
)}
</CardContent>
</Card>
{/* 필터 목록 */}
{config.filter?.enabled && (
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 필터 추가 버튼 */}
{availableColumns.length > 0 && (
<div className="flex flex-wrap gap-2">
{availableColumns
.filter((col) => !config.filter?.filters?.find((f) => f.columnName === col.columnName))
.map((column) => (
<Button
key={column.columnName}
variant="outline"
size="sm"
onClick={() => addFilter(column.columnName)}
className="flex items-center gap-1"
>
<Plus className="h-3 w-3" />
{column.label || column.columnName}
<Badge variant="secondary" className="text-xs">
{column.dataType}
</Badge>
</Button>
))}
</div>
)}
{/* 설정된 필터 목록 */}
{config.filter?.filters && config.filter.filters.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium"> </h4>
{config.filter.filters.map((filter, index) => (
<div key={filter.columnName} className="space-y-3 rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline">{filter.widgetType}</Badge>
<span className="font-medium">{filter.label}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeFilter(index)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs"></Label>
<Input
value={filter.label}
onChange={(e) => updateFilter(index, "label", e.target.value)}
placeholder="필터 라벨"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.gridColumns.toString()}
onValueChange={(value) => updateFilter(index, "gridColumns", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="6">6</SelectItem>
</SelectContent>
</Select>
</div>
{/* 숫자 타입인 경우 검색 모드 선택 */}
{(filter.widgetType === "number" || filter.widgetType === "decimal") && (
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.numberFilterMode || "range"}
onValueChange={(value) => updateFilter(index, "numberFilterMode", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="exact"> </SelectItem>
<SelectItem value="range"> </SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* 코드 타입인 경우 코드 카테고리 */}
{filter.widgetType === "code" && (
<div>
<Label className="text-xs"> </Label>
<Input
value={filter.codeCategory || ""}
onChange={(e) => updateFilter(index, "codeCategory", e.target.value)}
placeholder="코드 카테고리"
/>
</div>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
)}
</ScrollArea>
</TabsContent>

View File

@@ -59,10 +59,7 @@ export const TableListDefinition = createComponentDefinition({
// 필터 설정
filter: {
enabled: true,
quickSearch: true,
showColumnSelector: true, // 검색컬럼 선택기 표시 기본값
advancedFilter: false,
filterableColumns: [],
filters: [], // 사용자가 설정할 필터 목록
},
// 액션 설정

View File

@@ -71,14 +71,17 @@ export interface ColumnConfig {
*/
export interface FilterConfig {
enabled: boolean;
quickSearch: boolean;
showColumnSelector?: boolean; // 검색 컬럼 선택기 표시 여부
advancedFilter: boolean;
filterableColumns: string[];
defaultFilters?: Array<{
column: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN";
value: any;
// 사용할 필터 목록 (DataTableFilter 타입 사용)
filters: Array<{
columnName: string;
widgetType: string;
label: string;
gridColumns: number;
numberFilterMode?: "exact" | "range"; // 숫자 필터 모드
codeCategory?: string;
referenceTable?: string;
referenceColumn?: string;
displayColumn?: string;
}>;
}