import { Row } from "@tanstack/react-table" export type FilterOperator = | "iLike" | "notILike" | "eq" | "ne" | "isEmpty" | "isNotEmpty" | "lt" | "lte" | "gt" | "gte" | "isBetween" | "isRelativeToToday" export type ColumnType = "text" | "number" | "date" | "boolean" | "select" | "multi-select" interface FilterValue { operator: FilterOperator value: any } /** * 글로벌 필터 함수 생성 * @param type - 컬럼 타입 * @returns 필터 함수 */ export const createFilterFn = (type: ColumnType) => { return (row: Row, columnId: string, filterValue: FilterValue) => { const cellValue = row.getValue(columnId) const { operator, value } = filterValue // 공통 처리: isEmpty/isNotEmpty if (operator === "isEmpty") { if (type === "multi-select") { return !cellValue || (Array.isArray(cellValue) && cellValue.length === 0) } return cellValue == null || cellValue === "" || cellValue === undefined } if (operator === "isNotEmpty") { if (type === "multi-select") { return cellValue != null && Array.isArray(cellValue) && cellValue.length > 0 } return cellValue != null && cellValue !== "" && cellValue !== undefined } // value가 없고 isEmpty/isNotEmpty가 아닌 경우 if ((value === "" || value == null) && operator !== "isEmpty" && operator !== "isNotEmpty") { return true } // 타입별 처리 switch (type) { case "text": { const cellStr = String(cellValue || "").toLowerCase() const filterStr = String(value || "").toLowerCase() switch (operator) { case "iLike": return cellStr.includes(filterStr) case "notILike": return !cellStr.includes(filterStr) case "eq": return cellStr === filterStr case "ne": return cellStr !== filterStr default: return true } } case "number": { const cellNum = cellValue != null ? Number(cellValue) : null const filterNum = value != null ? Number(value) : null if (cellNum == null || filterNum == null) { return false } switch (operator) { case "eq": return cellNum === filterNum case "ne": return cellNum !== filterNum case "lt": return cellNum < filterNum case "lte": return cellNum <= filterNum case "gt": return cellNum > filterNum case "gte": return cellNum >= filterNum default: return true } } case "date": { const cellDate = cellValue ? new Date(cellValue as string | Date) : null if (!cellDate || isNaN(cellDate.getTime())) { return false } switch (operator) { case "eq": { if (!value) return false const filterDate = new Date(value) return cellDate.toDateString() === filterDate.toDateString() } case "ne": { if (!value) return true const filterDate = new Date(value) return cellDate.toDateString() !== filterDate.toDateString() } case "lt": { if (!value) return false const filterDate = new Date(value) return cellDate < filterDate } case "lte": { if (!value) return false const filterDate = new Date(value) filterDate.setHours(23, 59, 59, 999) // 그 날의 끝까지 포함 return cellDate <= filterDate } case "gt": { if (!value) return false const filterDate = new Date(value) return cellDate > filterDate } case "gte": { if (!value) return false const filterDate = new Date(value) filterDate.setHours(0, 0, 0, 0) // 그 날의 시작부터 포함 return cellDate >= filterDate } case "isBetween": { if (!Array.isArray(value) || value.length !== 2) return false const [startDate, endDate] = value if (!startDate || !endDate) return false const start = new Date(startDate) const end = new Date(endDate) start.setHours(0, 0, 0, 0) end.setHours(23, 59, 59, 999) return cellDate >= start && cellDate <= end } case "isRelativeToToday": { const today = new Date() today.setHours(0, 0, 0, 0) const tomorrow = new Date(today) tomorrow.setDate(tomorrow.getDate() + 1) // value는 상대적 날짜 지정자 (예: "today", "yesterday", "thisWeek", "lastWeek", "thisMonth", "lastMonth") switch (value) { case "today": return cellDate >= today && cellDate < tomorrow case "yesterday": { const yesterday = new Date(today) yesterday.setDate(yesterday.getDate() - 1) return cellDate >= yesterday && cellDate < today } case "tomorrow": { const dayAfterTomorrow = new Date(tomorrow) dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 1) return cellDate >= tomorrow && cellDate < dayAfterTomorrow } case "thisWeek": { const startOfWeek = new Date(today) startOfWeek.setDate(today.getDate() - today.getDay()) // 일요일부터 시작 const endOfWeek = new Date(startOfWeek) endOfWeek.setDate(startOfWeek.getDate() + 6) endOfWeek.setHours(23, 59, 59, 999) return cellDate >= startOfWeek && cellDate <= endOfWeek } case "lastWeek": { const startOfLastWeek = new Date(today) startOfLastWeek.setDate(today.getDate() - today.getDay() - 7) const endOfLastWeek = new Date(startOfLastWeek) endOfLastWeek.setDate(startOfLastWeek.getDate() + 6) endOfLastWeek.setHours(23, 59, 59, 999) return cellDate >= startOfLastWeek && cellDate <= endOfLastWeek } case "thisMonth": { const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1) const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0) endOfMonth.setHours(23, 59, 59, 999) return cellDate >= startOfMonth && cellDate <= endOfMonth } case "lastMonth": { const startOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1) const endOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0) endOfLastMonth.setHours(23, 59, 59, 999) return cellDate >= startOfLastMonth && cellDate <= endOfLastMonth } case "thisYear": { const startOfYear = new Date(today.getFullYear(), 0, 1) const endOfYear = new Date(today.getFullYear(), 11, 31) endOfYear.setHours(23, 59, 59, 999) return cellDate >= startOfYear && cellDate <= endOfYear } case "lastYear": { const startOfLastYear = new Date(today.getFullYear() - 1, 0, 1) const endOfLastYear = new Date(today.getFullYear() - 1, 11, 31) endOfLastYear.setHours(23, 59, 59, 999) return cellDate >= startOfLastYear && cellDate <= endOfLastYear } default: // 숫자가 오면 일 단위 상대 날짜로 처리 (예: "7" = 7일 이내, "-7" = 7일 전) const days = parseInt(value) if (!isNaN(days)) { if (days > 0) { // 미래 n일 이내 const futureDate = new Date(today) futureDate.setDate(futureDate.getDate() + days) return cellDate >= today && cellDate <= futureDate } else if (days < 0) { // 과거 n일 이내 const pastDate = new Date(today) pastDate.setDate(pastDate.getDate() + days) return cellDate >= pastDate && cellDate <= today } } return true } } default: return true } } case "boolean": { const cellBool = cellValue === true || cellValue === "true" || cellValue === 1 const filterBool = value === true || value === "true" switch (operator) { case "eq": return cellBool === filterBool case "ne": return cellBool !== filterBool default: return true } } case "select": { const cellStr = String(cellValue || "") const filterStr = String(value || "") switch (operator) { case "eq": return cellStr === filterStr case "ne": return cellStr !== filterStr default: return true } } case "multi-select": { const cellArray = Array.isArray(cellValue) ? cellValue : [] const filterArray = Array.isArray(value) ? value : [] switch (operator) { case "eq": // 선택된 모든 값들이 포함되어 있는지 확인 return filterArray.every(v => cellArray.includes(v)) case "ne": // 선택된 값들 중 하나라도 포함되어 있지 않은지 확인 return !filterArray.some(v => cellArray.includes(v)) default: return true } } default: return true } } } /** * AND/OR 조건으로 여러 필터 결합 * @param filters - 필터 배열 * @param joinOperator - 결합 연산자 ("and" | "or") */ export const combineFilters = ( row: Row, filters: Array<{ columnId: string filterValue: FilterValue type: ColumnType }>, joinOperator: "and" | "or" = "and" ): boolean => { if (filters.length === 0) return true if (joinOperator === "and") { return filters.every(filter => { const filterFn = createFilterFn(filter.type) return filterFn(row, filter.columnId, filter.filterValue) }) } else { return filters.some(filter => { const filterFn = createFilterFn(filter.type) return filterFn(row, filter.columnId, filter.filterValue) }) } } /** * 테이블 전체에 대한 커스텀 필터 함수 * ClientDataTableAdvancedFilter와 함께 사용 */ export const globalFilterFn = ( row: Row, columnId: string, filterValue: any ): boolean => { // filterValue가 객체 형태로 전달되는 경우를 처리 if (filterValue && typeof filterValue === 'object' && 'filters' in filterValue) { const { filters, joinOperator } = filterValue return combineFilters(row, filters, joinOperator) } // 단일 필터의 경우 if (filterValue && typeof filterValue === 'object' && 'operator' in filterValue) { // 컬럼 타입을 추론하거나 전달받아야 함 // 기본적으로 text로 처리 const filterFn = createFilterFn("text") return filterFn(row, columnId, filterValue) } return true }