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,111 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { validateExpression, } from "./expression";
|
|
3
|
+
const DEFAULT_MAX_SLOTS = 10;
|
|
4
|
+
const DEFAULT_PREFIX = "vc_";
|
|
5
|
+
/** 슬롯 목록 `vc_1 … vc_N` (기존 ERP 규칙) */
|
|
6
|
+
export function virtualColumnSlots(maxSlots = DEFAULT_MAX_SLOTS, prefix = DEFAULT_PREFIX) {
|
|
7
|
+
return Array.from({ length: maxSlots }, (_, i) => `${prefix}${i + 1}`);
|
|
8
|
+
}
|
|
9
|
+
function slotOrder(slot, prefix) {
|
|
10
|
+
const n = Number(slot.slice(prefix.length));
|
|
11
|
+
return Number.isFinite(n) ? n : Number.MAX_SAFE_INTEGER;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 가상 열 상태 — `SearchState.virtualColumns` 위에서 CRUD 를 제공한다.
|
|
15
|
+
*
|
|
16
|
+
* 기존 ERP `useVirtualColumns` 는 목록을 로컬 state 로 들고 `virtualColumnsForRequest` 를
|
|
17
|
+
* 호출부가 요청에 직접 얹었다. 여기서는 검색 상태가 **원본**이다 — `search.request.virtualColumns`
|
|
18
|
+
* 로 자동으로 실리고, URL 동기화·프리셋·reset 도 같은 경로를 탄다. 이름만 로컬이다
|
|
19
|
+
* (요청 계약 `VirtualColumnSpec` 에 이름 자리가 없다).
|
|
20
|
+
*/
|
|
21
|
+
export function useVirtualColumns(search, options = {}) {
|
|
22
|
+
const { applyImmediately = true, maxSlots = DEFAULT_MAX_SLOTS, slotPrefix = DEFAULT_PREFIX, fields = [], initialNames, } = options;
|
|
23
|
+
const [names, setNames] = useState(() => ({ ...initialNames }));
|
|
24
|
+
const source = applyImmediately ? search.params : search.draft;
|
|
25
|
+
const spec = source.virtualColumns;
|
|
26
|
+
const columns = useMemo(() => {
|
|
27
|
+
if (!spec)
|
|
28
|
+
return [];
|
|
29
|
+
return Object.entries(spec)
|
|
30
|
+
.sort(([a], [b]) => slotOrder(a, slotPrefix) - slotOrder(b, slotPrefix))
|
|
31
|
+
.map(([slotKey, value]) => ({
|
|
32
|
+
slotKey,
|
|
33
|
+
name: names[slotKey] ?? slotKey,
|
|
34
|
+
expr: value.expr,
|
|
35
|
+
type: value.type ?? "number",
|
|
36
|
+
}));
|
|
37
|
+
}, [spec, names, slotPrefix]);
|
|
38
|
+
const nextSlot = useMemo(() => {
|
|
39
|
+
const used = new Set(Object.keys(spec ?? {}));
|
|
40
|
+
return virtualColumnSlots(maxSlots, slotPrefix).find((slot) => !used.has(slot)) ?? null;
|
|
41
|
+
}, [spec, maxSlots, slotPrefix]);
|
|
42
|
+
const write = useCallback((next) => {
|
|
43
|
+
const value = Object.keys(next).length > 0 ? next : undefined;
|
|
44
|
+
if (applyImmediately)
|
|
45
|
+
search.patchParams({ virtualColumns: value });
|
|
46
|
+
else
|
|
47
|
+
search.patchDraft({ virtualColumns: value });
|
|
48
|
+
}, [applyImmediately, search]);
|
|
49
|
+
const validate = useCallback((column) => {
|
|
50
|
+
if (!column.name.trim())
|
|
51
|
+
return { ok: false, message: "이름을 입력하세요." };
|
|
52
|
+
return validateExpression(column.expr, { type: column.type, fields: fields.length > 0 ? fields : undefined });
|
|
53
|
+
}, [fields]);
|
|
54
|
+
const add = useCallback((column) => {
|
|
55
|
+
const check = validate(column);
|
|
56
|
+
if (!check.ok)
|
|
57
|
+
return check;
|
|
58
|
+
const current = (applyImmediately ? search.params : search.draft).virtualColumns ?? {};
|
|
59
|
+
const used = new Set(Object.keys(current));
|
|
60
|
+
const slot = virtualColumnSlots(maxSlots, slotPrefix).find((s) => !used.has(s));
|
|
61
|
+
if (!slot)
|
|
62
|
+
return { ok: false, message: `가상 열은 최대 ${maxSlots}개까지 만들 수 있습니다.` };
|
|
63
|
+
setNames((prev) => ({ ...prev, [slot]: column.name.trim() }));
|
|
64
|
+
write({ ...current, [slot]: { expr: column.expr.trim(), type: column.type } });
|
|
65
|
+
return { ok: true, slotKey: slot };
|
|
66
|
+
}, [validate, applyImmediately, search, maxSlots, slotPrefix, write]);
|
|
67
|
+
const update = useCallback((slotKey, patch) => {
|
|
68
|
+
const current = (applyImmediately ? search.params : search.draft).virtualColumns ?? {};
|
|
69
|
+
const existing = current[slotKey];
|
|
70
|
+
if (!existing)
|
|
71
|
+
return { ok: false, message: `'${slotKey}' 슬롯이 없습니다.` };
|
|
72
|
+
const merged = {
|
|
73
|
+
name: patch.name ?? names[slotKey] ?? slotKey,
|
|
74
|
+
expr: patch.expr ?? existing.expr,
|
|
75
|
+
type: patch.type ?? existing.type ?? "number",
|
|
76
|
+
};
|
|
77
|
+
const check = validate(merged);
|
|
78
|
+
if (!check.ok)
|
|
79
|
+
return check;
|
|
80
|
+
if (patch.name !== undefined)
|
|
81
|
+
setNames((prev) => ({ ...prev, [slotKey]: merged.name.trim() }));
|
|
82
|
+
write({ ...current, [slotKey]: { expr: merged.expr.trim(), type: merged.type } });
|
|
83
|
+
return { ok: true, slotKey };
|
|
84
|
+
}, [applyImmediately, search, names, validate, write]);
|
|
85
|
+
const remove = useCallback((slotKey) => {
|
|
86
|
+
const current = (applyImmediately ? search.params : search.draft).virtualColumns ?? {};
|
|
87
|
+
if (!(slotKey in current))
|
|
88
|
+
return;
|
|
89
|
+
const { [slotKey]: _removed, ...rest } = current;
|
|
90
|
+
setNames((prev) => {
|
|
91
|
+
const { [slotKey]: _name, ...others } = prev;
|
|
92
|
+
return others;
|
|
93
|
+
});
|
|
94
|
+
write(rest);
|
|
95
|
+
}, [applyImmediately, search, write]);
|
|
96
|
+
const request = useMemo(() => search.request.virtualColumns ?? {}, [search.request.virtualColumns]);
|
|
97
|
+
return useMemo(() => ({
|
|
98
|
+
search,
|
|
99
|
+
columns,
|
|
100
|
+
add,
|
|
101
|
+
update,
|
|
102
|
+
remove,
|
|
103
|
+
validate,
|
|
104
|
+
request,
|
|
105
|
+
isMaxReached: nextSlot === null,
|
|
106
|
+
nextSlot,
|
|
107
|
+
applyImmediately,
|
|
108
|
+
names,
|
|
109
|
+
fields,
|
|
110
|
+
}), [search, columns, add, update, remove, validate, request, nextSlot, applyImmediately, names, fields]);
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TableColumnDefs, VirtualColumnSpec } from "../../../core/src/index";
|
|
3
|
+
import type { VirtualColumnField } from "./expression";
|
|
4
|
+
import type { VirtualColumn } from "./useVirtualColumns";
|
|
5
|
+
export interface VirtualColumnDefsOptions {
|
|
6
|
+
/** 슬롯 → 헤더 이름. 없으면 슬롯 키가 헤더가 된다 (`useVirtualColumns().names`) */
|
|
7
|
+
names?: Record<string, string>;
|
|
8
|
+
/** 열 폭 */
|
|
9
|
+
size?: number;
|
|
10
|
+
/** 필터 UI 에 노출할지. 기본 false — 카드 섹션이 필터를 맡는다 */
|
|
11
|
+
filterable?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 가상 컬럼을 **표시 컬럼**으로 만든다.
|
|
15
|
+
*
|
|
16
|
+
* 값은 서버가 식을 평가해 응답 행에 `{ vc_1: 123 }` 처럼 실어 준다 — 클라이언트 모드에서는
|
|
17
|
+
* 값이 없다(2층 엔진은 식을 평가하지 않는다). 이 헬퍼는 그 필드를 읽는 `accessorFn` 컬럼을
|
|
18
|
+
* 만들어 응답에 실린 값이 표에 보이게 한다. `useDataTable({ columns })` 에 기본 컬럼과 함께 넘긴다.
|
|
19
|
+
*
|
|
20
|
+
* `spec` 은 `search.request.virtualColumns` 든 `useVirtualColumns().columns` 든 받는다.
|
|
21
|
+
*/
|
|
22
|
+
export declare function virtualColumnDefs<T extends RowData>(spec: Record<string, VirtualColumnSpec> | ReadonlyArray<VirtualColumn> | undefined, options?: VirtualColumnDefsOptions): TableColumnDefs<T>;
|
|
23
|
+
/**
|
|
24
|
+
* 수식에 참조할 수 있는 필드 목록 — `describeColumns(columns)` 로 컬럼 id·헤더를 뽑는다.
|
|
25
|
+
* 표시 전용 컬럼(`koko-`)과 값 보관 전용(`forceHidden`) 컬럼, 가상 컬럼 자신(`vc_`)은 제외한다.
|
|
26
|
+
*/
|
|
27
|
+
export declare function virtualColumnFields<T extends RowData>(columns: TableColumnDefs<T>, options?: {
|
|
28
|
+
slotPrefix?: string;
|
|
29
|
+
}): VirtualColumnField[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describeColumns, formatByDataType } from "../../../core/src/index";
|
|
2
|
+
function dataTypeOf(type) {
|
|
3
|
+
return type === "boolean" ? "boolean" : type === "string" ? "text" : "number";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 가상 컬럼을 **표시 컬럼**으로 만든다.
|
|
7
|
+
*
|
|
8
|
+
* 값은 서버가 식을 평가해 응답 행에 `{ vc_1: 123 }` 처럼 실어 준다 — 클라이언트 모드에서는
|
|
9
|
+
* 값이 없다(2층 엔진은 식을 평가하지 않는다). 이 헬퍼는 그 필드를 읽는 `accessorFn` 컬럼을
|
|
10
|
+
* 만들어 응답에 실린 값이 표에 보이게 한다. `useDataTable({ columns })` 에 기본 컬럼과 함께 넘긴다.
|
|
11
|
+
*
|
|
12
|
+
* `spec` 은 `search.request.virtualColumns` 든 `useVirtualColumns().columns` 든 받는다.
|
|
13
|
+
*/
|
|
14
|
+
export function virtualColumnDefs(spec, options = {}) {
|
|
15
|
+
if (!spec)
|
|
16
|
+
return [];
|
|
17
|
+
const entries = Array.isArray(spec)
|
|
18
|
+
? spec.map((c) => ({ slotKey: c.slotKey, name: c.name, type: c.type }))
|
|
19
|
+
: Object.entries(spec).map(([slotKey, value]) => ({
|
|
20
|
+
slotKey,
|
|
21
|
+
name: options.names?.[slotKey] ?? slotKey,
|
|
22
|
+
type: value.type ?? "number",
|
|
23
|
+
}));
|
|
24
|
+
return entries.map(({ slotKey, name, type }) => ({
|
|
25
|
+
id: slotKey,
|
|
26
|
+
header: options.names?.[slotKey] ?? name,
|
|
27
|
+
accessorFn: (row) => row[slotKey],
|
|
28
|
+
// 숫자는 콤마 포맷(`ColumnIsNumber` 와 같은 규칙), 참/거짓은 글자로
|
|
29
|
+
cell: ({ getValue }) => {
|
|
30
|
+
const value = getValue();
|
|
31
|
+
if (type === "boolean")
|
|
32
|
+
return value === null || value === undefined ? "" : value ? "참" : "거짓";
|
|
33
|
+
return formatByDataType(value, dataTypeOf(type));
|
|
34
|
+
},
|
|
35
|
+
...(options.size !== undefined ? { size: options.size } : {}),
|
|
36
|
+
enableSorting: true,
|
|
37
|
+
enableGrouping: false,
|
|
38
|
+
meta: { dataType: dataTypeOf(type) },
|
|
39
|
+
filter: options.filterable ? { variant: type === "number" ? "number" : type === "boolean" ? "boolean" : "text" } : { hidden: true },
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 수식에 참조할 수 있는 필드 목록 — `describeColumns(columns)` 로 컬럼 id·헤더를 뽑는다.
|
|
44
|
+
* 표시 전용 컬럼(`koko-`)과 값 보관 전용(`forceHidden`) 컬럼, 가상 컬럼 자신(`vc_`)은 제외한다.
|
|
45
|
+
*/
|
|
46
|
+
export function virtualColumnFields(columns, options = {}) {
|
|
47
|
+
const prefix = options.slotPrefix ?? "vc_";
|
|
48
|
+
return describeColumns(columns)
|
|
49
|
+
.filter((c) => !c.forceHidden && !c.id.startsWith(prefix))
|
|
50
|
+
.map((c) => ({ key: c.id, header: c.header }));
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mui/src/virtual";
|
package/dist/mui.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mui/src/index";
|
package/dist/mui.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useServerTableData, type ServerTableDataOptions, type ServerTableData, type TableQueryFn, } from "./useServerTableData";
|
|
2
|
+
export { useServerGrouping, type UseServerGroupingOptions, type ServerGroupingResult, } from "./useServerGrouping";
|
|
3
|
+
export { serverPivotResult, type ServerPivotResponse } from "./serverPivot";
|
|
4
|
+
export { useServerTable, type ServerTableBundle } from "./useServerTable";
|
|
5
|
+
export type { ServerTableQueryOptions } from "./useServerTableData";
|
|
6
|
+
export { toWireRequest } from "./wire";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useServerTableData, } from "./useServerTableData";
|
|
2
|
+
export { useServerGrouping, } from "./useServerGrouping";
|
|
3
|
+
export { serverPivotResult } from "./serverPivot";
|
|
4
|
+
// ── 테이블 정의 ──
|
|
5
|
+
export { useServerTable } from "./useServerTable";
|
|
6
|
+
// ── 서버 계약 ──
|
|
7
|
+
export { toWireRequest } from "./wire";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { LabelResolver, PivotResult } from "../../core/src/index";
|
|
2
|
+
/**
|
|
3
|
+
* 백엔드 `PivotBuilder` 응답.
|
|
4
|
+
*
|
|
5
|
+
* 서버는 **평평한 행**과 **컬럼 메타 트리**를 준다. 행에는 축 컬럼 값과
|
|
6
|
+
* `채널|1월` 처럼 합쳐진 피벗 키가 한 객체에 섞여 있다.
|
|
7
|
+
*/
|
|
8
|
+
export interface ServerPivotResponse {
|
|
9
|
+
/** 평평한 결과 행 — 축 값 + 피벗 키가 한 객체에 섞여 있다 */
|
|
10
|
+
data: Array<Record<string, unknown>>;
|
|
11
|
+
/** 열 축 메타 트리 */
|
|
12
|
+
columns: ServerPivotColumn[];
|
|
13
|
+
/** 행 축 컬럼명 (상위 → 하위) */
|
|
14
|
+
rowKeys?: string[];
|
|
15
|
+
/** 값 키 (`payment_amount_SUM` 등) */
|
|
16
|
+
valueKeys?: string[];
|
|
17
|
+
meta?: {
|
|
18
|
+
total_count?: number | null;
|
|
19
|
+
} | null;
|
|
20
|
+
}
|
|
21
|
+
export interface ServerPivotColumn {
|
|
22
|
+
key: string;
|
|
23
|
+
label: string;
|
|
24
|
+
/** `row` = 행 축 컬럼, `value` = 집계 값, `group` = 열 축 그룹 */
|
|
25
|
+
type?: "row" | "value" | "group";
|
|
26
|
+
columns?: ServerPivotColumn[];
|
|
27
|
+
}
|
|
28
|
+
export interface ServerPivotOptions {
|
|
29
|
+
/** 축 값 → 라벨. CommonCode idx 를 이름으로 바꿀 때 쓴다 */
|
|
30
|
+
labelOf?: LabelResolver;
|
|
31
|
+
/** 값 키. 미지정이면 응답의 `valueKeys`, 그것도 없으면 첫 leaf 에서 유추 */
|
|
32
|
+
valueKeys?: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 서버 피벗 응답 → 렌더러가 쓰는 `PivotResult`.
|
|
36
|
+
*
|
|
37
|
+
* 클라이언트 계산(`runClientPivot`)과 **같은 타입**을 내므로 `PivotTable` 은
|
|
38
|
+
* 어느 쪽에서 왔는지 몰라도 된다. 데이터 규모에 따라 계산 위치만 갈아끼운다.
|
|
39
|
+
*
|
|
40
|
+
* 서버는 평평한 행을 주므로 행 축 컬럼값을 따라 트리로 되접는다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function serverPivotResult(response: ServerPivotResponse, options?: ServerPivotOptions): PivotResult;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PIVOT_SEPARATOR } from "../../core/src/index";
|
|
2
|
+
function collectLeafKeys(nodes, into = []) {
|
|
3
|
+
for (const node of nodes) {
|
|
4
|
+
if (node.columns && node.columns.length > 0)
|
|
5
|
+
collectLeafKeys(node.columns, into);
|
|
6
|
+
else
|
|
7
|
+
into.push(node.key);
|
|
8
|
+
}
|
|
9
|
+
return into;
|
|
10
|
+
}
|
|
11
|
+
function toAxisNodes(nodes, depth, labels, parentLabels) {
|
|
12
|
+
return nodes.map((node) => {
|
|
13
|
+
const chain = [...parentLabels, node.label];
|
|
14
|
+
const children = node.columns && node.columns.length > 0
|
|
15
|
+
? toAxisNodes(node.columns, depth + 1, labels, chain)
|
|
16
|
+
: undefined;
|
|
17
|
+
if (!children)
|
|
18
|
+
labels[node.key] = chain;
|
|
19
|
+
return {
|
|
20
|
+
key: node.key,
|
|
21
|
+
label: node.label,
|
|
22
|
+
// 서버 키는 이미 합쳐진 문자열이라 원자 값이 없다 — 마지막 조각을 쓴다
|
|
23
|
+
value: node.key.split(PIVOT_SEPARATOR).pop() ?? node.key,
|
|
24
|
+
source: "",
|
|
25
|
+
depth,
|
|
26
|
+
children,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 서버 피벗 응답 → 렌더러가 쓰는 `PivotResult`.
|
|
32
|
+
*
|
|
33
|
+
* 클라이언트 계산(`runClientPivot`)과 **같은 타입**을 내므로 `PivotTable` 은
|
|
34
|
+
* 어느 쪽에서 왔는지 몰라도 된다. 데이터 규모에 따라 계산 위치만 갈아끼운다.
|
|
35
|
+
*
|
|
36
|
+
* 서버는 평평한 행을 주므로 행 축 컬럼값을 따라 트리로 되접는다.
|
|
37
|
+
*/
|
|
38
|
+
export function serverPivotResult(response, options = {}) {
|
|
39
|
+
const leafLabels = {};
|
|
40
|
+
const groupColumns = response.columns.filter((column) => column.type !== "row" && column.type !== "value");
|
|
41
|
+
const columns = toAxisNodes(groupColumns, 0, leafLabels, []);
|
|
42
|
+
const leafKeys = collectLeafKeys(groupColumns);
|
|
43
|
+
const rowKeys = response.rowKeys ??
|
|
44
|
+
response.columns.filter((column) => column.type === "row").map((column) => column.key);
|
|
45
|
+
const valueKeys = options.valueKeys ??
|
|
46
|
+
response.valueKeys ??
|
|
47
|
+
response.columns.filter((column) => column.type === "value").map((column) => column.key);
|
|
48
|
+
const label = (source, value) => options.labelOf?.(source, value) ?? (value === null || value === undefined ? "(비어 있음)" : String(value));
|
|
49
|
+
const sumOf = (rows, key) => {
|
|
50
|
+
let total = 0;
|
|
51
|
+
let seen = false;
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const value = row[key];
|
|
54
|
+
if (typeof value === "number") {
|
|
55
|
+
total += value;
|
|
56
|
+
seen = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return seen ? total : null;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* 행 축 값으로 트리를 되접는다.
|
|
63
|
+
*
|
|
64
|
+
* 상위 노드의 값은 **하위 합**이다. 서버가 소계 행을 따로 주지 않기 때문인데,
|
|
65
|
+
* `AVG`/`MIN`/`MAX` 는 합으로 굴릴 수 없다 — 그런 집계를 상위에서 봐야 하면
|
|
66
|
+
* 서버가 소계를 내려 주거나 클라이언트 계산을 써야 한다.
|
|
67
|
+
*/
|
|
68
|
+
const build = (rows, depth, parentKey) => {
|
|
69
|
+
const source = rowKeys[depth];
|
|
70
|
+
if (!source)
|
|
71
|
+
return [];
|
|
72
|
+
const buckets = new Map();
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const value = row[source];
|
|
75
|
+
const bucketKey = String(value);
|
|
76
|
+
const bucket = buckets.get(bucketKey);
|
|
77
|
+
if (bucket)
|
|
78
|
+
bucket.rows.push(row);
|
|
79
|
+
else
|
|
80
|
+
buckets.set(bucketKey, { value, rows: [row] });
|
|
81
|
+
}
|
|
82
|
+
const nodes = [];
|
|
83
|
+
for (const bucket of buckets.values()) {
|
|
84
|
+
const key = parentKey ? `${parentKey}${PIVOT_SEPARATOR}${String(bucket.value)}` : String(bucket.value);
|
|
85
|
+
const isLeaf = depth === rowKeys.length - 1;
|
|
86
|
+
const cells = {};
|
|
87
|
+
for (const leafKey of leafKeys) {
|
|
88
|
+
const perValue = {};
|
|
89
|
+
let any = false;
|
|
90
|
+
for (const valueKey of valueKeys) {
|
|
91
|
+
// 서버 행의 셀 키는 `열키` 또는 `열키__값키` 형태를 모두 허용한다
|
|
92
|
+
const combined = `${leafKey}${PIVOT_SEPARATOR}${valueKey}`;
|
|
93
|
+
const has = bucket.rows.some((row) => combined in row);
|
|
94
|
+
const cellKey = has ? combined : leafKey;
|
|
95
|
+
const value = sumOf(bucket.rows, cellKey);
|
|
96
|
+
if (value !== null)
|
|
97
|
+
any = true;
|
|
98
|
+
perValue[valueKey] = value;
|
|
99
|
+
}
|
|
100
|
+
if (any)
|
|
101
|
+
cells[leafKey] = perValue;
|
|
102
|
+
}
|
|
103
|
+
const total = {};
|
|
104
|
+
for (const valueKey of valueKeys) {
|
|
105
|
+
total[valueKey] = sumOf(bucket.rows, valueKey) ?? sumOf(bucket.rows, leafKeys[0] ?? valueKey);
|
|
106
|
+
}
|
|
107
|
+
nodes.push({
|
|
108
|
+
key,
|
|
109
|
+
label: label(source, bucket.value),
|
|
110
|
+
value: bucket.value,
|
|
111
|
+
source,
|
|
112
|
+
depth,
|
|
113
|
+
cells,
|
|
114
|
+
total,
|
|
115
|
+
count: bucket.rows.length,
|
|
116
|
+
children: isLeaf ? undefined : build(bucket.rows, depth + 1, key),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return nodes;
|
|
120
|
+
};
|
|
121
|
+
const rows = build(response.data, 0, "");
|
|
122
|
+
const grandTotal = {};
|
|
123
|
+
for (const valueKey of valueKeys)
|
|
124
|
+
grandTotal[valueKey] = sumOf(response.data, valueKey);
|
|
125
|
+
// 열 합계 — 응답 행 전체에서 열 leaf 별 셀을 합산한다. 행 트리의 상위 노드와 같은
|
|
126
|
+
// 이유로 SUM 만 정확하다 (서버가 열 소계를 내려 주지 않는다).
|
|
127
|
+
const colTotals = {};
|
|
128
|
+
for (const leafKey of leafKeys) {
|
|
129
|
+
const perValue = {};
|
|
130
|
+
let any = false;
|
|
131
|
+
for (const valueKey of valueKeys) {
|
|
132
|
+
const combined = `${leafKey}${PIVOT_SEPARATOR}${valueKey}`;
|
|
133
|
+
const has = response.data.some((row) => combined in row);
|
|
134
|
+
const value = sumOf(response.data, has ? combined : leafKey);
|
|
135
|
+
if (value !== null)
|
|
136
|
+
any = true;
|
|
137
|
+
perValue[valueKey] = value;
|
|
138
|
+
}
|
|
139
|
+
if (any)
|
|
140
|
+
colTotals[leafKey] = perValue;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
rows,
|
|
144
|
+
columns,
|
|
145
|
+
leafKeys,
|
|
146
|
+
leafLabels,
|
|
147
|
+
valueKeys,
|
|
148
|
+
grandTotal,
|
|
149
|
+
colTotals,
|
|
150
|
+
// 서버는 상한을 넘긴 열을 응답에서 이미 빼고 준다 — 여기선 알 수 없다
|
|
151
|
+
droppedColumns: 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { ExpandState, FlatItem, GroupPath, GroupTreeNode, TableColumnDefs, TableRequest, TableResponse } from "../../core/src/index";
|
|
3
|
+
import type { TableQueryFn } from "./useServerTableData";
|
|
4
|
+
export interface UseServerGroupingOptions<T extends RowData> {
|
|
5
|
+
/** 적용된 요청 — 필터·정렬이 그대로 그룹 조회에 실린다 */
|
|
6
|
+
request: TableRequest;
|
|
7
|
+
queryFn: TableQueryFn<T>;
|
|
8
|
+
/** 상위 → 하위 순서의 그룹 컬럼. 비어 있으면 아무것도 하지 않는다 */
|
|
9
|
+
grouping: string[];
|
|
10
|
+
columns?: TableColumnDefs<T>;
|
|
11
|
+
/**
|
|
12
|
+
* 최상위 그룹 조회 응답의 `groups` (`useServerTableData(...).groups`).
|
|
13
|
+
*
|
|
14
|
+
* 현행 백엔드는 `groupExpansion: { path: [] … }` 에 `groups.nodes`(한 레벨 `GroupNode[]`)로
|
|
15
|
+
* 답한다. 구 `groupHeaderOnly` 포맷의 `groups.headers` 도 하위 호환으로 읽는다
|
|
16
|
+
* (`nodes` 가 비어 있을 때만).
|
|
17
|
+
*/
|
|
18
|
+
headers?: TableResponse<T>["groups"];
|
|
19
|
+
/** 한 번에 받아 올 하위 개수 */
|
|
20
|
+
batchSize?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ServerGroupingResult<T extends RowData> {
|
|
23
|
+
tree: Array<GroupTreeNode<T>>;
|
|
24
|
+
items: Array<FlatItem<T>>;
|
|
25
|
+
/**
|
|
26
|
+
* 지금까지 받아 온 **말단 행 전체**.
|
|
27
|
+
*
|
|
28
|
+
* 3층(`useDataTable`)에 넘겨야 셀이 컬럼 정의대로 그려진다. 헤더만 받는
|
|
29
|
+
* 모드에서는 `data.rows` 가 비어 있어서, 이걸 안 넘기면 그룹을 펼쳐도
|
|
30
|
+
* 행이 조용히 빈 채로 남는다.
|
|
31
|
+
*/
|
|
32
|
+
rows: T[];
|
|
33
|
+
expand: ExpandState;
|
|
34
|
+
/** "더 보기" — 그 경로의 하위를 배치만큼 더 받는다 */
|
|
35
|
+
loadMore: (path: GroupPath) => void;
|
|
36
|
+
readonly enabled: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 서버 그룹핑 — 최상위 그룹 노드를 먼저 받고, 펼칠 때 하위를 가져온다.
|
|
40
|
+
*
|
|
41
|
+
* 요청은 백엔드 `GroupMixin._build_group_query` 규칙 그대로다: `groupExpansion.path` 깊이가
|
|
42
|
+
* `grouping` 깊이보다 얕으면 다음 레벨 노드(`groups.nodes`), 같으면 말단 행(`data`)이 오고,
|
|
43
|
+
* `meta.total_count` 는 그 레벨의 전체 개수(노드 수 또는 행 수)다.
|
|
44
|
+
*
|
|
45
|
+
* 클라이언트 그룹핑(`useGrouping`)과 **결과 모양이 같다** (`tree` / `items` /
|
|
46
|
+
* `expand`). 그래서 렌더러는 어느 쪽인지 몰라도 되고, 같은 화면을 데이터 규모에
|
|
47
|
+
* 따라 갈아끼울 수 있다.
|
|
48
|
+
*
|
|
49
|
+
* 차이는 하나: 하위가 페이지 단위로 오므로 `items` 에 "더 보기"(`loadMore`) 행이
|
|
50
|
+
* 섞인다. 전부 받아 놓고 자르는 게 아니라 **필요할 때만** 요청한다.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useServerGrouping<T extends RowData>(options: UseServerGroupingOptions<T>): ServerGroupingResult<T>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createLabelResolver, flattenGroups, pathKey } from "../../core/src/index";
|
|
3
|
+
import { toWireRequest } from "./wire";
|
|
4
|
+
const DEFAULT_BATCH = 50;
|
|
5
|
+
function toAggregates(raw) {
|
|
6
|
+
return raw;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 서버 그룹핑 — 최상위 그룹 노드를 먼저 받고, 펼칠 때 하위를 가져온다.
|
|
10
|
+
*
|
|
11
|
+
* 요청은 백엔드 `GroupMixin._build_group_query` 규칙 그대로다: `groupExpansion.path` 깊이가
|
|
12
|
+
* `grouping` 깊이보다 얕으면 다음 레벨 노드(`groups.nodes`), 같으면 말단 행(`data`)이 오고,
|
|
13
|
+
* `meta.total_count` 는 그 레벨의 전체 개수(노드 수 또는 행 수)다.
|
|
14
|
+
*
|
|
15
|
+
* 클라이언트 그룹핑(`useGrouping`)과 **결과 모양이 같다** (`tree` / `items` /
|
|
16
|
+
* `expand`). 그래서 렌더러는 어느 쪽인지 몰라도 되고, 같은 화면을 데이터 규모에
|
|
17
|
+
* 따라 갈아끼울 수 있다.
|
|
18
|
+
*
|
|
19
|
+
* 차이는 하나: 하위가 페이지 단위로 오므로 `items` 에 "더 보기"(`loadMore`) 행이
|
|
20
|
+
* 섞인다. 전부 받아 놓고 자르는 게 아니라 **필요할 때만** 요청한다.
|
|
21
|
+
*/
|
|
22
|
+
export function useServerGrouping(options) {
|
|
23
|
+
const { request, queryFn, grouping, columns, headers, batchSize = DEFAULT_BATCH, } = options;
|
|
24
|
+
const enabled = grouping.length > 0;
|
|
25
|
+
const resolveLabel = useMemo(() => createLabelResolver(columns), [columns]);
|
|
26
|
+
const [levels, setLevels] = useState({});
|
|
27
|
+
const [expandedKeys, setExpandedKeys] = useState(new Set());
|
|
28
|
+
// 요청이 바뀌면(필터·정렬) 받아 둔 하위는 전부 무효다 — 안 버리면 새 조건에
|
|
29
|
+
// 맞지 않는 행이 펼친 그룹 아래 그대로 남는다
|
|
30
|
+
const requestRef = useRef(request);
|
|
31
|
+
if (requestRef.current !== request) {
|
|
32
|
+
requestRef.current = request;
|
|
33
|
+
if (Object.keys(levels).length > 0)
|
|
34
|
+
setLevels({});
|
|
35
|
+
if (expandedKeys.size > 0)
|
|
36
|
+
setExpandedKeys(new Set());
|
|
37
|
+
}
|
|
38
|
+
/** 서버 응답 `groups` → 최상위 노드. `nodes`(현행) 우선, 비어 있으면 `headers`(구 포맷) */
|
|
39
|
+
const roots = useMemo(() => {
|
|
40
|
+
const nodes = headers?.nodes ?? [];
|
|
41
|
+
if (nodes.length > 0) {
|
|
42
|
+
return nodes.map((node) => {
|
|
43
|
+
const key = node.group_key || grouping[0] || "";
|
|
44
|
+
return {
|
|
45
|
+
groupKey: key,
|
|
46
|
+
groupValue: node.group_value ?? null,
|
|
47
|
+
label: resolveLabel(key, node.group_value) ?? undefined,
|
|
48
|
+
count: node.count,
|
|
49
|
+
aggregates: toAggregates(node.aggregates),
|
|
50
|
+
path: [{ key, value: node.group_value ?? null }],
|
|
51
|
+
depth: 0,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const list = headers?.headers ?? [];
|
|
56
|
+
return list.map((header) => {
|
|
57
|
+
const path = header.group_values.map((step) => ({
|
|
58
|
+
key: step.key,
|
|
59
|
+
value: step.value,
|
|
60
|
+
}));
|
|
61
|
+
const last = header.group_values[header.group_values.length - 1];
|
|
62
|
+
return {
|
|
63
|
+
groupKey: last?.key ?? grouping[0] ?? "",
|
|
64
|
+
groupValue: last?.value ?? null,
|
|
65
|
+
label: last?.label ?? resolveLabel(last?.key ?? "", last?.value) ?? undefined,
|
|
66
|
+
count: header.count,
|
|
67
|
+
aggregates: toAggregates(header.aggregates),
|
|
68
|
+
path,
|
|
69
|
+
depth: 0,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}, [headers, grouping, resolveLabel]);
|
|
73
|
+
/** 받아 둔 레벨을 노드에 붙여 트리를 만든다 */
|
|
74
|
+
const attach = useCallback((node) => {
|
|
75
|
+
const level = levels[pathKey(node.path)];
|
|
76
|
+
if (!level) {
|
|
77
|
+
// 아직 안 받았으면 "펼칠 수 있다"만 표시한다 — 자식 배열을 비워 두면
|
|
78
|
+
// 렌더러가 "자식 없음"으로 오해한다
|
|
79
|
+
return { ...node, hasMore: false };
|
|
80
|
+
}
|
|
81
|
+
const children = level.nodes?.map((child) => attach({ ...child, depth: node.depth + 1 }));
|
|
82
|
+
return {
|
|
83
|
+
...node,
|
|
84
|
+
children,
|
|
85
|
+
rows: level.rows,
|
|
86
|
+
hasMore: level.hasMore,
|
|
87
|
+
loadedCount: level.loaded,
|
|
88
|
+
totalCount: level.total,
|
|
89
|
+
isPending: level.pending,
|
|
90
|
+
};
|
|
91
|
+
}, [levels]);
|
|
92
|
+
const tree = useMemo(() => (enabled ? roots.map(attach) : []), [enabled, roots, attach]);
|
|
93
|
+
const fetchLevel = useCallback(async (path, offset) => {
|
|
94
|
+
const key = pathKey(path);
|
|
95
|
+
setLevels((prev) => ({
|
|
96
|
+
...prev,
|
|
97
|
+
[key]: { ...(prev[key] ?? { loaded: 0, hasMore: false }), pending: true },
|
|
98
|
+
}));
|
|
99
|
+
try {
|
|
100
|
+
// 백엔드 `GroupExpansion` — path 깊이로 노드/말단을 가른다. `toWireRequest` 가
|
|
101
|
+
// 호출부가 남긴 `groupHeaderOnly` 를 걷어낸다(백엔드 계약에 없는 키).
|
|
102
|
+
const response = await queryFn(toWireRequest({
|
|
103
|
+
...requestRef.current,
|
|
104
|
+
groupExpansion: { path: [...path], offset, limit: batchSize },
|
|
105
|
+
}), { signal: new AbortController().signal });
|
|
106
|
+
const nodes = (response.groups?.nodes ?? []).map((node) => ({
|
|
107
|
+
groupKey: node.group_key,
|
|
108
|
+
groupValue: node.group_value,
|
|
109
|
+
label: resolveLabel(node.group_key, node.group_value),
|
|
110
|
+
count: node.count,
|
|
111
|
+
aggregates: toAggregates(node.aggregates),
|
|
112
|
+
path: [...path, { key: node.group_key, value: node.group_value }],
|
|
113
|
+
depth: path.length,
|
|
114
|
+
}));
|
|
115
|
+
const rows = response.data ?? [];
|
|
116
|
+
const received = nodes.length > 0 ? nodes.length : rows.length;
|
|
117
|
+
// 백엔드는 has_more 를 주지 않는다 — ERP 와 같이 `meta.total_count`(그 레벨의 전체
|
|
118
|
+
// 노드/행 수, 그룹 노드는 `_total` 윈도우 값)로 판정하고, 없으면 배치 크기로 추정한다
|
|
119
|
+
const total = response.meta?.total_count ?? undefined;
|
|
120
|
+
setLevels((prev) => {
|
|
121
|
+
const before = prev[key];
|
|
122
|
+
const loaded = offset + received;
|
|
123
|
+
return {
|
|
124
|
+
...prev,
|
|
125
|
+
[key]: {
|
|
126
|
+
nodes: nodes.length > 0 ? [...(before?.nodes ?? []), ...nodes] : before?.nodes,
|
|
127
|
+
rows: rows.length > 0 ? [...(before?.rows ?? []), ...rows] : before?.rows,
|
|
128
|
+
loaded,
|
|
129
|
+
total: total ?? undefined,
|
|
130
|
+
// 총 개수를 모르면 배치가 가득 찼는지로 판단한다
|
|
131
|
+
hasMore: total != null ? loaded < total : received === batchSize,
|
|
132
|
+
pending: false,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// 실패해도 펼침 상태는 유지한다 — 접혀 버리면 사용자가 뭘 눌렀는지 잃는다
|
|
139
|
+
setLevels((prev) => ({
|
|
140
|
+
...prev,
|
|
141
|
+
[key]: { ...(prev[key] ?? { loaded: 0, hasMore: false }), pending: false },
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
}, [queryFn, batchSize, resolveLabel]);
|
|
145
|
+
const toggle = useCallback((path) => {
|
|
146
|
+
const key = pathKey(path);
|
|
147
|
+
setExpandedKeys((prev) => {
|
|
148
|
+
const next = new Set(prev);
|
|
149
|
+
if (next.has(key))
|
|
150
|
+
next.delete(key);
|
|
151
|
+
else {
|
|
152
|
+
next.add(key);
|
|
153
|
+
// 처음 펼치는 경로면 그때 받아 온다
|
|
154
|
+
if (!levels[key])
|
|
155
|
+
void fetchLevel(path, 0);
|
|
156
|
+
}
|
|
157
|
+
return next;
|
|
158
|
+
});
|
|
159
|
+
}, [levels, fetchLevel]);
|
|
160
|
+
const loadMore = useCallback((path) => {
|
|
161
|
+
const level = levels[pathKey(path)];
|
|
162
|
+
void fetchLevel(path, level?.loaded ?? 0);
|
|
163
|
+
}, [levels, fetchLevel]);
|
|
164
|
+
const expand = useMemo(() => ({
|
|
165
|
+
isExpanded: (path) => expandedKeys.has(pathKey(path)),
|
|
166
|
+
toggle,
|
|
167
|
+
// 서버 모드는 전부 펼치면 트리 전체를 받아 와야 한다. 몇 건인지 모르는 채
|
|
168
|
+
// 그렇게 하면 화면이 멈추므로 최상위만 펼친다.
|
|
169
|
+
expandAll: () => setExpandedKeys(new Set(roots.map((node) => pathKey(node.path)))),
|
|
170
|
+
collapseAll: () => setExpandedKeys(new Set()),
|
|
171
|
+
expandToDepth: (depth) => setExpandedKeys(depth <= 0 ? new Set() : new Set(roots.map((n) => pathKey(n.path)))),
|
|
172
|
+
depth: expandedKeys.size > 0 ? 1 : 0,
|
|
173
|
+
maxDepth: grouping.length,
|
|
174
|
+
canExpand: grouping.length > 0,
|
|
175
|
+
}), [expandedKeys, toggle, roots, grouping.length]);
|
|
176
|
+
const items = useMemo(() => (enabled ? flattenGroups(tree, { expanded: expandedKeys }) : []), [enabled, tree, expandedKeys]);
|
|
177
|
+
const rows = useMemo(() => {
|
|
178
|
+
const collected = [];
|
|
179
|
+
for (const level of Object.values(levels)) {
|
|
180
|
+
if (level.rows)
|
|
181
|
+
collected.push(...level.rows);
|
|
182
|
+
}
|
|
183
|
+
return collected;
|
|
184
|
+
}, [levels]);
|
|
185
|
+
return { tree, items, rows, expand, loadMore, enabled };
|
|
186
|
+
}
|