From da9985cd24793f31e7c4c6389eac5c7be55cdf32 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 14:26:18 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/tableManagementService.ts | 546 ++++++++++++++++-- .../screen/InteractiveDataTable.tsx | 156 +---- .../screen/filters/AdvancedSearchFilters.tsx | 427 ++++++++++++++ .../screen/filters/ModernDatePicker.tsx | 215 +++++++ .../screen/panels/DataTableConfigPanel.tsx | 19 +- .../screen/templates/DataTableTemplate.tsx | 20 +- .../table-list/TableListComponent.tsx | 178 ++++-- .../table-list/TableListConfigPanel.tsx | 260 +++++++-- .../registry/components/table-list/index.ts | 5 +- .../registry/components/table-list/types.ts | 19 +- frontend/types/screen-legacy-backup.ts | 10 + 11 files changed, 1537 insertions(+), 318 deletions(-) create mode 100644 frontend/components/screen/filters/AdvancedSearchFilters.tsx create mode 100644 frontend/components/screen/filters/ModernDatePicker.tsx diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index daa9f526..94f8aa30 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1019,6 +1019,434 @@ export class TableManagementService { } } + /** + * 고급 검색 조건 구성 + */ + private async buildAdvancedSearchCondition( + tableName: string, + columnName: string, + value: any, + paramIndex: number + ): Promise<{ + whereClause: string; + values: any[]; + paramCount: number; + } | null> { + try { + // "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음 + if ( + value === "__ALL__" || + value === "" || + value === null || + value === undefined + ) { + return null; + } + + // 컬럼 타입 정보 조회 + const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); + + if (!columnInfo) { + // 컬럼 정보가 없으면 기본 문자열 검색 + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + + const webType = columnInfo.webType; + + // 웹타입별 검색 조건 구성 + switch (webType) { + case "date": + case "datetime": + return this.buildDateRangeCondition(columnName, value, paramIndex); + + case "number": + case "decimal": + return this.buildNumberRangeCondition(columnName, value, paramIndex); + + case "code": + return await this.buildCodeSearchCondition( + tableName, + columnName, + value, + paramIndex + ); + + case "entity": + return await this.buildEntitySearchCondition( + tableName, + columnName, + value, + paramIndex + ); + + default: + // 기본 문자열 검색 + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + } catch (error) { + logger.error( + `고급 검색 조건 구성 실패: ${tableName}.${columnName}`, + error + ); + // 오류 시 기본 검색으로 폴백 + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + } + + /** + * 날짜 범위 검색 조건 구성 + */ + private buildDateRangeCondition( + columnName: string, + value: any, + paramIndex: number + ): { + whereClause: string; + values: any[]; + paramCount: number; + } { + const conditions: string[] = []; + const values: any[] = []; + let paramCount = 0; + + if (typeof value === "object" && value !== null) { + if (value.from) { + conditions.push(`${columnName} >= $${paramIndex + paramCount}`); + values.push(value.from); + paramCount++; + } + if (value.to) { + conditions.push(`${columnName} <= $${paramIndex + paramCount}`); + values.push(value.to); + paramCount++; + } + } else if (typeof value === "string" && value.trim() !== "") { + // 단일 날짜 검색 (해당 날짜의 데이터) + conditions.push(`DATE(${columnName}) = DATE($${paramIndex})`); + values.push(value); + paramCount = 1; + } + + if (conditions.length === 0) { + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + + return { + whereClause: `(${conditions.join(" AND ")})`, + values, + paramCount, + }; + } + + /** + * 숫자 범위 검색 조건 구성 + */ + private buildNumberRangeCondition( + columnName: string, + value: any, + paramIndex: number + ): { + whereClause: string; + values: any[]; + paramCount: number; + } { + const conditions: string[] = []; + const values: any[] = []; + let paramCount = 0; + + if (typeof value === "object" && value !== null) { + if (value.min !== undefined && value.min !== null && value.min !== "") { + conditions.push( + `${columnName}::numeric >= $${paramIndex + paramCount}` + ); + values.push(parseFloat(value.min)); + paramCount++; + } + if (value.max !== undefined && value.max !== null && value.max !== "") { + conditions.push( + `${columnName}::numeric <= $${paramIndex + paramCount}` + ); + values.push(parseFloat(value.max)); + paramCount++; + } + } else if (typeof value === "string" || typeof value === "number") { + // 정확한 값 검색 + conditions.push(`${columnName}::numeric = $${paramIndex}`); + values.push(parseFloat(value.toString())); + paramCount = 1; + } + + if (conditions.length === 0) { + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + + return { + whereClause: `(${conditions.join(" AND ")})`, + values, + paramCount, + }; + } + + /** + * 코드 검색 조건 구성 + */ + private async buildCodeSearchCondition( + tableName: string, + columnName: string, + value: any, + paramIndex: number + ): Promise<{ + whereClause: string; + values: any[]; + paramCount: number; + }> { + try { + const codeTypeInfo = await this.getCodeTypeInfo(tableName, columnName); + + if (!codeTypeInfo.isCodeType || !codeTypeInfo.codeCategory) { + // 코드 타입이 아니면 기본 검색 + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + + if (typeof value === "string" && value.trim() !== "") { + // 코드값 또는 코드명으로 검색 + return { + whereClause: `( + ${columnName}::text = $${paramIndex} OR + EXISTS ( + SELECT 1 FROM code_info ci + WHERE ci.code_category = $${paramIndex + 1} + AND ci.code_value = ${columnName} + AND ci.code_name ILIKE $${paramIndex + 2} + ) + )`, + values: [value, codeTypeInfo.codeCategory, `%${value}%`], + paramCount: 3, + }; + } else { + // 정확한 코드값 매칭 + return { + whereClause: `${columnName} = $${paramIndex}`, + values: [value], + paramCount: 1, + }; + } + } catch (error) { + logger.error( + `코드 검색 조건 구성 실패: ${tableName}.${columnName}`, + error + ); + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + } + + /** + * 엔티티 검색 조건 구성 + */ + private async buildEntitySearchCondition( + tableName: string, + columnName: string, + value: any, + paramIndex: number + ): Promise<{ + whereClause: string; + values: any[]; + paramCount: number; + }> { + try { + const entityTypeInfo = await this.getEntityTypeInfo( + tableName, + columnName + ); + + if (!entityTypeInfo.isEntityType || !entityTypeInfo.referenceTable) { + // 엔티티 타입이 아니면 기본 검색 + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + + if (typeof value === "string" && value.trim() !== "") { + const displayColumn = entityTypeInfo.displayColumn || "name"; + const referenceColumn = entityTypeInfo.referenceColumn || "id"; + + // 참조 테이블의 표시 컬럼으로 검색 + return { + whereClause: `EXISTS ( + SELECT 1 FROM ${entityTypeInfo.referenceTable} ref + WHERE ref.${referenceColumn} = ${columnName} + AND ref.${displayColumn} ILIKE $${paramIndex} + )`, + values: [`%${value}%`], + paramCount: 1, + }; + } else { + // 정확한 참조값 매칭 + return { + whereClause: `${columnName} = $${paramIndex}`, + values: [value], + paramCount: 1, + }; + } + } catch (error) { + logger.error( + `엔티티 검색 조건 구성 실패: ${tableName}.${columnName}`, + error + ); + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + } + + /** + * 불린 검색 조건 구성 + */ + private buildBooleanCondition( + columnName: string, + value: any, + paramIndex: number + ): { + whereClause: string; + values: any[]; + paramCount: number; + } { + if (value === "true" || value === true) { + return { + whereClause: `${columnName} = true`, + values: [], + paramCount: 0, + }; + } else if (value === "false" || value === false) { + return { + whereClause: `${columnName} = false`, + values: [], + paramCount: 0, + }; + } else { + // 기본 검색 + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${value}%`], + paramCount: 1, + }; + } + } + + /** + * 컬럼 웹타입 정보 조회 + */ + private async getColumnWebTypeInfo( + tableName: string, + columnName: string + ): Promise<{ + webType: string; + codeCategory?: string; + referenceTable?: string; + referenceColumn?: string; + displayColumn?: string; + } | null> { + try { + const result = await prisma.column_labels.findFirst({ + where: { + table_name: tableName, + column_name: columnName, + }, + select: { + web_type: true, + code_category: true, + reference_table: true, + reference_column: true, + display_column: true, + }, + }); + + if (!result) { + return null; + } + + return { + webType: result.web_type || "", + codeCategory: result.code_category || undefined, + referenceTable: result.reference_table || undefined, + referenceColumn: result.reference_column || undefined, + displayColumn: result.display_column || undefined, + }; + } catch (error) { + logger.error( + `컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`, + error + ); + return null; + } + } + + /** + * 엔티티 타입 정보 조회 + */ + private async getEntityTypeInfo( + tableName: string, + columnName: string + ): Promise<{ + isEntityType: boolean; + referenceTable?: string; + referenceColumn?: string; + displayColumn?: string; + }> { + try { + const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); + + if (!columnInfo || columnInfo.webType !== "entity") { + return { isEntityType: false }; + } + + return { + isEntityType: true, + referenceTable: columnInfo.referenceTable, + referenceColumn: columnInfo.referenceColumn, + displayColumn: columnInfo.displayColumn, + }; + } catch (error) { + logger.error( + `엔티티 타입 정보 조회 실패: ${tableName}.${columnName}`, + error + ); + return { isEntityType: false }; + } + } + /** * 테이블 데이터 조회 (페이징 + 검색) */ @@ -1071,42 +1499,19 @@ export class TableManagementService { // 안전한 컬럼명 검증 (SQL 인젝션 방지) const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, ""); - if (typeof value === "string") { - // 🎯 코드 타입 컬럼의 경우 코드값과 코드명 모두로 검색 - const codeTypeInfo = await this.getCodeTypeInfo( - tableName, - safeColumn - ); + // 🎯 고급 필터 처리 + const condition = await this.buildAdvancedSearchCondition( + tableName, + safeColumn, + value, + paramIndex + ); - if (codeTypeInfo.isCodeType && codeTypeInfo.codeCategory) { - // 코드 타입 컬럼: 코드값 또는 코드명으로 검색 - // 1) 컬럼 값이 직접 검색어와 일치하는 경우 - // 2) 컬럼 값이 코드값이고, 해당 코드의 코드명이 검색어와 일치하는 경우 - whereConditions.push(`( - ${safeColumn}::text ILIKE $${paramIndex} OR - EXISTS ( - SELECT 1 FROM code_info ci - WHERE ci.code_category = $${paramIndex + 1} - AND ci.code_value = ${safeColumn} - AND ci.code_name ILIKE $${paramIndex + 2} - ) - )`); - searchValues.push(`%${value}%`); // 직접 값 검색용 - searchValues.push(codeTypeInfo.codeCategory); // 코드 카테고리 - searchValues.push(`%${value}%`); // 코드명 검색용 - paramIndex += 2; // 추가 파라미터로 인해 인덱스 증가 - } else { - // 일반 컬럼: 기존 방식 - whereConditions.push( - `${safeColumn}::text ILIKE $${paramIndex}` - ); - searchValues.push(`%${value}%`); - } - } else { - whereConditions.push(`${safeColumn} = $${paramIndex}`); - searchValues.push(value); + if (condition) { + whereConditions.push(condition.whereClause); + searchValues.push(...condition.values); + paramIndex += condition.paramCount; } - paramIndex++; } } } @@ -1698,7 +2103,10 @@ export class TableManagementService { const selectColumns = columns.data.map((col: any) => col.column_name); // WHERE 절 구성 - const whereClause = this.buildWhereClause(options.search); + const whereClause = await this.buildWhereClause( + tableName, + options.search + ); // ORDER BY 절 구성 const orderBy = options.sortBy @@ -2061,21 +2469,77 @@ export class TableManagementService { } /** - * WHERE 절 구성 + * WHERE 절 구성 (고급 검색 지원) */ - private buildWhereClause(search?: Record): string { + private async buildWhereClause( + tableName: string, + search?: Record + ): Promise { if (!search || Object.keys(search).length === 0) { return ""; } const conditions: string[] = []; - for (const [key, value] of Object.entries(search)) { - if (value !== undefined && value !== null && value !== "") { + for (const [columnName, value] of Object.entries(search)) { + if ( + value === undefined || + value === null || + value === "" || + value === "__ALL__" + ) { + continue; + } + + try { + // 고급 검색 조건 구성 + const searchCondition = await this.buildAdvancedSearchCondition( + tableName, + columnName, + value, + 1 // paramIndex는 실제로는 사용되지 않음 (직접 값 삽입) + ); + + if (searchCondition) { + // SQL 인젝션 방지를 위해 값을 직접 삽입하는 대신 안전한 방식 사용 + let condition = searchCondition.whereClause; + + // 파라미터를 실제 값으로 치환 (안전한 방식) + searchCondition.values.forEach((val, index) => { + const paramPlaceholder = `$${index + 1}`; + if (typeof val === "string") { + condition = condition.replace( + paramPlaceholder, + `'${val.replace(/'/g, "''")}'` + ); + } else if (typeof val === "number") { + condition = condition.replace(paramPlaceholder, val.toString()); + } else { + condition = condition.replace( + paramPlaceholder, + `'${String(val).replace(/'/g, "''")}'` + ); + } + }); + + // main. 접두사 추가 (조인 쿼리용) + condition = condition.replace( + new RegExp(`\\b${columnName}\\b`, "g"), + `main.${columnName}` + ); + conditions.push(condition); + } + } catch (error) { + logger.warn(`검색 조건 구성 실패: ${columnName}`, error); + // 폴백: 기본 문자열 검색 if (typeof value === "string") { - conditions.push(`main.${key} ILIKE '%${value}%'`); + conditions.push( + `main.${columnName}::text ILIKE '%${value.replace(/'/g, "''")}%'` + ); } else { - conditions.push(`main.${key} = '${value}'`); + conditions.push( + `main.${columnName} = '${String(value).replace(/'/g, "''")}'` + ); } } } diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 41b88030..f95dc025 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -41,11 +41,12 @@ import { import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; import { getCurrentUser, UserInfo } from "@/lib/api/client"; -import { DataTableComponent, DataTableColumn, DataTableFilter, AttachedFileInfo } from "@/types/screen"; +import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { cn } from "@/lib/utils"; import { downloadFile, getLinkedFiles, getFilePreviewUrl, getDirectFileUrl } from "@/lib/api/file"; import { toast } from "sonner"; import { FileUpload } from "@/components/screen/widgets/FileUpload"; +import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters"; // 파일 데이터 타입 정의 (AttachedFileInfo와 호환) interface FileInfo { @@ -332,7 +333,6 @@ export const InteractiveDataTable: React.FC = ({ // 검색 가능한 컬럼만 필터링 const visibleColumns = component.columns?.filter((col: DataTableColumn) => col.visible) || []; - const searchFilters = component.filters || []; // 컬럼의 실제 웹 타입 정보 찾기 const getColumnWebType = useCallback( @@ -525,6 +525,11 @@ export const InteractiveDataTable: React.FC = ({ } }, [component.tableName]); + // 실제 사용할 필터 (설정된 필터만 사용, 자동 생성 안함) + const searchFilters = useMemo(() => { + return component.filters || []; + }, [component.filters]); + // 초기 데이터 로드 useEffect(() => { loadData(1, searchValues); @@ -1480,115 +1485,7 @@ export const InteractiveDataTable: React.FC = ({ } }; - // 검색 필터 렌더링 - const renderSearchFilter = (filter: DataTableFilter) => { - const value = searchValues[filter.columnName] || ""; - - switch (filter.widgetType) { - case "text": - case "email": - case "tel": - return ( - handleSearchValueChange(filter.columnName, e.target.value)} - onKeyPress={(e) => { - if (e.key === "Enter") { - handleSearch(); - } - }} - /> - ); - - case "number": - case "decimal": - return ( - handleSearchValueChange(filter.columnName, e.target.value)} - onKeyPress={(e) => { - if (e.key === "Enter") { - handleSearch(); - } - }} - /> - ); - - case "date": - return ( - handleSearchValueChange(filter.columnName, e.target.value)} - /> - ); - - case "datetime": - return ( - handleSearchValueChange(filter.columnName, e.target.value)} - /> - ); - - case "select": - // TODO: 선택 옵션은 추후 구현 - return ( - - ); - - case "code": - // 코드 타입은 텍스트 검색으로 처리 (코드명으로 검색 가능) - return ( - handleSearchValueChange(filter.columnName, e.target.value)} - onKeyPress={(e) => { - if (e.key === "Enter") { - handleSearch(); - } - }} - /> - ); - - default: - return ( - handleSearchValueChange(filter.columnName, e.target.value)} - onKeyPress={(e) => { - if (e.key === "Enter") { - handleSearch(); - } - }} - /> - ); - } - }; + // 기존 renderSearchFilter 함수는 AdvancedSearchFilters 컴포넌트로 대체됨 // 파일 다운로드 const handleDownloadFile = useCallback(async (fileInfo: FileInfo) => { @@ -1847,31 +1744,22 @@ export const InteractiveDataTable: React.FC = ({ - {/* 검색 필터 */} - {searchFilters.length > 0 && ( + {/* 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} + {tableColumns && tableColumns.length > 0 && ( <> -
-
- - 검색 필터 -
-
`${filter.gridColumns || 3}fr`) - .join(" "), - }} - > - {searchFilters.map((filter: DataTableFilter) => ( -
- - {renderSearchFilter(filter)} -
- ))} -
-
+ 0 ? searchFilters : []} + searchValues={searchValues} + onSearchValueChange={handleSearchValueChange} + onSearch={handleSearch} + onClearFilters={() => { + setSearchValues({}); + loadData(1, {}); + }} + tableColumns={tableColumns} + tableName={component.tableName} + /> )} diff --git a/frontend/components/screen/filters/AdvancedSearchFilters.tsx b/frontend/components/screen/filters/AdvancedSearchFilters.tsx new file mode 100644 index 00000000..a509cd5d --- /dev/null +++ b/frontend/components/screen/filters/AdvancedSearchFilters.tsx @@ -0,0 +1,427 @@ +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Search, X } from "lucide-react"; +import { ModernDatePicker } from "./ModernDatePicker"; +import { cn } from "@/lib/utils"; +import { commonCodeApi } from "@/lib/api/commonCode"; +import { EntityReferenceAPI } from "@/lib/api/entityReference"; +import type { DataTableFilter } from "@/types/screen-legacy-backup"; +import type { CodeInfo } from "@/types/commonCode"; + +interface AdvancedSearchFiltersProps { + filters: DataTableFilter[]; + searchValues: Record; + onSearchValueChange: (columnName: string, value: any) => void; + onSearch: () => void; + onClearFilters: () => void; + className?: string; + tableColumns?: any[]; // 테이블 컬럼 정보 + tableName?: string; // 테이블명 +} + +interface DateRangeValue { + from?: Date; + to?: Date; +} + +interface CodeOption { + value: string; + label: string; +} + +interface EntityOption { + value: string; + label: string; +} + +export const AdvancedSearchFilters: React.FC = ({ + filters, + searchValues, + onSearchValueChange, + onSearch, + onClearFilters, + className = "", + tableColumns = [], + tableName = "", +}) => { + // 코드 옵션 캐시 + const [codeOptions, setCodeOptions] = useState>({}); + // 엔티티 옵션 캐시 + const [entityOptions, setEntityOptions] = useState>({}); + // 로딩 상태 + const [loadingStates, setLoadingStates] = useState>({}); + + // 자동 필터 생성 (설정된 필터가 없을 때 테이블 컬럼 기반으로 생성) + const autoGeneratedFilters = useMemo(() => { + if (filters.length > 0 || !tableColumns || tableColumns.length === 0) { + return []; + } + + // 필터 가능한 웹타입들 + const filterableWebTypes = ["text", "email", "tel", "number", "decimal", "date", "datetime", "code", "entity"]; + + return tableColumns + .filter((col) => { + const webType = col.webType || col.web_type; + return filterableWebTypes.includes(webType) && col.isVisible !== false; + }) + .slice(0, 6) // 최대 6개까지만 자동 생성 + .map((col) => ({ + columnName: col.columnName || col.column_name, + widgetType: col.webType || col.web_type, + label: col.displayName || col.column_label || col.columnName || col.column_name, + gridColumns: 3, + codeCategory: col.codeCategory || col.code_category, + referenceTable: col.referenceTable || col.reference_table, + referenceColumn: col.referenceColumn || col.reference_column, + displayColumn: col.displayColumn || col.display_column, + })); + }, [filters, tableColumns]); + + // 실제 사용할 필터 (설정된 필터가 있으면 우선, 없으면 자동 생성) + const effectiveFilters = useMemo(() => { + return filters.length > 0 ? filters : autoGeneratedFilters; + }, [filters, autoGeneratedFilters]); + + // 코드 데이터 로드 + const loadCodeOptions = useCallback( + async (codeCategory: string) => { + if (codeOptions[codeCategory] || loadingStates[codeCategory]) return; + + setLoadingStates((prev) => ({ ...prev, [codeCategory]: true })); + + try { + const response = await EntityReferenceAPI.getCodeReferenceData(codeCategory, { limit: 1000 }); + const options = response.options.map((option) => ({ + value: option.value, + label: option.label, + })); + + setCodeOptions((prev) => ({ ...prev, [codeCategory]: options })); + } catch (error) { + console.error(`코드 카테고리 ${codeCategory} 로드 실패:`, error); + setCodeOptions((prev) => ({ ...prev, [codeCategory]: [] })); + } finally { + setLoadingStates((prev) => ({ ...prev, [codeCategory]: false })); + } + }, + [codeOptions, loadingStates], + ); + + // 엔티티 데이터 로드 + const loadEntityOptions = useCallback( + async (tableName: string, columnName: string) => { + const key = `${tableName}.${columnName}`; + if (entityOptions[key] || loadingStates[key]) return; + + setLoadingStates((prev) => ({ ...prev, [key]: true })); + + try { + const response = await EntityReferenceAPI.getEntityReferenceData(tableName, columnName, { limit: 1000 }); + const options = response.options.map((option) => ({ + value: option.value, + label: option.label, + })); + + setEntityOptions((prev) => ({ ...prev, [key]: options })); + } catch (error) { + console.error(`엔티티 ${tableName}.${columnName} 로드 실패:`, error); + setEntityOptions((prev) => ({ ...prev, [key]: [] })); + } finally { + setLoadingStates((prev) => ({ ...prev, [key]: false })); + } + }, + [entityOptions, loadingStates], + ); + + // 즉시 검색을 위한 onChange 핸들러 + const handleChange = useCallback( + (columnName: string, newValue: any) => { + onSearchValueChange(columnName, newValue); + // 즉시 검색 실행 (디바운싱 제거) + onSearch(); + }, + [onSearchValueChange, onSearch], + ); + + // 텍스트 입력용 핸들러 (Enter 키 또는 blur 시에만 검색) + const handleTextChange = useCallback( + (columnName: string, newValue: string) => { + onSearchValueChange(columnName, newValue); + // 텍스트는 즉시 검색하지 않고 상태만 업데이트 + }, + [onSearchValueChange], + ); + + // Enter 키 또는 blur 시 검색 실행 + const handleTextSearch = useCallback(() => { + onSearch(); + }, [onSearch]); + + // 필터별 렌더링 함수 + const renderFilter = (filter: DataTableFilter) => { + const value = searchValues[filter.columnName] || ""; + + switch (filter.widgetType) { + case "text": + case "email": + case "tel": + return ( + handleTextChange(filter.columnName, e.target.value)} + onKeyPress={(e) => { + if (e.key === "Enter") { + handleTextSearch(); + } + }} + onBlur={handleTextSearch} + /> + ); + + case "number": + case "decimal": + // 숫자 필터 모드에 따라 다른 UI 렌더링 + if (filter.numberFilterMode === "exact") { + // 정확한 값 검색 + return ( + handleChange(filter.columnName, e.target.value)} + /> + ); + } else { + // 범위 검색 (기본값) + return ( +
+ + handleChange(filter.columnName, { + ...value, + min: e.target.value, + }) + } + /> + + handleChange(filter.columnName, { + ...value, + max: e.target.value, + }) + } + /> +
+ ); + } + + case "date": + case "datetime": + return ( + handleChange(filter.columnName, newValue)} + includeTime={filter.widgetType === "datetime"} + /> + ); + + case "code": + console.log("🔍 코드 필터 렌더링:", { + columnName: filter.columnName, + codeCategory: filter.codeCategory, + options: codeOptions[filter.codeCategory || ""], + loading: loadingStates[filter.codeCategory || ""], + }); + return ( + handleChange(filter.columnName, newValue)} + options={codeOptions[filter.codeCategory || ""] || []} + loading={loadingStates[filter.codeCategory || ""]} + onLoadOptions={() => filter.codeCategory && loadCodeOptions(filter.codeCategory)} + /> + ); + + case "entity": + return ( + handleChange(filter.columnName, newValue)} + options={entityOptions[`${filter.referenceTable}.${filter.columnName}`] || []} + loading={loadingStates[`${filter.referenceTable}.${filter.columnName}`]} + onLoadOptions={() => filter.referenceTable && loadEntityOptions(filter.referenceTable, filter.columnName)} + /> + ); + + case "select": + case "dropdown": + return ( + + ); + + default: + return ( + handleTextChange(filter.columnName, e.target.value)} + onKeyPress={(e) => { + if (e.key === "Enter") { + handleTextSearch(); + } + }} + onBlur={handleTextSearch} + /> + ); + } + }; + + // 활성 필터 개수 계산 + const activeFiltersCount = Object.values(searchValues).filter((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 !== ""; + }).length; + + return ( +
+ {/* 필터 헤더 */} +
+ + 검색 필터 + {autoGeneratedFilters.length > 0 && (자동 생성)} +
+ + {/* 필터 그리드 - 적절한 너비로 조정 */} + {effectiveFilters.length > 0 && ( +
+ {effectiveFilters.map((filter: DataTableFilter) => { + // 필터 개수에 따라 적절한 너비 계산 + const getFilterWidth = () => { + const filterCount = effectiveFilters.length; + if (filterCount === 1) return "w-80"; // 1개일 때는 적당한 크기 + if (filterCount === 2) return "w-64"; // 2개일 때는 조금 작게 + if (filterCount === 3) return "w-52"; // 3개일 때는 더 작게 + return "w-48"; // 4개 이상일 때는 가장 작게 + }; + + return ( +
+ + {renderFilter(filter)} +
+ ); + })} +
+ )} + + {/* 필터 상태 및 초기화 버튼 */} + {activeFiltersCount > 0 && ( +
+
{activeFiltersCount}개 필터 적용 중
+ +
+ )} +
+ ); +}; + +// 코드 필터 컴포넌트 +const CodeFilter: React.FC<{ + filter: DataTableFilter; + value: string; + onChange: (value: string) => void; + options: CodeOption[]; + loading: boolean; + onLoadOptions: () => void; +}> = ({ filter, value, onChange, options, loading, onLoadOptions }) => { + useEffect(() => { + if (filter.codeCategory && options.length === 0 && !loading) { + onLoadOptions(); + } + }, [filter.codeCategory, options.length, loading, onLoadOptions]); + + return ( + + ); +}; + +// 엔티티 필터 컴포넌트 +const EntityFilter: React.FC<{ + filter: DataTableFilter; + value: string; + onChange: (value: string) => void; + options: EntityOption[]; + loading: boolean; + onLoadOptions: () => void; +}> = ({ filter, value, onChange, options, loading, onLoadOptions }) => { + useEffect(() => { + if (filter.referenceTable && options.length === 0 && !loading) { + onLoadOptions(); + } + }, [filter.referenceTable, options.length, loading, onLoadOptions]); + + return ( + + ); +}; diff --git a/frontend/components/screen/filters/ModernDatePicker.tsx b/frontend/components/screen/filters/ModernDatePicker.tsx new file mode 100644 index 00000000..55a9c64f --- /dev/null +++ b/frontend/components/screen/filters/ModernDatePicker.tsx @@ -0,0 +1,215 @@ +"use client"; + +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react"; +import { + format, + addMonths, + subMonths, + startOfMonth, + endOfMonth, + eachDayOfInterval, + isSameMonth, + isSameDay, + isToday, +} from "date-fns"; +import { ko } from "date-fns/locale"; +import { cn } from "@/lib/utils"; + +interface DateRangeValue { + from?: Date; + to?: Date; +} + +interface ModernDatePickerProps { + label: string; + value: DateRangeValue; + onChange: (value: DateRangeValue) => void; + includeTime?: boolean; +} + +export const ModernDatePicker: React.FC = ({ label, value, onChange, includeTime = false }) => { + const [isOpen, setIsOpen] = useState(false); + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [selectingType, setSelectingType] = useState<"from" | "to">("from"); + + const formatDate = (date: Date | undefined) => { + if (!date) return ""; + if (includeTime) { + return format(date, "yyyy-MM-dd HH:mm", { locale: ko }); + } + return format(date, "yyyy-MM-dd", { locale: ko }); + }; + + const displayValue = () => { + if (value?.from && value?.to) { + return `${formatDate(value.from)} ~ ${formatDate(value.to)}`; + } + if (value?.from) { + return `${formatDate(value.from)} ~`; + } + if (value?.to) { + return `~ ${formatDate(value.to)}`; + } + return ""; + }; + + const handleDateClick = (date: Date) => { + if (selectingType === "from") { + const newValue = { ...value, from: date }; + onChange(newValue); + setSelectingType("to"); + } else { + const newValue = { ...value, to: date }; + onChange(newValue); + setSelectingType("from"); + } + }; + + const handleClear = () => { + onChange({}); + setSelectingType("from"); + }; + + const handleConfirm = () => { + setIsOpen(false); + setSelectingType("from"); + // 날짜는 이미 선택 시점에 onChange가 호출되므로 중복 호출 제거 + }; + + const monthStart = startOfMonth(currentMonth); + const monthEnd = endOfMonth(currentMonth); + const days = eachDayOfInterval({ start: monthStart, end: monthEnd }); + + // 달력 시작을 월요일로 맞추기 위해 앞의 빈 칸들 계산 + const startDate = new Date(monthStart); + const dayOfWeek = startDate.getDay(); + const paddingDays = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 일요일(0)이면 6개, 나머지는 -1 + + const allDays = [...Array(paddingDays).fill(null), ...days]; + + const isInRange = (date: Date) => { + if (!value.from || !value.to) return false; + return date >= value.from && date <= value.to; + }; + + const isRangeStart = (date: Date) => { + return value.from && isSameDay(date, value.from); + }; + + const isRangeEnd = (date: Date) => { + return value.to && isSameDay(date, value.to); + }; + + return ( + + + + + +
+ {/* 헤더 */} +
+

기간 선택

+
+ {selectingType === "from" ? "시작일을 선택하세요" : "종료일을 선택하세요"} +
+
+ + {/* 월 네비게이션 */} +
+ +
{format(currentMonth, "yyyy년 MM월", { locale: ko })}
+ +
+ + {/* 요일 헤더 */} +
+ {["월", "화", "수", "목", "금", "토", "일"].map((day) => ( +
+ {day} +
+ ))} +
+ + {/* 날짜 그리드 */} +
+ {allDays.map((date, index) => { + if (!date) { + return
; + } + + const isCurrentMonth = isSameMonth(date, currentMonth); + const isSelected = isRangeStart(date) || isRangeEnd(date); + const isInRangeDate = isInRange(date); + const isTodayDate = isToday(date); + + return ( + + ); + })} +
+ + {/* 선택된 범위 표시 */} + {(value.from || value.to) && ( +
+
선택된 기간
+
+ {value.from && 시작: {formatDate(value.from)}} + {value.from && value.to && ~} + {value.to && 종료: {formatDate(value.to)}} +
+
+ )} + + {/* 액션 버튼 */} +
+ +
+ + +
+
+
+ + + ); +}; diff --git a/frontend/components/screen/panels/DataTableConfigPanel.tsx b/frontend/components/screen/panels/DataTableConfigPanel.tsx index 2d9d58a2..f408b009 100644 --- a/frontend/components/screen/panels/DataTableConfigPanel.tsx +++ b/frontend/components/screen/panels/DataTableConfigPanel.tsx @@ -320,18 +320,8 @@ const DataTableConfigPanelComponent: React.FC = ({ columnsCount: table.columns.length, }); - // 테이블의 모든 컬럼을 기본 설정으로 추가 - const defaultColumns: DataTableColumn[] = table.columns.map((col, index) => ({ - id: generateComponentId(), - columnName: col.columnName, - label: col.columnLabel || col.columnName, - widgetType: getWidgetTypeFromColumn(col), - gridColumns: 2, // 기본 2칸 - visible: index < 6, // 처음 6개만 기본으로 표시 - filterable: isFilterableWebType(getWidgetTypeFromColumn(col)), - sortable: true, - searchable: ["text", "email", "tel"].includes(getWidgetTypeFromColumn(col)), - })); + // 테이블 변경 시 컬럼을 자동으로 추가하지 않음 (사용자가 수동으로 추가해야 함) + const defaultColumns: DataTableColumn[] = []; console.log("✅ 생성된 컬럼 설정:", { defaultColumnsCount: defaultColumns.length, @@ -785,6 +775,11 @@ const DataTableConfigPanelComponent: React.FC = ({ widgetType, label: targetColumn.columnLabel || targetColumn.columnName, gridColumns: 3, + // 웹타입별 추가 정보 설정 + codeCategory: targetColumn.codeCategory, + referenceTable: targetColumn.referenceTable, + referenceColumn: targetColumn.referenceColumn, + displayColumn: targetColumn.displayColumn, }; console.log("➕ 필터 추가 시작:", { diff --git a/frontend/components/screen/templates/DataTableTemplate.tsx b/frontend/components/screen/templates/DataTableTemplate.tsx index 041ec848..86422e7f 100644 --- a/frontend/components/screen/templates/DataTableTemplate.tsx +++ b/frontend/components/screen/templates/DataTableTemplate.tsx @@ -121,25 +121,9 @@ export const DataTableTemplate: React.FC = ({ className = "", isPreview = true, }) => { - // 미리보기용 기본 컬럼 데이터 + // 설정된 컬럼만 사용 (자동 생성 안함) const defaultColumns = React.useMemo(() => { - if (columns.length > 0) return columns; - - return [ - { id: "id", label: "ID", type: "number", visible: true, sortable: true, filterable: false, width: 80 }, - { id: "name", label: "이름", type: "text", visible: true, sortable: true, filterable: true, width: 150 }, - { id: "email", label: "이메일", type: "email", visible: true, sortable: true, filterable: true, width: 200 }, - { id: "status", label: "상태", type: "select", visible: true, sortable: true, filterable: true, width: 100 }, - { - id: "created_date", - label: "생성일", - type: "date", - visible: true, - sortable: true, - filterable: true, - width: 120, - }, - ]; + return columns || []; }, [columns]); // 미리보기용 샘플 데이터 diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 6bf59d30..785d089c 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -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 = ({ const [columnLabels, setColumnLabels] = useState>({}); const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 - const [selectedSearchColumn, setSelectedSearchColumn] = useState(""); // 선택된 검색 컬럼 const [displayColumns, setDisplayColumns] = useState([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨) const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) + // 고급 필터 관련 state + const [searchValues, setSearchValues] = useState>({}); + // 체크박스 상태 관리 const [selectedRows, setSelectedRows] = useState>(new Set()); // 선택된 행들의 키 집합 const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태 @@ -234,56 +236,66 @@ export const TableListComponent: React.FC = ({ 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 = ({ } }; - // 검색 - 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 = ({ 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 = ({ .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 = ({
)} - {/* 검색 */} - {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && ( + {/* 검색 - 기존 방식은 주석처리 */} + {/* {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && (
@@ -831,7 +865,6 @@ export const TableListComponent: React.FC = ({ className="w-64 pl-8" />
- {/* 검색 컬럼 선택 드롭다운 */} {tableConfig.filter?.showColumnSelector && ( )}
- )} + )} */} {/* 새로고침 */} + ))} + + )} + + {/* 설정된 필터 목록 */} + {config.filter?.filters && config.filter.filters.length > 0 && ( +
+

설정된 필터

+ {config.filter.filters.map((filter, index) => ( +
+
+
+ {filter.widgetType} + {filter.label} +
+ +
+ +
+
+ + updateFilter(index, "label", e.target.value)} + placeholder="필터 라벨" + /> +
+
+ + +
+ + {/* 숫자 타입인 경우 검색 모드 선택 */} + {(filter.widgetType === "number" || filter.widgetType === "decimal") && ( +
+ + +
+ )} + + {/* 코드 타입인 경우 코드 카테고리 */} + {filter.widgetType === "code" && ( +
+ + updateFilter(index, "codeCategory", e.target.value)} + placeholder="코드 카테고리" + /> +
+ )} +
+
+ ))} +
+ )} + + + )} diff --git a/frontend/lib/registry/components/table-list/index.ts b/frontend/lib/registry/components/table-list/index.ts index fbec7fc6..fe3eb7ff 100644 --- a/frontend/lib/registry/components/table-list/index.ts +++ b/frontend/lib/registry/components/table-list/index.ts @@ -59,10 +59,7 @@ export const TableListDefinition = createComponentDefinition({ // 필터 설정 filter: { enabled: true, - quickSearch: true, - showColumnSelector: true, // 검색컬럼 선택기 표시 기본값 - advancedFilter: false, - filterableColumns: [], + filters: [], // 사용자가 설정할 필터 목록 }, // 액션 설정 diff --git a/frontend/lib/registry/components/table-list/types.ts b/frontend/lib/registry/components/table-list/types.ts index 2d6d9aee..9ed2fd29 100644 --- a/frontend/lib/registry/components/table-list/types.ts +++ b/frontend/lib/registry/components/table-list/types.ts @@ -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; }>; } diff --git a/frontend/types/screen-legacy-backup.ts b/frontend/types/screen-legacy-backup.ts index c27fce44..4bde00b6 100644 --- a/frontend/types/screen-legacy-backup.ts +++ b/frontend/types/screen-legacy-backup.ts @@ -432,6 +432,9 @@ export interface DataTableColumn { }; } +// 숫자 필터 검색 모드 +export type NumberFilterMode = "exact" | "range"; + // 데이터 테이블 필터 설정 export interface DataTableFilter { columnName: string; @@ -439,6 +442,13 @@ export interface DataTableFilter { label: string; gridColumns: number; // 필터에서 차지할 컬럼 수 webTypeConfig?: WebTypeConfig; + + // 고급 필터를 위한 추가 속성들 + codeCategory?: string; // 코드 타입용 카테고리 + referenceTable?: string; // 엔티티 타입용 참조 테이블 + referenceColumn?: string; // 엔티티 타입용 참조 컬럼 + displayColumn?: string; // 엔티티 타입용 표시 컬럼 + numberFilterMode?: NumberFilterMode; // 숫자 필터 모드 (exact: 정확한 값, range: 범위) } // 데이터 테이블 페이지네이션 설정