summaryrefslogtreecommitdiff
path: root/components/client-data-table/table-filters.ts
blob: 443919992ed7ddf7533bf433402f185fb64fbd91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
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 <TData>(row: Row<TData>, 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 = <TData>(
  row: Row<TData>,
  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 = <TData>(
  row: Row<TData>,
  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
}