summaryrefslogtreecommitdiff
path: root/components/client-data-table
diff options
context:
space:
mode:
Diffstat (limited to 'components/client-data-table')
-rw-r--r--components/client-data-table/data-table-filter-list.tsx23
-rw-r--r--components/client-data-table/table-filters.ts344
2 files changed, 351 insertions, 16 deletions
diff --git a/components/client-data-table/data-table-filter-list.tsx b/components/client-data-table/data-table-filter-list.tsx
index f06d837e..6a9da8ba 100644
--- a/components/client-data-table/data-table-filter-list.tsx
+++ b/components/client-data-table/data-table-filter-list.tsx
@@ -84,25 +84,16 @@ export function ClientDataTableAdvancedFilter<TData>({
// 3) Sync to table
React.useEffect(() => {
const newColumnFilters = filters.map((f) => {
- // If it's numeric, transform f.value from string → number
- if (f.type === "number") {
- return {
- id: String(f.id),
- value: {
- operator: f.operator,
- inputValue: parseFloat(String(f.value)),
- }
- }
- }
- else {
- // For text, date, boolean, etc., it's fine to keep value as a string or whatever
- return {
- id: String(f.id),
- value: f.value,
+ // 모든 타입에 대해 operator와 value를 함께 전달
+ return {
+ id: String(f.id),
+ value: {
+ operator: f.operator,
+ value: f.type === "number" ? parseFloat(String(f.value)) : f.value,
}
}
})
-
+
table.setColumnFilters(newColumnFilters)
}, [filters, joinOperator, table])
diff --git a/components/client-data-table/table-filters.ts b/components/client-data-table/table-filters.ts
new file mode 100644
index 00000000..44391999
--- /dev/null
+++ b/components/client-data-table/table-filters.ts
@@ -0,0 +1,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
+} \ No newline at end of file