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,162 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { Box, Button, Chip, Collapse, Divider, IconButton, Paper, Stack, Tooltip, Typography, useMediaQuery, useTheme, } from "@mui/material";
4
+ import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
5
+ import ChevronRightIcon from "@mui/icons-material/ChevronRight";
6
+ import ExpandLessIcon from "@mui/icons-material/ExpandLess";
7
+ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
8
+ import FilterListIcon from "@mui/icons-material/FilterList";
9
+ import RestartAltIcon from "@mui/icons-material/RestartAlt";
10
+ import SearchIcon from "@mui/icons-material/Search";
11
+ import { ResizeHandle, clampWidth, useResizeWidth } from "./layout/ResizeHandle";
12
+ import { useKokoTableConfig } from "./provider/KokoTableProvider";
13
+ /** 검색·초기화 버튼 — 필터 구역 아래에 붙는다 */
14
+ export function SearchBar({ onSearch, onReset, isDirty, disabled }) {
15
+ const { labels } = useKokoTableConfig();
16
+ return (_jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", startIcon: _jsx(RestartAltIcon, {}), onClick: onReset, disabled: disabled, children: labels.ui.reset }), _jsx(Button, { size: "small", variant: isDirty ? "contained" : "outlined", startIcon: _jsx(SearchIcon, {}), onClick: onSearch, disabled: disabled, children: labels.ui.search })] }));
17
+ }
18
+ /** 제목 + 좌·우 두 슬롯. 셋 다 없으면 그리지 않는다 */
19
+ function ToolbarRow({ title, left, right }) {
20
+ if (!title && !left && !right)
21
+ return null;
22
+ return (_jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 2, sx: { flexWrap: "wrap", rowGap: 1 }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 2, sx: { minWidth: 0, flex: "1 1 auto" }, children: [title && _jsx(Typography, { variant: "h6", sx: { whiteSpace: "nowrap" }, children: title }), left && _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { flex: "1 1 auto", minWidth: 0 }, children: left })] }), right && _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { flexShrink: 0 }, children: right })] }));
23
+ }
24
+ function readStored(storageKey) {
25
+ if (!storageKey || typeof window === "undefined")
26
+ return {};
27
+ try {
28
+ const raw = window.localStorage.getItem(storageKey);
29
+ if (!raw)
30
+ return {};
31
+ const parsed = JSON.parse(raw);
32
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
38
+ function writeStored(storageKey, patch) {
39
+ if (!storageKey || typeof window === "undefined")
40
+ return;
41
+ try {
42
+ window.localStorage.setItem(storageKey, JSON.stringify({ ...readStored(storageKey), ...patch }));
43
+ }
44
+ catch {
45
+ // 저장 실패는 조용히 — 기억 못 하는 것뿐이다
46
+ }
47
+ }
48
+ /** xs(모바일) 화면인가 — SSR 첫 렌더는 false */
49
+ function useIsXs() {
50
+ const theme = useTheme();
51
+ return useMediaQuery(theme.breakpoints.down("sm"), { noSsr: true });
52
+ }
53
+ /**
54
+ * 접힘 상태 — 제어/비제어 겸용.
55
+ * 비제어이고 `defaultCollapsed` 가 없으면 xs 로 **바뀔 때** 접히고, xs 를 벗어나면 펼쳐진다.
56
+ */
57
+ function useCollapsed(props, isXs) {
58
+ const { collapsible = false, collapsed: controlled, defaultCollapsed, onCollapsedChange, storageKey } = props;
59
+ const [internal, setInternal] = useState(() => {
60
+ if (!collapsible)
61
+ return false;
62
+ const stored = readStored(storageKey).collapsed;
63
+ if (stored !== undefined)
64
+ return stored;
65
+ return defaultCollapsed ?? isXs;
66
+ });
67
+ const isControlled = controlled !== undefined;
68
+ const value = collapsible ? (isControlled ? controlled : internal) : false;
69
+ const onChangeRef = useRef(onCollapsedChange);
70
+ onChangeRef.current = onCollapsedChange;
71
+ const set = useCallback((next) => {
72
+ if (!isControlled)
73
+ setInternal(next);
74
+ writeStored(storageKey, { collapsed: next });
75
+ onChangeRef.current?.(next);
76
+ }, [isControlled, storageKey]);
77
+ // xs 진입·이탈에 반응한다 — 첫 렌더는 초기값이 이미 반영했으므로 건너뛴다
78
+ const previousXs = useRef(isXs);
79
+ useEffect(() => {
80
+ if (previousXs.current === isXs)
81
+ return;
82
+ previousXs.current = isXs;
83
+ if (!collapsible || isControlled || defaultCollapsed !== undefined)
84
+ return;
85
+ setInternal(isXs);
86
+ onChangeRef.current?.(isXs);
87
+ }, [isXs, collapsible, isControlled, defaultCollapsed]);
88
+ return [value, set];
89
+ }
90
+ function CountChip({ count, small }) {
91
+ if (!count)
92
+ return null;
93
+ return (_jsx(Chip, { label: count, size: "small", color: "primary", sx: small ? { height: 18, minWidth: 18, fontSize: "0.7rem", "& .MuiChip-label": { px: 0.5 } } : { height: 20 } }));
94
+ }
95
+ /**
96
+ * 상단 필터 레이아웃 — 필터 / 콘텐츠 / 페이지네이션 세 구역을 세로로 쌓는다.
97
+ *
98
+ * 레이아웃은 세 구역을 **배치만** 한다. 각 구역이 무엇에 의존하는지는
99
+ * 구역 컴포넌트가 각자 정하므로, 여기서 테이블 인스턴스를 요구하지 않는다.
100
+ * `collapsible` 이면 필터 Paper 에 접기 헤더가 생기고, 접힌 상태에는 요약 문구만 남는다.
101
+ */
102
+ export function TopFilterLayout(props) {
103
+ const { filter, content, pagination, toolbar, toolbarLeft, toolbarRight, title, collapsible = false, activeCount, filterTitle = "검색 조건", collapsedSummary, } = props;
104
+ const isXs = useIsXs();
105
+ const [collapsed, setCollapsed] = useCollapsed(props, isXs);
106
+ const summary = collapsedSummary ?? (activeCount ? `${activeCount}개 조건 적용 중` : "조건 없음");
107
+ return (_jsxs(Stack, { spacing: 2, children: [_jsx(ToolbarRow, { title: title, left: toolbarLeft, right: toolbarRight ?? toolbar }), filter && (_jsxs(Paper, { variant: "outlined", sx: { p: collapsible ? 0 : 2 }, children: [collapsible && (_jsxs(Box, { onClick: () => setCollapsed(!collapsed), sx: {
108
+ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1,
109
+ px: 2, py: 1, cursor: "pointer", userSelect: "none",
110
+ borderBottom: collapsed ? "none" : "1px solid", borderColor: "divider",
111
+ "&:hover": { bgcolor: "action.hover" },
112
+ }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { minWidth: 0 }, children: [_jsx(FilterListIcon, { fontSize: "small", color: "action" }), _jsx(Typography, { variant: "subtitle2", sx: { whiteSpace: "nowrap" }, children: filterTitle }), _jsx(CountChip, { count: activeCount }), collapsed && (_jsx(Typography, { variant: "body2", color: "text.secondary", noWrap: true, "data-testid": "koko-collapsed-summary", children: summary }))] }), _jsx(IconButton, { size: "small", "aria-label": collapsed ? "필터 펼치기" : "필터 접기", "aria-expanded": !collapsed, onClick: (event) => { event.stopPropagation(); setCollapsed(!collapsed); }, children: collapsed ? _jsx(ExpandMoreIcon, { fontSize: "small" }) : _jsx(ExpandLessIcon, { fontSize: "small" }) })] })), collapsible ? (_jsx(Collapse, { in: !collapsed, timeout: 200, unmountOnExit: true, children: _jsx(Box, { sx: { p: 2 }, children: filter }) })) : filter] })), _jsx(Box, { children: content }), pagination] }));
113
+ }
114
+ const COLLAPSED_WIDTH = 44;
115
+ /**
116
+ * 사이드 필터 레이아웃 — 필터를 옆 패널로 뺀다 (기본 오른쪽).
117
+ *
118
+ * - `collapsible`: 패널을 세로 탭으로 접는다. xs 에서는 가로 막대가 된다
119
+ * - `resizable`: 안쪽 변의 드래그 핸들로 폭을 바꾼다 (`minWidth`~`maxWidth`)
120
+ * - `storageKey`: 접힘·폭을 localStorage 에 기억한다
121
+ */
122
+ export function SideFilterLayout(props) {
123
+ const { filter, content, pagination, toolbar, toolbarLeft, toolbarRight, title, filterWidth = 280, filterPosition = "right", collapsible = false, resizable = false, minWidth = 200, maxWidth = 640, onFilterWidthChange, activeCount, filterTitle = "검색 조건", storageKey, collapsedWidth = COLLAPSED_WIDTH, } = props;
124
+ const isXs = useIsXs();
125
+ const [collapsed, setCollapsed] = useCollapsed(props, isXs);
126
+ // 폭: 저장값 → prop 순. prop 이 바뀌면(제어) 따라간다
127
+ const [width, setWidth] = useState(() => {
128
+ const stored = resizable ? readStored(storageKey).width : undefined;
129
+ return clampWidth(stored ?? filterWidth, minWidth, maxWidth);
130
+ });
131
+ const previousProp = useRef(filterWidth);
132
+ useEffect(() => {
133
+ if (previousProp.current === filterWidth)
134
+ return;
135
+ previousProp.current = filterWidth;
136
+ setWidth(clampWidth(filterWidth, minWidth, maxWidth));
137
+ }, [filterWidth, minWidth, maxWidth]);
138
+ const onWidthChangeRef = useRef(onFilterWidthChange);
139
+ onWidthChangeRef.current = onFilterWidthChange;
140
+ const handle = useResizeWidth({
141
+ width, min: minWidth, max: maxWidth,
142
+ reversed: filterPosition === "right",
143
+ onChange: (next) => { setWidth(next); onWidthChangeRef.current?.(next); },
144
+ onDragEnd: (next) => writeStored(storageKey, { width: next }),
145
+ });
146
+ const left = filterPosition === "left";
147
+ const expandIcon = left ? _jsx(ChevronRightIcon, { fontSize: "small" }) : _jsx(ChevronLeftIcon, { fontSize: "small" });
148
+ const collapseIcon = left ? _jsx(ChevronLeftIcon, { fontSize: "small" }) : _jsx(ChevronRightIcon, { fontSize: "small" });
149
+ const panel = filter && (collapsed ? (isXs ? (_jsxs(Button, { fullWidth: true, variant: "outlined", color: "inherit", size: "small", startIcon: _jsx(FilterListIcon, {}), onClick: () => setCollapsed(false), "aria-label": "\uD544\uD130 \uD3BC\uCE58\uAE30", "aria-expanded": false, sx: { justifyContent: "flex-start", gap: 1 }, children: [filterTitle, _jsx(CountChip, { count: activeCount, small: true })] })) : (_jsxs(Paper, { variant: "outlined", role: "button", tabIndex: 0, "aria-label": "\uD544\uD130 \uD3BC\uCE58\uAE30", "aria-expanded": false, onClick: () => setCollapsed(false), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") {
150
+ event.preventDefault();
151
+ setCollapsed(false);
152
+ } }, sx: {
153
+ width: collapsedWidth, flexShrink: 0, alignSelf: "stretch", minHeight: 160,
154
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 1, py: 1,
155
+ cursor: "pointer", "&:hover": { bgcolor: "action.hover" },
156
+ }, children: [_jsx(Tooltip, { title: "\uD544\uD130 \uD3BC\uCE58\uAE30", placement: left ? "right" : "left", children: _jsx("span", { children: expandIcon }) }), _jsx(FilterListIcon, { fontSize: "small", color: "action" }), _jsx(CountChip, { count: activeCount, small: true }), _jsx(Typography, { variant: "caption", color: "text.secondary", sx: { writingMode: "vertical-rl", textOrientation: "mixed", letterSpacing: 1, whiteSpace: "nowrap" }, children: filterTitle })] }))) : (_jsxs(Paper, { variant: "outlined", sx: {
157
+ p: 2, position: "relative", flexShrink: 0,
158
+ width: { xs: "100%", md: width },
159
+ transition: handle.isDragging ? "none" : "width 0.2s ease",
160
+ }, children: [resizable && (_jsx(Box, { sx: { display: { xs: "none", md: "block" } }, children: _jsx(ResizeHandle, { ...handle, position: left ? "right" : "left", label: "\uD544\uD130 \uD328\uB110 \uD3ED \uC870\uC808" }) })), _jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", sx: { mb: 1 }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "subtitle2", children: filterTitle }), _jsx(CountChip, { count: activeCount })] }), collapsible && (_jsx(IconButton, { size: "small", onClick: () => setCollapsed(true), "aria-label": "\uD544\uD130 \uC811\uAE30", "aria-expanded": true, children: isXs ? _jsx(ExpandLessIcon, { fontSize: "small" }) : collapseIcon }))] }), _jsx(Divider, { sx: { mb: 2 } }), filter] })));
161
+ return (_jsxs(Stack, { spacing: 2, children: [_jsx(ToolbarRow, { title: title, left: toolbarLeft, right: toolbarRight ?? toolbar }), _jsxs(Stack, { direction: { xs: "column", md: "row" }, spacing: 2, alignItems: "flex-start", children: [left && panel, _jsxs(Box, { sx: { flex: 1, minWidth: 0, width: "100%" }, children: [content, pagination] }), !left && panel] })] }));
162
+ }
@@ -0,0 +1,70 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Row, RowData, Table as TanTable } from "@tanstack/react-table";
3
+ import type { TableBundle, TableDefinition } from "../../core/src/index";
4
+ import { type SideFilterLayoutProps, type TopFilterLayoutProps } from "./TableLayout";
5
+ import { type FilterAreaProps } from "./filter/FilterArea";
6
+ import { type TableContentProps } from "./content/TableContent";
7
+ import { type PaginationAreaProps } from "./pagination/PaginationArea";
8
+ import { type WithDisplayColumnsOptions } from "./columns/displayColumns";
9
+ /** 슬롯 값 — 노드를 직접 주거나, 번들을 받아 그리는 함수. `null` 이면 그 구역을 비운다 */
10
+ export type SlotValue<T extends RowData> = ReactNode | ((bundle: TableBundle<T>) => ReactNode);
11
+ export interface TableScreenSlots<T extends RowData> {
12
+ /** 필터 구역 전체(FilterArea + SearchBar) 대체 */
13
+ filter?: SlotValue<T>;
14
+ /** 테이블 본문 대체 — 카드 그리드 등 */
15
+ content?: SlotValue<T>;
16
+ pagination?: SlotValue<T>;
17
+ /** 그룹핑 바 대체 (`definition.grouping.enabled` 일 때만 그려진다) */
18
+ groupingBar?: SlotValue<T>;
19
+ /** 선택 액션바의 액션 — `null` 이면 바 자체를 뺀다 */
20
+ selectionActions?: ((selectedRows: T[], bundle: TableBundle<T>) => ReactNode) | null;
21
+ /** 0건일 때 본문 (`TableContent renderEmpty`) */
22
+ emptyState?: () => ReactNode;
23
+ /** 펼친 행 아래 패널 — 주면 확장 열이 켜진다 */
24
+ detailPanel?: (row: Row<T>, table: TanTable<T>) => ReactNode;
25
+ /** 행 액션 열 렌더 — `features.rowActions.render` 보다 우선한다 */
26
+ rowActions?: (row: Row<T>, table: TanTable<T>) => ReactNode;
27
+ }
28
+ type LayoutPassthrough = Partial<Omit<TopFilterLayoutProps & SideFilterLayoutProps, "filter" | "content" | "pagination" | "title" | "toolbar" | "toolbarLeft" | "toolbarRight">>;
29
+ export interface TableScreenProps<T extends RowData> {
30
+ /** `useTableFromDefinition` / `useServerTable` 결과 */
31
+ bundle: TableBundle<T>;
32
+ /** 필터 위치 (기본 top) */
33
+ layout?: "top" | "side";
34
+ title?: ReactNode;
35
+ /** 제목 옆 좌측 슬롯 (기존 `renderTopToolbarCustomActions`) */
36
+ toolbarLeft?: ReactNode;
37
+ /** 우측 슬롯 (기존 `renderToolbarInternalActions`) */
38
+ toolbarRight?: ReactNode;
39
+ slots?: TableScreenSlots<T>;
40
+ /** `TableContent` 에 그대로 얹는다 — 정의에서 파생한 값 위에 덮인다 */
41
+ contentProps?: Partial<Omit<TableContentProps<T>, "table">>;
42
+ filterProps?: Partial<Pick<FilterAreaProps<T>, "direction" | "gridCols" | "collapseDetail">>;
43
+ /** 레이아웃 옵션(접기·폭·storageKey …) */
44
+ layoutProps?: LayoutPassthrough;
45
+ paginationProps?: Partial<Omit<PaginationAreaProps<T>, "source">>;
46
+ }
47
+ /** 내부 — 가상화 진입점(`VirtualTableScreen`)이 본문 렌더만 바꿔 끼운다 */
48
+ export interface TableScreenBaseProps<T extends RowData> extends TableScreenProps<T> {
49
+ renderContent: (props: TableContentProps<T>) => ReactNode;
50
+ }
51
+ /**
52
+ * 정의의 `features` → `withDisplayColumns` 옵션.
53
+ * 직접 조립하는 화면이 `bundle.setTableColumns(withDisplayColumns(columns, displayColumnOptionsOf(def)))` 로 쓴다.
54
+ */
55
+ export declare function displayColumnOptionsOf<T extends RowData>(definition: TableDefinition<T>, overrides?: {
56
+ rowActions?: (row: Row<T>, table: TanTable<T>) => ReactNode;
57
+ detailPanel?: boolean;
58
+ }): WithDisplayColumnsOptions<T>;
59
+ /**
60
+ * 정의 한 장으로 화면을 그린다 — 필터 / 콘텐츠 / 페이지네이션 세 구역 + 툴바 + 그룹핑 바 + 선택 액션바.
61
+ *
62
+ * - `features` 대로 표시 전용 열(`withDisplayColumns`)을 3층에 등록하고(`bundle.setTableColumns`),
63
+ * 서버 모드면 요청에 실린 가상 컬럼의 값 열(`virtualColumnDefs`)도 함께 끼운다
64
+ * - `slots` 로 구역을 통째로 바꾼다. `null` 은 비움
65
+ * - 가상화(`features.virtualize`)는 **여기서 그리지 않는다** — `kokotable/mui/virtual` 의
66
+ * `VirtualTableScreen` 을 쓴다. 메인 진입점에 virtualizer 를 두면 쓰지 않는 소비자 번들에도 들어간다
67
+ */
68
+ export declare function TableScreen<T extends RowData>(props: TableScreenProps<T>): import("react").JSX.Element;
69
+ export declare function TableScreenBase<T extends RowData>({ bundle, layout, title, toolbarLeft, toolbarRight, slots, contentProps: contentOverrides, filterProps, layoutProps, paginationProps, renderContent, }: TableScreenBaseProps<T>): import("react").JSX.Element;
70
+ export {};
@@ -0,0 +1,174 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
3
+ import { Stack } from "@mui/material";
4
+ import { contentSource, deriveAggregation, paginationSource, sortSource } from "../../core/src/index";
5
+ import { SearchBar, SideFilterLayout, TopFilterLayout, } from "./TableLayout";
6
+ import { FilterArea, resolveFilterGridSize } from "./filter/FilterArea";
7
+ import { TableContent } from "./content/TableContent";
8
+ import { SelectionActionBar } from "./content/SelectionActionBar";
9
+ import { ColumnVisibilityMenu } from "./content/ColumnVisibilityMenu";
10
+ import { PaginationArea } from "./pagination/PaginationArea";
11
+ import { GroupingBar } from "./grouping/GroupingBar";
12
+ import { StepExpandButton } from "./grouping/StepExpandButton";
13
+ import { aggregatableColumns } from "./grouping/AggregationChips";
14
+ import { AggregationSettingsButton } from "./grouping/AggregationSettingsButton";
15
+ import { GlobalFilterField } from "./toolbar/GlobalFilterField";
16
+ import { FilterStatusButton } from "./addons/FilterStatusModal";
17
+ import { countActiveConditions } from "./addons/searchSummary";
18
+ import { withDisplayColumns } from "./columns/displayColumns";
19
+ import { useVirtualColumns } from "./virtualColumns/useVirtualColumns";
20
+ import { virtualColumnDefs, virtualColumnFields } from "./virtualColumns/virtualColumnDefs";
21
+ import { VirtualColumnSection } from "./virtualColumns/VirtualColumnSection";
22
+ function resolveSlot(slot, bundle) {
23
+ return typeof slot === "function" ? slot(bundle) : slot;
24
+ }
25
+ /** 기능 토글의 **원시 값만** 추린 키 — 정의 객체가 인라인이어도 컬럼 등록이 렌더마다 반복되지 않게 */
26
+ function featureKey(features) {
27
+ const f = features ?? {};
28
+ const rowNumbers = typeof f.rowNumbers === "object"
29
+ ? { id: f.rowNumbers.id, size: f.rowNumbers.size, header: typeof f.rowNumbers.header === "string" ? f.rowNumbers.header : !!f.rowNumbers.header }
30
+ : !!f.rowNumbers;
31
+ const selection = typeof f.selection === "object"
32
+ ? { selectAll: f.selection.selectAll, multi: f.selection.multi }
33
+ : !!f.selection;
34
+ const expand = typeof f.expand === "object" ? { expandAll: f.expand.expandAll } : !!f.expand;
35
+ const rowActions = f.rowActions
36
+ ? { id: f.rowActions.id, size: f.rowActions.size, position: f.rowActions.position, header: typeof f.rowActions.header === "string" ? f.rowActions.header : !!f.rowActions.header }
37
+ : false;
38
+ return JSON.stringify({ rowNumbers, selection, expand, rowActions, legacyIds: !!f.legacyIds });
39
+ }
40
+ /**
41
+ * 정의의 `features` → `withDisplayColumns` 옵션.
42
+ * 직접 조립하는 화면이 `bundle.setTableColumns(withDisplayColumns(columns, displayColumnOptionsOf(def)))` 로 쓴다.
43
+ */
44
+ export function displayColumnOptionsOf(definition, overrides = {}) {
45
+ const f = definition.features ?? {};
46
+ const options = { legacyIds: f.legacyIds };
47
+ if (f.rowNumbers)
48
+ options.rowNumber = typeof f.rowNumbers === "object" ? f.rowNumbers : true;
49
+ if (f.selection) {
50
+ const s = typeof f.selection === "object" ? f.selection : {};
51
+ options.selection = s.selectAll === undefined ? true : { selectAll: s.selectAll };
52
+ }
53
+ if (f.expand || overrides.detailPanel) {
54
+ const e = typeof f.expand === "object" ? f.expand : {};
55
+ options.expand = e.expandAll === undefined ? true : { expandAll: e.expandAll };
56
+ }
57
+ const render = overrides.rowActions ?? f.rowActions?.render;
58
+ if (render) {
59
+ const { render: _ignored, ...rest } = f.rowActions ?? { render };
60
+ options.rowActions = { ...rest, render };
61
+ }
62
+ return options;
63
+ }
64
+ /**
65
+ * 정의 한 장으로 화면을 그린다 — 필터 / 콘텐츠 / 페이지네이션 세 구역 + 툴바 + 그룹핑 바 + 선택 액션바.
66
+ *
67
+ * - `features` 대로 표시 전용 열(`withDisplayColumns`)을 3층에 등록하고(`bundle.setTableColumns`),
68
+ * 서버 모드면 요청에 실린 가상 컬럼의 값 열(`virtualColumnDefs`)도 함께 끼운다
69
+ * - `slots` 로 구역을 통째로 바꾼다. `null` 은 비움
70
+ * - 가상화(`features.virtualize`)는 **여기서 그리지 않는다** — `kokotable/mui/virtual` 의
71
+ * `VirtualTableScreen` 을 쓴다. 메인 진입점에 virtualizer 를 두면 쓰지 않는 소비자 번들에도 들어간다
72
+ */
73
+ export function TableScreen(props) {
74
+ const virtualize = !!props.bundle.definition.features?.virtualize;
75
+ useEffect(() => {
76
+ if (virtualize && typeof console !== "undefined") {
77
+ console.warn(`[koko-table] TableScreen("${props.bundle.definition.key}"): features.virtualize 는 무시됩니다 — ` +
78
+ 'import { VirtualTableScreen } from "kokotable/mui/virtual" 을 쓰세요.');
79
+ }
80
+ }, [virtualize, props.bundle.definition.key]);
81
+ return _jsx(TableScreenBase, { ...props, renderContent: (contentProps) => _jsx(TableContent, { ...contentProps }) });
82
+ }
83
+ export function TableScreenBase({ bundle, layout = "top", title, toolbarLeft, toolbarRight, slots = {}, contentProps: contentOverrides, filterProps, layoutProps, paginationProps, renderContent, }) {
84
+ const { definition, search, data, table, grouping, mode } = bundle;
85
+ const features = definition.features ?? {};
86
+ const selection = typeof features.selection === "object" ? features.selection : (features.selection ? {} : undefined);
87
+ // ── 가상 컬럼 (필터 섹션 + 값 열) ──
88
+ const vcFeature = typeof features.virtualColumns === "object" ? features.virtualColumns : {};
89
+ const vcEnabled = !!features.virtualColumns;
90
+ const vcFields = useMemo(() => virtualColumnFields(definition.columns), [definition.columns]);
91
+ const vc = useVirtualColumns(search, {
92
+ applyImmediately: vcFeature.applyImmediately ?? true,
93
+ maxSlots: vcFeature.maxSlots,
94
+ fields: vcFields,
95
+ });
96
+ const virtualSpec = bundle.request.virtualColumns;
97
+ const virtualDefs = useMemo(
98
+ // 클라이언트 모드에는 값이 없다(2층 엔진은 식을 평가하지 않는다) — 서버 응답 값만 열로 그린다
99
+ () => (mode === "server" ? virtualColumnDefs(virtualSpec, { names: vc.names }) : []), [mode, virtualSpec, vc.names]);
100
+ // ── 표시 전용 열 등록 ──
101
+ // 렌더 함수는 ref 로 넘긴다 — 인라인 함수가 렌더마다 새로 만들어져도 컬럼 등록이 반복되지 않는다
102
+ const rowActionsRef = useRef(slots.rowActions ?? features.rowActions?.render);
103
+ rowActionsRef.current = slots.rowActions ?? features.rowActions?.render;
104
+ const hasRowActions = !!rowActionsRef.current;
105
+ const hasDetailPanel = !!slots.detailPanel;
106
+ const fKey = featureKey(features);
107
+ const tableColumns = useMemo(() => {
108
+ const options = displayColumnOptionsOf(definition, {
109
+ rowActions: hasRowActions ? (row, tan) => rowActionsRef.current?.(row, tan) : undefined,
110
+ detailPanel: hasDetailPanel,
111
+ });
112
+ return withDisplayColumns([...definition.columns, ...virtualDefs], options);
113
+ // eslint-disable-next-line react-hooks/exhaustive-deps
114
+ }, [definition.columns, virtualDefs, fKey, hasRowActions, hasDetailPanel]);
115
+ const { setTableColumns } = bundle;
116
+ useLayoutEffect(() => {
117
+ setTableColumns(tableColumns);
118
+ }, [setTableColumns, tableColumns]);
119
+ // ── 툴바 ──
120
+ const left = (features.globalFilter || toolbarLeft) ? (_jsxs(_Fragment, { children: [features.globalFilter && _jsx(GlobalFilterField, { search: search }), toolbarLeft] })) : undefined;
121
+ const right = (features.filterStatus || features.columnVisibilityMenu || toolbarRight) ? (_jsxs(_Fragment, { children: [toolbarRight, features.filterStatus && _jsx(FilterStatusButton, { search: search, onApply: search.apply }), features.columnVisibilityMenu && _jsx(ColumnVisibilityMenu, { table: table })] })) : undefined;
122
+ // ── 필터 구역 ──
123
+ const direction = filterProps?.direction ?? (layout === "side" ? "column" : "grid");
124
+ const gridSize = direction === "grid" ? resolveFilterGridSize(filterProps?.gridCols) : undefined;
125
+ const hasFilterFields = search.getFilterFields().length > 0;
126
+ let filterNode;
127
+ if (slots.filter !== undefined) {
128
+ filterNode = resolveSlot(slots.filter, bundle);
129
+ }
130
+ else if (hasFilterFields || vcEnabled) {
131
+ filterNode = (_jsxs(Stack, { spacing: 2, children: [_jsx(FilterArea, { source: search, direction: direction, gridCols: filterProps?.gridCols, collapseDetail: filterProps?.collapseDetail ?? true, children: vcEnabled && (_jsx(VirtualColumnSection, { virtualColumns: vc, gridSize: gridSize, enableFilter: vcFeature.enableFilter ?? true })) }), _jsx(SearchBar, { onSearch: search.apply, onReset: search.reset, isDirty: search.isDirty })] }));
132
+ }
133
+ // ── 선택 액션바 ──
134
+ const selectionBar = selection && slots.selectionActions !== null ? (_jsx(SelectionActionBar, { source: table, renderActions: slots.selectionActions ? (rows) => slots.selectionActions(rows, bundle) : undefined })) : null;
135
+ // ── 그룹핑 바 ──
136
+ let groupingBar = null;
137
+ if (grouping && slots.groupingBar !== undefined) {
138
+ groupingBar = resolveSlot(slots.groupingBar, bundle);
139
+ }
140
+ else if (grouping) {
141
+ const aggregatable = aggregatableColumns(definition.columns, { serverOnly: mode === "server" }).length > 0;
142
+ groupingBar = (_jsx(GroupingBar, { grouping: grouping.grouping, onChange: grouping.setGrouping, columns: definition.columns, batchSize: grouping.batchSize, onBatchSizeChange: grouping.setBatchSize, extra: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", sx: { flexWrap: "wrap", gap: 0.5 }, children: [aggregatable && grouping.enabled && (_jsx(AggregationSettingsButton, { value: search.params.aggregation ?? deriveAggregation(definition.columns) ?? {}, onChange: (next) => search.patchParams({ aggregation: next }), columns: definition.columns, serverOnly: mode === "server" })), _jsx(StepExpandButton, { expand: grouping.expand })] }) }));
143
+ }
144
+ // ── 본문 ──
145
+ const groupingActive = !!grouping && grouping.enabled;
146
+ const contentProps = {
147
+ table,
148
+ content: contentSource(data),
149
+ sort: sortSource(search),
150
+ groupItems: groupingActive ? grouping.items : undefined,
151
+ onToggleGroup: grouping ? (node) => grouping.expand.toggle(node.path) : undefined,
152
+ isGroupExpanded: grouping ? (node) => grouping.expand.isExpanded(node.path) : undefined,
153
+ onLoadMore: grouping?.loadMore,
154
+ renderDetailPanel: slots.detailPanel,
155
+ selectOnRowClick: selection?.selectOnRowClick,
156
+ onResetFilters: search.reset,
157
+ renderEmpty: slots.emptyState,
158
+ groupedColumnMode: features.groupedColumnMode,
159
+ aggregation: groupingActive ? (search.params.aggregation ?? deriveAggregation(definition.columns) ?? {}) : undefined,
160
+ onAggregationChange: groupingActive ? (next) => search.patchParams({ aggregation: next }) : undefined,
161
+ density: features.density,
162
+ enableColumnMenu: features.columnMenu,
163
+ ...contentOverrides,
164
+ };
165
+ const contentNode = slots.content !== undefined ? resolveSlot(slots.content, bundle) : renderContent(contentProps);
166
+ const contentRegion = (_jsxs(Stack, { spacing: 1, children: [selectionBar, groupingBar, contentNode] }));
167
+ // ── 페이지네이션 ──
168
+ const paginationNode = slots.pagination !== undefined
169
+ ? resolveSlot(slots.pagination, bundle)
170
+ : _jsx(PaginationArea, { source: paginationSource(search, data), ...paginationProps });
171
+ const activeCount = countActiveConditions(search.params, search.operators);
172
+ const Layout = layout === "side" ? SideFilterLayout : TopFilterLayout;
173
+ return (_jsx(Layout, { title: title, toolbarLeft: left, toolbarRight: right, filter: filterNode, content: contentRegion, pagination: paginationNode, activeCount: activeCount, ...layoutProps }));
174
+ }
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from "react";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ import type { AnyFilterSource, TableSearch } from "../../../core/src/index";
4
+ import { type GridColCount, type GridColsConfig } from "../filter/FilterArea";
5
+ /** 모달이 요구하는 최소 검색 상태 — 검색·취소·초기화가 각각 부르는 메서드 */
6
+ export type FilterModalSearch<T extends RowData> = Pick<TableSearch<T>, "params" | "operators" | "isDirty" | "apply" | "reset" | "setDraft" | "getFilterFields">;
7
+ export interface FilterModalProps<T extends RowData> {
8
+ search: FilterModalSearch<T>;
9
+ /** 필터 필드 출처. 없으면 `search` (표시 전용 컬럼이 섞인 테이블을 쓰려면 `table`) */
10
+ source?: AnyFilterSource<T>;
11
+ open: boolean;
12
+ onClose: () => void;
13
+ title?: ReactNode;
14
+ /** 기본 column (모달은 세로 일렬). grid 면 `gridCols` 가 적용된다 */
15
+ direction?: "column" | "grid";
16
+ gridCols?: Partial<GridColsConfig> | GridColCount;
17
+ collapseDetail?: boolean;
18
+ maxWidth?: "xs" | "sm" | "md" | "lg";
19
+ /** FilterArea 끝에 덧붙일 항목 (가상 컬럼 섹션 등) */
20
+ children?: ReactNode;
21
+ labels?: Partial<{
22
+ search: string;
23
+ cancel: string;
24
+ reset: string;
25
+ }>;
26
+ /** 초기화 뒤 모달을 닫는다 (기본 false — 비워진 필드를 확인하고 닫는다) */
27
+ closeOnReset?: boolean;
28
+ }
29
+ /**
30
+ * 필터 모달 — 기존 ERP `ModalFilter` + `HeadlessFilterModal` 대응.
31
+ *
32
+ * - 검색: `apply()` 뒤 닫힘
33
+ * - 취소(배경 클릭·ESC 포함): `setDraft(params)` 로 편집분을 버린다
34
+ * - 초기화: `reset()` — 즉시 적용된다
35
+ */
36
+ export declare function FilterModal<T extends RowData>({ search, source, open, onClose, title, direction, gridCols, collapseDetail, maxWidth, children, labels, closeOnReset, }: FilterModalProps<T>): import("react").JSX.Element;
37
+ export interface FilterModalButtonProps<T extends RowData> extends Omit<FilterModalProps<T>, "open" | "onClose"> {
38
+ /** 버튼 라벨. 없으면 아이콘 버튼 */
39
+ label?: ReactNode;
40
+ tooltip?: string;
41
+ size?: "small" | "medium";
42
+ variant?: "text" | "outlined" | "contained";
43
+ }
44
+ /** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
45
+ export declare function FilterModalButton<T extends RowData>({ label, tooltip, size, variant, ...modalProps }: FilterModalButtonProps<T>): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tooltip, } from "@mui/material";
4
+ import CloseIcon from "@mui/icons-material/Close";
5
+ import FilterListIcon from "@mui/icons-material/FilterList";
6
+ import { FilterArea } from "../filter/FilterArea";
7
+ import { countActiveConditions } from "./searchSummary";
8
+ /**
9
+ * 필터 모달 — 기존 ERP `ModalFilter` + `HeadlessFilterModal` 대응.
10
+ *
11
+ * - 검색: `apply()` 뒤 닫힘
12
+ * - 취소(배경 클릭·ESC 포함): `setDraft(params)` 로 편집분을 버린다
13
+ * - 초기화: `reset()` — 즉시 적용된다
14
+ */
15
+ export function FilterModal({ search, source, open, onClose, title = "검색 조건", direction = "column", gridCols, collapseDetail = true, maxWidth = "xs", children, labels, closeOnReset = false, }) {
16
+ const text = { search: "검색", cancel: "취소", reset: "초기화", ...labels };
17
+ const count = countActiveConditions(search.params, search.operators);
18
+ const cancel = () => {
19
+ search.setDraft(search.params);
20
+ onClose();
21
+ };
22
+ const apply = () => {
23
+ search.apply();
24
+ onClose();
25
+ };
26
+ const reset = () => {
27
+ search.reset();
28
+ if (closeOnReset)
29
+ onClose();
30
+ };
31
+ return (_jsxs(Dialog, { open: open, onClose: cancel, maxWidth: maxWidth, fullWidth: true, "aria-labelledby": "koko-filter-modal-title", children: [_jsxs(DialogTitle, { id: "koko-filter-modal-title", sx: { display: "flex", alignItems: "center", gap: 1, pr: 6 }, children: [_jsx(FilterListIcon, { fontSize: "small" }), _jsx("span", { children: title }), count > 0 && _jsx(Chip, { size: "small", color: "primary", label: `${count}개 적용 중`, sx: { height: 22 } }), _jsx(IconButton, { size: "small", onClick: cancel, "aria-label": "\uB2EB\uAE30", sx: { position: "absolute", right: 12, top: 12 }, children: _jsx(CloseIcon, { fontSize: "small" }) })] }), _jsx(DialogContent, { dividers: true, children: _jsx(FilterArea, { source: (source ?? search), direction: direction, gridCols: gridCols, collapseDetail: collapseDetail, children: children }) }), _jsxs(DialogActions, { sx: { justifyContent: "space-between" }, children: [_jsx(Button, { size: "small", color: "inherit", onClick: reset, children: text.reset }), _jsxs(Box, { sx: { display: "flex", gap: 1 }, children: [_jsx(Button, { size: "small", onClick: cancel, children: text.cancel }), _jsx(Button, { size: "small", variant: "contained", onClick: apply, children: text.search })] })] })] }));
32
+ }
33
+ /** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
34
+ export function FilterModalButton({ label, tooltip = "필터 설정", size = "small", variant = "outlined", ...modalProps }) {
35
+ const [open, setOpen] = useState(false);
36
+ const count = countActiveConditions(modalProps.search.params, modalProps.search.operators);
37
+ const trigger = label !== undefined ? (_jsx(Button, { size: size, variant: variant, startIcon: _jsx(FilterListIcon, {}), onClick: () => setOpen(true), "aria-label": tooltip, children: _jsx(Badge, { badgeContent: count, color: "primary", sx: { "& .MuiBadge-badge": { right: -14, top: 8 } }, children: label }) })) : (_jsx(IconButton, { size: size, color: "primary", onClick: () => setOpen(true), "aria-label": tooltip, children: _jsx(Badge, { badgeContent: count, color: "primary", children: _jsx(FilterListIcon, { fontSize: "small" }) }) }));
38
+ return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: tooltip, children: trigger }), _jsx(FilterModal, { ...modalProps, open: open, onClose: () => setOpen(false) })] }));
39
+ }
@@ -0,0 +1,40 @@
1
+ import { type ReactNode } from "react";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ import { type TableColumnDefs, type TableSearch } from "../../../core/src/index";
4
+ import { type SearchSummary } from "./searchSummary";
5
+ /** 모달·버튼·내용이 공통으로 요구하는 최소 검색 상태 */
6
+ export type FilterStatusSearch<T extends RowData> = Pick<TableSearch<T>, "columns" | "params" | "draft" | "operators" | "draftOperators" | "isDirty">;
7
+ export interface FilterStatusContentProps<T extends RowData> {
8
+ search: FilterStatusSearch<T>;
9
+ /** 헤더·선택지를 읽을 컬럼. 없으면 `search.columns` */
10
+ columns?: TableColumnDefs<T>;
11
+ /** draft 와 비교해 미적용 항목을 표시한다 (기본 true) */
12
+ compareDraft?: boolean;
13
+ /** 정렬·그룹핑·페이지(즉시 적용 항목) 를 보인다 (기본 true) */
14
+ showImmediate?: boolean;
15
+ }
16
+ /**
17
+ * 적용 조건 표 — `search.params` 를 컬럼 헤더 · 연산자 라벨 · 값으로 풀어 보인다.
18
+ * draft 와 다른 항목은 "미적용" 칩과 함께 `적용값 → 편집값` 으로 그린다.
19
+ */
20
+ export declare function FilterStatusContent<T extends RowData>({ search, columns, compareDraft, showImmediate, }: FilterStatusContentProps<T>): import("react").JSX.Element;
21
+ /** `summarizeSearch` 의 메모 버전 — 버튼 배지와 내용이 같은 요약을 본다 */
22
+ export declare function useFilterStatusSummary<T extends RowData>(search: FilterStatusSearch<T>, columns?: TableColumnDefs<T>, compareDraft?: boolean): SearchSummary;
23
+ export interface FilterStatusModalProps<T extends RowData> extends FilterStatusContentProps<T> {
24
+ open: boolean;
25
+ onClose: () => void;
26
+ title?: ReactNode;
27
+ /** 미적용 변경이 있을 때 "편집 중인 조건 적용" 버튼을 보인다 — `search.apply` 가 필요하다 */
28
+ onApply?: () => void;
29
+ maxWidth?: "xs" | "sm" | "md";
30
+ }
31
+ /** 적용 조건 확인 모달 — 기존 ERP `FilterStatusModal` 대응 */
32
+ export declare function FilterStatusModal<T extends RowData>({ open, onClose, title, onApply, maxWidth, ...contentProps }: FilterStatusModalProps<T>): import("react").JSX.Element;
33
+ export interface FilterStatusButtonProps<T extends RowData> extends Omit<FilterStatusModalProps<T>, "open" | "onClose"> {
34
+ tooltip?: string;
35
+ size?: "small" | "medium";
36
+ /** 미적용 변경이 있으면 배지를 warning 색으로 (기본 true) */
37
+ highlightDirty?: boolean;
38
+ }
39
+ /** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
40
+ export declare function FilterStatusButton<T extends RowData>({ tooltip, size, highlightDirty, ...modalProps }: FilterStatusButtonProps<T>): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Badge, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, Stack, Table, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography, } from "@mui/material";
4
+ import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
5
+ import CloseIcon from "@mui/icons-material/Close";
6
+ import VisibilityIcon from "@mui/icons-material/Visibility";
7
+ import { DEFAULT_DESCRIBE_LABELS } from "../../../core/src/index";
8
+ import { summarizeSearch, } from "./searchSummary";
9
+ import { useKokoTableConfig } from "../provider/KokoTableProvider";
10
+ const STATUS_LABEL = {
11
+ changed: "미적용(변경)",
12
+ added: "미적용(추가)",
13
+ removed: "미적용(삭제)",
14
+ };
15
+ function PendingChip({ status }) {
16
+ if (status === "applied")
17
+ return null;
18
+ return _jsx(Chip, { size: "small", color: "warning", variant: "outlined", label: STATUS_LABEL[status], sx: { height: 20 } });
19
+ }
20
+ function ValueCell({ row }) {
21
+ if (row.status === "applied") {
22
+ return _jsx(Typography, { variant: "body2", sx: { wordBreak: "break-all" }, children: row.value });
23
+ }
24
+ return (_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", sx: { flexWrap: "wrap", rowGap: 0.5 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { wordBreak: "break-all" }, children: row.value }), _jsx(ArrowForwardIcon, { sx: { fontSize: 14, color: "warning.main" } }), _jsx(Typography, { variant: "body2", sx: { fontWeight: 600, wordBreak: "break-all" }, children: row.draftValue })] }));
25
+ }
26
+ function OperatorCell({ row }) {
27
+ const changed = row.status !== "applied" && row.draftOperator && row.draftOperator !== row.operator;
28
+ return (_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Chip, { size: "small", label: row.operatorLabel, sx: { height: 20 } }), changed && (_jsxs(_Fragment, { children: [_jsx(ArrowForwardIcon, { sx: { fontSize: 14, color: "warning.main" } }), _jsx(Chip, { size: "small", color: "warning", label: row.draftOperatorLabel, sx: { height: 20 } })] }))] }));
29
+ }
30
+ function SectionTitle({ children, extra }) {
31
+ return (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 0.5 }, children: [_jsx(Typography, { variant: "subtitle2", sx: { color: "primary.main" }, children: children }), extra] }));
32
+ }
33
+ /**
34
+ * 적용 조건 표 — `search.params` 를 컬럼 헤더 · 연산자 라벨 · 값으로 풀어 보인다.
35
+ * draft 와 다른 항목은 "미적용" 칩과 함께 `적용값 → 편집값` 으로 그린다.
36
+ */
37
+ export function FilterStatusContent({ search, columns, compareDraft = true, showImmediate = true, }) {
38
+ const summary = useFilterStatusSummary(search, columns, compareDraft);
39
+ const { conditions, globalFilter, sorting, grouping, pagination } = summary;
40
+ return (_jsxs(Stack, { spacing: 2, children: [_jsxs(Box, { children: [_jsx(SectionTitle, { children: "\uC804\uC5ED\uAC80\uC0C9" }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "body2", color: globalFilter.value ? "text.primary" : "text.disabled", children: globalFilter.value || "(없음)" }), globalFilter.pending && (_jsxs(_Fragment, { children: [_jsx(ArrowForwardIcon, { sx: { fontSize: 14, color: "warning.main" } }), _jsx(Typography, { variant: "body2", sx: { fontWeight: 600 }, children: globalFilter.draftValue || "(없음)" }), _jsx(PendingChip, { status: "changed" })] }))] })] }), _jsx(Divider, {}), _jsxs(Box, { children: [_jsx(SectionTitle, { extra: summary.appliedCount > 0 && _jsx(Chip, { size: "small", color: "primary", label: `${summary.appliedCount}개 적용`, sx: { height: 20 } }), children: "\uCEEC\uB7FC \uC870\uAC74" }), conditions.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.disabled", children: "\uC801\uC6A9\uB41C \uCEEC\uB7FC \uC870\uAC74 \uC5C6\uC74C" })) : (_jsx(Box, { sx: { overflowX: "auto" }, children: _jsxs(Table, { size: "small", "aria-label": "\uC801\uC6A9 \uC870\uAC74", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: { whiteSpace: "nowrap" }, children: "\uCEEC\uB7FC" }), _jsx(TableCell, { sx: { whiteSpace: "nowrap" }, children: "\uC5F0\uC0B0\uC790" }), _jsx(TableCell, { children: "\uAC12" }), compareDraft && _jsx(TableCell, { sx: { whiteSpace: "nowrap" }, children: "\uC0C1\uD0DC" })] }) }), _jsx(TableBody, { children: conditions.map((row) => (_jsxs(TableRow, { "data-status": row.status, sx: row.status !== "applied" ? { bgcolor: "warning.50" } : undefined, children: [_jsx(TableCell, { sx: { whiteSpace: "nowrap", fontWeight: 500 }, children: row.header }), _jsx(TableCell, { children: _jsx(OperatorCell, { row: row }) }), _jsx(TableCell, { children: _jsx(ValueCell, { row: row }) }), compareDraft && _jsx(TableCell, { children: _jsx(PendingChip, { status: row.status }) })] }, row.id))) })] }) }))] }), showImmediate && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Box, { children: [_jsx(SectionTitle, { children: "\uC989\uC2DC \uC801\uC6A9 \uD56D\uBAA9" }), _jsx(Typography, { variant: "caption", color: "text.secondary", component: "div", sx: { mb: 1 }, children: "\uC815\uB82C\u00B7\uADF8\uB8F9\uD551\u00B7\uD398\uC774\uC9C0\uB294 \uAC80\uC0C9 \uBC84\uD2BC \uC5C6\uC774 \uC989\uC2DC \uBC18\uC601\uB429\uB2C8\uB2E4." }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { flexWrap: "wrap", rowGap: 1 }, children: [_jsx(Chip, { size: "small", variant: "outlined", label: `정렬: ${sorting.length > 0 ? sorting.map((s) => s.label).join(", ") : "(없음)"}` }), _jsx(Chip, { size: "small", variant: "outlined", label: `그룹핑: ${grouping.length > 0 ? grouping.join(" > ") : "(없음)"}` }), _jsx(Chip, { size: "small", variant: "outlined", label: `페이지: ${pagination.pageIndex + 1} / ${pagination.pageSize}개씩` })] })] })] }))] }));
41
+ }
42
+ /** `summarizeSearch` 의 메모 버전 — 버튼 배지와 내용이 같은 요약을 본다 */
43
+ export function useFilterStatusSummary(search, columns, compareDraft = true) {
44
+ const resolved = columns ?? search.columns;
45
+ // Provider 의 연산자 라벨을 kind describe 에도 흘린다 (다국어 통로 하나)
46
+ const { labels: providerLabels } = useKokoTableConfig();
47
+ const labels = useMemo(() => ({ ...DEFAULT_DESCRIBE_LABELS, operators: providerLabels.operators }), [providerLabels.operators]);
48
+ return useMemo(() => summarizeSearch({
49
+ columns: resolved,
50
+ params: search.params,
51
+ draft: compareDraft ? search.draft : undefined,
52
+ operators: search.operators,
53
+ draftOperators: search.draftOperators,
54
+ labels,
55
+ }), [resolved, search.params, search.draft, search.operators, search.draftOperators, compareDraft, labels]);
56
+ }
57
+ /** 적용 조건 확인 모달 — 기존 ERP `FilterStatusModal` 대응 */
58
+ export function FilterStatusModal({ open, onClose, title = "적용 조건", onApply, maxWidth = "sm", ...contentProps }) {
59
+ const { search } = contentProps;
60
+ return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: maxWidth, fullWidth: true, "aria-labelledby": "koko-filter-status-title", children: [_jsxs(DialogTitle, { id: "koko-filter-status-title", sx: { display: "flex", alignItems: "center", gap: 1, pr: 6 }, children: [_jsx(VisibilityIcon, { fontSize: "small" }), _jsx("span", { children: title }), search.isDirty && _jsx(Chip, { size: "small", color: "warning", label: "\uBBF8\uC801\uC6A9 \uBCC0\uACBD \uC788\uC74C", sx: { height: 22 } }), _jsx(IconButton, { size: "small", onClick: onClose, "aria-label": "\uB2EB\uAE30", sx: { position: "absolute", right: 12, top: 12 }, children: _jsx(CloseIcon, { fontSize: "small" }) })] }), _jsx(DialogContent, { dividers: true, children: _jsx(FilterStatusContent, { ...contentProps }) }), _jsxs(DialogActions, { children: [onApply && search.isDirty && (_jsx(Button, { onClick: () => { onApply(); onClose(); }, children: "\uD3B8\uC9D1 \uC911\uC778 \uC870\uAC74 \uC801\uC6A9" })), _jsx(Button, { onClick: onClose, children: "\uB2EB\uAE30" })] })] }));
61
+ }
62
+ /** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
63
+ export function FilterStatusButton({ tooltip = "적용 조건 보기", size = "small", highlightDirty = true, ...modalProps }) {
64
+ const [open, setOpen] = useState(false);
65
+ const summary = useFilterStatusSummary(modalProps.search, modalProps.columns, modalProps.compareDraft ?? true);
66
+ const dirty = highlightDirty && modalProps.search.isDirty;
67
+ return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: dirty ? `${tooltip} (미적용 변경 있음)` : tooltip, children: _jsx(IconButton, { size: size, onClick: () => setOpen(true), "aria-label": tooltip, children: _jsx(Badge, { badgeContent: summary.appliedCount, color: dirty ? "warning" : "primary", max: 99, children: _jsx(VisibilityIcon, { fontSize: "small" }) }) }) }), _jsx(FilterStatusModal, { ...modalProps, open: open, onClose: () => setOpen(false) })] }));
68
+ }