kokotable 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +320 -0
- package/contract/table-request.schema.json +424 -0
- package/dist/core/src/adapters/sources.d.ts +59 -0
- package/dist/core/src/adapters/sources.js +235 -0
- package/dist/core/src/client/aggregate.d.ts +94 -0
- package/dist/core/src/client/aggregate.js +222 -0
- package/dist/core/src/client/groupRows.d.ts +40 -0
- package/dist/core/src/client/groupRows.js +145 -0
- package/dist/core/src/client/runClientQuery.d.ts +67 -0
- package/dist/core/src/client/runClientQuery.js +157 -0
- package/dist/core/src/columns/ColumnBuilder.d.ts +121 -0
- package/dist/core/src/columns/ColumnBuilder.js +273 -0
- package/dist/core/src/columns/cascade.d.ts +24 -0
- package/dist/core/src/columns/cascade.js +93 -0
- package/dist/core/src/columns/deriveOperators.d.ts +38 -0
- package/dist/core/src/columns/deriveOperators.js +94 -0
- package/dist/core/src/columns/expandSorting.d.ts +19 -0
- package/dist/core/src/columns/expandSorting.js +52 -0
- package/dist/core/src/columns/helper.d.ts +196 -0
- package/dist/core/src/columns/helper.js +250 -0
- package/dist/core/src/columns/labels.d.ts +12 -0
- package/dist/core/src/columns/labels.js +21 -0
- package/dist/core/src/contract.d.ts +12 -0
- package/dist/core/src/contract.js +12 -0
- package/dist/core/src/contracts/aiColumnMeta.d.ts +102 -0
- package/dist/core/src/contracts/aiColumnMeta.js +197 -0
- package/dist/core/src/contracts/describeColumns.d.ts +48 -0
- package/dist/core/src/contracts/describeColumns.js +40 -0
- package/dist/core/src/contracts/exportData.d.ts +150 -0
- package/dist/core/src/contracts/exportData.js +228 -0
- package/dist/core/src/contracts/index.d.ts +6 -0
- package/dist/core/src/contracts/index.js +12 -0
- package/dist/core/src/contracts/legacyUrl.d.ts +47 -0
- package/dist/core/src/contracts/legacyUrl.js +156 -0
- package/dist/core/src/contracts/preset.d.ts +104 -0
- package/dist/core/src/contracts/preset.js +141 -0
- package/dist/core/src/contracts/presetStore.d.ts +81 -0
- package/dist/core/src/contracts/presetStore.js +146 -0
- package/dist/core/src/contracts/usePresets.d.ts +51 -0
- package/dist/core/src/contracts/usePresets.js +170 -0
- package/dist/core/src/defaults.d.ts +7 -0
- package/dist/core/src/defaults.js +9 -0
- package/dist/core/src/filters/codec.d.ts +20 -0
- package/dist/core/src/filters/codec.js +61 -0
- package/dist/core/src/filters/date.d.ts +30 -0
- package/dist/core/src/filters/date.js +179 -0
- package/dist/core/src/filters/describe.d.ts +45 -0
- package/dist/core/src/filters/describe.js +189 -0
- package/dist/core/src/filters/kind.d.ts +52 -0
- package/dist/core/src/filters/kind.js +110 -0
- package/dist/core/src/filters/kinds/_list.d.ts +13 -0
- package/dist/core/src/filters/kinds/_list.js +42 -0
- package/dist/core/src/filters/kinds/boolean.d.ts +3 -0
- package/dist/core/src/filters/kinds/boolean.js +21 -0
- package/dist/core/src/filters/kinds/cascade.d.ts +6 -0
- package/dist/core/src/filters/kinds/cascade.js +16 -0
- package/dist/core/src/filters/kinds/commonCode.d.ts +3 -0
- package/dist/core/src/filters/kinds/commonCode.js +13 -0
- package/dist/core/src/filters/kinds/date.d.ts +9 -0
- package/dist/core/src/filters/kinds/date.js +32 -0
- package/dist/core/src/filters/kinds/enum.d.ts +4 -0
- package/dist/core/src/filters/kinds/enum.js +13 -0
- package/dist/core/src/filters/kinds/index.d.ts +49 -0
- package/dist/core/src/filters/kinds/index.js +39 -0
- package/dist/core/src/filters/kinds/linear.d.ts +5 -0
- package/dist/core/src/filters/kinds/linear.js +16 -0
- package/dist/core/src/filters/kinds/number.d.ts +5 -0
- package/dist/core/src/filters/kinds/number.js +19 -0
- package/dist/core/src/filters/kinds/period.d.ts +4 -0
- package/dist/core/src/filters/kinds/period.js +17 -0
- package/dist/core/src/filters/kinds/relation.d.ts +5 -0
- package/dist/core/src/filters/kinds/relation.js +17 -0
- package/dist/core/src/filters/kinds/schedule.d.ts +5 -0
- package/dist/core/src/filters/kinds/schedule.js +16 -0
- package/dist/core/src/filters/kinds/text.d.ts +4 -0
- package/dist/core/src/filters/kinds/text.js +18 -0
- package/dist/core/src/filters/linear.d.ts +33 -0
- package/dist/core/src/filters/linear.js +65 -0
- package/dist/core/src/filters/list.d.ts +32 -0
- package/dist/core/src/filters/list.js +87 -0
- package/dist/core/src/filters/number.d.ts +17 -0
- package/dist/core/src/filters/number.js +82 -0
- package/dist/core/src/filters/registry.d.ts +23 -0
- package/dist/core/src/filters/registry.js +121 -0
- package/dist/core/src/filters/relation.d.ts +26 -0
- package/dist/core/src/filters/relation.js +75 -0
- package/dist/core/src/filters/schedule.d.ts +21 -0
- package/dist/core/src/filters/schedule.js +60 -0
- package/dist/core/src/filters/text.d.ts +44 -0
- package/dist/core/src/filters/text.js +127 -0
- package/dist/core/src/hooks/index.d.ts +6 -0
- package/dist/core/src/hooks/index.js +6 -0
- package/dist/core/src/hooks/useDataTable.d.ts +13 -0
- package/dist/core/src/hooks/useDataTable.js +109 -0
- package/dist/core/src/hooks/useGrouping.d.ts +33 -0
- package/dist/core/src/hooks/useGrouping.js +57 -0
- package/dist/core/src/hooks/useSearchHistory.d.ts +20 -0
- package/dist/core/src/hooks/useSearchHistory.js +52 -0
- package/dist/core/src/hooks/useTableData.d.ts +25 -0
- package/dist/core/src/hooks/useTableData.js +49 -0
- package/dist/core/src/hooks/useTableFromDefinition.d.ts +47 -0
- package/dist/core/src/hooks/useTableFromDefinition.js +174 -0
- package/dist/core/src/hooks/useTableSearch.d.ts +10 -0
- package/dist/core/src/hooks/useTableSearch.js +434 -0
- package/dist/core/src/hooks/useUrlSync.d.ts +64 -0
- package/dist/core/src/hooks/useUrlSync.js +145 -0
- package/dist/core/src/index.d.ts +38 -0
- package/dist/core/src/index.js +43 -0
- package/dist/core/src/pivot/runClientPivot.d.ts +101 -0
- package/dist/core/src/pivot/runClientPivot.js +230 -0
- package/dist/core/src/pivot/transforms.d.ts +19 -0
- package/dist/core/src/pivot/transforms.js +105 -0
- package/dist/core/src/registry.d.ts +46 -0
- package/dist/core/src/registry.js +67 -0
- package/dist/core/src/types/column.d.ts +213 -0
- package/dist/core/src/types/column.js +1 -0
- package/dist/core/src/types/data.d.ts +63 -0
- package/dist/core/src/types/data.js +1 -0
- package/dist/core/src/types/definition.d.ts +239 -0
- package/dist/core/src/types/definition.js +59 -0
- package/dist/core/src/types/filter.d.ts +94 -0
- package/dist/core/src/types/filter.js +1 -0
- package/dist/core/src/types/filterKind.d.ts +204 -0
- package/dist/core/src/types/filterKind.js +1 -0
- package/dist/core/src/types/grouping.d.ts +80 -0
- package/dist/core/src/types/grouping.js +4 -0
- package/dist/core/src/types/index.d.ts +8 -0
- package/dist/core/src/types/index.js +8 -0
- package/dist/core/src/types/operators.d.ts +64 -0
- package/dist/core/src/types/operators.js +108 -0
- package/dist/core/src/types/request.d.ts +114 -0
- package/dist/core/src/types/request.js +1 -0
- package/dist/core/src/types/search.d.ts +112 -0
- package/dist/core/src/types/search.js +1 -0
- package/dist/core/src/types/sources.d.ts +84 -0
- package/dist/core/src/types/sources.js +1 -0
- package/dist/core/src/types/table.d.ts +40 -0
- package/dist/core/src/types/table.js +1 -0
- package/dist/core/src/utils/compare.d.ts +13 -0
- package/dist/core/src/utils/compare.js +53 -0
- package/dist/core/src/utils/dateText.d.ts +12 -0
- package/dist/core/src/utils/dateText.js +30 -0
- package/dist/core/src/utils/getValue.d.ts +9 -0
- package/dist/core/src/utils/getValue.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/mui/src/TableLayout.d.ts +75 -0
- package/dist/mui/src/TableLayout.js +162 -0
- package/dist/mui/src/TableScreen.d.ts +70 -0
- package/dist/mui/src/TableScreen.js +174 -0
- package/dist/mui/src/addons/FilterModal.d.ts +45 -0
- package/dist/mui/src/addons/FilterModal.js +39 -0
- package/dist/mui/src/addons/FilterStatusModal.d.ts +40 -0
- package/dist/mui/src/addons/FilterStatusModal.js +68 -0
- package/dist/mui/src/addons/PresetBar.d.ts +56 -0
- package/dist/mui/src/addons/PresetBar.js +145 -0
- package/dist/mui/src/addons/ShareDropdown.d.ts +35 -0
- package/dist/mui/src/addons/ShareDropdown.js +41 -0
- package/dist/mui/src/addons/searchStore.d.ts +44 -0
- package/dist/mui/src/addons/searchStore.js +112 -0
- package/dist/mui/src/addons/searchSummary.d.ts +106 -0
- package/dist/mui/src/addons/searchSummary.js +109 -0
- package/dist/mui/src/columns/displayColumns.d.ts +100 -0
- package/dist/mui/src/columns/displayColumns.js +174 -0
- package/dist/mui/src/content/CardContent.d.ts +24 -0
- package/dist/mui/src/content/CardContent.js +25 -0
- package/dist/mui/src/content/ColumnMenu.d.ts +43 -0
- package/dist/mui/src/content/ColumnMenu.js +40 -0
- package/dist/mui/src/content/ColumnVisibilityMenu.d.ts +14 -0
- package/dist/mui/src/content/ColumnVisibilityMenu.js +37 -0
- package/dist/mui/src/content/EditCell.d.ts +19 -0
- package/dist/mui/src/content/EditCell.js +61 -0
- package/dist/mui/src/content/HeaderCell.d.ts +59 -0
- package/dist/mui/src/content/HeaderCell.js +88 -0
- package/dist/mui/src/content/SelectionActionBar.d.ts +17 -0
- package/dist/mui/src/content/SelectionActionBar.js +19 -0
- package/dist/mui/src/content/SortControl.d.ts +17 -0
- package/dist/mui/src/content/SortControl.js +45 -0
- package/dist/mui/src/content/TableContent.d.ts +191 -0
- package/dist/mui/src/content/TableContent.js +525 -0
- package/dist/mui/src/content/TableErrorFallback.d.ts +24 -0
- package/dist/mui/src/content/TableErrorFallback.js +29 -0
- package/dist/mui/src/content/cellError.d.ts +13 -0
- package/dist/mui/src/content/cellError.js +20 -0
- package/dist/mui/src/content/cellRenderers.d.ts +71 -0
- package/dist/mui/src/content/cellRenderers.js +127 -0
- package/dist/mui/src/content/cellValue.d.ts +27 -0
- package/dist/mui/src/content/cellValue.js +82 -0
- package/dist/mui/src/content/columnOrder.d.ts +7 -0
- package/dist/mui/src/content/columnOrder.js +48 -0
- package/dist/mui/src/content/density.d.ts +13 -0
- package/dist/mui/src/content/density.js +12 -0
- package/dist/mui/src/content/menuItems.d.ts +118 -0
- package/dist/mui/src/content/menuItems.js +128 -0
- package/dist/mui/src/content/pinning.d.ts +17 -0
- package/dist/mui/src/content/pinning.js +65 -0
- package/dist/mui/src/exporters/index.d.ts +1 -0
- package/dist/mui/src/exporters/index.js +1 -0
- package/dist/mui/src/exporters/useExportFormatters.d.ts +13 -0
- package/dist/mui/src/exporters/useExportFormatters.js +17 -0
- package/dist/mui/src/filter/FilterArea.d.ts +40 -0
- package/dist/mui/src/filter/FilterArea.js +37 -0
- package/dist/mui/src/filter/FilterInput.d.ts +14 -0
- package/dist/mui/src/filter/FilterInput.js +18 -0
- package/dist/mui/src/filter/inputs/DateFilterInput.d.ts +14 -0
- package/dist/mui/src/filter/inputs/DateFilterInput.js +112 -0
- package/dist/mui/src/filter/inputs/LinearFilterInput.d.ts +10 -0
- package/dist/mui/src/filter/inputs/LinearFilterInput.js +35 -0
- package/dist/mui/src/filter/inputs/NumberFilterInput.d.ts +12 -0
- package/dist/mui/src/filter/inputs/NumberFilterInput.js +111 -0
- package/dist/mui/src/filter/inputs/OperatorMenu.d.ts +14 -0
- package/dist/mui/src/filter/inputs/OperatorMenu.js +25 -0
- package/dist/mui/src/filter/inputs/RelationFilterInput.d.ts +11 -0
- package/dist/mui/src/filter/inputs/RelationFilterInput.js +67 -0
- package/dist/mui/src/filter/inputs/ScheduleFilterInput.d.ts +8 -0
- package/dist/mui/src/filter/inputs/ScheduleFilterInput.js +17 -0
- package/dist/mui/src/filter/inputs/SelectFilterInput.d.ts +16 -0
- package/dist/mui/src/filter/inputs/SelectFilterInput.js +57 -0
- package/dist/mui/src/filter/inputs/TextFilterInput.d.ts +13 -0
- package/dist/mui/src/filter/inputs/TextFilterInput.js +55 -0
- package/dist/mui/src/filter/kinds.d.ts +58 -0
- package/dist/mui/src/filter/kinds.js +103 -0
- package/dist/mui/src/grouping/AggregateChip.d.ts +55 -0
- package/dist/mui/src/grouping/AggregateChip.js +128 -0
- package/dist/mui/src/grouping/AggregationChips.d.ts +49 -0
- package/dist/mui/src/grouping/AggregationChips.js +80 -0
- package/dist/mui/src/grouping/AggregationSettingsButton.d.ts +14 -0
- package/dist/mui/src/grouping/AggregationSettingsButton.js +19 -0
- package/dist/mui/src/grouping/GroupingBar.d.ts +59 -0
- package/dist/mui/src/grouping/GroupingBar.js +267 -0
- package/dist/mui/src/grouping/StepExpandButton.d.ts +12 -0
- package/dist/mui/src/grouping/StepExpandButton.js +18 -0
- package/dist/mui/src/grouping/groupRow.d.ts +68 -0
- package/dist/mui/src/grouping/groupRow.js +129 -0
- package/dist/mui/src/headers/defaultHeader.d.ts +28 -0
- package/dist/mui/src/headers/defaultHeader.js +90 -0
- package/dist/mui/src/headers/defineHeader.d.ts +21 -0
- package/dist/mui/src/headers/defineHeader.js +68 -0
- package/dist/mui/src/headers/index.d.ts +5 -0
- package/dist/mui/src/headers/index.js +3 -0
- package/dist/mui/src/headers/meta.d.ts +18 -0
- package/dist/mui/src/headers/meta.js +11 -0
- package/dist/mui/src/headers/types.d.ts +84 -0
- package/dist/mui/src/headers/types.js +1 -0
- package/dist/mui/src/index.d.ts +110 -0
- package/dist/mui/src/index.js +79 -0
- package/dist/mui/src/layout/ResizeHandle.d.ts +30 -0
- package/dist/mui/src/layout/ResizeHandle.js +66 -0
- package/dist/mui/src/menus/columnMenu.d.ts +45 -0
- package/dist/mui/src/menus/columnMenu.js +144 -0
- package/dist/mui/src/menus/copy.d.ts +5 -0
- package/dist/mui/src/menus/copy.js +25 -0
- package/dist/mui/src/menus/index.d.ts +6 -0
- package/dist/mui/src/menus/index.js +4 -0
- package/dist/mui/src/menus/resolve.d.ts +15 -0
- package/dist/mui/src/menus/resolve.js +60 -0
- package/dist/mui/src/menus/rowMenu.d.ts +46 -0
- package/dist/mui/src/menus/rowMenu.js +76 -0
- package/dist/mui/src/pagination/PaginationArea.d.ts +16 -0
- package/dist/mui/src/pagination/PaginationArea.js +23 -0
- package/dist/mui/src/pivot/PivotTable.d.ts +18 -0
- package/dist/mui/src/pivot/PivotTable.js +100 -0
- package/dist/mui/src/provider/KokoTableProvider.d.ts +184 -0
- package/dist/mui/src/provider/KokoTableProvider.js +118 -0
- package/dist/mui/src/provider/labels.d.ts +85 -0
- package/dist/mui/src/provider/labels.js +104 -0
- package/dist/mui/src/registry.d.ts +30 -0
- package/dist/mui/src/registry.js +5 -0
- package/dist/mui/src/toolbar/GlobalFilterField.d.ts +36 -0
- package/dist/mui/src/toolbar/GlobalFilterField.js +78 -0
- package/dist/mui/src/virtual/VirtualTableContent.d.ts +41 -0
- package/dist/mui/src/virtual/VirtualTableContent.js +58 -0
- package/dist/mui/src/virtual/VirtualTableScreen.d.ts +13 -0
- package/dist/mui/src/virtual/VirtualTableScreen.js +23 -0
- package/dist/mui/src/virtual.d.ts +4 -0
- package/dist/mui/src/virtual.js +4 -0
- package/dist/mui/src/virtualColumns/VirtualColumnCard.d.ts +26 -0
- package/dist/mui/src/virtualColumns/VirtualColumnCard.js +29 -0
- package/dist/mui/src/virtualColumns/VirtualColumnDialog.d.ts +27 -0
- package/dist/mui/src/virtualColumns/VirtualColumnDialog.js +154 -0
- package/dist/mui/src/virtualColumns/VirtualColumnSection.d.ts +31 -0
- package/dist/mui/src/virtualColumns/VirtualColumnSection.js +55 -0
- package/dist/mui/src/virtualColumns/expression.d.ts +68 -0
- package/dist/mui/src/virtualColumns/expression.js +489 -0
- package/dist/mui/src/virtualColumns/useVirtualColumns.d.ts +67 -0
- package/dist/mui/src/virtualColumns/useVirtualColumns.js +111 -0
- package/dist/mui/src/virtualColumns/virtualColumnDefs.d.ts +29 -0
- package/dist/mui/src/virtualColumns/virtualColumnDefs.js +51 -0
- package/dist/mui-virtual.d.ts +1 -0
- package/dist/mui-virtual.js +2 -0
- package/dist/mui.d.ts +1 -0
- package/dist/mui.js +2 -0
- package/dist/query/src/index.d.ts +6 -0
- package/dist/query/src/index.js +7 -0
- package/dist/query/src/serverPivot.d.ts +42 -0
- package/dist/query/src/serverPivot.js +153 -0
- package/dist/query/src/useServerGrouping.d.ts +52 -0
- package/dist/query/src/useServerGrouping.js +186 -0
- package/dist/query/src/useServerTable.d.ts +20 -0
- package/dist/query/src/useServerTable.js +106 -0
- package/dist/query/src/useServerTableData.d.ts +48 -0
- package/dist/query/src/useServerTableData.js +64 -0
- package/dist/query/src/wire.d.ts +16 -0
- package/dist/query/src/wire.js +32 -0
- package/dist/query.d.ts +1 -0
- package/dist/query.js +2 -0
- package/package.json +125 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { BOOLEAN_OPERATORS, DATE_OPERATORS, LINEAR_OPERATORS, LIST_OPERATORS, NUMBER_OPERATORS, RELATION_OPERATORS, SCHEDULE_OPERATORS, TEXT_OPERATORS, } from "../types/operators";
|
|
2
|
+
import { resolveVariant } from "../columns/deriveOperators";
|
|
3
|
+
import { getValue } from "../utils/getValue";
|
|
4
|
+
import { matchText } from "./text";
|
|
5
|
+
import { matchNumber } from "./number";
|
|
6
|
+
import { matchDate } from "./date";
|
|
7
|
+
import { matchList, matchListBetween, matchArrayCell } from "./list";
|
|
8
|
+
import { matchRelation } from "./relation";
|
|
9
|
+
import { matchLinear } from "./linear";
|
|
10
|
+
import { matchSchedule } from "./schedule";
|
|
11
|
+
import { resolveFilterKind } from "./kind";
|
|
12
|
+
const has = (list, op) => list.includes(op);
|
|
13
|
+
/**
|
|
14
|
+
* 컬럼 타입과 무관하게 **가상 경로**로 해석되는 필터 UI 종류.
|
|
15
|
+
* 서버도 이 연산자들은 `column_type` 을 보기 전에 갈라낸다.
|
|
16
|
+
*/
|
|
17
|
+
const VIRTUAL_VARIANTS = new Set(["relation", "linear", "schedule"]);
|
|
18
|
+
export function isVirtualColumn(column) {
|
|
19
|
+
return !!column && VIRTUAL_VARIANTS.has(resolveVariant(column));
|
|
20
|
+
}
|
|
21
|
+
export function resolveDataKind(column) {
|
|
22
|
+
// kind 가 서버 `column_type` 대응을 선언했으면 그것이 가장 구체적이다
|
|
23
|
+
const kindDataKind = resolveFilterKind(column)?.dataKind;
|
|
24
|
+
if (kindDataKind)
|
|
25
|
+
return kindDataKind;
|
|
26
|
+
// `filter.variant` 가 가상 경로(관계·선형·일정)면 dataType 보다 먼저 판정한다.
|
|
27
|
+
// `ColumnBuilder` 는 dataType 을 항상 채우기 때문에(`ColumnIsRelation` 도 "text"),
|
|
28
|
+
// dataType 을 먼저 보면 관계 필터가 텍스트 fuzzy 폴백으로 떨어져 hasChild 가
|
|
29
|
+
// 0건, hasAny/hasAll 이 조건을 무시하는 결함이 있었다.
|
|
30
|
+
if (column?.filter?.variant === "relation")
|
|
31
|
+
return "relation";
|
|
32
|
+
const dataType = column?.meta?.dataType;
|
|
33
|
+
if (dataType === "number")
|
|
34
|
+
return "number";
|
|
35
|
+
if (dataType === "date" || dataType === "datetime")
|
|
36
|
+
return "date";
|
|
37
|
+
if (dataType === "boolean")
|
|
38
|
+
return "boolean";
|
|
39
|
+
if (dataType)
|
|
40
|
+
return "text";
|
|
41
|
+
if (!column)
|
|
42
|
+
return "text";
|
|
43
|
+
switch (resolveVariant(column)) {
|
|
44
|
+
case "number": return "number";
|
|
45
|
+
case "date":
|
|
46
|
+
case "datetime": return "date";
|
|
47
|
+
case "boolean": return "boolean";
|
|
48
|
+
case "relation": return "relation";
|
|
49
|
+
default: return "text";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 컬럼 필터 하나를 행에 적용한다.
|
|
54
|
+
*
|
|
55
|
+
* 분기 순서는 백엔드 `_apply_filter_by_type` 을 그대로 따른다 —
|
|
56
|
+
* 순서가 어긋나면 같은 요청이 client/server 에서 다른 결과를 낸다.
|
|
57
|
+
* (예: `between` 은 숫자·날짜·리스트 셋 다 지원하므로 컬럼 종류가 먼저 결정돼야 한다)
|
|
58
|
+
*/
|
|
59
|
+
export function matchColumnFilter(row, columnId, filterValue, operator, column, ctx = {}) {
|
|
60
|
+
// 1. custom — 서버가 필터를 건너뛴다 (호출부가 값 해석을 전담)
|
|
61
|
+
if (operator === "custom")
|
|
62
|
+
return true;
|
|
63
|
+
// 1'. kind 경로 — 컬럼이 kind 를 선언했고 연산자가 그 kind 에 있으면 `operators[op].client` 가 판정한다.
|
|
64
|
+
// 내장 kind 는 아래 분기와 같은 함수를 부르므로 결과가 같다 (kinds.test.ts 가 대조한다).
|
|
65
|
+
// 연산자가 kind 에 없으면(사용자가 다른 연산자를 골랐을 때) 아래 variant 경로로 떨어진다.
|
|
66
|
+
const kind = resolveFilterKind(column);
|
|
67
|
+
const kindOperator = kind?.operators[operator];
|
|
68
|
+
if (kind && kindOperator) {
|
|
69
|
+
return kindOperator.client(getValue(row, columnId), filterValue, { row, columnId, now: ctx.now });
|
|
70
|
+
}
|
|
71
|
+
// 2. Linear (가상 경로 — 컬럼 타입과 무관하게 먼저)
|
|
72
|
+
if (has(LINEAR_OPERATORS, operator)) {
|
|
73
|
+
return matchLinear(row, columnId, filterValue, operator);
|
|
74
|
+
}
|
|
75
|
+
// 3. Schedule (가상 경로)
|
|
76
|
+
if (has(SCHEDULE_OPERATORS, operator)) {
|
|
77
|
+
return matchSchedule(row, filterValue, operator);
|
|
78
|
+
}
|
|
79
|
+
const dataKind = resolveDataKind(column);
|
|
80
|
+
const cellValue = getValue(row, columnId);
|
|
81
|
+
// 4. Relation
|
|
82
|
+
if (dataKind === "relation" && has(RELATION_OPERATORS, operator)) {
|
|
83
|
+
return matchRelation(row, columnId, filterValue, operator);
|
|
84
|
+
}
|
|
85
|
+
// 5. Number
|
|
86
|
+
if (dataKind === "number" && has(NUMBER_OPERATORS, operator)) {
|
|
87
|
+
return matchNumber(cellValue, filterValue, operator);
|
|
88
|
+
}
|
|
89
|
+
// 6. Date
|
|
90
|
+
if (dataKind === "date" && has(DATE_OPERATORS, operator)) {
|
|
91
|
+
return matchDate(cellValue, filterValue, operator, ctx.now);
|
|
92
|
+
}
|
|
93
|
+
// 7. Boolean — 백엔드는 equals 만 지원한다
|
|
94
|
+
if (dataKind === "boolean" && has(BOOLEAN_OPERATORS, operator)) {
|
|
95
|
+
return String(cellValue) === String(filterValue);
|
|
96
|
+
}
|
|
97
|
+
// 8. List (컬럼 종류와 무관)
|
|
98
|
+
if (has(LIST_OPERATORS, operator)) {
|
|
99
|
+
switch (operator) {
|
|
100
|
+
case "between":
|
|
101
|
+
return matchListBetween(cellValue, filterValue);
|
|
102
|
+
case "notIn":
|
|
103
|
+
case "notInArray":
|
|
104
|
+
return matchList(cellValue, filterValue, true);
|
|
105
|
+
case "arrIncludes":
|
|
106
|
+
return matchArrayCell(cellValue, filterValue, "one");
|
|
107
|
+
case "arrIncludesSome":
|
|
108
|
+
return matchArrayCell(cellValue, filterValue, "some");
|
|
109
|
+
case "arrIncludesAll":
|
|
110
|
+
return matchArrayCell(cellValue, filterValue, "all");
|
|
111
|
+
default:
|
|
112
|
+
return matchList(cellValue, filterValue, false);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 9. Text
|
|
116
|
+
if (has(TEXT_OPERATORS, operator)) {
|
|
117
|
+
return matchText(cellValue, filterValue, operator);
|
|
118
|
+
}
|
|
119
|
+
// 10. 전부 미해당 — 백엔드도 fuzzy 로 떨어진다
|
|
120
|
+
return matchText(cellValue, filterValue, "fuzzy");
|
|
121
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** 1:N 관계 필터 연산자 */
|
|
2
|
+
export type RelationOperatorName = "hasAny" | "hasAll" | "hasChild" | "hasNotChild";
|
|
3
|
+
export interface RelationCondition {
|
|
4
|
+
/** 자식 행의 컬럼 경로 (`"item.name"` 등) */
|
|
5
|
+
column: string;
|
|
6
|
+
operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "isNull" | "isNotNull";
|
|
7
|
+
value: string | number | boolean | Array<string | number>;
|
|
8
|
+
}
|
|
9
|
+
export interface RelationConditionGroup {
|
|
10
|
+
/** 같은 자식 행에서 모두 만족해야 하는 조건들 (AND) */
|
|
11
|
+
conditions: RelationCondition[];
|
|
12
|
+
}
|
|
13
|
+
export interface RelationFilterValue {
|
|
14
|
+
groups: RelationConditionGroup[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 느슨한 동등 — 서버는 `inner_column == value` 를 컬럼 타입으로 바인딩하므로
|
|
18
|
+
* 정수 컬럼에 문자열 `'2'` 를 주면 PG 가 캐스팅해 `2` 와 일치한다.
|
|
19
|
+
* 양쪽이 숫자로 읽히면 숫자로, 아니면 문자열로 비교한다.
|
|
20
|
+
*/
|
|
21
|
+
export declare function looseEquals(a: unknown, b: unknown): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 관계 필터 — 그룹 내 조건은 AND, 그룹 간은 연산자에 따라 OR(`hasAny`) / AND(`hasAll`).
|
|
24
|
+
* `hasChild` / `hasNotChild` 는 자식 존재 여부만 본다.
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchRelation(row: unknown, columnId: string, filterValue: unknown, operator: RelationOperatorName): boolean;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getValue } from "../utils/getValue";
|
|
2
|
+
const asNumber = (value) => {
|
|
3
|
+
if (value === null || value === undefined || value === "" || typeof value === "boolean")
|
|
4
|
+
return null;
|
|
5
|
+
if (typeof value === "object")
|
|
6
|
+
return null;
|
|
7
|
+
const num = Number(value);
|
|
8
|
+
return Number.isNaN(num) ? null : num;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* 느슨한 동등 — 서버는 `inner_column == value` 를 컬럼 타입으로 바인딩하므로
|
|
12
|
+
* 정수 컬럼에 문자열 `'2'` 를 주면 PG 가 캐스팅해 `2` 와 일치한다.
|
|
13
|
+
* 양쪽이 숫자로 읽히면 숫자로, 아니면 문자열로 비교한다.
|
|
14
|
+
*/
|
|
15
|
+
export function looseEquals(a, b) {
|
|
16
|
+
const an = asNumber(a);
|
|
17
|
+
const bn = asNumber(b);
|
|
18
|
+
if (an !== null && bn !== null)
|
|
19
|
+
return an === bn;
|
|
20
|
+
return String(a) === String(b);
|
|
21
|
+
}
|
|
22
|
+
function evaluateCondition(item, condition) {
|
|
23
|
+
const itemValue = getValue(item, condition.column);
|
|
24
|
+
// isNull 계열을 제외하면 NULL 셀은 SQL 3값 논리상 어떤 비교도 참이 되지 않는다
|
|
25
|
+
// (`NULL != 'x'` 도 NULL 이라 notEquals/notContains 로도 살아나지 않는다)
|
|
26
|
+
if (condition.operator === "isNull") {
|
|
27
|
+
return condition.value === true ? itemValue == null : itemValue != null;
|
|
28
|
+
}
|
|
29
|
+
if (condition.operator === "isNotNull")
|
|
30
|
+
return itemValue != null;
|
|
31
|
+
if (itemValue === null || itemValue === undefined)
|
|
32
|
+
return false;
|
|
33
|
+
switch (condition.operator) {
|
|
34
|
+
case "equals": return looseEquals(itemValue, condition.value);
|
|
35
|
+
case "notEquals": return !looseEquals(itemValue, condition.value);
|
|
36
|
+
case "contains":
|
|
37
|
+
return String(itemValue).toLowerCase().includes(String(condition.value).toLowerCase());
|
|
38
|
+
case "notContains":
|
|
39
|
+
return !String(itemValue).toLowerCase().includes(String(condition.value).toLowerCase());
|
|
40
|
+
case "greaterThan": return Number(itemValue) > Number(condition.value);
|
|
41
|
+
case "lessThan": return Number(itemValue) < Number(condition.value);
|
|
42
|
+
case "greaterThanOrEqual": return Number(itemValue) >= Number(condition.value);
|
|
43
|
+
case "lessThanOrEqual": return Number(itemValue) <= Number(condition.value);
|
|
44
|
+
default: return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function relationItems(row, columnId) {
|
|
48
|
+
const data = getValue(row, columnId);
|
|
49
|
+
return Array.isArray(data) ? data : null;
|
|
50
|
+
}
|
|
51
|
+
/** 그룹 하나를 만족하는 자식이 하나라도 있는가 (그룹 내 조건은 AND) */
|
|
52
|
+
function groupMatches(items, group) {
|
|
53
|
+
if (!group.conditions || group.conditions.length === 0)
|
|
54
|
+
return true; // 빈 그룹은 항상 참
|
|
55
|
+
return items.some((item) => group.conditions.every((c) => evaluateCondition(item, c)));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 관계 필터 — 그룹 내 조건은 AND, 그룹 간은 연산자에 따라 OR(`hasAny`) / AND(`hasAll`).
|
|
59
|
+
* `hasChild` / `hasNotChild` 는 자식 존재 여부만 본다.
|
|
60
|
+
*/
|
|
61
|
+
export function matchRelation(row, columnId, filterValue, operator) {
|
|
62
|
+
const items = relationItems(row, columnId);
|
|
63
|
+
if (operator === "hasChild")
|
|
64
|
+
return items !== null && items.length > 0;
|
|
65
|
+
if (operator === "hasNotChild")
|
|
66
|
+
return items === null || items.length === 0;
|
|
67
|
+
if (!items || items.length === 0)
|
|
68
|
+
return false;
|
|
69
|
+
const value = filterValue;
|
|
70
|
+
if (!value?.groups || value.groups.length === 0)
|
|
71
|
+
return true; // 자식 존재만 확인
|
|
72
|
+
return operator === "hasAll"
|
|
73
|
+
? value.groups.every((g) => groupMatches(items, g))
|
|
74
|
+
: value.groups.some((g) => groupMatches(items, g));
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 일정 필터 — 예정(planned) 기간과 실제(actual) 기간을 **OR 로 동시 검증**한다.
|
|
3
|
+
* 백엔드 `ScheduleFilter` 와 같은 규칙.
|
|
4
|
+
*/
|
|
5
|
+
export type ScheduleOperatorName = "scheduleContains" | "scheduleOverlaps";
|
|
6
|
+
export interface ScheduleFields {
|
|
7
|
+
plannedStart?: string;
|
|
8
|
+
plannedEnd?: string;
|
|
9
|
+
actualStart?: string;
|
|
10
|
+
actualEnd?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ScheduleContainsValue {
|
|
13
|
+
date: string;
|
|
14
|
+
fields?: ScheduleFields;
|
|
15
|
+
}
|
|
16
|
+
export interface ScheduleOverlapsValue {
|
|
17
|
+
start: string;
|
|
18
|
+
end: string;
|
|
19
|
+
fields?: ScheduleFields;
|
|
20
|
+
}
|
|
21
|
+
export declare function matchSchedule(row: unknown, filterValue: unknown, operator: ScheduleOperatorName): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getValue } from "../utils/getValue";
|
|
2
|
+
import { parseDateValue } from "./date";
|
|
3
|
+
const DEFAULT_FIELDS = {
|
|
4
|
+
plannedStart: "planned_process_date",
|
|
5
|
+
plannedEnd: "planned_finish_date",
|
|
6
|
+
actualStart: "actual_process_date",
|
|
7
|
+
actualEnd: "actual_finish_date",
|
|
8
|
+
};
|
|
9
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
10
|
+
function readPair(row, startPath, endPath) {
|
|
11
|
+
const start = getValue(row, startPath);
|
|
12
|
+
const end = getValue(row, endPath);
|
|
13
|
+
return { start, end, present: start !== undefined || end !== undefined };
|
|
14
|
+
}
|
|
15
|
+
function toTime(value) {
|
|
16
|
+
const parsed = parseDateValue(value);
|
|
17
|
+
return parsed === null ? null : parsed.date.getTime();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `start < hi && end >= lo` — 상한은 **반열림**(서버 `start_col < range_end`).
|
|
21
|
+
* `hi` 는 종료일 다음 날 00:00 이므로, 정확히 그 시각에 시작하는 행은 겹치지 않는다.
|
|
22
|
+
* NULL 이 하나라도 있으면 SQL 3값 논리상 거짓.
|
|
23
|
+
*/
|
|
24
|
+
function overlaps(pair, lo, hi) {
|
|
25
|
+
const start = toTime(pair.start);
|
|
26
|
+
const end = toTime(pair.end);
|
|
27
|
+
if (start === null || end === null)
|
|
28
|
+
return false;
|
|
29
|
+
return start < hi && end >= lo;
|
|
30
|
+
}
|
|
31
|
+
export function matchSchedule(row, filterValue, operator) {
|
|
32
|
+
if (filterValue === null || typeof filterValue !== "object")
|
|
33
|
+
return true;
|
|
34
|
+
const value = filterValue;
|
|
35
|
+
const fields = { ...DEFAULT_FIELDS, ...(value.fields ?? {}) };
|
|
36
|
+
let lo;
|
|
37
|
+
let hi;
|
|
38
|
+
if (operator === "scheduleOverlaps") {
|
|
39
|
+
if (!value.start || !value.end)
|
|
40
|
+
return true; // 값 미입력 — 필터 스킵
|
|
41
|
+
lo = toTime(value.start);
|
|
42
|
+
const endTime = toTime(value.end);
|
|
43
|
+
hi = endTime === null ? null : endTime + MS_PER_DAY;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (!value.date)
|
|
47
|
+
return true;
|
|
48
|
+
lo = toTime(value.date);
|
|
49
|
+
hi = lo === null ? null : lo + MS_PER_DAY;
|
|
50
|
+
}
|
|
51
|
+
if (lo === null || hi === null)
|
|
52
|
+
return true; // 파싱 실패 — 백엔드도 필터를 건너뛴다
|
|
53
|
+
const planned = readPair(row, fields.plannedStart, fields.plannedEnd);
|
|
54
|
+
const actual = readPair(row, fields.actualStart, fields.actualEnd);
|
|
55
|
+
// 유효한 일정 컬럼이 하나도 없으면 조건 없음 (백엔드와 동일)
|
|
56
|
+
if (!planned.present && !actual.present)
|
|
57
|
+
return true;
|
|
58
|
+
return ((planned.present && overlaps(planned, lo, hi)) ||
|
|
59
|
+
(actual.present && overlaps(actual, lo, hi)));
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 텍스트 논리식 — `배추 ∨ ¬김치` 처럼 글리프로 항을 잇는다.
|
|
3
|
+
* 백엔드 `TextLogicValue.parse` 와 문법이 같아야 한다 (어긋나면 서버가 400).
|
|
4
|
+
*/
|
|
5
|
+
export declare const OP_SYMBOLS: {
|
|
6
|
+
readonly or: "∨";
|
|
7
|
+
readonly and: "∧";
|
|
8
|
+
readonly not: "¬";
|
|
9
|
+
};
|
|
10
|
+
/** 항 수 상한 — 서버 파서와 같은 값. 초과하면 서버가 400 을 낸다 */
|
|
11
|
+
export declare const MAX_TEXT_LOGIC_TERMS = 20;
|
|
12
|
+
export interface TextLogicTerm {
|
|
13
|
+
text: string;
|
|
14
|
+
not: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ParsedTextLogic {
|
|
17
|
+
chain: "or" | "and";
|
|
18
|
+
terms: TextLogicTerm[];
|
|
19
|
+
}
|
|
20
|
+
export declare function hasLogicGlyph(value: string): boolean;
|
|
21
|
+
export declare function trimTerm(text: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* 전송 문자열 → 논리식. 글리프가 없거나 문법이 어긋나면 `null`(평문 취급).
|
|
24
|
+
* 서버는 같은 입력을 400 으로 거절하지만, 입력창은 사용자 텍스트를 잃지 않는 쪽이 낫다.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseTextLogic(value: string): ParsedTextLogic | null;
|
|
27
|
+
/**
|
|
28
|
+
* 항 하나의 매칭 — 백엔드 `TextFilter._apply_term` 과 의미를 맞춘다.
|
|
29
|
+
*
|
|
30
|
+
* - `fuzzy` 는 검색어와 셀 **양쪽 공백을 제거**하고 대소문자 무시 (DB "듀 벨" ↔ 검색 "듀벨")
|
|
31
|
+
* - `equals`/`notEquals` 는 대소문자를 **구분한다** (SQL `column_str == value`)
|
|
32
|
+
* - 나머지 부분일치 계열은 소문자 비교
|
|
33
|
+
*/
|
|
34
|
+
export declare function matchTextTerm(cellValue: unknown, term: string, operator: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* `empty` 판정 — 백엔드는 NULL, 빈 문자열, 공백만 셋 다 "비어 있음"으로 본다.
|
|
37
|
+
* 값이 false(문자열 "false"/"False"/"0" 포함)면 방향이 뒤집힌다.
|
|
38
|
+
*/
|
|
39
|
+
export declare function matchTextEmpty(cellValue: unknown, filterValue: unknown): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* 텍스트 필터 전체 — 논리식이면 항별로 매칭 후 결합한다.
|
|
42
|
+
* 서버 `TextFilter.apply` 와 같은 순서: 글리프 파싱 -> 항별 조건 -> or/and 결합.
|
|
43
|
+
*/
|
|
44
|
+
export declare function matchText(cellValue: unknown, filterValue: unknown, operator: string): boolean;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { normalizeSearchText } from "../utils/compare";
|
|
2
|
+
import { isBlank } from "../utils/getValue";
|
|
3
|
+
/**
|
|
4
|
+
* 텍스트 논리식 — `배추 ∨ ¬김치` 처럼 글리프로 항을 잇는다.
|
|
5
|
+
* 백엔드 `TextLogicValue.parse` 와 문법이 같아야 한다 (어긋나면 서버가 400).
|
|
6
|
+
*/
|
|
7
|
+
export const OP_SYMBOLS = { or: "∨", and: "∧", not: "¬" };
|
|
8
|
+
/** 항 수 상한 — 서버 파서와 같은 값. 초과하면 서버가 400 을 낸다 */
|
|
9
|
+
export const MAX_TEXT_LOGIC_TERMS = 20;
|
|
10
|
+
export function hasLogicGlyph(value) {
|
|
11
|
+
return (value.includes(OP_SYMBOLS.or) ||
|
|
12
|
+
value.includes(OP_SYMBOLS.and) ||
|
|
13
|
+
value.includes(OP_SYMBOLS.not));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 항 트림 — 백엔드 `strip()` 과 같은 공백 집합.
|
|
17
|
+
* JS `trim()` 은 U+0085 를 안 지우고 Python `strip()` 은 U+FEFF 를 안 지운다.
|
|
18
|
+
* 양끝이 같은 항을 보려면 합집합을 명시해야 한다.
|
|
19
|
+
*/
|
|
20
|
+
const TERM_TRIM_PATTERN = /^[\s\uFEFF\u0085]+|[\s\uFEFF\u0085]+$/g;
|
|
21
|
+
export function trimTerm(text) {
|
|
22
|
+
return text.replace(TERM_TRIM_PATTERN, "");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 전송 문자열 → 논리식. 글리프가 없거나 문법이 어긋나면 `null`(평문 취급).
|
|
26
|
+
* 서버는 같은 입력을 400 으로 거절하지만, 입력창은 사용자 텍스트를 잃지 않는 쪽이 낫다.
|
|
27
|
+
*/
|
|
28
|
+
export function parseTextLogic(value) {
|
|
29
|
+
if (!hasLogicGlyph(value))
|
|
30
|
+
return null;
|
|
31
|
+
const hasOr = value.includes(OP_SYMBOLS.or);
|
|
32
|
+
const hasAnd = value.includes(OP_SYMBOLS.and);
|
|
33
|
+
if (hasOr && hasAnd)
|
|
34
|
+
return null; // 체인 혼합 — 서버가 400 을 내는 문법 위반
|
|
35
|
+
const chain = hasAnd ? "and" : "or";
|
|
36
|
+
const rawTerms = hasOr || hasAnd ? value.split(hasAnd ? OP_SYMBOLS.and : OP_SYMBOLS.or) : [value];
|
|
37
|
+
const terms = [];
|
|
38
|
+
for (const rawTerm of rawTerms) {
|
|
39
|
+
const trimmed = trimTerm(rawTerm);
|
|
40
|
+
const negated = trimmed.startsWith(OP_SYMBOLS.not);
|
|
41
|
+
const text = negated ? trimTerm(trimmed.slice(OP_SYMBOLS.not.length)) : trimmed;
|
|
42
|
+
// 빈 항(선두·말단·연속 체인 연산자)과 항 중간 not 글리프는 문법 위반
|
|
43
|
+
if (text === "" || text.includes(OP_SYMBOLS.not))
|
|
44
|
+
return null;
|
|
45
|
+
terms.push({ text, not: negated });
|
|
46
|
+
}
|
|
47
|
+
if (terms.length === 0 || terms.length > MAX_TEXT_LOGIC_TERMS)
|
|
48
|
+
return null;
|
|
49
|
+
return { chain, terms };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 항 하나의 매칭 — 백엔드 `TextFilter._apply_term` 과 의미를 맞춘다.
|
|
53
|
+
*
|
|
54
|
+
* - `fuzzy` 는 검색어와 셀 **양쪽 공백을 제거**하고 대소문자 무시 (DB "듀 벨" ↔ 검색 "듀벨")
|
|
55
|
+
* - `equals`/`notEquals` 는 대소문자를 **구분한다** (SQL `column_str == value`)
|
|
56
|
+
* - 나머지 부분일치 계열은 소문자 비교
|
|
57
|
+
*/
|
|
58
|
+
export function matchTextTerm(cellValue, term, operator) {
|
|
59
|
+
// NULL 은 SQL 3값 논리상 어떤 조건에도 걸리지 않는다 (부정 항은 결합부에서 처리)
|
|
60
|
+
if (cellValue === null || cellValue === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
const cell = String(cellValue);
|
|
63
|
+
const lowerCell = cell.toLowerCase();
|
|
64
|
+
const lowerTerm = term.toLowerCase();
|
|
65
|
+
switch (operator) {
|
|
66
|
+
case "fuzzy": {
|
|
67
|
+
const needle = normalizeSearchText(term);
|
|
68
|
+
// 공백만인 검색어는 정규화하면 빈 문자열이 된다. 서버 `create_fuzzy_conditions` 는
|
|
69
|
+
// 이때 조건을 만들지 않고 `ILIKE '% %'`(원문 그대로 contains) 로 폴백한다 —
|
|
70
|
+
// 빈 needle 을 includes 하면 전행이 통과해 서버와 달라진다.
|
|
71
|
+
if (needle === "")
|
|
72
|
+
return lowerCell.includes(lowerTerm);
|
|
73
|
+
return normalizeSearchText(cell).includes(needle);
|
|
74
|
+
}
|
|
75
|
+
case "equals":
|
|
76
|
+
return cell === term;
|
|
77
|
+
case "notEquals":
|
|
78
|
+
return cell !== term;
|
|
79
|
+
case "startsWith":
|
|
80
|
+
return lowerCell.startsWith(lowerTerm);
|
|
81
|
+
case "endsWith":
|
|
82
|
+
return lowerCell.endsWith(lowerTerm);
|
|
83
|
+
case "notContains":
|
|
84
|
+
return !lowerCell.includes(lowerTerm);
|
|
85
|
+
case "contains":
|
|
86
|
+
default:
|
|
87
|
+
return lowerCell.includes(lowerTerm);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `empty` 판정 — 백엔드는 NULL, 빈 문자열, 공백만 셋 다 "비어 있음"으로 본다.
|
|
92
|
+
* 값이 false(문자열 "false"/"False"/"0" 포함)면 방향이 뒤집힌다.
|
|
93
|
+
*/
|
|
94
|
+
export function matchTextEmpty(cellValue, filterValue) {
|
|
95
|
+
const wantEmpty = !(filterValue === false ||
|
|
96
|
+
filterValue === "false" ||
|
|
97
|
+
filterValue === "False" ||
|
|
98
|
+
filterValue === "0");
|
|
99
|
+
const empty = isBlank(cellValue);
|
|
100
|
+
return wantEmpty ? empty : !empty;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 텍스트 필터 전체 — 논리식이면 항별로 매칭 후 결합한다.
|
|
104
|
+
* 서버 `TextFilter.apply` 와 같은 순서: 글리프 파싱 -> 항별 조건 -> or/and 결합.
|
|
105
|
+
*/
|
|
106
|
+
export function matchText(cellValue, filterValue, operator) {
|
|
107
|
+
if (operator === "empty" || operator === "notEmpty") {
|
|
108
|
+
// notEmpty 는 방향이 고정 — empty 에 false 를 준 것과 같다
|
|
109
|
+
return operator === "notEmpty"
|
|
110
|
+
? matchTextEmpty(cellValue, false)
|
|
111
|
+
: matchTextEmpty(cellValue, filterValue);
|
|
112
|
+
}
|
|
113
|
+
if (typeof filterValue !== "string") {
|
|
114
|
+
return matchTextTerm(cellValue, String(filterValue ?? ""), operator);
|
|
115
|
+
}
|
|
116
|
+
const parsed = parseTextLogic(filterValue);
|
|
117
|
+
if (parsed === null)
|
|
118
|
+
return matchTextTerm(cellValue, filterValue, operator);
|
|
119
|
+
// NULL 셀은 부정 항으로도 살아나지 않는다 — 서버의 부정 조건이 NULL 을 걸러내는 것과 같다
|
|
120
|
+
if (cellValue === null || cellValue === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
const results = parsed.terms.map((term) => {
|
|
123
|
+
const matched = matchTextTerm(cellValue, term.text, operator);
|
|
124
|
+
return term.not ? !matched : matched;
|
|
125
|
+
});
|
|
126
|
+
return parsed.chain === "and" ? results.every(Boolean) : results.some(Boolean);
|
|
127
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useTableSearch } from "./useTableSearch";
|
|
2
|
+
export { useTableData, resolveMode, type UseTableDataOptions } from "./useTableData";
|
|
3
|
+
export { useDataTable } from "./useDataTable";
|
|
4
|
+
export { useGrouping, type UseGroupingOptions, type GroupingResult, } from "./useGrouping";
|
|
5
|
+
export { useUrlSync, readUrlSearchState, encodeUrlSearchState, decodeUrlSearchState, DEFAULT_URL_PARAM_KEY, type UseUrlSyncOptions, type UrlSync, } from "./useUrlSync";
|
|
6
|
+
export { useSearchHistory, type SearchHistory, type UseSearchHistoryOptions, } from "./useSearchHistory";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useTableSearch } from "./useTableSearch";
|
|
2
|
+
export { useTableData, resolveMode } from "./useTableData";
|
|
3
|
+
export { useDataTable } from "./useDataTable";
|
|
4
|
+
export { useGrouping, } from "./useGrouping";
|
|
5
|
+
export { useUrlSync, readUrlSearchState, encodeUrlSearchState, decodeUrlSearchState, DEFAULT_URL_PARAM_KEY, } from "./useUrlSync";
|
|
6
|
+
export { useSearchHistory, } from "./useSearchHistory";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { DataTable, DataTableOptions } from "../types/table";
|
|
3
|
+
/**
|
|
4
|
+
* 3층 — 1·2층을 TanStack 인스턴스에 연결한다. **선택 사항이다.**
|
|
5
|
+
*
|
|
6
|
+
* 필터·정렬·페이지는 이미 2층이 계산했으므로 TanStack 에는 `manual*` 을 켜서
|
|
7
|
+
* 다시 계산하지 않게 한다. 두 번 거르면 client 모드에서 결과가 달라진다
|
|
8
|
+
* (2층은 서버 의미, TanStack 은 자체 filterFn 의미로 걸기 때문).
|
|
9
|
+
*
|
|
10
|
+
* `search` / `data` 는 프로퍼티로 얹기만 하고 `getState()` 는 감싸지 않는다 —
|
|
11
|
+
* 기존 `extendTableInstance` 는 렌더마다 `getState` 를 다시 감싸 래퍼가 누적됐다.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useDataTable<T extends RowData>(options: DataTableOptions<T>): DataTable<T>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { getCoreRowModel, useReactTable, } from "@tanstack/react-table";
|
|
3
|
+
import { columnKey } from "../columns/deriveOperators";
|
|
4
|
+
/**
|
|
5
|
+
* 비어 있는 grouping 상태의 **고정 참조**.
|
|
6
|
+
*
|
|
7
|
+
* `search.params.grouping ?? []` 처럼 렌더마다 새 배열을 넘기면 TanStack 의
|
|
8
|
+
* row model 메모가 매번 무효화되고, 그 `onChange` 가 `_autoResetExpanded()` 를
|
|
9
|
+
* 큐에 넣어 상태를 바꾼다 -> 다시 렌더 -> 또 새 배열 -> **무한 루프**로 탭이 멈춘다.
|
|
10
|
+
* (실측: 그룹 펼치기·단순 리렌더 양쪽에서 렌더러 정지)
|
|
11
|
+
*/
|
|
12
|
+
const EMPTY_GROUPING = [];
|
|
13
|
+
/** `meta.isVisible === false` 인 컬럼의 기본 숨김 맵 */
|
|
14
|
+
function defaultVisibility(columns) {
|
|
15
|
+
const map = {};
|
|
16
|
+
for (const column of columns) {
|
|
17
|
+
const id = columnKey(column);
|
|
18
|
+
if (!id)
|
|
19
|
+
continue;
|
|
20
|
+
if (column.meta?.forceHidden || column.meta?.isVisible === false)
|
|
21
|
+
map[id] = false;
|
|
22
|
+
}
|
|
23
|
+
return map;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 값 보관 전용 컬럼 — 토글·프리셋보다 우선해 **항상** 숨긴다.
|
|
27
|
+
* `isVisible: false` 는 기본값일 뿐이라 사용자가 되살릴 수 있지만,
|
|
28
|
+
* 헤더가 없는 컬럼(통합 cascade 에 흡수된 레벨 등)은 되살아나면 빈 열이 된다.
|
|
29
|
+
*/
|
|
30
|
+
function forceHiddenColumns(columns) {
|
|
31
|
+
const map = {};
|
|
32
|
+
for (const column of columns) {
|
|
33
|
+
const id = columnKey(column);
|
|
34
|
+
if (id && column.meta?.forceHidden)
|
|
35
|
+
map[id] = false;
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 3층 — 1·2층을 TanStack 인스턴스에 연결한다. **선택 사항이다.**
|
|
41
|
+
*
|
|
42
|
+
* 필터·정렬·페이지는 이미 2층이 계산했으므로 TanStack 에는 `manual*` 을 켜서
|
|
43
|
+
* 다시 계산하지 않게 한다. 두 번 거르면 client 모드에서 결과가 달라진다
|
|
44
|
+
* (2층은 서버 의미, TanStack 은 자체 filterFn 의미로 걸기 때문).
|
|
45
|
+
*
|
|
46
|
+
* `search` / `data` 는 프로퍼티로 얹기만 하고 `getState()` 는 감싸지 않는다 —
|
|
47
|
+
* 기존 `extendTableInstance` 는 렌더마다 `getState` 를 다시 감싸 래퍼가 누적됐다.
|
|
48
|
+
*/
|
|
49
|
+
export function useDataTable(options) {
|
|
50
|
+
const { columns, search, data, fieldErrors, tableOptions } = options;
|
|
51
|
+
const sorting = useMemo(() => search.params.sorting.map((s) => ({ id: s.id, desc: s.desc })), [search.params.sorting]);
|
|
52
|
+
const onSortingChange = (updater) => {
|
|
53
|
+
const next = typeof updater === "function" ? updater(sorting) : updater;
|
|
54
|
+
search.setSorting(next.map((s) => ({ id: s.id, desc: s.desc })));
|
|
55
|
+
};
|
|
56
|
+
// 컬럼 표시는 **완전 제어형**이다. onChange 를 연결하지 않고 state 만 밀어넣으면
|
|
57
|
+
// 컬럼 메뉴의 "숨기기"와 프리셋 복원이 다음 렌더에 되돌아간다.
|
|
58
|
+
const baseVisibility = useMemo(() => defaultVisibility(columns), [columns]);
|
|
59
|
+
const forced = useMemo(() => forceHiddenColumns(columns), [columns]);
|
|
60
|
+
const [columnVisibility, setColumnVisibility] = useState(() => ({
|
|
61
|
+
...baseVisibility,
|
|
62
|
+
...tableOptions?.initialState?.columnVisibility,
|
|
63
|
+
...forced,
|
|
64
|
+
}));
|
|
65
|
+
// 컬럼 정의가 바뀌면 새 기본값을 합치되 사용자가 바꾼 값은 유지한다
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setColumnVisibility((prev) => {
|
|
68
|
+
const merged = { ...baseVisibility, ...prev, ...forced };
|
|
69
|
+
const prevKeys = Object.keys(prev);
|
|
70
|
+
const same = Object.keys(merged).length === prevKeys.length &&
|
|
71
|
+
prevKeys.every((key) => merged[key] === prev[key]);
|
|
72
|
+
return same ? prev : merged;
|
|
73
|
+
});
|
|
74
|
+
}, [baseVisibility, forced]);
|
|
75
|
+
const onColumnVisibilityChange = useCallback((updater) => {
|
|
76
|
+
setColumnVisibility((prev) => ({
|
|
77
|
+
...(typeof updater === "function" ? updater(prev) : updater),
|
|
78
|
+
// 프리셋 복원은 맵을 통째로 교체하므로 여기서 다시 고정한다
|
|
79
|
+
...forced,
|
|
80
|
+
}));
|
|
81
|
+
}, [forced]);
|
|
82
|
+
// state 객체도 매 렌더 새로 만들지 않는다 — 위와 같은 이유다
|
|
83
|
+
const state = useMemo(() => ({
|
|
84
|
+
sorting,
|
|
85
|
+
columnVisibility,
|
|
86
|
+
pagination: search.params.pagination,
|
|
87
|
+
globalFilter: search.params.globalFilter,
|
|
88
|
+
grouping: search.params.grouping ?? EMPTY_GROUPING,
|
|
89
|
+
...tableOptions?.state,
|
|
90
|
+
}), [sorting, columnVisibility, search.params, tableOptions?.state]);
|
|
91
|
+
const table = useReactTable({
|
|
92
|
+
data: data.rows,
|
|
93
|
+
columns: columns,
|
|
94
|
+
getCoreRowModel: getCoreRowModel(),
|
|
95
|
+
// 그룹핑·확장은 `useGrouping` 이 따로 계산한다. TanStack 의 grouped/expanded
|
|
96
|
+
// row model 을 등록하면 쓰지도 않는 auto-reset 기계장치만 딸려온다 —
|
|
97
|
+
// 필요하면 호출부가 `tableOptions` 로 직접 넣는다.
|
|
98
|
+
// 2층이 이미 필터·정렬·페이지를 적용했다. 여기서 또 거르면 이중 적용이 된다.
|
|
99
|
+
manualFiltering: true,
|
|
100
|
+
manualSorting: true,
|
|
101
|
+
manualPagination: true,
|
|
102
|
+
rowCount: data.total,
|
|
103
|
+
...tableOptions,
|
|
104
|
+
state,
|
|
105
|
+
onSortingChange,
|
|
106
|
+
onColumnVisibilityChange,
|
|
107
|
+
});
|
|
108
|
+
return useMemo(() => Object.assign(table, { search, data, fieldErrors }), [table, search, data, fieldErrors]);
|
|
109
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TableColumnDefs } from "../types/column";
|
|
3
|
+
import type { ExpandState, FlatItem, GroupTreeNode } from "../types/grouping";
|
|
4
|
+
import type { AggregationDef, AggregationSpec } from "../client/aggregate";
|
|
5
|
+
import type { Registry } from "../registry";
|
|
6
|
+
export interface UseGroupingOptions<T extends RowData> {
|
|
7
|
+
rows: T[];
|
|
8
|
+
columns?: TableColumnDefs<T>;
|
|
9
|
+
/** 상위 → 하위 순서의 그룹 컬럼. 비어 있으면 그룹핑을 하지 않는다 */
|
|
10
|
+
grouping: string[];
|
|
11
|
+
aggregation?: AggregationSpec;
|
|
12
|
+
/** 집계 레지스트리 — 미지정 시 전역 `aggregations`. mui 는 `useAggregations()`(Provider) 결과를 넘긴다 */
|
|
13
|
+
aggregations?: Registry<AggregationDef>;
|
|
14
|
+
labelOf?: (columnId: string, value: unknown) => string | undefined;
|
|
15
|
+
/** 마운트 시 전부 펼침 */
|
|
16
|
+
initialExpanded?: boolean;
|
|
17
|
+
includeLeaves?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupingResult<T extends RowData> {
|
|
20
|
+
/** 그룹 트리 — 그룹핑이 꺼져 있으면 빈 배열 */
|
|
21
|
+
tree: Array<GroupTreeNode<T>>;
|
|
22
|
+
/** 화면 순서대로 평탄화된 항목 */
|
|
23
|
+
items: Array<FlatItem<T>>;
|
|
24
|
+
expand: ExpandState;
|
|
25
|
+
readonly enabled: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 클라이언트 그룹핑 — 이미 필터·정렬이 끝난 행을 트리로 묶는다.
|
|
29
|
+
*
|
|
30
|
+
* 서버 그룹핑(`groupExpansion` — 최상위 `path: []` / 경로 확장)과 결과 모양을 맞춰 두었기
|
|
31
|
+
* 때문에, 나중에 서버 모드를 붙여도 렌더러는 그대로 쓸 수 있다.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useGrouping<T extends RowData>(options: UseGroupingOptions<T>): GroupingResult<T>;
|