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.
Files changed (308) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +320 -0
  3. package/contract/table-request.schema.json +424 -0
  4. package/dist/core/src/adapters/sources.d.ts +59 -0
  5. package/dist/core/src/adapters/sources.js +235 -0
  6. package/dist/core/src/client/aggregate.d.ts +94 -0
  7. package/dist/core/src/client/aggregate.js +222 -0
  8. package/dist/core/src/client/groupRows.d.ts +40 -0
  9. package/dist/core/src/client/groupRows.js +145 -0
  10. package/dist/core/src/client/runClientQuery.d.ts +67 -0
  11. package/dist/core/src/client/runClientQuery.js +157 -0
  12. package/dist/core/src/columns/ColumnBuilder.d.ts +121 -0
  13. package/dist/core/src/columns/ColumnBuilder.js +273 -0
  14. package/dist/core/src/columns/cascade.d.ts +24 -0
  15. package/dist/core/src/columns/cascade.js +93 -0
  16. package/dist/core/src/columns/deriveOperators.d.ts +38 -0
  17. package/dist/core/src/columns/deriveOperators.js +94 -0
  18. package/dist/core/src/columns/expandSorting.d.ts +19 -0
  19. package/dist/core/src/columns/expandSorting.js +52 -0
  20. package/dist/core/src/columns/helper.d.ts +196 -0
  21. package/dist/core/src/columns/helper.js +250 -0
  22. package/dist/core/src/columns/labels.d.ts +12 -0
  23. package/dist/core/src/columns/labels.js +21 -0
  24. package/dist/core/src/contract.d.ts +12 -0
  25. package/dist/core/src/contract.js +12 -0
  26. package/dist/core/src/contracts/aiColumnMeta.d.ts +102 -0
  27. package/dist/core/src/contracts/aiColumnMeta.js +197 -0
  28. package/dist/core/src/contracts/describeColumns.d.ts +48 -0
  29. package/dist/core/src/contracts/describeColumns.js +40 -0
  30. package/dist/core/src/contracts/exportData.d.ts +150 -0
  31. package/dist/core/src/contracts/exportData.js +228 -0
  32. package/dist/core/src/contracts/index.d.ts +6 -0
  33. package/dist/core/src/contracts/index.js +12 -0
  34. package/dist/core/src/contracts/legacyUrl.d.ts +47 -0
  35. package/dist/core/src/contracts/legacyUrl.js +156 -0
  36. package/dist/core/src/contracts/preset.d.ts +104 -0
  37. package/dist/core/src/contracts/preset.js +141 -0
  38. package/dist/core/src/contracts/presetStore.d.ts +81 -0
  39. package/dist/core/src/contracts/presetStore.js +146 -0
  40. package/dist/core/src/contracts/usePresets.d.ts +51 -0
  41. package/dist/core/src/contracts/usePresets.js +170 -0
  42. package/dist/core/src/defaults.d.ts +7 -0
  43. package/dist/core/src/defaults.js +9 -0
  44. package/dist/core/src/filters/codec.d.ts +20 -0
  45. package/dist/core/src/filters/codec.js +61 -0
  46. package/dist/core/src/filters/date.d.ts +30 -0
  47. package/dist/core/src/filters/date.js +179 -0
  48. package/dist/core/src/filters/describe.d.ts +45 -0
  49. package/dist/core/src/filters/describe.js +189 -0
  50. package/dist/core/src/filters/kind.d.ts +52 -0
  51. package/dist/core/src/filters/kind.js +110 -0
  52. package/dist/core/src/filters/kinds/_list.d.ts +13 -0
  53. package/dist/core/src/filters/kinds/_list.js +42 -0
  54. package/dist/core/src/filters/kinds/boolean.d.ts +3 -0
  55. package/dist/core/src/filters/kinds/boolean.js +21 -0
  56. package/dist/core/src/filters/kinds/cascade.d.ts +6 -0
  57. package/dist/core/src/filters/kinds/cascade.js +16 -0
  58. package/dist/core/src/filters/kinds/commonCode.d.ts +3 -0
  59. package/dist/core/src/filters/kinds/commonCode.js +13 -0
  60. package/dist/core/src/filters/kinds/date.d.ts +9 -0
  61. package/dist/core/src/filters/kinds/date.js +32 -0
  62. package/dist/core/src/filters/kinds/enum.d.ts +4 -0
  63. package/dist/core/src/filters/kinds/enum.js +13 -0
  64. package/dist/core/src/filters/kinds/index.d.ts +49 -0
  65. package/dist/core/src/filters/kinds/index.js +39 -0
  66. package/dist/core/src/filters/kinds/linear.d.ts +5 -0
  67. package/dist/core/src/filters/kinds/linear.js +16 -0
  68. package/dist/core/src/filters/kinds/number.d.ts +5 -0
  69. package/dist/core/src/filters/kinds/number.js +19 -0
  70. package/dist/core/src/filters/kinds/period.d.ts +4 -0
  71. package/dist/core/src/filters/kinds/period.js +17 -0
  72. package/dist/core/src/filters/kinds/relation.d.ts +5 -0
  73. package/dist/core/src/filters/kinds/relation.js +17 -0
  74. package/dist/core/src/filters/kinds/schedule.d.ts +5 -0
  75. package/dist/core/src/filters/kinds/schedule.js +16 -0
  76. package/dist/core/src/filters/kinds/text.d.ts +4 -0
  77. package/dist/core/src/filters/kinds/text.js +18 -0
  78. package/dist/core/src/filters/linear.d.ts +33 -0
  79. package/dist/core/src/filters/linear.js +65 -0
  80. package/dist/core/src/filters/list.d.ts +32 -0
  81. package/dist/core/src/filters/list.js +87 -0
  82. package/dist/core/src/filters/number.d.ts +17 -0
  83. package/dist/core/src/filters/number.js +82 -0
  84. package/dist/core/src/filters/registry.d.ts +23 -0
  85. package/dist/core/src/filters/registry.js +121 -0
  86. package/dist/core/src/filters/relation.d.ts +26 -0
  87. package/dist/core/src/filters/relation.js +75 -0
  88. package/dist/core/src/filters/schedule.d.ts +21 -0
  89. package/dist/core/src/filters/schedule.js +60 -0
  90. package/dist/core/src/filters/text.d.ts +44 -0
  91. package/dist/core/src/filters/text.js +127 -0
  92. package/dist/core/src/hooks/index.d.ts +6 -0
  93. package/dist/core/src/hooks/index.js +6 -0
  94. package/dist/core/src/hooks/useDataTable.d.ts +13 -0
  95. package/dist/core/src/hooks/useDataTable.js +109 -0
  96. package/dist/core/src/hooks/useGrouping.d.ts +33 -0
  97. package/dist/core/src/hooks/useGrouping.js +57 -0
  98. package/dist/core/src/hooks/useSearchHistory.d.ts +20 -0
  99. package/dist/core/src/hooks/useSearchHistory.js +52 -0
  100. package/dist/core/src/hooks/useTableData.d.ts +25 -0
  101. package/dist/core/src/hooks/useTableData.js +49 -0
  102. package/dist/core/src/hooks/useTableFromDefinition.d.ts +47 -0
  103. package/dist/core/src/hooks/useTableFromDefinition.js +174 -0
  104. package/dist/core/src/hooks/useTableSearch.d.ts +10 -0
  105. package/dist/core/src/hooks/useTableSearch.js +434 -0
  106. package/dist/core/src/hooks/useUrlSync.d.ts +64 -0
  107. package/dist/core/src/hooks/useUrlSync.js +145 -0
  108. package/dist/core/src/index.d.ts +38 -0
  109. package/dist/core/src/index.js +43 -0
  110. package/dist/core/src/pivot/runClientPivot.d.ts +101 -0
  111. package/dist/core/src/pivot/runClientPivot.js +230 -0
  112. package/dist/core/src/pivot/transforms.d.ts +19 -0
  113. package/dist/core/src/pivot/transforms.js +105 -0
  114. package/dist/core/src/registry.d.ts +46 -0
  115. package/dist/core/src/registry.js +67 -0
  116. package/dist/core/src/types/column.d.ts +213 -0
  117. package/dist/core/src/types/column.js +1 -0
  118. package/dist/core/src/types/data.d.ts +63 -0
  119. package/dist/core/src/types/data.js +1 -0
  120. package/dist/core/src/types/definition.d.ts +239 -0
  121. package/dist/core/src/types/definition.js +59 -0
  122. package/dist/core/src/types/filter.d.ts +94 -0
  123. package/dist/core/src/types/filter.js +1 -0
  124. package/dist/core/src/types/filterKind.d.ts +204 -0
  125. package/dist/core/src/types/filterKind.js +1 -0
  126. package/dist/core/src/types/grouping.d.ts +80 -0
  127. package/dist/core/src/types/grouping.js +4 -0
  128. package/dist/core/src/types/index.d.ts +8 -0
  129. package/dist/core/src/types/index.js +8 -0
  130. package/dist/core/src/types/operators.d.ts +64 -0
  131. package/dist/core/src/types/operators.js +108 -0
  132. package/dist/core/src/types/request.d.ts +114 -0
  133. package/dist/core/src/types/request.js +1 -0
  134. package/dist/core/src/types/search.d.ts +112 -0
  135. package/dist/core/src/types/search.js +1 -0
  136. package/dist/core/src/types/sources.d.ts +84 -0
  137. package/dist/core/src/types/sources.js +1 -0
  138. package/dist/core/src/types/table.d.ts +40 -0
  139. package/dist/core/src/types/table.js +1 -0
  140. package/dist/core/src/utils/compare.d.ts +13 -0
  141. package/dist/core/src/utils/compare.js +53 -0
  142. package/dist/core/src/utils/dateText.d.ts +12 -0
  143. package/dist/core/src/utils/dateText.js +30 -0
  144. package/dist/core/src/utils/getValue.d.ts +9 -0
  145. package/dist/core/src/utils/getValue.js +32 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.js +2 -0
  148. package/dist/mui/src/TableLayout.d.ts +75 -0
  149. package/dist/mui/src/TableLayout.js +162 -0
  150. package/dist/mui/src/TableScreen.d.ts +70 -0
  151. package/dist/mui/src/TableScreen.js +174 -0
  152. package/dist/mui/src/addons/FilterModal.d.ts +45 -0
  153. package/dist/mui/src/addons/FilterModal.js +39 -0
  154. package/dist/mui/src/addons/FilterStatusModal.d.ts +40 -0
  155. package/dist/mui/src/addons/FilterStatusModal.js +68 -0
  156. package/dist/mui/src/addons/PresetBar.d.ts +56 -0
  157. package/dist/mui/src/addons/PresetBar.js +145 -0
  158. package/dist/mui/src/addons/ShareDropdown.d.ts +35 -0
  159. package/dist/mui/src/addons/ShareDropdown.js +41 -0
  160. package/dist/mui/src/addons/searchStore.d.ts +44 -0
  161. package/dist/mui/src/addons/searchStore.js +112 -0
  162. package/dist/mui/src/addons/searchSummary.d.ts +106 -0
  163. package/dist/mui/src/addons/searchSummary.js +109 -0
  164. package/dist/mui/src/columns/displayColumns.d.ts +100 -0
  165. package/dist/mui/src/columns/displayColumns.js +174 -0
  166. package/dist/mui/src/content/CardContent.d.ts +24 -0
  167. package/dist/mui/src/content/CardContent.js +25 -0
  168. package/dist/mui/src/content/ColumnMenu.d.ts +43 -0
  169. package/dist/mui/src/content/ColumnMenu.js +40 -0
  170. package/dist/mui/src/content/ColumnVisibilityMenu.d.ts +14 -0
  171. package/dist/mui/src/content/ColumnVisibilityMenu.js +37 -0
  172. package/dist/mui/src/content/EditCell.d.ts +19 -0
  173. package/dist/mui/src/content/EditCell.js +61 -0
  174. package/dist/mui/src/content/HeaderCell.d.ts +59 -0
  175. package/dist/mui/src/content/HeaderCell.js +88 -0
  176. package/dist/mui/src/content/SelectionActionBar.d.ts +17 -0
  177. package/dist/mui/src/content/SelectionActionBar.js +19 -0
  178. package/dist/mui/src/content/SortControl.d.ts +17 -0
  179. package/dist/mui/src/content/SortControl.js +45 -0
  180. package/dist/mui/src/content/TableContent.d.ts +191 -0
  181. package/dist/mui/src/content/TableContent.js +525 -0
  182. package/dist/mui/src/content/TableErrorFallback.d.ts +24 -0
  183. package/dist/mui/src/content/TableErrorFallback.js +29 -0
  184. package/dist/mui/src/content/cellError.d.ts +13 -0
  185. package/dist/mui/src/content/cellError.js +20 -0
  186. package/dist/mui/src/content/cellRenderers.d.ts +71 -0
  187. package/dist/mui/src/content/cellRenderers.js +127 -0
  188. package/dist/mui/src/content/cellValue.d.ts +27 -0
  189. package/dist/mui/src/content/cellValue.js +82 -0
  190. package/dist/mui/src/content/columnOrder.d.ts +7 -0
  191. package/dist/mui/src/content/columnOrder.js +48 -0
  192. package/dist/mui/src/content/density.d.ts +13 -0
  193. package/dist/mui/src/content/density.js +12 -0
  194. package/dist/mui/src/content/menuItems.d.ts +118 -0
  195. package/dist/mui/src/content/menuItems.js +128 -0
  196. package/dist/mui/src/content/pinning.d.ts +17 -0
  197. package/dist/mui/src/content/pinning.js +65 -0
  198. package/dist/mui/src/exporters/index.d.ts +1 -0
  199. package/dist/mui/src/exporters/index.js +1 -0
  200. package/dist/mui/src/exporters/useExportFormatters.d.ts +13 -0
  201. package/dist/mui/src/exporters/useExportFormatters.js +17 -0
  202. package/dist/mui/src/filter/FilterArea.d.ts +40 -0
  203. package/dist/mui/src/filter/FilterArea.js +37 -0
  204. package/dist/mui/src/filter/FilterInput.d.ts +14 -0
  205. package/dist/mui/src/filter/FilterInput.js +18 -0
  206. package/dist/mui/src/filter/inputs/DateFilterInput.d.ts +14 -0
  207. package/dist/mui/src/filter/inputs/DateFilterInput.js +112 -0
  208. package/dist/mui/src/filter/inputs/LinearFilterInput.d.ts +10 -0
  209. package/dist/mui/src/filter/inputs/LinearFilterInput.js +35 -0
  210. package/dist/mui/src/filter/inputs/NumberFilterInput.d.ts +12 -0
  211. package/dist/mui/src/filter/inputs/NumberFilterInput.js +111 -0
  212. package/dist/mui/src/filter/inputs/OperatorMenu.d.ts +14 -0
  213. package/dist/mui/src/filter/inputs/OperatorMenu.js +25 -0
  214. package/dist/mui/src/filter/inputs/RelationFilterInput.d.ts +11 -0
  215. package/dist/mui/src/filter/inputs/RelationFilterInput.js +67 -0
  216. package/dist/mui/src/filter/inputs/ScheduleFilterInput.d.ts +8 -0
  217. package/dist/mui/src/filter/inputs/ScheduleFilterInput.js +17 -0
  218. package/dist/mui/src/filter/inputs/SelectFilterInput.d.ts +16 -0
  219. package/dist/mui/src/filter/inputs/SelectFilterInput.js +57 -0
  220. package/dist/mui/src/filter/inputs/TextFilterInput.d.ts +13 -0
  221. package/dist/mui/src/filter/inputs/TextFilterInput.js +55 -0
  222. package/dist/mui/src/filter/kinds.d.ts +58 -0
  223. package/dist/mui/src/filter/kinds.js +103 -0
  224. package/dist/mui/src/grouping/AggregateChip.d.ts +55 -0
  225. package/dist/mui/src/grouping/AggregateChip.js +128 -0
  226. package/dist/mui/src/grouping/AggregationChips.d.ts +49 -0
  227. package/dist/mui/src/grouping/AggregationChips.js +80 -0
  228. package/dist/mui/src/grouping/AggregationSettingsButton.d.ts +14 -0
  229. package/dist/mui/src/grouping/AggregationSettingsButton.js +19 -0
  230. package/dist/mui/src/grouping/GroupingBar.d.ts +59 -0
  231. package/dist/mui/src/grouping/GroupingBar.js +267 -0
  232. package/dist/mui/src/grouping/StepExpandButton.d.ts +12 -0
  233. package/dist/mui/src/grouping/StepExpandButton.js +18 -0
  234. package/dist/mui/src/grouping/groupRow.d.ts +68 -0
  235. package/dist/mui/src/grouping/groupRow.js +129 -0
  236. package/dist/mui/src/headers/defaultHeader.d.ts +28 -0
  237. package/dist/mui/src/headers/defaultHeader.js +90 -0
  238. package/dist/mui/src/headers/defineHeader.d.ts +21 -0
  239. package/dist/mui/src/headers/defineHeader.js +68 -0
  240. package/dist/mui/src/headers/index.d.ts +5 -0
  241. package/dist/mui/src/headers/index.js +3 -0
  242. package/dist/mui/src/headers/meta.d.ts +18 -0
  243. package/dist/mui/src/headers/meta.js +11 -0
  244. package/dist/mui/src/headers/types.d.ts +84 -0
  245. package/dist/mui/src/headers/types.js +1 -0
  246. package/dist/mui/src/index.d.ts +110 -0
  247. package/dist/mui/src/index.js +79 -0
  248. package/dist/mui/src/layout/ResizeHandle.d.ts +30 -0
  249. package/dist/mui/src/layout/ResizeHandle.js +66 -0
  250. package/dist/mui/src/menus/columnMenu.d.ts +45 -0
  251. package/dist/mui/src/menus/columnMenu.js +144 -0
  252. package/dist/mui/src/menus/copy.d.ts +5 -0
  253. package/dist/mui/src/menus/copy.js +25 -0
  254. package/dist/mui/src/menus/index.d.ts +6 -0
  255. package/dist/mui/src/menus/index.js +4 -0
  256. package/dist/mui/src/menus/resolve.d.ts +15 -0
  257. package/dist/mui/src/menus/resolve.js +60 -0
  258. package/dist/mui/src/menus/rowMenu.d.ts +46 -0
  259. package/dist/mui/src/menus/rowMenu.js +76 -0
  260. package/dist/mui/src/pagination/PaginationArea.d.ts +16 -0
  261. package/dist/mui/src/pagination/PaginationArea.js +23 -0
  262. package/dist/mui/src/pivot/PivotTable.d.ts +18 -0
  263. package/dist/mui/src/pivot/PivotTable.js +100 -0
  264. package/dist/mui/src/provider/KokoTableProvider.d.ts +184 -0
  265. package/dist/mui/src/provider/KokoTableProvider.js +118 -0
  266. package/dist/mui/src/provider/labels.d.ts +85 -0
  267. package/dist/mui/src/provider/labels.js +104 -0
  268. package/dist/mui/src/registry.d.ts +30 -0
  269. package/dist/mui/src/registry.js +5 -0
  270. package/dist/mui/src/toolbar/GlobalFilterField.d.ts +36 -0
  271. package/dist/mui/src/toolbar/GlobalFilterField.js +78 -0
  272. package/dist/mui/src/virtual/VirtualTableContent.d.ts +41 -0
  273. package/dist/mui/src/virtual/VirtualTableContent.js +58 -0
  274. package/dist/mui/src/virtual/VirtualTableScreen.d.ts +13 -0
  275. package/dist/mui/src/virtual/VirtualTableScreen.js +23 -0
  276. package/dist/mui/src/virtual.d.ts +4 -0
  277. package/dist/mui/src/virtual.js +4 -0
  278. package/dist/mui/src/virtualColumns/VirtualColumnCard.d.ts +26 -0
  279. package/dist/mui/src/virtualColumns/VirtualColumnCard.js +29 -0
  280. package/dist/mui/src/virtualColumns/VirtualColumnDialog.d.ts +27 -0
  281. package/dist/mui/src/virtualColumns/VirtualColumnDialog.js +154 -0
  282. package/dist/mui/src/virtualColumns/VirtualColumnSection.d.ts +31 -0
  283. package/dist/mui/src/virtualColumns/VirtualColumnSection.js +55 -0
  284. package/dist/mui/src/virtualColumns/expression.d.ts +68 -0
  285. package/dist/mui/src/virtualColumns/expression.js +489 -0
  286. package/dist/mui/src/virtualColumns/useVirtualColumns.d.ts +67 -0
  287. package/dist/mui/src/virtualColumns/useVirtualColumns.js +111 -0
  288. package/dist/mui/src/virtualColumns/virtualColumnDefs.d.ts +29 -0
  289. package/dist/mui/src/virtualColumns/virtualColumnDefs.js +51 -0
  290. package/dist/mui-virtual.d.ts +1 -0
  291. package/dist/mui-virtual.js +2 -0
  292. package/dist/mui.d.ts +1 -0
  293. package/dist/mui.js +2 -0
  294. package/dist/query/src/index.d.ts +6 -0
  295. package/dist/query/src/index.js +7 -0
  296. package/dist/query/src/serverPivot.d.ts +42 -0
  297. package/dist/query/src/serverPivot.js +153 -0
  298. package/dist/query/src/useServerGrouping.d.ts +52 -0
  299. package/dist/query/src/useServerGrouping.js +186 -0
  300. package/dist/query/src/useServerTable.d.ts +20 -0
  301. package/dist/query/src/useServerTable.js +106 -0
  302. package/dist/query/src/useServerTableData.d.ts +48 -0
  303. package/dist/query/src/useServerTableData.js +64 -0
  304. package/dist/query/src/wire.d.ts +16 -0
  305. package/dist/query/src/wire.js +32 -0
  306. package/dist/query.d.ts +1 -0
  307. package/dist/query.js +2 -0
  308. package/package.json +125 -0
@@ -0,0 +1,525 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment, useMemo, useState, } from "react";
3
+ import { Box, Button, CircularProgress, IconButton, LinearProgress, Skeleton, Stack, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TableRow, Tooltip, Typography, } from "@mui/material";
4
+ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
5
+ import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
6
+ import { flexRender, } from "@tanstack/react-table";
7
+ import { columnMetaOf } from "../../../core/src/index";
8
+ import { HeaderCell, resolveCellAlign } from "./HeaderCell";
9
+ import { EditCell } from "./EditCell";
10
+ import { TableErrorFallback } from "./TableErrorFallback";
11
+ import { renderCellValue } from "./cellValue";
12
+ import { useCellRenderers } from "./cellRenderers";
13
+ import { useAggregations } from "../grouping/AggregateChip";
14
+ import { cellErrorSx, getCellError } from "./cellError";
15
+ import { explicitWidth, pinnedSx } from "./pinning";
16
+ import { resolveDensity } from "./density";
17
+ import { canExpandRow, isDisplayColumnId } from "../columns/displayColumns";
18
+ import { AggregateCellBox, GROUP_DEPTH_INDENT, GroupValueText, groupRowBgColor, groupRowHoverColor, renderAggregateCellContent, resolveGroupValue, } from "../grouping/groupRow";
19
+ import { resolveTableProps, useKokoTableConfig } from "../provider/KokoTableProvider";
20
+ import { ContextMenu, createRowMenuCtx, defaultRowMenuItems, } from "./menuItems";
21
+ /**
22
+ * 그룹 컬럼 배치 — 렌더 순서만 바꾼다.
23
+ * `reorder`: 왼쪽 고정 → 그룹 컬럼(grouping 순) → 나머지 → 오른쪽 고정.
24
+ * `remove`: 그룹 컬럼 제외.
25
+ * `false`: 선언 순서. `declaredRank`(선언 순서 + `columnOrder`, 그룹핑 무시)를 주면 그 순서로 되돌린다 —
26
+ * TanStack 은 `grouping` 상태가 있으면 자체 `groupedColumnMode`(기본 `reorder`)로 이미 그룹 컬럼을
27
+ * 앞으로 당겨 놓기 때문에, "그대로" 를 그리려면 되돌려야 한다.
28
+ * 고정 컬럼은 sticky 오프셋(`getStart`)이 TanStack 순서 기준이므로 자리를 옮기지 않는다.
29
+ */
30
+ export function arrangeGroupedColumns(columns, grouping, mode, declaredRank) {
31
+ if (grouping.length === 0)
32
+ return columns;
33
+ const groupSet = new Set(grouping);
34
+ if (mode === "remove")
35
+ return columns.filter((column) => !groupSet.has(column.id));
36
+ if (!mode && !declaredRank)
37
+ return columns;
38
+ const left = [];
39
+ const right = [];
40
+ const rest = [];
41
+ const byId = new Map();
42
+ for (const column of columns) {
43
+ const pinned = column.getIsPinned();
44
+ if (pinned === "left")
45
+ left.push(column);
46
+ else if (pinned === "right")
47
+ right.push(column);
48
+ else if (mode && groupSet.has(column.id))
49
+ byId.set(column.id, column);
50
+ else
51
+ rest.push(column);
52
+ }
53
+ if (declaredRank) {
54
+ const rank = (column) => declaredRank.get(column.id) ?? Number.MAX_SAFE_INTEGER;
55
+ rest.sort((a, b) => rank(a) - rank(b));
56
+ }
57
+ const groupColumns = grouping.map((id) => byId.get(id)).filter((c) => !!c);
58
+ return [...left, ...groupColumns, ...rest, ...right];
59
+ }
60
+ /**
61
+ * 선언 순서(+ `columnOrder` 상태) 기준 리프 컬럼 순위 — TanStack `_getOrderColumnsFn` 과 같은 규칙에서
62
+ * 그룹핑 재배치만 뺀 것. `getAllFlatColumns()` 는 `orderColumns` 를 거치지 않아 선언 순서가 살아 있다.
63
+ */
64
+ function declaredLeafRank(table) {
65
+ const leaves = table.getAllFlatColumns().filter((column) => !column.columns || column.columns.length === 0);
66
+ const order = table.getState().columnOrder ?? [];
67
+ let ordered = leaves;
68
+ if (order.length > 0) {
69
+ const remaining = [...leaves];
70
+ const picked = [];
71
+ for (const id of order) {
72
+ const index = remaining.findIndex((column) => column.id === id);
73
+ if (index > -1)
74
+ picked.push(remaining.splice(index, 1)[0]);
75
+ }
76
+ ordered = [...picked, ...remaining];
77
+ }
78
+ return new Map(ordered.map((column, index) => [column.id, index]));
79
+ }
80
+ /** 열 가상화에서 비어 있는 좌·우 폭을 채우는 셀 */
81
+ function PadCell({ width, component = "td" }) {
82
+ return (_jsx(TableCell, { component: component, "aria-hidden": true, "data-testid": `virtual-pad-${component === "th" ? "head" : "cell"}`, sx: { p: 0, border: 0, width, minWidth: width, maxWidth: width } }));
83
+ }
84
+ function formatAggregate(value) {
85
+ if (value === null || value === undefined)
86
+ return "-";
87
+ if (typeof value === "number") {
88
+ return Number.isInteger(value) ? value.toLocaleString("ko-KR") : value.toFixed(2);
89
+ }
90
+ return String(value);
91
+ }
92
+ function mergeSx(...parts) {
93
+ const list = [];
94
+ for (const part of parts) {
95
+ if (!part)
96
+ continue;
97
+ if (Array.isArray(part))
98
+ list.push(...part);
99
+ else
100
+ list.push(part);
101
+ }
102
+ return list;
103
+ }
104
+ /**
105
+ * 콘텐츠 구역 — 테이블 본문.
106
+ *
107
+ * 로딩·에러·빈 상태를 **제자리에서** 그린다. 조회 실패를 throw 해서 상위
108
+ * ErrorBoundary 로 올리면 테이블 트리가 통째로 파괴되고, 리마운트가 캐시의
109
+ * 에러를 다시 읽어 즉시 재-throw 하므로 경계 리셋으로 복구가 안 된다.
110
+ *
111
+ * 행 선택·확장·고정·리사이즈·순서는 TanStack 상태를 그대로 그린다. 기능을 켜는 것은
112
+ * `useDataTable` 의 `tableOptions` (`enableRowSelection`, `enableColumnResizing`,
113
+ * `getRowCanExpand` …) 몫이고, 체크박스·확장 토글·행 번호·행 액션 열은
114
+ * `withDisplayColumns()` 로 컬럼 목록에 끼운다.
115
+ */
116
+ export function TableContent(props) {
117
+ return _jsx(TableContentBase, { ...props });
118
+ }
119
+ export function TableContentBase(rawProps) {
120
+ const config = useKokoTableConfig();
121
+ const { components, labels } = config;
122
+ const { table, content, sort, groupItems, onToggleGroup, isGroupExpanded, onLoadMore, emptyMessage = labels.ui.empty, renderEmpty, skeletonRows = 5, onResetFilters, renderError, dense = true, density, maxHeight, stickyHeader = true, getRowProps, selectOnRowClick = false, renderDetailPanel, getDetailPanelProps, editing, renderEditDialog, fieldErrors: fieldErrorsProp, onNavigate, enableColumnMenu = true, columnMenuItems, onGroupingChange, aggregation: aggregationProp, onAggregationChange, onOpenFilter, headerCellProps, renderHeader, rowMenuItems, onExpandGroupToDepth, groupedColumnMode = "reorder", grouping: groupingProp, groupRowLayout = "columns", groupRowHeight, aggregateSelection: aggregateSelectionProp, onAggregateSelectionChange, virtual, virtualColumns, containerRef, } = resolveTableProps(rawProps, config.defaults);
123
+ const HeaderCellComponent = (components.HeaderCell ?? HeaderCell);
124
+ const EditCellComponent = (components.EditCell ?? EditCell);
125
+ const status = content?.getStatus();
126
+ const visibleLeafColumns = table.getVisibleLeafColumns();
127
+ const rows = table.getRowModel().rows;
128
+ const grouped = !!groupItems && groupItems.length > 0;
129
+ const headerGroupCount = table.getHeaderGroups().length;
130
+ /**
131
+ * 그룹 컬럼 배치가 실제로 적용되는 기준. 열 가상화·다단 헤더에서는 끈다 —
132
+ * 가상화는 가운데 컬럼 인덱스로 그리고, 다단 헤더는 colSpan 을 다시 계산할 수 없다.
133
+ * `null` 이면 TanStack 이 준 순서를 그대로 쓴다.
134
+ *
135
+ * TanStack 은 `grouping` 상태만 있어도 자체 `groupedColumnMode`(기본 `reorder`)로 리프 순서를
136
+ * 바꿔 놓는다. 그래서 `false` 도 "아무것도 안 함" 이 아니라 **선언 순서로 되돌리는** 일이다.
137
+ * (TanStack 옵션이 `remove` 면 컬럼 자체가 없어 여기서 되살릴 수 없다 — `tableOptions` 에서 끄면 된다.)
138
+ */
139
+ const groupingIds = groupingProp ?? table.getState().grouping ?? [];
140
+ const activeGroupedMode = grouped && groupingIds.length > 0 && !virtualColumns && headerGroupCount === 1
141
+ ? groupedColumnMode
142
+ : null;
143
+ const groupingKey = groupingIds.join("|");
144
+ const columnOrderState = table.getState().columnOrder;
145
+ const allFlatColumns = table.getAllFlatColumns();
146
+ const declaredRank = useMemo(() => (activeGroupedMode === null ? undefined : declaredLeafRank(table)), [table, activeGroupedMode, columnOrderState, allFlatColumns]);
147
+ const leafColumns = useMemo(() => activeGroupedMode === null
148
+ ? visibleLeafColumns
149
+ : arrangeGroupedColumns(visibleLeafColumns, groupingIds, activeGroupedMode, declaredRank), [visibleLeafColumns, groupingKey, activeGroupedMode, declaredRank]);
150
+ /**
151
+ * 열 가상화 — 고정(left/right) 컬럼은 전부, 가운데 컬럼은 보이는 범위만 그린다.
152
+ * 그려지지 않은 폭은 좌·우 패딩 셀 하나씩이 대신 차지해 스크롤 폭·고정 오프셋이 유지된다.
153
+ * 컬럼 그룹 헤더(2단 이상)는 colSpan 을 맞출 수 없어 가상화하지 않는다.
154
+ */
155
+ const columnLayout = useMemo(() => {
156
+ const headerGroups = table.getHeaderGroups();
157
+ if (!virtualColumns || headerGroups.length !== 1)
158
+ return null;
159
+ const center = table.getCenterVisibleLeafColumns();
160
+ const items = virtualColumns.items;
161
+ const padLeft = items.length > 0 ? items[0].start : 0;
162
+ const padRight = items.length > 0 ? virtualColumns.totalSize - items[items.length - 1].end : 0;
163
+ return {
164
+ left: table.getLeftVisibleLeafColumns(),
165
+ center: items.map((item) => center[item.index]).filter(Boolean),
166
+ right: table.getRightVisibleLeafColumns(),
167
+ padLeft,
168
+ padRight,
169
+ };
170
+ }, [table, virtualColumns, leafColumns]);
171
+ /** 실제로 그려지는 열 수 — colSpan 이 이 값을 넘으면 표가 넓어진다 */
172
+ const columnCount = columnLayout
173
+ ? columnLayout.left.length + columnLayout.center.length + columnLayout.right.length
174
+ + (columnLayout.padLeft > 0 ? 1 : 0) + (columnLayout.padRight > 0 ? 1 : 0)
175
+ : leafColumns.length;
176
+ const renderedColumns = columnLayout
177
+ ? [...columnLayout.left, ...columnLayout.center, ...columnLayout.right]
178
+ : leafColumns;
179
+ const spec = resolveDensity(density, dense);
180
+ const resizingEnabled = !!table.options.enableColumnResizing;
181
+ const fieldErrors = fieldErrorsProp ?? table.fieldErrors;
182
+ // Provider 레지스트리(셀·집계)는 내장 세트 위에 얹힌 것 — 예전 맵(`config.cellRenderers`)도 같은 레지스트리에 들어간다
183
+ const cellRegistry = useCellRenderers();
184
+ const aggregationRegistry = useAggregations();
185
+ const cellOptions = useMemo(() => ({ onNavigate, renderers: config.cellRenderers, registry: cellRegistry }), [onNavigate, config.cellRenderers, cellRegistry]);
186
+ // ── 행 컨텍스트 메뉴 ──
187
+ const [rowMenu, setRowMenu] = useState(null);
188
+ const openRowMenu = (event, partial) => {
189
+ if (event.defaultPrevented)
190
+ return;
191
+ const defaultItems = defaultRowMenuItems(partial, labels, config.menus.row);
192
+ const ctx = { ...createRowMenuCtx(partial, labels), defaultItems };
193
+ const items = rowMenuItems ? rowMenuItems(ctx) : defaultItems;
194
+ if (items.length === 0)
195
+ return;
196
+ event.preventDefault();
197
+ event.stopPropagation();
198
+ setRowMenu({ position: { top: event.clientY, left: event.clientX }, ctx, items });
199
+ };
200
+ // ── 그룹 행 집계 함수 선택 ──
201
+ const [internalSelection, setInternalSelection] = useState({});
202
+ const aggregateSelection = aggregateSelectionProp ?? internalSelection;
203
+ const selectAggregate = (columnId, fn) => {
204
+ onAggregateSelectionChange?.(columnId, fn);
205
+ if (aggregateSelectionProp === undefined)
206
+ setInternalSelection((prev) => ({ ...prev, [columnId]: fn }));
207
+ };
208
+ // ── 편집 상태 ──
209
+ const [editingCell, setEditingCell] = useState(null);
210
+ const [internalEditingRowId, setInternalEditingRowId] = useState(null);
211
+ const [modalRow, setModalRow] = useState(null);
212
+ const editingRowId = editing?.editingRowId !== undefined ? editing.editingRowId : internalEditingRowId;
213
+ const commit = (value, row, columnId) => {
214
+ if (editing?.onCommit) {
215
+ editing.onCommit(value, row, columnId);
216
+ return;
217
+ }
218
+ const editSpec = columnMetaOf(row.getAllCells().find((c) => c.column.id === columnId)?.column.columnDef)?.edit;
219
+ if (editSpec?.onCommit) {
220
+ editSpec.onCommit(value, row.original);
221
+ return;
222
+ }
223
+ row.original[columnId] = value;
224
+ };
225
+ const enterRowEdit = (row) => {
226
+ editing?.onEditRow?.(row);
227
+ if (editing?.mode === "row" && editing.editingRowId === undefined)
228
+ setInternalEditingRowId(row.id);
229
+ if (editing?.mode === "modal" && renderEditDialog)
230
+ setModalRow(row);
231
+ };
232
+ /**
233
+ * 원본 행 객체 → TanStack Row.
234
+ *
235
+ * 그룹 트리의 말단은 **그룹 순서**로 정렬돼 있고 `table.getRowModel().rows` 는
236
+ * 원본(정렬) 순서다. 순번으로 맞추면 펼친 그룹 아래에 다른 그룹의 행이 뜬다.
237
+ * 객체 동일성으로 찾아야 한다.
238
+ */
239
+ const rowByOriginal = useMemo(() => {
240
+ const map = new Map();
241
+ for (const row of rows)
242
+ map.set(row.original, row);
243
+ return map;
244
+ }, [rows]);
245
+ const renderCell = (cell, row, index, indent, rowEditing) => {
246
+ const column = cell.column;
247
+ const meta = columnMetaOf(column.columnDef);
248
+ const isDisplay = isDisplayColumnId(column.id);
249
+ const editSpec = meta?.edit;
250
+ const editable = !!editing && !!editSpec?.enabled && !isDisplay;
251
+ const isEditingCell = editable && (editing.mode === "cell"
252
+ ? editingCell?.rowId === row.id && editingCell.columnId === column.id
253
+ : editing.mode === "row" && rowEditing);
254
+ const error = fieldErrors ? getCellError(fieldErrors, row.index, column.id) : { hasError: false };
255
+ // 열 가상화는 모든 셀에 명시 폭이 있어야 패딩 셀과 합이 맞는다
256
+ const width = columnLayout ? column.getSize() : explicitWidth(column, table);
257
+ const sx = mergeSx(index === 0 && indent > 0 && { pl: 2 + indent * 2 }, meta?.ellipsis && { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: width ?? 200 }, spec.cellPy !== undefined && { py: spec.cellPy }, spec.cellPx !== undefined && { px: spec.cellPx }, width !== undefined && { width, minWidth: width, ...(resizingEnabled ? { maxWidth: width } : {}) }, pinnedSx(column, 1), editable && editing.mode === "cell" && !isEditingCell && {
258
+ cursor: "cell", "&:hover": { backgroundColor: "action.hover" },
259
+ }, isEditingCell && { py: 0.25 }, error.hasError && cellErrorSx);
260
+ const inner = isEditingCell ? (_jsx(EditCellComponent, { spec: editSpec, value: cell.getValue(), row: row.original, onCommit: (value) => {
261
+ commit(value, row, column.id);
262
+ if (editing.mode === "cell")
263
+ setEditingCell(null);
264
+ }, onCancel: () => {
265
+ if (editing.mode === "cell")
266
+ setEditingCell(null);
267
+ else if (editing.editingRowId === undefined)
268
+ setInternalEditingRowId(null);
269
+ editing.onCancel?.(row);
270
+ } })) : renderCellValue(cell, cellOptions);
271
+ const cellEl = (_jsx(TableCell, { align: isDisplay ? "center" : resolveCellAlign(column.columnDef), size: spec.size, "data-column-id": column.id, "aria-invalid": error.hasError || undefined, onDoubleClick: editable && editing.mode === "cell"
272
+ ? (event) => { event.stopPropagation(); setEditingCell({ rowId: row.id, columnId: column.id }); }
273
+ : undefined, sx: sx, children: inner }, cell.id));
274
+ return error.hasError && error.message ? (_jsx(Tooltip, { title: error.message, arrow: true, placement: "top", children: cellEl }, cell.id)) : cellEl;
275
+ };
276
+ /** 행의 셀 — 열 가상화가 켜져 있으면 고정 + 보이는 가운데 + 패딩으로 구성한다 */
277
+ const renderRowCells = (row, indent, rowEditing) => {
278
+ if (!columnLayout) {
279
+ const cells = activeGroupedMode !== null
280
+ ? arrangeGroupedColumns(row.getVisibleCells().map((cell) => ({ id: cell.column.id, getIsPinned: () => cell.column.getIsPinned(), cell })), groupingIds, activeGroupedMode, declaredRank).map((entry) => entry.cell)
281
+ : row.getVisibleCells();
282
+ return cells.map((cell, index) => renderCell(cell, row, index, indent, rowEditing));
283
+ }
284
+ const byColumn = new Map(row.getAllCells().map((cell) => [cell.column.id, cell]));
285
+ const cellOf = (columnId) => byColumn.get(columnId);
286
+ const out = [];
287
+ let index = 0;
288
+ for (const column of columnLayout.left)
289
+ out.push(renderCell(cellOf(column.id), row, index++, indent, rowEditing));
290
+ if (columnLayout.padLeft > 0)
291
+ out.push(_jsx(PadCell, { width: columnLayout.padLeft }, "pad-left"));
292
+ for (const column of columnLayout.center)
293
+ out.push(renderCell(cellOf(column.id), row, index++, indent, rowEditing));
294
+ if (columnLayout.padRight > 0)
295
+ out.push(_jsx(PadCell, { width: columnLayout.padRight }, "pad-right"));
296
+ for (const column of columnLayout.right)
297
+ out.push(renderCell(cellOf(column.id), row, index++, indent, rowEditing));
298
+ return out;
299
+ };
300
+ const renderLeafRow = (row, key, indent = 0, virtualIndex) => {
301
+ const rowProps = getRowProps?.(row, table) ?? {};
302
+ const { sx: rowSx, onClick: rowOnClick, onDoubleClick: rowOnDoubleClick, onContextMenu: rowOnContextMenu, selected, hover, ...restRowProps } = rowProps;
303
+ const rowEditing = editing?.mode === "row" && editingRowId === row.id;
304
+ const expanded = !!renderDetailPanel && canExpandRow(row, table) && row.getIsExpanded();
305
+ const clickable = selectOnRowClick && row.getCanSelect();
306
+ const onClick = (event) => {
307
+ rowOnClick?.(event);
308
+ if (clickable && !event.defaultPrevented)
309
+ row.toggleSelected();
310
+ };
311
+ const onDoubleClick = (event) => {
312
+ rowOnDoubleClick?.(event);
313
+ if (editing && (editing.mode === "row" || editing.mode === "modal") && !event.defaultPrevented)
314
+ enterRowEdit(row);
315
+ };
316
+ const onContextMenu = (event) => {
317
+ rowOnContextMenu?.(event);
318
+ openRowMenu(event, { row, table });
319
+ };
320
+ return (_jsxs(Fragment, { children: [_jsx(TableRow, { hover: hover ?? true, selected: selected ?? row.getIsSelected(), "data-index": virtualIndex, "data-row-id": row.id, ref: virtual?.measureElement, ...restRowProps, onClick: onClick, onDoubleClick: onDoubleClick, onContextMenu: onContextMenu, sx: mergeSx(clickable && { cursor: "pointer" }, rowEditing && { backgroundColor: "action.selected" }, rowSx), children: renderRowCells(row, indent, rowEditing) }), expanded && (() => {
321
+ const panelProps = getDetailPanelProps?.(row, table);
322
+ const { sx: panelRowSx, ...panelRowRest } = panelProps?.row ?? {};
323
+ const { sx: panelCellSx, ...panelCellRest } = panelProps?.cell ?? {};
324
+ const panelContent = renderDetailPanel(row, table);
325
+ if (components.DetailPanelRow) {
326
+ return components.DetailPanelRow({ row, table, columnCount, content: panelContent });
327
+ }
328
+ return (_jsx(TableRow, { className: "koko-detail-row", "data-row-id": row.id, ...panelRowRest, sx: panelRowSx, children: _jsx(TableCell, { colSpan: columnCount, ...panelCellRest, sx: mergeSx({ p: 0, borderBottom: "1px solid", borderColor: "divider" }, panelCellSx), children: _jsx(Box, { sx: { p: 2, backgroundColor: "background.default", borderTop: "1px solid", borderColor: "divider" }, children: panelContent }) }) }));
329
+ })()] }, key));
330
+ };
331
+ /**
332
+ * 그룹 행.
333
+ *
334
+ * `groupRowLayout="columns"`(기본) 는 ERP 의 `ReorderGroupRow`/`RemoveGroupRow` 와 같은 배치다 —
335
+ * 현재 depth 의 그룹 컬럼 셀에 펼침 아이콘 + 그룹값 + `(n)`, 다른 그룹 컬럼은 빈 셀,
336
+ * 나머지 컬럼에는 그 컬럼의 집계 칩. 그룹 컬럼이 그려지지 않는 경우(`remove` 모드·숨김)는 첫 데이터
337
+ * 컬럼 셀이 그룹값을 맡는다. 열 가상화가 켜져 있으면 말단 행과 같은 `PadCell` 규칙으로 채운다.
338
+ * 배경은 depth 별 단계 색이고 sticky 는 하지 않는다 — 가상화의 위치 계산과 충돌한다.
339
+ */
340
+ const renderGroupRow = (item, virtualIndex) => {
341
+ const node = item.node;
342
+ const depth = item.depth;
343
+ const expanded = isGroupExpanded?.(node) ?? false;
344
+ const toggle = () => onToggleGroup?.(node);
345
+ const groupColumnId = groupingIds[depth] ?? node.groupKey;
346
+ const groupColumn = table.getAllFlatColumns().find((column) => column.id === groupColumnId);
347
+ const groupSet = new Set(groupingIds.length > 0 ? groupingIds : [node.groupKey]);
348
+ const toggleButton = (_jsx(IconButton, { size: "small", "aria-label": expanded ? labels.ui.collapse : labels.ui.expand, sx: { p: 0.25, color: expanded ? "primary.main" : "action.active" },
349
+ // 행에도 핸들러가 있으므로 버블링을 막지 않으면 두 번 토글된다
350
+ onClick: (event) => {
351
+ event.stopPropagation();
352
+ toggle();
353
+ }, children: node.isPending
354
+ ? _jsx(CircularProgress, { size: 14 })
355
+ : expanded ? _jsx(KeyboardArrowDownIcon, { fontSize: "small" }) : _jsx(KeyboardArrowRightIcon, { fontSize: "small" }) }));
356
+ const groupValue = (_jsxs(_Fragment, { children: [_jsx(GroupValueText, { children: resolveGroupValue(node, groupColumn) }), _jsxs(Typography, { component: "span", variant: "caption", color: "text.secondary", sx: { whiteSpace: "nowrap" }, children: ["(", node.count, ")"] })] }));
357
+ const rowProps = {
358
+ "data-index": virtualIndex,
359
+ "data-group-depth": depth,
360
+ ref: virtual?.measureElement,
361
+ // 화살표만 누를 수 있으면 표적이 너무 작다 — 행 전체를 누를 수 있게 한다
362
+ onClick: toggle,
363
+ onKeyDown: (event) => {
364
+ if (event.key === "Enter" || event.key === " ") {
365
+ event.preventDefault();
366
+ toggle();
367
+ }
368
+ },
369
+ tabIndex: onToggleGroup ? 0 : -1,
370
+ onContextMenu: (event) => openRowMenu(event, {
371
+ table,
372
+ group: {
373
+ node, depth, expanded, toggle,
374
+ subLevels: groupingIds.slice(depth + 1),
375
+ expandToDepth: onExpandGroupToDepth ? (target) => onExpandGroupToDepth(target, node) : undefined,
376
+ },
377
+ }),
378
+ sx: mergeSx({ backgroundColor: groupRowBgColor(depth) }, groupRowHeight !== undefined && { height: groupRowHeight }, !!onToggleGroup && { cursor: "pointer", "&:hover": { backgroundColor: groupRowHoverColor(depth) } }),
379
+ };
380
+ const rowKey = `group-${node.path.map((p) => String(p.value)).join("/")}`;
381
+ if (components.GroupRow) {
382
+ return (_jsx(Fragment, { children: components.GroupRow({
383
+ node, depth, columns: renderedColumns, aggregates: node.aggregates ?? {}, toggle, expanded, table,
384
+ columnCount, groupColumn, groupValue: resolveGroupValue(node, groupColumn), rowProps,
385
+ }) }, rowKey));
386
+ }
387
+ // 그룹값을 맡을 셀 — 그룹 컬럼이 그려지면 그 셀, 아니면(remove·숨김) 첫 데이터 컬럼
388
+ const dataColumns = renderedColumns.filter((column) => !isDisplayColumnId(column.id));
389
+ const hostId = dataColumns.some((column) => column.id === groupColumnId)
390
+ ? groupColumnId
391
+ : (dataColumns.find((column) => !groupSet.has(column.id)) ?? dataColumns[0])?.id;
392
+ if (groupRowLayout === "inline" || hostId === undefined) {
393
+ const aggregates = node.aggregates ?? {};
394
+ return (_jsx(TableRow, { ...rowProps, children: _jsx(TableCell, { colSpan: columnCount, size: spec.size, sx: { pl: 1 + depth * GROUP_DEPTH_INDENT, py: 0.5 }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { flexWrap: "wrap", rowGap: 0.25 }, children: [toggleButton, groupValue, Object.entries(aggregates).map(([columnId, perFn]) => Object.entries(perFn).map(([fn, value]) => (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: [columnId, " ", labels.aggregationsShort[fn] ?? fn, " ", formatAggregate(value)] }, `${columnId}-${fn}`))))] }) }) }, rowKey));
395
+ }
396
+ const groupCell = (column) => {
397
+ const width = columnLayout ? column.getSize() : explicitWidth(column, table);
398
+ const pinned = !!column.getIsPinned();
399
+ const base = mergeSx({ py: 0.5 }, spec.cellPx !== undefined && { px: spec.cellPx }, width !== undefined && { width, minWidth: width, ...(resizingEnabled || columnLayout ? { maxWidth: width } : {}) }, pinnedSx(column, 1),
400
+ // 고정 셀은 sticky 배경이 필요하다 — 행 배경 대신 같은 단계 색을 준다
401
+ pinned && { backgroundColor: groupRowBgColor(depth) });
402
+ if (column.id === hostId) {
403
+ return (_jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "value", sx: base, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, sx: { pl: depth * GROUP_DEPTH_INDENT, minWidth: 0 }, children: [toggleButton, groupValue] }) }, column.id));
404
+ }
405
+ if (isDisplayColumnId(column.id)) {
406
+ return _jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "display", sx: base }, column.id);
407
+ }
408
+ if (groupSet.has(column.id)) {
409
+ return (_jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "empty", sx: mergeSx(base, { borderBottom: "none" }) }, column.id));
410
+ }
411
+ const content = renderAggregateCellContent({
412
+ column, node, selection: aggregateSelection, onSelect: selectAggregate, chip: components.AggregateChip,
413
+ registry: aggregationRegistry,
414
+ });
415
+ return (_jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "aggregate", align: "center", sx: base, children: content === null || content === undefined ? null : _jsx(AggregateCellBox, { children: content }) }, column.id));
416
+ };
417
+ let cells;
418
+ if (!columnLayout) {
419
+ cells = renderedColumns.map(groupCell);
420
+ }
421
+ else {
422
+ const out = [];
423
+ for (const column of columnLayout.left)
424
+ out.push(groupCell(column));
425
+ if (columnLayout.padLeft > 0)
426
+ out.push(_jsx(PadCell, { width: columnLayout.padLeft }, "pad-left"));
427
+ for (const column of columnLayout.center)
428
+ out.push(groupCell(column));
429
+ if (columnLayout.padRight > 0)
430
+ out.push(_jsx(PadCell, { width: columnLayout.padRight }, "pad-right"));
431
+ for (const column of columnLayout.right)
432
+ out.push(groupCell(column));
433
+ cells = out;
434
+ }
435
+ return _jsx(TableRow, { ...rowProps, children: cells }, rowKey);
436
+ };
437
+ /**
438
+ * 서버 그룹핑에서 하위가 페이지 단위로 온다. 몇 개 중 몇 개를 받았는지
439
+ * 같이 보여 주지 않으면 사용자가 "이게 전부인가" 를 알 수 없다.
440
+ */
441
+ const renderLoadMoreRow = (item, virtualIndex) => (_jsx(TableRow, { "data-index": virtualIndex, ref: virtual?.measureElement, children: _jsx(TableCell, { colSpan: columnCount, size: spec.size, sx: { pl: 1 + item.depth * 2, py: 0.25 }, children: _jsx(Button, { size: "small", disabled: item.isPending, onClick: () => onLoadMore?.(item.path), children: item.isPending ? labels.ui.loading : labels.ui.loadMore(item.loaded, item.total) }) }) }, `more-${item.index}`));
442
+ const renderGroupItem = (item, virtualIndex) => {
443
+ if (item.type === "group")
444
+ return renderGroupRow(item, virtualIndex);
445
+ if (item.type === "loadMore")
446
+ return renderLoadMoreRow(item, virtualIndex);
447
+ const row = rowByOriginal.get(item.row);
448
+ if (row)
449
+ return renderLeafRow(row, `leaf-${item.index}`, item.depth, virtualIndex);
450
+ // 그룹 트리에는 있는데 테이블 행 모델에 없다 — 서버 그룹핑에서
451
+ // `useServerGrouping().rows` 를 3층에 안 넘긴 경우다.
452
+ // 조용히 비우면 "행이 없는 그룹" 으로 보여 원인을 찾기 어렵다.
453
+ return (_jsx(TableRow, { "data-index": virtualIndex, ref: virtual?.measureElement, children: _jsx(TableCell, { colSpan: columnCount, sx: { pl: 1 + item.depth * 2, py: 0.5, color: "warning.main" }, children: labels.ui.missingRows }) }, `missing-${item.index}`));
454
+ };
455
+ const renderItem = (index, virtualIndex) => grouped ? renderGroupItem(groupItems[index], virtualIndex) : renderLeafRow(rows[index], rows[index].id, 0, virtualIndex);
456
+ const itemCount = grouped ? groupItems.length : rows.length;
457
+ const renderSkeleton = () => {
458
+ if (components.LoadingSkeleton) {
459
+ const LoadingSkeleton = components.LoadingSkeleton;
460
+ return _jsx(LoadingSkeleton, { rows: skeletonRows, columns: renderedColumns, columnCount: columnCount, size: spec.size });
461
+ }
462
+ if (skeletonRows <= 0) {
463
+ return (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columnCount, sx: { py: 6, textAlign: "center", color: "text.secondary" }, children: labels.ui.loading }) }));
464
+ }
465
+ return Array.from({ length: skeletonRows }, (_, i) => (_jsx(TableRow, { "data-testid": "skeleton-row", children: renderedColumns.map((column) => (_jsx(TableCell, { size: spec.size, children: _jsx(Skeleton, { variant: "text", animation: "wave" }) }, column.id))) }, `skeleton-${i}`)));
466
+ };
467
+ const body = () => {
468
+ if (status?.isError) {
469
+ const retry = () => content?.refetch();
470
+ const ErrorFallback = components.ErrorFallback ?? TableErrorFallback;
471
+ return (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columnCount, sx: { py: 2 }, children: renderError
472
+ ? renderError(status.error, retry)
473
+ : (_jsx(ErrorFallback, { compact: true, error: status.error, onRetry: content ? retry : undefined, onResetFilters: onResetFilters })) }) }));
474
+ }
475
+ if (status?.isInitialLoading)
476
+ return renderSkeleton();
477
+ if (itemCount === 0) {
478
+ const EmptyState = components.EmptyState;
479
+ return (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columnCount, sx: { py: 6, textAlign: "center", color: "text.secondary" }, children: renderEmpty
480
+ ? renderEmpty()
481
+ : EmptyState
482
+ ? _jsx(EmptyState, { message: emptyMessage, table: table, columnCount: columnCount })
483
+ : emptyMessage }) }));
484
+ }
485
+ if (virtual) {
486
+ const items = virtual.items;
487
+ const paddingTop = items.length > 0 ? items[0].start : 0;
488
+ const paddingBottom = items.length > 0 ? virtual.totalSize - items[items.length - 1].end : 0;
489
+ return (_jsxs(_Fragment, { children: [paddingTop > 0 && (_jsx(TableRow, { "aria-hidden": true, "data-testid": "virtual-pad-top", children: _jsx(TableCell, { colSpan: columnCount, sx: { p: 0, border: 0, height: paddingTop } }) })), items.map((item) => renderItem(item.index, item.index)), paddingBottom > 0 && (_jsx(TableRow, { "aria-hidden": true, "data-testid": "virtual-pad-bottom", children: _jsx(TableCell, { colSpan: columnCount, sx: { p: 0, border: 0, height: paddingBottom } }) }))] }));
490
+ }
491
+ return _jsx(_Fragment, { children: Array.from({ length: itemCount }, (_, index) => renderItem(index)) });
492
+ };
493
+ const hasFooter = leafColumns.some((column) => column.columnDef.footer !== undefined);
494
+ /**
495
+ * 열 가상화 헤더/푸터 — 단일 헤더 그룹의 header 를 컬럼 id 로 찾아 고정·가운데·패딩 순으로 놓는다.
496
+ * `HeaderCell` 은 그대로 쓴다(가상화를 몰라도 된다).
497
+ */
498
+ const renderVirtualHeaderRow = (headers, renderOne) => {
499
+ if (!columnLayout)
500
+ return null;
501
+ const byColumn = new Map(headers.map((header) => [header.column.id, header]));
502
+ const one = (columnId) => {
503
+ const header = byColumn.get(columnId);
504
+ return header ? renderOne(header) : null;
505
+ };
506
+ return (_jsxs(_Fragment, { children: [columnLayout.left.map((column) => one(column.id)), columnLayout.padLeft > 0 && _jsx(PadCell, { component: "th", width: columnLayout.padLeft }, "pad-left"), columnLayout.center.map((column) => one(column.id)), columnLayout.padRight > 0 && _jsx(PadCell, { component: "th", width: columnLayout.padRight }, "pad-right"), columnLayout.right.map((column) => one(column.id))] }));
507
+ };
508
+ /** 그룹 컬럼 배치를 헤더·푸터 행에도 적용한다 (가상화가 아닐 때만 — 가상화는 위 함수가 맡는다) */
509
+ const arrangeHeaders = (headers) => {
510
+ if (activeGroupedMode === null)
511
+ return headers;
512
+ return arrangeGroupedColumns(headers.map((header) => ({ id: header.column.id, getIsPinned: () => header.column.getIsPinned(), header })), groupingIds, activeGroupedMode, declaredRank).map((entry) => entry.header);
513
+ };
514
+ const renderFooterCell = (header) => (_jsx(TableCell, { colSpan: header.colSpan, size: spec.size, sx: mergeSx({ fontWeight: 600, color: "text.primary", borderTop: "2px solid", borderColor: "divider", whiteSpace: "nowrap" }, columnLayout && { width: header.column.getSize(), minWidth: header.column.getSize() }, pinnedSx(header.column, 1)), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.footer, header.getContext()) }, header.id));
515
+ const fixedLayout = resizingEnabled || !!columnLayout;
516
+ const renderHeaderCell = (header) => (_jsx(HeaderCellComponent, { header: header, sort: sort, table: table, enableColumnMenu: enableColumnMenu, size: spec.size, columnMenuItems: columnMenuItems, grouping: groupingProp, onGroupingChange: onGroupingChange, aggregation: aggregationProp, onAggregationChange: onAggregationChange, onOpenFilter: onOpenFilter, headerCellProps: headerCellProps ? (header, tbl) => headerCellProps(header, tbl ?? table) : undefined, renderHeader: renderHeader }, header.id));
517
+ return (_jsxs(Box, { sx: { position: "relative" }, children: [status?.isLoading && !status.isInitialLoading && (_jsx(LinearProgress, { sx: { position: "absolute", top: 0, left: 0, right: 0, zIndex: 2 } })), _jsx(TableContainer, { ref: containerRef, sx: { maxHeight, border: "1px solid", borderColor: "divider", borderRadius: 1 }, children: _jsxs(Table, { stickyHeader: stickyHeader, size: spec.size, sx: fixedLayout ? { tableLayout: "fixed", width: table.getTotalSize() } : undefined, children: [columnLayout && (
518
+ // 고정 레이아웃에서 열 폭은 첫 행(헤더)이 정한다 — 헤더 셀에 폭이 없는 컬럼(기본 150)도
519
+ // 패딩 셀과 합이 맞아야 하므로 colgroup 으로 폭을 못박는다
520
+ _jsxs("colgroup", { children: [columnLayout.left.map((column) => _jsx("col", { style: { width: column.getSize() } }, column.id)), columnLayout.padLeft > 0 && _jsx("col", { style: { width: columnLayout.padLeft } }, "pad-left"), columnLayout.center.map((column) => _jsx("col", { style: { width: column.getSize() } }, column.id)), columnLayout.padRight > 0 && _jsx("col", { style: { width: columnLayout.padRight } }, "pad-right"), columnLayout.right.map((column) => _jsx("col", { style: { width: column.getSize() } }, column.id))] })), _jsx(TableHead, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { children: columnLayout
521
+ ? renderVirtualHeaderRow(headerGroup.headers, (header) => renderHeaderCell(header))
522
+ : arrangeHeaders(headerGroup.headers).map((header) => renderHeaderCell(header)) }, headerGroup.id))) }), _jsx(TableBody, { children: body() }), hasFooter && (_jsx(TableFooter, { children: table.getFooterGroups().map((footerGroup) => (_jsx(TableRow, { children: columnLayout
523
+ ? renderVirtualHeaderRow(footerGroup.headers, (header) => renderFooterCell(header))
524
+ : arrangeHeaders(footerGroup.headers).map((header) => renderFooterCell(header)) }, footerGroup.id))) }))] }) }), modalRow && renderEditDialog && renderEditDialog(modalRow, () => setModalRow(null)), _jsx(ContextMenu, { position: rowMenu?.position ?? null, items: rowMenu?.items ?? [], ctx: rowMenu?.ctx ?? { ...createRowMenuCtx({ table }, labels), defaultItems: [] }, onClose: () => setRowMenu(null) })] }));
525
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+ export interface TableErrorFallbackProps {
3
+ error?: Error | null;
4
+ /** 같은 조건으로 다시 조회 (`data.refetch`) */
5
+ onRetry?: () => void;
6
+ /** 있으면 "필터 초기화" 버튼을 그린다 (`search.reset`) */
7
+ onResetFilters?: () => void;
8
+ /** 400 이 아닐 때의 제목. 기본 `labels.ui.loadFailed` */
9
+ title?: ReactNode;
10
+ /** 표 본문 안의 행에 들어갈 때 여백을 줄인다 */
11
+ compact?: boolean;
12
+ }
13
+ /** axios(`response.status`)·fetch 래퍼(`status`) 양쪽에서 HTTP 상태를 읽는다 */
14
+ export declare function errorStatus(error: unknown): number | undefined;
15
+ export declare function isBadRequestError(error: unknown): boolean;
16
+ /**
17
+ * 조회 실패 UI — 기존 `TableErrorFallback.tsx` 대응.
18
+ *
19
+ * 400 이면 "필터를 해석할 수 없습니다"(서버가 컬럼 경로·연산자를 거부한 것) 로 구분하고,
20
+ * 재시도와 함께 `onResetFilters` 가 있을 때만 "필터 초기화" 를 보여 준다.
21
+ * ErrorBoundary 를 쓰지 않으므로 "테이블 재시작" 은 없다 — 에러는 `TableData.error` 로
22
+ * 내려와 제자리에서 그려지고, 재조회하면 그대로 복구된다.
23
+ */
24
+ export declare function TableErrorFallback({ error, onRetry, onResetFilters, title, compact, }: TableErrorFallbackProps): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert, Box, Button, Stack, Typography } from "@mui/material";
3
+ import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
4
+ import RefreshIcon from "@mui/icons-material/Refresh";
5
+ import { useKokoTableConfig } from "../provider/KokoTableProvider";
6
+ /** axios(`response.status`)·fetch 래퍼(`status`) 양쪽에서 HTTP 상태를 읽는다 */
7
+ export function errorStatus(error) {
8
+ if (!error || typeof error !== "object")
9
+ return undefined;
10
+ const e = error;
11
+ const status = e.status ?? e.response?.status;
12
+ return typeof status === "number" ? status : undefined;
13
+ }
14
+ export function isBadRequestError(error) {
15
+ return errorStatus(error) === 400;
16
+ }
17
+ /**
18
+ * 조회 실패 UI — 기존 `TableErrorFallback.tsx` 대응.
19
+ *
20
+ * 400 이면 "필터를 해석할 수 없습니다"(서버가 컬럼 경로·연산자를 거부한 것) 로 구분하고,
21
+ * 재시도와 함께 `onResetFilters` 가 있을 때만 "필터 초기화" 를 보여 준다.
22
+ * ErrorBoundary 를 쓰지 않으므로 "테이블 재시작" 은 없다 — 에러는 `TableData.error` 로
23
+ * 내려와 제자리에서 그려지고, 재조회하면 그대로 복구된다.
24
+ */
25
+ export function TableErrorFallback({ error, onRetry, onResetFilters, title, compact = false, }) {
26
+ const { labels } = useKokoTableConfig();
27
+ const badRequest = isBadRequestError(error);
28
+ return (_jsxs(Box, { role: "alert", sx: { p: compact ? 1 : 3, textAlign: "center" }, children: [_jsxs(Alert, { severity: "error", icon: _jsx(ErrorOutlineIcon, {}), sx: { mb: compact ? 1 : 2, textAlign: "left" }, children: [_jsx(Typography, { variant: compact ? "subtitle2" : "h6", sx: { mb: 0.5 }, children: badRequest ? labels.ui.badRequest : (title ?? labels.ui.loadFailed) }), error?.message && (_jsx(Typography, { variant: "body2", color: "text.secondary", children: error.message }))] }), _jsxs(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", children: [onRetry && (_jsx(Button, { size: "small", variant: "contained", startIcon: _jsx(RefreshIcon, {}), onClick: onRetry, children: labels.ui.retry })), onResetFilters && (_jsx(Button, { size: "small", variant: "outlined", onClick: onResetFilters, children: labels.ui.resetFilters }))] })] }));
29
+ }
@@ -0,0 +1,13 @@
1
+ import type { SxProps, Theme } from "@mui/material";
2
+ import type { FieldErrors } from "../../../core/src/index";
3
+ export interface CellErrorInfo {
4
+ hasError: boolean;
5
+ message?: string;
6
+ }
7
+ /**
8
+ * `fieldErrors[rowIndex][columnId]` 를 읽는다. react-hook-form `FieldErrors` 와 같은 모양이라
9
+ * 폼 검증 결과를 그대로 넘길 수 있다. `rowIndex` 는 TanStack `row.index`(페이지 내 순번)다.
10
+ */
11
+ export declare function getCellError(fieldErrors: FieldErrors, rowIndex: number, columnId: string): CellErrorInfo;
12
+ /** 에러 셀 스타일 — 빨간 배경 + 셀 안쪽 outline (기존 `CellError.tsx` 와 같다) */
13
+ export declare const cellErrorSx: SxProps<Theme>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `fieldErrors[rowIndex][columnId]` 를 읽는다. react-hook-form `FieldErrors` 와 같은 모양이라
3
+ * 폼 검증 결과를 그대로 넘길 수 있다. `rowIndex` 는 TanStack `row.index`(페이지 내 순번)다.
4
+ */
5
+ export function getCellError(fieldErrors, rowIndex, columnId) {
6
+ const cellError = fieldErrors?.[rowIndex]?.[columnId];
7
+ if (!cellError)
8
+ return { hasError: false };
9
+ const message = typeof cellError === "object" && "message" in cellError
10
+ ? cellError.message
11
+ : undefined;
12
+ return { hasError: true, message };
13
+ }
14
+ /** 에러 셀 스타일 — 빨간 배경 + 셀 안쪽 outline (기존 `CellError.tsx` 와 같다) */
15
+ export const cellErrorSx = {
16
+ backgroundColor: "rgba(211, 47, 47, 0.08)",
17
+ outline: "1px solid",
18
+ outlineColor: "error.main",
19
+ outlineOffset: "-2px",
20
+ };