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,146 @@
|
|
|
1
|
+
const now = () => new Date().toISOString();
|
|
2
|
+
export function newPresetId() {
|
|
3
|
+
const c = globalThis.crypto;
|
|
4
|
+
if (c?.randomUUID)
|
|
5
|
+
return c.randomUUID();
|
|
6
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
7
|
+
}
|
|
8
|
+
/** 같은 표에서 기본 프리셋은 하나 — `id` 만 남기고 나머지의 `isDefault` 를 내린다 */
|
|
9
|
+
function applyDefault(list, tableKey, id) {
|
|
10
|
+
return list.map((p) => p.tableKey !== tableKey ? p : { ...p, isDefault: id !== null && p.id === id });
|
|
11
|
+
}
|
|
12
|
+
/** 목록에 하나를 넣거나 바꾼다 (id 기준). 새 항목이면 시각도 채운다 */
|
|
13
|
+
function upsert(list, input) {
|
|
14
|
+
const existing = input.id ? list.find((p) => p.id === input.id) : undefined;
|
|
15
|
+
const saved = {
|
|
16
|
+
...existing,
|
|
17
|
+
...input,
|
|
18
|
+
id: input.id ?? newPresetId(),
|
|
19
|
+
createdAt: existing?.createdAt ?? input.createdAt ?? now(),
|
|
20
|
+
updatedAt: now(),
|
|
21
|
+
};
|
|
22
|
+
let next = existing ? list.map((p) => (p.id === saved.id ? saved : p)) : [...list, saved];
|
|
23
|
+
if (saved.isDefault)
|
|
24
|
+
next = applyDefault(next, saved.tableKey, saved.id);
|
|
25
|
+
return { list: next, saved };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 메모리 저장소 — 테스트·스토리·"서버가 아직 없는" 화면용.
|
|
29
|
+
* 돌려주는 객체는 매번 복사본이라 호출부가 바꿔도 내부 상태가 오염되지 않는다.
|
|
30
|
+
*/
|
|
31
|
+
export function createMemoryPresetStore(options = {}) {
|
|
32
|
+
let list = (options.seed ?? []).map((p) => ({ ...p }));
|
|
33
|
+
const wait = (value) => options.delayMs ? new Promise((resolve) => setTimeout(() => resolve(value), options.delayMs)) : Promise.resolve(value);
|
|
34
|
+
const clone = (items) => items.map((p) => ({ ...p, state: { ...p.state } }));
|
|
35
|
+
return {
|
|
36
|
+
list: (tableKey) => wait(clone(list.filter((p) => p.tableKey === tableKey))),
|
|
37
|
+
save: (input) => {
|
|
38
|
+
const result = upsert(list, { ...input, state: { ...input.state } });
|
|
39
|
+
list = result.list;
|
|
40
|
+
return wait({ ...result.saved });
|
|
41
|
+
},
|
|
42
|
+
remove: (id) => {
|
|
43
|
+
list = list.filter((p) => p.id !== id);
|
|
44
|
+
return wait(undefined);
|
|
45
|
+
},
|
|
46
|
+
setDefault: (tableKey, id) => {
|
|
47
|
+
list = applyDefault(list, tableKey, id);
|
|
48
|
+
return wait(undefined);
|
|
49
|
+
},
|
|
50
|
+
dump: () => clone(list),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* localStorage 저장소 — 서버 없이 브라우저에만 남긴다.
|
|
55
|
+
* 접근 실패(사파리 프라이빗 모드·권한)는 삼키고 빈 목록으로 동작한다.
|
|
56
|
+
*/
|
|
57
|
+
export function createLocalStoragePresetStore(namespaceOrOptions = {}) {
|
|
58
|
+
const options = typeof namespaceOrOptions === "string" ? { namespace: namespaceOrOptions } : namespaceOrOptions;
|
|
59
|
+
const namespace = options.namespace ?? "koko-table:presets";
|
|
60
|
+
const storage = options.storage ?? globalThis.localStorage;
|
|
61
|
+
// storage 가 없거나 깨졌을 때의 폴백
|
|
62
|
+
const memory = new Map();
|
|
63
|
+
const keyOf = (tableKey) => `${namespace}:${tableKey}`;
|
|
64
|
+
const read = (tableKey) => {
|
|
65
|
+
if (!storage)
|
|
66
|
+
return memory.get(tableKey) ?? [];
|
|
67
|
+
try {
|
|
68
|
+
const raw = storage.getItem(keyOf(tableKey));
|
|
69
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
70
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return memory.get(tableKey) ?? [];
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const write = (tableKey, list) => {
|
|
77
|
+
memory.set(tableKey, list);
|
|
78
|
+
if (!storage)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
if (list.length === 0)
|
|
82
|
+
storage.removeItem(keyOf(tableKey));
|
|
83
|
+
else
|
|
84
|
+
storage.setItem(keyOf(tableKey), JSON.stringify(list));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// 저장이 막혀도 화면은 동작해야 한다 — 메모리 폴백이 이번 세션을 버틴다
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// remove 는 tableKey 를 받지 않는다 — id 로 어느 표인지 찾아야 하므로 id → tableKey 색인을 둔다
|
|
91
|
+
const indexKey = `${namespace}:__index`;
|
|
92
|
+
const readIndex = () => {
|
|
93
|
+
if (!storage)
|
|
94
|
+
return {};
|
|
95
|
+
try {
|
|
96
|
+
const raw = storage.getItem(indexKey);
|
|
97
|
+
return raw ? JSON.parse(raw) : {};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const writeIndex = (index) => {
|
|
104
|
+
if (!storage)
|
|
105
|
+
return;
|
|
106
|
+
try {
|
|
107
|
+
storage.setItem(indexKey, JSON.stringify(index));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// 위와 같다
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const memoryIndex = new Map();
|
|
114
|
+
return {
|
|
115
|
+
list: async (tableKey) => read(tableKey).map((p) => ({ ...p })),
|
|
116
|
+
save: async (input) => {
|
|
117
|
+
const result = upsert(read(input.tableKey), input);
|
|
118
|
+
write(input.tableKey, result.list);
|
|
119
|
+
memoryIndex.set(result.saved.id, input.tableKey);
|
|
120
|
+
writeIndex({ ...readIndex(), [result.saved.id]: input.tableKey });
|
|
121
|
+
return { ...result.saved };
|
|
122
|
+
},
|
|
123
|
+
remove: async (id) => {
|
|
124
|
+
const tableKey = memoryIndex.get(id) ?? readIndex()[id];
|
|
125
|
+
if (!tableKey)
|
|
126
|
+
return;
|
|
127
|
+
write(tableKey, read(tableKey).filter((p) => p.id !== id));
|
|
128
|
+
memoryIndex.delete(id);
|
|
129
|
+
const index = readIndex();
|
|
130
|
+
delete index[id];
|
|
131
|
+
writeIndex(index);
|
|
132
|
+
},
|
|
133
|
+
setDefault: async (tableKey, id) => {
|
|
134
|
+
write(tableKey, applyDefault(read(tableKey), tableKey, id));
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 기본 프리셋 하나를 미리 읽는다 — 마운트 시점에 `useTableSearch({ initial })` 의 초기값으로
|
|
140
|
+
* 쓰려면 훅보다 먼저(라우트 loader·상위 컴포넌트) 불러 `presetToSearchInitial(preset.state)` 를
|
|
141
|
+
* 넘긴다. 이미 마운트된 표에는 `usePresets({ autoApplyDefault: true })` 가 같은 일을 한다.
|
|
142
|
+
*/
|
|
143
|
+
export async function loadDefaultPreset(store, tableKey) {
|
|
144
|
+
const list = await store.list(tableKey);
|
|
145
|
+
return list.find((p) => p.isDefault) ?? null;
|
|
146
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { DataTable } from "../types/table";
|
|
3
|
+
import { type PresetDensity, type PresetState } from "./preset";
|
|
4
|
+
import type { PresetScope, PresetStore, TablePreset } from "./presetStore";
|
|
5
|
+
export interface UsePresetsOptions<T extends RowData> {
|
|
6
|
+
tableKey: string;
|
|
7
|
+
store: PresetStore;
|
|
8
|
+
/** 3층 인스턴스 — 있으면 `extractCurrent` / `apply` 가 표 상태를 읽고 쓴다 */
|
|
9
|
+
table?: DataTable<T>;
|
|
10
|
+
/** 표에 없는 density 상태 — 저장 시 실리고 적용 시 `setDensity` 로 돌아온다 */
|
|
11
|
+
density?: PresetDensity;
|
|
12
|
+
setDensity?: (density: PresetDensity) => void;
|
|
13
|
+
/** 첫 목록 로드 때 기본 프리셋을 자동 적용한다 (마운트 이후라 첫 조회가 한 번 더 나간다) */
|
|
14
|
+
autoApplyDefault?: boolean;
|
|
15
|
+
/** `table` 없이 쓰거나 적용 뒤 추가 작업이 필요할 때 — `apply(id)` 직후 불린다 */
|
|
16
|
+
onApply?: (preset: TablePreset) => void;
|
|
17
|
+
onError?: (error: unknown) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface SavePresetOptions {
|
|
20
|
+
scope?: PresetScope;
|
|
21
|
+
isDefault?: boolean;
|
|
22
|
+
/** 미지정이면 `extractCurrent()` */
|
|
23
|
+
state?: PresetState;
|
|
24
|
+
}
|
|
25
|
+
export type PresetPatch = Partial<Pick<TablePreset, "name" | "scope" | "isDefault" | "state">>;
|
|
26
|
+
export interface Presets {
|
|
27
|
+
presets: TablePreset[];
|
|
28
|
+
/** 마지막으로 적용(또는 저장)한 프리셋 id. 표 상태를 손으로 바꿔도 유지된다 — "출발점" 표시용 */
|
|
29
|
+
activeId: string | null;
|
|
30
|
+
activePreset: TablePreset | null;
|
|
31
|
+
defaultPreset: TablePreset | null;
|
|
32
|
+
/** 목록을 읽는 중 (첫 로드·refresh) */
|
|
33
|
+
loading: boolean;
|
|
34
|
+
/** 저장·삭제·기본 지정이 진행 중 — 버튼 비활성화용 */
|
|
35
|
+
busy: boolean;
|
|
36
|
+
error: unknown | null;
|
|
37
|
+
/** 현재 표 상태를 새 프리셋으로 저장하고 활성으로 표시한다 */
|
|
38
|
+
save(name: string, options?: SavePresetOptions): Promise<TablePreset>;
|
|
39
|
+
/** 이름·범위·기본·상태 일부를 바꾼다 */
|
|
40
|
+
update(id: string, patch: PresetPatch): Promise<TablePreset>;
|
|
41
|
+
/** "현재 상태를 덮어쓰기" — `update(id, { state: extractCurrent() })` */
|
|
42
|
+
overwrite(id: string): Promise<TablePreset>;
|
|
43
|
+
remove(id: string): Promise<void>;
|
|
44
|
+
/** 표에 적용 (동기 — 이미 읽어 둔 목록에서 찾는다) */
|
|
45
|
+
apply(id: string): void;
|
|
46
|
+
setDefault(id: string | null): Promise<void>;
|
|
47
|
+
refresh(): Promise<void>;
|
|
48
|
+
extractCurrent(): PresetState;
|
|
49
|
+
setActiveId(id: string | null): void;
|
|
50
|
+
}
|
|
51
|
+
export declare function usePresets<T extends RowData>(options: UsePresetsOptions<T>): Presets;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 프리셋 훅 — `PresetStore` 를 붙여 목록·저장·적용·기본 지정을 한 곳에서 한다.
|
|
3
|
+
*
|
|
4
|
+
* react-query 에 의존하지 않는다(useState/useEffect 만). ERP 의 `usePreset` 은 react-query
|
|
5
|
+
* 캐시 무효화로 목록을 다시 받았는데, 여기서는 각 조작이 끝난 뒤 `store.list` 를 다시 읽어
|
|
6
|
+
* 같은 효과를 낸다 — 서버가 채운 id·시각·기본 플래그가 그대로 화면에 온다.
|
|
7
|
+
*
|
|
8
|
+
* 적용은 `applyPresetState(table, state, { setDensity })` 그대로다. `table` 이 없으면
|
|
9
|
+
* (검색 상태만 있는 카드 화면 등) `activeId` 만 바뀌고 실제 적용은 `onApply` 가 한다.
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { applyPresetState, extractPresetState } from "./preset";
|
|
13
|
+
export function usePresets(options) {
|
|
14
|
+
const { tableKey, store, table, density, setDensity, autoApplyDefault = false, onApply, onError } = options;
|
|
15
|
+
const [presets, setPresets] = useState([]);
|
|
16
|
+
const [activeId, setActiveId] = useState(null);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [busy, setBusy] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
// 최신 값을 콜백 안에서 읽는다 — 의존성 배열이 매 렌더 바뀌어 effect 가 재실행되는 것을 막는다
|
|
21
|
+
const latest = useRef({ table, density, setDensity, onApply, onError, store });
|
|
22
|
+
latest.current = { table, density, setDensity, onApply, onError, store };
|
|
23
|
+
const mounted = useRef(true);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
mounted.current = true;
|
|
26
|
+
return () => { mounted.current = false; };
|
|
27
|
+
}, []);
|
|
28
|
+
// 늦게 도착한 옛 응답이 새 목록을 덮지 않도록 요청 번호를 매긴다
|
|
29
|
+
const requestSeq = useRef(0);
|
|
30
|
+
const fail = useCallback((err) => {
|
|
31
|
+
if (!mounted.current)
|
|
32
|
+
return;
|
|
33
|
+
setError(err);
|
|
34
|
+
latest.current.onError?.(err);
|
|
35
|
+
}, []);
|
|
36
|
+
const load = useCallback(async (opts = {}) => {
|
|
37
|
+
const seq = ++requestSeq.current;
|
|
38
|
+
if (!opts.silent)
|
|
39
|
+
setLoading(true);
|
|
40
|
+
try {
|
|
41
|
+
const list = await latest.current.store.list(tableKey);
|
|
42
|
+
if (!mounted.current || seq !== requestSeq.current)
|
|
43
|
+
return;
|
|
44
|
+
setPresets(list);
|
|
45
|
+
setError(null);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (seq === requestSeq.current)
|
|
49
|
+
fail(err);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (mounted.current && seq === requestSeq.current)
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}, [tableKey, fail]);
|
|
56
|
+
// tableKey 나 store 가 바뀌면 다시 읽는다
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
void load();
|
|
59
|
+
}, [load, store]);
|
|
60
|
+
const extractCurrent = useCallback(() => {
|
|
61
|
+
const { table: t, density: d } = latest.current;
|
|
62
|
+
if (!t)
|
|
63
|
+
return {};
|
|
64
|
+
return extractPresetState(t, d !== undefined ? { density: d } : {});
|
|
65
|
+
}, []);
|
|
66
|
+
const applyPreset = useCallback((preset) => {
|
|
67
|
+
const { table: t, setDensity: sd, onApply: cb } = latest.current;
|
|
68
|
+
if (t)
|
|
69
|
+
applyPresetState(t, preset.state, sd ? { setDensity: sd } : {});
|
|
70
|
+
setActiveId(preset.id);
|
|
71
|
+
cb?.(preset);
|
|
72
|
+
}, []);
|
|
73
|
+
// 첫 로드 뒤 기본 프리셋 자동 적용 — 한 번만
|
|
74
|
+
const autoApplied = useRef(false);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!autoApplyDefault || loading || autoApplied.current)
|
|
77
|
+
return;
|
|
78
|
+
const preset = presets.find((p) => p.isDefault);
|
|
79
|
+
if (!preset)
|
|
80
|
+
return;
|
|
81
|
+
autoApplied.current = true;
|
|
82
|
+
applyPreset(preset);
|
|
83
|
+
}, [autoApplyDefault, loading, presets, applyPreset]);
|
|
84
|
+
const run = useCallback(async (work) => {
|
|
85
|
+
setBusy(true);
|
|
86
|
+
try {
|
|
87
|
+
const result = await work();
|
|
88
|
+
await load({ silent: true });
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
fail(err);
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (mounted.current)
|
|
97
|
+
setBusy(false);
|
|
98
|
+
}
|
|
99
|
+
}, [load, fail]);
|
|
100
|
+
const save = useCallback((name, opts = {}) => run(async () => {
|
|
101
|
+
const input = {
|
|
102
|
+
tableKey,
|
|
103
|
+
name,
|
|
104
|
+
state: opts.state ?? extractCurrent(),
|
|
105
|
+
scope: opts.scope ?? "user",
|
|
106
|
+
isDefault: opts.isDefault ?? false,
|
|
107
|
+
};
|
|
108
|
+
const saved = await latest.current.store.save(input);
|
|
109
|
+
if (mounted.current)
|
|
110
|
+
setActiveId(saved.id);
|
|
111
|
+
return saved;
|
|
112
|
+
}), [run, tableKey, extractCurrent]);
|
|
113
|
+
const setDefault = useCallback((id) => run(async () => {
|
|
114
|
+
const s = latest.current.store;
|
|
115
|
+
if (s.setDefault) {
|
|
116
|
+
await s.setDefault(tableKey, id);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// setDefault 가 없는 저장소 — 표의 기본 플래그를 save 로 하나씩 맞춘다
|
|
120
|
+
const current = await s.list(tableKey);
|
|
121
|
+
for (const p of current) {
|
|
122
|
+
const shouldBe = id !== null && p.id === id;
|
|
123
|
+
if ((p.isDefault ?? false) !== shouldBe)
|
|
124
|
+
await s.save({ ...p, isDefault: shouldBe });
|
|
125
|
+
}
|
|
126
|
+
}), [run, tableKey]);
|
|
127
|
+
const update = useCallback((id, patch) => run(async () => {
|
|
128
|
+
const s = latest.current.store;
|
|
129
|
+
const current = presets.find((p) => p.id === id) ?? (await s.list(tableKey)).find((p) => p.id === id);
|
|
130
|
+
if (!current)
|
|
131
|
+
throw new Error(`[koko-table] 프리셋을 찾을 수 없습니다: ${id}`);
|
|
132
|
+
const { isDefault, ...rest } = patch;
|
|
133
|
+
let saved = current;
|
|
134
|
+
if (Object.keys(rest).length > 0)
|
|
135
|
+
saved = await s.save({ ...current, ...rest });
|
|
136
|
+
if (isDefault !== undefined && isDefault !== (current.isDefault ?? false)) {
|
|
137
|
+
if (s.setDefault)
|
|
138
|
+
await s.setDefault(tableKey, isDefault ? id : null);
|
|
139
|
+
else
|
|
140
|
+
saved = await s.save({ ...saved, isDefault });
|
|
141
|
+
}
|
|
142
|
+
return { ...saved, ...(isDefault !== undefined ? { isDefault } : {}) };
|
|
143
|
+
}), [run, presets, tableKey]);
|
|
144
|
+
const overwrite = useCallback((id) => update(id, { state: extractCurrent() }).then((saved) => {
|
|
145
|
+
if (mounted.current)
|
|
146
|
+
setActiveId(saved.id);
|
|
147
|
+
return saved;
|
|
148
|
+
}), [update, extractCurrent]);
|
|
149
|
+
const remove = useCallback((id) => run(async () => {
|
|
150
|
+
await latest.current.store.remove(id);
|
|
151
|
+
if (mounted.current)
|
|
152
|
+
setActiveId((prev) => (prev === id ? null : prev));
|
|
153
|
+
}), [run]);
|
|
154
|
+
const apply = useCallback((id) => {
|
|
155
|
+
const preset = presets.find((p) => p.id === id);
|
|
156
|
+
if (!preset)
|
|
157
|
+
return;
|
|
158
|
+
applyPreset(preset);
|
|
159
|
+
}, [presets, applyPreset]);
|
|
160
|
+
const refresh = useCallback(() => load(), [load]);
|
|
161
|
+
const activePreset = useMemo(() => presets.find((p) => p.id === activeId) ?? null, [presets, activeId]);
|
|
162
|
+
const defaultPreset = useMemo(() => presets.find((p) => p.isDefault) ?? null, [presets]);
|
|
163
|
+
return useMemo(() => ({
|
|
164
|
+
presets, activeId, activePreset, defaultPreset, loading, busy, error,
|
|
165
|
+
save, update, overwrite, remove, apply, setDefault, refresh, extractCurrent, setActiveId,
|
|
166
|
+
}), [
|
|
167
|
+
presets, activeId, activePreset, defaultPreset, loading, busy, error,
|
|
168
|
+
save, update, overwrite, remove, apply, setDefault, refresh, extractCurrent,
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Pagination } from "./types/request";
|
|
2
|
+
/** "전체" 페이지 크기 — 서버가 페이지네이션을 사실상 끄는 값 */
|
|
3
|
+
export declare const PAGE_SIZE_ALL = 999999;
|
|
4
|
+
export declare const DEFAULT_PAGE_SIZE = 30;
|
|
5
|
+
export declare const DEFAULT_PAGINATION: Pagination;
|
|
6
|
+
export declare const DEFAULT_GROUP_BATCH_SIZE = 50;
|
|
7
|
+
export declare const DEFAULT_TABLE_STALE_TIME: number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** "전체" 페이지 크기 — 서버가 페이지네이션을 사실상 끄는 값 */
|
|
2
|
+
export const PAGE_SIZE_ALL = 999999;
|
|
3
|
+
export const DEFAULT_PAGE_SIZE = 30;
|
|
4
|
+
export const DEFAULT_PAGINATION = {
|
|
5
|
+
pageIndex: 0,
|
|
6
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7
|
+
};
|
|
8
|
+
export const DEFAULT_GROUP_BATCH_SIZE = 50;
|
|
9
|
+
export const DEFAULT_TABLE_STALE_TIME = 5 * 60_000;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kind codec 연결 — URL·프리셋에 실리는 필터 값의 encode/decode.
|
|
3
|
+
*
|
|
4
|
+
* kind 가 `codec` 을 선언한 컬럼만 바꾸고 나머지는 그대로 둔다. 바뀐 것이 없으면
|
|
5
|
+
* 같은 배열 참조를 돌려주므로 호출부가 메모이즈 근거로 써도 된다.
|
|
6
|
+
*/
|
|
7
|
+
import type { RowData } from "@tanstack/react-table";
|
|
8
|
+
import type { TableColumnDefs } from "../types/column";
|
|
9
|
+
import type { ColumnFilterEntry } from "../types/request";
|
|
10
|
+
import type { AnyFilterKind } from "../types/filterKind";
|
|
11
|
+
import { type FilterKindStore } from "./kind";
|
|
12
|
+
/** 컬럼 id → 해석된 kind (kind 없는 컬럼은 빠진다) */
|
|
13
|
+
export declare function filterKindMap<T extends RowData>(columns: TableColumnDefs<T> | undefined, store?: FilterKindStore): Map<string, AnyFilterKind>;
|
|
14
|
+
/** 저장용(JSON 안전) 값으로 — `kind.codec.encode` */
|
|
15
|
+
export declare function encodeFilterValues<T extends RowData>(columns: TableColumnDefs<T> | undefined, filters: ColumnFilterEntry[], store?: FilterKindStore): ColumnFilterEntry[];
|
|
16
|
+
/**
|
|
17
|
+
* 저장된 값 → 필터 값 — `kind.codec.decode`. decode 가 `undefined` 를 돌려주면(읽을 수 없는 값)
|
|
18
|
+
* 그 항목을 뺀다 — 잘못된 값이 조건으로 남아 조용히 0건이 되는 것보다 낫다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodeFilterValues<T extends RowData>(columns: TableColumnDefs<T> | undefined, filters: ColumnFilterEntry[], store?: FilterKindStore): ColumnFilterEntry[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { columnKey } from "../columns/deriveOperators";
|
|
2
|
+
import { resolveFilterKind } from "./kind";
|
|
3
|
+
/** 컬럼 id → 해석된 kind (kind 없는 컬럼은 빠진다) */
|
|
4
|
+
export function filterKindMap(columns, store) {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
for (const column of columns ?? []) {
|
|
7
|
+
const id = columnKey(column);
|
|
8
|
+
const kind = resolveFilterKind(column, store);
|
|
9
|
+
if (id && kind)
|
|
10
|
+
map.set(id, kind);
|
|
11
|
+
}
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
/** 저장용(JSON 안전) 값으로 — `kind.codec.encode` */
|
|
15
|
+
export function encodeFilterValues(columns, filters, store) {
|
|
16
|
+
const kinds = filterKindMap(columns, store);
|
|
17
|
+
if (kinds.size === 0)
|
|
18
|
+
return filters;
|
|
19
|
+
let changed = false;
|
|
20
|
+
const result = filters.map((entry) => {
|
|
21
|
+
const codec = entry ? kinds.get(entry.id)?.codec : undefined;
|
|
22
|
+
if (!codec)
|
|
23
|
+
return entry;
|
|
24
|
+
const value = codec.encode(entry.value);
|
|
25
|
+
if (value === entry.value)
|
|
26
|
+
return entry;
|
|
27
|
+
changed = true;
|
|
28
|
+
return { id: entry.id, value };
|
|
29
|
+
});
|
|
30
|
+
return changed ? result : filters;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 저장된 값 → 필터 값 — `kind.codec.decode`. decode 가 `undefined` 를 돌려주면(읽을 수 없는 값)
|
|
34
|
+
* 그 항목을 뺀다 — 잘못된 값이 조건으로 남아 조용히 0건이 되는 것보다 낫다.
|
|
35
|
+
*/
|
|
36
|
+
export function decodeFilterValues(columns, filters, store) {
|
|
37
|
+
const kinds = filterKindMap(columns, store);
|
|
38
|
+
if (kinds.size === 0)
|
|
39
|
+
return filters;
|
|
40
|
+
let changed = false;
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const entry of filters) {
|
|
43
|
+
const codec = entry ? kinds.get(entry.id)?.codec : undefined;
|
|
44
|
+
if (!codec) {
|
|
45
|
+
result.push(entry);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const value = codec.decode(entry.value);
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
changed = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (value === entry.value) {
|
|
54
|
+
result.push(entry);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
changed = true;
|
|
58
|
+
result.push({ id: entry.id, value });
|
|
59
|
+
}
|
|
60
|
+
return changed ? result : filters;
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 날짜 필터 — 백엔드 `DateFilter` 의 **반열림 경계 규칙**을 그대로 옮긴다.
|
|
3
|
+
*
|
|
4
|
+
* `"2025-01-31"` 과 `"2025-01-31T00:00:00"` 은 다른 뜻이다. 전자는 "그 날",
|
|
5
|
+
* 후자는 "그 날 자정". 프론트는 전자를 보낸다. 이 구분이 없으면
|
|
6
|
+
* `<= 2025-01-31 00:00` 이 되어 **마지막 날이 통째로 빠진다**
|
|
7
|
+
* (백엔드 주석의 실측: 6,963행 중 684행 = 9.8% 유실).
|
|
8
|
+
*
|
|
9
|
+
* 기존 클라이언트 `dateBetween` 은 닫힌 구간이라 서버와 결과가 달랐다.
|
|
10
|
+
*/
|
|
11
|
+
export interface ParsedDate {
|
|
12
|
+
date: Date;
|
|
13
|
+
dateOnly: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseDateValue(value: unknown): ParsedDate | null;
|
|
16
|
+
/** 그 날 00:00 의 다음 날 00:00 */
|
|
17
|
+
export declare function dayAfter(date: Date): Date;
|
|
18
|
+
/** 단일 비교 — 백엔드 `DateFilter._single` */
|
|
19
|
+
export declare function matchDateSingle(cellValue: unknown, filterValue: unknown, operator: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 구간 비교 — 백엔드 `DateFilter._between`.
|
|
22
|
+
* 끝값이 날짜만이면 경계가 하루 단위로 움직이고, 하한과 상한이 **대칭**으로 움직인다.
|
|
23
|
+
*/
|
|
24
|
+
export declare function matchDateBetween(cellValue: unknown, filterValue: unknown, operator: "between" | "betweenInclusive" | "dateBetween"): boolean;
|
|
25
|
+
export declare const PERIODS: Record<string, (now: Date) => [Date, Date]>;
|
|
26
|
+
export declare const PERIOD_TOKENS: string[];
|
|
27
|
+
/** 토큰 → 반열림 [start, end). 모르는 토큰이면 null */
|
|
28
|
+
export declare function resolvePeriod(token: string, now?: Date): [Date, Date] | null;
|
|
29
|
+
export declare function matchInPeriod(cellValue: unknown, filterValue: unknown, now?: Date): boolean;
|
|
30
|
+
export declare function matchDate(cellValue: unknown, filterValue: unknown, operator: string, now?: Date): boolean;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 날짜 필터 — 백엔드 `DateFilter` 의 **반열림 경계 규칙**을 그대로 옮긴다.
|
|
3
|
+
*
|
|
4
|
+
* `"2025-01-31"` 과 `"2025-01-31T00:00:00"` 은 다른 뜻이다. 전자는 "그 날",
|
|
5
|
+
* 후자는 "그 날 자정". 프론트는 전자를 보낸다. 이 구분이 없으면
|
|
6
|
+
* `<= 2025-01-31 00:00` 이 되어 **마지막 날이 통째로 빠진다**
|
|
7
|
+
* (백엔드 주석의 실측: 6,963행 중 684행 = 9.8% 유실).
|
|
8
|
+
*
|
|
9
|
+
* 기존 클라이언트 `dateBetween` 은 닫힌 구간이라 서버와 결과가 달랐다.
|
|
10
|
+
*/
|
|
11
|
+
/** 시각 표기가 없으면 "날짜만" — 백엔드 `_TIME_MARKS` 와 같은 판정 */
|
|
12
|
+
const TIME_MARKS = ["T", " ", ":"];
|
|
13
|
+
export function parseDateValue(value) {
|
|
14
|
+
if (value instanceof Date) {
|
|
15
|
+
return Number.isNaN(value.getTime()) ? null : { date: value, dateOnly: false };
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "number") {
|
|
18
|
+
const d = new Date(value);
|
|
19
|
+
return Number.isNaN(d.getTime()) ? null : { date: d, dateOnly: false };
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== "string" || value === "")
|
|
22
|
+
return null;
|
|
23
|
+
const dateOnly = !TIME_MARKS.some((mark) => value.includes(mark));
|
|
24
|
+
// 날짜만인 값은 **로컬 자정**으로 읽는다. new Date("2025-01-31") 은 UTC 자정이라
|
|
25
|
+
// KST 에서 하루 밀린다 — 백엔드 datetime.fromisoformat 은 로컬 naive 다.
|
|
26
|
+
if (dateOnly) {
|
|
27
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
28
|
+
if (match) {
|
|
29
|
+
const [, y, m, d] = match;
|
|
30
|
+
return { date: new Date(Number(y), Number(m) - 1, Number(d)), dateOnly: true };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const parsed = new Date(value.replace("Z", "+00:00"));
|
|
34
|
+
return Number.isNaN(parsed.getTime()) ? null : { date: parsed, dateOnly };
|
|
35
|
+
}
|
|
36
|
+
/** 그 날 00:00 의 다음 날 00:00 */
|
|
37
|
+
export function dayAfter(date) {
|
|
38
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
39
|
+
d.setDate(d.getDate() + 1);
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
function cellDate(cellValue) {
|
|
43
|
+
if (cellValue == null)
|
|
44
|
+
return null;
|
|
45
|
+
const parsed = parseDateValue(cellValue);
|
|
46
|
+
return parsed?.date ?? null;
|
|
47
|
+
}
|
|
48
|
+
/** 단일 비교 — 백엔드 `DateFilter._single` */
|
|
49
|
+
export function matchDateSingle(cellValue, filterValue, operator) {
|
|
50
|
+
const bound = parseDateValue(filterValue);
|
|
51
|
+
const cell = cellDate(cellValue);
|
|
52
|
+
if (cell === null)
|
|
53
|
+
return false;
|
|
54
|
+
if (bound === null)
|
|
55
|
+
return false;
|
|
56
|
+
const dt = bound.date.getTime();
|
|
57
|
+
const nxt = bound.dateOnly ? dayAfter(bound.date).getTime() : dt;
|
|
58
|
+
const c = cell.getTime();
|
|
59
|
+
switch (operator) {
|
|
60
|
+
case "notEquals":
|
|
61
|
+
return bound.dateOnly ? c < dt || c >= nxt : c !== dt;
|
|
62
|
+
case "lessThan":
|
|
63
|
+
return c < dt;
|
|
64
|
+
case "lessThanOrEqualTo":
|
|
65
|
+
// "1/31 까지" = 1/31 을 포함 = 2/1 미만
|
|
66
|
+
return bound.dateOnly ? c < nxt : c <= dt;
|
|
67
|
+
case "greaterThan":
|
|
68
|
+
// "1/31 초과" = 1/31 을 제외 = 2/1 이상
|
|
69
|
+
return bound.dateOnly ? c >= nxt : c > dt;
|
|
70
|
+
case "greaterThanOrEqualTo":
|
|
71
|
+
return c >= dt;
|
|
72
|
+
case "equals":
|
|
73
|
+
default:
|
|
74
|
+
return bound.dateOnly ? c >= dt && c < nxt : c === dt;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 구간 비교 — 백엔드 `DateFilter._between`.
|
|
79
|
+
* 끝값이 날짜만이면 경계가 하루 단위로 움직이고, 하한과 상한이 **대칭**으로 움직인다.
|
|
80
|
+
*/
|
|
81
|
+
export function matchDateBetween(cellValue, filterValue, operator) {
|
|
82
|
+
if (!Array.isArray(filterValue) || filterValue.length !== 2)
|
|
83
|
+
return true;
|
|
84
|
+
const [rawLo, rawHi] = filterValue;
|
|
85
|
+
const lo = rawLo == null || rawLo === "" ? null : parseDateValue(rawLo);
|
|
86
|
+
const hi = rawHi == null || rawHi === "" ? null : parseDateValue(rawHi);
|
|
87
|
+
if (lo === null && hi === null)
|
|
88
|
+
return true;
|
|
89
|
+
const cell = cellDate(cellValue);
|
|
90
|
+
if (cell === null)
|
|
91
|
+
return false;
|
|
92
|
+
const c = cell.getTime();
|
|
93
|
+
const open = operator === "between";
|
|
94
|
+
if (lo !== null) {
|
|
95
|
+
if (!open) {
|
|
96
|
+
if (c < lo.date.getTime())
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
else if (lo.dateOnly) {
|
|
100
|
+
if (c < dayAfter(lo.date).getTime())
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
else if (c <= lo.date.getTime()) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (hi !== null) {
|
|
108
|
+
if (!open) {
|
|
109
|
+
const limit = hi.dateOnly ? dayAfter(hi.date).getTime() : hi.date.getTime();
|
|
110
|
+
if (hi.dateOnly ? c >= limit : c > limit)
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
else if (c >= hi.date.getTime()) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
// ─────────────────────────────────────────────────────────────
|
|
120
|
+
// 상대 기간 토큰 — 백엔드 period.py 와 1:1
|
|
121
|
+
// ─────────────────────────────────────────────────────────────
|
|
122
|
+
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
123
|
+
const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
|
|
124
|
+
const addMonths = (d, n) => new Date(d.getFullYear(), d.getMonth() + n, 1);
|
|
125
|
+
const startOfQuarter = (d) => new Date(d.getFullYear(), Math.floor(d.getMonth() / 3) * 3, 1);
|
|
126
|
+
const startOfYear = (d) => new Date(d.getFullYear(), 0, 1);
|
|
127
|
+
const addDays = (d, n) => {
|
|
128
|
+
const next = new Date(d);
|
|
129
|
+
next.setDate(next.getDate() + n);
|
|
130
|
+
return next;
|
|
131
|
+
};
|
|
132
|
+
/** ISO 주 시작(월요일) */
|
|
133
|
+
const startOfWeek = (d) => {
|
|
134
|
+
const day = startOfDay(d);
|
|
135
|
+
const weekday = (day.getDay() + 6) % 7; // 월=0
|
|
136
|
+
return addDays(day, -weekday);
|
|
137
|
+
};
|
|
138
|
+
const trailingDays = (n) => (now) => [addDays(startOfDay(now), -(n - 1)), addDays(startOfDay(now), 1)];
|
|
139
|
+
const trailingMonths = (n) => (now) => [addMonths(startOfMonth(now), -(n - 1)), addMonths(startOfMonth(now), 1)];
|
|
140
|
+
export const PERIODS = {
|
|
141
|
+
today: (now) => [startOfDay(now), addDays(startOfDay(now), 1)],
|
|
142
|
+
yesterday: (now) => [addDays(startOfDay(now), -1), startOfDay(now)],
|
|
143
|
+
thisWeek: (now) => [startOfWeek(now), addDays(startOfWeek(now), 7)],
|
|
144
|
+
lastWeek: (now) => [addDays(startOfWeek(now), -7), startOfWeek(now)],
|
|
145
|
+
thisMonth: (now) => [startOfMonth(now), addMonths(startOfMonth(now), 1)],
|
|
146
|
+
lastMonth: (now) => [addMonths(startOfMonth(now), -1), startOfMonth(now)],
|
|
147
|
+
thisQuarter: (now) => [startOfQuarter(now), addMonths(startOfQuarter(now), 3)],
|
|
148
|
+
lastQuarter: (now) => [addMonths(startOfQuarter(now), -3), startOfQuarter(now)],
|
|
149
|
+
thisYear: (now) => [startOfYear(now), new Date(now.getFullYear() + 1, 0, 1)],
|
|
150
|
+
lastYear: (now) => [new Date(now.getFullYear() - 1, 0, 1), startOfYear(now)],
|
|
151
|
+
last7days: trailingDays(7),
|
|
152
|
+
last30days: trailingDays(30),
|
|
153
|
+
last90days: trailingDays(90),
|
|
154
|
+
last12months: trailingMonths(12),
|
|
155
|
+
};
|
|
156
|
+
export const PERIOD_TOKENS = Object.keys(PERIODS);
|
|
157
|
+
/** 토큰 → 반열림 [start, end). 모르는 토큰이면 null */
|
|
158
|
+
export function resolvePeriod(token, now = new Date()) {
|
|
159
|
+
const resolver = PERIODS[token];
|
|
160
|
+
return resolver ? resolver(now) : null;
|
|
161
|
+
}
|
|
162
|
+
export function matchInPeriod(cellValue, filterValue, now) {
|
|
163
|
+
const range = resolvePeriod(String(filterValue), now ?? new Date());
|
|
164
|
+
if (range === null)
|
|
165
|
+
return true; // 모르는 토큰은 서버가 400 — 클라이언트는 필터를 무시한다
|
|
166
|
+
const cell = cellDate(cellValue);
|
|
167
|
+
if (cell === null)
|
|
168
|
+
return false;
|
|
169
|
+
const c = cell.getTime();
|
|
170
|
+
return c >= range[0].getTime() && c < range[1].getTime();
|
|
171
|
+
}
|
|
172
|
+
export function matchDate(cellValue, filterValue, operator, now) {
|
|
173
|
+
if (operator === "inPeriod")
|
|
174
|
+
return matchInPeriod(cellValue, filterValue, now);
|
|
175
|
+
if (operator === "between" || operator === "betweenInclusive" || operator === "dateBetween") {
|
|
176
|
+
return matchDateBetween(cellValue, filterValue, operator);
|
|
177
|
+
}
|
|
178
|
+
return matchDateSingle(cellValue, filterValue, operator);
|
|
179
|
+
}
|