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,30 @@
1
+ import type { ReactNode } from "react";
2
+ import type { RegistryPatch } from "../../core/src/index";
3
+ /**
4
+ * 확장점 공통 레지스트리 (설계 문서 4절) — 구현은 core 에 있다(집계·export 포맷터는 core 가 소비하므로).
5
+ * mui 는 같은 것을 re-export 한다: `createRegistry` · `Registry` · `RegistryPatch` · `applyPatch` · `isRegistry` · `toRegistry`.
6
+ */
7
+ export { createRegistry, applyPatch, isRegistry, toRegistry, type Registry, type RegistryPatch, } from "../../core/src/index";
8
+ export interface MenuItemDef<Ctx> {
9
+ id: string;
10
+ label: string | ((ctx: Ctx) => string);
11
+ icon?: ReactNode | ((ctx: Ctx) => ReactNode);
12
+ /** 노출 조건 — 거짓이면 항목이 빠진다 */
13
+ when?: (ctx: Ctx) => boolean;
14
+ run: (ctx: Ctx) => void;
15
+ disabled?: (ctx: Ctx) => boolean;
16
+ /** 같은 group 끼리 묶이고 group 사이에 구분선 */
17
+ group?: string;
18
+ /** 숫자 작을수록 앞. 미지정이면 레지스트리 위치(바로 앞 `order` 항목 뒤)를 지킨다 */
19
+ order?: number;
20
+ /** 이 항목 앞에 구분선 */
21
+ divider?: boolean;
22
+ /**
23
+ * 하나의 정의가 여러 항목으로 펼쳐질 때 (예: "N단계까지 펼치기" 를 단계마다).
24
+ * 있으면 이 정의 자리에 돌려준 항목들이 들어가고 자신의 `run` 은 쓰이지 않는다.
25
+ */
26
+ items?: (ctx: Ctx) => Array<MenuItemDef<Ctx>>;
27
+ }
28
+ export type MenuPatch<Ctx> = RegistryPatch<MenuItemDef<Ctx>>;
29
+ export type { CellRendererDef, CellContext, CellRendererId } from "./content/cellRenderers";
30
+ export type { AggregationDef, ExportFormatterDef } from "../../core/src/index";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 확장점 공통 레지스트리 (설계 문서 4절) — 구현은 core 에 있다(집계·export 포맷터는 core 가 소비하므로).
3
+ * mui 는 같은 것을 re-export 한다: `createRegistry` · `Registry` · `RegistryPatch` · `applyPatch` · `isRegistry` · `toRegistry`.
4
+ */
5
+ export { createRegistry, applyPatch, isRegistry, toRegistry, } from "../../core/src/index";
@@ -0,0 +1,36 @@
1
+ import { type ReactNode } from "react";
2
+ import { type SxProps, type Theme } from "@mui/material";
3
+ import type { RowData } from "@tanstack/react-table";
4
+ import type { TableSearch } from "../../../core/src/index";
5
+ export interface GlobalFilterFieldProps<T extends RowData> {
6
+ /**
7
+ * `useTableSearch` 결과. `draft.globalFilter` 를 보고 `setGlobalFilter` / `apply` 를 부른다.
8
+ * `pendingApply` 가 있으면(코어 debounce 대기 중) 입력 끝에 스피너를 그린다.
9
+ */
10
+ search: Pick<TableSearch<T>, "draft" | "setGlobalFilter" | "apply"> & Partial<Pick<TableSearch<T>, "pendingApply">>;
11
+ /** Enter 로 즉시 검색. 끄면 blur 시 draft 만 갱신되고 검색 버튼이 적용한다 */
12
+ applyOnEnter?: boolean;
13
+ /**
14
+ * 타이핑만으로 조회한다 — 마지막 입력 뒤 `debounceMs` 가 지나면 `setGlobalFilter` + `apply`.
15
+ * Enter 는 대기를 건너뛰고 즉시 적용한다. 대기 중에는 입력 끝에 작은 스피너가 돈다.
16
+ */
17
+ applyOnChange?: boolean;
18
+ /** `applyOnChange` 의 지연(ms). 기본 300 */
19
+ debounceMs?: number;
20
+ /**
21
+ * 입력 오른쪽 슬롯 — ERP 의 AI 검색 버튼(`AiQueryLauncher`) 이 여기 꽂힌다.
22
+ * 전역검색어와 무관한 별도 진입점이므로 값 상태에 손대지 않는다.
23
+ */
24
+ actions?: ReactNode;
25
+ label?: string;
26
+ placeholder?: string;
27
+ fullWidth?: boolean;
28
+ sx?: SxProps<Theme>;
29
+ }
30
+ /**
31
+ * 전역검색 입력 — 기존 `GlobalFilterField.tsx` 대응.
32
+ * 로컬 상태로 타이핑하고 blur/Enter 에만 draft 에 반영한다(글자마다 검색 상태를 흔들지 않는다).
33
+ * `applyOnChange` 면 여기서 debounce 해 `setGlobalFilter` + `apply` 를 한 번에 낸다 —
34
+ * 코어 옵션(`globalFilterApplyOnChange`) 없이도 필드 하나로 동작한다.
35
+ */
36
+ export declare function GlobalFilterField<T extends RowData>({ search, applyOnEnter, applyOnChange, debounceMs, actions, label, placeholder, fullWidth, sx, }: GlobalFilterFieldProps<T>): import("react").JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { CircularProgress, InputAdornment, TextField } from "@mui/material";
4
+ import SearchIcon from "@mui/icons-material/Search";
5
+ /**
6
+ * 전역검색 입력 — 기존 `GlobalFilterField.tsx` 대응.
7
+ * 로컬 상태로 타이핑하고 blur/Enter 에만 draft 에 반영한다(글자마다 검색 상태를 흔들지 않는다).
8
+ * `applyOnChange` 면 여기서 debounce 해 `setGlobalFilter` + `apply` 를 한 번에 낸다 —
9
+ * 코어 옵션(`globalFilterApplyOnChange`) 없이도 필드 하나로 동작한다.
10
+ */
11
+ export function GlobalFilterField({ search, applyOnEnter = true, applyOnChange = false, debounceMs = 300, actions, label = "전역검색", placeholder = "검색어를 입력하세요", fullWidth = false, sx, }) {
12
+ const draftValue = search.draft.globalFilter ?? "";
13
+ const [local, setLocal] = useState(draftValue);
14
+ const inputRef = useRef(null);
15
+ // 필드 자체 debounce — 코어의 pendingApply 와 합쳐 스피너를 그린다
16
+ const timerRef = useRef(null);
17
+ const [localPending, setLocalPending] = useState(false);
18
+ const searchRef = useRef(search);
19
+ searchRef.current = search;
20
+ const cancelTimer = () => {
21
+ if (timerRef.current !== null) {
22
+ clearTimeout(timerRef.current);
23
+ timerRef.current = null;
24
+ }
25
+ setLocalPending(false);
26
+ };
27
+ useEffect(() => () => { if (timerRef.current !== null)
28
+ clearTimeout(timerRef.current); }, []);
29
+ // 초기화 버튼 등으로 밖에서 바뀌면 따라간다
30
+ useEffect(() => { setLocal(draftValue); }, [draftValue]);
31
+ const applyValue = (value) => {
32
+ // setGlobalFilter 가 draftRef 를 즉시 갱신하므로 같은 핸들러에서 apply 해도 최신 값을 본다
33
+ searchRef.current.setGlobalFilter(value);
34
+ searchRef.current.apply();
35
+ };
36
+ const onChange = (value) => {
37
+ setLocal(value);
38
+ if (!applyOnChange)
39
+ return;
40
+ if (timerRef.current !== null)
41
+ clearTimeout(timerRef.current);
42
+ if (!(debounceMs > 0)) {
43
+ applyValue(value);
44
+ return;
45
+ }
46
+ setLocalPending(true);
47
+ timerRef.current = setTimeout(() => {
48
+ timerRef.current = null;
49
+ setLocalPending(false);
50
+ applyValue(value);
51
+ }, debounceMs);
52
+ };
53
+ const flush = () => {
54
+ // 대기 중이면 타이머가 처리한다 — blur 로 draft 만 바꾸면 조회 없이 isDirty 만 오른다
55
+ if (localPending)
56
+ return;
57
+ if (local !== draftValue)
58
+ search.setGlobalFilter(local);
59
+ };
60
+ const onKeyDown = (event) => {
61
+ event.stopPropagation();
62
+ if (event.key !== "Enter")
63
+ return;
64
+ event.preventDefault();
65
+ cancelTimer();
66
+ search.setGlobalFilter(local);
67
+ if (applyOnEnter || applyOnChange)
68
+ search.apply();
69
+ inputRef.current?.focus();
70
+ };
71
+ const pending = localPending || search.pendingApply === true;
72
+ return (_jsx(TextField, { inputRef: inputRef, size: "small", label: label, placeholder: placeholder, value: local, fullWidth: fullWidth, onChange: (event) => onChange(event.target.value), onBlur: flush, onKeyDown: onKeyDown, slotProps: {
73
+ input: {
74
+ startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small", sx: { color: "text.disabled" } }) })),
75
+ endAdornment: pending || actions ? (_jsxs(InputAdornment, { position: "end", children: [pending && (_jsx(CircularProgress, { size: 14, thickness: 5, "aria-label": "\uC870\uD68C \uB300\uAE30 \uC911", sx: { mr: actions ? 0.5 : 0 } })), actions] })) : undefined,
76
+ },
77
+ }, sx: { minWidth: fullWidth ? "100%" : 220, ...sx } }));
78
+ }
@@ -0,0 +1,41 @@
1
+ import type { RowData } from "@tanstack/react-table";
2
+ import { type TableContentProps } from "../content/TableContent";
3
+ export interface VirtualizeOptions {
4
+ /** 행 높이 추정치(px). 실제 높이는 렌더 후 측정해 보정한다. 기본 small 밀도 기준 37 */
5
+ estimateRowHeight?: number;
6
+ /** 화면 밖에 미리 그릴 행 수. 기본 8 */
7
+ overscan?: number;
8
+ }
9
+ export interface VirtualizeColumnsOptions {
10
+ /** 화면 밖에 미리 그릴 열 수. 기본 3 */
11
+ overscan?: number;
12
+ }
13
+ export interface VirtualTableContentProps<T extends RowData> extends TableContentProps<T> {
14
+ /** `true` 또는 옵션. false 면 일반 `TableContent` 와 같다 */
15
+ virtualize?: boolean | VirtualizeOptions;
16
+ /**
17
+ * 열 가상화 — 고정되지 않은 가운데 컬럼 중 **가로 스크롤 범위에 보이는 것만** 그린다.
18
+ * 고정(left/right) 컬럼은 항상 그린다. 폭은 `column.getSize()` 를 그대로 쓰므로(측정하지 않음)
19
+ * 컬럼에 `size` 를 지정해 두는 편이 정확하다. 컬럼 그룹 헤더(2단 이상)에서는 자동으로 꺼진다.
20
+ */
21
+ virtualizeColumns?: boolean | VirtualizeColumnsOptions;
22
+ /**
23
+ * 가상화는 스크롤 컨테이너가 있어야 성립한다 — `maxHeight` 가 없으면 컨테이너가 내용만큼
24
+ * 늘어나 모든 행이 "화면 안" 이 되고, 결국 전부 그린다. 반드시 지정한다.
25
+ */
26
+ maxHeight: number | string;
27
+ }
28
+ /**
29
+ * 행(·열) 가상화 테이블 — `@tanstack/react-virtual` 로 보이는 행만 그린다.
30
+ *
31
+ * 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다. 메인 진입점에 두면 가상화를
32
+ * 쓰지 않는 소비자 번들에도 virtual-core 가 들어가고, `@tanstack/react-virtual` 을 설치하지
33
+ * 않은 소비자는 번들러 해석 단계에서 실패한다(optional peer).
34
+ *
35
+ * 그룹핑(`groupItems`) 도 평탄화 항목 단위로 가상화된다. detail panel 이 펼쳐진 행은
36
+ * 본문 행만 측정되므로 스크롤 위치 추정이 조금 어긋날 수 있다(그려지는 데는 문제 없다).
37
+ *
38
+ * 열 가상화(`virtualizeColumns`)는 같은 스크롤 컨테이너의 `scrollLeft` 를 읽는 가로 virtualizer 다.
39
+ * 그려지지 않은 폭은 좌·우 패딩 셀이 대신 차지해 스크롤 폭과 고정 컬럼 오프셋이 유지된다.
40
+ */
41
+ export declare function VirtualTableContent<T extends RowData>({ virtualize, virtualizeColumns, ...props }: VirtualTableContentProps<T>): import("react").JSX.Element;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from "react";
3
+ import { useVirtualizer } from "@tanstack/react-virtual";
4
+ import { TableContentBase } from "../content/TableContent";
5
+ /**
6
+ * 행(·열) 가상화 테이블 — `@tanstack/react-virtual` 로 보이는 행만 그린다.
7
+ *
8
+ * 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다. 메인 진입점에 두면 가상화를
9
+ * 쓰지 않는 소비자 번들에도 virtual-core 가 들어가고, `@tanstack/react-virtual` 을 설치하지
10
+ * 않은 소비자는 번들러 해석 단계에서 실패한다(optional peer).
11
+ *
12
+ * 그룹핑(`groupItems`) 도 평탄화 항목 단위로 가상화된다. detail panel 이 펼쳐진 행은
13
+ * 본문 행만 측정되므로 스크롤 위치 추정이 조금 어긋날 수 있다(그려지는 데는 문제 없다).
14
+ *
15
+ * 열 가상화(`virtualizeColumns`)는 같은 스크롤 컨테이너의 `scrollLeft` 를 읽는 가로 virtualizer 다.
16
+ * 그려지지 않은 폭은 좌·우 패딩 셀이 대신 차지해 스크롤 폭과 고정 컬럼 오프셋이 유지된다.
17
+ */
18
+ export function VirtualTableContent({ virtualize = true, virtualizeColumns = false, ...props }) {
19
+ const options = typeof virtualize === "object" ? virtualize : {};
20
+ const enabled = virtualize !== false;
21
+ const columnOptions = typeof virtualizeColumns === "object" ? virtualizeColumns : {};
22
+ const columnsEnabled = virtualizeColumns !== false;
23
+ const containerRef = useRef(null);
24
+ const grouped = !!props.groupItems && props.groupItems.length > 0;
25
+ const count = grouped ? props.groupItems.length : props.table.getRowModel().rows.length;
26
+ const virtualizer = useVirtualizer({
27
+ count: enabled ? count : 0,
28
+ getScrollElement: () => containerRef.current,
29
+ estimateSize: () => options.estimateRowHeight ?? 37,
30
+ overscan: options.overscan ?? 8,
31
+ // data-index 를 읽어 어느 행을 측정했는지 안다 (TableContentBase 가 행마다 붙인다)
32
+ indexAttribute: "data-index",
33
+ });
34
+ // ── 열 가상화 ──
35
+ const centerColumns = props.table.getCenterVisibleLeafColumns();
36
+ const columnSizing = props.table.getState().columnSizing;
37
+ // 폭은 측정하지 않고 컬럼 정의(리사이즈 상태 포함)를 그대로 믿는다
38
+ const sizeKey = useMemo(() => centerColumns.map((column) => `${column.id}:${column.getSize()}`).join("|"), [centerColumns, columnSizing]);
39
+ const columnVirtualizer = useVirtualizer({
40
+ horizontal: true,
41
+ count: columnsEnabled ? centerColumns.length : 0,
42
+ getScrollElement: () => containerRef.current,
43
+ estimateSize: (index) => centerColumns[index]?.getSize() ?? 150,
44
+ overscan: columnOptions.overscan ?? 3,
45
+ });
46
+ // 리사이즈·표시 토글로 폭 배열이 바뀌면 캐시된 측정치를 버린다
47
+ useEffect(() => { columnVirtualizer.measure(); }, [sizeKey, columnVirtualizer]);
48
+ if (!enabled && !columnsEnabled)
49
+ return _jsx(TableContentBase, { ...props });
50
+ return (_jsx(TableContentBase, { ...props, containerRef: containerRef, virtual: enabled ? {
51
+ items: virtualizer.getVirtualItems(),
52
+ totalSize: virtualizer.getTotalSize(),
53
+ measureElement: virtualizer.measureElement,
54
+ } : undefined, virtualColumns: columnsEnabled ? {
55
+ items: columnVirtualizer.getVirtualItems(),
56
+ totalSize: columnVirtualizer.getTotalSize(),
57
+ } : undefined }));
58
+ }
@@ -0,0 +1,13 @@
1
+ import type { RowData } from "@tanstack/react-table";
2
+ import { type TableScreenProps } from "../TableScreen";
3
+ export type VirtualTableScreenProps<T extends RowData> = TableScreenProps<T>;
4
+ /**
5
+ * 가상화 화면 — `TableScreen` 과 같은 props 에 본문만 `VirtualTableContent` 로 바꾼 것.
6
+ *
7
+ * `definition.features.virtualize` 를 읽는다: `true` 면 행 가상화, 객체면 `rows`/`columns`/
8
+ * `estimateRowHeight`/`maxHeight`. 스크롤 컨테이너 높이는 `contentProps.maxHeight` →
9
+ * `features.virtualize.maxHeight` → 520 순이다 (없으면 가상화가 성립하지 않는다).
10
+ *
11
+ * 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다 — `@tanstack/react-virtual` 은 optional peer 다.
12
+ */
13
+ export declare function VirtualTableScreen<T extends RowData>(props: VirtualTableScreenProps<T>): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { TableScreenBase } from "../TableScreen";
3
+ import { VirtualTableContent } from "./VirtualTableContent";
4
+ const DEFAULT_MAX_HEIGHT = 520;
5
+ /**
6
+ * 가상화 화면 — `TableScreen` 과 같은 props 에 본문만 `VirtualTableContent` 로 바꾼 것.
7
+ *
8
+ * `definition.features.virtualize` 를 읽는다: `true` 면 행 가상화, 객체면 `rows`/`columns`/
9
+ * `estimateRowHeight`/`maxHeight`. 스크롤 컨테이너 높이는 `contentProps.maxHeight` →
10
+ * `features.virtualize.maxHeight` → 520 순이다 (없으면 가상화가 성립하지 않는다).
11
+ *
12
+ * 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다 — `@tanstack/react-virtual` 은 optional peer 다.
13
+ */
14
+ export function VirtualTableScreen(props) {
15
+ const feature = props.bundle.definition.features?.virtualize;
16
+ const options = typeof feature === "object" ? feature : {};
17
+ const rows = feature === false ? false : (options.rows ?? true);
18
+ const virtualize = rows === true
19
+ ? (options.estimateRowHeight !== undefined ? { estimateRowHeight: options.estimateRowHeight } : true)
20
+ : rows;
21
+ const virtualizeColumns = options.columns ?? false;
22
+ return (_jsx(TableScreenBase, { ...props, renderContent: (contentProps) => (_jsx(VirtualTableContent, { ...contentProps, virtualize: virtualize, virtualizeColumns: virtualizeColumns, maxHeight: contentProps.maxHeight ?? options.maxHeight ?? DEFAULT_MAX_HEIGHT })) }));
23
+ }
@@ -0,0 +1,4 @@
1
+ export { VirtualTableContent } from "./virtual/VirtualTableContent";
2
+ export type { VirtualTableContentProps, VirtualizeOptions, VirtualizeColumnsOptions } from "./virtual/VirtualTableContent";
3
+ export { VirtualTableScreen } from "./virtual/VirtualTableScreen";
4
+ export type { VirtualTableScreenProps } from "./virtual/VirtualTableScreen";
@@ -0,0 +1,4 @@
1
+ // 별도 진입점 `kokotable/mui/virtual` — `@tanstack/react-virtual` 이 필요한 것만 여기서 export 한다.
2
+ // 메인 진입점(`./index`) 은 이 파일을 import 하지 않는다.
3
+ export { VirtualTableContent } from "./virtual/VirtualTableContent";
4
+ export { VirtualTableScreen } from "./virtual/VirtualTableScreen";
@@ -0,0 +1,26 @@
1
+ import type { FilterOperator } from "../../../core/src/index";
2
+ import { type VirtualColumnField } from "./expression";
3
+ import type { VirtualColumn } from "./useVirtualColumns";
4
+ /** 숫자 가상 열 필터 연산자 (기존 ERP 카드와 같은 목록) */
5
+ export declare const VIRTUAL_NUMBER_OPERATORS: ReadonlyArray<{
6
+ value: FilterOperator;
7
+ label: string;
8
+ }>;
9
+ export interface VirtualColumnCardProps {
10
+ column: VirtualColumn;
11
+ onEdit: () => void;
12
+ onRemove: () => void;
13
+ /** 수식 표시에 쓸 필드 (키 → 헤더 치환) */
14
+ fields?: ReadonlyArray<VirtualColumnField>;
15
+ /**
16
+ * 필터 입력. 주면 카드 아래에 (숫자면 연산자 + 값, 아니면 텍스트) 입력이 붙는다.
17
+ * `search.setFilter(slotKey, value)` / `search.setOperator(slotKey, op)` 로 잇는다.
18
+ */
19
+ filter?: {
20
+ value: unknown;
21
+ operator: FilterOperator;
22
+ onChange: (value: unknown, operator: FilterOperator) => void;
23
+ };
24
+ }
25
+ /** 가상 열 카드 — 이름 · 수식 요약 · 타입 · 편집/삭제 (+ 선택적 필터) */
26
+ export declare function VirtualColumnCard({ column, onEdit, onRemove, fields, filter }: VirtualColumnCardProps): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Chip, IconButton, MenuItem, Select, TextField, Tooltip, Typography } from "@mui/material";
3
+ import EditIcon from "@mui/icons-material/Edit";
4
+ import CloseIcon from "@mui/icons-material/Close";
5
+ import { describeExpression } from "./expression";
6
+ /** 숫자 가상 열 필터 연산자 (기존 ERP 카드와 같은 목록) */
7
+ export const VIRTUAL_NUMBER_OPERATORS = [
8
+ { value: "greaterThanOrEqualTo", label: ">=" },
9
+ { value: "lessThanOrEqualTo", label: "<=" },
10
+ { value: "equals", label: "=" },
11
+ { value: "greaterThan", label: ">" },
12
+ { value: "lessThan", label: "<" },
13
+ ];
14
+ const TYPE_LABELS = { number: "숫자", string: "문자열", boolean: "참/거짓" };
15
+ /** 가상 열 카드 — 이름 · 수식 요약 · 타입 · 편집/삭제 (+ 선택적 필터) */
16
+ export function VirtualColumnCard({ column, onEdit, onRemove, fields = [], filter }) {
17
+ const isNumber = column.type === "number";
18
+ const display = fields.length > 0 ? describeExpression(column.expr, fields) : column.expr;
19
+ return (_jsxs(Box, { "data-testid": `virtual-column-card-${column.slotKey}`, sx: {
20
+ display: "flex", flexDirection: "column",
21
+ border: "1px solid", borderColor: "divider", borderRadius: 1,
22
+ borderLeft: "4px solid", borderLeftColor: "info.main",
23
+ p: 1, minWidth: 0,
24
+ }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 0.5, gap: 0.5 }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75, minWidth: 0 }, children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 600, color: "info.dark", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: column.name }), _jsx(Chip, { label: TYPE_LABELS[column.type], size: "small", variant: "outlined", sx: { height: 18, fontSize: 11 } })] }), _jsxs(Box, { sx: { display: "flex", flexShrink: 0 }, children: [_jsx(Tooltip, { title: "\uD3B8\uC9D1", children: _jsx(IconButton, { size: "small", "aria-label": `${column.name} 편집`, onClick: onEdit, sx: { p: 0.25 }, children: _jsx(EditIcon, { sx: { fontSize: 16 } }) }) }), _jsx(Tooltip, { title: "\uC0AD\uC81C", children: _jsx(IconButton, { size: "small", "aria-label": `${column.name} 삭제`, onClick: onRemove, sx: { p: 0.25 }, children: _jsx(CloseIcon, { sx: { fontSize: 16 } }) }) })] })] }), _jsx(Tooltip, { title: `${column.slotKey}: ${column.expr}`, placement: "top", children: _jsxs(Typography, { variant: "caption", sx: { color: "text.secondary", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "monospace" }, children: ["= ", display] }) }), filter && (_jsx(Box, { sx: { display: "flex", gap: 0.5, alignItems: "center", mt: 1 }, children: isNumber ? (_jsxs(_Fragment, { children: [_jsx(Select, { size: "small", value: filter.operator, onChange: (event) => filter.onChange(filter.value, event.target.value), inputProps: { "aria-label": `${column.name} 연산자` }, sx: { minWidth: 64, fontSize: "0.8rem" }, children: VIRTUAL_NUMBER_OPERATORS.map((op) => (_jsx(MenuItem, { value: op.value, sx: { fontSize: "0.8rem" }, children: op.label }, op.value))) }), _jsx(TextField, { size: "small", type: "number", placeholder: "\uAC12", value: filter.value ?? "", inputProps: { "aria-label": `${column.name} 필터` }, onChange: (event) => {
25
+ const raw = event.target.value;
26
+ const parsed = Number.parseFloat(raw);
27
+ filter.onChange(raw === "" || Number.isNaN(parsed) ? undefined : parsed, filter.operator);
28
+ }, sx: { flex: 1, "& input": { fontSize: "0.8rem" } } })] })) : (_jsx(TextField, { size: "small", placeholder: "\uD544\uD130 \uAC12 \uC785\uB825", fullWidth: true, value: filter.value ?? "", inputProps: { "aria-label": `${column.name} 필터` }, onChange: (event) => filter.onChange(event.target.value || undefined, filter.operator), sx: { "& input": { fontSize: "0.8rem" } } })) }))] }));
29
+ }
@@ -0,0 +1,27 @@
1
+ import { type ExpressionValidation, type VirtualColumnField } from "./expression";
2
+ import type { VirtualColumn, VirtualColumnInput } from "./useVirtualColumns";
3
+ export interface VirtualColumnDialogProps {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ /**
7
+ * 저장. 실패(`ok: false`)를 돌려주면 다이얼로그가 닫히지 않고 메시지를 보여 준다 —
8
+ * `useVirtualColumns().add/update` 를 그대로 넘기면 된다.
9
+ */
10
+ onSubmit: (column: VirtualColumnInput) => ExpressionValidation | {
11
+ ok: true;
12
+ slotKey: string;
13
+ } | void;
14
+ /** 편집 모드일 때 기존 값 */
15
+ initialValues?: VirtualColumn;
16
+ /** 수식에 쓸 수 있는 필드 (`virtualColumnFields(columns)`) */
17
+ fields: ReadonlyArray<VirtualColumnField>;
18
+ /** 저장 전 검증. 없으면 `validateExpression` 만 건다 */
19
+ validate?: (column: VirtualColumnInput) => ExpressionValidation;
20
+ }
21
+ /**
22
+ * 가상 열 추가·편집 다이얼로그 — 이름 · 칩 기반 수식 빌더 · 연산자 · 필드 · 타입.
23
+ *
24
+ * 기존 ERP `CustomColumnDialog` 의 UX 를 그대로 옮겼다(칩 삽입, Backspace 로 마지막 세그먼트
25
+ * 제거, Enter 저장, 한글 헤더 미리보기). 검증 실패는 인라인 Alert 로 보여 주고 닫지 않는다.
26
+ */
27
+ export declare function VirtualColumnDialog({ open, onClose, onSubmit, initialValues, fields, validate, }: VirtualColumnDialogProps): import("react").JSX.Element;
@@ -0,0 +1,154 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, IconButton, Radio, RadioGroup, TextField, Tooltip, Typography, } from "@mui/material";
4
+ import CloseIcon from "@mui/icons-material/Close";
5
+ import { TEMPLATE_FIELD_PATTERN, validateExpression, } from "./expression";
6
+ const OPERATORS = [
7
+ { label: "+", value: " + ", tooltip: "더하기" },
8
+ { label: "-", value: " - ", tooltip: "빼기" },
9
+ { label: "*", value: " * ", tooltip: "곱하기" },
10
+ { label: "/", value: " / ", tooltip: "나누기" },
11
+ { label: "(", value: "(", tooltip: "여는 괄호" },
12
+ { label: ")", value: ")", tooltip: "닫는 괄호" },
13
+ ];
14
+ const TYPE_OPTIONS = [
15
+ { value: "number", label: "숫자" },
16
+ { value: "string", label: "문자열" },
17
+ { value: "boolean", label: "참/거짓" },
18
+ ];
19
+ /**
20
+ * 수식 문자열 → 세그먼트. `{key}` 는 필드 칩, 필드 목록에 있는 맨 식별자도 칩으로 올린다.
21
+ * 나머지는 텍스트다.
22
+ */
23
+ function parseSegments(expr, fields) {
24
+ if (!expr)
25
+ return [];
26
+ const byKey = new Map(fields.map((f) => [f.key, f.header]));
27
+ const segments = [];
28
+ const pushText = (value) => {
29
+ if (!value)
30
+ return;
31
+ const last = segments[segments.length - 1];
32
+ if (last && last.type === "text")
33
+ last.value += value;
34
+ else
35
+ segments.push({ type: "text", value });
36
+ };
37
+ const bare = [...byKey.keys()].sort((a, b) => b.length - a.length)
38
+ .map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
39
+ const pattern = new RegExp(`${TEMPLATE_FIELD_PATTERN.source}${bare.length > 0 ? `|(?<![\\w.])(${bare.join("|")})(?![\\w.])` : ""}`, "g");
40
+ let cursor = 0;
41
+ for (const match of expr.matchAll(pattern)) {
42
+ pushText(expr.slice(cursor, match.index));
43
+ const key = match[1] ?? match[2];
44
+ segments.push({ type: "field", key, header: byKey.get(key) ?? key });
45
+ cursor = match.index + match[0].length;
46
+ }
47
+ pushText(expr.slice(cursor));
48
+ return segments;
49
+ }
50
+ /** 세그먼트 → 수식. 필드는 항상 `{key}` 로 통일한다 (서버가 정규화한다) */
51
+ function segmentsToExpr(segments, pending) {
52
+ const all = pending ? [...segments, { type: "text", value: pending }] : segments;
53
+ return all.map((s) => (s.type === "field" ? `{${s.key}}` : s.value)).join("");
54
+ }
55
+ /**
56
+ * 가상 열 추가·편집 다이얼로그 — 이름 · 칩 기반 수식 빌더 · 연산자 · 필드 · 타입.
57
+ *
58
+ * 기존 ERP `CustomColumnDialog` 의 UX 를 그대로 옮겼다(칩 삽입, Backspace 로 마지막 세그먼트
59
+ * 제거, Enter 저장, 한글 헤더 미리보기). 검증 실패는 인라인 Alert 로 보여 주고 닫지 않는다.
60
+ */
61
+ export function VirtualColumnDialog({ open, onClose, onSubmit, initialValues, fields, validate, }) {
62
+ const [name, setName] = useState("");
63
+ const [segments, setSegments] = useState([]);
64
+ const [textInput, setTextInput] = useState("");
65
+ const [type, setType] = useState("number");
66
+ const [error, setError] = useState(null);
67
+ const inputRef = useRef(null);
68
+ useEffect(() => {
69
+ if (!open)
70
+ return;
71
+ setError(null);
72
+ setTextInput("");
73
+ if (initialValues) {
74
+ setName(initialValues.name);
75
+ setType(initialValues.type);
76
+ setSegments(parseSegments(initialValues.expr, fields));
77
+ }
78
+ else {
79
+ setName("");
80
+ setType("number");
81
+ setSegments([]);
82
+ }
83
+ }, [open, initialValues, fields]);
84
+ const commitText = useCallback(() => {
85
+ if (!textInput)
86
+ return;
87
+ setSegments((prev) => [...prev, { type: "text", value: textInput }]);
88
+ setTextInput("");
89
+ }, [textInput]);
90
+ const addField = (field) => {
91
+ commitText();
92
+ setSegments((prev) => [...prev, { type: "field", key: field.key, header: field.header }]);
93
+ setError(null);
94
+ inputRef.current?.focus();
95
+ };
96
+ const addOperator = (value) => {
97
+ commitText();
98
+ setSegments((prev) => [...prev, { type: "text", value }]);
99
+ setError(null);
100
+ inputRef.current?.focus();
101
+ };
102
+ const removeSegment = (index) => {
103
+ setSegments((prev) => prev.filter((_, i) => i !== index));
104
+ inputRef.current?.focus();
105
+ };
106
+ const expr = segmentsToExpr(segments, textInput).trim();
107
+ const hasExpr = expr.length > 0;
108
+ const canSubmit = name.trim().length > 0 && hasExpr;
109
+ const preview = useMemo(() => {
110
+ const all = textInput ? [...segments, { type: "text", value: textInput }] : segments;
111
+ return all.map((s) => (s.type === "field" ? s.header : s.value)).join("");
112
+ }, [segments, textInput]);
113
+ const handleSubmit = () => {
114
+ if (!canSubmit)
115
+ return;
116
+ const column = { name: name.trim(), expr, type };
117
+ const check = validate ? validate(column) : validateExpression(expr, { type, fields });
118
+ if (!check.ok) {
119
+ setError(check.message);
120
+ return;
121
+ }
122
+ const result = onSubmit(column);
123
+ if (result && result.ok === false) {
124
+ setError(result.message);
125
+ return;
126
+ }
127
+ onClose();
128
+ };
129
+ const handleKeyDown = (event) => {
130
+ if (event.key === "Backspace" && textInput === "" && segments.length > 0) {
131
+ event.preventDefault();
132
+ const last = segments[segments.length - 1];
133
+ setSegments((prev) => prev.slice(0, -1));
134
+ if (last.type === "text")
135
+ setTextInput(last.value);
136
+ }
137
+ if (event.key === "Enter") {
138
+ event.preventDefault();
139
+ handleSubmit();
140
+ }
141
+ };
142
+ const isEdit = !!initialValues;
143
+ return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: "sm", fullWidth: true, "aria-labelledby": "virtual-column-dialog-title", children: [_jsxs(DialogTitle, { id: "virtual-column-dialog-title", sx: { display: "flex", alignItems: "center", justifyContent: "space-between", pb: 1 }, children: [_jsx(Typography, { variant: "h6", component: "span", children: isEdit ? "가상 열 편집" : "가상 열 추가" }), _jsx(IconButton, { size: "small", onClick: onClose, "aria-label": "\uB2EB\uAE30", children: _jsx(CloseIcon, {}) })] }), _jsx(DialogContent, { dividers: true, children: _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2.5 }, children: [_jsx(TextField, { label: "\uC774\uB984", placeholder: "\uC608: \uD560\uC778\uC561", value: name, onChange: (event) => { setName(event.target.value); setError(null); }, size: "small", fullWidth: true, autoFocus: true, inputProps: { "aria-label": "가상 열 이름" } }), _jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: "text.secondary", mb: 0.5 }, children: "\uC218\uC2DD" }), _jsxs(Box, { onClick: () => inputRef.current?.focus(), sx: {
144
+ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 0.5,
145
+ border: "1px solid", borderColor: error ? "error.main" : "grey.400", borderRadius: 1,
146
+ px: 1, py: 0.75, minHeight: 42, cursor: "text",
147
+ "&:focus-within": { borderColor: error ? "error.main" : "primary.main" },
148
+ }, children: [segments.map((segment, index) => segment.type === "field" ? (_jsx(Chip, { label: segment.header, size: "small", color: "primary", variant: "outlined", onDelete: () => removeSegment(index), sx: { fontWeight: 500 } }, index)) : (_jsx(Typography, { variant: "body2", component: "span", sx: { fontFamily: "monospace", fontWeight: 600, color: "text.secondary", whiteSpace: "pre" }, children: segment.value }, index))), _jsx("input", { ref: inputRef, "aria-label": "\uC218\uC2DD \uC785\uB825", value: textInput, onChange: (event) => { setTextInput(event.target.value); setError(null); }, onKeyDown: handleKeyDown, placeholder: segments.length === 0 ? "아래 필드와 연산자를 클릭하거나 직접 입력하세요" : "", style: {
149
+ border: "none", outline: "none", flex: 1, minWidth: 80,
150
+ fontFamily: "monospace", fontSize: "0.875rem", padding: 0, background: "transparent",
151
+ } })] }), _jsx(Typography, { variant: "caption", sx: { color: "text.secondary", display: "block", mt: 0.5 }, children: type === "string"
152
+ ? "문자열 템플릿 — {필드} 참조가 하나 이상 필요합니다. 예: {customer} / {channel}"
153
+ : "사칙연산 · 괄호 · 숫자 · DATE_TRUNC('month', 날짜) · EXTRACT('year', 날짜)" }), preview && (_jsxs(Box, { sx: { mt: 1, px: 1.5, py: 0.75, bgcolor: "action.hover", borderRadius: 1, border: "1px solid", borderColor: "divider" }, children: [_jsx(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: "\uBBF8\uB9AC\uBCF4\uAE30" }), _jsxs(Typography, { variant: "body2", sx: { fontWeight: 500 }, "data-testid": "virtual-column-preview", children: ["= ", preview] })] }))] }), _jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: "text.secondary", mb: 0.5 }, children: "\uC5F0\uC0B0\uC790" }), _jsx(Box, { sx: { display: "flex", gap: 0.5 }, children: OPERATORS.map((op) => (_jsx(Tooltip, { title: op.tooltip, placement: "top", children: _jsx(Button, { variant: "outlined", size: "small", "aria-label": `연산자 ${op.label}`, onClick: () => addOperator(op.value), sx: { minWidth: 36, px: 1, fontFamily: "monospace", fontWeight: 700, fontSize: "1rem" }, children: op.label }) }, op.label))) })] }), fields.length > 0 && (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: "text.secondary", mb: 1 }, children: "\uD544\uB4DC (\uD074\uB9AD\uD558\uC5EC \uC218\uC2DD\uC5D0 \uC0BD\uC785)" }), _jsx(Box, { sx: { display: "flex", flexWrap: "wrap", gap: 0.5 }, children: fields.map((field) => (_jsx(Chip, { label: field.header, size: "small", variant: "outlined", onClick: () => addField(field), "data-testid": `virtual-field-${field.key}`, sx: { cursor: "pointer", "&:hover": { borderColor: "primary.main" } } }, field.key))) })] })), _jsxs(FormControl, { children: [_jsx(FormLabel, { sx: { fontSize: "0.875rem", fontWeight: 500 }, children: "\uACB0\uACFC \uD0C0\uC785" }), _jsx(RadioGroup, { row: true, value: type, onChange: (event) => { setType(event.target.value); setError(null); }, children: TYPE_OPTIONS.map((option) => (_jsx(FormControlLabel, { value: option.value, control: _jsx(Radio, { size: "small" }), label: option.label }, option.value))) })] }), error && _jsx(Alert, { severity: "error", role: "alert", children: error })] }) }), _jsxs(DialogActions, { sx: { px: 3, py: 2 }, children: [_jsx(Button, { onClick: onClose, color: "inherit", children: "\uCDE8\uC18C" }), _jsx(Button, { variant: "contained", onClick: handleSubmit, disabled: !canSubmit, children: isEdit ? "수정" : "추가" })] })] }));
154
+ }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from "react";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ import type { VirtualColumnField } from "./expression";
4
+ import type { UseVirtualColumnsReturn } from "./useVirtualColumns";
5
+ export interface VirtualColumnSectionProps<T extends RowData = RowData> {
6
+ /** `useVirtualColumns(search)` 결과 */
7
+ virtualColumns: UseVirtualColumnsReturn<T>;
8
+ /** 수식에 쓸 필드. 생략하면 훅의 `fields` 를 쓴다 */
9
+ fields?: ReadonlyArray<VirtualColumnField>;
10
+ /**
11
+ * `FilterArea direction="grid"` 의 children 으로 넣을 때 카드 하나의 Grid size —
12
+ * `resolveFilterGridSize(gridCols)` 값을 그대로. 없으면 카드를 가로로 흘려 놓는다(툴바용).
13
+ */
14
+ gridSize?: Record<string, number>;
15
+ /**
16
+ * 카드에 필터 입력을 붙인다. 값은 `search.setFilter(slotKey, …)` / `setOperator` 로 들어가
17
+ * 다른 필터와 같이 draft 에 쌓인다 (검색 버튼으로 적용).
18
+ */
19
+ enableFilter?: boolean;
20
+ /** 추가 버튼 문구 */
21
+ addLabel?: ReactNode;
22
+ /** 카드 폭 (inline 배치일 때). 기본 220 */
23
+ cardWidth?: number;
24
+ }
25
+ /**
26
+ * 가상 열 섹션 — 카드 목록 + 추가 버튼 + 추가/편집 다이얼로그.
27
+ *
28
+ * 기존 ERP `VirtualColumnSection` 대응. `FilterArea` 의 children 이나 툴바 어디든 놓을 수 있고
29
+ * 다이얼로그 상태를 스스로 관리한다. 상태는 훅(`useVirtualColumns`)이 검색 상태 위에서 들고 있다.
30
+ */
31
+ export declare function VirtualColumnSection<T extends RowData>({ virtualColumns, fields: fieldsProp, gridSize, enableFilter, addLabel, cardWidth, }: VirtualColumnSectionProps<T>): import("react").JSX.Element;