@vmscloud/moz-ui-grid-vue 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/MozGrid.vue.d.ts +127 -0
- package/dist/bridge/VueRenderer.d.ts +37 -0
- package/dist/bridge/fieldBuilder.d.ts +9 -0
- package/dist/bridge/lifecycle.d.ts +6 -0
- package/dist/bridge/templateRegistry.d.ts +8 -0
- package/dist/chunks/index-BDQ0HlwR.js +3404 -0
- package/dist/chunks/index-D5KJLBGW.cjs +14 -0
- package/dist/chunks/notification.contract-BTOch6ed.js +16 -0
- package/dist/chunks/notification.contract-oFkBaE2U.cjs +1 -0
- package/dist/compat/MozGridVue.vue.d.ts +645 -0
- package/dist/compat/MozPivotVue.vue.d.ts +606 -0
- package/dist/compat/index.d.ts +9 -0
- package/dist/compat/legacyGridAliases.d.ts +8 -0
- package/dist/composables/useGridPreset.d.ts +19 -0
- package/dist/contracts/UserLayoutInstance.d.ts +38 -0
- package/dist/contracts/notification.contract.d.ts +41 -0
- package/dist/contracts/userLayout.contract.d.ts +16 -0
- package/dist/index.cjs +26 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +90599 -0
- package/dist/legacy/components/common/Dragging.d.ts +17 -0
- package/dist/legacy/components/common/Dragging.vue.d.ts +49 -0
- package/dist/legacy/components/common/custom-cell/FormulaEngine.d.ts +256 -0
- package/dist/legacy/components/common/custom-cell/FormulaInput.vue.d.ts +33 -0
- package/dist/legacy/components/common/custom-cell/GridCustomCell.vue.d.ts +23 -0
- package/dist/legacy/components/common/custom-cell/useFormulaAutocomplete.d.ts +44 -0
- package/dist/legacy/components/common/excel/ClientExcelUploadPopup.vue.d.ts +201 -0
- package/dist/legacy/components/common/excel/assets/IconDropZone.vue.d.ts +2 -0
- package/dist/legacy/components/common/excel/assets/IconFileUpload.vue.d.ts +2 -0
- package/dist/legacy/components/common/filter/GridFilter.vue.d.ts +23 -0
- package/dist/legacy/components/common/footer/GridFooter.vue.d.ts +29 -0
- package/dist/legacy/components/common/sort/GridSort.vue.d.ts +23 -0
- package/dist/legacy/components/common/toolbar/GridSearchInput.vue.d.ts +33 -0
- package/dist/legacy/components/grid/MozGridVue.vue.d.ts +2041 -0
- package/dist/legacy/components/grid/MozGridVueCore.vue.d.ts +118 -0
- package/dist/legacy/components/grid/bulk-editor/GridBulkEditor.vue.d.ts +10 -0
- package/dist/legacy/components/grid/column-settings/AlignPicker.vue.d.ts +10 -0
- package/dist/legacy/components/grid/column-settings/ColorSwatchPicker.vue.d.ts +11 -0
- package/dist/legacy/components/grid/column-settings/ColumnListItem.vue.d.ts +17 -0
- package/dist/legacy/components/grid/column-settings/ColumnOptionsPanel.vue.d.ts +32 -0
- package/dist/legacy/components/grid/column-settings/FontSizePicker.vue.d.ts +10 -0
- package/dist/legacy/components/grid/column-settings/FontStyleToggles.vue.d.ts +15 -0
- package/dist/legacy/components/grid/column-settings/MaskPresetPicker.vue.d.ts +14 -0
- package/dist/legacy/components/grid/context-menu/GridContextMenu.vue.d.ts +208 -0
- package/dist/legacy/components/grid/group-panel/GroupPanel.vue.d.ts +18 -0
- package/dist/legacy/components/grid/toolbar/ColumnPicker.vue.d.ts +17 -0
- package/dist/legacy/components/grid/toolbar/ColumnSettings.vue.d.ts +30 -0
- package/dist/legacy/components/grid/toolbar/GridOptionModal.vue.d.ts +211 -0
- package/dist/legacy/components/grid/toolbar/GridToolBox.vue.d.ts +69 -0
- package/dist/legacy/components/pivot/MozPivotVue.vue.d.ts +1868 -0
- package/dist/legacy/components/pivot/PivotContextMenu.vue.d.ts +634 -0
- package/dist/legacy/components/pivot/PivotFieldChip.vue.d.ts +16 -0
- package/dist/legacy/components/pivot/PivotFieldDropZone.vue.d.ts +26 -0
- package/dist/legacy/components/pivot/PivotFieldPanel.vue.d.ts +28 -0
- package/dist/legacy/components/pivot/PivotOptionModal.vue.d.ts +534 -0
- package/dist/legacy/components/pivot/PivotToolBox.vue.d.ts +79 -0
- package/dist/legacy/components/pivot/chart/EChart.vue.d.ts +13 -0
- package/dist/legacy/components/pivot/chart/MozPivotChart.types.d.ts +16 -0
- package/dist/legacy/components/pivot/chart/MozPivotChart.vue.d.ts +14 -0
- package/dist/legacy/components/pivot/chart/PivotChartDetail.vue.d.ts +13 -0
- package/dist/legacy/components/pivot/chart/PivotChartMini.vue.d.ts +18 -0
- package/dist/legacy/components/pivot/chart/PivotChartMultiple.vue.d.ts +18 -0
- package/dist/legacy/components/pivot/chart/PivotChartToolbar.vue.d.ts +38 -0
- package/dist/legacy/components/pivot/chart/chartUtils.d.ts +45 -0
- package/dist/legacy/components/pivot/chart/useChartData.d.ts +35 -0
- package/dist/legacy/composables/useCellTooltip.d.ts +8 -0
- package/dist/legacy/composables/useClientExcelExport.d.ts +24 -0
- package/dist/legacy/composables/useClientExcelStore.d.ts +16 -0
- package/dist/legacy/composables/useColumnFormatter.d.ts +45 -0
- package/dist/legacy/composables/useColumnSettings.d.ts +35 -0
- package/dist/legacy/composables/useContextMenu.d.ts +71 -0
- package/dist/legacy/composables/useGridBulkEditor.d.ts +36 -0
- package/dist/legacy/composables/useGridChanges.d.ts +28 -0
- package/dist/legacy/composables/useGridColumns.d.ts +37 -0
- package/dist/legacy/composables/useGridCustomCell.d.ts +34 -0
- package/dist/legacy/composables/useGridEvents.d.ts +14 -0
- package/dist/legacy/composables/useGridFilter.d.ts +38 -0
- package/dist/legacy/composables/useGridInstance.d.ts +16 -0
- package/dist/legacy/composables/useGridSelect.d.ts +18 -0
- package/dist/legacy/composables/useGridSelection.d.ts +18 -0
- package/dist/legacy/composables/useGridSort.d.ts +38 -0
- package/dist/legacy/composables/useGridUndoRedo.d.ts +28 -0
- package/dist/legacy/composables/useGridValidation.d.ts +28 -0
- package/dist/legacy/composables/useMaskPresets.d.ts +38 -0
- package/dist/legacy/composables/useSearchHighlight.d.ts +12 -0
- package/dist/legacy/icons/IconAdd.vue.d.ts +11 -0
- package/dist/legacy/icons/IconArrowDownWithTail.vue.d.ts +11 -0
- package/dist/legacy/icons/IconArrowLeft.vue.d.ts +11 -0
- package/dist/legacy/icons/IconArrowLeftWithTail.vue.d.ts +11 -0
- package/dist/legacy/icons/IconArrowRight.vue.d.ts +11 -0
- package/dist/legacy/icons/IconArrowUpCenter.vue.d.ts +11 -0
- package/dist/legacy/icons/IconArrowUpWithTail.vue.d.ts +11 -0
- package/dist/legacy/icons/IconChart.vue.d.ts +11 -0
- package/dist/legacy/icons/IconClose.vue.d.ts +13 -0
- package/dist/legacy/icons/IconColorPicker.vue.d.ts +11 -0
- package/dist/legacy/icons/IconCustomCell.vue.d.ts +11 -0
- package/dist/legacy/icons/IconDecide.vue.d.ts +11 -0
- package/dist/legacy/icons/IconDownload.vue.d.ts +11 -0
- package/dist/legacy/icons/IconDragHandler.vue.d.ts +11 -0
- package/dist/legacy/icons/IconFilter.vue.d.ts +11 -0
- package/dist/legacy/icons/IconInfoFilled.vue.d.ts +11 -0
- package/dist/legacy/icons/IconRedo.vue.d.ts +9 -0
- package/dist/legacy/icons/IconReset.vue.d.ts +9 -0
- package/dist/legacy/icons/IconResetGridToolbar.vue.d.ts +11 -0
- package/dist/legacy/icons/IconSearch.vue.d.ts +11 -0
- package/dist/legacy/icons/IconSetting.vue.d.ts +11 -0
- package/dist/legacy/icons/IconSettingFilled.vue.d.ts +11 -0
- package/dist/legacy/icons/IconSort.vue.d.ts +11 -0
- package/dist/legacy/icons/IconToastWarning.vue.d.ts +11 -0
- package/dist/legacy/icons/IconUndo.vue.d.ts +9 -0
- package/dist/legacy/icons/index.d.ts +25 -0
- package/dist/legacy/index.d.ts +69 -0
- package/dist/legacy/shared/colorPresets.d.ts +36 -0
- package/dist/legacy/store/UploadRefreshRegistry.d.ts +5 -0
- package/dist/legacy/store/excelStore.d.ts +89 -0
- package/dist/legacy/store/index.d.ts +231 -0
- package/dist/legacy/stores/index.d.ts +2 -0
- package/dist/legacy/stores/projectInfo/projectInfo.contract.d.ts +130 -0
- package/dist/legacy/stories/Autofit.stories.d.ts +9 -0
- package/dist/legacy/stories/CRUD.stories.d.ts +9 -0
- package/dist/legacy/stories/CellAttributes.stories.d.ts +19 -0
- package/dist/legacy/stories/CellMask.stories.d.ts +19 -0
- package/dist/legacy/stories/ClientExcel.stories.d.ts +8 -0
- package/dist/legacy/stories/Clipboard.stories.d.ts +12 -0
- package/dist/legacy/stories/ColumnVisibility.stories.d.ts +12 -0
- package/dist/legacy/stories/ColumnWidths.stories.d.ts +11 -0
- package/dist/legacy/stories/CustomSortFilter.stories.d.ts +7 -0
- package/dist/legacy/stories/DynamicRowHeight.stories.d.ts +13 -0
- package/dist/legacy/stories/Editors.stories.d.ts +9 -0
- package/dist/legacy/stories/EmptyState.stories.d.ts +10 -0
- package/dist/legacy/stories/FormatRow.stories.d.ts +20 -0
- package/dist/legacy/stories/Grouping.stories.d.ts +14 -0
- package/dist/legacy/stories/HeaderGroup.stories.d.ts +11 -0
- package/dist/legacy/stories/LargeData.stories.d.ts +19 -0
- package/dist/legacy/stories/MasterDetail.stories.d.ts +17 -0
- package/dist/legacy/stories/Merge.stories.d.ts +10 -0
- package/dist/legacy/stories/MultiRow.stories.d.ts +12 -0
- package/dist/legacy/stories/MultiRowCrud.stories.d.ts +11 -0
- package/dist/legacy/stories/MultiRowGroup.stories.d.ts +11 -0
- package/dist/legacy/stories/Pinning.stories.d.ts +44 -0
- package/dist/legacy/stories/Pivot.stories.d.ts +23 -0
- package/dist/legacy/stories/PivotAdvanced.stories.d.ts +20 -0
- package/dist/legacy/stories/PivotChart.stories.d.ts +23 -0
- package/dist/legacy/stories/PivotNonNumeric.stories.d.ts +27 -0
- package/dist/legacy/stories/Preset.stories.d.ts +11 -0
- package/dist/legacy/stories/ReactiveAPI.stories.d.ts +8 -0
- package/dist/legacy/stories/ReactiveSafety.stories.d.ts +28 -0
- package/dist/legacy/stories/Repro.stories.d.ts +36 -0
- package/dist/legacy/stories/RowAPI.stories.d.ts +12 -0
- package/dist/legacy/stories/SelectionModes.stories.d.ts +35 -0
- package/dist/legacy/stories/Streaming.stories.d.ts +12 -0
- package/dist/legacy/stories/Tree.stories.d.ts +13 -0
- package/dist/legacy/stories/WorkerMode.stories.d.ts +15 -0
- package/dist/legacy/stories/_helpers/localDataLoader.d.ts +7 -0
- package/dist/legacy/stories/_helpers/sampleData.d.ts +117 -0
- package/dist/legacy/types/FileParameter.d.ts +31 -0
- package/dist/legacy/types/StreamingParameter.d.ts +18 -0
- package/dist/legacy/types/excel.types.d.ts +48 -0
- package/dist/legacy/types/index.d.ts +10 -0
- package/dist/legacy/types/pivot-props.d.ts +140 -0
- package/dist/legacy/types/props.d.ts +176 -0
- package/dist/legacy/types/toolbar.d.ts +225 -0
- package/dist/legacy/utils/async/debounce.d.ts +4 -0
- package/dist/legacy/utils/crypto/crypto.d.ts +7 -0
- package/dist/legacy/utils/dialog/dialog.d.ts +19 -0
- package/dist/legacy/utils/excel/ExcelGuard.d.ts +73 -0
- package/dist/legacy/utils/excel/ExcelImportUtil.d.ts +34 -0
- package/dist/legacy/utils/excel/ExcelQueueManager.d.ts +57 -0
- package/dist/legacy/utils/excel/ExcelTaskRunner.d.ts +17 -0
- package/dist/legacy/utils/excel/ExcelTimeoutManager.d.ts +37 -0
- package/dist/legacy/utils/file.d.ts +34 -0
- package/dist/legacy/utils/http/rest.d.ts +5 -0
- package/dist/legacy/utils/id/generator.d.ts +2 -0
- package/dist/legacy/utils/index.d.ts +10 -0
- package/dist/store.cjs +1 -0
- package/dist/store.js +8 -0
- package/dist/stories/ColumnLockVisible.stories.d.ts +42 -0
- package/dist/stories/DateBooleanFilter.stories.d.ts +42 -0
- package/dist/stories/DisplayedRowApi.stories.d.ts +28 -0
- package/dist/stories/FieldId.stories.d.ts +113 -0
- package/dist/stories/GroupPanelWithoutToolbox.stories.d.ts +28 -0
- package/dist/stories/GroupRowRecycle.stories.d.ts +45 -0
- package/dist/stories/GroupTreeExpandNav.stories.d.ts +41 -0
- package/dist/stories/LargeData.stories.d.ts +20 -0
- package/dist/stories/MozGrid.stories.d.ts +51 -0
- package/dist/stories/PivotBarInitiallyOpen.stories.d.ts +69 -0
- package/dist/stories/PivotBarWithoutToolbox.stories.d.ts +39 -0
- package/dist/stories/SelectionApiPlayground.stories.d.ts +28 -0
- package/dist/stories/SingleSelectionModes.stories.d.ts +47 -0
- package/dist/stories/WideColumns.stories.d.ts +17 -0
- package/dist/style.css +1 -0
- package/dist/templates/CellEditor.d.ts +27 -0
- package/dist/templates/CellTemplate.d.ts +27 -0
- package/dist/templates/DetailTemplate.d.ts +60 -0
- package/dist/templates/GroupTemplate.d.ts +17 -0
- package/dist/templates/HeaderTemplate.d.ts +25 -0
- package/dist/types/injection-keys.d.ts +24 -0
- package/dist/types/slots.d.ts +49 -0
- package/dist/utils/excel/ExcelGuard.cjs +1 -0
- package/dist/utils/excel/ExcelGuard.js +74 -0
- package/dist/utils/excel/ExcelImportUtil.cjs +1 -0
- package/dist/utils/excel/ExcelImportUtil.js +61 -0
- package/dist/utils/excel/ExcelQueueManager.cjs +1 -0
- package/dist/utils/excel/ExcelQueueManager.js +10 -0
- package/dist/utils/excel/ExcelTaskRunner.cjs +1 -0
- package/dist/utils/excel/ExcelTaskRunner.js +12 -0
- package/dist/utils/excel/ExcelTimeoutManager.cjs +1 -0
- package/dist/utils/excel/ExcelTimeoutManager.js +420 -0
- package/docs/manual/examples.md +2385 -0
- package/docs/manual/reference.md +4695 -0
- package/docs/manual/reference_vue.md +709 -0
- package/package.json +129 -0
|
@@ -0,0 +1,4695 @@
|
|
|
1
|
+
# moz-ui-grid-core 레퍼런스 (Config · 타입 · API · 이벤트)
|
|
2
|
+
|
|
3
|
+
> 패키지: `@vmscloud/moz-ui-grid-core` (export 클래스명은 호환을 위해 `PureSheet` 유지)
|
|
4
|
+
> 버전: 0.1.41
|
|
5
|
+
> 마지막 업데이트: 2026-09-02
|
|
6
|
+
|
|
7
|
+
> 📋 **사용 예제 코드는 별도 파일**: [examples.md](examples.md) — Sub-API별 사용, Reactive React 통합, 이벤트, 선택 모드, Excel I/O 등 약 30개 시나리오
|
|
8
|
+
|
|
9
|
+
## 목차
|
|
10
|
+
|
|
11
|
+
- [개요](#개요)
|
|
12
|
+
- [비동기 API 규칙](#비동기-api-규칙)
|
|
13
|
+
- [Config (설정)](#config-설정)
|
|
14
|
+
- [PureSheetConfig](#puresheetconfig)
|
|
15
|
+
- [FlatModeConfig](#flatmodeconfig)
|
|
16
|
+
- [PivotModeConfig](#pivotmodeconfig)
|
|
17
|
+
- [FieldDef (필드 정의)](#fielddef-필드-정의)
|
|
18
|
+
- [PureSheet 최상위 API](#puresheet-최상위-api)
|
|
19
|
+
- [Sub-API](#sub-api)
|
|
20
|
+
- [grid.rows — 행 API](#gridrows--행-api)
|
|
21
|
+
- [grid.columns — 컬럼 API](#gridcolumns--컬럼-api)
|
|
22
|
+
- [grid.changes — 변경 추적 API](#gridchanges--변경-추적-api)
|
|
23
|
+
- [grid.undoManager — Undo/Redo API](#gridundomanager--undoredo-api)
|
|
24
|
+
- [grid.rowGroup — 그룹화 API](#gridrowgroup--그룹화-api)
|
|
25
|
+
- [grid.tree — 트리 API](#gridtree--트리-api)
|
|
26
|
+
- [grid.pivot — 피벗 API](#gridpivot--피벗-api)
|
|
27
|
+
- [grid.masterDetail — 마스터-디테일 API](#gridmasterdetail--마스터-디테일-api)
|
|
28
|
+
- [grid.validation — 검증 API](#gridvalidation--검증-api)
|
|
29
|
+
- [grid.clipboard — 클립보드 API](#gridclipboard--클립보드-api)
|
|
30
|
+
- [grid.headers — 컬럼 그룹 API](#gridheaders--컬럼-그룹-api)
|
|
31
|
+
- [grid.io — Excel Import/Export API](#gridio--excel-importexport-api)
|
|
32
|
+
- [grid.dragDrop — 드래그앤드롭 API](#griddragdrop--드래그앤드롭-api)
|
|
33
|
+
- [Reactive API (반응형 구독)](#reactive-api-반응형-구독)
|
|
34
|
+
- [grid.select()](#gridselect)
|
|
35
|
+
- [GridState 필드](#gridstate-필드)
|
|
36
|
+
- [grid.rows.observeById()](#gridrowsobservebyid)
|
|
37
|
+
- [ReactiveRow](#reactiverow)
|
|
38
|
+
- [ReactiveCell](#reactivecell)
|
|
39
|
+
- [이벤트](#이벤트)
|
|
40
|
+
- [타입 정의](#타입-정의)
|
|
41
|
+
- [ColumnGroupDef](#columngroupdef)
|
|
42
|
+
- [TreeConfig](#treeconfig)
|
|
43
|
+
- [MasterDetailConfig](#masterdetailconfig)
|
|
44
|
+
- [GroupConfig](#groupconfig)
|
|
45
|
+
- [GroupingConfig](#groupingconfig)
|
|
46
|
+
- [EditorConfig](#editorconfig)
|
|
47
|
+
- [MaskConfig](#maskconfig)
|
|
48
|
+
- [피벗 필드 타입](#피벗-필드-타입)
|
|
49
|
+
- [열거형/유니온 타입](#열거형유니온-타입)
|
|
50
|
+
- [공용/내부 타입 레퍼런스](#공용내부-타입-레퍼런스)
|
|
51
|
+
- [데이터 기본 타입 (CellValue / Row / ColumnDef / ColumnState / ColumnInfo / SortState / FilterState 등)](#데이터-기본-타입)
|
|
52
|
+
- [데이터 로더 — LoaderConfig](#데이터-로더--loaderconfig)
|
|
53
|
+
- [Drag-Drop — DragDropConfig](#drag-drop--dragdropconfig-adr-110)
|
|
54
|
+
- [셀 병합 — MergeConfig](#셀-병합--mergeconfig)
|
|
55
|
+
- [스트리밍 — StreamingOptions](#스트리밍--streamingoptions)
|
|
56
|
+
- [트랜잭션 — RowTransaction / TransactionOptions / TransactionResult](#트랜잭션--rowtransaction--transactionoptions--transactionresult)
|
|
57
|
+
- [Multi-Row 레이아웃 — RowTemplate](#multi-row-레이아웃--rowtemplate)
|
|
58
|
+
- [구조적 행 — RowConfig / AggregateConfig](#구조적-행--rowconfig)
|
|
59
|
+
- [검증 — ValidationRule / ValidationError](#검증--validationrule--validationerror)
|
|
60
|
+
- [셀 에디터 — EditorOption / CustomEditorParams](#셀-에디터--editoroption--customeditorparams)
|
|
61
|
+
- [셀 렌더러 (Lifecycle 포함)](#셀-렌더러-lifecycle-포함)
|
|
62
|
+
- [설정 스냅샷 — GridConfigExport / GridConfigSnapshot](#설정-스냅샷--gridconfigexport--gridconfigsnapshot)
|
|
63
|
+
- [저장 스키마 버전 — GridConfigMigrator](#저장-스키마-버전--gridconfigmigrator)
|
|
64
|
+
- [컬럼 식별자 유틸 — columnKeys](#컬럼-식별자-유틸--columnkeys)
|
|
65
|
+
- [수식 엔진 — FormulaEngine](#수식-엔진--formulaengine)
|
|
66
|
+
- [값 목록 위젯 — ValueCheckList](#값-목록-위젯--valuechecklist)
|
|
67
|
+
- [formatRow 컨텍스트 — FormatRowInfo](#formatrow-컨텍스트--formatrowinfo)
|
|
68
|
+
- [formatHeader 컨텍스트 — FormatHeaderContext](#formatheader-컨텍스트--formatheadercontext)
|
|
69
|
+
- [RowHeaderConfig](#rowheaderconfig)
|
|
70
|
+
- [셀 속성 — CellAttributes / CellAttributesFn](#셀-속성--cellattributes--cellattributesfn)
|
|
71
|
+
- [DataLoader 클래스](#dataloader-클래스--워커-기반-커스텀-데이터-로딩)
|
|
72
|
+
- [UndoStack 클래스](#undostack-클래스--공유-undoredo)
|
|
73
|
+
- [Row 클래스 정적 팩토리](#row-클래스-정적-팩토리)
|
|
74
|
+
- [에러 코드 전체 레퍼런스](#에러-코드-전체-레퍼런스)
|
|
75
|
+
- [기타 export 항목](#기타-export-항목)
|
|
76
|
+
- [API 네이밍 규칙](#api-네이밍-규칙)
|
|
77
|
+
- [사용 예제](#사용-예제) — 상세 코드는 [examples.md](examples.md)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 개요
|
|
82
|
+
|
|
83
|
+
moz-ui-grid-core는 고성능 데이터 그리드 라이브러리로, 대용량 데이터 처리, 피벗, 그룹화, CRUD 등을 지원합니다.
|
|
84
|
+
패키지명은 `@vmscloud/moz-ui-grid-core`이며, 메인 클래스는 호환성 유지를 위해 `PureSheet`라는 이름을 그대로 사용합니다.
|
|
85
|
+
|
|
86
|
+
### 주요 특징
|
|
87
|
+
|
|
88
|
+
- **고성능**: Web Worker 기반 대용량 데이터 처리 (500만 건+)
|
|
89
|
+
- **피벗/그룹화**: 강력한 데이터 집계 및 분석
|
|
90
|
+
- **CRUD**: Dirty State 및 Undo/Redo 지원
|
|
91
|
+
- **트리**: 계층형 데이터, lazy loading 지원
|
|
92
|
+
- **마스터-디테일**: 행 확장 패널
|
|
93
|
+
- **커스터마이징**: formatRow, cellRenderer, cellAttributes, 마스킹 등
|
|
94
|
+
- **반응형**: Reactive API (React useSyncExternalStore 호환)
|
|
95
|
+
- **타입 안전**: TypeScript strict 모드 완벽 지원
|
|
96
|
+
|
|
97
|
+
### 기본 사용법
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { PureSheet } from '@vmscloud/moz-ui-grid-core';
|
|
101
|
+
// 코어 패키지(@vmscloud/moz-ui-grid-core)는 CSS를 동봉하지 않습니다 — 그리드 스타일은 wrapper 패키지가 제공합니다.
|
|
102
|
+
import '@vmscloud/moz-ui-grid-wrapper/style.css';
|
|
103
|
+
|
|
104
|
+
const grid = new PureSheet(container, {
|
|
105
|
+
mode: 'flat',
|
|
106
|
+
fields: [
|
|
107
|
+
{ id: 'name', header: '이름', dataType: 'string', width: 200 },
|
|
108
|
+
{ id: 'age', header: '나이', dataType: 'number', width: 100 },
|
|
109
|
+
],
|
|
110
|
+
data: [
|
|
111
|
+
{ id: 1, name: '홍길동', age: 30 },
|
|
112
|
+
{ id: 2, name: '김철수', age: 25 },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 비동기 API 규칙
|
|
120
|
+
|
|
121
|
+
moz-ui-grid-core API는 **상태 변경 = async**, **순수 조회 = sync** 원칙을 따릅니다.
|
|
122
|
+
|
|
123
|
+
### 기준
|
|
124
|
+
|
|
125
|
+
| 분류 | 반환 타입 | 설명 |
|
|
126
|
+
|------|-----------|------|
|
|
127
|
+
| 상태 변경 | `Promise<void>` | 데이터 로드, 정렬, 필터, 그룹 접기/펼치기, 디테일 토글 등 |
|
|
128
|
+
| 순수 조회 | 즉시 반환 | `getConfig()`, `isCollapsed()`, `getMode()` 등 |
|
|
129
|
+
|
|
130
|
+
### 사용 패턴
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// ✅ 올바른 사용: await로 완료 대기
|
|
134
|
+
await grid.rowGroup.collapseAll();
|
|
135
|
+
await grid.tree.expandToLevel(2);
|
|
136
|
+
await grid.masterDetail.expandAll();
|
|
137
|
+
|
|
138
|
+
// ✅ 순수 조회는 await 불필요
|
|
139
|
+
const config = grid.rowGroup.getConfig();
|
|
140
|
+
const collapsed = grid.tree.isCollapsed('node-1');
|
|
141
|
+
|
|
142
|
+
// ⚠️ fire-and-forget (완료를 기다릴 필요 없는 경우)
|
|
143
|
+
// 이벤트 핸들러 내에서는 await 없이 호출 가능하나,
|
|
144
|
+
// 후속 작업이 결과에 의존하면 반드시 await 사용
|
|
145
|
+
grid.on('row:click', ({ rowIndex }) => {
|
|
146
|
+
grid.masterDetail.toggle(rowIndex); // fire-and-forget OK
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Sub-API별 비동기 현황
|
|
151
|
+
|
|
152
|
+
| Sub-API | 상태 변경 | 순수 조회 |
|
|
153
|
+
|---------|-----------|-----------|
|
|
154
|
+
| `grid.rows` | `deleteSelectedRows()`, `updateSelectedRows()`, `discardSelectedRows()` 등 async | `getById()`, `all()`, `getSelectedRowIds()` 등 sync |
|
|
155
|
+
| `grid.columns` | 전체 sync (DOM 즉시 반영) | 전체 sync |
|
|
156
|
+
| `grid.changes` | 전체 async (`addRow`, `updateCellById`, `commit`, `deleteSelectedRows` 등) | `hasChanges`, `getAll()`, `getRowStateById()` sync |
|
|
157
|
+
| `grid.rowGroup` | `setConfig`, `toggle`, `expand`, `collapse` 등 async | `getConfig()`, `isCollapsed()` sync |
|
|
158
|
+
| `grid.tree` | `setConfig`, `toggle`, `expand`, `collapse` 등 async | `getConfig()`, `isCollapsed()` sync |
|
|
159
|
+
| `grid.pivot` | 대부분 async (프로세서 호출) | `getMode()`, `getConfig()`, `getResult()`, `getState()`, `isRowCollapsed()`, `isColumnCollapsed()`, `getCollapsedRowNodes()`, `getCollapsedColumnNodes()`, `getPivotMeta()` sync. `getFieldValues()` / `getDataMatrix()` / `getSeriesData()` / `getAllRows()`는 async |
|
|
160
|
+
| `grid.masterDetail` | `setConfig`, `toggle`, `expand`, `collapse` 등 async | `getConfig()`, `isExpanded()` sync |
|
|
161
|
+
| `grid.headers` | 전체 sync (DOM 즉시 반영) | 전체 sync |
|
|
162
|
+
| `grid.validation` | `clear()` async | `validateAll()`, `hasErrors()`, `getCellErrors()`, `setCellErrors()`, `nextInvalid()`, `prevInvalid()` 등 sync |
|
|
163
|
+
| `grid.clipboard` | 전체 async | — |
|
|
164
|
+
| `grid.io` | 전체 async (`toExcel`, `saveExcelToFile`, `fromExcel`, `previewExcel`, `getSheetInfo`) | — |
|
|
165
|
+
| `grid.dragDrop` | `setConfig()` async | `getConfig()`, `isActive()` sync |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Config (설정)
|
|
170
|
+
|
|
171
|
+
`PureSheetConfig = FlatModeConfig | PivotModeConfig`
|
|
172
|
+
|
|
173
|
+
### PureSheetConfig
|
|
174
|
+
|
|
175
|
+
#### 공통 속성 (PureSheetConfigBase)
|
|
176
|
+
|
|
177
|
+
| 속성 | 타입 | 기본값 | 설명 |
|
|
178
|
+
|------|------|--------|------|
|
|
179
|
+
| `data` | `Row[]` | — | 데이터 배열 |
|
|
180
|
+
| `loader` | `LoaderConfig` | — | Worker 기반 데이터 로더 설정 |
|
|
181
|
+
| `fields` | `FieldDef[]` | 필수 | 필드(컬럼) 정의 |
|
|
182
|
+
| `keyFields` | `string[]` | `['id']` | 행 고유 ID 식별 필드 목록. 복합 키도 가능(예: `['exchange', 'symbol']`) |
|
|
183
|
+
| `enableReferCache` | `boolean` | `false` | 행 객체 참조 캐시 활성화 — 행 객체 기반 API(`updateCell(row, ...)`, `deleteRow(row)` 등) 사용 시 필수 |
|
|
184
|
+
| **엔진** | | | |
|
|
185
|
+
| `engine` | `'native'` | `'native'` | 데이터 처리 엔진. **`'aq'`(Arquero)는 제거됨** — 단일 엔진(native)만 남았습니다(ITSM-2026-002190) |
|
|
186
|
+
| `useWorker` | `boolean` | `false` | Web Worker 사용 여부 |
|
|
187
|
+
| **저장본 호환** | | | |
|
|
188
|
+
| `migrateConfig` | `GridConfigMigrator` | — | `applyConfig()` 가 옛 버전 저장본을 받았을 때 호출할 변환기. core 는 **호출 시점·순서만** 책임지고 변환 내용은 소비처(wrapper)가 소유 (→ [저장 스키마 버전](#저장-스키마-버전--gridconfigmigrator)) |
|
|
189
|
+
| **UI** | | | |
|
|
190
|
+
| `theme` | `'light' \| 'dark' \| 'auto' \| 'excel' \| 'sheets' \| 'material' \| (string & {})` | `'light'` | 테마. 빌트인 프리셋 외 임의 문자열도 허용(사용자가 `.ps-theme-{name}` 클래스 정의 시 자동 적용) |
|
|
191
|
+
| `rowHeight` | `number` | — | 행 높이 (px) |
|
|
192
|
+
| `dynamicRowHeights` | `boolean` | `false` | 동적 행 높이 활성화 |
|
|
193
|
+
| `rowHeightChangeThreshold` | `number` | `2` | 높이 변경 감지 임계값 (px) |
|
|
194
|
+
| `headerHeight` | `number` | — | 헤더 높이 (px) |
|
|
195
|
+
| `showHeader` | `boolean` | `true` | 헤더 표시 여부 |
|
|
196
|
+
| `rowHeader` | `boolean \| RowHeaderConfig` | `false` | 좌측 행 헤더 컬럼(`_rowHeader`) 표시. `true`면 빈 셀(너비 40, 체크박스 트리거 시 체크박스), 객체면 `width`/`cellRenderer`/`headerRenderer`로 커스터마이즈 (→ [RowHeaderConfig](#rowheaderconfig)). `rowSelection.trigger`가 `'rowHeader'`/`'checkbox'`면 미지정이어도 자동 ON. (구 `showRowHeader` — deprecated alias 유지) |
|
|
197
|
+
| `showStatusBar` | `boolean` | `true` | 상태 바 표시 |
|
|
198
|
+
| `alternatingRows` | `boolean` | `false` | 줄무늬 행 스타일 |
|
|
199
|
+
| `rowStyle` | `string` | — | 행 CSS 스타일 |
|
|
200
|
+
| `headerStyle` | `string` | — | 헤더 CSS 스타일 |
|
|
201
|
+
| **컬럼 동작** | | | |
|
|
202
|
+
| `resizableColumns` | `boolean` | `true` | 컬럼 리사이즈 허용 |
|
|
203
|
+
| `reorderableColumns` | `boolean` | `true` | 컬럼 재정렬 허용 |
|
|
204
|
+
| `autoFitColumns` | `boolean` | `false` | 로드 시 전체 컬럼 자동 맞춤 |
|
|
205
|
+
| **선택** | | | |
|
|
206
|
+
| `rowSelection` | `RowSelectionConfig` | — | 행 선택 설정 (identity 추종, rowId 기반). 서브필드: `mode`, `trigger`, `includeInSelectAll`. 자세한 내용은 "선택 모드 사용 예제" 참조 |
|
|
207
|
+
| `cellSelection` | `CellSelectionConfig` | — | 셀 선택 설정 (자리 추종, viewIndex 기반). 서브필드: `mode`. 둘 다 미지정 시 기본 `{mode:'range'}` (셀 전용 Excel 스타일) |
|
|
208
|
+
| `cellSelectionImpliesRows` | `boolean` | `false` | 셀만 선택된 상태에서 행 액션(Ctrl+H 등) 작동 여부 — Policy B fallback. Ctrl+A 동작은 `rowSelection.includeInSelectAll`이 별도 제어 |
|
|
209
|
+
| **서버/수동 row-model** | | | |
|
|
210
|
+
| `rowModel` | `'client' \| 'server'` | `'client'` | `'server'`이면 `sort()`/`filter()`는 정렬·필터 상태와 이벤트만 갱신하고 supplied row order/membership을 로컬에서 변경하지 않습니다. 데이터 재조회/append는 호출자가 서버 정본으로 수행합니다. |
|
|
211
|
+
| `endReachedThresholdRows` | `number` | `0` | `scroll:endReached` 선발화 행 임계값. 현재 가시 범위 끝에서 남은 행 수가 이 값 이하이면 끝 도달로 간주합니다. |
|
|
212
|
+
| **편집** | | | |
|
|
213
|
+
| `editable` | `boolean` | `false` | 전역 편집 가능 여부 |
|
|
214
|
+
| `preventKeyEdit` | `boolean` | `true` | 키 컬럼 편집 방지 — `keyFields` 컬럼은 신규 행(added)에서만 편집 가능, 기존 행(pristine/modified)에서는 자동 readonly |
|
|
215
|
+
| **드래그앤드롭** | | | |
|
|
216
|
+
| `dragDropConfig` | `DragDropConfig` | — | 행 드래그앤드롭 설정 (같은 그리드 reorder, 크로스-그리드 드롭, `rowOrderField`, `mutateOnDirty` 등). 런타임 변경은 `grid.dragDrop.setConfig()` |
|
|
217
|
+
| **툴팁** | | | |
|
|
218
|
+
| `tooltipMode` | `'none' \| 'overflow' \| 'always'` | `'none'` | 툴팁 표시 모드 |
|
|
219
|
+
| **Undo** | | | |
|
|
220
|
+
| `undoStack` | `UndoStack` | — | 외부 UndoStack 주입 |
|
|
221
|
+
| **국제화** | | | |
|
|
222
|
+
| `locale` | `string` | 자동 감지 | 로케일 (예: `'ko-KR'`) |
|
|
223
|
+
| `translations` | `PartialTranslationMap` | — | 번역 오버라이드 |
|
|
224
|
+
| **포맷팅** | | | |
|
|
225
|
+
| `formatRow` | `FormatRowCallback` | — | 행 포맷팅 콜백 (config에서 설정 시 `_config` 키로 자동 등록) |
|
|
226
|
+
| `formatHeader` | `FormatHeaderCallback` | — | 헤더 행 포맷팅 콜백 (config에서 설정 시 `_config` 키로 자동 등록). 헤더 (재)렌더 시 발화. 스크롤로는 발화 X |
|
|
227
|
+
| **이벤트 핸들러** | | | |
|
|
228
|
+
| ~~`onRowClick`~~ | — | — | **제거됨.** `grid.on('row:click', ...)` 이벤트 구독으로 대체 (→ [이벤트](#이벤트)) |
|
|
229
|
+
| ~~`onCellClick`~~ | — | — | **제거됨.** `grid.on('cell:click', ...)` 이벤트 구독으로 대체 (→ [이벤트](#이벤트)) |
|
|
230
|
+
| ~~`onGroupToggle`~~ | — | — | **제거됨.** 로우 그룹 토글은 이벤트로 노출됩니다 — `rowGroup:toggle`(단일), `rowGroup:batchToggle`(일괄: `scope`로 `level`/`toLevel`/`all` 구분). `grid.on('rowGroup:toggle', ...)` 등으로 구독 (→ [이벤트](#이벤트)) |
|
|
231
|
+
|
|
232
|
+
### FlatModeConfig
|
|
233
|
+
|
|
234
|
+
`PureSheetConfigBase`에 추가:
|
|
235
|
+
|
|
236
|
+
| 속성 | 타입 | 기본값 | 설명 |
|
|
237
|
+
|------|------|--------|------|
|
|
238
|
+
| `mode` | `'flat'` | `'flat'` | 그리드 모드 |
|
|
239
|
+
| `rowGroup` | `GroupConfig` | — | 초기 그룹화 설정 (`{ columns, subtotals }`). 런타임에는 `grid.rowGroup.setConfig(GroupingConfig)`로 변경하며 시그니처가 다름 (→ [GroupConfig](#groupconfig) vs [GroupingConfig](#groupingconfig)) |
|
|
240
|
+
| `group` | `GroupConfig` | — | **deprecated 하위호환 별칭** — `rowGroup`과 동일. 둘 다 주면 `rowGroup`이 우선. 신규 코드는 `rowGroup` 사용 |
|
|
241
|
+
| `rowTemplate` | `RowTemplate` | — | Multi-Row 레이아웃 템플릿 |
|
|
242
|
+
| `columnGroups` | `ColumnGroupDef[]` | — | 컬럼 그룹(다단계 컬럼 헤더) 정의 (→ [ColumnGroupDef](#columngroupdef)). (구 `headerGroups`) |
|
|
243
|
+
| ~~`onColumnGroupToggle`~~ | — | — | **제거됨.** 컬럼 그룹 토글은 `columnGroup:toggle` 이벤트로 노출됩니다. `grid.on('columnGroup:toggle', ...)`로 구독 (→ [이벤트](#이벤트)) |
|
|
244
|
+
| `treeConfig` | `TreeConfig` | — | 트리 모드 초기 설정 — `parentField`/`pathField`로 flat 데이터를 트리로 표시. 런타임 변경은 `grid.tree.setConfig()` (→ [TreeConfig](#treeconfig)) |
|
|
245
|
+
| `masterDetailConfig` | `MasterDetailConfig` | — | 마스터-디테일 초기 설정 — 데이터 행을 확장하면 아래에 디테일 패널 표시. 런타임 변경은 `grid.masterDetail.setConfig()` (→ [MasterDetailConfig](#masterdetailconfig)) |
|
|
246
|
+
|
|
247
|
+
### PivotModeConfig
|
|
248
|
+
|
|
249
|
+
`PureSheetConfigBase`에서 `fields` 대신 피벗 필드 사용:
|
|
250
|
+
|
|
251
|
+
| 속성 | 타입 | 설명 |
|
|
252
|
+
|------|------|------|
|
|
253
|
+
| `mode` | `'pivot'` | 피벗 모드 (필수) |
|
|
254
|
+
| `rowFields` | `PivotRowField[]` | 행 축 필드 |
|
|
255
|
+
| `columnFields` | `PivotColumnField[]` | 열 축 필드 (피벗) |
|
|
256
|
+
| `valueFields` | `PivotValueField[]` | 값 필드 (집계) |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## FieldDef (필드 정의)
|
|
261
|
+
|
|
262
|
+
각 컬럼의 동작, 외형, 편집 설정을 정의합니다.
|
|
263
|
+
|
|
264
|
+
### 기본 속성
|
|
265
|
+
|
|
266
|
+
| 속성 | 타입 | 필수 | 설명 |
|
|
267
|
+
|------|------|------|------|
|
|
268
|
+
| `id` | `string` | O | 컬럼 **식별자** — 이 컬럼을 *가리킬* 때 쓴다. 정렬/필터 상태(`SortState.id`/`FilterState.id`), 컬럼 폭 상태, DOM dataset, 저장 레이아웃이 전부 이 값을 참조 |
|
|
269
|
+
| `field` | `string` | — | 원본 데이터 객체의 **키** — 값을 *읽을* 때만 쓴다(`row[field]`). 생략 시 `id` 와 같은 값으로 정규화 |
|
|
270
|
+
| `header` | `string` | O | 헤더 표시명 |
|
|
271
|
+
| `dataType` | `DataType` | O | 데이터 타입 (`'string' \| 'number' \| 'boolean' \| 'date' \| 'array'`) |
|
|
272
|
+
|
|
273
|
+
> **`id` ≠ `field` — 이중 역할 해소 (ITSM-2026-002188)**
|
|
274
|
+
> 예전에는 `id` 하나가 "식별자"와 "데이터 키"를 겸했습니다("필드 ID = 데이터 객체의 키"). 이제 두 축이 분리되어 **가리킬 때는 `id`, 읽을 때는 `field`** 입니다.
|
|
275
|
+
>
|
|
276
|
+
> `field` 를 생략하면 경계에서 1회 `field = id` 로 정규화되므로 **기존 소비처는 아무것도 바꾸지 않아도 동작이 동일**합니다.
|
|
277
|
+
>
|
|
278
|
+
> ```typescript
|
|
279
|
+
> // 같은 원본 컬럼을 두 번 배치 — id 로 구분
|
|
280
|
+
> { id: 'dept_raw', field: 'dept', header: '부서(원본)', dataType: 'string' }
|
|
281
|
+
> { id: 'dept_short', field: 'dept', header: '부서(약칭)', dataType: 'string' }
|
|
282
|
+
> ```
|
|
283
|
+
>
|
|
284
|
+
> ⚠️ 계산 필드(`formula`)는 원본 데이터에 존재하지 않으므로 `field` 가 의미 없습니다 — `id` 만 지정하고 값 조회는 `FormulaEngine` 이 담당합니다.
|
|
285
|
+
>
|
|
286
|
+
> 정규화 헬퍼는 core 가 export 합니다: `normalizeColumnKeys` / `getColumnId` / `getColumnField` / `resolveFieldKey` (→ [컬럼 식별자 유틸](#컬럼-식별자-유틸--columnkeys)).
|
|
287
|
+
|
|
288
|
+
### 너비/크기
|
|
289
|
+
|
|
290
|
+
| 속성 | 타입 | 설명 |
|
|
291
|
+
|------|------|------|
|
|
292
|
+
| `width` | `number` | 너비 (px) |
|
|
293
|
+
| `minWidth` | `number` | 최소 너비 (px) |
|
|
294
|
+
| `maxWidth` | `number` | 최대 너비 (px) |
|
|
295
|
+
| `flex` | `number` | Flex 비율 (남은 공간 배분) |
|
|
296
|
+
| `autoFit` | `boolean` | 콘텐츠에 맞게 자동 너비 조절 |
|
|
297
|
+
| `style` | `string` | CSS 스타일 문자열 |
|
|
298
|
+
|
|
299
|
+
### 정렬/표시
|
|
300
|
+
|
|
301
|
+
| 속성 | 타입 | 설명 |
|
|
302
|
+
|------|------|------|
|
|
303
|
+
| `align` | `'left' \| 'center' \| 'right'` | 셀 정렬 (dataType에서 자동 추론) |
|
|
304
|
+
| `hidden` | `boolean` | 숨김 필드 |
|
|
305
|
+
| `lockVisible` | `boolean` | 가시성 잠금 (기본 `false`). 아래 참조 |
|
|
306
|
+
| `pinned` | `'left' \| 'right'` | 컬럼 고정 위치 |
|
|
307
|
+
|
|
308
|
+
> **`lockVisible` — 가시성 하드 잠금**
|
|
309
|
+
> `true` 면 **현재 가시성 상태가 고정**되어 표시/숨김을 바꿀 수 없습니다. 컬럼 옵션 패널의 체크박스가 비활성화되고(체크 상태는 실제 가시성 그대로), 전체표시 토글의 대상에서도 제외됩니다. UI 뿐 아니라 **프로그램적 API**(`columns.hide` / `show` / `setHidden` / `toggle` / `update({ hidden })`)도 거부하고 `console.warn` 을 남깁니다.
|
|
310
|
+
>
|
|
311
|
+
> 초기 상태는 `hidden` 과 조합해 정합니다 — `hidden:false`+잠금 = **항상 표시**, `hidden:true`+잠금 = **항상 숨김**.
|
|
312
|
+
>
|
|
313
|
+
> 요청값이 현재값과 같으면 조용한 no-op 이므로 `reset()` / `resetAll()` / `load()` / `updateFields()` 의 상태 복원 경로는 영향받지 않습니다. 저장된 레이아웃(`load`)이나 새 `fields`(`updateFields`)가 잠긴 컬럼을 반대 상태로 담고 있어도 선언된 고정 가시성으로 되돌립니다.
|
|
314
|
+
>
|
|
315
|
+
> 잠기는 것은 **가시성뿐**입니다 — 순서·너비·고정·서식은 그대로 변경 가능합니다.
|
|
316
|
+
>
|
|
317
|
+
> (AG Grid 의 동명 옵션 `lockVisible` 은 UI 만 막고 API 는 통과시키지만, moz-ui-grid 에서는 소비처가 기대하는 불변식이 "그 컬럼은 반드시 보인다/숨는다" 이므로 **하드 불변식**으로 정의했습니다.)
|
|
318
|
+
|
|
319
|
+
### 정렬/필터
|
|
320
|
+
|
|
321
|
+
| 속성 | 타입 | 기본값 | 설명 |
|
|
322
|
+
|------|------|--------|------|
|
|
323
|
+
| `sortable` | `boolean` | `true` | 정렬 허용. `PureSheet.toggleSort` 에 게이트가 있어 실제로 동작 |
|
|
324
|
+
| `showFilterIcon` | `boolean` | `true` | 헤더에 **필터 아이콘을 표시할지** (ITSM-2026-002194) |
|
|
325
|
+
| `filterable` | `boolean` | `true` | 필터 허용 — ⚠️ **현재 동작에 반영되지 않음**(아래 참조) |
|
|
326
|
+
|
|
327
|
+
> **`showFilterIcon` — 표시 축 (ITSM-2026-002194)**
|
|
328
|
+
> `false` 면 core 가 아이콘 span **자체를 만들지 않습니다**. CSS 로 숨기는 것과 달리 클릭 대상이 존재하지 않으므로 `header:filterClick` 도 발생하지 않습니다(소비 지점마다 걸러야 하는 무증상 실패를 만들지 않기 위한 선택 — `resizable` 이 리사이즈 핸들 DOM 생성을 가르는 것과 같은 층위).
|
|
329
|
+
>
|
|
330
|
+
> ⚠️ 이것은 **표시 축**이며 필터 **능력**을 막지 않습니다. 툴바 필터 창·수식 등 다른 경로로는 여전히 조건을 걸고 **해제**할 수 있습니다 — 능력까지 막으면 이미 걸어 둔 조건을 되돌릴 수 없습니다. (AG Grid 도 `suppressHeaderFilterButton`(표시)과 `filter: false`(능력)를 분리합니다.)
|
|
331
|
+
>
|
|
332
|
+
> 정렬 쪽에는 대응 옵션이 **없습니다.** 정렬 진입점은 아이콘과 **헤더 셀 클릭** 둘이라 아이콘만 숨기면 "정렬은 되는데 상태 표시가 없는" 상태가 됩니다. 정렬을 막으려면 `sortable` 을 쓰세요.
|
|
333
|
+
|
|
334
|
+
> **⚠️ `filterable` 은 현재 미동작입니다 (ITSM-2026-002167, 실측)**
|
|
335
|
+
> 값은 `configAdapter` 를 거쳐 `ColumnDef.filterable` 로 전달되지만 **이를 읽어 분기하는 코드가 없습니다** — 툴바 필터 창·컨텍스트 메뉴의 컬럼 후보에서 제외되지 않습니다. (`sortable` 은 게이트가 있어 동작하는데 그 짝이 없는 상태입니다.)
|
|
336
|
+
>
|
|
337
|
+
> 헤더 필터 아이콘만 감추려면 **`showFilterIcon: false`** 를 쓰세요. `filterable` 은 "필터 능력" 축을 위해 예약돼 있으며, 실제 게이트가 생기기 전까지 표시 축과 혼동하지 마세요.
|
|
338
|
+
|
|
339
|
+
### 편집
|
|
340
|
+
|
|
341
|
+
| 속성 | 타입 | 설명 |
|
|
342
|
+
|------|------|------|
|
|
343
|
+
| `editable` | `boolean` | 편집 가능 여부 |
|
|
344
|
+
| `readonly` | `boolean` | 읽기 전용 (전역 editable 오버라이드) |
|
|
345
|
+
| `editor` | `EditorConfig` | 에디터 설정 |
|
|
346
|
+
| `editableWhen` | `(params) => boolean` | 동적 편집 가능 조건 |
|
|
347
|
+
| `editorResolver` | `(params) => EditorConfig \| null` | 동적 에디터 결정 |
|
|
348
|
+
| `validation` | `ValidationRule[]` | 검증 규칙 |
|
|
349
|
+
|
|
350
|
+
### 포맷팅
|
|
351
|
+
|
|
352
|
+
| 속성 | 타입 | 설명 |
|
|
353
|
+
|------|------|------|
|
|
354
|
+
| `mask` | `MaskConfig` | 마스킹 설정 |
|
|
355
|
+
| `aggregate` | `AggregateFunc` | 그룹/피벗 집계 함수 |
|
|
356
|
+
|
|
357
|
+
### 그룹 행
|
|
358
|
+
|
|
359
|
+
| 속성 | 타입 | 기본값 | 설명 |
|
|
360
|
+
|------|------|--------|------|
|
|
361
|
+
| `unmergeOnGroupRow` | `boolean` | `false` | 그룹 행의 배너 머지에서 이 컬럼을 제외하고 **자기 셀을 유지**해 집계값(`aggregate`)을 그룹 행에 표시한다. 그룹 행의 center 영역은 라벨/집계를 보여주는 하나의 긴 배너(머지 셀)인데, 이 옵션이 켜진 컬럼은 배너에 흡수되지 않는다 |
|
|
362
|
+
|
|
363
|
+
> **`unmergeOnGroupRow` 활성 조건** — 배너 머지는 **연속된 범위**여야 하므로 이 옵션은 필드가 center 영역의 연속된 **맨 왼쪽(prefix)** 또는 **맨 오른쪽(suffix)** 에 위치할 때만 활성화됩니다(중간에 위치하면 무시되고 배너에 흡수). 강제 컬럼 이동은 하지 않으므로 헤더·데이터·그룹 행 셀이 자동 정렬됩니다. `pinned` 컬럼은 이미 자기 고정 셀에 집계를 표시하므로 이 옵션과 무관합니다. 그룹 행에서 합계 등 집계값을 컬럼별로 보이게 하려는 용도입니다.
|
|
364
|
+
|
|
365
|
+
### 계산 필드
|
|
366
|
+
|
|
367
|
+
| 속성 | 타입 | 설명 |
|
|
368
|
+
|------|------|------|
|
|
369
|
+
| `formula` | `string \| ((row) => unknown)` | 다른 컬럼 기반의 계산 필드. 문자열은 단순 표현식, 함수는 임의 계산. 결과는 표시·정렬·필터·집계에 사용됨 |
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
{
|
|
373
|
+
id: 'total',
|
|
374
|
+
header: '합계',
|
|
375
|
+
dataType: 'number',
|
|
376
|
+
// 함수 수식
|
|
377
|
+
formula: (row) => (row.price as number) * (row.quantity as number),
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 렌더링
|
|
382
|
+
|
|
383
|
+
| 속성 | 타입 | 설명 |
|
|
384
|
+
|------|------|------|
|
|
385
|
+
| `cellRenderer` | `CellRenderer` (Simple \| Lifecycle) | 커스텀 셀 렌더러 — `(params) => HTMLElement\|string`(Simple, arity 1) 또는 `(container, params) => CellRendererInstance`(Lifecycle, arity 2). 상세는 [CellRendererParams / CellRenderer](#) 타입 정의 참조 |
|
|
386
|
+
| `cellAttributes` | `CellAttributes \| CellAttributesFn` | 셀 HTML 속성 (정적/동적) |
|
|
387
|
+
| `headerRenderer` | `(params) => HTMLElement \| string` | 커스텀 헤더 렌더러 |
|
|
388
|
+
| `headerAttributes` | `CellAttributes` | 헤더 셀 HTML 속성 |
|
|
389
|
+
| `headerTooltip` | `string` | 헤더 커스텀 툴팁 문구(고정). 설정 시 헤더 hover 툴팁이 라벨 대신 이 문구 표시(overflow 무관 항상). `tooltipMode !== 'none'` 필요 |
|
|
390
|
+
| `cellTooltip` | `(params: { value, row, columnId }) => string` | 셀 커스텀 툴팁 문구(행별 동적). **hover 시점에만** 호출(렌더/스크롤 시 전 셀 호출 아님 → 성능 부담 없음). 반환값을 라벨 대신 표시(overflow 무관 항상). `tooltipMode !== 'none'` 필요 |
|
|
391
|
+
|
|
392
|
+
### cellRenderer 예시
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
{
|
|
396
|
+
id: 'status',
|
|
397
|
+
header: '상태',
|
|
398
|
+
dataType: 'string',
|
|
399
|
+
cellRenderer: ({ value, rowData }) => {
|
|
400
|
+
const span = document.createElement('span');
|
|
401
|
+
span.className = `badge badge-${value}`;
|
|
402
|
+
span.textContent = String(value);
|
|
403
|
+
return span;
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### cellAttributes 예시
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
{
|
|
412
|
+
id: 'amount',
|
|
413
|
+
header: '금액',
|
|
414
|
+
dataType: 'number',
|
|
415
|
+
cellAttributes: ({ value }) => ({
|
|
416
|
+
style: Number(value) < 0 ? 'color: red' : 'color: blue',
|
|
417
|
+
class: 'amount-cell',
|
|
418
|
+
}),
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## PureSheet 최상위 API
|
|
425
|
+
|
|
426
|
+
PureSheet 인스턴스에서 직접 호출하는 메서드들입니다. Sub-API 네임스페이스(`grid.rows`, `grid.columns` 등)는 별도 섹션에서 설명합니다.
|
|
427
|
+
|
|
428
|
+
### 라이프사이클
|
|
429
|
+
|
|
430
|
+
| 메서드 | 반환 | 설명 |
|
|
431
|
+
|--------|------|------|
|
|
432
|
+
| `waitForReady()` | `Promise<void>` | 초기 렌더링 완료까지 대기 |
|
|
433
|
+
| `refresh()` | `Promise<void>` | 데이터 재처리 + 렌더링 갱신 |
|
|
434
|
+
| `notifyResize()` | `void` | 컨테이너 가시성/크기 변경을 호스트가 명시적으로 알리는 훅. viewport 재측정 + 스크롤 재동기화 + 가상 가시범위 재계산 → 재렌더. 데이터/컬럼 변경 없이 레이아웃만 다시 맞추므로 `refresh()`보다 가벼움 (ResizeObserver로 감지 못 한 컨테이너 표시/크기 변경 시나리오용) |
|
|
435
|
+
| `destroy()` | `void` | 리소스 해제, DOM 정리 |
|
|
436
|
+
|
|
437
|
+
### 데이터
|
|
438
|
+
|
|
439
|
+
| 메서드 | 반환 | 설명 |
|
|
440
|
+
|--------|------|------|
|
|
441
|
+
| `loadData(data)` | `Promise<void>` | 데이터 로드 (기존 데이터 교체) |
|
|
442
|
+
| `getAllData()` | `Promise<RowData[]>` | 전체 데이터 조회 |
|
|
443
|
+
| `getVisibleData()` | `Promise<RowData[]>` | 필터/정렬 적용된 processor 데이터 조회. 하위 호환 API라 rowGroup/tree collapse로 renderer에서 숨긴 자식 행은 제외하지 않음 |
|
|
444
|
+
| `getDisplayedData()` | `Promise<RowData[]>` | 현재 renderer view-index 순서의 순수 도메인 데이터 행. rowGroup/tree collapse 반영(접힌 자식 제외), group header/subtotal/grand-total/master-detail 구조 행 제외, DOM/viewport 가상화와 무관 |
|
|
445
|
+
| `getDisplayedRowIds()` | `Promise<RowId[]>` | `getDisplayedData()`와 같은 순서의 도메인 rowId 목록 |
|
|
446
|
+
| `rows.displayedCount()` | `number` | displayed 좌표계 총 행 수(그룹/트리 collapse 반영). traversal 경계 계산용 |
|
|
447
|
+
| `rows.getByDisplayedIndex(i)` | `Promise<DisplayedRowInfo \\| null>` | displayed 좌표 i의 단일 행 정보(O(1)~O(log g), 배열 materialize 없음). 다음/이전 이동 등 traversal용. 범위 밖 `null` |
|
|
448
|
+
| `rows.getDisplayedIndexById(id)` | `Promise<number>` | rowId → displayed 인덱스. 접힘/필터로 숨겨졌으면 `-1`. rowId만으로 호출 시 idMap O(N) 유발 가능하므로 대용량 traversal은 index 보관 권장 |
|
|
449
|
+
| `getRowById(rowId)` | `Promise<RowData \| undefined>` | ID로 단일 행 조회 |
|
|
450
|
+
| `getRowIndexById(rowId)` | `Promise<number>` | ID → 데이터 인덱스 |
|
|
451
|
+
| `getRowVirtualIndexById(rowId)` | `Promise<number>` | ID → 가상 인덱스 (그룹 행 포함) |
|
|
452
|
+
|
|
453
|
+
> sourceIndex 기반 단일 행 조회는 `grid.rows.getByIndex(idx).data` 또는 `grid.changes.getRowStateByIndex(idx)` 경로를 사용하세요. 최상위 `getRowByIndex` 메서드는 더 이상 존재하지 않습니다.
|
|
454
|
+
|
|
455
|
+
### CRUD
|
|
456
|
+
|
|
457
|
+
최상위에는 별도의 CRUD 메서드가 없습니다. 모든 추가/수정/삭제는 `grid.changes` 네임스페이스로 통합되어 있습니다. 자세한 내용은 [grid.changes — 변경 추적 API](#gridchanges--변경-추적-api) 참조.
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// ✅ 행 추가/수정/삭제는 grid.changes로
|
|
461
|
+
await grid.changes.addRow({ name: '신규' });
|
|
462
|
+
await grid.changes.updateCellById('row-1', 'name', '수정됨');
|
|
463
|
+
await grid.changes.deleteRowById('row-1');
|
|
464
|
+
await grid.changes.commit();
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### 정렬 / 필터
|
|
468
|
+
|
|
469
|
+
| 메서드 | 반환 | 설명 |
|
|
470
|
+
|--------|------|------|
|
|
471
|
+
| `sort(sorts)` | `Promise<void>` | 정렬 적용 |
|
|
472
|
+
| `filter(filters)` | `Promise<void>` | 필터 적용. `FilterState[]`(평면, 하위 호환) 또는 **`FilterGroupMap`**(소스별 그룹) 둘 다 받음 |
|
|
473
|
+
| `setFilterGroup(key, group)` | `Promise<void>` | 그룹 하나만 교체/삭제(`null`=삭제). 다른 그룹은 건드리지 않음 |
|
|
474
|
+
| `getFilterGroup(key)` | `FilterGroup \| undefined` | 그룹 하나 조회 (sync) |
|
|
475
|
+
| `getFilterGroups()` | `FilterGroupMap` | 전체 그룹 맵 조회 (sync) |
|
|
476
|
+
| `getViewState()` | `{ sorts, filters, filterGroups }` | 현재 뷰 상태 조회 (sync). `filters` 는 그룹을 평탄화한 **파생값** |
|
|
477
|
+
| `setViewState(state)` | `Promise<void>` | 정렬+필터 일괄 적용 |
|
|
478
|
+
| `toggleSort(columnId, opts?)` | `void` | 헤더 클릭형 정렬 토글. 대상 컬럼을 none → asc → desc → none 사이클로 토글하고 `sort()`를 적용. `opts.additive: true`면 기존 정렬을 유지하며 해당 컬럼만 토글/추가(다중 정렬, shift+클릭 용도), `false`(기본)면 단일 컬럼 정렬. `sortable: false` 컬럼은 무시 |
|
|
479
|
+
|
|
480
|
+
> **헤더 클릭 = 정렬 정책은 소비처 소유.** core는 헤더 클릭 시 자동으로 정렬하지 **않고** `header:click` 이벤트만 발행합니다(아래 [이벤트 목록](#이벤트-목록) 참조). "헤더 클릭 = 정렬"을 원하면 소비처가 `header:click`을 구독해 `grid.toggleSort(columnId, { additive: event.shiftKey })`를 직접 호출하세요. 정책을 비활성화하려면 구독하지 않으면 됩니다.
|
|
481
|
+
|
|
482
|
+
#### 값 목록 필터 (엑셀식 체크박스)
|
|
483
|
+
|
|
484
|
+
| 메서드 | 반환 | 설명 |
|
|
485
|
+
|--------|------|------|
|
|
486
|
+
| `getFilterValues(columnId, options?)` | `Promise<ColumnValuesResult>` | 값 목록 후보 수집. `options`: `{ keyword?, limit?, onProgress? }`. **자기 컬럼의 값 목록 조건(`in`/`notIn`)만 제외**한 나머지 필터를 적용해 수집 |
|
|
487
|
+
| `getUniqueValues(columnId)` | `Promise<CellValue[]>` | 고유값 배열 (하위 호환·단순 조회용) |
|
|
488
|
+
| `formatColumnValue(columnId, raw)` | `string` | 원본값 → 그 컬럼의 화면 표시 문구(mask 적용). 소비처가 mask 규칙을 재구현하면 어긋나므로 core 가 소유 |
|
|
489
|
+
|
|
490
|
+
> **왜 "컬럼"이 아니라 "값 목록 조건"만 제외하나 (ITSM-2026-000531)**
|
|
491
|
+
> 한 컬럼에 값 목록 조건과 **조건식**(`neq`, `contains`, 수식 …)이 **함께** 걸릴 수 있습니다. 컬럼 id 만 보고 제외하면 조건식까지 빠져, 조건식으로 숨긴 값이 목록에 되살아납니다(실측: 이름 컬럼에 "김철수 제외" 조건식을 걸어도 목록에 김철수가 남음). 되돌릴 수 있어야 하는 것은 **이 패널이 만든 체크 상태**뿐입니다.
|
|
492
|
+
>
|
|
493
|
+
> **표시값(mask) 기준 dedupe** — 컬럼에 mask 가 있으면(명시 `mask` 또는 number 컬럼의 천단위 구분자) 표시값 기준으로 dedupe 합니다. 검색·필터가 이미 표시값 기준이라 목록만 원본값을 쓰면 "보이는 값으로 골랐는데 안 걸리는" 불일치가 납니다. 다만 **필터에 실을 값은 `rawValues`(원본)** 입니다 — 표시값 비교는 5M 행에서 30.6배 느리고 함수형 mask 는 Worker 복제가 안 됩니다.
|
|
494
|
+
>
|
|
495
|
+
> **상한** — 기본 `DEFAULT_VALUE_LIST_LIMIT = 10,000`(Excel AutoFilter 와 동일). 도달하면 스캔을 조기 종료하고 `truncated: true` 로 알립니다. 소비처는 이때 "검색으로 좁혀 주세요"를 안내합니다.
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
const res = await grid.getFilterValues('dept', { keyword: '영업', limit: 5000 });
|
|
499
|
+
res.values; // 표시 라벨 (mask 적용)
|
|
500
|
+
res.rawValues; // 필터에 실을 원본값 — values[i] 와 1:1
|
|
501
|
+
res.truncated; // true 면 상한 도달 (검색 유도)
|
|
502
|
+
|
|
503
|
+
await grid.filter([{ id: 'dept', operator: 'in', filterValue: res.rawValues, sequence: 0 }]);
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 셀 병합
|
|
507
|
+
|
|
508
|
+
| 메서드 | 반환 | 설명 |
|
|
509
|
+
|--------|------|------|
|
|
510
|
+
| `setMergeConfig(config)` | `Promise<void>` | 셀 병합 설정 (`null`로 해제) |
|
|
511
|
+
|
|
512
|
+
### 트랜잭션
|
|
513
|
+
|
|
514
|
+
| 메서드 | 반환 | 설명 |
|
|
515
|
+
|--------|------|------|
|
|
516
|
+
| `applyTransaction(tx, options?)` | `Promise<TransactionResult>` | 동기 트랜잭션 (add/update/remove 일괄) |
|
|
517
|
+
| `applyTransactionAsync(tx, callback?)` | `void` | 비동기 트랜잭션 큐잉 |
|
|
518
|
+
| `flushAsyncTransactions()` | `Promise<TransactionResult[]>` | 대기 중인 비동기 트랜잭션 일괄 실행 |
|
|
519
|
+
|
|
520
|
+
### 데이터 로더
|
|
521
|
+
|
|
522
|
+
| 메서드 | 반환 | 설명 |
|
|
523
|
+
|--------|------|------|
|
|
524
|
+
| `updateLoaderParams(params)` | `Promise<void>` | 로더 파라미터 변경 + 재조회 |
|
|
525
|
+
| `refreshLoaderData()` | `Promise<void>` | 로더 데이터 재조회 |
|
|
526
|
+
|
|
527
|
+
### 스크롤 / 내비게이션
|
|
528
|
+
|
|
529
|
+
| 메서드 | 반환 | 설명 |
|
|
530
|
+
|--------|------|------|
|
|
531
|
+
| `scrollToRow(rowIndex)` | `void` | 특정 행으로 스크롤 |
|
|
532
|
+
| `ensureCellVisible(dataIndex, columnId)` | `void` | 특정 셀이 보이도록 스크롤 |
|
|
533
|
+
|
|
534
|
+
### 검색
|
|
535
|
+
|
|
536
|
+
전체 데이터(가상 스크롤 영역 포함, 피벗 모드에서는 `cachedPivotData`)를 대상으로 청크 분할 검색을 수행합니다. 진행률은 `cell:tooltip`이 아닌 `search:highlighted` 이벤트로 전달됩니다. 검색 활동 중에는 `GridState.activity === 'search'`와 `GridState.isSearching === true`가 true입니다.
|
|
537
|
+
|
|
538
|
+
| 메서드 | 반환 | 설명 |
|
|
539
|
+
|--------|------|------|
|
|
540
|
+
| `search(keyword, options?)` | `Promise<{ matches: CellPosition[]; total: number; currentIndex: number }>` | 키워드 검색. `options`: `{ caseSensitive?, useRegex? }`. 빈 문자열이면 `clearSearch()`와 동일 |
|
|
541
|
+
| `findNext()` | `CellPosition \| null` | 다음 매칭으로 이동(순환). 매칭 없으면 `null` |
|
|
542
|
+
| `findPrev()` | `CellPosition \| null` | 이전 매칭으로 이동(순환). 매칭 없으면 `null` |
|
|
543
|
+
| `findAt(index)` | `CellPosition \| null` | 지정 인덱스의 검색 매칭으로 이동(스크롤+하이라이트). `index`는 0-based(`getSearchState().currentIndex`와 동일 규약). 범위 밖이거나 매칭 없으면 `null` |
|
|
544
|
+
| `clearSearch()` | `void` | 검색 결과 초기화 + 하이라이트 해제 |
|
|
545
|
+
| `getSearchState()` | `SearchHighlightState & { matches, total, currentIndex }` | 현재 키워드/매칭 좌표/커서 인덱스 등 검색 상태 스냅샷 |
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
const result = await grid.search('홍길동');
|
|
549
|
+
console.log(result.total); // 매칭 셀 수
|
|
550
|
+
console.log(result.currentIndex); // 0이면 첫 매칭 셀이 선택됨
|
|
551
|
+
|
|
552
|
+
grid.findNext(); // 다음 매칭으로 스크롤
|
|
553
|
+
grid.findPrev(); // 이전 매칭
|
|
554
|
+
|
|
555
|
+
const state = grid.getSearchState();
|
|
556
|
+
state.matches.forEach(({ rowIndex, columnId }) => { /* ... */ });
|
|
557
|
+
|
|
558
|
+
grid.clearSearch(); // 종료
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### 레이아웃 / UI
|
|
562
|
+
|
|
563
|
+
| 메서드 | 반환 | 설명 |
|
|
564
|
+
|--------|------|------|
|
|
565
|
+
| `setDynamicRowHeights(enabled)` | `void` | 동적 행 높이 켜기/끄기 |
|
|
566
|
+
| `isDynamicRowHeightsEnabled()` | `boolean` | 동적 행 높이 상태 조회 |
|
|
567
|
+
| `setHeaderVisible(visible)` | `void` | 헤더 표시/숨김 |
|
|
568
|
+
| `isHeaderVisible()` | `boolean` | 헤더 표시 상태 조회 |
|
|
569
|
+
| `setStatusBarVisible(visible)` | `void` | 상태 바 표시/숨김 |
|
|
570
|
+
| `formatRow.addHandler(id, cb, opts?)` | `void` | 행 포맷 핸들러 등록 (네임드 키 + priority) |
|
|
571
|
+
| `formatRow.removeHandler(id)` | `void` | 행 포맷 핸들러 해제 |
|
|
572
|
+
| `formatRow.has(id)` | `boolean` | 핸들러 등록 여부 확인 |
|
|
573
|
+
| `formatRow.size` | `number` | 등록된 핸들러 수 |
|
|
574
|
+
| `formatRow.clear()` | `void` | 모든 핸들러 제거 |
|
|
575
|
+
| `formatHeader.addHandler(id, cb, opts?)` | `void` | **헤더 행** 포맷 핸들러 등록 (네임드 키 + priority). 헤더 (재)렌더 시 발화 — formatRow 의 헤더 카운터파트 |
|
|
576
|
+
| `formatHeader.removeHandler(id)` | `void` | 헤더 핸들러 해제 |
|
|
577
|
+
| `formatHeader.has(id)` | `boolean` | 등록 여부 |
|
|
578
|
+
| `formatHeader.size` | `number` | 등록된 핸들러 수 |
|
|
579
|
+
| `formatHeader.clear()` | `void` | 모든 헤더 핸들러 제거 |
|
|
580
|
+
|
|
581
|
+
### 스트리밍
|
|
582
|
+
|
|
583
|
+
| 메서드 | 반환 | 설명 |
|
|
584
|
+
|--------|------|------|
|
|
585
|
+
| `setStreamingOptions(options)` | `void` | 스트리밍 옵션 설정 |
|
|
586
|
+
| `flushRowBatch()` | `void` | 배치된 행 즉시 렌더링 |
|
|
587
|
+
|
|
588
|
+
### 선택
|
|
589
|
+
|
|
590
|
+
선택 동작은 생성자의 `rowSelection` / `cellSelection` 옵션으로만 설정. 런타임 모드 변경은
|
|
591
|
+
재생성으로 처리(소수 사용처). 선택 결과 조회는 `grid.rows.getSelectedRowIds()` / `grid.rows.getSelectedRowsAccessor()`
|
|
592
|
+
또는 SelectionManager 직접 접근(`getSelectedRows`, `getSelectedCells` 등).
|
|
593
|
+
|
|
594
|
+
> **그룹 행과 선택** — 그룹 행(그룹 배너)은 데이터 없는 구조적 행이라 선택 대상이 아닙니다.
|
|
595
|
+
> 드래그 range는 그룹 경계에서 **분리(split)** 되고, `selectAll()`은 그룹 행을 제외한 **모든
|
|
596
|
+
> 데이터 행**을 덮습니다(중간에 그룹 행이 끼어 있어도 하단까지 포함). 자세한 동작은 예제 문서의
|
|
597
|
+
> 「그룹 행과 선택」 참조.
|
|
598
|
+
|
|
599
|
+
### 이벤트
|
|
600
|
+
|
|
601
|
+
| 메서드 | 반환 | 설명 |
|
|
602
|
+
|--------|------|------|
|
|
603
|
+
| `on(event, handler)` | `Unsubscribe` | 이벤트 구독 (해제 함수 반환) |
|
|
604
|
+
| `off(event, handler)` | `void` | 이벤트 구독 해제 |
|
|
605
|
+
|
|
606
|
+
### 카운트
|
|
607
|
+
|
|
608
|
+
| 메서드 | 반환 | 설명 |
|
|
609
|
+
|--------|------|------|
|
|
610
|
+
| `getTotalRowCount()` | `number` | 전체 행 수 (필터 전) |
|
|
611
|
+
| `getVisibleRowCount()` | `number` | 가시 행 수 (필터 후) |
|
|
612
|
+
|
|
613
|
+
### 내보내기
|
|
614
|
+
|
|
615
|
+
| 메서드 | 반환 | 설명 |
|
|
616
|
+
|--------|------|------|
|
|
617
|
+
| `getExportData(options?)` | `Promise<{ headers, rows }>` | 데이터 내보내기 (scope: `'all'` \| `'visible'` \| `'selected'`) |
|
|
618
|
+
|
|
619
|
+
### 필드 / 로케일
|
|
620
|
+
|
|
621
|
+
| 메서드 | 반환 | 설명 |
|
|
622
|
+
|--------|------|------|
|
|
623
|
+
| `getFields()` | `FieldDef[] \| null` | 필드 정의 조회 (flat 모드 전용, 피벗에서 null) |
|
|
624
|
+
| `updateFields(fields)` | `Promise<void>` | 필드 정의 교체 (flat 모드 전용). 기존 컬럼의 레이아웃 상태(너비, 순서, 가시성, 고정)를 보존하면서 필드를 교체. 새 필드는 추가, 삭제된 필드는 제거. **`getFields()` 가 반환하는 원본 필드 목록도 함께 갱신됩니다**(ITSM-2026-002177) — 이전에는 갱신되지 않아 화면에는 있는데 필드 목록에 없는 동적 컬럼이 생겼고, `getFields().find(id)` 로 필드를 찾는 소비처(필터 팝오버·툴바 필터 창·정렬 패널)가 **에러 없이 조용히 무시**했습니다(실측: 동적 컬럼의 필터 아이콘 무반응). 합성 컬럼(`_rowHeader`/`__drag_handle__`)은 core 주입분이라 원본 필드 목록에 포함되지 않습니다 |
|
|
625
|
+
| `getPivotFields()` | `{ rowFields, columnFields, valueFields, filterFields } \| null` | 피벗 필드 조회 (각 카테고리별 FieldDef[], flat에서 null) |
|
|
626
|
+
| `setLocale(locale)` | `void` | 로케일 변경 |
|
|
627
|
+
| `getLocale()` | `string` | 현재 로케일 조회 |
|
|
628
|
+
|
|
629
|
+
### 설정 저장/복원 (Config Snapshot)
|
|
630
|
+
|
|
631
|
+
그리드의 **레이아웃/뷰 상태**(컬럼 너비·순서·숨김·고정, 정렬/필터, 피벗·트리·디테일 설정, UI 옵션 등)와 **reset baseline** 을 직렬화 가능한 bundle 로 추출하고 다시 적용합니다. 사용자별 레이아웃 저장, 프리셋 전환, 백엔드 동기화에 활용합니다.
|
|
632
|
+
|
|
633
|
+
| 메서드 | 반환 | 설명 |
|
|
634
|
+
|--------|------|------|
|
|
635
|
+
| `getConfig()` | `GridConfigExport` | 현재 그리드 상태 + reset baseline 을 `{ current, snapshot }` bundle 로 반환 |
|
|
636
|
+
| `applyConfig(cfg, options?)` | `Promise<void>` | Export bundle 을 현재 그리드에 적용. `cfg.snapshot` 이 있고 `options.applySnapshot !== false` 면 baseline 도 함께 복원 |
|
|
637
|
+
|
|
638
|
+
**`GridConfigExport` bundle 구조**
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
interface GridConfigExport {
|
|
642
|
+
/** 현재 그리드 상태 (런타임 — 컬럼 width/visible/pinned/order, sort/filter/group/pivot 등 모두 반영) */
|
|
643
|
+
current: GridConfigSnapshot;
|
|
644
|
+
/**
|
|
645
|
+
* Reset 의 복귀 기준 baseline. 현재는 컬럼 정적 정의(`fields`) 만 반영됨.
|
|
646
|
+
* 없으면 `applyConfig` 가 기존 baseline 유지.
|
|
647
|
+
*/
|
|
648
|
+
snapshot?: GridConfigSnapshot;
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
- **`current`** — `columns.update` / `setWidth` / `pinLeft` / sort / filter / pivot 변경 등 **현재 그리드 상태**
|
|
653
|
+
- **`snapshot`** — `columns.reset / resetAll` 호출 시 복귀할 **baseline**. 코어 내부의 `initialSnapshot` (construction-time 또는 `captureSnapshot` 으로 박힌 시점) 의 직렬화
|
|
654
|
+
|
|
655
|
+
`applyConfig` 의 `options.applySnapshot` (default `true`):
|
|
656
|
+
- `true` (default) — `cfg.snapshot` 이 있으면 baseline 도 복원. 라운드트립 보장
|
|
657
|
+
- `false` — `cfg.snapshot` 이 있어도 무시. `current` 만 임시 layout 으로 적용 (기존 baseline 유지)
|
|
658
|
+
|
|
659
|
+
**`current` / `snapshot` 에 포함되는 항목 — `GridConfigSnapshot` 형식**
|
|
660
|
+
|
|
661
|
+
| 카테고리 | 내용 |
|
|
662
|
+
|---|---|
|
|
663
|
+
| **모드** | `mode` (`'flat'` / `'pivot'`) |
|
|
664
|
+
| **컬럼(flat)** | `fields[]` — 각 필드의 `id`, `width`, `hidden`, `pinned`, 사용자 정의 순서 |
|
|
665
|
+
| **컬럼 그룹(flat)** | `columnGroups` |
|
|
666
|
+
| **행 템플릿(flat)** | `rowTemplate` (Multi-Row 레이아웃) |
|
|
667
|
+
| **그룹핑(flat)** | `group` |
|
|
668
|
+
| **피벗** | `rowFields`, `columnFields`, `valueFields`, 모든 subtotal/grandTotal 옵션, `rowDisplayMode`, `outlineColumnWidth`, `outlineIndentSize`, `showZeros`, `trackSourceRows`, `rowCollapsible`, `columnCollapsible` 등 |
|
|
669
|
+
| **뷰 상태** | `sorts`, `filters` |
|
|
670
|
+
| **서버/수동 row-model** | `rowModel`, `endReachedThresholdRows` |
|
|
671
|
+
| **트리/디테일** | `treeConfig`, `masterDetailConfig` |
|
|
672
|
+
| **UI 옵션** | `theme`, `rowHeight`, `headerHeight`, `rowSelection`, `cellSelection`, `cellSelectionImpliesRows`, `editable`, `tooltipMode`, `alternatingRows`, `showHeader`, `resizableColumns`, `reorderableColumns`, `rowHeader`, `reorderableRows`, `showDragHandleColumn`, `autoFitColumns`, `showStatusBar`, `locale`, `translations`, `keyFields` 등 |
|
|
673
|
+
|
|
674
|
+
**제외되는 항목**
|
|
675
|
+
|
|
676
|
+
| 항목 | 이유 |
|
|
677
|
+
|---|---|
|
|
678
|
+
| **data** | 원본 데이터는 스냅샷에 포함되지 않음. `loadData`로 별도 복원 |
|
|
679
|
+
| **함수 (cellRenderer, formatRow, validator 등)** | 직렬화 불가. 사용자 코드에서 다시 등록 |
|
|
680
|
+
| **DOM/객체 참조 (undoStack, loader, dragDropConfig 등)** | 직렬화 불가 |
|
|
681
|
+
| **dirty 상태** | ChangeTracker는 스냅샷에 포함되지 않음. 필요하면 `grid.changes.getAll()`로 별도 저장 |
|
|
682
|
+
|
|
683
|
+
**특징**
|
|
684
|
+
|
|
685
|
+
- **라운드트립 보장** — `applyConfig(getConfig())` 가 항상 동일한 상태로 복원 (baseline 포함)
|
|
686
|
+
- **`current.fields` 등 부분 적용** — `current` 안의 키들은 `Partial` 형태로 처리 — 누락된 키는 그리드 기존 값 유지
|
|
687
|
+
- **모드 전환 자동 처리** — `current.mode` 가 현재와 다르면 `pivot.setMode()` 자동 호출
|
|
688
|
+
- **순서 복원** — flat 모드의 `current.fields` 배열 순서 그대로 컬럼이 재정렬됨
|
|
689
|
+
- **getConfig 는 sync, applyConfig 는 async** — 복원 중 엔진 재계산(정렬/필터/피벗)이 포함되기 때문
|
|
690
|
+
- **`snapshot` 구조** — 현재는 `fields` 만 채워서 반환 (`columns.initialSnapshot` 의 정적 정의 직렬화). 나머지 키는 향후 확장 여지
|
|
691
|
+
|
|
692
|
+
**사용 예제**
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
// 1) localStorage 에 사용자 레이아웃 저장 (baseline 포함)
|
|
696
|
+
function saveLayout() {
|
|
697
|
+
const cfg = grid.getConfig();
|
|
698
|
+
localStorage.setItem('gridLayout', JSON.stringify(cfg));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 2) 복원 — baseline 까지 함께 복원 (라운드트립)
|
|
702
|
+
async function loadLayout() {
|
|
703
|
+
const raw = localStorage.getItem('gridLayout');
|
|
704
|
+
if (!raw) return;
|
|
705
|
+
const cfg = JSON.parse(raw);
|
|
706
|
+
await grid.applyConfig(cfg);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// 3) bundle 의 current 만 적용, baseline 손대지 않음 — 임시 layout 미리보기
|
|
710
|
+
await grid.applyConfig(savedCfg, { applySnapshot: false });
|
|
711
|
+
grid.columns.resetAll(); // → 원래 baseline 으로 복귀
|
|
712
|
+
|
|
713
|
+
// 4) 정렬/필터만 적용 (snapshot 키 생략 → baseline 유지)
|
|
714
|
+
await grid.applyConfig({
|
|
715
|
+
current: {
|
|
716
|
+
mode: 'flat',
|
|
717
|
+
sorts: [{ id: 'price', direction: 'desc', sequence: 1 }],
|
|
718
|
+
filters: [{ columnId: 'brand', operator: 'equals', value: 'Apple' }],
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// 5) 피벗 상태 전환
|
|
723
|
+
await grid.applyConfig({
|
|
724
|
+
current: {
|
|
725
|
+
mode: 'pivot',
|
|
726
|
+
rowFields: [{ field: 'region' }, { field: 'category' }],
|
|
727
|
+
valueFields: [{ field: 'amount', aggregate: 'sum' }],
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// 6) 현재 상태를 새 baseline 으로 승격
|
|
732
|
+
const cfg = grid.getConfig();
|
|
733
|
+
await grid.applyConfig({ current: cfg.current, snapshot: cfg.current });
|
|
734
|
+
// 이후 columns.reset 호출 시 이 시점의 layout 으로 복귀
|
|
735
|
+
|
|
736
|
+
// 7) 백엔드에 사용자별 프리셋 저장
|
|
737
|
+
async function syncToServer(userId: string) {
|
|
738
|
+
const cfg = grid.getConfig();
|
|
739
|
+
await fetch(`/api/users/${userId}/grid-preset`, {
|
|
740
|
+
method: 'PUT',
|
|
741
|
+
body: JSON.stringify(cfg),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**주의사항**
|
|
747
|
+
|
|
748
|
+
- `applyConfig` 호출 전후로 `data`는 유지됩니다. 데이터 자체는 `grid.loadData()`로 별도 관리.
|
|
749
|
+
- `current.fields` 배열의 `id` 가 현재 컬럼과 매칭되지 않으면 해당 항목은 조용히 무시됩니다. 완전히 다른 스키마로 전환하려면 새 `PureSheet` 인스턴스를 만드세요.
|
|
750
|
+
- 복원 직후 DOM이 즉시 갱신되지만, scroll 위치는 복원하지 않습니다. 필요하면 별도로 저장/복원하세요 (`grid.select('scroll')` 관찰 등).
|
|
751
|
+
|
|
752
|
+
### 성능 측정
|
|
753
|
+
|
|
754
|
+
| 메서드 | 반환 | 설명 |
|
|
755
|
+
|--------|------|------|
|
|
756
|
+
| `addTiming(name, duration)` | `void` | 타이밍 기록 추가 |
|
|
757
|
+
| `clearTimings()` | `void` | 타이밍 기록 초기화 |
|
|
758
|
+
| `setTimings(timings)` | `void` | 타이밍 기록 일괄 설정 |
|
|
759
|
+
| `getTimings()` | `PerformanceTiming[]` | 타이밍 기록 조회 |
|
|
760
|
+
|
|
761
|
+
### 피벗 mask 캐시 진단 (ADR-112)
|
|
762
|
+
|
|
763
|
+
피벗 컬럼 mask 캐시의 hit-rate와 signature 비용을 측정합니다. 벤치마크/회귀 진단용. 운영 동작에는 영향 없으며 정수 카운터 + 누적 시간만 기록.
|
|
764
|
+
|
|
765
|
+
| 메서드 | 반환 | 설명 |
|
|
766
|
+
|--------|------|------|
|
|
767
|
+
| `getPivotMaskCacheStats()` | `{ hits, misses, hitRate, signatureTimeMs, avgSignatureUs }` | 누적 진단 통계 조회 |
|
|
768
|
+
| `resetPivotMaskCacheStats()` | `void` | 카운터 리셋 |
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
grid.resetPivotMaskCacheStats();
|
|
772
|
+
await runWorkload();
|
|
773
|
+
console.log(grid.getPivotMaskCacheStats());
|
|
774
|
+
// → { hits: 10000, misses: 5, hitRate: 0.9995, signatureTimeMs: 12.4, avgSignatureUs: 1.24 }
|
|
775
|
+
|
|
776
|
+
// 추가로, 캐시 검증 모드 (테스트/디버깅 전용):
|
|
777
|
+
PureSheet.debugPivotMaskCache = true;
|
|
778
|
+
// 캐시 hit마다 fresh 빌드와 동등성 검증 — signature 누락 입력 탐지.
|
|
779
|
+
// production에서는 절대 활성화하지 말 것 (Fast-Path 무효화).
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### 반응형 구독
|
|
783
|
+
|
|
784
|
+
| 메서드 | 반환 | 설명 |
|
|
785
|
+
|--------|------|------|
|
|
786
|
+
| `select(selector)` | `Subscribable<T>` | GridState 파생 상태 구독 ([상세](#gridselect)) |
|
|
787
|
+
|
|
788
|
+
### Sub-API 접근
|
|
789
|
+
|
|
790
|
+
| 속성 | 타입 | 설명 |
|
|
791
|
+
|------|------|------|
|
|
792
|
+
| `rows` | `IRowsAPI` | 행 API |
|
|
793
|
+
| `columns` | `IColumnsAPI` | 컬럼 API |
|
|
794
|
+
| `changes` | `IChangesAPI` | 변경 추적 API |
|
|
795
|
+
| `undoManager` | `IUndoManagerAPI` | Undo/Redo API |
|
|
796
|
+
| `group` | `IGroupAPI` | 그룹화 API |
|
|
797
|
+
| `tree` | `ITreeAPI` | 트리 API |
|
|
798
|
+
| `pivot` | `IPivotAPI` | 피벗 API |
|
|
799
|
+
| `masterDetail` | `IMasterDetailAPI` | 마스터-디테일 API |
|
|
800
|
+
| `validation` | `IValidationAPI` | 검증 API |
|
|
801
|
+
| `clipboard` | `IClipboardAPI` | 클립보드 API |
|
|
802
|
+
| `headers` | `IHeadersAPI` | 컬럼 그룹(다단계 컬럼 헤더) API |
|
|
803
|
+
| `io` | `IIOApi` | Excel Import/Export API |
|
|
804
|
+
| `dragDrop` | `IDragDropAPI` | 행 드래그앤드롭 / 크로스-그리드 드롭 설정 API |
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## Sub-API
|
|
809
|
+
|
|
810
|
+
PureSheet 인스턴스는 기능별 Sub-API 네임스페이스를 제공합니다.
|
|
811
|
+
|
|
812
|
+
### grid.rows — 행 API
|
|
813
|
+
|
|
814
|
+
#### 행 조회
|
|
815
|
+
|
|
816
|
+
| 메서드 | 반환 | 설명 |
|
|
817
|
+
|--------|------|------|
|
|
818
|
+
| `get(row)` | `SingleRowAccessor` | 행 객체로 단일 행 접근자 (`enableReferCache` 필요) |
|
|
819
|
+
| `getById(id)` | `SingleRowAccessor` | ID로 단일 행 접근자 |
|
|
820
|
+
| `getByIndex(sourceIndex)` | `SingleRowAccessor` | sourceIndex로 단일 행 접근 O(1) |
|
|
821
|
+
| `getByIds(ids)` | `MultiRowAccessor` | ID 목록으로 행 접근 |
|
|
822
|
+
| `getByIndices(indices)` | `MultiRowAccessor` | sourceIndex 목록으로 행 접근 |
|
|
823
|
+
| `all()` | `MultiRowAccessor` | 전체 행 접근자 |
|
|
824
|
+
| `where(predicate)` | `MultiRowAccessor` | 조건부 행 접근자. `predicate: (data, state) => boolean` (state는 `'pristine'\|'added'\|'modified'\|'deleted'`) |
|
|
825
|
+
|
|
826
|
+
#### 반응형
|
|
827
|
+
|
|
828
|
+
| 메서드 | 반환 | 설명 |
|
|
829
|
+
|--------|------|------|
|
|
830
|
+
| `observe(row)` | `ReactiveRow` | 행 객체로 반응형 구독 (`enableReferCache` 필요) |
|
|
831
|
+
| `observeById(id)` | `ReactiveRow` | ID로 행 반응형 구독 ([상세](#gridrowsobservebyid)) |
|
|
832
|
+
| `observeByIndex(sourceIndex)` | `ReactiveRow` | sourceIndex로 행 반응형 구독 |
|
|
833
|
+
|
|
834
|
+
#### 선택 (행 — 쓰기)
|
|
835
|
+
|
|
836
|
+
| 메서드 | 반환 | 설명 |
|
|
837
|
+
|--------|------|------|
|
|
838
|
+
| `selectRowsByIds(ids)` | `void` | rowId 배열로 행 선택 (enableReferCache 필요) |
|
|
839
|
+
| `selectRowsByViewIndices(viewIndices)` | `void` | viewIndex(화면 물리 순서) 배열로 행 선택. 정렬/필터 후에도 "화면상 그 위치"를 선택(sourceIndex 아님), data 행만 |
|
|
840
|
+
| `selectRowRangeByViewIndex(startViewIndex, endViewIndex)` | `void` | viewIndex 범위로 행 선택. "화면상 N~M번째" 범위, 구조행 자동 제외 |
|
|
841
|
+
| `selectRowRangeByIds(startRowId, endRowId)` | `void` | rowId 범위로 행 선택(정렬/필터 안정). 한쪽이라도 숨김이면 no-op |
|
|
842
|
+
| `selectAll()` | `void` | 전체 행 선택 |
|
|
843
|
+
| `clearSelection()` | `void` | 행 선택 해제 |
|
|
844
|
+
|
|
845
|
+
#### 선택 조회 (행 — grid.rows)
|
|
846
|
+
|
|
847
|
+
| 메서드 | 반환 | 설명 |
|
|
848
|
+
|--------|------|------|
|
|
849
|
+
| `selected()` | `MultiRowAccessor` | 선택된 행 접근자 |
|
|
850
|
+
| `getSelectedRowIds()` | `(string \| number)[]` | 선택된 행 rowId |
|
|
851
|
+
| `getSelectedRowIndices()` | `Promise<number[]>` | 선택된 행 sourceIndex |
|
|
852
|
+
| `getSelectedRowViewIndices()` | `Promise<number[]>` | 선택된 행 viewIndex(화면 물리 순서) |
|
|
853
|
+
| `getSelectedRowDisplayedIndices()` | `Promise<number[]>` | 선택된 행 displayedIndex(화면 표시 순서, 접힘/필터 반영). 숨김 행 제외 |
|
|
854
|
+
| `selectionAggregates()` | `SelectionAggregateResult \| null` | 선택 범위 집계 (컬럼별 + 총 집계) |
|
|
855
|
+
|
|
856
|
+
> **displayed 좌표계 조회**(`displayedCount`/`getByDisplayedIndex`/`getDisplayedIndexById`)는 위 [행 조회](#grid.rows) 표 참조 — 다음/이전(traversal) 이동에 사용.
|
|
857
|
+
|
|
858
|
+
### grid.cells — 셀 선택 API
|
|
859
|
+
|
|
860
|
+
셀 = 행(rowId/viewIndex) + 열(columnId)의 교차점. 쓰기(select)와 조회(selected)를 제공한다.
|
|
861
|
+
|
|
862
|
+
#### 선택 (셀 — 쓰기)
|
|
863
|
+
|
|
864
|
+
| 메서드 | 반환 | 설명 |
|
|
865
|
+
|--------|------|------|
|
|
866
|
+
| `selectCellsByIds(cells)` | `void` | `{rowId, columnId}` 배열로 셀 선택. 숨김 행은 건너뜀 |
|
|
867
|
+
| `selectCellsByViewIndices(cells)` | `void` | `{viewIndex, columnId}` 배열로 셀 선택 |
|
|
868
|
+
| `selectCellRangeByViewIndex(start, end)` | `void` | `{viewIndex, columnId}` 두 점으로 사각형 범위 선택 |
|
|
869
|
+
| `selectCellRangeByIds(start, end)` | `void` | `{rowId, columnId}` 두 점으로 사각형 범위 선택(정렬/필터 안정) |
|
|
870
|
+
| `clearCellSelection()` | `void` | 셀 선택만 해제 (행 선택 유지) |
|
|
871
|
+
|
|
872
|
+
#### 선택 조회 (셀 — grid.cells)
|
|
873
|
+
|
|
874
|
+
| 메서드 | 반환 | 설명 |
|
|
875
|
+
|--------|------|------|
|
|
876
|
+
| `selected()` | `{rowId, columnId}[]` | 선택된 셀 목록 |
|
|
877
|
+
| `getSelectedCellRowIds()` | `string[]` | 선택된 셀이 걸친 행 rowId 목록(중복 제거) |
|
|
878
|
+
| `getSelectedCellColumnIds()` | `string[]` | 선택된 셀이 걸친 컬럼 columnId 목록(중복 제거) |
|
|
879
|
+
| `getSelectedCellIndices()` | `number[]` | 선택된 셀이 걸친 행 sourceIndex 목록(중복 제거) |
|
|
880
|
+
| `getSelectedCellViewIndices()` | `number[]` | 선택된 셀이 걸친 행 viewIndex 목록(중복 제거) |
|
|
881
|
+
| `getSelectedCellDisplayedIndices()` | `Promise<number[]>` | 선택된 셀이 걸친 행 displayedIndex 목록(중복 제거, async) |
|
|
882
|
+
|
|
883
|
+
### grid.selection — 선택 상태 조회 (RowHandle 기반, 행/셀 공통)
|
|
884
|
+
|
|
885
|
+
rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다를 수 있으며(행은 3,5인데 셀 range는 7~9행 등), 각각 **RowHandle**(viewIndex/sourceIndex 좌표 채워진 가상 행 — data·조작 접근 가능) 기반으로 조회한다. 앵커(anchorCell)는 행/셀 어느 쪽이든 "마지막 상호작용"이 덮어쓰는 단일 고정점으로 traversal 시작점이다.
|
|
886
|
+
|
|
887
|
+
| 메서드 | 반환 | 설명 |
|
|
888
|
+
|--------|------|------|
|
|
889
|
+
| `selection.getSelectedRows()` | `RowHandle[]` | 현재 선택된 행들(viewIndex/sourceIndex 채워짐) |
|
|
890
|
+
| `selection.getActiveRow()` | `RowHandle \| null` | 마지막 상호작용(행/셀 클릭) 행. 앵커 없으면 선택 행 마지막 폴백. traversal 앵커 |
|
|
891
|
+
| `selection.getSelectedCells()` | `{ row: RowHandle; columnId }[]` | 현재 선택된 셀들 |
|
|
892
|
+
| `selection.getActiveCell()` | `{ row: RowHandle; columnId } \| null` | 마지막 상호작용 셀. traversal 컬럼 유지용 |
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
#### 선택 일괄
|
|
896
|
+
|
|
897
|
+
| 메서드 | 반환 | 설명 |
|
|
898
|
+
|--------|------|------|
|
|
899
|
+
| `deleteSelectedRows()` | `Promise<number>` | 선택 행 삭제 (건수 반환) |
|
|
900
|
+
| `updateSelectedRows(field, value)` | `Promise<number>` | 선택 **행**들의 지정 필드 일괄 업데이트 |
|
|
901
|
+
| `discardSelectedRows()` | `Promise<number>` | 선택 행 변경 취소 |
|
|
902
|
+
|
|
903
|
+
#### 데이터 행 고정 (기존 데이터 행을 상단/하단에 고정)
|
|
904
|
+
|
|
905
|
+
> `pinTop`/`pinBottom`은 **데이터 행 객체**를 받아 그 행을 고정하는 메서드입니다 (`enableReferCache` 필요). 새로운 행/구성을 추가하려면 [구조적 행 고정](#구조적-행-고정) 섹션의 `addPinnedTop` / `addPinnedBottom`을 사용하세요.
|
|
906
|
+
|
|
907
|
+
| 메서드 | 반환 | 설명 |
|
|
908
|
+
|--------|------|------|
|
|
909
|
+
| `pinTop(row)` | `Row` | 데이터 행 객체로 상단 고정 (`enableReferCache` 필요) |
|
|
910
|
+
| `pinTopById(rowId)` | `Row` | ID로 기존 데이터 행을 상단에 고정 |
|
|
911
|
+
| `pinTopByIndex(sourceIndex)` | `Row` | sourceIndex로 기존 데이터 행을 상단에 고정 |
|
|
912
|
+
| `pinBottom(row)` | `Row` | 데이터 행 객체로 하단 고정 (`enableReferCache` 필요) |
|
|
913
|
+
| `pinBottomById(rowId)` | `Row` | ID로 기존 데이터 행을 하단에 고정 |
|
|
914
|
+
| `pinBottomByIndex(sourceIndex)` | `Row` | sourceIndex로 기존 데이터 행을 하단에 고정 |
|
|
915
|
+
|
|
916
|
+
#### 구조적 행 고정
|
|
917
|
+
|
|
918
|
+
| 메서드 | 반환 | 설명 |
|
|
919
|
+
|--------|------|------|
|
|
920
|
+
| `addPinnedTop(row \| RowConfig)` | `Row` | 새 행/설정으로 상단 고정 행 추가 |
|
|
921
|
+
| `addPinnedBottom(row \| RowConfig)` | `Row` | 새 행/설정으로 하단 고정 행 추가 |
|
|
922
|
+
| `addGrandTotal(aggregates)` | `Row` | 전체 합계 행 추가 |
|
|
923
|
+
| `addFilter()` | `Row` | 필터 행 추가 |
|
|
924
|
+
|
|
925
|
+
#### 고정 관리
|
|
926
|
+
|
|
927
|
+
| 메서드 | 반환 | 설명 |
|
|
928
|
+
|--------|------|------|
|
|
929
|
+
| `unpin(row)` | `boolean` | `Row` 인스턴스로 고정 해제 |
|
|
930
|
+
| `unpinById(rowId)` | `boolean` | ID로 행 고정 해제 |
|
|
931
|
+
| `getPinned()` | `{ top: Row[]; bottom: Row[] }` | 고정된 행 조회 |
|
|
932
|
+
| `clearPinned()` | `void` | 전체 고정 해제 |
|
|
933
|
+
| `refreshPinned()` | `void` | 고정 행 표시 갱신 |
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
### grid.columns — 컬럼 API
|
|
938
|
+
|
|
939
|
+
#### 위치 / 너비 / 가시성 (per-column 빠른 setter)
|
|
940
|
+
|
|
941
|
+
| 메서드 | 반환 | 설명 |
|
|
942
|
+
|--------|------|------|
|
|
943
|
+
| `pinLeft(colId)` | `void` | 왼쪽 고정 |
|
|
944
|
+
| `pinRight(colId)` | `void` | 오른쪽 고정 |
|
|
945
|
+
| `unpin(colId)` | `void` | 고정 해제 |
|
|
946
|
+
| `setWidth(colId, width)` | `void` | 너비 설정 |
|
|
947
|
+
| `autoFit(colId, auto)` | `void` | 자동 너비 |
|
|
948
|
+
| `autoFitAll(options?)` | `void` | 전체 자동 너비. `options` 는 `boolean`(= `persistent`, 하위호환) 또는 `{ persistent?, lazyOnScroll? }` 객체. `lazyOnScroll: true` 면 off-screen 컬럼은 처음 노출 시 1회만 측정 |
|
|
949
|
+
| `setOrder(order, options?)` | `void` | 컬럼 순서 설정 (일반/컬럼 그룹 모두 지원) |
|
|
950
|
+
| `move(columnId, targetId, position?)` | `void` | 단일 컬럼 이동 (`'before'` \| `'after'`, 기본 `'before'`) |
|
|
951
|
+
| `moveGroup(groupId, targetGroupId, position?)` | `void` | 그룹 통째로 이동 (같은 레벨 형제 간) |
|
|
952
|
+
| `hide(colId)` | `void` | 컬럼 숨김 |
|
|
953
|
+
| `show(colId)` | `void` | 컬럼 표시 |
|
|
954
|
+
| `setHidden(colId, hidden)` | `void` | 숨김 상태 설정 |
|
|
955
|
+
| `toggle(colId)` | `void` | 가시성 토글 |
|
|
956
|
+
|
|
957
|
+
> 위 4개는 모두 `setHidden` 한 경로로 수렴하며, 대상 컬럼이 [`lockVisible`](#정렬표시) 이면 **거부**되고 `console.warn` 을 남깁니다(요청값이 현재값과 같으면 조용한 no-op). 가시성 잠금은 UI/API 를 가리지 않는 하드 불변식입니다.
|
|
958
|
+
|
|
959
|
+
#### 통합 변경 / 복원 — `update` · `reset` · `resetAll`
|
|
960
|
+
|
|
961
|
+
| 메서드 | 반환 | 설명 |
|
|
962
|
+
|--------|------|------|
|
|
963
|
+
| `update(colId, partial, options?)` | `void` | FieldDef/ColumnDef 키들을 런타임 부분 갱신. 변경 키별 적절한 렌더 경로 자동 발화 |
|
|
964
|
+
| `reset(colId)` | `void` | 1개 컬럼을 초기 스냅샷으로 복귀 (정적 + 런타임 상태 양쪽). 컬럼 순서는 복원하지 않음 |
|
|
965
|
+
| `resetAll()` | `void` | 모든 컬럼을 초기 스냅샷으로 복귀. 컬럼 그룹이 없을 때 컬럼 순서도 함께 복원 |
|
|
966
|
+
| `captureSnapshot()` | `void` | 현재 모든 컬럼의 상태를 새 baseline 스냅샷으로 박음. 이후 `reset/resetAll` 은 이 시점으로 복귀 |
|
|
967
|
+
|
|
968
|
+
**`update(colId, partial, options?)` — 키 카테고리별 동작**
|
|
969
|
+
|
|
970
|
+
| 키 카테고리 | 처리 방식 |
|
|
971
|
+
|---|---|
|
|
972
|
+
| `width` / `hidden` / `pinned` / `frozen` / `autoFit` | 기존 setter 위임 (per-column DOM 패치, refresh 불필요) |
|
|
973
|
+
| `header` / `label` / `headerAttributes` / `headerRenderer` | def mutate → 헤더 재렌더 (전체 refresh) |
|
|
974
|
+
| `align` / `mask` / `cellAttributes` / `cellRenderer` / `decimalFormat` / `useThousandsSeparator` / `dataType` / `type` | def mutate → body 셀 재페인트 |
|
|
975
|
+
| `aggregate` / `mergeStrategy` | def mutate → 재집계/재머지 |
|
|
976
|
+
| `flex` / `minWidth` / `maxWidth` | def mutate → 레이아웃 재계산 |
|
|
977
|
+
| `sortable` / `filterable` / `editable` / `readonly` / `editableWhen` / `validation` / `editor` / `editorResolver` / `formula` | mutate-only (다음 인터랙션 시 자동 반영) |
|
|
978
|
+
| `id` / `pivotValueField` / `pivotValueFieldConfig` | warn + skip (변경 위험 키) |
|
|
979
|
+
| 알 수 없는 키 | 보수적으로 mutate-only |
|
|
980
|
+
|
|
981
|
+
**Satellite 키 자동 동기화** — `update` 가 내부적으로 동시에 mutate 하는 짝:
|
|
982
|
+
- `pinned` ↔ `frozen` (서로 alias)
|
|
983
|
+
- `header` ↔ `label` (header 입력 시 label 도 함께 세팅)
|
|
984
|
+
- `dataType` → `type`
|
|
985
|
+
|
|
986
|
+
**`options.updateSnapshot: true`** — 변경분을 초기 스냅샷에도 미러합니다. 이후 `reset(colId)` 호출 시 이번 변경을 **포함한** 새 baseline 으로 복귀. 사용자 UI 의 임시 오버레이가 아닌 "**새 영구 baseline 으로 승격**" 의도일 때 사용합니다.
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
// 임시 오버레이 (reset 시 사라짐)
|
|
990
|
+
grid.columns.update('amount', {
|
|
991
|
+
cellAttributes: { class: 'user-yellow', style: 'background:#fffbe6' },
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// 새 baseline 으로 승격 (reset 후에도 유지)
|
|
995
|
+
grid.columns.update('amount', { header: '판매금액' }, { updateSnapshot: true });
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
#### 조회 (모두 `ColumnInfo` shape 통일)
|
|
999
|
+
|
|
1000
|
+
`getById / getAll / getMap / getVisibleColumns / getState` 5종은 **모두 동일한 `ColumnInfo` shape** 을 반환합니다. 정적 ColumnDef + 런타임 ColumnState 합본 한 객체. 차이는 어떤 집합/순서로 묶어주는지뿐:
|
|
1001
|
+
|
|
1002
|
+
| 메서드 | 반환 | 어떤 컬럼 / 어떤 순서 |
|
|
1003
|
+
|--------|------|------------------------|
|
|
1004
|
+
| `getById(columnId)` | `ColumnInfo \| null` | 단건 |
|
|
1005
|
+
| `getAll()` | `ColumnInfo[]` | 전체, **컬럼 정의 순서** |
|
|
1006
|
+
| `getMap()` | `ColumnInfo[]` | `getAll` 의 별칭 (하위호환) |
|
|
1007
|
+
| `getVisibleColumns()` | `ColumnInfo[]` | 보이는 것만, **`order` 오름차순** |
|
|
1008
|
+
| `getState()` | `ColumnInfo[]` | 전체, **state 배열 순서** (정렬 X) |
|
|
1009
|
+
|
|
1010
|
+
**충돌 키 정책** — state(런타임)가 승:
|
|
1011
|
+
- `width` / `pinned` / `visible` / `autoFit` → 현재값 (state)
|
|
1012
|
+
- `hidden` → `!visible` 로 파생
|
|
1013
|
+
- `header` → `def.header ?? def.label ?? def.id` 로 항상 비어있지 않음
|
|
1014
|
+
|
|
1015
|
+
#### 저장/복원 (직렬화)
|
|
1016
|
+
|
|
1017
|
+
| 메서드 | 반환 | 설명 |
|
|
1018
|
+
|--------|------|------|
|
|
1019
|
+
| `save()` | `string` | 컬럼 상태 JSON 저장 |
|
|
1020
|
+
| `load(json)` | `void` | 컬럼 상태 JSON 복원 |
|
|
1021
|
+
| `getInitialSnapshotDefs()` | `ColumnDef[]` | 현재 initial snapshot 의 ColumnDef[] 복사본. `PureSheet.getConfig()` 가 내부적으로 호출해서 `GridConfigExport.snapshot.fields` 에 담음. 외부 직렬화/공유용 |
|
|
1022
|
+
| `setInitialSnapshotDefs(defs)` | `void` | 외부 ColumnDef[] 로 initial snapshot 일괄 교체. `PureSheet.applyConfig({ snapshot })` 가 내부적으로 호출. 이후 `reset/resetAll` 의 복귀 기준이 됨 |
|
|
1023
|
+
|
|
1024
|
+
#### 기타
|
|
1025
|
+
|
|
1026
|
+
| 메서드 | 반환 | 설명 |
|
|
1027
|
+
|--------|------|------|
|
|
1028
|
+
| `syncSelectionColumnOrder()` | `void` | SelectionManager 컬럼 인덱스 맵 동기화 (컬럼 가시성/순서를 직접 조작한 뒤 호출) |
|
|
1029
|
+
|
|
1030
|
+
#### 발화 이벤트
|
|
1031
|
+
|
|
1032
|
+
| 이벤트 | payload | 발화 시점 |
|
|
1033
|
+
|---|---|---|
|
|
1034
|
+
| `column:resize` | `{ columnId, width }` | `setWidth` / `update({width})` / **헤더 드래그 리사이즈** (마우스로 컬럼 경계를 끌어 너비 변경) |
|
|
1035
|
+
| `column:pin` | `{ columnId, position }` | `pinLeft` / `pinRight` / `unpin` / `update({pinned})` |
|
|
1036
|
+
| `column:reorder` | `{ order, source?, sourceId? }` | `setOrder` / `move` / `moveGroup` / **헤더 드래그 재정렬** |
|
|
1037
|
+
| `column:visibility` | `{ columnId, visible }` | `hide` / `show` / `setHidden` / `toggle` |
|
|
1038
|
+
| `column:update` | `{ columnId, changedKeys: string[] }` | `update(...)` |
|
|
1039
|
+
| `column:reset` | `{ columnId: string \| null }` | `reset` / `resetAll` (resetAll 시 `columnId: null`) |
|
|
1040
|
+
|
|
1041
|
+
> **드래그 vs API**: 드래그 경로는 코어 내부에서 `GridRenderer.handleColumnResize / handleColumnReorder` → `PureSheet.handleColumnResize / handleColumnReorder` 콜백을 통해 위 이벤트를 emit. API 경로 (`columns.setWidth` 등) 와 동일한 페이로드 — wrapper 가 한 번만 listen 하면 두 경로 모두 받음.
|
|
1042
|
+
|
|
1043
|
+
#### 어떤 도구를 언제 쓰나 — 의사결정 가이드
|
|
1044
|
+
|
|
1045
|
+
컬럼 관련 작업은 여러 도구로 가능. 시나리오별 권장:
|
|
1046
|
+
|
|
1047
|
+
| 의도 | 권장 도구 | 이유 |
|
|
1048
|
+
|---|---|---|
|
|
1049
|
+
| 설계 시점에 컬럼 옵션 고정 | `FieldDef` (config) | 정적, 직렬화 자동 |
|
|
1050
|
+
| 런타임 1회 변경 (예: 사용자가 헤더 라벨 변경) | `columns.update(id, partial)` | 단일 진입점, 부분 갱신 |
|
|
1051
|
+
| 런타임 변경을 **새 baseline 으로 영구화** | `columns.update(id, partial, { updateSnapshot: true })` | reset 후에도 유지 |
|
|
1052
|
+
| 컬럼 초기 상태로 복귀 | `columns.reset(id)` / `resetAll()` | 스냅샷 기반 복귀 |
|
|
1053
|
+
| **사용자 UI 의 컬럼별 서식** (body 셀) — 자주 변함 | `formatRow.addHandler` | 스크롤 친화, multi-owner 공존 |
|
|
1054
|
+
| **사용자 UI 의 컬럼별 서식** (헤더) — 자주 변함 | `formatHeader.addHandler` | 헤더 라이프사이클 매칭, multi-owner 공존 |
|
|
1055
|
+
| 행 데이터 기반 동적 셀 서식 (예: 음수=빨강) | `cellAttributes(fn)` 또는 `formatRow` | 행 컨텍스트 접근 가능 |
|
|
1056
|
+
| 정렬/필터 메타 기반 헤더 시각 마커 | `formatHeader.addHandler` | ctx.sortStates / filterStates / cell.sortState 활용 |
|
|
1057
|
+
| 컬럼 표시/숨김·순서·너비·핀 토글 | per-column setter (`setHidden / setOrder / setWidth / pinLeft`) | 가장 가볍고 직관적 |
|
|
1058
|
+
| 전체 레이아웃 저장·복원 | `save()` / `load(json)` (단일 컬럼) 또는 `getConfig() / applyConfig()` (전체) | 직렬화 가능 |
|
|
1059
|
+
|
|
1060
|
+
**일반 원칙**:
|
|
1061
|
+
- 정적 / 1회성 → **`columns.update` 또는 setter**
|
|
1062
|
+
- 자주 변하는 동적 서식 → **formatRow / formatHeader hook**
|
|
1063
|
+
- 사용자 wrapper UI 가 만드는 서식 → **hook + addHandler** (wrapper-managed)
|
|
1064
|
+
- baseline 의미를 가진 변경 → **`updateSnapshot: true`**
|
|
1065
|
+
- 임시 오버레이 (reset 으로 사라져야 함) → **`updateSnapshot` 옵션 X**
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
### grid.changes — 변경 추적 API
|
|
1070
|
+
|
|
1071
|
+
#### CRUD (Dirty State)
|
|
1072
|
+
|
|
1073
|
+
| 메서드 | 반환 | 설명 |
|
|
1074
|
+
|--------|------|------|
|
|
1075
|
+
| `addRow(row, idx?)` | `Promise<string \| number>` | 행 추가 (dirty) |
|
|
1076
|
+
| `updateCell(row, field, value)` | `Promise<void>` | 행 객체로 셀 수정 (`enableReferCache` 필요) |
|
|
1077
|
+
| `updateCellById(id, field, value, sourceIndex?)` | `Promise<void>` | ID로 셀 수정. 호출자가 sourceIndex를 알고 있으면 4번째 인자로 전달하여 idMap fallback을 회피할 수 있음 |
|
|
1078
|
+
| `updateCellByIndex(idx, field, value)` | `Promise<void>` | sourceIndex로 셀 수정 O(1) |
|
|
1079
|
+
| `deleteRow(row)` | `Promise<void>` | 행 객체로 삭제 마킹 (`enableReferCache` 필요) |
|
|
1080
|
+
| `deleteRowById(id, sourceIndex?, originalDataHint?)` | `Promise<void>` | ID로 행 삭제 마킹. 선택적 hint 인자로 idMap O(N) lookup 회피 가능 |
|
|
1081
|
+
| `deleteRowByIndex(idx)` | `Promise<void>` | sourceIndex로 행 삭제 마킹 O(1) |
|
|
1082
|
+
| `undeleteRow(row)` | `Promise<void>` | 행 객체로 삭제 복원 (`enableReferCache` 필요) |
|
|
1083
|
+
| `undeleteRowById(id)` | `Promise<void>` | ID로 삭제 복원 |
|
|
1084
|
+
| `undeleteRowByIndex(idx)` | `Promise<void>` | sourceIndex로 삭제 복원 O(1) |
|
|
1085
|
+
| `undeleteRows(rows)` | `Promise<number>` | 행 객체 배열로 다중 삭제 복원 |
|
|
1086
|
+
| `undeleteRowsById(ids)` | `Promise<number>` | ID 배열로 다중 삭제 복원 |
|
|
1087
|
+
| `deleteSelectedRows()` | `Promise<number>` | 선택 행 삭제 (삭제된 건수 반환). 참고: `grid.rows.deleteSelectedRows()`와 동작은 동일하지만 ChangesAPI 쪽 메서드 이름은 `deleteSelectedRows`로 다르다 |
|
|
1088
|
+
|
|
1089
|
+
#### 변경 취소
|
|
1090
|
+
|
|
1091
|
+
| 메서드 | 반환 | 설명 |
|
|
1092
|
+
|--------|------|------|
|
|
1093
|
+
| `discard(row)` | `Promise<void>` | 행 객체로 변경 취소 (`enableReferCache` 필요) |
|
|
1094
|
+
| `discardById(id)` | `Promise<void>` | ID로 단일 행 변경 취소 |
|
|
1095
|
+
| `discardByIndex(idx)` | `Promise<void>` | sourceIndex로 변경 취소 O(1) |
|
|
1096
|
+
| `discardSelectedRows()` | `Promise<number>` | 선택 행 변경 취소 |
|
|
1097
|
+
| `discardImmediate(row)` | `Promise<void>` | 행 객체로 즉시 취소 (`enableReferCache` 필요) |
|
|
1098
|
+
| `discardImmediateById(id)` | `Promise<void>` | ID로 즉시 취소 (undo 스택 미경유) |
|
|
1099
|
+
| `discardImmediateByIndex(idx)` | `Promise<void>` | sourceIndex로 즉시 취소 O(1) |
|
|
1100
|
+
| `discardAll()` | `Promise<void>` | 전체 변경 취소 |
|
|
1101
|
+
|
|
1102
|
+
#### 커밋
|
|
1103
|
+
|
|
1104
|
+
| 메서드 | 반환 | 설명 |
|
|
1105
|
+
|--------|------|------|
|
|
1106
|
+
| `commit(options?)` | `Promise<void>` | 전체 변경 커밋 |
|
|
1107
|
+
|
|
1108
|
+
**`CommitOptions`**
|
|
1109
|
+
|
|
1110
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1111
|
+
|------|------|--------|------|
|
|
1112
|
+
| `allowInvalid` | `boolean` | `false` | `true`면 유효성 검사를 건너뛰고 강제 commit. 데이터 정합성은 사용자 책임. |
|
|
1113
|
+
|
|
1114
|
+
**commit 가드 흐름**
|
|
1115
|
+
|
|
1116
|
+
1. **keyField 충돌 검증** — ADR-105 `KeyIndex.validateAllKeys()`가 모든 dirty 행의 키 중복을 단일 pass로 검사. 충돌 발생 시 해당 행의 **모든 keyField 셀**에 에러 마킹 (양방향).
|
|
1117
|
+
2. **사용자 validation rule 검증** — `commit` trigger가 붙은 규칙을 전체 visible row에 대해 재실행.
|
|
1118
|
+
3. **에러 집합 체크** — `validationManager.hasErrors()`가 `true`면:
|
|
1119
|
+
- `deps.jumpToFirstInvalid?.()` 호출 → 첫 invalid 셀로 자동 스크롤 + 선택
|
|
1120
|
+
- `PS715` throw (`"유효하지 않은 셀이 {count}개 있습니다..."`)
|
|
1121
|
+
4. 에러 없으면 `update → delete → add` 순으로 실제 `sourceData` 반영.
|
|
1122
|
+
|
|
1123
|
+
**`allowInvalid: true`로 우회**
|
|
1124
|
+
|
|
1125
|
+
```js
|
|
1126
|
+
// 정상: invalid가 있으면 PS715 throw + 첫 invalid로 이동
|
|
1127
|
+
await grid.changes.commit();
|
|
1128
|
+
|
|
1129
|
+
// 우회: validation 전체 skip (key 충돌 검사도 skip)
|
|
1130
|
+
await grid.changes.commit({ allowInvalid: true });
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
#### 조회
|
|
1134
|
+
|
|
1135
|
+
| 속성/메서드 | 반환 | 설명 |
|
|
1136
|
+
|-------------|------|------|
|
|
1137
|
+
| `hasChanges` | `boolean` | 변경 사항 존재 여부 |
|
|
1138
|
+
| `getAll()` | `ChangesSummary` | 전체 변경 요약 |
|
|
1139
|
+
| `getRowState(row)` | `RowState` | 행 객체로 변경 상태 조회 (`enableReferCache` 필요) |
|
|
1140
|
+
| `getRowStateById(id)` | `RowState` | ID로 행 변경 상태 조회 |
|
|
1141
|
+
| `getRowStateByIndex(idx)` | `RowState` | sourceIndex로 행 변경 상태 조회 O(1) |
|
|
1142
|
+
|
|
1143
|
+
#### 대량 업데이트
|
|
1144
|
+
|
|
1145
|
+
| 메서드 | 반환 | 설명 |
|
|
1146
|
+
|--------|------|------|
|
|
1147
|
+
| `bulkUpdate(columnId, value, scope?)` | `Promise<number>` | 일괄 셀 업데이트 |
|
|
1148
|
+
|
|
1149
|
+
#### 행 재정렬
|
|
1150
|
+
|
|
1151
|
+
| 메서드 | 반환 | 설명 |
|
|
1152
|
+
|--------|------|------|
|
|
1153
|
+
| `reorderRow(from, to)` | `Promise<void>` | 행 순서 변경 |
|
|
1154
|
+
| `getRowOrder()` | `(string \| number)[] \| null` | 행 순서 조회 |
|
|
1155
|
+
| `isReordered()` | `boolean` | 재정렬 여부 |
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
### grid.undoManager — Undo/Redo API
|
|
1160
|
+
|
|
1161
|
+
| 메서드/속성 | 반환 | 설명 |
|
|
1162
|
+
|-------------|------|------|
|
|
1163
|
+
| `undo()` | `boolean` | 실행 취소 (스택 top 1개) |
|
|
1164
|
+
| `redo()` | `boolean` | 다시 실행 |
|
|
1165
|
+
| `undoThrough(commandId)` | `CommandId[]` | **특정 command까지 cascade undo** — 낙관적 파이프라인 롤백용. 아래 참조 |
|
|
1166
|
+
| `canUndo` | `boolean` | Undo 가능 여부 |
|
|
1167
|
+
| `canRedo` | `boolean` | Redo 가능 여부 |
|
|
1168
|
+
| `beginBatch(desc?, opts?)` | `void` | 배치 시작 |
|
|
1169
|
+
| `endBatch()` | `Promise<void>` | 배치 종료 |
|
|
1170
|
+
| `batch(fn, opts?)` | `Promise<void>` | 콜백 배치 |
|
|
1171
|
+
| `getStack()` | `UndoStack` | 내부 스택 조회 |
|
|
1172
|
+
|
|
1173
|
+
> **`undoThrough(commandId): CommandId[]`** — target command와 **그 위(더 최신)의 모든 command**를 top→target 순으로 cascade undo합니다 (target 포함). 되돌린 command id 배열(top→target 순)을 동기 반환합니다.
|
|
1174
|
+
> - **용도**: 낙관적(optimistic) 백엔드 동기화에서 여러 reorder가 파이프라인(in-flight)일 때, 그중 하나가 **실패**하면 롤백해야 합니다. 위치 기반 `undo()`는 스택 top(엉뚱한 최신 작업)을 되돌리지만, `undoThrough(실패한 commandId)`는 그 작업과 그 위에 쌓인 것 전부를 정확히 되돌립니다 (그 위 작업들은 실패한 작업 위에 놓였으므로 함께 무효).
|
|
1175
|
+
> - **correlation key**: `commandId`는 `row:reordered` / `changes:undone` / `changes:redone` 이벤트가 주는 값을 그대로 넘깁니다. 이 값은 reorder를 감싼 **BatchCommand.id**입니다 (R1 — inner command id 아님).
|
|
1176
|
+
> - **idempotent**: 미존재 id / 빈 스택 / 열린 batch / inner(child) id → `[]` 반환 (throw 없음). 이미 롤백된 id를 또 불러도 안전.
|
|
1177
|
+
> - cascade는 command마다 `changes:undone`을 재발화하므로, 그 작업들의 **백엔드 롤백도 자동으로 함께 태워집니다** (핸들러 무수정).
|
|
1178
|
+
>
|
|
1179
|
+
> ```js
|
|
1180
|
+
> const pending = new Map();
|
|
1181
|
+
> grid.on('row:reordered', async (e) => {
|
|
1182
|
+
> pending.set(e.commandId, e);
|
|
1183
|
+
> try {
|
|
1184
|
+
> await api.applyReorder(e.patches); // 낙관적: UI는 이미 반영됨
|
|
1185
|
+
> pending.delete(e.commandId);
|
|
1186
|
+
> } catch {
|
|
1187
|
+
> // 실패한 그 작업 + 그 위 전부 cascade 롤백
|
|
1188
|
+
> const rolledBack = grid.undoManager.undoThrough(e.commandId);
|
|
1189
|
+
> rolledBack.forEach((id) => pending.delete(id));
|
|
1190
|
+
> // 백엔드 롤백은 changes:undone이 cascade 개수만큼 자동 발화 → 별도 핸들러가 처리
|
|
1191
|
+
> }
|
|
1192
|
+
> });
|
|
1193
|
+
> ```
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
1197
|
+
### grid.rowGroup — 그룹화 API
|
|
1198
|
+
|
|
1199
|
+
> 정식 네임스페이스는 `grid.rowGroup`입니다. 구 `grid.group`은 **deprecated 별칭**으로 계속 동작하나(하위호환, 다음 major에서 제거 예정) 신규 코드는 `grid.rowGroup`을 사용하세요.
|
|
1200
|
+
|
|
1201
|
+
> `getConfig()`는 내부 상태와 동일 참조를 반환합니다. `aggregates`에 커스텀 함수가 포함될 수 있어 `structuredClone`을 사용하지 않습니다 (DataCloneError 방지). 반환값을 직접 수정하지 마세요 (변경은 `setConfig()` 사용).
|
|
1202
|
+
> 그룹화 미설정 시 `getConfig()`는 `null` 반환.
|
|
1203
|
+
|
|
1204
|
+
> **토글 이벤트** — 헤더 클릭 및 프로그래매틱 토글 모두 이벤트를 발화합니다: 단일 토글(`toggle`/`expand`/`collapse`)은 `rowGroup:toggle`, 여러 그룹 일괄 토글은 `rowGroup:batchToggle` 하나로 통합되어 `scope` 필드로 구분합니다 — 동일 depth 일괄(`expandAtLevel`/`collapseAtLevel`)은 `scope:'level'`, 특정 depth까지 누적(`expandToLevel`/`collapseToLevel`)은 `scope:'toLevel'`, 전체(`expandAll`/`collapseAll`)는 `scope:'all'`. 프로그래매틱 호출은 payload `source:'api'`로 발화되므로 핸들러에서 `if (e.source === 'api') return;`으로 재진입 무한루프를 막을 수 있습니다 (→ [이벤트](#이벤트)).
|
|
1205
|
+
> **참고**: `expandToLevel`/`collapseToLevel`(레벨까지 누적)은 `rowGroup:batchToggle`을 `scope:'toLevel'`로 발화합니다.
|
|
1206
|
+
|
|
1207
|
+
| 메서드 | 반환 | 설명 |
|
|
1208
|
+
|--------|------|------|
|
|
1209
|
+
| `setConfig(config)` | `Promise<void>` | 그룹화 설정 (`null`로 해제) |
|
|
1210
|
+
| `groupBy(columns)` | `Promise<void>` | 컬럼으로 그룹화 |
|
|
1211
|
+
| `clear()` | `Promise<void>` | 그룹화 해제 |
|
|
1212
|
+
| `getConfig()` | `GroupingConfig \| null` | 현재 그룹화 설정 조회 (내부 참조) |
|
|
1213
|
+
| `toggle(groupId)` | `Promise<void>` | 그룹 접힘/펼침 토글 |
|
|
1214
|
+
| `expand(groupId)` | `Promise<void>` | 특정 그룹 펼침 |
|
|
1215
|
+
| `collapse(groupId)` | `Promise<void>` | 특정 그룹 접힘 |
|
|
1216
|
+
| `expandAll()` | `Promise<void>` | 전체 그룹 펼침 |
|
|
1217
|
+
| `collapseAll()` | `Promise<void>` | 전체 그룹 접힘 |
|
|
1218
|
+
| `isCollapsed(groupId)` | `boolean` | 접힘 상태 조회 |
|
|
1219
|
+
| `addColumn(columnId)` | `Promise<void>` | 그룹 컬럼 추가 |
|
|
1220
|
+
| `removeColumn(columnId)` | `Promise<void>` | 그룹 컬럼 제거 |
|
|
1221
|
+
| `expandToLevel(level)` | `Promise<void>` | 특정 레벨까지 펼침 (`level < 0` 시 경고) |
|
|
1222
|
+
| `collapseToLevel(level)` | `Promise<void>` | 특정 레벨까지 접힘 (`level < 0` 시 경고) |
|
|
1223
|
+
| `expandAtLevel(level)` | `Promise<void>` | 정확히 그 레벨의 그룹만 펼침 — 다른 레벨 보존 (Ctrl/Cmd+클릭 제스처와 동일, `level < 0` 시 경고) |
|
|
1224
|
+
| `collapseAtLevel(level)` | `Promise<void>` | 정확히 그 레벨의 그룹만 접힘 — 다른 레벨 보존 (`level < 0` 시 경고) |
|
|
1225
|
+
| `getGroupRows(groupId)` | `RowId[]` | 그룹에 속한 데이터 행 rowId 목록. `groupId`는 `"col:val/col:val"` 경로 문자열(= `GroupRowContext.groupId`). 소스 전체 스캔이라 **접힘/필터 무관**, 중첩 그룹이면 하위 전체 포함. 미존재/규약 위반 시 `[]` |
|
|
1226
|
+
| `expandToRow(rowId)` | `Promise<void>` | 특정 data 행이 보이도록 그 행의 **조상 그룹을 모두 펼친다**(`tree.expandPath`의 그룹 대칭). `grid.rows.getDisplayedIndexById(rowId)`가 `-1`(접힘으로 숨김)일 때 이 메서드로 조상을 펼친 뒤 재조회하면 유효한 displayed 인덱스를 얻는다. 그룹 모드 아님/행 없음 시 no-op. O(그룹 depth) |
|
|
1227
|
+
|
|
1228
|
+
> **`getGroupRows(groupId)`** — 그룹 행에서 하위 멤버 전체를 일괄 조작할 때 사용합니다 (예: 그룹 단위 선택·체크박스 cascade). 멤버십은 그룹 컬럼 값 기준 매칭(`String(row[col]) === value`)이며, `groupId`의 각 세그먼트는 **첫 `:` 만** 구분자로 사용하므로 값에 `:`가 포함돼도 안전합니다(값에 `/` 포함은 경로 모호성 — 규약상 한계). `grid.rows.getByIds(getGroupRows(id))` 조합으로 일괄 수정하세요.
|
|
1229
|
+
|
|
1230
|
+
---
|
|
1231
|
+
|
|
1232
|
+
### grid.tree — 트리 API
|
|
1233
|
+
|
|
1234
|
+
> 트리 미설정 시 `getConfig()`는 `null` 반환.
|
|
1235
|
+
> `expandToLevel`/`collapseToLevel`에 음수 전달 시 `psWarn(PS311)` 경고 후 무시.
|
|
1236
|
+
|
|
1237
|
+
| 메서드 | 반환 | 설명 |
|
|
1238
|
+
|--------|------|------|
|
|
1239
|
+
| `setConfig(config)` | `Promise<void>` | 트리 설정 (`null`로 해제) |
|
|
1240
|
+
| `getConfig()` | `TreeConfig \| null` | 트리 설정 조회 |
|
|
1241
|
+
| `loadData(data, config)` | `Promise<void>` | 중첩 데이터 로드 → flat 변환 |
|
|
1242
|
+
| `toggle(nodeId)` | `Promise<void>` | 노드 접힘/펼침 |
|
|
1243
|
+
| `expand(nodeId)` | `Promise<void>` | 노드 펼침 |
|
|
1244
|
+
| `collapse(nodeId)` | `Promise<void>` | 노드 접힘 |
|
|
1245
|
+
| `expandAll()` | `Promise<void>` | 전체 펼침 |
|
|
1246
|
+
| `collapseAll()` | `Promise<void>` | 전체 접힘 |
|
|
1247
|
+
| `expandToLevel(level)` | `Promise<void>` | 특정 레벨까지 펼침 |
|
|
1248
|
+
| `collapseToLevel(level)` | `Promise<void>` | 특정 레벨까지 접힘 |
|
|
1249
|
+
| `expandAtLevel(level)` | `Promise<void>` | 정확히 그 레벨의 노드만 펼침 — 다른 레벨 보존 (Ctrl/Cmd+클릭 제스처와 동일) |
|
|
1250
|
+
| `collapseAtLevel(level)` | `Promise<void>` | 정확히 그 레벨의 노드만 접힘 — 다른 레벨 보존 |
|
|
1251
|
+
| `isCollapsed(nodeId)` | `boolean` | 접힘 상태 조회 |
|
|
1252
|
+
| `expandPath(nodeId)` | `Promise<void>` | 조상 전체 펼치기 (검색 결과 표시용) |
|
|
1253
|
+
| `moveNode(nodeId, newParentId, newIndex?)` | `Promise<void>` | **프로그램 방식 트리 노드 이동** (드래그 없이 코드로). flat 그리드 `changes.reorderRow`의 tree 대칭 API. 아래 참조 |
|
|
1254
|
+
| `getChildren(nodeId)` | `RowId[]` | 직계 자식 rowId 목록 (소스 순서). 리프/미존재 시 `[]` |
|
|
1255
|
+
| `getDescendants(nodeId)` | `RowId[]` | 모든 자손 rowId (BFS — 얕은 레벨 먼저). 순환 데이터에도 종료 보장 |
|
|
1256
|
+
| `getParent(nodeId)` | `RowId \| null` | 부모 rowId. root/미존재 시 `null` |
|
|
1257
|
+
| `getAncestors(nodeId)` | `RowId[]` | 조상 rowId 목록 (가까운 부모부터 root 방향). root/미존재 시 `[]` |
|
|
1258
|
+
|
|
1259
|
+
> **트래버설 (`getChildren` / `getDescendants` / `getParent` / `getAncestors`)** — `treeConfig.idField`/`parentField` 값 기준으로 **소스 전체를 스캔**하므로 접힘·필터·정렬 상태와 무관하게 논리적 트리 구조를 반환합니다. 반환값은 rowId 배열(원형 유지)이며 `grid.rows.getByIds(...)`와 조합해 일괄 수정에 사용합니다 (부모↔자손 체크박스 cascade 등). `treeConfig` 미설정 시 모두 빈 결과. 조회당 O(N) — 프레임 내 반복 호출은 피하세요.
|
|
1260
|
+
|
|
1261
|
+
> **`moveNode(nodeId, newParentId, newIndex?)`** — `nodeId`를 `newParentId`의 `newIndex`번째 자식으로 이동합니다.
|
|
1262
|
+
> - `newIndex`는 **이동 완료 후 최종 위치**(0-based). 같은 부모 안 forward 이동의 off-by-one은 내부에서 보정됩니다.
|
|
1263
|
+
> - `newParentId = null` → root 레벨로 승격. `newIndex` 생략 → 대상 부모의 맨 끝에 append. 범위 밖 값은 `[0, 자식수]`로 clamp.
|
|
1264
|
+
> - 드래그 경로와 **동일한 공통 코어**를 타므로 하나의 원자 undo 단위(Ctrl+Z 1회 복원)로 커밋되고 `row:reordered {kind:'tree'}` 이벤트를 발화합니다 (`commandId` 포함).
|
|
1265
|
+
> - **드래그와 달리 조용한 no-op이 아니라 명시적 throw** — 프로그래머 실수를 표면화합니다: `PS401`(nodeId 부재), `PS402`(newParentId 부재), `PS403`(cycle — 자기 자신 또는 자기 후손으로 이동).
|
|
1266
|
+
> - 트리 미설정(`treeConfig` 부재) 시 호출하면 동작하지 않습니다.
|
|
1267
|
+
>
|
|
1268
|
+
> ```js
|
|
1269
|
+
> // [A, B, C, D]에서 A를 최종 index 2로 → [B, C, A, D]
|
|
1270
|
+
> await grid.tree.moveNode('A', null, 2);
|
|
1271
|
+
> // 자식 노드를 다른 부모의 맨 끝으로
|
|
1272
|
+
> await grid.tree.moveNode('leaf1', 'folder2');
|
|
1273
|
+
> ```
|
|
1274
|
+
|
|
1275
|
+
---
|
|
1276
|
+
|
|
1277
|
+
### grid.pivot — 피벗 API
|
|
1278
|
+
|
|
1279
|
+
> 피벗 미설정 시 `getConfig()`, `getResult()`, `getState()` 모두 `null` 반환.
|
|
1280
|
+
> `setConfig(config)`에 빈 `rowFields`/`valueFields` 전달 시 `psWarn(PS201/PS202)` 경고만 출력하고 throw 하지 않습니다 — 피벗을 유지한 채 결과를 비워 렌더합니다(플랫 전환 X).
|
|
1281
|
+
> `getConfig()`는 내부 객체와 동일 참조를 반환합니다. `mask`에 함수가 포함될 수 있어 `structuredClone`을 사용하지 않습니다. 반환값을 직접 수정하지 마세요 (필요하면 호출 측에서 복사).
|
|
1282
|
+
|
|
1283
|
+
#### 모드 관리
|
|
1284
|
+
|
|
1285
|
+
| 메서드 | 반환 | 설명 |
|
|
1286
|
+
|--------|------|------|
|
|
1287
|
+
| `getMode()` | `'flat' \| 'pivot'` | 현재 모드 |
|
|
1288
|
+
| `setMode(mode)` | `Promise<void>` | 모드 전환 |
|
|
1289
|
+
|
|
1290
|
+
#### 설정
|
|
1291
|
+
|
|
1292
|
+
| 메서드 | 반환 | 설명 |
|
|
1293
|
+
|--------|------|------|
|
|
1294
|
+
| `setConfig(config)` | `Promise<void>` | 피벗 설정 |
|
|
1295
|
+
| `getConfig()` | `PivotConfig \| null` | 피벗 설정 조회 (내부 참조) |
|
|
1296
|
+
| `getResult()` | `PivotResult \| null` | 피벗 결과 |
|
|
1297
|
+
| `setSubTotals(options)` | `Promise<void>` | 행/열 소계 설정 (show, fields, position) |
|
|
1298
|
+
| `setGrandTotals(options)` | `Promise<void>` | 행/열 총합계 설정 (show, position) |
|
|
1299
|
+
| `setRowFields(fields)` | `Promise<void>` | 행 필드만 변경 |
|
|
1300
|
+
| `setColumnFields(fields)` | `Promise<void>` | 열 필드만 변경 |
|
|
1301
|
+
| `setValueFields(fields)` | `Promise<void>` | 값 필드만 변경 |
|
|
1302
|
+
|
|
1303
|
+
#### 상태 저장/복원
|
|
1304
|
+
|
|
1305
|
+
| 메서드 | 반환 | 설명 |
|
|
1306
|
+
|--------|------|------|
|
|
1307
|
+
| `getState()` | `{ config, collapsedRowNodes: string[], collapsedColumnNodes: string[] } \| null` | 피벗 상태 |
|
|
1308
|
+
| `setState(state)` | `Promise<void>` | 피벗 상태 복원 (`collapsedRowNodes` / `collapsedColumnNodes`는 선택) |
|
|
1309
|
+
|
|
1310
|
+
#### 노드 네비게이션 (Row 축)
|
|
1311
|
+
|
|
1312
|
+
> 행/열을 독립적으로 접기 위해 각각 `Row` / `Column` 접미사가 붙은 메서드를 사용합니다.
|
|
1313
|
+
>
|
|
1314
|
+
> **`PivotConfig.rowCollapsible` / `columnCollapsible`** 옵션 (default `true`) 으로 사용자 클릭 토글을 끌 수 있습니다 — 옵션 `false` 면 chevron 미표시 + 클릭/키보드 토글 무시. 단 **아래 프로그래매틱 API 는 옵션과 무관하게 작동** (개발자가 직접 호출하는 path 는 유지). "사용자 클릭만 막기" 가 의도.
|
|
1315
|
+
|
|
1316
|
+
| 메서드 | 반환 | 설명 |
|
|
1317
|
+
|--------|------|------|
|
|
1318
|
+
| `toggleRow(nodeId)` | `Promise<void>` | 행 노드 토글 |
|
|
1319
|
+
| `expandRow(nodeId)` | `Promise<void>` | 행 노드 펼침 |
|
|
1320
|
+
| `collapseRow(nodeId)` | `Promise<void>` | 행 노드 접힘 |
|
|
1321
|
+
| `expandAllRows()` | `Promise<void>` | 모든 행 노드 펼침 |
|
|
1322
|
+
| `collapseAllRows()` | `Promise<void>` | 모든 행 노드 접힘 |
|
|
1323
|
+
| `expandRowAtLevel(level)` | `Promise<void>` | 해당 레벨의 노드만 펼침 |
|
|
1324
|
+
| `collapseRowAtLevel(level)` | `Promise<void>` | 해당 레벨의 노드만 접힘 |
|
|
1325
|
+
| `expandRowToLevel(level)` | `Promise<void>` | 지정 레벨까지 펼침 |
|
|
1326
|
+
| `collapseRowToLevel(level)` | `Promise<void>` | 지정 레벨까지 접힘 |
|
|
1327
|
+
| `isRowCollapsed(nodeId)` | `boolean` | 행 접힘 상태 |
|
|
1328
|
+
| `getRowViewIndex(nodeId)` | `Promise<number \| null>` | nodeId 의 현재 가시(view) 행 인덱스. 없거나 숨김이면 `null` (인덱스 기반 스크롤 앵커용) |
|
|
1329
|
+
| `getCollapsedRowNodes()` | `ReadonlySet<string>` | 접힌 행 노드 목록 |
|
|
1330
|
+
|
|
1331
|
+
#### 노드 네비게이션 (Column 축)
|
|
1332
|
+
|
|
1333
|
+
| 메서드 | 반환 | 설명 |
|
|
1334
|
+
|--------|------|------|
|
|
1335
|
+
| `toggleColumn(nodeId)` | `Promise<void>` | 열 노드 토글 |
|
|
1336
|
+
| `expandColumn(nodeId)` | `Promise<void>` | 열 노드 펼침 |
|
|
1337
|
+
| `collapseColumn(nodeId)` | `Promise<void>` | 열 노드 접힘 |
|
|
1338
|
+
| `expandAllColumns()` | `Promise<void>` | 모든 열 노드 펼침 |
|
|
1339
|
+
| `collapseAllColumns()` | `Promise<void>` | 모든 열 노드 접힘 |
|
|
1340
|
+
| `expandColumnAtLevel(level)` | `Promise<void>` | 해당 레벨의 노드만 펼침 |
|
|
1341
|
+
| `collapseColumnAtLevel(level)` | `Promise<void>` | 해당 레벨의 노드만 접힘 |
|
|
1342
|
+
| `expandColumnToLevel(level)` | `Promise<void>` | 지정 레벨까지 펼침 |
|
|
1343
|
+
| `collapseColumnToLevel(level)` | `Promise<void>` | 지정 레벨까지 접힘 |
|
|
1344
|
+
| `isColumnCollapsed(nodeId)` | `boolean` | 열 접힘 상태 |
|
|
1345
|
+
| `getCollapsedColumnNodes()` | `ReadonlySet<string>` | 접힌 열 노드 목록 |
|
|
1346
|
+
|
|
1347
|
+
#### 소스 행 매핑 / 차트 보조
|
|
1348
|
+
|
|
1349
|
+
| 메서드 | 반환 | 설명 |
|
|
1350
|
+
|--------|------|------|
|
|
1351
|
+
| `getSourceRowsForCell(rowIdx, colId)` | `Promise<number[]>` | 셀의 소스 행 (`trackSourceRows: true` 필요) |
|
|
1352
|
+
| `getSourceRowsForRow(rowIdx)` | `Promise<number[]>` | 행의 소스 행 (`trackSourceRows: true` 필요) |
|
|
1353
|
+
| `getSourceRowsForSelection()` | `Promise<number[]>` | 선택 영역의 소스 행 (`trackSourceRows: true` 필요) |
|
|
1354
|
+
| `getGrandTotals()` | `Promise<PivotGrandTotal[]>` | 값 필드별 전체 총합계(코너). 현재 필터 반영, 표시 on/off 무관 상시 계산. 비피벗이면 `[]` |
|
|
1355
|
+
| `getColumnGrandTotals(columnIds?)` | `Promise<PivotColumnGrandTotal[]>` | 컬럼별 행 총합계. `columnIds` 생략 시 전체 데이터 컬럼. 표시 on/off 무관. 비피벗이면 `[]` |
|
|
1356
|
+
| `getFieldValues(fieldId, options?)` | `Promise<CellValue[]>` | 필드의 값 목록 (행/열 자동 판별). `options.unique` 기본 `true` |
|
|
1357
|
+
| `getAllRows()` | `Promise<PivotRow[]>` | lazy 모드에서도 전체 피벗 행을 가져옴 |
|
|
1358
|
+
| `getDataMatrix(options?)` | `Promise<DataMatrix \| null>` | 피벗 결과를 2D 표로 변환 |
|
|
1359
|
+
| `getPivotMeta()` | `PivotMeta \| null` | 읽기 전용 경량 메타 스냅샷 (필드/행수/열수) |
|
|
1360
|
+
| `getSeriesData(options?)` | `Promise<PivotSeriesData>` | 차트/대시보드용 그룹 시리즈 데이터 |
|
|
1361
|
+
| `getRowHeaderPath(rowHeaders, categoryField?)` | `string` | 행 rowHeaders → 행 헤더 경로(= 차트 카테고리 라벨). `getSeriesData`와 동일 규칙(SSOT). leaf+다중 행필드면 "부모 › 잎" 경로, 그 외엔 해당 필드 값. (sync) |
|
|
1362
|
+
|
|
1363
|
+
#### getFieldValues
|
|
1364
|
+
|
|
1365
|
+
```typescript
|
|
1366
|
+
// 행 필드 값 조회 (async — Promise<CellValue[]>)
|
|
1367
|
+
const regions = await grid.pivot.getFieldValues('region');
|
|
1368
|
+
// → ['서울', '부산', '대구', ...]
|
|
1369
|
+
|
|
1370
|
+
// 열 필드 값 조회
|
|
1371
|
+
const quarters = await grid.pivot.getFieldValues('quarter'); // → ['Q1', 'Q2', 'Q3', 'Q4']
|
|
1372
|
+
|
|
1373
|
+
// 중복 포함 (순서 유지)
|
|
1374
|
+
const raw = await grid.pivot.getFieldValues('quarter', { unique: false });
|
|
1375
|
+
// → ['Q1','Q2','Q3','Q4','Q1','Q2','Q3','Q4'] (연도별 반복)
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
#### getDataMatrix
|
|
1379
|
+
|
|
1380
|
+
피벗 결과를 CSV 내보내기, 차트, 외부 연동에 적합한 2D 표로 변환합니다.
|
|
1381
|
+
|
|
1382
|
+
```typescript
|
|
1383
|
+
const matrix = await grid.pivot.getDataMatrix();
|
|
1384
|
+
// → {
|
|
1385
|
+
// headers: ['지역', '제품', 'Q1', 'Q2', 'Q3', 'Q4'],
|
|
1386
|
+
// rows: [
|
|
1387
|
+
// ['서울', '노트북', 5000, 3000, 4000, 6000],
|
|
1388
|
+
// ['서울', '마우스', 2000, 1500, 1800, 2200],
|
|
1389
|
+
// ...
|
|
1390
|
+
// ],
|
|
1391
|
+
// rowTypes: ['data', 'data', 'subtotal', ...]
|
|
1392
|
+
// }
|
|
1393
|
+
|
|
1394
|
+
// 소계/총합계 제외 (전부 빼기)
|
|
1395
|
+
await grid.pivot.getDataMatrix({ subtotals: 'none', grandTotals: 'none' });
|
|
1396
|
+
|
|
1397
|
+
// 토글 OFF로 숨겨진 소계/총합계까지 전부 포함 (raw)
|
|
1398
|
+
await grid.pivot.getDataMatrix({ subtotals: 'all', grandTotals: 'all' });
|
|
1399
|
+
|
|
1400
|
+
// 열 소계/총합계 컬럼 제외
|
|
1401
|
+
await grid.pivot.getDataMatrix({ excludeStructuralColumns: true });
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
| 옵션 | 타입 | 기본 | 설명 |
|
|
1405
|
+
|------|------|------|------|
|
|
1406
|
+
| `subtotals` | `'all' \| 'visible' \| 'none'` | `'visible'` | 소계 행 노출 (all=숨김 포함 raw, visible=토글 ON만, none=제외) |
|
|
1407
|
+
| `grandTotals` | `'all' \| 'visible' \| 'none'` | `'visible'` | 총합계 행 노출 (값 의미는 `subtotals` 와 동일) |
|
|
1408
|
+
| `excludeStructuralColumns` | `boolean` | `false` | 열 소계/총합계 컬럼 제외 |
|
|
1409
|
+
| `formatted` | `boolean` | `false` | mask 적용된 문자열로 반환 |
|
|
1410
|
+
|
|
1411
|
+
#### getPivotMeta / getSeriesData
|
|
1412
|
+
|
|
1413
|
+
```typescript
|
|
1414
|
+
// 읽기 전용 경량 메타 스냅샷
|
|
1415
|
+
const meta = grid.pivot.getPivotMeta();
|
|
1416
|
+
// → { rowFields: [{ field, header }, ...], columnFields, valueFields, rowCount, columnCount }
|
|
1417
|
+
|
|
1418
|
+
// 차트용 시리즈 데이터
|
|
1419
|
+
const series = await grid.pivot.getSeriesData({
|
|
1420
|
+
categoryField: 'region', // 기본: 첫 번째 rowField
|
|
1421
|
+
columnFieldLevel: 0, // X축 레벨 (기본: 모든 열 필드 조합)
|
|
1422
|
+
valueFields: ['sales'], // 기본: 모든 valueFields
|
|
1423
|
+
subtotals: 'all', // 기본 'all' — 부모 카테고리 차트가 소계 토글 무관 동작
|
|
1424
|
+
grandTotals: 'none', // 기본 'none' — 총합계 카테고리는 'all'/'visible' 지정 시 추가
|
|
1425
|
+
});
|
|
1426
|
+
// → { xLabels: ['Q1','Q2','Q3','Q4'], categories: [{ name: '서울', series: [{ name:'sales', data:[...] }, ...] }, ...] }
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
### grid.masterDetail — 마스터-디테일 API
|
|
1432
|
+
|
|
1433
|
+
> 디테일 미설정 시 `getConfig()`는 `null` 반환.
|
|
1434
|
+
> `singleExpand` 모드에서 `expandAll()` 호출 시 `psWarn(PS310)` 경고 후 무시.
|
|
1435
|
+
|
|
1436
|
+
| 메서드 | 반환 | 설명 |
|
|
1437
|
+
|--------|------|------|
|
|
1438
|
+
| `setConfig(config)` | `Promise<void>` | 디테일 설정 (`null`로 해제, 모든 cleanup 실행) |
|
|
1439
|
+
| `getConfig()` | `MasterDetailConfig \| null` | 디테일 설정 조회 |
|
|
1440
|
+
| `toggle(dataIndex)` | `Promise<void>` | 디테일 토글 |
|
|
1441
|
+
| `expand(dataIndex)` | `Promise<void>` | 디테일 펼침 |
|
|
1442
|
+
| `collapse(dataIndex)` | `Promise<void>` | 디테일 접힘 |
|
|
1443
|
+
| `expandAll()` | `Promise<void>` | 전체 디테일 펼침 (singleExpand 시 무시) |
|
|
1444
|
+
| `collapseAll()` | `Promise<void>` | 전체 디테일 접힘 |
|
|
1445
|
+
| `isExpanded(dataIndex)` | `boolean` | 확장 상태 조회 |
|
|
1446
|
+
|
|
1447
|
+
---
|
|
1448
|
+
|
|
1449
|
+
### grid.validation — 검증 API
|
|
1450
|
+
|
|
1451
|
+
#### 검증 실행 / 전역 상태
|
|
1452
|
+
|
|
1453
|
+
| 메서드 | 반환 | 설명 |
|
|
1454
|
+
|--------|------|------|
|
|
1455
|
+
| `validateAll(trigger?)` | `ValidationError[]` | 전체 검증 실행. `trigger`는 `'blur' \| 'commit' \| 'change' \| 'input'` (기본 `'commit'` — 내부적으로 `['blur', 'commit']` 둘 다 실행) |
|
|
1456
|
+
| `getErrors()` | `ValidationError[]` | 현재 `errorMap`에 쌓인 모든 검증 에러 (`[{ rowId, field, messages }, ...]`) |
|
|
1457
|
+
| `hasErrors()` | `boolean` | 에러 존재 여부 (O(1)) |
|
|
1458
|
+
| `clear()` | `Promise<void>` | 전체 에러 초기화 + refresh |
|
|
1459
|
+
|
|
1460
|
+
#### 셀/행 단위 조회
|
|
1461
|
+
|
|
1462
|
+
| 메서드 | 반환 | 설명 |
|
|
1463
|
+
|--------|------|------|
|
|
1464
|
+
| `getCellErrors(rowId, field)` | `string[] \| undefined` | 특정 셀의 에러 메시지 배열 (에러 없으면 `undefined`) |
|
|
1465
|
+
| `hasCellError(rowId, field)` | `boolean` | 특정 셀 에러 존재 여부 (O(1)) |
|
|
1466
|
+
| `getRowErrors(rowId)` | `Map<string, string[]> \| undefined` | 한 행의 모든 에러를 `field → messages` 맵으로 반환 (에러 없으면 `undefined`). 내부 상태의 안전한 복사본이라 mutate해도 원본에 영향 없음 |
|
|
1467
|
+
|
|
1468
|
+
#### 외부 에러 주입
|
|
1469
|
+
|
|
1470
|
+
커스텀 validation rule만으로는 표현하기 어려운 제약(예: "이미 등록된 이메일", "재고 초과" 등 **서버 측 검증 결과**)을 그리드에 반영할 때 사용합니다. 한번 주입된 에러는 `ps-cell-invalid` 클래스 자동 적용, `nextInvalid/prevInvalid` 탐색 포함, `commit()` 가드의 차단 대상으로 동일하게 처리됩니다.
|
|
1471
|
+
|
|
1472
|
+
| 메서드 | 반환 | 설명 |
|
|
1473
|
+
|--------|------|------|
|
|
1474
|
+
| `setCellErrors(rowId, field, messages)` | `void` | 특정 셀의 에러 메시지를 설정. 빈 배열(`[]`)을 넘기면 해당 셀의 에러가 제거됨 |
|
|
1475
|
+
| `clearCellErrors(rowId, field)` | `void` | 특정 셀의 에러만 초기화 |
|
|
1476
|
+
| `clearRowErrors(rowId)` | `void` | 특정 행의 모든 에러 초기화 |
|
|
1477
|
+
|
|
1478
|
+
#### Invalid 셀 네비게이션
|
|
1479
|
+
|
|
1480
|
+
| 메서드 | 반환 | 설명 |
|
|
1481
|
+
|--------|------|------|
|
|
1482
|
+
| `nextInvalid(options?)` | `InvalidCell \| null` | 현재 선택 셀 **이후**의 첫 invalid 셀로 스크롤 + 선택 후 좌표 반환 |
|
|
1483
|
+
| `prevInvalid(options?)` | `InvalidCell \| null` | 현재 선택 셀 **이전**의 첫 invalid 셀로 스크롤 + 선택 후 좌표 반환 |
|
|
1484
|
+
|
|
1485
|
+
**`NavigateInvalidOptions`**
|
|
1486
|
+
|
|
1487
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1488
|
+
|------|------|--------|------|
|
|
1489
|
+
| `wrap` | `boolean` | `true` | 끝에 도달하면 반대쪽으로 순환 |
|
|
1490
|
+
| `fromStart` | `boolean` | `false` | `true`면 현재 선택 셀을 무시하고 `next`는 첫 invalid, `prev`는 마지막 invalid부터 시작 |
|
|
1491
|
+
|
|
1492
|
+
**`InvalidCell` 반환 타입**
|
|
1493
|
+
|
|
1494
|
+
```typescript
|
|
1495
|
+
interface InvalidCell {
|
|
1496
|
+
rowId: string | number;
|
|
1497
|
+
field: string;
|
|
1498
|
+
}
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
**특징**
|
|
1502
|
+
|
|
1503
|
+
- **visible order 준수** — 필터/정렬/컬럼 재정렬/숨김 상태를 모두 반영한 화면 순서로 순회합니다.
|
|
1504
|
+
- **rowId → effectiveIndex 변환** — 내부적으로 `BaseEngine.getReverseEffectiveMap()`을 사용해 O(1) 좌표 해석.
|
|
1505
|
+
- **dirty 추가 행 지원** — `dirtyAdded`의 row에 발생한 invalid도 정상 좌표로 포함.
|
|
1506
|
+
- **숨겨진 컬럼/필터된 행은 자동 제외** — hidden column의 invalid는 네비게이션 대상에서 빠집니다.
|
|
1507
|
+
- **commit 실패 시 자동 점프** — `grid.changes.commit()`이 invalid를 감지하면 `nextInvalid({ fromStart: true })`를 내부적으로 호출합니다.
|
|
1508
|
+
|
|
1509
|
+
**엣지 케이스**
|
|
1510
|
+
|
|
1511
|
+
- invalid가 0개 → `null`
|
|
1512
|
+
- anchor 셀 없음 → `next`는 첫 번째 invalid, `prev`는 마지막 invalid 반환
|
|
1513
|
+
- anchor가 invalid 셀 자체 → `next`는 **건너뛰고** 다음 invalid로 이동 (strict greater)
|
|
1514
|
+
- anchor 행이 필터로 사라진 경우 → `fromStart`와 동일한 동작
|
|
1515
|
+
- `wrap: false`에서 끝 도달 → `null`
|
|
1516
|
+
|
|
1517
|
+
---
|
|
1518
|
+
|
|
1519
|
+
### grid.clipboard — 클립보드 API
|
|
1520
|
+
|
|
1521
|
+
| 메서드 | 반환 | 설명 |
|
|
1522
|
+
|--------|------|------|
|
|
1523
|
+
| `copy()` | `Promise<string \| null>` | 선택 셀 복사 |
|
|
1524
|
+
| `copy({ includeHeaders: true })` | `Promise<string \| null>` | 헤더 포함 복사 |
|
|
1525
|
+
| `paste()` | `Promise<number>` | 클립보드에서 붙여넣기 (셀 수 반환) |
|
|
1526
|
+
|
|
1527
|
+
#### 키보드 단축키
|
|
1528
|
+
|
|
1529
|
+
| 단축키 | 설명 |
|
|
1530
|
+
|--------|------|
|
|
1531
|
+
| `Ctrl+C` | 선택 셀 복사 |
|
|
1532
|
+
| `Ctrl+Shift+C` | 헤더 포함 복사 |
|
|
1533
|
+
| `Ctrl+V` | 붙여넣기 |
|
|
1534
|
+
|
|
1535
|
+
---
|
|
1536
|
+
|
|
1537
|
+
### grid.headers — 컬럼 그룹 API
|
|
1538
|
+
|
|
1539
|
+
> **용어**: "컬럼 그룹"(`columnGroups`)은 여러 컬럼을 하나의 상위 헤더로 묶는 **다단계 컬럼 헤더(밴딩)**입니다. 데이터 행을 접는 **행 그룹화**(`grid.rowGroup`, `type:'group-row'`)와는 별개 기능입니다. (구 명칭 `headerGroups`/`HeaderGroupDef` → `columnGroups`/`ColumnGroupDef`.)
|
|
1540
|
+
|
|
1541
|
+
| 메서드 | 반환 | 설명 |
|
|
1542
|
+
|--------|------|------|
|
|
1543
|
+
| `setColumnGroups(defs)` | `void` | 컬럼 그룹 정의(`ColumnGroupDef[]`)를 런타임에 설정/교체. `undefined` 전달 시 해제 |
|
|
1544
|
+
| `hasGroups()` | `boolean` | 컬럼 그룹이 설정되어 있는지 여부 |
|
|
1545
|
+
| `collapse(groupId)` | `void` | 컬럼 그룹 접힘 |
|
|
1546
|
+
| `expand(groupId)` | `void` | 컬럼 그룹 펼침 |
|
|
1547
|
+
| `toggle(groupId)` | `void` | 컬럼 그룹 토글 |
|
|
1548
|
+
| `isCollapsed(groupId)` | `boolean` | 접힘 상태 |
|
|
1549
|
+
| `collapseAll()` | `void` | 전체 접힘 |
|
|
1550
|
+
| `expandAll()` | `void` | 전체 펼침 |
|
|
1551
|
+
| `collapseAtLevel(level)` | `void` | 특정 레벨(정확히 그 레벨)의 컬럼 그룹만 접힘 (Ctrl/Cmd+클릭 "같은 depth 일괄" 제스처와 동일 의미) |
|
|
1552
|
+
| `expandAtLevel(level)` | `void` | 특정 레벨(정확히 그 레벨)의 컬럼 그룹만 펼침 |
|
|
1553
|
+
|
|
1554
|
+
---
|
|
1555
|
+
|
|
1556
|
+
### grid.io — Excel Import/Export API
|
|
1557
|
+
|
|
1558
|
+
**외부 라이브러리 의존성 없이** XLSX 스트리밍 Import/Export를 제공합니다. 500만 행 이상의 대용량도 Worker 기반 스트리밍으로 처리하며, File System Access API를 통한 디스크 직접 저장도 지원합니다.
|
|
1559
|
+
|
|
1560
|
+
**v0.4.2 이후 지원**:
|
|
1561
|
+
- **피벗 export** — 다단계 컬럼 헤더(`columnHeaderTree`) + rowHeader/rowMerge + 컬럼 subtotal / grand total을 Excel `<mergeCells>`로 완전 보존
|
|
1562
|
+
- **플랫 셀 병합** — `MergeStrategy` 결과(`meta.merge`)를 스트리밍 중 감지해 `<mergeCells>`로 누적
|
|
1563
|
+
- **플랫 컬럼 그룹** — `columnGroups` 트리를 다단계 헤더로 복원
|
|
1564
|
+
- **플랫 행 그룹화** — 그룹 행 포함/제외 3 모드
|
|
1565
|
+
- **원본 데이터 export** — 피벗/그룹 상태 유지하면서 loadData 원본만 추출
|
|
1566
|
+
|
|
1567
|
+
#### 메서드
|
|
1568
|
+
|
|
1569
|
+
| 메서드 | 반환 | 설명 |
|
|
1570
|
+
|--------|------|------|
|
|
1571
|
+
| `toExcel(options?)` | `Promise<Blob>` | XLSX로 export하여 메모리에 Blob 반환 |
|
|
1572
|
+
| `saveExcelToFile(options?)` | `Promise<SaveExcelToFileResult>` | FSA로 디스크에 직접 스트리밍 저장 (미지원 환경에서는 다운로드로 폴백) |
|
|
1573
|
+
| `fromExcel(file, options?)` | `Promise<ImportResult>` | XLSX 파일을 그리드로 import (dirty 모드 단일 경로) |
|
|
1574
|
+
| `getSheetInfo(file)` | `Promise<SheetInfo[]>` | 시트 목록과 예상 행 수 조회 (import 전 미리보기용) |
|
|
1575
|
+
| `previewExcel(file, options?)` | `Promise<PreviewResult>` | 헤더 + 첫 N행 반환 (그리드에 로드하지 않음) |
|
|
1576
|
+
|
|
1577
|
+
#### Export 옵션 — `ExcelExportOptions`
|
|
1578
|
+
|
|
1579
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1580
|
+
|------|------|--------|------|
|
|
1581
|
+
| `scope` | `'all' \| 'visible' \| 'selected'` | `'all'` | 데이터 범위 |
|
|
1582
|
+
| `columns` | `string[]` | 보이는 컬럼 전체 | 특정 컬럼 ID만 |
|
|
1583
|
+
| `includeHeaders` | `boolean` | `true` | 헤더 행 포함 |
|
|
1584
|
+
| `sheetName` | `string` | `'Sheet1'` | 시트 이름 |
|
|
1585
|
+
| `preserveFormatting` | `boolean` | `false` | mask/decimalFormat → Excel numFmt 매핑 |
|
|
1586
|
+
| `dateFormat` | `string` | `'yyyy-mm-dd'` | 날짜 서식 |
|
|
1587
|
+
| `chunkSize` | `number` | `10000` | 청크당 행 수 |
|
|
1588
|
+
| `compression` | `'deflate' \| 'store'` | `'deflate'` | 압축 방식 |
|
|
1589
|
+
| `freezeHeader` | `boolean` | `false` | 헤더 행 고정 (freeze pane) |
|
|
1590
|
+
| `autoFilter` | `boolean` | `false` | 자동 필터 적용 |
|
|
1591
|
+
| `sheetSplit` | `false \| 'auto' \| number` | `false` | 시트 분할 (Excel 100만행 한계 대응). `'auto'`면 1,048,576 직전 자동 분할, `number`면 지정 행 수마다 분할 |
|
|
1592
|
+
| `sheetNamePattern` | `string` | `'{base} ({n})'` | 분할 시트 이름 패턴 (`{base}`, `{n}` placeholder) |
|
|
1593
|
+
| `useWorker` | `boolean` | auto (50K행 초과) | Worker 사용 여부 |
|
|
1594
|
+
| `signal` | `AbortSignal` | — | 취소 signal |
|
|
1595
|
+
| `onProgress` | `(ExportProgress) => void` | — | 진행률 콜백 |
|
|
1596
|
+
|
|
1597
|
+
##### 원본 데이터 export 옵션
|
|
1598
|
+
|
|
1599
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1600
|
+
|------|------|--------|------|
|
|
1601
|
+
| `exportSource` | `boolean` | `false` | 피벗/그룹/트리/병합을 모두 무시하고 `grid.getAllDataSync()` 원본 데이터를 그대로 내보냄. **모든 그리드 모드**(flat/pivot/tree/grouping)에서 동작. 이 옵션이 `true`면 아래의 모든 모드별 옵션은 무시됨 |
|
|
1602
|
+
| `sourceColumns` | `'all' \| 'used'` | `'all'` | `exportSource: true`일 때만 유효. `'all'`: 원본의 모든 컬럼(flat은 `fields` 전체, pivot은 첫 source 행 `Object.keys` + `rowFields/columnFields`의 `header` 라벨 enrichment + 값 기반 type 추론). `'used'`: 피벗에 참여하는 필드만(`rowFields + columnFields + valueFields`의 `field` 기준, flat에서는 `'all'`과 동일) |
|
|
1603
|
+
| `sourceFields` | `Array<{ field: string; header?: string; dataType?: string }>` | — (`originalConfig.fields`로 폴백) | `exportSource: true`(특히 pivot `sourceColumns: 'all'`)에서 원본 컬럼의 header/type 라벨링에 쓰는 **소스 필드 카탈로그**. 피벗 행/열/값에 **배치되지 않은** 필드도 raw id 대신 `header` 라벨로 export되게 한다 — 피벗 모드는 배치된 필드(`rowFields`/`columnFields`)에만 header가 있어 미배치 필드가 id로 새던 문제를 해결. 우선순위: 카탈로그(`sourceFields`, 없으면 `originalConfig.fields`)가 header/type을 시드 → 피벗 `rowFields`/`columnFields`의 `header`가 override(valueFields는 집계 라벨이라 제외), type은 카탈로그의 `dataType` 우선·없으면 첫 행 값으로 추론. 소비처가 자신의 소스 필드 카탈로그를 export 시 넘길 수 있다 |
|
|
1604
|
+
|
|
1605
|
+
##### 피벗 export 옵션
|
|
1606
|
+
|
|
1607
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1608
|
+
|------|------|--------|------|
|
|
1609
|
+
| `pivotExpandCollapsed` | `boolean` | `true` | 피벗 모드에서 접힌 행/열을 펼친 상태로 내보냄. `true`면 `pivot.getAllRows()`로 collapse 상태와 무관하게 전수 export하고 다단계 컬럼 헤더 + `rowMergeInfo` 병합까지 복원. `false`면 `getVisibleDataAsync` 경로로 "화면에 보이는 것만" export하되 row merge는 생략 (v1 안전 가드). flat 모드에서는 무시 |
|
|
1610
|
+
|
|
1611
|
+
##### 플랫 병합 / 컬럼 그룹 옵션
|
|
1612
|
+
|
|
1613
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1614
|
+
|------|------|--------|------|
|
|
1615
|
+
| `preserveMerges` | `boolean` | `true` | 셀 병합 보존 여부. `true`면 플랫 모드의 `MergeStrategy` 결과(`meta.merge`)와 피벗의 `rowMergeInfo`를 Excel `<mergeCells>`로 내보냄. `false`면 기존 평탄 export. 플랫 경로는 `writeChunk` 도중 각 행의 `__merge`를 스트리밍으로 감지해 누적 |
|
|
1616
|
+
| `preserveColumnGroups` | `boolean` | `true` | 플랫 모드 `columnGroups` 트리를 다단계 헤더 + `<mergeCells>` 병합으로 보존. 피벗 모드에서는 자체 다단계 헤더가 이 옵션과 독립으로 동작. flat 모드에 `columnGroups`가 없으면 무시 |
|
|
1617
|
+
|
|
1618
|
+
##### 그룹 행 export 옵션
|
|
1619
|
+
|
|
1620
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1621
|
+
|------|------|--------|------|
|
|
1622
|
+
| `groupRowMode` | `'include' \| 'visible' \| 'dataOnly'` | `'include'` | 플랫 행 그룹화(`grid.rowGroup.setConfig`) 활성 시 export 동작. `'include'`: 접힘 상태 무시하고 모든 그룹 행 + data를 펼친 상태로 export. `'visible'`: 현재 접힘 상태 그대로 (collapsed 자식 제외). `'dataOnly'`: 그룹 행 제외하고 data 행만 |
|
|
1623
|
+
| `groupOutline` | `boolean` | `true` | Excel 네이티브 outline (+/- 버튼) 생성. 그룹 행을 summary row로, 데이터 행에 `outlineLevel`을 부여. `groupRowMode: 'dataOnly'`이면 무시. 중첩 그룹(`columns: ['dept','role']`)은 depth별 outlineLevel 자동 계산 |
|
|
1624
|
+
|
|
1625
|
+
##### 트리 그리드 export 옵션
|
|
1626
|
+
|
|
1627
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1628
|
+
|------|------|--------|------|
|
|
1629
|
+
| `treeRowMode` | `'include' \| 'visible'` | `'include'` | 트리 그리드(`grid.tree.setConfig`) 활성 시 export 동작. `'include'`: 모든 트리 노드를 펼친 상태로 내보냄(collapsed 무시). `'visible'`: 현재 접힘 상태 그대로 (collapsed 자식 제외). 원본 평탄 데이터만 원하면 `exportSource: true` 사용 |
|
|
1630
|
+
| `treeOutline` | `boolean` | `true` | Excel 네이티브 outline (+/- 버튼) 생성. 트리 depth를 `outlineLevel`로 변환. Excel outline은 최대 7단계까지 지원 |
|
|
1631
|
+
|
|
1632
|
+
> **경로 우선순위**: `exportSource` > 피벗 > 트리 > 그룹 > 병합 > 기본. 상위 경로가 활성이면 하위 옵션은 무시됩니다.
|
|
1633
|
+
|
|
1634
|
+
> **조합 매트릭스**: 플랫의 columnGroups + grouping + merge는 서로 직교 조합 가능.
|
|
1635
|
+
> 피벗 경로가 활성이면 flat 옵션들은 무시되고 피벗 자체의 다단계 헤더 + `rowMergeInfo`가 우선 적용됩니다.
|
|
1636
|
+
> 트리와 그룹은 상호배타 — 트리 모드가 활성이면 그룹 옵션은 무시됩니다.
|
|
1637
|
+
|
|
1638
|
+
> **Multi-Row(`rowTemplate`) export**: 현재 논리 행 1개 = xlsx 행 1개로 평탄화되며 `console.warn`이 1회 발생합니다. 논리 행 → 물리 행 N개 확장 + subRow cell placement 지원은 Phase 2 후속 작업.
|
|
1639
|
+
|
|
1640
|
+
#### `saveExcelToFile` 전용 옵션 — `SaveExcelToFileOptions extends ExcelExportOptions`
|
|
1641
|
+
|
|
1642
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1643
|
+
|------|------|--------|------|
|
|
1644
|
+
| `suggestedName` | `string` | `'export.xlsx'` | 저장 다이얼로그 기본 파일명 |
|
|
1645
|
+
| `fallbackToDownload` | `boolean` | `true` | FSA 미지원 환경에서 `toExcel` + 다운로드로 폴백. `false`면 에러 throw |
|
|
1646
|
+
| `onFileNameResolved` | `(fileName: string) => void` | – | 저장 파일명이 **확정된 시점**(FSA는 사용자가 저장 클릭 직후 `handle.name`, 폴백은 즉시 `suggestedName`) 호출. 스트리밍 시작 전이라 호스트가 진행률 알림을 실제 파일명으로 띄울 수 있다. 다이얼로그 취소 시 미호출 |
|
|
1647
|
+
|
|
1648
|
+
`SaveExcelToFileResult`: `{ mode: 'fileSystemAccess' \| 'download', fileName: string, fileSize?: number }`
|
|
1649
|
+
|
|
1650
|
+
#### Import 옵션 — `ExcelImportOptions`
|
|
1651
|
+
|
|
1652
|
+
##### 기본 옵션
|
|
1653
|
+
|
|
1654
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1655
|
+
|------|------|--------|------|
|
|
1656
|
+
| `sheetIndex` | `number` | `0` | 가져올 시트 인덱스 |
|
|
1657
|
+
| `sheetName` | `string` | — | 시트 이름 (sheetIndex 대신) |
|
|
1658
|
+
| `mergeSheets` | `boolean \| 'auto'` | `false` | 모든 시트 합치기 (분할 export된 파일용). `'auto'`면 시트 2개 이상이면 자동 |
|
|
1659
|
+
| `columnMapping` | `Record<string, string> \| 'auto'` | `'auto'` | Excel 헤더 → grid columnId 매핑. `'auto'`는 헤더 텍스트 기반 자동 매칭 |
|
|
1660
|
+
| `headerRow` | `number` | `0` | 헤더 행 번호 |
|
|
1661
|
+
| `startRow` | `number` | `headerRow + 1` | 데이터 시작 행 |
|
|
1662
|
+
| `typeCoercion` | `boolean` | `true` | 자동 타입 감지 |
|
|
1663
|
+
| `dateColumns` | `string[]` | — | 날짜 컬럼 강제 지정 |
|
|
1664
|
+
| `preview` | `number` | — | 첫 N행만 로드 (미리보기) |
|
|
1665
|
+
| `batchSize` | `number` | `10000` | 배치당 행 수 |
|
|
1666
|
+
| `useWorker` | `boolean` | auto (50MB 초과) | Worker 사용 여부 |
|
|
1667
|
+
| `signal` | `AbortSignal` | — | 취소 signal |
|
|
1668
|
+
| `onProgress` | `(ImportProgress) => void` | — | 진행률 콜백 |
|
|
1669
|
+
| `validate` | `boolean` | `false` | import 후 즉시 validation rule을 실행하고 결과를 `ImportResult`에 포함 |
|
|
1670
|
+
|
|
1671
|
+
##### Dirty 모드 옵션 (93369b4 커밋)
|
|
1672
|
+
|
|
1673
|
+
모든 import는 **dirty 상태**로 반영되며, 사용자가 `grid.changes.commit()`으로 확정해야 합니다 (`autoCommit` 옵션 제외). 기존 dirty 상태가 있으면 `PS711`로 거부됩니다.
|
|
1674
|
+
|
|
1675
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
1676
|
+
|------|------|--------|------|
|
|
1677
|
+
| `mode` | `'replace' \| 'append' \| 'upsert'` | `'replace'` | 기존 데이터 처리 방식 |
|
|
1678
|
+
| `conflictStrategy` | `'error' \| 'skip' \| 'update'` | `'error'` | `mode: 'append'`에서 키 충돌 처리. `replace`/`upsert`에선 무시 |
|
|
1679
|
+
| `autoCommit` | `boolean` | `false` | import 완료 후 자동 `commit()` 호출 |
|
|
1680
|
+
| `dryRun` | `boolean` | `false` | ChangeTracker에 실제 반영 없이 변경 요약만 반환 (대용량 preview) |
|
|
1681
|
+
|
|
1682
|
+
**`mode` 상세**
|
|
1683
|
+
|
|
1684
|
+
- **`'replace'`** — 기존 committed 전체를 Excel 데이터로 대체.
|
|
1685
|
+
- Phase 1: committed N행 전체를 `dirtyDeleted`로 pre-mark
|
|
1686
|
+
- Phase 2: 각 Excel row의 키가 committed와 hit이면 `undeleteRow` + 필드별 `updateCell` (값 동일 시 pristine 유지). Miss이면 `addRow`
|
|
1687
|
+
- Excel에 **없는** committed 행은 `deletedCount`로 남음
|
|
1688
|
+
- **`'upsert'`** — 키 있으면 update, 없으면 add. Excel에 없는 기존 행은 **그대로 유지**. `mode: 'append'` + `conflictStrategy: 'update'`와 의미는 같지만 이름으로 의도 명확화
|
|
1689
|
+
- **`'append'`** — 기존 유지 + 끝에 추가. 키 충돌은 `conflictStrategy`로 처리
|
|
1690
|
+
|
|
1691
|
+
**`conflictStrategy` 상세** (`mode: 'append'` 전용)
|
|
1692
|
+
|
|
1693
|
+
- **`'error'`** — 충돌 발생 즉시 `PS712` throw + 전체 dirty 롤백
|
|
1694
|
+
- **`'skip'`** — 충돌 행 무시 + `skippedCount++` + `conflicts[]`에 기록 (최대 100개)
|
|
1695
|
+
- **`'update'`** — 기존 행을 Excel 값으로 업데이트 (`sameValue` diff로 동일 값은 pristine 유지)
|
|
1696
|
+
|
|
1697
|
+
#### Import 결과 — `ImportResult`
|
|
1698
|
+
|
|
1699
|
+
| 필드 | 타입 | 설명 |
|
|
1700
|
+
|------|------|------|
|
|
1701
|
+
| `rowCount` | `number` | Excel에서 읽은 총 행 수 (파싱 기준) |
|
|
1702
|
+
| `columnCount` | `number` | import된 컬럼 수 |
|
|
1703
|
+
| `columns` | `ImportColumnInfo[]` | 컬럼 매핑 결과 `{ excelHeader, mappedTo, type }` |
|
|
1704
|
+
| `errors` | `ImportError[]` | 치명적이지 않은 에러 (warning/error) |
|
|
1705
|
+
| `duration` | `number` | 소요 시간 (ms) |
|
|
1706
|
+
| `addedCount` | `number` | dirty로 추가된 행 수 |
|
|
1707
|
+
| `updatedCount` | `number` | 실제 값이 다른 경우만 카운트 (sameValue diff 통과) |
|
|
1708
|
+
| `skippedCount` | `number` | skip 전략 또는 키 누락 |
|
|
1709
|
+
| `deletedCount` | `number` | `replace` 모드에서 dirtyDeleted 표시된 행 수 |
|
|
1710
|
+
| `unchangedCount` | `number` | 값이 완전 동일해서 pristine 유지 |
|
|
1711
|
+
| `committed` | `boolean` | `autoCommit` 실행 여부 |
|
|
1712
|
+
| `conflicts?` | `ImportConflict[]` | 충돌 목록 (최대 100개). `{ row, key, reason: 'duplicate' \| 'missing_key' }` |
|
|
1713
|
+
| `conflictsTruncated?` | `boolean` | 100개 초과로 잘렸는지 |
|
|
1714
|
+
| `warnings?` | `string[]` | 경고 목록 (`'LARGE_IMPORT'`, `'DRY_RUN'` 등) |
|
|
1715
|
+
|
|
1716
|
+
#### 진행률 — `ImportProgress` / `ExportProgress`
|
|
1717
|
+
|
|
1718
|
+
**`ImportProgress`**
|
|
1719
|
+
|
|
1720
|
+
| 필드 | 타입 | 설명 |
|
|
1721
|
+
|------|------|------|
|
|
1722
|
+
| `phase` | `'reading' \| 'parsing' \| 'loading' \| 'diffing' \| 'committing' \| 'validating'` | 현재 단계 |
|
|
1723
|
+
| `rowsProcessed` | `number` | 처리된 행 수 |
|
|
1724
|
+
| `totalRows?` | `number` | 스트리밍 중에는 `undefined` |
|
|
1725
|
+
| `percent` | `number` | 0~100 |
|
|
1726
|
+
|
|
1727
|
+
**`ExportProgress`**
|
|
1728
|
+
|
|
1729
|
+
| 필드 | 타입 | 설명 |
|
|
1730
|
+
|------|------|------|
|
|
1731
|
+
| `phase` | `'preparing' \| 'generating' \| 'compressing' \| 'finalizing'` | 현재 단계 |
|
|
1732
|
+
| `rowsProcessed` | `number` | 처리된 행 수 |
|
|
1733
|
+
| `totalRows` | `number` | 전체 행 수 |
|
|
1734
|
+
| `percent` | `number` | 0~100 |
|
|
1735
|
+
| `currentSheet?` | `number` | 분할 시에만 (1-based) |
|
|
1736
|
+
| `totalSheets?` | `number` | 분할 시에만 |
|
|
1737
|
+
|
|
1738
|
+
#### 에러 코드
|
|
1739
|
+
|
|
1740
|
+
| 코드 | 경로 | 설명 |
|
|
1741
|
+
|------|------|------|
|
|
1742
|
+
| `PS700` | export | export 일반 실패 |
|
|
1743
|
+
| `PS701` | import | import 일반 실패 |
|
|
1744
|
+
| `PS702` | import | 유효하지 않은 XLSX 파일 |
|
|
1745
|
+
| `PS703` | import | 시트를 찾을 수 없음 |
|
|
1746
|
+
| `PS704` | import | 컬럼 매핑 실패 |
|
|
1747
|
+
| `PS705` | export | `CompressionStream` 미지원 (STORE로 fallback, warning) |
|
|
1748
|
+
| `PS706` | export | AbortSignal 취소 |
|
|
1749
|
+
| `PS707` | import | AbortSignal 취소 |
|
|
1750
|
+
| `PS708` | import | Worker 생성 실패 (메인 스레드로 fallback, warning) |
|
|
1751
|
+
| `PS709` | export | 시트당 최대 행 수 초과 (`sheetSplit` 미사용 시) |
|
|
1752
|
+
| `PS710` | export | 시트당 최대 컬럼 수 초과 |
|
|
1753
|
+
| `PS711` | import | 기존 dirty 상태가 있어 import 거부 |
|
|
1754
|
+
| `PS712` | import | `append` 모드 키 충돌 (`conflictStrategy: 'error'`) |
|
|
1755
|
+
| `PS713` | import | 키 컬럼이 Excel에 없음 |
|
|
1756
|
+
| `PS714` | import | `autoCommit` 단계 실패 (dirty는 보존) |
|
|
1757
|
+
|
|
1758
|
+
#### 이벤트
|
|
1759
|
+
|
|
1760
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
1761
|
+
|--------|---------|------|
|
|
1762
|
+
| `io:export:complete` | `{ rowCount, columnCount, fileSize }` | XLSX export 완료 |
|
|
1763
|
+
| `io:import:complete` | `{ rowCount, columnCount, duration }` | XLSX import 완료 |
|
|
1764
|
+
|
|
1765
|
+
#### 예제
|
|
1766
|
+
|
|
1767
|
+
```typescript
|
|
1768
|
+
// 기본 export (메모리)
|
|
1769
|
+
const blob = await grid.io.toExcel({ scope: 'visible', includeHeaders: true });
|
|
1770
|
+
|
|
1771
|
+
// 디스크 직접 저장 (FSA)
|
|
1772
|
+
const result = await grid.io.saveExcelToFile({
|
|
1773
|
+
suggestedName: 'report.xlsx',
|
|
1774
|
+
scope: 'all',
|
|
1775
|
+
sheetSplit: 'auto',
|
|
1776
|
+
preserveFormatting: true,
|
|
1777
|
+
onProgress: (p) => console.log(`${p.phase}: ${p.percent}%`),
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
// 대량 import with replace + autoCommit
|
|
1781
|
+
const importResult = await grid.io.fromExcel(file, {
|
|
1782
|
+
mode: 'replace',
|
|
1783
|
+
autoCommit: false, // false이면 dirty 상태로 검토 가능
|
|
1784
|
+
signal: abortController.signal,
|
|
1785
|
+
onProgress: (p) => updateProgressBar(p.percent),
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
console.log(`추가 ${importResult.addedCount}, 수정 ${importResult.updatedCount}, ` +
|
|
1789
|
+
`삭제 ${importResult.deletedCount}, 동일 ${importResult.unchangedCount}`);
|
|
1790
|
+
|
|
1791
|
+
// 검토 후 수동 확정
|
|
1792
|
+
if (importResult.warnings?.includes('LARGE_IMPORT')) {
|
|
1793
|
+
if (confirm('20만 행 이상 import됩니다. 계속?')) {
|
|
1794
|
+
await grid.changes.commit();
|
|
1795
|
+
} else {
|
|
1796
|
+
await grid.changes.discardAll();
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// append + skip 전략
|
|
1801
|
+
await grid.io.fromExcel(file, {
|
|
1802
|
+
mode: 'append',
|
|
1803
|
+
conflictStrategy: 'skip',
|
|
1804
|
+
autoCommit: true,
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
// dry-run으로 변경 요약만 확인
|
|
1808
|
+
const summary = await grid.io.fromExcel(file, { mode: 'upsert', dryRun: true });
|
|
1809
|
+
console.log(`변경 예상: +${summary.addedCount} ~${summary.updatedCount} -${summary.deletedCount}`);
|
|
1810
|
+
```
|
|
1811
|
+
|
|
1812
|
+
---
|
|
1813
|
+
|
|
1814
|
+
### grid.dragDrop — 드래그앤드롭 API
|
|
1815
|
+
|
|
1816
|
+
행 단위 드래그앤드롭(같은 그리드 내 reorder, 크로스-그리드 드롭 등)을 제어합니다. 그리드 생성 시 `dragDropConfig`로 초기 설정을 전달할 수 있으며, 런타임에 `setConfig`로 전체 교체가 가능합니다.
|
|
1817
|
+
|
|
1818
|
+
| 메서드 | 반환 | 설명 |
|
|
1819
|
+
|--------|------|------|
|
|
1820
|
+
| `setConfig(config)` | `Promise<void>` | 새 `DragDropConfig`로 교체 (전체 교체 — partial update 아님) |
|
|
1821
|
+
| `getConfig()` | `Readonly<DragDropConfig>` | 현재 설정 조회 |
|
|
1822
|
+
| `isActive()` | `boolean` | drag-drop 기능 중 하나라도 활성화되어 있는지 |
|
|
1823
|
+
|
|
1824
|
+
> 관련 이벤트: `row:reordered` (같은 그리드 내 reorder), `row:dragOut` / `row:dragIn` (크로스-그리드 이동 — ADR-108).
|
|
1825
|
+
|
|
1826
|
+
```typescript
|
|
1827
|
+
// 초기 설정 (생성자)
|
|
1828
|
+
new PureSheet(container, {
|
|
1829
|
+
dragDropConfig: {
|
|
1830
|
+
rowOrderField: { field: 'displayOrder', strategy: 'dense' },
|
|
1831
|
+
mutateOnDirty: (row) => ({ updatedAt: new Date().toISOString() }),
|
|
1832
|
+
// crossGridDragGroup: 'tasks', // 같은 그룹의 다른 그리드로 행 이동
|
|
1833
|
+
// onRowDragOut: (e) => true, onRowDragIn: (e) => true, // 거부 시 false 반환
|
|
1834
|
+
},
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
// 런타임 변경
|
|
1838
|
+
await grid.dragDrop.setConfig({
|
|
1839
|
+
rowOrderField: { field: 'sortOrder', strategy: 'sparse' },
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
if (grid.dragDrop.isActive()) {
|
|
1843
|
+
console.log('drag-drop config:', grid.dragDrop.getConfig());
|
|
1844
|
+
}
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
#### 드래그 시작 위치 — `dragTrigger`
|
|
1848
|
+
|
|
1849
|
+
| 값 | 설명 |
|
|
1850
|
+
|---|---|
|
|
1851
|
+
| `'handle'` (default) | drag 핸들 컬럼에서만 reorder 시작. row 본문은 기존 동작(cell/row 드래그 선택) |
|
|
1852
|
+
| `'row'` | row 본문 어디서나 reorder 시작. mousedown → 5px 이상 이동 시 drag, 그 미만은 click(선택)으로 처리 (Shift/Ctrl+click 정상). **trade-off**: cell/row 드래그 선택은 비활성. 보통 `showDragHandleColumn: false`와 함께 사용 |
|
|
1853
|
+
|
|
1854
|
+
#### 트리 드래그 reorder — `treeReorder`
|
|
1855
|
+
|
|
1856
|
+
`treeConfig` + `reorderableRows: true`가 함께 설정되면 **트리 인식 드래그 reorder**가 자동 활성화됩니다. 형제 순서 변경 + 다른 부모로 재부모화(reparent)를 드래그로 처리하며, 부모를 드래그하면 후손이 함께 이동합니다(Policy B). `dragDropConfig.treeReorder`(`TreeReorderConfig`)로 세부 조정하거나 `false`로 비활성화합니다.
|
|
1857
|
+
|
|
1858
|
+
```typescript
|
|
1859
|
+
new PureSheet(container, {
|
|
1860
|
+
treeConfig: { idField: 'id', parentField: 'parentId', treeColumn: 'name' },
|
|
1861
|
+
dragDropConfig: {
|
|
1862
|
+
reorderableRows: true,
|
|
1863
|
+
dragTrigger: 'row', // 행 본문 어디서나 드래그 (파일 탐색기 UX)
|
|
1864
|
+
showDragHandleColumn: false,
|
|
1865
|
+
treeReorder: {
|
|
1866
|
+
allowReparent: true, // as-child 드롭(재부모화) 허용 (@default true)
|
|
1867
|
+
// 폴더 위 dwell → drop-onto(자식화), 파일 위 → insert-between(형제)
|
|
1868
|
+
resolveDropMode: (row) => (row.type === 'folder' ? 'drop-onto' : 'insert-between'),
|
|
1869
|
+
isDroppableAsChild: (row) => row.type === 'folder', // 자식 수용 가능 행 제한
|
|
1870
|
+
},
|
|
1871
|
+
},
|
|
1872
|
+
});
|
|
1873
|
+
```
|
|
1874
|
+
|
|
1875
|
+
**`TreeReorderConfig` 필드**
|
|
1876
|
+
|
|
1877
|
+
| 필드 | 타입 | 기본 | 설명 |
|
|
1878
|
+
|------|------|------|------|
|
|
1879
|
+
| `allowReparent` | `boolean` | `true` | as-child 드롭(다른 부모로 재부모화) 허용 |
|
|
1880
|
+
| `expandOnHover` | `number \| false` | `700` | collapsed 부모 위 hover 시 자동 펼침 지연(ms). `false`면 비활성 |
|
|
1881
|
+
| `isDroppableAsChild` | `(row) => boolean` | — | 특정 행이 자식을 받을 수 있는지 (예: 폴더만). 미설정 시 모두 허용 |
|
|
1882
|
+
| `resolveDropMode` | `(row, ctx) => DropMode \| Promise<DropMode>` | — | 행별 드롭 모드 결정. `'insert-between'`(형제 line) vs `'drop-onto'`(행 highlight 자식화). 비동기 가능 |
|
|
1883
|
+
| `dropModeDwellMs` | `number` | `500` | `resolveDropMode` 호출 전 cursor가 같은 행 위에 머물러야 하는 dwell(ms) |
|
|
1884
|
+
| `dropOntoEdgeThreshold` | `number \| { px?, ratio? }` | `2` | drop-onto 모드여도 행 위/아래 가장자리 이 범위 안이면 insert-between으로 fall-back (폴더-폴더 사이 삽입용). 낮은 rowHeight 대응 위해 작게 |
|
|
1885
|
+
| `indentSize` | `number` | `20` | 드롭 인디케이터 indent 계산에 쓰는 레벨당 px |
|
|
1886
|
+
| `indicatorStartOffset` | `number` | `10` | 인디케이터 시작 X 오프셋(px) |
|
|
1887
|
+
| `mapCrossGridParent` | `(sourceRow, sourceGrid) => RowId \| null` | — | 크로스-그리드 트리 드롭 시 source parentId → target parentId 변환 |
|
|
1888
|
+
|
|
1889
|
+
> **프로그램 방식 이동**은 드래그 없이 [`grid.tree.moveNode(nodeId, newParentId, newIndex?)`](#gridtree--트리-api)로 수행합니다 (flat의 `changes.reorderRow` 대칭).
|
|
1890
|
+
> **완료 통지**는 `row:reordered {kind:'tree'}` 이벤트로 발화하며 `commandId` / `patches` / `dropMode`를 담습니다.
|
|
1891
|
+
> **낙관적 백엔드 동기화**는 `row:reordered` + `changes:undone`/`changes:redone` 3종 구독 + 실패 시 `undoManager.undoThrough(commandId)` 패턴을 쓰세요 ([Undo/Redo 이벤트](#undoredo) 참조).
|
|
1892
|
+
|
|
1893
|
+
---
|
|
1894
|
+
|
|
1895
|
+
## Reactive API (반응형 구독)
|
|
1896
|
+
|
|
1897
|
+
moz-ui-grid-core는 React `useSyncExternalStore` 호환 반응형 구독 시스템을 제공합니다.
|
|
1898
|
+
|
|
1899
|
+
### grid.select()
|
|
1900
|
+
|
|
1901
|
+
```typescript
|
|
1902
|
+
grid.select<T>(selector: (state: GridState) => T): Subscribable<T>
|
|
1903
|
+
```
|
|
1904
|
+
|
|
1905
|
+
GridState에서 파생 상태를 구독합니다. selector 결과가 변경될 때만 구독자에게 알림.
|
|
1906
|
+
|
|
1907
|
+
#### Subscribable 인터페이스
|
|
1908
|
+
|
|
1909
|
+
```typescript
|
|
1910
|
+
interface Subscribable<T> {
|
|
1911
|
+
subscribe(onStoreChange: () => void): Unsubscribe;
|
|
1912
|
+
getSnapshot(): T;
|
|
1913
|
+
}
|
|
1914
|
+
```
|
|
1915
|
+
|
|
1916
|
+
#### React에서 사용
|
|
1917
|
+
|
|
1918
|
+
```typescript
|
|
1919
|
+
// React Hook
|
|
1920
|
+
function useGridState<T>(grid: PureSheet, selector: (state: GridState) => T): T {
|
|
1921
|
+
const subscribable = useMemo(() => grid.select(selector), [grid, selector]);
|
|
1922
|
+
return useSyncExternalStore(subscribable.subscribe, subscribable.getSnapshot);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// 사용 예시
|
|
1926
|
+
const rowCount = useGridState(grid, s => s.visibleRowCount);
|
|
1927
|
+
const isEditing = useGridState(grid, s => s.isEditing);
|
|
1928
|
+
const canUndo = useGridState(grid, s => s.canUndo);
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
#### Vanilla JS에서 사용
|
|
1932
|
+
|
|
1933
|
+
```typescript
|
|
1934
|
+
const sub = grid.select(s => s.selectedRowCount);
|
|
1935
|
+
const unsubscribe = sub.subscribe(() => {
|
|
1936
|
+
console.log('선택된 행 수:', sub.getSnapshot());
|
|
1937
|
+
});
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
### grid.store
|
|
1941
|
+
|
|
1942
|
+
```typescript
|
|
1943
|
+
get store(): Store<GridState>
|
|
1944
|
+
```
|
|
1945
|
+
|
|
1946
|
+
`grid.select()` 내부에서 사용하는 Store 인스턴스에 직접 접근합니다. 첫 접근 시 lazy 초기화되며 GridCore 이벤트 + ChangeTracker 이벤트가 자동 연결됩니다. `createGridStore`를 수동 호출할 필요는 없습니다.
|
|
1947
|
+
|
|
1948
|
+
```typescript
|
|
1949
|
+
const store = grid.store;
|
|
1950
|
+
|
|
1951
|
+
// 1) 즉시 스냅샷 조회 — selector 없이 전체 상태
|
|
1952
|
+
const { columnStates, visibleRowCount } = store.getState();
|
|
1953
|
+
|
|
1954
|
+
// 2) 다중 selector 구성 시 동일 store 재사용
|
|
1955
|
+
const a = store.select(s => s.canUndo);
|
|
1956
|
+
const b = store.select(s => s.canRedo);
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
> 단발성 조회는 `grid.select(s => …).getSnapshot()`이 더 간결합니다. `store.getState()`는 여러 필드를 한 번에 꺼내거나 디버깅 콘솔에서 전체 상태를 들여다볼 때 유용합니다.
|
|
1960
|
+
|
|
1961
|
+
### GridState 필드
|
|
1962
|
+
|
|
1963
|
+
`grid.select(s => s.필드명)` 으로 구독 가능한 전체 상태:
|
|
1964
|
+
|
|
1965
|
+
#### 데이터 & 컬럼
|
|
1966
|
+
|
|
1967
|
+
| 필드 | 타입 | 설명 |
|
|
1968
|
+
|------|------|------|
|
|
1969
|
+
| `columns` | `readonly ColumnDef[]` | 현재 컬럼 정의 |
|
|
1970
|
+
| `columnStates` | `readonly ColumnState[]` | 컬럼 UI 상태 (너비, 가시성, 고정, 순서) |
|
|
1971
|
+
|
|
1972
|
+
#### 행 수
|
|
1973
|
+
|
|
1974
|
+
| 필드 | 타입 | 설명 |
|
|
1975
|
+
|------|------|------|
|
|
1976
|
+
| `totalRowCount` | `number` | 전체 행 수 (필터 전) |
|
|
1977
|
+
| `visibleRowCount` | `number` | 표시 행 수 (필터 후) |
|
|
1978
|
+
|
|
1979
|
+
#### 뷰 상태
|
|
1980
|
+
|
|
1981
|
+
| 필드 | 타입 | 설명 |
|
|
1982
|
+
|------|------|------|
|
|
1983
|
+
| `viewState` | `Readonly<ViewState>` | 정렬, 필터, 그룹 설정 |
|
|
1984
|
+
#### Feature 모듈 설정 (반응형 관측 계약)
|
|
1985
|
+
|
|
1986
|
+
사용자 변경 가능한 feature 모듈(`setConfig`류 API 보유)의 live config를 **대칭적으로** 반응형 노출합니다. 외부 소비자가 자신의 설정 UI를 `getConfig()` 폴링 없이 구독으로 바인딩할 수 있습니다. 각 필드는 변경 시에만 새 참조로 교체되며(shallowEqual 호환), 미설정 모듈은 `null`(또는 `pivotMode`는 `'flat'`)로 노출됩니다. 노출된 config는 내부 mutable 참조이므로 직접 수정하지 말고 해당 `setConfig`류 API를 사용하세요.
|
|
1987
|
+
|
|
1988
|
+
| 필드 | 타입 | 설명 |
|
|
1989
|
+
|------|------|------|
|
|
1990
|
+
| `pivotConfig` | `Readonly<PivotConfig> \| null` | 현재 피벗 설정 (피벗 모드가 아니어도 보존될 수 있음 → 모드 판별은 `pivotMode` 사용) |
|
|
1991
|
+
| `pivotMode` | `'flat' \| 'pivot'` | 현재 피벗 모드 (`setMode()`의 권위 있는 신호) |
|
|
1992
|
+
| `treeConfig` | `Readonly<TreeConfig> \| null` | 현재 트리 설정 |
|
|
1993
|
+
| `masterDetailConfig` | `Readonly<MasterDetailConfig> \| null` | 현재 마스터-디테일 설정 |
|
|
1994
|
+
| `groupConfig` | `Readonly<GroupingConfig> \| null` | 현재 그룹화 설정 (`viewState.groups`는 이로부터 합성됨) |
|
|
1995
|
+
| `dragDropConfig` | `Readonly<DragDropConfig> \| null` | 현재 드래그-드롭 설정 |
|
|
1996
|
+
| `mergeConfig` | `Readonly<MergeConfig> \| null` | 현재 셀 병합 설정 |
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
#### 변경 추적
|
|
2000
|
+
|
|
2001
|
+
| 필드 | 타입 | 설명 |
|
|
2002
|
+
|------|------|------|
|
|
2003
|
+
| `hasChanges` | `boolean` | 미커밋 변경 존재 여부 |
|
|
2004
|
+
| `changeVersion` | `number` | 변경 버전 카운터 (변경 감지용) |
|
|
2005
|
+
|
|
2006
|
+
#### 비동기 상태
|
|
2007
|
+
|
|
2008
|
+
| 필드 | 타입 | 설명 |
|
|
2009
|
+
|------|------|------|
|
|
2010
|
+
| `activity` | `GridActivity` | 현재 실행 중인 비동기 작업 (중첩 시 가장 바깥쪽만 노출) |
|
|
2011
|
+
| `isSearching` | `boolean` | 검색 진행 중 여부 (background 작업, `activity`와 독립) |
|
|
2012
|
+
|
|
2013
|
+
`GridActivity` 전체 값:
|
|
2014
|
+
|
|
2015
|
+
| 범주 | 값 |
|
|
2016
|
+
|---|---|
|
|
2017
|
+
| Idle | `'idle'` |
|
|
2018
|
+
| 데이터 로딩 | `'loadData'`, `'loadTreeData'`, `'refreshLoaderData'` |
|
|
2019
|
+
| 뷰 변경 | `'sort'`, `'filter'`, `'setMode'`, `'setPivotConfig'`, `'setMergeConfig'`, `'updateFields'` |
|
|
2020
|
+
| Committed CRUD | `'addRow'`, `'updateRow'`, `'removeRow'` |
|
|
2021
|
+
| Dirty CRUD | `'addRowDirty'`, `'updateCellDirty'`, `'deleteRowDirty'`, `'deleteSelectedRowsDirty'`, `'undeleteRowsDirty'`, `'undeleteRowDirty'`, `'discardRowDirty'`, `'discardSelectedRowsDirty'`, `'reorderRow'` |
|
|
2022
|
+
| 트랜잭션 | `'applyTransaction'`, `'flushAsyncTransactions'` |
|
|
2023
|
+
| 커밋/리프레시 | `'commitChanges'`, `'refresh'` |
|
|
2024
|
+
| 클립보드 | `'copyToClipboard'`, `'pasteFromClipboard'` |
|
|
2025
|
+
| 선택 일괄 | `'deleteSelectedRows'`, `'updateSelectedCells'` |
|
|
2026
|
+
| 검색 | `'search'` |
|
|
2027
|
+
| IO | `'exportExcel'`, `'importExcel'` |
|
|
2028
|
+
|
|
2029
|
+
#### 선택
|
|
2030
|
+
|
|
2031
|
+
| 필드 | 타입 | 설명 |
|
|
2032
|
+
|------|------|------|
|
|
2033
|
+
| `selectedRowCount` | `number` | 선택된 행 수 |
|
|
2034
|
+
| `selectedCellCount` | `number` | 선택된 셀 수 |
|
|
2035
|
+
|
|
2036
|
+
#### 편집
|
|
2037
|
+
|
|
2038
|
+
| 필드 | 타입 | 설명 |
|
|
2039
|
+
|------|------|------|
|
|
2040
|
+
| `isEditing` | `boolean` | 셀 편집 중 여부 |
|
|
2041
|
+
| `editingCell` | `Readonly<CellPosition> \| null` | 편집 중인 셀 위치 |
|
|
2042
|
+
|
|
2043
|
+
#### Undo/Redo
|
|
2044
|
+
|
|
2045
|
+
| 필드 | 타입 | 설명 |
|
|
2046
|
+
|------|------|------|
|
|
2047
|
+
| `canUndo` | `boolean` | Undo 가능 여부 |
|
|
2048
|
+
| `canRedo` | `boolean` | Redo 가능 여부 |
|
|
2049
|
+
|
|
2050
|
+
#### 검증
|
|
2051
|
+
|
|
2052
|
+
| 필드 | 타입 | 설명 |
|
|
2053
|
+
|------|------|------|
|
|
2054
|
+
| `hasValidationErrors` | `boolean` | 검증 에러 존재 여부 |
|
|
2055
|
+
| `validationErrorCount` | `number` | 검증 에러 수 |
|
|
2056
|
+
|
|
2057
|
+
#### 검색
|
|
2058
|
+
|
|
2059
|
+
| 필드 | 타입 | 설명 |
|
|
2060
|
+
|------|------|------|
|
|
2061
|
+
| `isSearching` | `boolean` | 검색 background 작업 진행 중 여부 |
|
|
2062
|
+
|
|
2063
|
+
#### 스크롤
|
|
2064
|
+
|
|
2065
|
+
| 필드 | 타입 | 설명 |
|
|
2066
|
+
|------|------|------|
|
|
2067
|
+
| `scrollTop` | `number` | 수직 스크롤 위치 (px) |
|
|
2068
|
+
| `visibleStartIndex` | `number` | 보이는 첫 행 인덱스 (overscan 포함) |
|
|
2069
|
+
| `visibleEndIndex` | `number` | 보이는 마지막 행 인덱스 (overscan 포함) |
|
|
2070
|
+
|
|
2071
|
+
---
|
|
2072
|
+
|
|
2073
|
+
### grid.rows.observeById()
|
|
2074
|
+
|
|
2075
|
+
```typescript
|
|
2076
|
+
grid.rows.observeById(id: string | number): ReactiveRow
|
|
2077
|
+
grid.rows.observeByIndex(sourceIndex: number): ReactiveRow
|
|
2078
|
+
```
|
|
2079
|
+
|
|
2080
|
+
특정 행을 반응형으로 관찰합니다. 해당 행의 데이터, 선택 상태, CRUD 상태 변경 시 알림.
|
|
2081
|
+
`observeById`는 ID 기반, `observeByIndex`는 sourceIndex 기반으로 접근합니다.
|
|
2082
|
+
|
|
2083
|
+
> `observe(row)`는 행 객체를 받아 같은 효과를 내지만 `enableReferCache: true`가 필요합니다.
|
|
2084
|
+
|
|
2085
|
+
### ReactiveRow
|
|
2086
|
+
|
|
2087
|
+
`Subscribable<Row | undefined>` 인터페이스를 구현하며, 추가 속성/메서드를 제공합니다.
|
|
2088
|
+
|
|
2089
|
+
#### 구독
|
|
2090
|
+
|
|
2091
|
+
```typescript
|
|
2092
|
+
const row = grid.rows.observeById('row-1');
|
|
2093
|
+
const unsub = row.subscribe(() => {
|
|
2094
|
+
console.log('행 변경:', row.data);
|
|
2095
|
+
});
|
|
2096
|
+
```
|
|
2097
|
+
|
|
2098
|
+
#### 속성 (getter)
|
|
2099
|
+
|
|
2100
|
+
| 속성 | 타입 | 설명 |
|
|
2101
|
+
|------|------|------|
|
|
2102
|
+
| `id` | `string \| number` | 행 식별자 |
|
|
2103
|
+
| `data` | `Record<string, unknown>` | 현재 행 데이터 |
|
|
2104
|
+
| `exists` | `boolean` | 행 존재 여부 |
|
|
2105
|
+
| `state` | `RowState` | CRUD 상태 (`'pristine' \| 'added' \| 'modified' \| 'deleted'`) |
|
|
2106
|
+
| `selected` | `boolean` | 선택 여부 |
|
|
2107
|
+
| `readOnly` | `boolean` | 읽기 전용 여부 |
|
|
2108
|
+
| `visible` | `boolean` | 가시성 여부 |
|
|
2109
|
+
| `pinned` | `'top' \| 'bottom' \| null` | 고정 상태 |
|
|
2110
|
+
| `cssClass` | `string \| null` | CSS 클래스 |
|
|
2111
|
+
| `height` | `number \| null` | 행 높이 (px) |
|
|
2112
|
+
| `changedFields` | `string[] \| null` | 변경된 필드 목록 |
|
|
2113
|
+
|
|
2114
|
+
#### 쓰기 메서드
|
|
2115
|
+
|
|
2116
|
+
| 메서드 | 설명 |
|
|
2117
|
+
|--------|------|
|
|
2118
|
+
| `setValue(field, value)` | 단일 필드 값 갱신 (내부적으로 dirty 셀 수정 — `updateCellDirty`로 위임) |
|
|
2119
|
+
| `setValue(patch)` | 여러 필드를 객체로 일괄 갱신 (2개 이상이면 `beginBatch`/`endBatch`로 묶여 Undo 1단위) |
|
|
2120
|
+
| `setProps(props)` | 행 속성(고정, CSS 클래스, 높이 등) 갱신 |
|
|
2121
|
+
|
|
2122
|
+
#### 셀 접근
|
|
2123
|
+
|
|
2124
|
+
```typescript
|
|
2125
|
+
const cell = row.cell('name');
|
|
2126
|
+
```
|
|
2127
|
+
|
|
2128
|
+
### ReactiveCell
|
|
2129
|
+
|
|
2130
|
+
| 메서드 | 설명 |
|
|
2131
|
+
|--------|------|
|
|
2132
|
+
| `subscribe(cb)` | 셀 값 변경 구독 |
|
|
2133
|
+
| `getSnapshot()` | 현재 셀 값 |
|
|
2134
|
+
| `set(value)` | 셀 값 설정 |
|
|
2135
|
+
|
|
2136
|
+
```typescript
|
|
2137
|
+
// React에서 사용
|
|
2138
|
+
function useCellValue(grid: PureSheet, rowId: string, field: string) {
|
|
2139
|
+
const cell = useMemo(() => grid.rows.observeById(rowId).cell(field), [grid, rowId, field]);
|
|
2140
|
+
return useSyncExternalStore(cell.subscribe, cell.getSnapshot);
|
|
2141
|
+
}
|
|
2142
|
+
```
|
|
2143
|
+
|
|
2144
|
+
---
|
|
2145
|
+
|
|
2146
|
+
## 이벤트
|
|
2147
|
+
|
|
2148
|
+
### 이벤트 구독
|
|
2149
|
+
|
|
2150
|
+
```typescript
|
|
2151
|
+
const unsubscribe = grid.on('cell:click', (payload) => {
|
|
2152
|
+
console.log(payload.columnId, payload.value);
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
// 구독 해제
|
|
2156
|
+
unsubscribe();
|
|
2157
|
+
// 또는
|
|
2158
|
+
grid.off('cell:click', handler);
|
|
2159
|
+
```
|
|
2160
|
+
|
|
2161
|
+
### 이벤트 목록
|
|
2162
|
+
|
|
2163
|
+
#### 데이터
|
|
2164
|
+
|
|
2165
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2166
|
+
|--------|---------|------|
|
|
2167
|
+
| `data:loaded` | `{ rowCount, columnCount }` | 데이터 로드 완료 |
|
|
2168
|
+
|
|
2169
|
+
#### 행
|
|
2170
|
+
|
|
2171
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2172
|
+
|--------|---------|------|
|
|
2173
|
+
| `row:click` | `{ row, rowIndex, event }` | 행 클릭 |
|
|
2174
|
+
| `row:dblclick` | `{ row, rowIndex, event }` | (예약) 현재 코드 경로에서는 발행되지 않음 — `cell:dblclick`을 사용하세요 |
|
|
2175
|
+
| `row:pinned` | `{ row, position }` | 행 고정 |
|
|
2176
|
+
| `row:unpinned` | `{ rowId, position? }` | 행 고정 해제 |
|
|
2177
|
+
| `row:reordered` | `RowReorderedEvent` (discriminated, 아래) | 행/노드 재정렬 완료 (드래그 또는 프로그램 API) |
|
|
2178
|
+
| `row:dragOut` | `{ row, sourceGridId, ... }` | 다른 그리드로 드래그아웃 (ADR-108, 크로스-그리드 드롭) |
|
|
2179
|
+
| `row:dragIn` | `{ row, targetGridId, ... }` | 다른 그리드에서 드래그인 (ADR-108) |
|
|
2180
|
+
|
|
2181
|
+
**`RowReorderedEvent`** — `kind`로 분기하는 discriminated union (flat 그리드 vs 트리 그리드).
|
|
2182
|
+
|
|
2183
|
+
| `kind` | 필드 | 설명 |
|
|
2184
|
+
|--------|------|------|
|
|
2185
|
+
| `'flat'` | `commandId`, `fromIndex`, `toIndex` | 평탄 그리드 행 재정렬 |
|
|
2186
|
+
| `'tree'` | `commandId`, `rowIds`, `patches`, `dropMode` | 트리 노드 재정렬. `patches`=`TreeMovePatch[]`(promoted root만, old/new parent+sibling 포함), `dropMode`=`'before'\|'after'\|'asChild'` |
|
|
2187
|
+
|
|
2188
|
+
> `commandId`는 이 재정렬을 감싼 **BatchCommand.id**입니다 — [`undoManager.undoThrough(commandId)`](#gridundomanager--undoredo-api)의 correlation key로 그대로 사용하세요 (낙관적 파이프라인 실패 롤백).
|
|
2189
|
+
> `row:reordered`는 **정방향(드래그/`moveNode`)에서만** 발화합니다. undo/redo 시엔 발화하지 않으므로, 백엔드 동기화는 [`changes:undone`/`changes:redone`](#undoredo)도 함께 구독해야 합니다.
|
|
2190
|
+
|
|
2191
|
+
#### 셀
|
|
2192
|
+
|
|
2193
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2194
|
+
|--------|---------|------|
|
|
2195
|
+
| `cell:click` | `{ row, columnId, value, event }` | 셀 클릭 |
|
|
2196
|
+
| `cell:dblclick` | `{ row, columnId, value, event }` | 셀 더블클릭 |
|
|
2197
|
+
| `cell:change` | `{ rowIndex, rowId, columnId, oldValue, newValue }` | 셀 값 변경. `rowId`는 정렬/필터 무관 안정 식별자(정규화된 문자열) — cascade 등 rowId 기반 로직의 트리거로 사용 |
|
|
2198
|
+
| `cell:beforeEdit` | `{ ..., cancel() }` | 편집 시작 전 (취소 가능) |
|
|
2199
|
+
| `cell:editCommitting` | `{ ..., cancel() }` | 편집 커밋 전 (취소 가능) |
|
|
2200
|
+
| `cell:editCommitted` | `{ rowIndex, rowId, dataIndex, columnId, oldValue, newValue }` | 편집 커밋 완료. `rowId` 포함 (위와 동일) — boolean 셀 즉시 토글도 이 이벤트로 발화되어 체크박스 cascade 트리거로 쓰인다 |
|
|
2201
|
+
| `cell:tooltip` | `CellTooltipPayload` (아래 참조) | 셀·컬럼 헤더 호버 시 툴팁 표시 요청 (`tooltipMode` 로 발행 제어) |
|
|
2202
|
+
|
|
2203
|
+
**`CellTooltipPayload`** (26cd68a 커밋에서 validation 정보 통합)
|
|
2204
|
+
|
|
2205
|
+
| 필드 | 타입 | 설명 |
|
|
2206
|
+
|------|------|------|
|
|
2207
|
+
| `area` | `'cell' \| 'columnHeader'` | 영역 — **셀뿐 아니라 컬럼 헤더 호버 시에도 발행** |
|
|
2208
|
+
| `rowIndex?` | `number` | 행 인덱스 (dataIndex 기준). 헤더에선 `undefined` |
|
|
2209
|
+
| `rowId?` | `string \| number` | 행 ID (피벗 모드/헤더에선 `undefined` 가능) |
|
|
2210
|
+
| `columnId?` | `string` | 컬럼 ID. 그룹/멀티레벨 헤더 셀에선 `undefined` 가능 |
|
|
2211
|
+
| `groupId?` | `string` | 그룹/멀티레벨/피벗 헤더 그룹 식별자 (`columnHeader` 에서만) |
|
|
2212
|
+
| `value?` | `unknown` | 원본 셀 값. 헤더에는 없음 |
|
|
2213
|
+
| `formattedValue` | `string` | 렌더링된 텍스트 (헤더는 컬럼 라벨) |
|
|
2214
|
+
| `tooltipText?` | `string` | 컬럼 커스텀 툴팁 문구(`ColumnDef.headerTooltip`/`cellTooltip`). 있으면 `formattedValue` 대신 표시에 우선 |
|
|
2215
|
+
| `isOverflowing` | `boolean` | 셀 내용이 잘려서 표시되는지 |
|
|
2216
|
+
| `cellRect` | `DOMRect` | 셀/헤더 DOM 영역 |
|
|
2217
|
+
| `element` | `HTMLElement` | 셀/헤더 DOM 요소 |
|
|
2218
|
+
| `errors?` | `string[]` | 유효성 검사 에러 메시지 (없으면 `undefined`). **셀 전용** |
|
|
2219
|
+
| `hasError?` | `boolean` | 에러 존재 여부 빠른 플래그. **셀 전용**(헤더엔 없음) |
|
|
2220
|
+
|
|
2221
|
+
> **컬럼 헤더 툴팁**: `area: 'columnHeader'` 페이로드는 헤더 호버 시 발행됩니다(셀 전용 필드 `rowId`/`value`/`errors`/`hasError`는 비어 있음). `tooltipText`가 있으면(= `ColumnDef.headerTooltip`/`cellTooltip` 지정) 내장 툴팁·커스텀 렌더러 모두 `formattedValue` 대신 이 문구를 표시하며, overflow 여부와 무관하게 항상 표시합니다. (발행 자체는 `tooltipMode !== 'none'` 필요.)
|
|
2222
|
+
|
|
2223
|
+
**tooltipMode 옵션** (PureSheetConfig)
|
|
2224
|
+
|
|
2225
|
+
| 값 | 설명 |
|
|
2226
|
+
|---|---|
|
|
2227
|
+
| `'none'` (default) | 이벤트 발행 안 함 |
|
|
2228
|
+
| `'overflow'` | 셀 내용이 잘리는 경우에만 |
|
|
2229
|
+
| `'always'` | 모든 셀 호버마다 |
|
|
2230
|
+
|
|
2231
|
+
커스텀 툴팁 구현 시 `errors` 필드로 바로 에러 메시지를 렌더링할 수 있습니다:
|
|
2232
|
+
|
|
2233
|
+
```js
|
|
2234
|
+
grid.on('cell:tooltip', ({ element, cellRect, value, errors, hasError }) => {
|
|
2235
|
+
if (hasError) {
|
|
2236
|
+
showCustomTooltip(element, cellRect, {
|
|
2237
|
+
type: 'error',
|
|
2238
|
+
lines: errors,
|
|
2239
|
+
});
|
|
2240
|
+
} else {
|
|
2241
|
+
showCustomTooltip(element, cellRect, { text: String(value) });
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
```
|
|
2245
|
+
|
|
2246
|
+
**컬럼별 커스텀 툴팁 문구** — `ColumnDef.headerTooltip`(헤더, 고정 문자열)과 `cellTooltip`(셀, 행별 동적 함수)을 컬럼 정의에 지정하면 `payload.tooltipText`로 전달돼 내장 툴팁·커스텀 렌더러 모두 라벨/값 대신 이 문구를 표시합니다(overflow 무관 항상 표시, `tooltipMode !== 'none'` 필요). `cellTooltip`은 **hover 시점에만** 호출되어(렌더/스크롤 시 전 셀 호출 아님) 대용량에서도 성능 부담이 없습니다.
|
|
2247
|
+
|
|
2248
|
+
```js
|
|
2249
|
+
{
|
|
2250
|
+
fields: [
|
|
2251
|
+
{
|
|
2252
|
+
id: 'salary', header: '급여', dataType: 'number',
|
|
2253
|
+
headerTooltip: '세전 월 급여 기준', // 헤더 hover 시
|
|
2254
|
+
cellTooltip: ({ value, row }) => `${row.name}: ${value}원`, // 셀 hover 시 (행별)
|
|
2255
|
+
},
|
|
2256
|
+
],
|
|
2257
|
+
tooltipMode: 'always', // 또는 'overflow' — 'none' 이면 툴팁 이벤트 자체가 발행 안 됨
|
|
2258
|
+
}
|
|
2259
|
+
```
|
|
2260
|
+
|
|
2261
|
+
#### 선택
|
|
2262
|
+
|
|
2263
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2264
|
+
|--------|---------|------|
|
|
2265
|
+
| `selection:changed` | `{ selectedRows, selectedCells }` | 선택 변경 |
|
|
2266
|
+
|
|
2267
|
+
#### 컬럼
|
|
2268
|
+
|
|
2269
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2270
|
+
|--------|---------|------|
|
|
2271
|
+
| `column:resize` | `{ columnId, width }` | 컬럼 너비 변경 |
|
|
2272
|
+
| `column:reorder` | `{ order, source, sourceId? }` | 컬럼 순서 변경 (`source`: `'order'` \| `'column'` \| `'group'` \| `'groupOrder'`) |
|
|
2273
|
+
| `column:pin` | `{ columnId, position }` | 컬럼 고정 변경 |
|
|
2274
|
+
| `column:visibility` | `{ columnId, visible }` | 컬럼 가시성 변경 |
|
|
2275
|
+
| `column:update` | `{ columnId, changedKeys: string[] }` | `columns.update()` 호출 — `changedKeys` 는 사용자가 넘긴 partial 의 키 목록 |
|
|
2276
|
+
| `column:reset` | `{ columnId: string \| null }` | `columns.reset(id)` 또는 `resetAll()` (resetAll 시 `columnId: null`) |
|
|
2277
|
+
|
|
2278
|
+
#### 정렬/필터
|
|
2279
|
+
|
|
2280
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2281
|
+
|--------|---------|------|
|
|
2282
|
+
| `sort:changed` | `{ sorts }` | 정렬 상태 변경 |
|
|
2283
|
+
| `filter:changed` | `{ filters }` | 필터 상태 변경 |
|
|
2284
|
+
| `header:click` | `{ columnId, event, nodeId? }` | 헤더 **셀 본체**(텍스트/여백) 클릭. `event`는 원본 DOM `MouseEvent` — 소비처가 `event.shiftKey` / `ctrlKey` / `altKey` / `metaKey` 모디파이어를 직접 읽음. **core는 이 이벤트만 발행하고 자동 정렬하지 않음** — 정렬은 소비처가 `grid.toggleSort(columnId, { additive: event.shiftKey })`를 호출해 처리 |
|
|
2285
|
+
| `header:sortClick` | `{ columnId, event, element }` | 헤더 **정렬 아이콘**(`.ps-sort-icon`) 클릭 (ITSM-2026-002185). `element`는 아이콘 DOM — 팝오버 앵커로 씀 |
|
|
2286
|
+
| `header:filterClick` | `{ columnId, event, element }` | 헤더 **필터 아이콘**(`.ps-filter-icon`) 클릭 (ITSM-2026-002185). `element`는 아이콘 DOM — 값 목록/조건식 팝오버 앵커로 씀 |
|
|
2287
|
+
|
|
2288
|
+
> **헤더 아이콘 클릭은 셀 본체 클릭과 분리됩니다 (ITSM-2026-002185).** 아이콘 클릭은 `HeaderCell` 에서 `stopPropagation` 되므로 `header:click` 과 **동시에 발생하지 않습니다**. 세 이벤트는 배타적이며, 소비처(wrapper)는 각각을 정렬 토글 / 정렬 패널 / 필터 팝오버로 배선합니다.
|
|
2289
|
+
>
|
|
2290
|
+
> ⚠️ 필드에 `showFilterIcon: false` 를 주면 아이콘 span 자체가 생성되지 않으므로 그 컬럼에서는 `header:filterClick` 이 **발생하지 않습니다**(→ [FieldDef 정렬/필터](#정렬필터)).
|
|
2291
|
+
|
|
2292
|
+
#### 스크롤
|
|
2293
|
+
|
|
2294
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2295
|
+
|--------|---------|------|
|
|
2296
|
+
| `scroll` | `ScrollPayload` | 세로 range 변경 시 발행. `scrollTop`, `scrollLeft`, `startIndex`, `endIndex`, `viewportHeight`, `contentHeight`, `maxScrollTop`, `totalRows`/`rowCount`, `isAtEnd` 포함 |
|
|
2297
|
+
| `scroll:endReached` | `ScrollEndReachedPayload` | 끝(또는 `endReachedThresholdRows` 이내) 도달 시 데이터셋 경계별 1회 발행. `boundaryRowCount`는 append로 행 수가 증가하면 재발화 가능한 새 경계를 나타냄 |
|
|
2298
|
+
|
|
2299
|
+
> **동작 변경 (정렬 정책 이관).** 이전에는 헤더 클릭 시 core가 자동으로 정렬을 토글했지만, 이제 core는 `header:click`만 발행합니다. "헤더 클릭 = 정렬"이 자동으로 동작하던 통합 코드는 `header:click`을 구독해 [`toggleSort()`](#puresheet-최상위-api)를 직접 호출하도록 변경해야 합니다.
|
|
2300
|
+
>
|
|
2301
|
+
> **헤더 필터 인디케이터.** 활성 필터가 걸린 컬럼의 헤더에는 필터 아이콘이 표시됩니다(필터 상태가 정렬 화살표 동기화와 동일한 패턴으로 헤더에 동기화됨).
|
|
2302
|
+
|
|
2303
|
+
#### Undo/Redo
|
|
2304
|
+
|
|
2305
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2306
|
+
|--------|---------|------|
|
|
2307
|
+
| `undo:stateChange` | UndoStack 상태 | Undo/Redo **버튼 활성화** 상태 변경 (`canUndo`/`canRedo`) |
|
|
2308
|
+
| `changes:undone` | `ChangesUndoneEvent` (아래 참조) | Undo 완료 — 되돌린 변경의 구조화 payload (`direction: 'undo'`) |
|
|
2309
|
+
| `changes:redone` | `ChangesRedoneEvent` (아래 참조) | Redo 완료 — 다시 적용한 변경의 구조화 payload (`direction: 'redo'`) |
|
|
2310
|
+
|
|
2311
|
+
> **`row:reordered`만 구독하면 undo/redo 시 백엔드와 어긋납니다.** `row:reordered`는 사용자의 정방향 드래그에서만 발화하고, 이후 Ctrl+Z(undo)/Ctrl+Y(redo)로 상태가 되돌아가도 **재발화하지 않습니다**. 서버 상태를 그리드와 동기화하려면 `changes:undone` / `changes:redone`을 함께 구독하세요 (ADR-115).
|
|
2312
|
+
|
|
2313
|
+
**`ChangesUndoneEvent` / `ChangesRedoneEvent` payload** (공통 베이스 `UndoRedoEventBase`)
|
|
2314
|
+
|
|
2315
|
+
| 필드 | 타입 | 설명 |
|
|
2316
|
+
|------|------|------|
|
|
2317
|
+
| `direction` | `'undo' \| 'redo'` | 이벤트가 undo 완료인지 redo 완료인지. 하나의 공통 핸들러로 두 이벤트를 처리 가능 |
|
|
2318
|
+
| `commandType` | `CommandType` | 처리된 command 타입 (`command.type`) |
|
|
2319
|
+
| `description` | `string` | command 설명 (로그/디버깅용) |
|
|
2320
|
+
| `canUndo` | `boolean` | 발화 시점의 undo 가능 여부 |
|
|
2321
|
+
| `canRedo` | `boolean` | 발화 시점의 redo 가능 여부 |
|
|
2322
|
+
| `change` | `UndoRedoChange` | 구조화된 변경 내역 (discriminated union, `kind` discriminator) |
|
|
2323
|
+
|
|
2324
|
+
**`change.kind`별 variant**
|
|
2325
|
+
|
|
2326
|
+
| `kind` | 발생 command | 주요 필드 |
|
|
2327
|
+
|--------|-------------|----------|
|
|
2328
|
+
| `'cell-edit'` | `updateCell` | `rowId`, `field`, `oldValue`, `newValue` |
|
|
2329
|
+
| `'flat-move'` | `reorderRow` | `rowId`, `beforeRowId`, `previousBeforeRowId`, `finalBeforeRowId` (방향별 최종 anchor, `null`=맨 끝) |
|
|
2330
|
+
| `'tree-reorder'` | `treeReorderRow` | `patches`(원본 `TreeMovePatch[]`, old/new 양쪽 포함), `siblingsAfterFinal`(방향별 최종 sibling 순서), `dropMode` |
|
|
2331
|
+
| `'add-row'` | `addRow` | `rowId`, `rowData`, `index`. undo=제거, redo=추가 |
|
|
2332
|
+
| `'delete-row'` | `deleteRow` | `rowId`, `rowData`, `index`. undo=복원, redo=삭제 |
|
|
2333
|
+
| `'undelete-row'` | `undeleteRow` | `rowId`, `rowData`, `index`. `delete-row`의 방향 반대 |
|
|
2334
|
+
| `'discard-row'` | `discardRow` | `rowId`, `wasAdded`/`wasModified`/`wasDeleted`, `restoredData`(undo 복원 스냅샷, 없으면 `null`) |
|
|
2335
|
+
| `'batch'` | `batch` | `children: UndoRedoChange[]` (flatten 없음 — `length===1`이어도 유지). **이미 direction-ordered**(undo면 내부에서 reverse 완료)이므로 **순차 적용, 재-reverse 금지** |
|
|
2336
|
+
|
|
2337
|
+
> **direction별 "현재 값" 해석.** `patches` swap이 없으므로(old/new 양쪽 포함) 소비처가 `direction`으로 선택합니다. `cell-edit`: undo→`oldValue`, redo→`newValue`. `tree-reorder`: undo→`patch.oldParentId`, redo→`patch.newParentId`. `siblingsAfterFinal` / `finalBeforeRowId`는 방향 분기 없이 곧바로 쓸 수 있게 **이미 방향별 최종값**으로 계산되어 담깁니다.
|
|
2338
|
+
>
|
|
2339
|
+
> `describeChange`를 구현하지 않은 command는 `changes:undone`/`changes:redone`을 **발화하지 않습니다** (`change`가 non-null 계약).
|
|
2340
|
+
>
|
|
2341
|
+
> **⚠️ 실질 변경(tree/flat reorder 등)은 거의 항상 `kind: 'batch'`로 래핑되어 옵니다.** `TreeReorderCoordinator`가 항상 batch로 감싸고 mutator가 추가 cell-edit(`updatedAt` 등)까지 넣기 때문입니다. `e.change.kind === 'tree-reorder'`처럼 **직접 비교하면 대부분 no-op**(조용한 실패)이 됩니다. 반드시 `@solasheet/core`의 `iterateLeafChanges(e.change)` / `findAllChangesByKind(e.change, kind)` / `findChangeByKind(e.change, kind)`로 batch를 평탄화한 뒤 분기하세요. 같은 kind가 여러 개 담길 수 있으므로(여러 tree-reorder + cell-edit) 하나만 필요한 게 아니면 `findAllChangesByKind`로 전부 수집해 유실을 막습니다.
|
|
2342
|
+
>
|
|
2343
|
+
> **⚠️ `batch.children`은 이미 direction-ordered입니다.** `BatchCommand.describeChange`가 undo 방향일 때 내부에서 이미 역순 정렬을 마칩니다. 소비처가 undo에서 다시 역순으로 순회하면 **이중 reverse**가 되어 순서가 깨집니다. 헬퍼가 방출한 순서를 **그대로 순차(pre-order) 적용**하세요.
|
|
2344
|
+
|
|
2345
|
+
```js
|
|
2346
|
+
// row:reordered(정방향) + changes:undone + changes:redone 3종 구독 = 완전한 백엔드 동기화
|
|
2347
|
+
import { iterateLeafChanges } from '@solasheet/core';
|
|
2348
|
+
|
|
2349
|
+
// leaf(=batch 아님) 단위 분기. batch/nested-batch는 헬퍼가 평탄화한다.
|
|
2350
|
+
function syncLeaf(change, direction) {
|
|
2351
|
+
switch (change.kind) {
|
|
2352
|
+
case 'cell-edit':
|
|
2353
|
+
if (change.field === 'order') break; // order는 siblingsAfterFinal 파생 → skip
|
|
2354
|
+
api.patch(change.rowId, {
|
|
2355
|
+
[change.field]: direction === 'undo' ? change.oldValue : change.newValue,
|
|
2356
|
+
});
|
|
2357
|
+
break;
|
|
2358
|
+
case 'flat-move':
|
|
2359
|
+
api.moveRow(change.rowId, { before: change.finalBeforeRowId });
|
|
2360
|
+
break;
|
|
2361
|
+
case 'tree-reorder':
|
|
2362
|
+
for (const p of change.patches) {
|
|
2363
|
+
api.setParent(p.rowId, direction === 'undo' ? p.oldParentId : p.newParentId);
|
|
2364
|
+
}
|
|
2365
|
+
for (const [parentId, order] of change.siblingsAfterFinal) {
|
|
2366
|
+
api.reorderChildren(parentId, order); // 이미 방향별 최종 순서 (order의 canonical)
|
|
2367
|
+
}
|
|
2368
|
+
break;
|
|
2369
|
+
case 'add-row':
|
|
2370
|
+
direction === 'undo'
|
|
2371
|
+
? api.deleteRow(change.rowId)
|
|
2372
|
+
: api.insertRow(change.rowData, change.index);
|
|
2373
|
+
break;
|
|
2374
|
+
case 'delete-row':
|
|
2375
|
+
direction === 'undo' ? api.undeleteRow(change.rowId) : api.deleteRow(change.rowId);
|
|
2376
|
+
break;
|
|
2377
|
+
case 'undelete-row':
|
|
2378
|
+
direction === 'undo' ? api.deleteRow(change.rowId) : api.undeleteRow(change.rowId);
|
|
2379
|
+
break;
|
|
2380
|
+
case 'discard-row':
|
|
2381
|
+
if (direction === 'undo' && change.restoredData) api.upsert(change.rowId, change.restoredData);
|
|
2382
|
+
break;
|
|
2383
|
+
// 'batch'는 여기 오지 않는다 — iterateLeafChanges가 이미 평탄화했다.
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// ⚠️ children은 이미 direction-ordered → 헬퍼가 방출한 순서 그대로 순차 적용. 재-reverse 금지.
|
|
2388
|
+
function syncChange(rootChange, direction) {
|
|
2389
|
+
for (const leaf of iterateLeafChanges(rootChange)) syncLeaf(leaf, direction);
|
|
2390
|
+
}
|
|
2391
|
+
grid.on('changes:undone', (e) => syncChange(e.change, 'undo'));
|
|
2392
|
+
grid.on('changes:redone', (e) => syncChange(e.change, 'redo'));
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
#### 클립보드
|
|
2396
|
+
|
|
2397
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2398
|
+
|--------|---------|------|
|
|
2399
|
+
| `clipboard:copy` | `{ text, rowCount, colCount }` | 복사 |
|
|
2400
|
+
| `clipboard:paste` | `{ cellCount }` | 붙여넣기 |
|
|
2401
|
+
|
|
2402
|
+
#### 그룹 (로우 그룹)
|
|
2403
|
+
|
|
2404
|
+
> 로우 그룹 토글 이벤트는 헤더 클릭(`source: 'user'`)과 프로그래매틱 API(`grid.rowGroup.*`, `source: 'api'`) 모두에서 발화됩니다. 프로그래매틱 호출이 다시 이벤트를 발생시키므로, 핸들러 안에서 재진입 무한루프를 막으려면 `if (e.source === 'api') return;`를 사용하세요.
|
|
2405
|
+
> **참고**: `grid.rowGroup.expandToLevel`/`collapseToLevel`(레벨까지 누적)은 `rowGroup:batchToggle`을 `scope:'toLevel'`로 발화합니다. (`expandAtLevel`/`collapseAtLevel`은 `scope:'level'`, `expandAll`/`collapseAll`은 `scope:'all'`.)
|
|
2406
|
+
|
|
2407
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2408
|
+
|--------|---------|------|
|
|
2409
|
+
| `rowGroup:toggle` | `{ groupId, collapsed, source }` | 단일 로우 그룹 토글. 헤더 클릭 또는 `grid.rowGroup.toggle/expand/collapse`. `source`=`'user'`(클릭)/`'api'`(프로그래매틱) |
|
|
2410
|
+
| `rowGroup:batchToggle` | `{ scope, collapsed, groupIds, level?, source }` | 여러 그룹 일괄 토글. 영향 그룹이 N개여도 **이벤트 1회로 요약 통지**(`groupIds`에 화면 밖 포함 전체 그룹 ID). `scope`로 구분: `'level'`(같은 depth 일괄 — Ctrl/Cmd+클릭 또는 `grid.rowGroup.expandAtLevel/collapseAtLevel`, `level` 포함), `'toLevel'`(특정 depth까지 누적 — `grid.rowGroup.expandToLevel/collapseToLevel`, `level` 포함), `'all'`(전체 — `grid.rowGroup.expandAll/collapseAll`, `level` undefined). `source`=`'user'`/`'api'` |
|
|
2411
|
+
| `rowGroup:checkbox` | `{ groupId, columnId, checked, memberCount }` | 그룹 체크박스 cascade — 그룹 헤더 체크박스로 하위 멤버 전체를 일괄 토글. `memberCount`=영향받은 데이터 행 수. (구 `group:checkbox`에서 리네임) |
|
|
2412
|
+
|
|
2413
|
+
#### 검증
|
|
2414
|
+
|
|
2415
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2416
|
+
|--------|---------|------|
|
|
2417
|
+
| `validation:changed` | 검증 에러 정보 | 검증 상태 변경 |
|
|
2418
|
+
|
|
2419
|
+
#### 컬럼 그룹
|
|
2420
|
+
|
|
2421
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2422
|
+
|--------|---------|------|
|
|
2423
|
+
| `columnGroup:toggle` | `{ groupId, collapsed, source? }` | 컬럼 그룹 토글 (구 `headerGroup:toggle`). 헤더 클릭 시 `source: 'user'` 포함, `grid.headers`의 프로그래매틱 토글 시엔 `source` 없음(`{ groupId, collapsed }`) |
|
|
2424
|
+
|
|
2425
|
+
#### 스크롤
|
|
2426
|
+
|
|
2427
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2428
|
+
|--------|---------|------|
|
|
2429
|
+
| `scroll` | `{ scrollTop, scrollLeft }` | (예약) 현재 코드 경로에서는 발행되지 않음. 스크롤 상태는 `grid.select(s => s.scrollTop / visibleStartIndex / visibleEndIndex)`로 구독하세요 |
|
|
2430
|
+
|
|
2431
|
+
#### 검색 / 구조 변경
|
|
2432
|
+
|
|
2433
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2434
|
+
|--------|---------|------|
|
|
2435
|
+
| `search:highlighted` | `{ keyword }` | 검색 매칭 셀 하이라이트가 갱신됨 (검색 실행/이동/취소 시 모두 발행). 빈 `keyword`는 해제 의미 |
|
|
2436
|
+
| `view:structureChanged` | `{ reason: 'sort' \| 'filter' \| 'group' \| 'hide' \| 'pin' \| 'expand' \| 'pivot-collapse' \| string }` | 뷰 구조가 변경됨 — 외부 의존 레이아웃(예: 별도 차트)의 재계산 트리거. `'pivot-collapse'`는 피벗 행/열 collapse·expand 시 발화 |
|
|
2437
|
+
|
|
2438
|
+
#### 컨텍스트 메뉴
|
|
2439
|
+
|
|
2440
|
+
| 이벤트 | 페이로드 | 설명 |
|
|
2441
|
+
|--------|---------|------|
|
|
2442
|
+
| `contextmenu` | `{ area, rowIndex?, dataIndex?, rowId?, columnId?, value?, rowData?, selectedRowIds, selectedCells, selectedPinnedCells, originalEvent }` | 우클릭 메뉴. `area`: `'cell' \| 'header' \| 'rowHeader' \| 'empty'`. `rowData`는 cell/rowHeader에서, `selectedPinnedCells`는 pinned 영역 좌표 |
|
|
2443
|
+
|
|
2444
|
+
---
|
|
2445
|
+
|
|
2446
|
+
## 타입 정의
|
|
2447
|
+
|
|
2448
|
+
### ColumnGroupDef
|
|
2449
|
+
|
|
2450
|
+
> 여러 컬럼을 하나의 상위 헤더로 묶는 **다단계 컬럼 헤더(밴딩)** 정의. flat 모드 `columnGroups` 및 `grid.headers.setColumnGroups(defs)`에 사용. (구 명칭 `HeaderGroupDef`.)
|
|
2451
|
+
|
|
2452
|
+
```typescript
|
|
2453
|
+
interface ColumnGroupDef {
|
|
2454
|
+
id: string; // 그룹 고유 ID
|
|
2455
|
+
header: string; // 헤더에 표시할 이름
|
|
2456
|
+
collapsible?: boolean; // 접기/펼치기 가능 여부 (기본: false)
|
|
2457
|
+
collapseTo?: string; // 접혔을 때 보이는 대표 컬럼 ID (미지정 시 첫 번째 리프)
|
|
2458
|
+
collapsed?: boolean; // 초기 접힘 상태 (기본: false)
|
|
2459
|
+
lockToGroup?: boolean; // 자식 컬럼을 그룹에 잠금 (기본: true)
|
|
2460
|
+
// true: 자식 컬럼이 그룹 밖으로 드래그 이동 불가
|
|
2461
|
+
// false: 그룹 경계를 넘어 자유롭게 이동 가능
|
|
2462
|
+
children: (string | ColumnGroupDef)[]; // 하위 자식 (필드 ID 또는 하위 그룹)
|
|
2463
|
+
headerAttributes?: CellAttributes; // 컬럼 그룹 헤더 셀 HTML 속성 (class, style 등)
|
|
2464
|
+
}
|
|
2465
|
+
```
|
|
2466
|
+
|
|
2467
|
+
### TreeConfig
|
|
2468
|
+
|
|
2469
|
+
```typescript
|
|
2470
|
+
interface TreeConfig {
|
|
2471
|
+
idField?: string; // 노드 ID 필드 (기본: 'id')
|
|
2472
|
+
parentField?: string; // 부모 ID 필드 (기본: 'parentId')
|
|
2473
|
+
childrenField?: string; // 자식 배열 필드 (중첩 구조용)
|
|
2474
|
+
treeColumn?: string; // 트리 아이콘 표시 컬럼 (기본: 첫 컬럼)
|
|
2475
|
+
defaultCollapsed?: boolean; // 기본 접힘 상태
|
|
2476
|
+
defaultExpandLevel?: number; // 기본 확장 레벨 (0=루트만, Infinity=전체)
|
|
2477
|
+
aggregates?: Record<string, AggregateFn>; // 집계 설정
|
|
2478
|
+
lazyLoad?: boolean; // 서버 lazy loading
|
|
2479
|
+
fetchChildren?: (nodeId: string | number) => Promise<Row[]>; // 자식 fetch 함수
|
|
2480
|
+
}
|
|
2481
|
+
```
|
|
2482
|
+
|
|
2483
|
+
### MasterDetailConfig
|
|
2484
|
+
|
|
2485
|
+
```typescript
|
|
2486
|
+
interface MasterDetailConfig {
|
|
2487
|
+
renderer: (
|
|
2488
|
+
container: HTMLElement,
|
|
2489
|
+
parentData: Row,
|
|
2490
|
+
dataIndex: number,
|
|
2491
|
+
) => (() => void) | void; // 디테일 렌더러 (cleanup 반환 가능)
|
|
2492
|
+
minHeight?: number; // 최소 높이 (기본: 100)
|
|
2493
|
+
maxHeight?: number; // 최대 높이
|
|
2494
|
+
singleExpand?: boolean; // 단일 확장 모드 (기본: false)
|
|
2495
|
+
keepExpandedOnDataChange?: boolean; // 데이터 변경 시 확장 유지 (기본: false)
|
|
2496
|
+
isExpandable?: (data: Row, dataIndex: number) => boolean; // 확장 가능 조건
|
|
2497
|
+
expandColumn?: string; // 확장 아이콘 컬럼 (기본: 첫 컬럼)
|
|
2498
|
+
keepMasterDetailRows?: boolean; // DOM 캐시 유지 (기본: true)
|
|
2499
|
+
keepMasterDetailRowsCount?: number; // LRU 캐시 크기 (기본: 10)
|
|
2500
|
+
}
|
|
2501
|
+
```
|
|
2502
|
+
|
|
2503
|
+
### GroupConfig
|
|
2504
|
+
|
|
2505
|
+
초기 그리드 생성 시 `FlatModeConfig.rowGroup`으로 전달하는 가벼운 설정 (구 `group` 필드는 deprecated 별칭). *런타임*에는 `grid.rowGroup.setConfig()`로 더 풍부한 [GroupingConfig](#groupingconfig)를 사용합니다.
|
|
2506
|
+
|
|
2507
|
+
```typescript
|
|
2508
|
+
interface GroupConfig {
|
|
2509
|
+
/** 그룹핑 기준 컬럼 (계층 순서) */
|
|
2510
|
+
columns: string[];
|
|
2511
|
+
/** 소계 표시할 컬럼 ID 목록 — 각 컬럼의 FieldDef.aggregate 함수를 사용 */
|
|
2512
|
+
subtotals?: string[];
|
|
2513
|
+
}
|
|
2514
|
+
```
|
|
2515
|
+
|
|
2516
|
+
#### 그룹 행 rowHeader 체크박스
|
|
2517
|
+
|
|
2518
|
+
`rowSelection.trigger === 'checkbox'` 와 함께 쓰면 그룹 행 rowHeader 에도 체크박스가 표시됩니다(기본 활성).
|
|
2519
|
+
옵션은 **`rowSelection.groupRowCheckbox`** 입니다 — 그룹핑 설정이 아니라 선택 동작의 확장이므로
|
|
2520
|
+
트리 모드의 `selectChildren` 과 같은 자리에 둡니다.
|
|
2521
|
+
|
|
2522
|
+
| 멤버 선택 상태 | 그룹 체크박스 |
|
|
2523
|
+
|---|---|
|
|
2524
|
+
| 전부 선택됨 | `checked` |
|
|
2525
|
+
| 일부만 선택됨 | `indeterminate` |
|
|
2526
|
+
| 아무것도 선택 안 됨 | `unchecked` |
|
|
2527
|
+
|
|
2528
|
+
클릭하면 그룹 멤버 전체를 선택/해제하며(다른 그룹의 선택은 보존), `rowGroup:checkbox` 이벤트가
|
|
2529
|
+
`columnId: '_rowHeader'` 로 발행됩니다.
|
|
2530
|
+
|
|
2531
|
+
그룹 행 자체는 선택 집합에 들어가지 않습니다 — `getSelectedRowIds()` 에는 데이터 행 id 만 담기고,
|
|
2532
|
+
그룹 체크 표시는 멤버 선택으로부터의 **파생값**입니다(상태를 따로 저장하지 않으므로 행 재사용/스크롤
|
|
2533
|
+
후에도 어긋나지 않습니다).
|
|
2534
|
+
|
|
2535
|
+
```typescript
|
|
2536
|
+
rowSelection: { mode: 'multi', trigger: 'checkbox', groupRowCheckbox: false } // 끄면 그룹 행 rowHeader 가 비워짐
|
|
2537
|
+
```
|
|
2538
|
+
|
|
2539
|
+
끄는 경우: 그룹 상태 판정이 그룹당 멤버 수에 비례하므로, 그룹 하나에 수만 행이 들어가는 초대용량
|
|
2540
|
+
화면에서 opt-out 용도로 씁니다(선택 Set 대조라 데이터 조회는 없습니다).
|
|
2541
|
+
|
|
2542
|
+
⚠️ `rowHeader.cellRenderer` 로 커스텀 렌더러를 지정하면 이 기능은 비활성입니다 — 그 칸의 내용은
|
|
2543
|
+
소비처 소유이므로 그리드가 덮어쓰지 않습니다.
|
|
2544
|
+
|
|
2545
|
+
### GroupingConfig
|
|
2546
|
+
|
|
2547
|
+
`grid.rowGroup.setConfig(config)`로 런타임에 사용. 집계 함수와 기본 접힘 상태를 직접 지정할 수 있습니다.
|
|
2548
|
+
|
|
2549
|
+
```typescript
|
|
2550
|
+
interface GroupingConfig {
|
|
2551
|
+
/** 그룹화할 컬럼 키들 (계층 순서) */
|
|
2552
|
+
columns: string[];
|
|
2553
|
+
/** 집계 설정 (컬럼 키 → 집계 함수). AggregateFn은 문자열 또는 콜백 */
|
|
2554
|
+
aggregates?: Record<string, AggregateFn>;
|
|
2555
|
+
/** 기본 접힘 상태 */
|
|
2556
|
+
defaultCollapsed?: boolean;
|
|
2557
|
+
}
|
|
2558
|
+
```
|
|
2559
|
+
|
|
2560
|
+
### EditorConfig
|
|
2561
|
+
|
|
2562
|
+
```typescript
|
|
2563
|
+
interface EditorConfig {
|
|
2564
|
+
type: EditorType;
|
|
2565
|
+
// 'text' | 'number' | 'date' | 'select' | 'checkbox'
|
|
2566
|
+
// | 'dropdown' | 'datepicker' | 'multiselect' | 'custom'
|
|
2567
|
+
options?: EditorOption[]; // dropdown/select 옵션
|
|
2568
|
+
searchable?: boolean; // 검색 활성화 (dropdown, multiselect)
|
|
2569
|
+
placeholder?: string; // placeholder 텍스트
|
|
2570
|
+
dateFormat?: string; // datepicker 포맷 (기본: 'yyyy-MM-dd')
|
|
2571
|
+
separator?: string; // multiselect 구분자 (기본: ',')
|
|
2572
|
+
renderer?: (params: CustomEditorParams) => HTMLElement; // 커스텀 에디터 렌더러
|
|
2573
|
+
getValue?: (el: HTMLElement) => CellValue; // 커스텀 값 추출
|
|
2574
|
+
destroy?: (el: HTMLElement) => void; // 커스텀 정리
|
|
2575
|
+
}
|
|
2576
|
+
```
|
|
2577
|
+
|
|
2578
|
+
### MaskConfig
|
|
2579
|
+
|
|
2580
|
+
5종 discriminated union. 공통 필드(`type`, `pattern`, `prefix?`, `suffix?`) + 타입별 추가 필드.
|
|
2581
|
+
|
|
2582
|
+
```typescript
|
|
2583
|
+
type MaskConfig =
|
|
2584
|
+
| NumericMaskConfig | StringMaskConfig | DateMaskConfig
|
|
2585
|
+
| SecureMaskConfig | FunctionMaskConfig;
|
|
2586
|
+
|
|
2587
|
+
interface NumericMaskConfig {
|
|
2588
|
+
type: 'numeric';
|
|
2589
|
+
/** 0: 필수 숫자 / #: 선택 숫자 / ,: 천단위 / .: 소수점 */
|
|
2590
|
+
pattern: string;
|
|
2591
|
+
prefix?: string;
|
|
2592
|
+
suffix?: string;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
interface StringMaskConfig {
|
|
2596
|
+
type: 'string';
|
|
2597
|
+
/** 0: 필수 숫자 / A: 필수 문자 / *: 아무 문자 / 기타: 리터럴 (-, /, . 등) */
|
|
2598
|
+
pattern: string;
|
|
2599
|
+
prefix?: string;
|
|
2600
|
+
suffix?: string;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
interface DateMaskConfig {
|
|
2604
|
+
type: 'date';
|
|
2605
|
+
/** YYYY/YY (년) MM (월) DD (일) HH/hh (시) mm (분) ss (초) A (AM/PM) */
|
|
2606
|
+
pattern: string;
|
|
2607
|
+
prefix?: string;
|
|
2608
|
+
suffix?: string;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
interface SecureMaskConfig {
|
|
2612
|
+
type: 'secure';
|
|
2613
|
+
pattern: string; // StringMask와 같은 문법
|
|
2614
|
+
/** 마스킹 문자 @default '*' */
|
|
2615
|
+
maskChar?: string;
|
|
2616
|
+
/** 앞에서 N자리 표시 */
|
|
2617
|
+
visibleFirst?: number;
|
|
2618
|
+
/** 뒤에서 N자리 표시 */
|
|
2619
|
+
visibleLast?: number;
|
|
2620
|
+
prefix?: string;
|
|
2621
|
+
suffix?: string;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
interface FunctionMaskConfig {
|
|
2625
|
+
type: 'function';
|
|
2626
|
+
/** 포맷 함수 — 추가 context(rowData, columnKey)는 피벗 등에서 자동 전달 */
|
|
2627
|
+
formatter: (value: unknown, ...context: any[]) => string;
|
|
2628
|
+
/** 역변환 함수 (스마트 필터링용, 선택) */
|
|
2629
|
+
parser?: (displayValue: string) => unknown;
|
|
2630
|
+
}
|
|
2631
|
+
```
|
|
2632
|
+
|
|
2633
|
+
```typescript
|
|
2634
|
+
// numeric — 1234.5 → "$1,234.50"
|
|
2635
|
+
{ type: 'numeric', pattern: '#,###.00', prefix: '$' }
|
|
2636
|
+
|
|
2637
|
+
// string — "01012345678" → "010-1234-5678"
|
|
2638
|
+
{ type: 'string', pattern: '000-0000-0000' }
|
|
2639
|
+
|
|
2640
|
+
// date — Date → "2024-01-15"
|
|
2641
|
+
{ type: 'date', pattern: 'YYYY-MM-DD' }
|
|
2642
|
+
|
|
2643
|
+
// secure — "1234567890123456" → "1234-****-****-3456"
|
|
2644
|
+
{ type: 'secure', pattern: '0000-0000-0000-0000', visibleFirst: 4, visibleLast: 4 }
|
|
2645
|
+
|
|
2646
|
+
// secure with custom mask char — "9501011234567" → "950101-#######"
|
|
2647
|
+
{ type: 'secure', pattern: '000000-0000000', visibleFirst: 6, maskChar: '#' }
|
|
2648
|
+
|
|
2649
|
+
// function with parser — 1234567 ↔ "1,234,567원"
|
|
2650
|
+
{
|
|
2651
|
+
type: 'function',
|
|
2652
|
+
formatter: (v) => `${(v as number)?.toLocaleString()}원`,
|
|
2653
|
+
parser: (str) => Number(str.replace(/[,원]/g, '')),
|
|
2654
|
+
}
|
|
2655
|
+
```
|
|
2656
|
+
|
|
2657
|
+
### 피벗 필드 타입
|
|
2658
|
+
|
|
2659
|
+
#### PivotRowField
|
|
2660
|
+
|
|
2661
|
+
```typescript
|
|
2662
|
+
interface PivotRowField {
|
|
2663
|
+
field: string; // 원본 데이터 키 — 값을 **읽을** 때 쓴다
|
|
2664
|
+
id?: string; // 식별자 — 미지정 시 field. 이 필드를 **가리킬** 때 쓴다 (ITSM-2026-002188)
|
|
2665
|
+
header?: string; // 표시명
|
|
2666
|
+
width?: number; // 너비
|
|
2667
|
+
minWidth?: number;
|
|
2668
|
+
maxWidth?: number;
|
|
2669
|
+
dataType?: 'string' | 'number' | 'date' | 'boolean'; // 데이터 타입
|
|
2670
|
+
formula?: string | ((row: Record<string, unknown>) => unknown); // 계산된 그룹 축 (집계 전)
|
|
2671
|
+
headerAttributes?: CellAttributes;
|
|
2672
|
+
cellAttributes?: CellAttributes | CellAttributesFn;
|
|
2673
|
+
}
|
|
2674
|
+
// 참고: 피벗 정렬은 field-level 옵션이 아니라 viewState 단일 진입점(SSOT)으로 적용한다 —
|
|
2675
|
+
// grid.sort([{ id: 'region', direction: 'asc', sequence: 1 }]). PivotRowField 에 sort/descending
|
|
2676
|
+
// 같은 필드를 넣어도 silently 무시된다.
|
|
2677
|
+
```
|
|
2678
|
+
|
|
2679
|
+
#### PivotColumnField
|
|
2680
|
+
|
|
2681
|
+
```typescript
|
|
2682
|
+
interface PivotColumnField {
|
|
2683
|
+
field: string;
|
|
2684
|
+
id?: string; // 식별자 — 미지정 시 field
|
|
2685
|
+
header?: string;
|
|
2686
|
+
width?: number;
|
|
2687
|
+
minWidth?: number;
|
|
2688
|
+
maxWidth?: number;
|
|
2689
|
+
dataType?: 'string' | 'number' | 'date' | 'boolean'; // 데이터 타입
|
|
2690
|
+
formula?: string | ((row: Record<string, unknown>) => unknown); // 계산된 열 축 (집계 전)
|
|
2691
|
+
headerAttributes?: CellAttributes;
|
|
2692
|
+
}
|
|
2693
|
+
// 참고: 열 축 정렬도 viewState 단일 진입점으로 — grid.sort([{ id, direction, sequence }]).
|
|
2694
|
+
// PivotColumnField 에 sort/sortByValue 를 넣어도 무시된다.
|
|
2695
|
+
```
|
|
2696
|
+
|
|
2697
|
+
> **`PivotFieldBase.id` — 값 필드 중복 시 필수 (ITSM-2026-002188)**
|
|
2698
|
+
> 플랫 그리드는 `FieldDef.id` 가 식별자인데 피벗은 `field` 가 그 역할을 겸해 왔습니다. 그런데 **같은 `field` 를 값 필드로 여러 번 넣을 수 있어서**(합계·평균 등) 식별자로서 깨집니다.
|
|
2699
|
+
>
|
|
2700
|
+
> `id` 는 **optional** 이며 미지정 시 `field` 로 읽힙니다 — 기존 설정은 그대로 동작합니다. 중복이 필요한 쪽에서만 불변 `id` 를 명시하세요. 자세한 규칙과 폴백 경고는 [컬럼 식별자 유틸](#컬럼-식별자-유틸--columnkeys) 참조.
|
|
2701
|
+
|
|
2702
|
+
#### PivotValueField
|
|
2703
|
+
|
|
2704
|
+
```typescript
|
|
2705
|
+
interface PivotValueField {
|
|
2706
|
+
field: string;
|
|
2707
|
+
id?: string; // 식별자 — 같은 field 를 여러 번 쓸 때 **반드시** 지정
|
|
2708
|
+
header?: string;
|
|
2709
|
+
width?: number;
|
|
2710
|
+
minWidth?: number;
|
|
2711
|
+
maxWidth?: number;
|
|
2712
|
+
aggregate?: AggregateFunc; // 집계 함수
|
|
2713
|
+
dataType?: 'string' | 'number' | 'date' | 'boolean';
|
|
2714
|
+
showValueAs?: ShowValueAs; // 값 표시 변환
|
|
2715
|
+
showOriginalValue?: boolean; // 원본 값 병행 표시
|
|
2716
|
+
recalculateShowValueAs?: boolean; // 행 sort/filter 시 재계산 여부 (미지정 시 메트릭별 default)
|
|
2717
|
+
decimalFormat?: { places: number; mode?: 'round' | 'floor' | 'ceil' | 'truncate' };
|
|
2718
|
+
/**
|
|
2719
|
+
* showValueAs 변환값에도 컬럼의 `decimalFormat`을 강제 적용할지.
|
|
2720
|
+
* 기본 `false` — percent/rank 등 파생 metric에는 메트릭별 default 소수점 자리수 사용
|
|
2721
|
+
* (예: percent → 2자리). `true`이면 사용자 `decimalFormat`이 변환값에도 적용됨.
|
|
2722
|
+
* 단위(unit)와 정밀도(precision)의 분리 정책. ADR-112 참조.
|
|
2723
|
+
*/
|
|
2724
|
+
useColumnDecimalForShowValueAs?: boolean;
|
|
2725
|
+
useThousandsSeparator?: boolean; // 천단위 구분자 (기본: true)
|
|
2726
|
+
mask?: MaskConfig;
|
|
2727
|
+
autoFit?: boolean;
|
|
2728
|
+
align?: CellAlign;
|
|
2729
|
+
cellAttributes?: CellAttributes | CellAttributesFn;
|
|
2730
|
+
headerAttributes?: CellAttributes;
|
|
2731
|
+
}
|
|
2732
|
+
```
|
|
2733
|
+
|
|
2734
|
+
#### ShowValueAs
|
|
2735
|
+
|
|
2736
|
+
| 값 | 설명 |
|
|
2737
|
+
|----|------|
|
|
2738
|
+
| `'none'` | 원본 집계값 |
|
|
2739
|
+
| `'differenceFromPrevRow'` | 이전 행과의 차이 |
|
|
2740
|
+
| `'percentDifferenceFromPrevRow'` | 이전 행 대비 % 차이 |
|
|
2741
|
+
| `'differenceFromPrevCol'` | 이전 열과의 차이 |
|
|
2742
|
+
| `'percentDifferenceFromPrevCol'` | 이전 열 대비 % 차이 |
|
|
2743
|
+
| `'percentOfGrandTotal'` | 총합 대비 % |
|
|
2744
|
+
| `'percentOfRowTotal'` | 행 합계 대비 % |
|
|
2745
|
+
| `'percentOfColumnTotal'` | 열 합계 대비 % |
|
|
2746
|
+
| `'percentOfPrevRow'` | 이전 행 값 대비 % |
|
|
2747
|
+
| `'percentOfPrevCol'` | 이전 열 값 대비 % |
|
|
2748
|
+
| `'runningTotal'` | 누적합 |
|
|
2749
|
+
| `'percentOfRunningTotal'` | 누적합 대비 % |
|
|
2750
|
+
| `'percentOfParentRow'` | 부모 행 대비 % |
|
|
2751
|
+
| `'percentOfParentCol'` | 부모 열 대비 % |
|
|
2752
|
+
| `'rankAsc'` | 오름차순 순위 |
|
|
2753
|
+
| `'rankDesc'` | 내림차순 순위 |
|
|
2754
|
+
| `'index'` | 인덱스 계산 (cell × grand / (row × col)) |
|
|
2755
|
+
|
|
2756
|
+
#### PivotConfig
|
|
2757
|
+
|
|
2758
|
+
`PivotConfig`의 전체 속성은 [PivotModeConfig](#pivotmodeconfig) 및 [grid.pivot — 피벗 API](#gridpivot--피벗-api) 섹션을 참조하세요.
|
|
2759
|
+
|
|
2760
|
+
주요 속성:
|
|
2761
|
+
|
|
2762
|
+
| 속성 | 타입 | 설명 |
|
|
2763
|
+
|------|------|------|
|
|
2764
|
+
| `rowFields` | `PivotRowField[]` | 행 축 필드 (필수) |
|
|
2765
|
+
| `columnFields` | `PivotColumnField[]` | 열 축 필드 |
|
|
2766
|
+
| `valueFields` | `PivotValueField[]` | 값 필드 (필수) |
|
|
2767
|
+
| `showRowSubTotals` | `boolean` | 행 소계 표시 |
|
|
2768
|
+
| `showRowGrandTotals` | `boolean` | 행 총합계 표시 |
|
|
2769
|
+
| `showColumnSubTotals` | `boolean` | 열 소계 표시 |
|
|
2770
|
+
| `showColumnGrandTotals` | `boolean` | 열 총합계 표시 |
|
|
2771
|
+
| `rowSubTotalPosition` | `'before' \| 'after'` | 행 소계 위치 (기본: `'after'`) |
|
|
2772
|
+
| `rowGrandTotalPosition` | `'before' \| 'after'` | 행 총합계 위치 |
|
|
2773
|
+
| `columnSubTotalPosition` | `'before' \| 'after'` | 열 소계 위치 |
|
|
2774
|
+
| `columnGrandTotalPosition` | `'before' \| 'after'` | 열 총합계 위치 |
|
|
2775
|
+
| `filterFields` | `PivotFilterField[]` | 화이트리스트 사전 필터 — 행/열 축에 넣지 않고 소스를 축소 (`{ field, values }`) |
|
|
2776
|
+
| `trackSourceRows` | `boolean` | 드릴스루/원본 export 용 소스 행 추적 (opt-in, 기본 `false`). 값 필터의 소계/총합 재집계는 이 옵션과 무관하게 동작 |
|
|
2777
|
+
| `showZeros` | `boolean` | 값이 없는(null/undefined) 셀을 `0`으로 표시 (기본: `false`) |
|
|
2778
|
+
| `rowDisplayMode` | `'tabular' \| 'outline'` | 피벗 행 표시 모드 (기본: `'tabular'`) |
|
|
2779
|
+
| `outlineColumnWidth` | `number` | outline 모드의 단일 rowHeader 컬럼 너비 |
|
|
2780
|
+
| `outlineIndentSize` | `number` | outline 모드의 레벨당 들여쓰기 픽셀 (기본: 20) |
|
|
2781
|
+
| `rowCollapsible` | `boolean` | **행** 그룹 접기/펼치기 허용 여부 (기본: `true`). `false` 면 행 헤더 chevron 미표시 + 사용자 클릭/키보드 토글 무시. `PivotAPI.expandAllRows / collapseAllRows / collapseRow / expandRow` 등 프로그래매틱 호출은 옵션과 무관하게 작동 (사용자 클릭만 막는 의도) |
|
|
2782
|
+
| `columnCollapsible` | `boolean` | **열** 그룹 접기/펼치기 허용 여부 (기본: `true`). `false` 면 열 헤더 chevron 미표시 + 클릭 무반응. `expandAllColumns / collapseAllColumns` 등은 옵션과 무관하게 작동 |
|
|
2783
|
+
|
|
2784
|
+
### 열거형/유니온 타입
|
|
2785
|
+
|
|
2786
|
+
| 타입 | 값 |
|
|
2787
|
+
|------|-----|
|
|
2788
|
+
| `DataType` | `'string' \| 'number' \| 'boolean' \| 'date' \| 'array'` |
|
|
2789
|
+
| `AggregateFunc` | `'sum' \| 'avg' \| 'min' \| 'max' \| 'count' \| 'countAll' \| 'first' \| 'last' \| 'range' \| 'stdev' \| 'stdevPop' \| 'variance' \| 'variancePop' \| 'countNumbers'` (FieldDef.aggregate / 피벗 valueField에서 사용). `count`=컬럼 non-null 값 수, `countAll`=그룹 전체 행 수(null 포함), `countNumbers`=숫자 타입 값 수 |
|
|
2790
|
+
| `AggregateType` | `'sum' \| 'avg' \| 'count' \| 'countAll' \| 'min' \| 'max' \| 'first' \| 'last'` (그룹화/트리 aggregates에서 사용) |
|
|
2791
|
+
| `AggregateFn` | `AggregateType \| ((values: CellValue[]) => CellValue)` |
|
|
2792
|
+
| `ValidationTrigger` | `'input' \| 'change' \| 'blur' \| 'commit'` |
|
|
2793
|
+
| `FilterOperator` | `'eq' \| 'neq' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'contains' \| 'notContains' \| 'startsWith' \| 'endsWith' \| 'between' \| 'isNull' \| 'isNotNull' \| 'arrayContains' \| 'arrayNotContains' \| 'arrayContainsAny' \| 'arrayContainsAll' \| 'in' \| 'notIn' \| 'formula' \| 'custom'` |
|
|
2794
|
+
| `ComparisonFilterOperator` | `FilterOperator` 에서 `'in'`/`'notIn'`/`'formula'`/`'custom'` 을 뺀 것 — 스칼라 `filterValue` 를 쓰는 연산자 집합 (→ [SortState / FilterState](#sortstate--filterstate)) |
|
|
2795
|
+
| `FilterGroupType` | `'conditional' \| 'values' \| 'formula'` |
|
|
2796
|
+
| `ValueListMode` | `'exclude' \| 'include'` — 값 목록의 저장 방향 (기본 `'exclude'`) |
|
|
2797
|
+
| `RowVariant` | `'data' \| 'group-row' \| 'subtotal' \| 'grandtotal' \| 'filter' \| 'masterDetail' \| 'custom'` |
|
|
2798
|
+
| `RowSelectionMode` | `'none' \| 'single' \| 'multi'` |
|
|
2799
|
+
| `CellSelectionMode` | `'none' \| 'single' \| 'range'` |
|
|
2800
|
+
| `RowSelectionTrigger` | `'cellClick' \| 'rowHeader' \| 'checkbox' \| ReadonlyArray<'rowHeader' \| 'checkbox'>` |
|
|
2801
|
+
| `Theme` | `'light' \| 'dark' \| 'auto' \| 'excel' \| 'sheets' \| 'material' \| (string & {})` |
|
|
2802
|
+
| `CellAlign` | `'left' \| 'center' \| 'right'` |
|
|
2803
|
+
| `PinPosition` | `'left' \| 'right' \| 'none'` |
|
|
2804
|
+
| `RowState` | `'pristine' \| 'added' \| 'modified' \| 'deleted'` |
|
|
2805
|
+
| `CellState` | `'pristine' \| 'modified'` |
|
|
2806
|
+
| `EditorType` | `'text' \| 'number' \| 'date' \| 'select' \| 'checkbox' \| 'dropdown' \| 'datepicker' \| 'multiselect' \| 'custom'` |
|
|
2807
|
+
| `EngineType` | `'native'` — **`'aq'`(Arquero)는 제거됨.** 단일 엔진화로 유니온에 값이 하나만 남았습니다(ITSM-2026-002190) |
|
|
2808
|
+
| `TotalPosition` | `'before' \| 'after'` |
|
|
2809
|
+
|
|
2810
|
+
> **`AggregateFunc` vs `AggregateFn` 구분**: `AggregateFunc`는 FieldDef/PivotValueField에서 사용하는 **문자열 enum**(14종). `AggregateFn`은 GroupingConfig/TreeConfig의 `aggregates` 맵에서 사용하며 **문자열 + 콜백** 모두 허용 (8종 enum + 커스텀 함수).
|
|
2811
|
+
|
|
2812
|
+
---
|
|
2813
|
+
|
|
2814
|
+
## 공용/내부 타입 레퍼런스
|
|
2815
|
+
|
|
2816
|
+
위 섹션들에서 시그니처로 등장하는 모든 보조 타입의 정의입니다.
|
|
2817
|
+
|
|
2818
|
+
### 데이터 기본 타입
|
|
2819
|
+
|
|
2820
|
+
#### CellValue
|
|
2821
|
+
|
|
2822
|
+
```typescript
|
|
2823
|
+
type CellValue = string | number | boolean | Date | null | undefined | CellValue[];
|
|
2824
|
+
```
|
|
2825
|
+
|
|
2826
|
+
#### Row / RowData
|
|
2827
|
+
|
|
2828
|
+
```typescript
|
|
2829
|
+
interface Row {
|
|
2830
|
+
[columnId: string]: CellValue;
|
|
2831
|
+
/** 내부 예약 필드 — PureSheet가 자동 관리, 사용자 수정 금지 (ADR-105) */
|
|
2832
|
+
__psRowId?: string | number;
|
|
2833
|
+
}
|
|
2834
|
+
type RowData = Row;
|
|
2835
|
+
```
|
|
2836
|
+
|
|
2837
|
+
#### ColumnDef
|
|
2838
|
+
|
|
2839
|
+
```typescript
|
|
2840
|
+
interface ColumnDef {
|
|
2841
|
+
id: string;
|
|
2842
|
+
label?: string; // 헤더 표시명 (FieldDef.header와 동일 역할)
|
|
2843
|
+
type?: 'string' | 'number' | 'boolean' | 'date' | 'array';
|
|
2844
|
+
width?: number;
|
|
2845
|
+
minWidth?: number;
|
|
2846
|
+
maxWidth?: number;
|
|
2847
|
+
flex?: number;
|
|
2848
|
+
hidden?: boolean;
|
|
2849
|
+
lockVisible?: boolean; // 가시성 하드 잠금 (UI/API 모두 거부)
|
|
2850
|
+
pinned?: 'left' | 'right';
|
|
2851
|
+
sortable?: boolean;
|
|
2852
|
+
filterable?: boolean;
|
|
2853
|
+
align?: CellAlign;
|
|
2854
|
+
// ... FieldDef와 거의 동일 (FieldDef는 사용자 입력용, ColumnDef는 정규화된 내부 형태)
|
|
2855
|
+
}
|
|
2856
|
+
```
|
|
2857
|
+
|
|
2858
|
+
> 사용자 코드에선 보통 `FieldDef`만 작성합니다. `ColumnDef`는 내부 정규화 타입이며, **`grid.columns.*` get류 API 의 반환은 `ColumnInfo`** (ColumnDef + ColumnState 합본) 입니다.
|
|
2859
|
+
|
|
2860
|
+
#### ColumnState
|
|
2861
|
+
|
|
2862
|
+
```typescript
|
|
2863
|
+
interface ColumnState {
|
|
2864
|
+
id: string;
|
|
2865
|
+
width: number;
|
|
2866
|
+
pinned: 'left' | 'right' | 'none';
|
|
2867
|
+
visible: boolean;
|
|
2868
|
+
order: number;
|
|
2869
|
+
offset?: number;
|
|
2870
|
+
autoFit?: boolean;
|
|
2871
|
+
renderedWidth?: number; // 디바이스 픽셀 보정된 렌더링 너비
|
|
2872
|
+
renderedLeft?: number; // 섹션 내 누적 left 오프셋
|
|
2873
|
+
}
|
|
2874
|
+
```
|
|
2875
|
+
|
|
2876
|
+
> `grid.select(s => s.columnStates)` 등 GridState 직접 구독 시에만 노출됩니다. **`grid.columns.*` API 반환에선 `ColumnInfo` 로 합쳐져 나옵니다.**
|
|
2877
|
+
|
|
2878
|
+
#### ColumnInfo
|
|
2879
|
+
|
|
2880
|
+
`grid.columns.getById / getAll / getMap / getVisibleColumns / getState` 모두가 반환하는 **단일 통합 shape**. 정적 `ColumnDef` + 런타임 `ColumnState` 합본.
|
|
2881
|
+
|
|
2882
|
+
```typescript
|
|
2883
|
+
interface ColumnInfo {
|
|
2884
|
+
// ── 식별 ──
|
|
2885
|
+
id: string;
|
|
2886
|
+
|
|
2887
|
+
// ── 데이터 타입 / 라벨 / 헤더 ──
|
|
2888
|
+
type?: ColumnType;
|
|
2889
|
+
label?: string;
|
|
2890
|
+
/** def.header ?? def.label ?? def.id 로 합성 — 항상 비어있지 않음 */
|
|
2891
|
+
header?: string;
|
|
2892
|
+
|
|
2893
|
+
// ── 크기 (state 우선) ──
|
|
2894
|
+
width: number;
|
|
2895
|
+
minWidth?: number;
|
|
2896
|
+
maxWidth?: number;
|
|
2897
|
+
flex?: number;
|
|
2898
|
+
autoFit?: boolean;
|
|
2899
|
+
|
|
2900
|
+
// ── 가시성 / 순서 (state 우선) ──
|
|
2901
|
+
visible: boolean;
|
|
2902
|
+
/** !visible 로 파생 */
|
|
2903
|
+
hidden: boolean;
|
|
2904
|
+
order: number;
|
|
2905
|
+
/** ColumnDef.lockVisible — true 면 표시/숨김 변경이 UI·API 모두 거부됨 */
|
|
2906
|
+
lockVisible?: boolean;
|
|
2907
|
+
|
|
2908
|
+
// ── 고정 ──
|
|
2909
|
+
/** state 우선 — 'left' | 'right' | 'none' */
|
|
2910
|
+
pinned: 'left' | 'right' | 'none';
|
|
2911
|
+
/** ColumnDef.frozen — pinned 의 alias */
|
|
2912
|
+
frozen?: 'left' | 'right';
|
|
2913
|
+
|
|
2914
|
+
// ── 정렬 / 표시 형식 ──
|
|
2915
|
+
align?: CellAlign;
|
|
2916
|
+
mask?: MaskConfig;
|
|
2917
|
+
decimalFormat?: DecimalFormat;
|
|
2918
|
+
useThousandsSeparator?: boolean;
|
|
2919
|
+
|
|
2920
|
+
// ── 셀 / 헤더 렌더링 ──
|
|
2921
|
+
cellRenderer?: CellRenderer;
|
|
2922
|
+
cellAttributes?: CellAttributes | CellAttributesFn;
|
|
2923
|
+
headerAttributes?: CellAttributes;
|
|
2924
|
+
headerRenderer?: (params: { columnId: string; header: string }) => HTMLElement | string;
|
|
2925
|
+
|
|
2926
|
+
// ── 동작 플래그 ──
|
|
2927
|
+
sortable?: boolean;
|
|
2928
|
+
filterable?: boolean;
|
|
2929
|
+
editable?: boolean;
|
|
2930
|
+
readonly?: boolean;
|
|
2931
|
+
editableWhen?: (params: { rowData; rowState; rowId }) => boolean;
|
|
2932
|
+
|
|
2933
|
+
// ── 수식 / 에디터 ──
|
|
2934
|
+
formula?: string | ((row) => unknown);
|
|
2935
|
+
editor?: EditorConfig;
|
|
2936
|
+
editorResolver?: (params) => EditorConfig | null;
|
|
2937
|
+
|
|
2938
|
+
// ── 집계 / 병합 ──
|
|
2939
|
+
aggregate?: AggregateFunc;
|
|
2940
|
+
mergeStrategy?: 'same-value' | 'none';
|
|
2941
|
+
|
|
2942
|
+
// ── 유효성 ──
|
|
2943
|
+
validation?: ValidationRule[];
|
|
2944
|
+
|
|
2945
|
+
// ── 피벗 전용 ──
|
|
2946
|
+
pivotType?: 'data' | 'subtotal' | 'grandtotal';
|
|
2947
|
+
structural?: boolean;
|
|
2948
|
+
pivotValueField?: string;
|
|
2949
|
+
pivotValueFieldConfig?: { ... };
|
|
2950
|
+
|
|
2951
|
+
// ── 런타임 전용 (가상화/렌더 내부) ──
|
|
2952
|
+
offset?: number;
|
|
2953
|
+
renderedWidth?: number;
|
|
2954
|
+
renderedLeft?: number;
|
|
2955
|
+
}
|
|
2956
|
+
```
|
|
2957
|
+
|
|
2958
|
+
**충돌 키 정책**: state(런타임)가 승.
|
|
2959
|
+
- `width` / `pinned` / `visible` / `autoFit` → 현재값 (state)
|
|
2960
|
+
- `hidden` → `!visible` 로 파생
|
|
2961
|
+
- `header` → `def.header ?? def.label ?? def.id` 로 fallback 합성 (항상 truthy)
|
|
2962
|
+
- `frozen` → ColumnDef 의 alias 그대로 노출 (정식 사용은 `pinned`)
|
|
2963
|
+
|
|
2964
|
+
#### CellPosition
|
|
2965
|
+
|
|
2966
|
+
```typescript
|
|
2967
|
+
interface CellPosition {
|
|
2968
|
+
/** 행 인덱스 (뷰 기준 - 그룹 행 포함). pinned 셀은 -1 */
|
|
2969
|
+
rowIndex: number;
|
|
2970
|
+
columnId: string;
|
|
2971
|
+
/** 실제 데이터 인덱스 (그룹 행 제외) */
|
|
2972
|
+
dataIndex?: number;
|
|
2973
|
+
rowId?: string | number;
|
|
2974
|
+
/** pinned 셀일 때만 채워짐 */
|
|
2975
|
+
pinnedPosition?: 'top' | 'bottom';
|
|
2976
|
+
pinnedIndex?: number;
|
|
2977
|
+
}
|
|
2978
|
+
```
|
|
2979
|
+
|
|
2980
|
+
#### SortState / FilterState
|
|
2981
|
+
|
|
2982
|
+
```typescript
|
|
2983
|
+
interface SortState {
|
|
2984
|
+
/** 컬럼 ID (피벗에선 행/열 필드 ID도 가능) */
|
|
2985
|
+
id: string;
|
|
2986
|
+
direction: 'asc' | 'desc';
|
|
2987
|
+
/** 우선순위 (낮을수록 먼저 적용) */
|
|
2988
|
+
sequence: number;
|
|
2989
|
+
/** 피벗 전용: 값 필드 기준 정렬 (레이블 대신 집계 값 기준) */
|
|
2990
|
+
byValue?: string;
|
|
2991
|
+
/** 커스텀 정렬 키 함수 — 셀 값을 정렬 가능한 키로 변환 */
|
|
2992
|
+
sortKey?: (value: CellValue) => number | string;
|
|
2993
|
+
/**
|
|
2994
|
+
* (피벗 전용) 정렬 적용 단계 override. 플랫 그리드에서는 무시.
|
|
2995
|
+
* 생략(기본) = `'post'`: 피벗된 행/열을 재정렬 (집계값 불변 — 인터랙티브 정렬의 일반 케이스).
|
|
2996
|
+
* `'pre'` = 소스 ORDER BY: 집계 **전** 원본을 정렬 (`first`/`last` 등 순서-민감 집계 결과를 결정할 때만, 니치).
|
|
2997
|
+
*/
|
|
2998
|
+
mode?: 'pre' | 'post';
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
interface FilterStateBase {
|
|
3002
|
+
/** 필터링할 컬럼 ID (플랫: 컬럼 ID, 피벗: 행/열 필드 ID) */
|
|
3003
|
+
id: string;
|
|
3004
|
+
/** 필터 우선순위 (낮을수록 먼저 적용) */
|
|
3005
|
+
sequence: number;
|
|
3006
|
+
/** 필터 간 논리 연산 (기본 'and') */
|
|
3007
|
+
logical?: 'or' | 'and';
|
|
3008
|
+
/**
|
|
3009
|
+
* 이 필터가 속한 그룹 키 — 평탄화 시 자동 부착. **그룹 경계는 괄호다.**
|
|
3010
|
+
* 같은 키끼리 먼저 접고 그룹 결과끼리 AND 로 합친다.
|
|
3011
|
+
*/
|
|
3012
|
+
groupKey?: string;
|
|
3013
|
+
/** 스마트 필터링용 mask (보통 컬럼의 mask가 자동 주입됨 — 직접 지정 불필요) */
|
|
3014
|
+
mask?: MaskConfig;
|
|
3015
|
+
/**
|
|
3016
|
+
* (피벗 전용) 필터 적용 단계 override. 플랫 그리드에서는 무시.
|
|
3017
|
+
* 생략(기본) = core 자동 분류: 행/열 필드(차원) 필터 → `'pre'`(소스 축소 후 재집계 = 소계/총합까지 반영),
|
|
3018
|
+
* 값(measure) 필터 → `'post'`(피벗된 값 기준 행 필터 + 살아남은 source 로 소계/총합 재집계).
|
|
3019
|
+
*/
|
|
3020
|
+
mode?: 'pre' | 'post';
|
|
3021
|
+
/**
|
|
3022
|
+
* (피벗 값 필터 전용) 조건을 만족하는 셀이 하나도 없는 리프 컬럼을 결과에서 제외 (기본 `false`).
|
|
3023
|
+
*/
|
|
3024
|
+
excludeUnmatchedColumns?: boolean;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
/** 값 하나를 비교 — 스칼라 filterValue */
|
|
3028
|
+
type ComparisonFilterOperator =
|
|
3029
|
+
| 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte'
|
|
3030
|
+
| 'contains' | 'notContains' | 'startsWith' | 'endsWith'
|
|
3031
|
+
| 'between' | 'isNull' | 'isNotNull'
|
|
3032
|
+
| 'arrayContains' | 'arrayNotContains' | 'arrayContainsAny' | 'arrayContainsAll';
|
|
3033
|
+
|
|
3034
|
+
interface ComparisonFilterState extends FilterStateBase {
|
|
3035
|
+
operator: ComparisonFilterOperator;
|
|
3036
|
+
filterValue: CellValue;
|
|
3037
|
+
filterValue2?: CellValue; // between 전용
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
/** 값 집합(엑셀식 체크박스) — filterValue 는 **원본값 배열** */
|
|
3041
|
+
interface ValueSetFilterState extends FilterStateBase {
|
|
3042
|
+
operator: 'in' | 'notIn';
|
|
3043
|
+
filterValue: CellValue[];
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
/** 수식 — **행 전체**를 판정. id 는 관례상 '__formula__' */
|
|
3047
|
+
interface FormulaFilterState extends FilterStateBase {
|
|
3048
|
+
operator: 'formula';
|
|
3049
|
+
/** 문자열 식 (함수 아님 — Worker 복제 + 레이아웃 JSON 저장을 위해) */
|
|
3050
|
+
expression: string;
|
|
3051
|
+
filterValue?: null;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
/** 사용자 술어 — 함수라 직렬화되지 않음(레이아웃 프리셋에 안 담김) */
|
|
3055
|
+
interface CustomFilterState extends FilterStateBase {
|
|
3056
|
+
operator: 'custom';
|
|
3057
|
+
predicate: (value: CellValue) => boolean;
|
|
3058
|
+
filterValue?: CellValue;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
/** `operator` 로 갈라지는 **판별 유니온** */
|
|
3062
|
+
type FilterState =
|
|
3063
|
+
| ComparisonFilterState
|
|
3064
|
+
| ValueSetFilterState
|
|
3065
|
+
| FormulaFilterState
|
|
3066
|
+
| CustomFilterState;
|
|
3067
|
+
```
|
|
3068
|
+
|
|
3069
|
+
> **⚠️ `FilterState` 는 이제 판별 유니온입니다 (ITSM-2026-002188).** 단일 인터페이스로 두면 잘못된 조합이 타입을 통과한 뒤 런타임에서 **조용히 "필터 없음"** 이 됩니다(실측):
|
|
3070
|
+
> - `operator:'formula'` 인데 `expression` 없음 → 빈 식 → 전 행 통과
|
|
3071
|
+
> - `operator:'in'` 인데 `filterValue` 가 스칼라 → `Array.isArray` 실패 → 전 행 통과
|
|
3072
|
+
>
|
|
3073
|
+
> **직렬화 모양(JSON)은 바뀌지 않습니다** — 기존 저장본은 그대로 유효합니다. `if (f.operator === 'formula')` 안에서 `f.expression` 이 `string` 으로 좁혀집니다.
|
|
3074
|
+
>
|
|
3075
|
+
> 종류를 가리지 않고 필드를 읽어야 하는 곳(해시·직렬화)은 core 가 export 하는 `filterField(f, key)` 를 쓰세요.
|
|
3076
|
+
|
|
3077
|
+
#### 필터 그룹 — FilterGroup / FilterGroupMap
|
|
3078
|
+
|
|
3079
|
+
필터 상태를 **소스별로** 나눠 담는 구조입니다 (ITSM-2026-002182).
|
|
3080
|
+
|
|
3081
|
+
```typescript
|
|
3082
|
+
type FilterGroupType = 'conditional' | 'values' | 'formula';
|
|
3083
|
+
|
|
3084
|
+
interface FilterGroup {
|
|
3085
|
+
/** 그룹 종류 — 소비처가 어떤 UI 로 편집할지 판단 */
|
|
3086
|
+
type: FilterGroupType;
|
|
3087
|
+
/** 적용 순서(작을수록 먼저). 그룹 간은 AND 라 결과는 순서와 무관하지만 쿼리 해시 안정성에 필요 */
|
|
3088
|
+
priority: number;
|
|
3089
|
+
/** false 면 상태는 남지만 필터링에 참여하지 않음(임시 끄기·편집 중 초안) */
|
|
3090
|
+
applied: boolean;
|
|
3091
|
+
/** 이 그룹이 소유한 필터들. 그룹 **안**에서는 각 항목의 `logical` 로 결합 */
|
|
3092
|
+
states: FilterState[];
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
/** 소스 키 → 그룹. 키는 소비처가 임의로 정한다 (예: 'toolbar', 'headerPopover:amount') */
|
|
3096
|
+
type FilterGroupMap = Record<string, FilterGroup>;
|
|
3097
|
+
```
|
|
3098
|
+
|
|
3099
|
+
**함께 export 되는 헬퍼**
|
|
3100
|
+
|
|
3101
|
+
| Export | 설명 |
|
|
3102
|
+
|---|---|
|
|
3103
|
+
| `flattenFilterGroups(groups)` | 그룹 맵 → 엔진에 넘길 평면 `FilterState[]`. `applied:false` 제외, `priority` 오름차순, 각 그룹 첫 항목의 `logical` 을 `'and'` 로 강제, `groupKey` 부착 |
|
|
3104
|
+
| `toFilterGroupMap(filters, key?)` | 평면 배열 → 그룹 맵 (하위 호환 승격). 기본 키 `DEFAULT_FILTER_GROUP_KEY = 'default'` |
|
|
3105
|
+
| `isFilterGroupMap(value)` | 그룹 맵인지 판별(배열이면 옛 형태) |
|
|
3106
|
+
| `DEFAULT_FILTER_GROUP_KEY` | `'default'` |
|
|
3107
|
+
|
|
3108
|
+
> **왜 그룹인가 (ITSM-2026-002182)**
|
|
3109
|
+
> 필터가 평면 배열 하나였을 때 컬럼 헤더 팝오버와 툴바 필터 창이 **같은 배열을 공유**했고, 각 UI 가 자기 것만 보여주려 배열을 슬라이싱하면서 다음이 반복해 깨졌습니다(모두 실측 버그): 값 목록에서 13개 고르면 조건식 창에 13줄 노출 / 팝오버를 열었다 닫는 것만으로 다른 UI 필터가 삭제 / 검색으로 좁힌 상태에서 해제하면 검색 밖 항목까지 해제.
|
|
3110
|
+
>
|
|
3111
|
+
> **그룹 간 결합은 AND 고정**입니다. OR 을 허용하면 그룹 경계를 넘는 결합이 생겨 "자기 그룹만 보면 안전하다"는 전제가 깨집니다(AG Grid 도 컬럼 필터끼리는 암묵적 AND). 컬럼 간 OR 이 필요하면 **수식 그룹**(`type: 'formula'`)이 담당합니다 — 식 안에서 괄호로 표현합니다.
|
|
3112
|
+
>
|
|
3113
|
+
> **`groupKey` 가 없으면 좌→우 fold 가 그룹을 넘나듭니다.** 실측: `dept=Sales` + `name∈{강호동,김태희}` 에서 `(dept=Sales AND name=강호동) OR name=김태희` 로 접혀 HR/김태희가 누출됐습니다. `groupKey` 가 있으면 `(dept=Sales) AND (name=강호동 OR name=김태희)` 로 올바르게 접힙니다.
|
|
3114
|
+
|
|
3115
|
+
#### 수식 필터 — FormulaFilterState
|
|
3116
|
+
|
|
3117
|
+
컬럼 간 비교·괄호·함수를 **식 하나**로 표현합니다 (ITSM-2026-002180). 문법은 **조건부 서식과 동일한 `FormulaEngine`** 을 공유합니다.
|
|
3118
|
+
|
|
3119
|
+
```typescript
|
|
3120
|
+
await grid.filter([{
|
|
3121
|
+
id: '__formula__', sequence: 0, operator: 'formula',
|
|
3122
|
+
expression: '([급여] > 50000 AND [부서] == "Engineering") OR [도시] == "서울"',
|
|
3123
|
+
}]);
|
|
3124
|
+
```
|
|
3125
|
+
|
|
3126
|
+
| Export | 설명 |
|
|
3127
|
+
|---|---|
|
|
3128
|
+
| `FORMULA_FILTER_OPERATOR` | `'formula'` |
|
|
3129
|
+
| `FORMULA_FILTER_ID` | `'__formula__'` — 수식 필터의 관례적 `id`(컬럼이 아니므로 엔진은 보지 않음) |
|
|
3130
|
+
| `evaluateFormulaFilter(row, expression, columns)` | 행 하나를 식으로 판정 |
|
|
3131
|
+
| `formulaFilterDependencies(expression)` | 식이 참조하는 컬럼 목록 |
|
|
3132
|
+
| `validateFilterFormula(expression)` | 유효성 검사 — 오류 문구 또는 `null` |
|
|
3133
|
+
| `clearFormulaFilterCache()` | 컴파일 캐시 비우기 |
|
|
3134
|
+
|
|
3135
|
+
> **왜 평면 배열이 아니라 식인가** — 기존 `FilterState[]` 는 precedence 없는 좌→우 fold 라 괄호를 표현할 수 없어 `(A AND B) OR C` 와 `A AND (B OR C)` 를 구분하지 못합니다. 수식은 **식 하나가 필터 하나**라 fold 에 끼어들 여지가 없습니다.
|
|
3136
|
+
>
|
|
3137
|
+
> **왜 문자열인가** — `predicate`(함수)는 Worker 구조화 복제가 안 되고 레이아웃 JSON 으로 저장할 수 없습니다. 문자열이면 Worker 전송·`getConfig()` 저장·옛 버전 호환이 전부 됩니다.
|
|
3138
|
+
>
|
|
3139
|
+
> **성능** — 파싱은 **식당 1회**(컴파일 캐시). 실측(5M 행): 평면 fold 62ms / 수식(캐시) 94ms / 수식(행마다 파싱) 297ms ← 반드시 피해야 하는 경로.
|
|
3140
|
+
|
|
3141
|
+
#### 레거시 조건식 → 수식 마이그레이션
|
|
3142
|
+
|
|
3143
|
+
소유자 개념이 없던 시절의 조건식 저장본을 고급 필터 수식으로 옮깁니다 (ITSM-2026-002183). core 는 **변환 함수만** 제공하고, 언제 태울지는 wrapper 의 마이그레이션 스텝이 정합니다.
|
|
3144
|
+
|
|
3145
|
+
| Export | 설명 |
|
|
3146
|
+
|---|---|
|
|
3147
|
+
| `conditionFiltersToFormula(filters)` | 조건식 배열 → 수식 문자열 (`ConditionFilterConversion`) |
|
|
3148
|
+
| `conditionFiltersToFormulaState(...)` | 조건식 배열 → `FormulaFilterState` |
|
|
3149
|
+
|
|
3150
|
+
#### ChangesSummary
|
|
3151
|
+
|
|
3152
|
+
`grid.changes.getAll()` 반환 타입:
|
|
3153
|
+
|
|
3154
|
+
```typescript
|
|
3155
|
+
interface ChangesSummary {
|
|
3156
|
+
added: AddedRow[];
|
|
3157
|
+
modified: ModifiedRow[];
|
|
3158
|
+
deleted: DeletedRow[];
|
|
3159
|
+
/** 재정렬된 행 순서 (null이면 변경 없음) */
|
|
3160
|
+
reorderedOrder?: (string | number)[] | null;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
interface AddedRow {
|
|
3164
|
+
rowId: string | number;
|
|
3165
|
+
data: Row;
|
|
3166
|
+
insertIndex: number;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
interface ModifiedRow {
|
|
3170
|
+
rowId: string | number;
|
|
3171
|
+
originalData: Row;
|
|
3172
|
+
currentData: Row;
|
|
3173
|
+
changedFields: Map<string, ChangedField>;
|
|
3174
|
+
sourceIndex?: number;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
interface ChangedField {
|
|
3178
|
+
originalValue: CellValue;
|
|
3179
|
+
currentValue: CellValue;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
interface DeletedRow {
|
|
3183
|
+
rowId: string | number;
|
|
3184
|
+
originalData: Row;
|
|
3185
|
+
originalIndex: number;
|
|
3186
|
+
}
|
|
3187
|
+
```
|
|
3188
|
+
|
|
3189
|
+
#### PerformanceTiming
|
|
3190
|
+
|
|
3191
|
+
```typescript
|
|
3192
|
+
interface PerformanceTiming {
|
|
3193
|
+
name: string;
|
|
3194
|
+
duration: number; // ms
|
|
3195
|
+
}
|
|
3196
|
+
```
|
|
3197
|
+
|
|
3198
|
+
---
|
|
3199
|
+
|
|
3200
|
+
### 데이터 로더 — LoaderConfig
|
|
3201
|
+
|
|
3202
|
+
Web Worker에서 직접 데이터를 페칭하는 모듈을 지정합니다. 메인스레드 비경유로 UI 블로킹 없음.
|
|
3203
|
+
|
|
3204
|
+
```typescript
|
|
3205
|
+
interface LoaderConfig {
|
|
3206
|
+
/** DataLoader를 default export하는 ES 모듈 URL */
|
|
3207
|
+
module: URL | string;
|
|
3208
|
+
/** 초기 파라미터 (직렬화 가능한 plain object). 런타임에 grid.updateLoaderParams()로 갱신 */
|
|
3209
|
+
params?: Record<string, unknown>;
|
|
3210
|
+
}
|
|
3211
|
+
```
|
|
3212
|
+
|
|
3213
|
+
```typescript
|
|
3214
|
+
// my-loader.ts
|
|
3215
|
+
import { DataLoader } from '@vmscloud/moz-ui-grid-core';
|
|
3216
|
+
export default class MyLoader extends DataLoader {
|
|
3217
|
+
async fetchData(): Promise<Row[]> {
|
|
3218
|
+
const res = await fetch(`/api/data?token=${this.params.authToken}`);
|
|
3219
|
+
return res.json();
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// 그리드 생성
|
|
3224
|
+
new PureSheet(container, {
|
|
3225
|
+
loader: {
|
|
3226
|
+
module: new URL('./my-loader.ts', import.meta.url),
|
|
3227
|
+
params: { authToken: 'abc' },
|
|
3228
|
+
},
|
|
3229
|
+
fields: [...],
|
|
3230
|
+
});
|
|
3231
|
+
```
|
|
3232
|
+
|
|
3233
|
+
---
|
|
3234
|
+
|
|
3235
|
+
### Drag-Drop — DragDropConfig (ADR-110)
|
|
3236
|
+
|
|
3237
|
+
행 드래그 재정렬 + 크로스-그리드 드롭 + drop validator + dirty mutator를 단일 객체로.
|
|
3238
|
+
|
|
3239
|
+
```typescript
|
|
3240
|
+
interface DragDropConfig {
|
|
3241
|
+
/**
|
|
3242
|
+
* 행 드래그 재정렬 활성화.
|
|
3243
|
+
* **flat 모드에서 `dragDropConfig`를 제공하면 기본 `true`**(opt-out) — `dragDropConfig`의
|
|
3244
|
+
* 존재 자체를 드래그 사용 의도로 간주한다. 내부 reorder를 끄려면 명시적으로 `false`
|
|
3245
|
+
* (예: cross-grid 드롭만 쓰고 그리드 내부 순서 변경은 차단). @default true (dragDropConfig 제공 시)
|
|
3246
|
+
*/
|
|
3247
|
+
reorderableRows?: boolean;
|
|
3248
|
+
/** 드래그 핸들 컬럼 표시 (reorderableRows 활성 시). @default true */
|
|
3249
|
+
showDragHandleColumn?: boolean;
|
|
3250
|
+
/** reorder 드래그 시작 위치. 'row'면 행 본문 어디서나 (보통 showDragHandleColumn:false와 함께). @default 'handle' */
|
|
3251
|
+
dragTrigger?: 'handle' | 'row';
|
|
3252
|
+
|
|
3253
|
+
/** Cross-grid 드래그 그룹 — 같은 값 그리드끼리 행 이동 가능 */
|
|
3254
|
+
crossGridDragGroup?: string;
|
|
3255
|
+
/** 행이 다른 그리드로 빠져나가기 직전. false 반환 시 거부 */
|
|
3256
|
+
onRowDragOut?: (e: DragOutEvent) => boolean | Promise<boolean>;
|
|
3257
|
+
/** 다른 그리드의 행이 들어오기 직전. false 반환 시 거부 */
|
|
3258
|
+
onRowDragIn?: (e: DragInEvent) => boolean | Promise<boolean>;
|
|
3259
|
+
|
|
3260
|
+
/** Drop 가능 여부 검증 (mousemove마다 호출). string 반환 시 거부 + tooltip */
|
|
3261
|
+
canDrop?: (e: CanDropEvent) => boolean | string;
|
|
3262
|
+
|
|
3263
|
+
/** 행 순서 필드 자동 관리 (flat reorder + tree reorder 모두 지원) */
|
|
3264
|
+
rowOrderField?: RowOrderFieldConfig;
|
|
3265
|
+
/** Dirty 작업 직후 행을 수정할 hook */
|
|
3266
|
+
mutateOnDirty?: MutateOnDirty;
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* 트리 인식 드래그 reorder 옵션. `treeConfig` + `reorderableRows: true`일 때 자동 활성.
|
|
3270
|
+
* `false`면 비활성(평탄 reorder 강제). 세부 필드는 grid.dragDrop API 섹션의 TreeReorderConfig 표 참조.
|
|
3271
|
+
* @requires treeConfig
|
|
3272
|
+
*/
|
|
3273
|
+
treeReorder?: false | TreeReorderConfig;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
interface RowOrderFieldConfig {
|
|
3277
|
+
/** 정렬 순서를 저장할 필드 (예: 'displayOrder') */
|
|
3278
|
+
field: string;
|
|
3279
|
+
/** 'dense'(0..N-1 연속) | 'sparse'(큰 간격, 1024 기본) @default 'dense' */
|
|
3280
|
+
strategy?: 'dense' | 'sparse';
|
|
3281
|
+
/** sparse 전용: 초기 간격 @default 1024 */
|
|
3282
|
+
spacing?: number;
|
|
3283
|
+
/** sparse 전용: 인접 gap이 이 값 이하면 rebalance 신호 @default 1 */
|
|
3284
|
+
rebalanceThreshold?: number;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
interface DragOutEvent { targetGrid: unknown; rowId: RowId; rowData: Row; }
|
|
3288
|
+
interface DragInEvent { sourceGrid: unknown; sourceRowId: RowId; rowData: Row; targetIndex: number; }
|
|
3289
|
+
interface CanDropEvent { sourceGrid: unknown; sourceRowIds: RowId[]; rowsData: Row[]; targetIndex: number; }
|
|
3290
|
+
|
|
3291
|
+
type MutateContext =
|
|
3292
|
+
| { type: 'add'; rowId: RowId }
|
|
3293
|
+
| { type: 'update'; rowId: RowId; changedField: string; oldValue: unknown; newValue: unknown }
|
|
3294
|
+
| {
|
|
3295
|
+
type: 'reorder'; // flat 그리드 행 재정렬
|
|
3296
|
+
rowId: RowId;
|
|
3297
|
+
newIndex: number;
|
|
3298
|
+
oldIndex: number;
|
|
3299
|
+
isDirectlyMoved: boolean;
|
|
3300
|
+
neighbors: { prev?: Row; next?: Row };
|
|
3301
|
+
}
|
|
3302
|
+
| {
|
|
3303
|
+
type: 'tree-reorder'; // 트리 노드 재정렬 (드래그 또는 moveNode)
|
|
3304
|
+
rowId: RowId;
|
|
3305
|
+
oldParentId: RowId | null;
|
|
3306
|
+
newParentId: RowId | null;
|
|
3307
|
+
oldSiblingIndex: number;
|
|
3308
|
+
newSiblingIndex: number;
|
|
3309
|
+
isDirectlyMoved: boolean; // 직접 이동한 promoted root면 true (밀려난 형제는 false)
|
|
3310
|
+
neighbors: { prev?: Row; next?: Row }; // 같은 새 부모 안 인접 형제
|
|
3311
|
+
};
|
|
3312
|
+
|
|
3313
|
+
// rowOrderField(dense/sparse) 및 mutateOnDirty는 tree-reorder에서도 동작한다.
|
|
3314
|
+
// 직접 이동한 노드 + 같은 부모 안에서 sibling index가 바뀐 형제 모두에 호출된다 (자손 제외).
|
|
3315
|
+
|
|
3316
|
+
/** 반환된 Partial<Row>가 dirty modified로 자동 적용. 동기 함수만 지원 (v1) */
|
|
3317
|
+
type MutateOnDirty = (row: Row, ctx: MutateContext) => Partial<Row> | void;
|
|
3318
|
+
```
|
|
3319
|
+
|
|
3320
|
+
---
|
|
3321
|
+
|
|
3322
|
+
### 셀 병합 — MergeConfig
|
|
3323
|
+
|
|
3324
|
+
3가지 모드의 discriminated union.
|
|
3325
|
+
|
|
3326
|
+
```typescript
|
|
3327
|
+
type MergeConfig = BuiltInMergeConfig | CustomMergeConfig;
|
|
3328
|
+
|
|
3329
|
+
interface BuiltInMergeConfig {
|
|
3330
|
+
/** 'content': 인접한 같은 값 / 'hierarchical': 상위 컬럼 범위 내에서만 하위도 병합 */
|
|
3331
|
+
type: 'content' | 'hierarchical';
|
|
3332
|
+
/** 병합 대상 컬럼 (순서대로 계층) */
|
|
3333
|
+
columns: string[];
|
|
3334
|
+
/** 'lazy'(동적 탐색) | 'eager'(사전 계산) @default 'lazy' */
|
|
3335
|
+
strategy?: 'lazy' | 'eager';
|
|
3336
|
+
/** Lazy 모드 최대 탐색 깊이 @default 500 */
|
|
3337
|
+
maxSearchDepth?: number;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
interface CustomMergeConfig {
|
|
3341
|
+
type: 'custom';
|
|
3342
|
+
columns: string[];
|
|
3343
|
+
/** Worker 모드에서 getRowData 행 데이터 버퍼 범위 @default 500 */
|
|
3344
|
+
maxSearchDepth?: number;
|
|
3345
|
+
/** 셀별 병합 범위 콜백 (null 반환 = 병합 없음) */
|
|
3346
|
+
getMergedRange?: (params: MergeRangeContext) => MergeRangeResult | null;
|
|
3347
|
+
/** 배치 콜백 (여러 셀 한 번에) — getMergedRange보다 우선 */
|
|
3348
|
+
getMergedRanges?: (contexts: MergeRangeContext[]) => (MergeRangeResult | null)[];
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
// 세로(행)/가로(컬럼) 탐색이 대칭으로 제공된다.
|
|
3352
|
+
// 세로: rowIndex / getRowData / totalRowCount
|
|
3353
|
+
// 가로: colIndex / columnsInOrder / totalColumnCount
|
|
3354
|
+
// 2D 공용: getCellValue(rowIndex, columnId)
|
|
3355
|
+
interface MergeRangeContext {
|
|
3356
|
+
rowIndex: number;
|
|
3357
|
+
/** 현재 컬럼의 렌더(가시) 순서 인덱스 — `rowIndex`의 가로 대칭. `columnsInOrder` 기준 */
|
|
3358
|
+
colIndex: number;
|
|
3359
|
+
columnId: string;
|
|
3360
|
+
/** columns(설정) 배열 내 인덱스 (계층 레벨) */
|
|
3361
|
+
columnLevel: number;
|
|
3362
|
+
value: CellValue;
|
|
3363
|
+
/** 인접 행 데이터 조회 (세로 경계 탐색용) */
|
|
3364
|
+
getRowData: (index: number) => Row | null;
|
|
3365
|
+
/** 임의 셀 값 조회 (2D 경계 탐색용) — `getRowData(rowIndex)?.[columnId]`와 동치 */
|
|
3366
|
+
getCellValue: (rowIndex: number, columnId: string) => CellValue;
|
|
3367
|
+
/** 렌더(가시) 순서의 전체 컬럼 ID 목록 (가로 경계 탐색용) */
|
|
3368
|
+
columnsInOrder: readonly string[];
|
|
3369
|
+
totalRowCount: number;
|
|
3370
|
+
/** 전체(가시) 컬럼 수 — `totalRowCount`의 가로 대칭 (= columnsInOrder.length) */
|
|
3371
|
+
totalColumnCount: number;
|
|
3372
|
+
/** 이전 컬럼의 병합 범위 (계층적 커스텀용) */
|
|
3373
|
+
parentRange?: { startRow: number; endRow: number };
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// 2D 사각형(세로/가로 대칭 절대범위). startCol/endCol 생략 시 현재 컬럼만 → 세로 병합.
|
|
3377
|
+
interface MergeRangeResult {
|
|
3378
|
+
startRow: number; // inclusive
|
|
3379
|
+
endRow: number; // inclusive
|
|
3380
|
+
startCol?: number; // inclusive, 렌더 컬럼 인덱스. 생략 시 현재 컬럼(세로 전용)
|
|
3381
|
+
endCol?: number; // inclusive, 렌더 컬럼 인덱스. 생략 시 현재 컬럼
|
|
3382
|
+
}
|
|
3383
|
+
```
|
|
3384
|
+
|
|
3385
|
+
> **세로 / 가로 / 2D 병합** — `startCol`/`endCol`을 **생략하면 세로 병합**(현재 컬럼만, 기존 `{ startRow, endRow }` 계약 그대로), **지정하면 가로/2D 병합**입니다. 컬럼 인덱스는 `columnsInOrder`(렌더/가시 순서) 기준이며, 가로 병합은 **같은 파티션(center/pinned) 내 연속 컬럼**만 시각적으로 올바릅니다. 영역 내 아무 셀에서 같은 사각형을 반환해도 전략이 anchor=`(startRow, startCol)`를 잡고 나머지를 숨김 처리합니다. `null` 또는 단일 셀(`startRow===endRow && startCol===endCol`)이면 병합 없음.
|
|
3386
|
+
|
|
3387
|
+
> **Worker 모드 제약**: `type: 'custom'`은 함수 직렬화 불가로 worker에서 사용 불가. content/hierarchical만 worker 호환.
|
|
3388
|
+
|
|
3389
|
+
---
|
|
3390
|
+
|
|
3391
|
+
### 스트리밍 — StreamingOptions
|
|
3392
|
+
|
|
3393
|
+
`grid.setStreamingOptions(opts)`로 설정.
|
|
3394
|
+
|
|
3395
|
+
```typescript
|
|
3396
|
+
interface StreamingOptions {
|
|
3397
|
+
/** 비동기 트랜잭션 배치 간격 (ms). @default 50 */
|
|
3398
|
+
asyncTransactionWaitMillis?: number;
|
|
3399
|
+
/** 셀 플래시 활성화. @default false */
|
|
3400
|
+
enableCellFlash?: boolean;
|
|
3401
|
+
/** 플래시 지속 시간 (ms). @default 500 */
|
|
3402
|
+
cellFlashDuration?: number;
|
|
3403
|
+
/** 페이드 지속 시간 (ms). @default 1000 */
|
|
3404
|
+
cellFadeDuration?: number;
|
|
3405
|
+
}
|
|
3406
|
+
```
|
|
3407
|
+
|
|
3408
|
+
---
|
|
3409
|
+
|
|
3410
|
+
### 트랜잭션 — RowTransaction / TransactionOptions / TransactionResult
|
|
3411
|
+
|
|
3412
|
+
`grid.applyTransaction(tx, options?)` / `applyTransactionAsync(tx, callback?)`에서 사용.
|
|
3413
|
+
|
|
3414
|
+
```typescript
|
|
3415
|
+
interface RowTransaction {
|
|
3416
|
+
add?: Row[];
|
|
3417
|
+
/** 삭제할 행 ID 목록 */
|
|
3418
|
+
remove?: (string | number)[];
|
|
3419
|
+
/** 수정할 행 (keyFields로 식별 가능한 필드 필수 포함) */
|
|
3420
|
+
update?: Row[];
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
interface TransactionOptions {
|
|
3424
|
+
/** update 후 정렬 재적용 @default true (증분 연산) */
|
|
3425
|
+
applySort?: boolean;
|
|
3426
|
+
/** update 후 필터 재적용 @default true (증분 연산) */
|
|
3427
|
+
applyFilter?: boolean;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
interface TransactionResult {
|
|
3431
|
+
addedCount: number;
|
|
3432
|
+
removedCount: number;
|
|
3433
|
+
updatedCount: number;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
type TransactionCallback = (results: TransactionResult[]) => void;
|
|
3437
|
+
```
|
|
3438
|
+
|
|
3439
|
+
```typescript
|
|
3440
|
+
// 동기 트랜잭션
|
|
3441
|
+
const result = await grid.applyTransaction({
|
|
3442
|
+
add: [{ id: 'TSLA', price: 250 }],
|
|
3443
|
+
remove: ['GOOG'],
|
|
3444
|
+
update: [{ id: 'AAPL', price: 183 }],
|
|
3445
|
+
});
|
|
3446
|
+
console.log(result.addedCount, result.removedCount, result.updatedCount);
|
|
3447
|
+
|
|
3448
|
+
// 비동기 큐잉 (고빈도 업데이트)
|
|
3449
|
+
grid.applyTransactionAsync(
|
|
3450
|
+
{ update: [{ id: 'AAPL', price: 184 }] },
|
|
3451
|
+
(results) => console.log('batch flushed:', results),
|
|
3452
|
+
);
|
|
3453
|
+
await grid.flushAsyncTransactions();
|
|
3454
|
+
```
|
|
3455
|
+
|
|
3456
|
+
---
|
|
3457
|
+
|
|
3458
|
+
### Multi-Row 레이아웃 — RowTemplate
|
|
3459
|
+
|
|
3460
|
+
한 데이터 행을 여러 시각 행으로 표시 (대시보드/카드 스타일).
|
|
3461
|
+
|
|
3462
|
+
```typescript
|
|
3463
|
+
interface RowTemplate {
|
|
3464
|
+
/** 한 데이터 행이 차지할 시각적 행 수 */
|
|
3465
|
+
rowCount: number;
|
|
3466
|
+
/** 각 행의 셀 배치 — layout[rowIdx][cellIdx] */
|
|
3467
|
+
layout: RowTemplateCell[][];
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
interface RowTemplateCell {
|
|
3471
|
+
/** 필드 ID */
|
|
3472
|
+
id: string;
|
|
3473
|
+
/** 가로 병합 */
|
|
3474
|
+
colSpan?: number;
|
|
3475
|
+
/** 세로 병합 */
|
|
3476
|
+
rowSpan?: number;
|
|
3477
|
+
}
|
|
3478
|
+
```
|
|
3479
|
+
|
|
3480
|
+
```typescript
|
|
3481
|
+
// 2-row 레이아웃: 첫 행은 name이 2칸 차지, 둘째 행은 description이 한 줄
|
|
3482
|
+
rowTemplate: {
|
|
3483
|
+
rowCount: 2,
|
|
3484
|
+
layout: [
|
|
3485
|
+
[{ id: 'name', colSpan: 2 }, { id: 'status' }],
|
|
3486
|
+
[{ id: 'description', colSpan: 3 }],
|
|
3487
|
+
],
|
|
3488
|
+
}
|
|
3489
|
+
```
|
|
3490
|
+
|
|
3491
|
+
---
|
|
3492
|
+
|
|
3493
|
+
### 구조적 행 — RowConfig
|
|
3494
|
+
|
|
3495
|
+
`grid.rows.addPinnedTop(row | RowConfig)`, `addPinnedBottom`, 또는 `new Row(config)`에서 사용.
|
|
3496
|
+
|
|
3497
|
+
```typescript
|
|
3498
|
+
interface RowConfig {
|
|
3499
|
+
id?: string;
|
|
3500
|
+
/** 구조적 행 여부 (선택/인덱스 제외) @default false */
|
|
3501
|
+
structural?: boolean;
|
|
3502
|
+
/** 행 변형 (렌더링 힌트) @default 'data' */
|
|
3503
|
+
variant?: RowVariant;
|
|
3504
|
+
/** 고정 위치 @default null */
|
|
3505
|
+
pinned?: 'top' | 'bottom' | null;
|
|
3506
|
+
/** 행 높이 (null이면 기본값) */
|
|
3507
|
+
height?: number | null;
|
|
3508
|
+
/** 행 데이터 (variant: 'data' / 'subtotal' 등) */
|
|
3509
|
+
data?: Record<string, unknown>;
|
|
3510
|
+
/** 그룹 정보 (variant: 'group-row' / 'subtotal') */
|
|
3511
|
+
group?: GroupInfo;
|
|
3512
|
+
/** 집계 설정 (variant: 'subtotal' / 'grandtotal') */
|
|
3513
|
+
aggregates?: AggregateConfig[];
|
|
3514
|
+
/** 커스텀 렌더러 (variant: 'custom') */
|
|
3515
|
+
render?: (container: HTMLElement, context: RowRenderContext) => void;
|
|
3516
|
+
/** 추가 CSS 클래스 */
|
|
3517
|
+
className?: string;
|
|
3518
|
+
}
|
|
3519
|
+
```
|
|
3520
|
+
|
|
3521
|
+
#### AggregateConfig
|
|
3522
|
+
|
|
3523
|
+
`grid.rows.addGrandTotal(aggregates)` 등에서 사용.
|
|
3524
|
+
|
|
3525
|
+
```typescript
|
|
3526
|
+
interface AggregateConfig {
|
|
3527
|
+
/** 대상 컬럼 ID */
|
|
3528
|
+
columnId: string;
|
|
3529
|
+
/**
|
|
3530
|
+
* 집계 함수. 14종 명명 함수(sum/avg/min/max/count/countAll/countNumbers/first/last/
|
|
3531
|
+
* range/stdev/stdevPop/variance/variancePop) 또는 커스텀 콜백 — AggregateFunc 타입.
|
|
3532
|
+
* 미지정 시 컬럼의 FieldDef.aggregate 사용, 그것도 없으면 'sum'
|
|
3533
|
+
*/
|
|
3534
|
+
func?: AggregateFunc;
|
|
3535
|
+
/** 표시 포맷터 */
|
|
3536
|
+
formatter?: (value: CellValue) => string;
|
|
3537
|
+
/** 라벨 (선택) */
|
|
3538
|
+
label?: string;
|
|
3539
|
+
}
|
|
3540
|
+
```
|
|
3541
|
+
|
|
3542
|
+
```typescript
|
|
3543
|
+
grid.rows.addGrandTotal([
|
|
3544
|
+
{ columnId: 'salary', formatter: v => '$' + v?.toLocaleString() },
|
|
3545
|
+
{ columnId: 'bonus' }, // FieldDef.aggregate 사용
|
|
3546
|
+
{ columnId: 'score', func: 'avg' }, // 이 행에서만 avg
|
|
3547
|
+
]);
|
|
3548
|
+
```
|
|
3549
|
+
|
|
3550
|
+
---
|
|
3551
|
+
|
|
3552
|
+
### 검증 — ValidationRule / ValidationError
|
|
3553
|
+
|
|
3554
|
+
```typescript
|
|
3555
|
+
interface ValidationRule {
|
|
3556
|
+
/** 검사 실패 시 표시할 에러 메시지 */
|
|
3557
|
+
message: string;
|
|
3558
|
+
/** 검사 실행 시점 */
|
|
3559
|
+
type: ValidationTrigger; // 'input' | 'change' | 'blur' | 'commit'
|
|
3560
|
+
/** 검사 함수: true면 유효, false면 에러 */
|
|
3561
|
+
validator: (value: CellValue, rowData?: Record<string, CellValue>) => boolean;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
interface ValidationError {
|
|
3565
|
+
rowId: string | number;
|
|
3566
|
+
field: string;
|
|
3567
|
+
/** 동일 셀에서 여러 규칙이 실패하면 메시지가 누적 */
|
|
3568
|
+
messages: string[];
|
|
3569
|
+
}
|
|
3570
|
+
```
|
|
3571
|
+
|
|
3572
|
+
```typescript
|
|
3573
|
+
fields: [
|
|
3574
|
+
{
|
|
3575
|
+
id: 'email',
|
|
3576
|
+
header: '이메일',
|
|
3577
|
+
validation: [
|
|
3578
|
+
{ type: 'change', message: '필수', validator: v => v != null && v !== '' },
|
|
3579
|
+
{ type: 'commit', message: '이메일 형식 오류', validator: v => /^.+@.+\..+$/.test(String(v)) },
|
|
3580
|
+
],
|
|
3581
|
+
},
|
|
3582
|
+
];
|
|
3583
|
+
```
|
|
3584
|
+
|
|
3585
|
+
---
|
|
3586
|
+
|
|
3587
|
+
### 셀 에디터 — EditorOption / CustomEditorParams
|
|
3588
|
+
|
|
3589
|
+
```typescript
|
|
3590
|
+
interface EditorOption {
|
|
3591
|
+
/** 실제 데이터 값 */
|
|
3592
|
+
value: CellValue;
|
|
3593
|
+
/** 표시할 라벨 */
|
|
3594
|
+
label: string;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
interface CustomEditorParams {
|
|
3598
|
+
value: CellValue;
|
|
3599
|
+
column: ColumnDef;
|
|
3600
|
+
rowData: Record<string, unknown>;
|
|
3601
|
+
/** 값을 확정하고 에디터 닫기 */
|
|
3602
|
+
onCommit: (newValue: CellValue) => void;
|
|
3603
|
+
/** 편집 취소하고 에디터 닫기 */
|
|
3604
|
+
onCancel: () => void;
|
|
3605
|
+
}
|
|
3606
|
+
```
|
|
3607
|
+
|
|
3608
|
+
```typescript
|
|
3609
|
+
// 드롭다운 (검색 가능)
|
|
3610
|
+
{
|
|
3611
|
+
id: 'grade', editable: true,
|
|
3612
|
+
editor: {
|
|
3613
|
+
type: 'dropdown',
|
|
3614
|
+
searchable: true,
|
|
3615
|
+
options: [
|
|
3616
|
+
{ value: 'S', label: 'S 등급' },
|
|
3617
|
+
{ value: 'A', label: 'A 등급' },
|
|
3618
|
+
{ value: 'B', label: 'B 등급' },
|
|
3619
|
+
],
|
|
3620
|
+
},
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
// 커스텀 에디터
|
|
3624
|
+
{
|
|
3625
|
+
id: 'color', editable: true,
|
|
3626
|
+
editor: {
|
|
3627
|
+
type: 'custom',
|
|
3628
|
+
renderer: (params: CustomEditorParams) => {
|
|
3629
|
+
const input = document.createElement('input');
|
|
3630
|
+
input.type = 'color';
|
|
3631
|
+
input.value = String(params.value ?? '#000000');
|
|
3632
|
+
input.addEventListener('change', () => params.onCommit(input.value));
|
|
3633
|
+
input.addEventListener('blur', () => params.onCancel());
|
|
3634
|
+
return input;
|
|
3635
|
+
},
|
|
3636
|
+
getValue: (el) => (el as HTMLInputElement).value,
|
|
3637
|
+
},
|
|
3638
|
+
}
|
|
3639
|
+
```
|
|
3640
|
+
|
|
3641
|
+
---
|
|
3642
|
+
|
|
3643
|
+
### 셀 렌더러 (Lifecycle 포함)
|
|
3644
|
+
|
|
3645
|
+
`FieldDef.cellRenderer`는 두 가지 시그니처를 지원하며 `function.length`로 자동 분기:
|
|
3646
|
+
|
|
3647
|
+
```typescript
|
|
3648
|
+
interface CellRendererParams {
|
|
3649
|
+
value: CellValue;
|
|
3650
|
+
rowData: Record<string, unknown>;
|
|
3651
|
+
columnId: string;
|
|
3652
|
+
rowId?: string | number;
|
|
3653
|
+
/**
|
|
3654
|
+
* 행 선택 상태 — rowHeader 셀(`rowHeader.cellRenderer`) 렌더 시에만 채워짐. 일반 데이터 셀은 undefined.
|
|
3655
|
+
* 렌더 시점 스냅샷이라 선택 "변경"만으로는 재렌더되지 않는다 — CSS(`.ps-row.ps-selected`) /
|
|
3656
|
+
* `ps-row-header-checkbox-input` 클래스 / `repaintRows`로 반영.
|
|
3657
|
+
*/
|
|
3658
|
+
selected?: boolean;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
/** Simple (arity 1) — vanilla JS 사용자 */
|
|
3662
|
+
type SimpleCellRenderer = (params: CellRendererParams) => HTMLElement | string;
|
|
3663
|
+
|
|
3664
|
+
/** Lifecycle (arity 2) — React/Vue 등 framework wrapper용 */
|
|
3665
|
+
type LifecycleCellRenderer = (
|
|
3666
|
+
container: HTMLElement,
|
|
3667
|
+
params: CellRendererParams,
|
|
3668
|
+
) => CellRendererInstance | void;
|
|
3669
|
+
|
|
3670
|
+
interface CellRendererInstance {
|
|
3671
|
+
/**
|
|
3672
|
+
* 셀이 재사용되며 데이터가 바뀔 때 호출.
|
|
3673
|
+
* - 반환 `false`: instance가 처리 못함 → 그리드가 destroy 후 re-mount
|
|
3674
|
+
* - 반환 `void`/`true`: instance가 자체 갱신함
|
|
3675
|
+
* 미정의 시: 첫 mount만 실행, 이후 framework reactivity에 위임.
|
|
3676
|
+
*/
|
|
3677
|
+
update?: (params: CellRendererParams) => boolean | void;
|
|
3678
|
+
/** 셀이 사라질 때 호출 — pool release / 컬럼 제거 / grid destroy 시 */
|
|
3679
|
+
destroy?: () => void;
|
|
3680
|
+
}
|
|
3681
|
+
```
|
|
3682
|
+
|
|
3683
|
+
```typescript
|
|
3684
|
+
// Simple 형태 — 매 렌더마다 새 요소 반환
|
|
3685
|
+
{ id: 'status', cellRenderer: ({ value }) => `<span class="badge-${value}">${value}</span>` }
|
|
3686
|
+
|
|
3687
|
+
// Lifecycle 형태 — React adapter 예시
|
|
3688
|
+
{
|
|
3689
|
+
id: 'chart',
|
|
3690
|
+
cellRenderer: (container, params) => {
|
|
3691
|
+
const root = createRoot(container);
|
|
3692
|
+
root.render(<Chart data={params.value} />);
|
|
3693
|
+
return {
|
|
3694
|
+
update: (next) => root.render(<Chart data={next.value} />),
|
|
3695
|
+
destroy: () => root.unmount(),
|
|
3696
|
+
};
|
|
3697
|
+
},
|
|
3698
|
+
}
|
|
3699
|
+
```
|
|
3700
|
+
|
|
3701
|
+
---
|
|
3702
|
+
|
|
3703
|
+
### 설정 스냅샷 — GridConfigExport / GridConfigSnapshot
|
|
3704
|
+
|
|
3705
|
+
`grid.getConfig()` 가 반환하고 `applyConfig(cfg)` 가 받는 직렬화 가능 타입.
|
|
3706
|
+
|
|
3707
|
+
```typescript
|
|
3708
|
+
/** Export bundle — 현재 상태 + reset baseline */
|
|
3709
|
+
interface GridConfigExport {
|
|
3710
|
+
/**
|
|
3711
|
+
* 저장 스키마 버전 (예: `'0.3'`).
|
|
3712
|
+
* `getConfig()` 가 항상 현재 버전(`GRID_CONFIG_VERSION`)을 실어 내보낸다.
|
|
3713
|
+
* 없으면 **버전 도입 이전 저장본**이므로 `applyConfig` 가 `'0.1'` 로 간주하고
|
|
3714
|
+
* 마이그레이션을 처음부터 태운다 (ITSM-2026-002191).
|
|
3715
|
+
*/
|
|
3716
|
+
version?: string;
|
|
3717
|
+
|
|
3718
|
+
/** 현재 그리드 상태 (런타임) */
|
|
3719
|
+
current: GridConfigSnapshot;
|
|
3720
|
+
/**
|
|
3721
|
+
* Reset 기준 baseline. 현재는 컬럼 정적 정의(`fields`) 만 반영됨.
|
|
3722
|
+
* 없으면 `applyConfig` 가 기존 baseline 유지.
|
|
3723
|
+
*/
|
|
3724
|
+
snapshot?: GridConfigSnapshot;
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
/** 단일 layer 의 직렬화된 상태 — `current` / `snapshot` 양쪽 모두 이 형식 */
|
|
3728
|
+
interface GridConfigSnapshot {
|
|
3729
|
+
mode: 'flat' | 'pivot';
|
|
3730
|
+
|
|
3731
|
+
// Flat 모드 ↓
|
|
3732
|
+
fields?: FieldDef[]; // 런타임 width/visible/order/pinned 반영됨
|
|
3733
|
+
group?: GroupConfig;
|
|
3734
|
+
rowTemplate?: RowTemplate;
|
|
3735
|
+
columnGroups?: ColumnGroupDef[];
|
|
3736
|
+
|
|
3737
|
+
// Pivot 모드 ↓
|
|
3738
|
+
rowFields?: PivotRowField[];
|
|
3739
|
+
columnFields?: PivotColumnField[];
|
|
3740
|
+
valueFields?: PivotValueField[];
|
|
3741
|
+
showRowSubTotals?: boolean;
|
|
3742
|
+
showRowGrandTotals?: boolean;
|
|
3743
|
+
showColumnSubTotals?: boolean;
|
|
3744
|
+
showColumnGrandTotals?: boolean;
|
|
3745
|
+
rowSubTotalFields?: string[];
|
|
3746
|
+
columnSubTotalFields?: string[];
|
|
3747
|
+
rowSubTotalPosition?: TotalPosition; // 'before' | 'after'
|
|
3748
|
+
rowGrandTotalPosition?: TotalPosition;
|
|
3749
|
+
columnSubTotalPosition?: TotalPosition;
|
|
3750
|
+
columnGrandTotalPosition?: TotalPosition;
|
|
3751
|
+
filterFields?: PivotFilterField[];
|
|
3752
|
+
trackSourceRows?: boolean;
|
|
3753
|
+
showZeros?: boolean;
|
|
3754
|
+
rowDisplayMode?: 'tabular' | 'outline';
|
|
3755
|
+
outlineColumnWidth?: number;
|
|
3756
|
+
outlineIndentSize?: number;
|
|
3757
|
+
rowCollapsible?: boolean; // 행 그룹 접기/펼치기 허용 (default true)
|
|
3758
|
+
columnCollapsible?: boolean; // 열 그룹 접기/펼치기 허용 (default true)
|
|
3759
|
+
|
|
3760
|
+
// 공통 view 상태 ↓
|
|
3761
|
+
sorts?: SortState[];
|
|
3762
|
+
filters?: FilterState[]; // 평탄화된 파생값 (옛 소비처 호환)
|
|
3763
|
+
filterGroups?: FilterGroupMap; // 소스별 그룹 — 있으면 이것이 우선 (ITSM-2026-002182)
|
|
3764
|
+
|
|
3765
|
+
// 트리/마스터-디테일 ↓
|
|
3766
|
+
treeConfig?: TreeConfig;
|
|
3767
|
+
masterDetailConfig?: MasterDetailConfig;
|
|
3768
|
+
|
|
3769
|
+
// UI / 동작 옵션 ↓ (PureSheetConfigBase에서 직렬화 가능한 항목만)
|
|
3770
|
+
theme?: Theme;
|
|
3771
|
+
rowHeight?: number;
|
|
3772
|
+
headerHeight?: number;
|
|
3773
|
+
rowSelection?: RowSelectionConfig;
|
|
3774
|
+
cellSelection?: CellSelectionConfig;
|
|
3775
|
+
cellSelectionImpliesRows?: boolean;
|
|
3776
|
+
editable?: boolean;
|
|
3777
|
+
tooltipMode?: 'none' | 'overflow' | 'always';
|
|
3778
|
+
alternatingRows?: boolean;
|
|
3779
|
+
showHeader?: boolean;
|
|
3780
|
+
resizableColumns?: boolean;
|
|
3781
|
+
reorderableColumns?: boolean;
|
|
3782
|
+
rowHeader?: boolean | RowHeaderConfig; // 구 showRowHeader (deprecated alias 유지)
|
|
3783
|
+
reorderableRows?: boolean;
|
|
3784
|
+
showDragHandleColumn?: boolean;
|
|
3785
|
+
autoFitColumns?: boolean;
|
|
3786
|
+
showStatusBar?: boolean;
|
|
3787
|
+
locale?: string;
|
|
3788
|
+
translations?: PartialTranslationMap;
|
|
3789
|
+
keyFields?: string[];
|
|
3790
|
+
}
|
|
3791
|
+
```
|
|
3792
|
+
|
|
3793
|
+
**`snapshot` 의 의미** — `columns.reset / resetAll` 의 복귀 기준. `applyConfig` 는 `cfg.snapshot?.fields` 가 있으면 `columns.setInitialSnapshotDefs(...)` 호출로 baseline 재구성. `options.applySnapshot: false` 면 무시.
|
|
3794
|
+
|
|
3795
|
+
**제외되는 항목** (직렬화 불가):
|
|
3796
|
+
- `data` — `loadData()`로 별도 관리
|
|
3797
|
+
- 모든 함수 (`cellRenderer`, `cellTooltip`, `formatHeader`, validator, mask formatter, `formatRow` 등) — 사용자 코드에서 재등록
|
|
3798
|
+
- DOM/객체 참조 (`undoStack`, `loader.module`, `dragDropConfig` 등)
|
|
3799
|
+
- dirty 상태 (`grid.changes.getAll()`로 별도 저장)
|
|
3800
|
+
|
|
3801
|
+
**현재 직렬화 안 되는 옵션 (향후 추가 여지)**:
|
|
3802
|
+
- `engine`, `useWorker`, `dynamicRowHeights`, `rowHeightChangeThreshold`
|
|
3803
|
+
- `enableReferCache`, `preventKeyEdit`
|
|
3804
|
+
- `rowStyle`, `headerStyle` (CSS 문자열 — `rowHeight`/`headerHeight` 와 일부 중복)
|
|
3805
|
+
- `PivotConfig.sorts` / `PivotConfig.filters` (피벗 전용 — `GridConfigSnapshot.sorts/filters` 와 별개)
|
|
3806
|
+
|
|
3807
|
+
---
|
|
3808
|
+
|
|
3809
|
+
### 저장 스키마 버전 — GridConfigMigrator
|
|
3810
|
+
|
|
3811
|
+
저장본(`getConfig()`/`applyConfig()`)에 **스키마 버전**이 붙습니다 (ITSM-2026-002191).
|
|
3812
|
+
|
|
3813
|
+
| Export | 값/시그니처 | 설명 |
|
|
3814
|
+
|---|---|---|
|
|
3815
|
+
| `GRID_CONFIG_VERSION` | `'0.3'` | 현재 저장 스키마 버전. `getConfig()` 가 항상 이 값을 실어 내보냄 |
|
|
3816
|
+
| `LEGACY_CONFIG_VERSION` | `'0.1'` | 버전 필드가 없는 저장본을 읽을 때 가정하는 버전 |
|
|
3817
|
+
| `compareConfigVersion(a, b)` | `number` | 버전 비교 — **숫자 단위**로 봄(`a<b` 음수, 같으면 0, `a>b` 양수) |
|
|
3818
|
+
| `GridConfigMigrator` | `(config: GridConfigExport, fromVersion: string) => GridConfigExport` | 하위호환 변환기 타입. `PureSheetConfig.migrateConfig` 로 주입 |
|
|
3819
|
+
|
|
3820
|
+
**버전 이력**
|
|
3821
|
+
|
|
3822
|
+
| 버전 | 내용 |
|
|
3823
|
+
|---|---|
|
|
3824
|
+
| `0.1` | 버전 필드가 없던 시절. `preFilters`/`preSorts` 사용, 수식은 `[라벨]` 참조 |
|
|
3825
|
+
| `0.2` | 수식 컬럼 참조가 `$id` 기준. `preFilters`/`preSorts` 폐지 |
|
|
3826
|
+
| `0.3` | 필터 소유권(`filterGroups`) 도입. 소유자 없는 옛 조건식을 고급 필터 수식으로 이관 |
|
|
3827
|
+
|
|
3828
|
+
> **왜 키 존재 여부로는 부족한가** — 지금까지 하위호환은 키 존재 여부로 감지했습니다(`preFilters` 가 있으면 구버전). 그런데 **키는 그대로인데 값의 의미만 바뀌는** 변경에서는 통하지 않습니다:
|
|
3829
|
+
> ```
|
|
3830
|
+
> "formula": "[급여] > 100" ← 옛 저장본 (라벨 참조)
|
|
3831
|
+
> "formula": "$salary > 100" ← 새 저장본 (id 참조)
|
|
3832
|
+
> ```
|
|
3833
|
+
> 둘 다 같은 키에 문자열이라 추측할 수밖에 없습니다. 버전이 있으면 **확실히** 압니다.
|
|
3834
|
+
>
|
|
3835
|
+
> **버전을 올리는 기준** — 올린다: 필드의 **의미**가 바뀔 때(`[라벨]` → `$id`), 필드가 **삭제/이름 변경**될 때. 올리지 않는다: 옵셔널 필드 **추가**(없으면 기본값이 적용되므로 옛 저장본이 그대로 유효).
|
|
3836
|
+
>
|
|
3837
|
+
> **마이그레이션은 누적 적용입니다.** `0.1` 저장본이 들어오면 `0.1 → 0.2 → …` 를 **순서대로 전부** 거칩니다. 최신 변환만 돌리면 중간 단계를 건너뛰어 조용히 어긋납니다.
|
|
3838
|
+
>
|
|
3839
|
+
> **변환 내용은 소비처(wrapper) 소유입니다.** core 는 호출 시점과 순서만 책임집니다 — core 가 변환까지 들고 있으면 소비처 고유 필드(`customCellConfig` 등)를 다룰 수 없고, 소비처마다 다른 진화 속도를 강제하게 됩니다. 변환기가 없거나 이미 최신이면 받은 것을 그대로 돌려줍니다(부작용 없음). 변환 중 예외가 나도 복원 자체는 막지 않고 경고(`PS901`)만 남깁니다.
|
|
3840
|
+
|
|
3841
|
+
```typescript
|
|
3842
|
+
const grid = new PureSheet(el, {
|
|
3843
|
+
fields,
|
|
3844
|
+
migrateConfig: (cfg, fromVersion) => {
|
|
3845
|
+
let out = cfg;
|
|
3846
|
+
if (compareConfigVersion(fromVersion, '0.2') < 0) out = to02(out);
|
|
3847
|
+
if (compareConfigVersion(fromVersion, '0.3') < 0) out = to03(out);
|
|
3848
|
+
return { ...out, version: GRID_CONFIG_VERSION };
|
|
3849
|
+
},
|
|
3850
|
+
});
|
|
3851
|
+
```
|
|
3852
|
+
|
|
3853
|
+
---
|
|
3854
|
+
|
|
3855
|
+
### 컬럼 식별자 유틸 — columnKeys
|
|
3856
|
+
|
|
3857
|
+
`FieldDef.id`(가리킬 때) 와 `FieldDef.field`(읽을 때) 를 정규화합니다 (ITSM-2026-002188).
|
|
3858
|
+
|
|
3859
|
+
| Export | 시그니처 | 설명 |
|
|
3860
|
+
|---|---|---|
|
|
3861
|
+
| `normalizeColumnKeys(input, context?)` | `ColumnKeys` | `{ id, field }` 를 채워 반환. `field` 미지정 시 `field = id` |
|
|
3862
|
+
| `getColumnId(input)` | `string` | 식별자만 |
|
|
3863
|
+
| `getColumnField(input)` | `string` | 데이터 키만 |
|
|
3864
|
+
| `resolveFieldKey(...)` | `string` | 컬럼 목록에서 id → 데이터 키 해석 |
|
|
3865
|
+
| `ColumnKeyInput` / `ColumnKeys` | 타입 | 입력/출력 형태 |
|
|
3866
|
+
|
|
3867
|
+
> **읽을 때 `field`, 가리킬 때 `id`.** 원본 행에서 값을 꺼내는 자리(`row[...]`)는 반드시 `field`, 정렬/필터/저장/DOM dataset 처럼 *컬럼을 지목*하는 자리는 `id` 입니다.
|
|
3868
|
+
|
|
3869
|
+
**피벗 쪽 대응** — `PivotFieldBase.id?` 가 추가됐습니다(미지정 시 `field`). 값 필드는 같은 `field` 를 합계·평균으로 **여러 번** 넣을 수 있어 `field` 만으로는 식별이 깨집니다.
|
|
3870
|
+
|
|
3871
|
+
| Export | 설명 |
|
|
3872
|
+
|---|---|
|
|
3873
|
+
| `getPivotFieldId(f)` | 피벗 필드의 식별자 — `f.id ?? f.field`. ⚠️ **데이터 접근 키가 아님** — 값은 `row[f.field]` |
|
|
3874
|
+
| `getValueFieldKey(vf, allValueFields)` | 값 필드 키. 우선순위: ① `vf.id` → ② `field` 중복 없으면 `field` → ③ 중복인데 id 미지정이면 `'sales-0'` 같은 **순서 기반 폴백 + 경고** |
|
|
3875
|
+
|
|
3876
|
+
> ⚠️ **③ 순서 기반 폴백은 위험합니다.** 값 필드 순서를 바꾸거나 하나를 지우면 인덱스가 밀려 **같은 저장본이 다른 필드를 가리킵니다**. 수식 필터/조건부 서식이 이 키를 참조하면 조건이 조용히 달라집니다. 같은 `field` 를 여러 번 쓸 거면 **불변 `id` 를 명시**하세요(자동 생성으로는 불변성을 보장할 수 없습니다).
|
|
3877
|
+
>
|
|
3878
|
+
> ```typescript
|
|
3879
|
+
> // 중복 없음 — id 불필요 (id = 'sales')
|
|
3880
|
+
> valueFields: [{ field: 'sales', aggregate: 'sum' }]
|
|
3881
|
+
>
|
|
3882
|
+
> // 같은 field 를 여러 번 — id 명시
|
|
3883
|
+
> valueFields: [
|
|
3884
|
+
> { field: 'sales', aggregate: 'sum', id: 'sales_sum' },
|
|
3885
|
+
> { field: 'sales', aggregate: 'avg', id: 'sales_avg' },
|
|
3886
|
+
> ]
|
|
3887
|
+
> ```
|
|
3888
|
+
|
|
3889
|
+
---
|
|
3890
|
+
|
|
3891
|
+
### 수식 엔진 — FormulaEngine
|
|
3892
|
+
|
|
3893
|
+
조건부 서식과 필터가 **같은 문법**을 씁니다. wrapper 에 있던 엔진을 core 로 이관했습니다 (ITSM-2026-002187) — 필터는 core 에서 평가되므로 wrapper 를 볼 수 없기 때문입니다.
|
|
3894
|
+
|
|
3895
|
+
**문법**
|
|
3896
|
+
|
|
3897
|
+
| 요소 | 표기 |
|
|
3898
|
+
|---|---|
|
|
3899
|
+
| 컬럼 참조 | `[컬럼헤더]`(표시) / `$id`(저장) — 공백·한글 헤더 지원 |
|
|
3900
|
+
| 논리 | `AND` / `OR` / `NOT` 또는 `&&` / `\|\|` / `!` |
|
|
3901
|
+
| 비교 | `==` `!=` `>` `>=` `<` `<=` |
|
|
3902
|
+
| 문자열 | `CONTAINS` `NOT_CONTAINS` `STARTS_WITH` `ENDS_WITH` `LIKE` `IS_EMPTY` `IS_NOT_EMPTY` |
|
|
3903
|
+
| 숫자 | `ABS` `ROUND` `FLOOR` `CEIL` `MIN` `MAX` |
|
|
3904
|
+
| 텍스트 | `LEN` `UPPER` `LOWER` `TRIM` `LEFT` `RIGHT` `CONCAT` |
|
|
3905
|
+
| 날짜 | `NOW` `TODAY` `DAYS`, `#2025-09-05#` 날짜 리터럴 |
|
|
3906
|
+
| 기타 | `IF` `COALESCE` |
|
|
3907
|
+
|
|
3908
|
+
**Export**
|
|
3909
|
+
|
|
3910
|
+
| Export | 설명 |
|
|
3911
|
+
|---|---|
|
|
3912
|
+
| `FormulaEngine`, `FormulaLexer`, `FormulaParser`, `FormulaEvaluator`, `TokenType` | 엔진 클래스/토큰 |
|
|
3913
|
+
| `evaluateFormula(...)` / `validateFormula(...)` | 평가 / 유효성 검사(→ AST) |
|
|
3914
|
+
| `toFriendlyFormulaError(err)` | 파서 원문 → 사용자 문구. **수식을 입력받는 UI 는 모두 이걸 거쳐야** 창마다 다른 말이 나오지 않음 |
|
|
3915
|
+
| `toFormulaDataType(dataType)` | `FieldDef.dataType`(문자열) → 엔진의 숫자 dataType. 안 실으면 타입 검사가 꺼짐 |
|
|
3916
|
+
| `astToFormula(node)` | AST → 수식 문자열 **역변환**. 고급 필터의 트리 탭이 편집 결과를 다시 수식으로 저장할 때 사용(수식 → AST 는 `validateFormula`) |
|
|
3917
|
+
| 타입 | `ASTNode`, `ParseResult`, `EvaluateResult`, `Token`, `LiteralNode`, `ColumnRefNode`, `BinaryOpNode`, `UnaryOpNode`, `FunctionCallNode`, `PostfixOpNode`, `FormulaColumnInfo`(core 의 `ColumnInfo` 와 이름 충돌을 피한 별칭) |
|
|
3918
|
+
|
|
3919
|
+
**컬럼 참조 변환 — 저장은 `$id`, 표시는 `[현재 라벨]`**
|
|
3920
|
+
|
|
3921
|
+
| Export | 설명 |
|
|
3922
|
+
|---|---|
|
|
3923
|
+
| `formulaToStorage(formula, columns)` | 표시 표기 → 저장 표기 |
|
|
3924
|
+
| `formulaToDisplay(formula, columns)` | 저장 표기 → 표시 표기 |
|
|
3925
|
+
| `migrateFormulaToStorage(formula, columns)` | 옛 라벨 참조 저장본 → `$id` 표기 |
|
|
3926
|
+
| `UNRESOLVED_REF_PREFIX` | `'?'` — 해석되지 않은 참조 접두사 |
|
|
3927
|
+
| `FormulaConversion`, `FormulaRefIssue` | 변환 결과/이슈 타입 |
|
|
3928
|
+
|
|
3929
|
+
> 라벨이 바뀌어도(다국어·헤더 편집) **같은 저장본이 늘 현재 라벨로 보입니다.** 필터 평가는 저장 표기(`$id`)를 그대로 쓰므로 라벨 변경이 조건을 깨뜨리지 않습니다.
|
|
3930
|
+
|
|
3931
|
+
---
|
|
3932
|
+
|
|
3933
|
+
### 값 목록 위젯 — ValueCheckList
|
|
3934
|
+
|
|
3935
|
+
엑셀식 값 필터의 값 선택 UI. 소비처가 팝오버 등에 직접 mount 합니다 (ITSM-2026-002179).
|
|
3936
|
+
|
|
3937
|
+
```typescript
|
|
3938
|
+
import { ValueCheckList, VALUE_LIST_BLANK_KEY, DEFAULT_VALUE_LIST_LIMIT } from '@vmscloud/moz-ui-grid-core';
|
|
3939
|
+
|
|
3940
|
+
const list = new ValueCheckList(container, {
|
|
3941
|
+
mode: 'exclude',
|
|
3942
|
+
onSelectionChange: (keys) => { /* 적용 시점은 소비처가 정한다 */ },
|
|
3943
|
+
onKeywordChange: async (kw) => { // 지정하면 검색을 소비처가 소유
|
|
3944
|
+
const r = await grid.getFilterValues(columnId, { keyword: kw });
|
|
3945
|
+
list.setValues(r.values, selectedKeys, { truncated: r.truncated, rawValues: r.rawValues });
|
|
3946
|
+
},
|
|
3947
|
+
});
|
|
3948
|
+
list.setLoading(true);
|
|
3949
|
+
const r = await grid.getFilterValues(columnId);
|
|
3950
|
+
list.setValues(r.values, /* selectedKeys 생략 = 모드 기본 체크 */ undefined,
|
|
3951
|
+
{ truncated: r.truncated, limit: DEFAULT_VALUE_LIST_LIMIT, rawValues: r.rawValues });
|
|
3952
|
+
list.getSelectedKeys(); // 선택 키
|
|
3953
|
+
list.getSelectedItems(); // 적용 시점에 읽어간다
|
|
3954
|
+
list.setMode('include'); // 저장 방향 전환 (체크가 뒤집힌다)
|
|
3955
|
+
list.destroy();
|
|
3956
|
+
```
|
|
3957
|
+
|
|
3958
|
+
**주요 메서드**
|
|
3959
|
+
|
|
3960
|
+
| 메서드 | 설명 |
|
|
3961
|
+
|---|---|
|
|
3962
|
+
| `setValues(values, selectedKeys?, meta?)` | 목록 채우기. `meta`: `{ truncated?, limit?, rawValues?, offListPicks? }`. `selectedKeys` 생략 시 **모드 기본 체크**(`'exclude'`=전체 선택, `'include'`=전체 해제) |
|
|
3963
|
+
| `setLoading(loading)` | 로딩 표시 |
|
|
3964
|
+
| `getSelectedKeys()` / `getSelectedItems()` | 현재 선택 |
|
|
3965
|
+
| `setMode(mode)` | 저장 방향 전환 — 지목 집합은 유지하고 의미만 뒤집으므로 **체크가 반전**됩니다 |
|
|
3966
|
+
| `destroy()` | 해제 |
|
|
3967
|
+
|
|
3968
|
+
**`ValueCheckListOptions`**
|
|
3969
|
+
|
|
3970
|
+
| 속성 | 타입 | 기본값 | 설명 |
|
|
3971
|
+
|---|---|---|---|
|
|
3972
|
+
| `mode` | `ValueListMode` (`'exclude' \| 'include'`) | `'exclude'` | 저장 방향. `'exclude'` 는 전체 체크로 시작하는 엑셀 AutoFilter 동작 |
|
|
3973
|
+
| `onSelectionChange` | `(selectedKeys: string[]) => void` | — | 선택 상태 변경 시 호출 |
|
|
3974
|
+
| `onKeywordChange` | `(keyword: string) => void` | — | 검색어 변경 시(디바운스 후). **지정하면 검색을 소비처가 소유** — 상한 초과로 목록이 잘린 경우 로컬 필터링으로는 상한 밖 값을 찾을 수 없으므로 엔진에 keyword 를 넘겨 재조회. 미지정이면 받은 목록 안에서만 로컬 필터링 |
|
|
3975
|
+
| `i18n` | `I18nManager` | 영문 기본 문구 | 다국어 |
|
|
3976
|
+
| `height` | `number` | `260` | 목록 영역 높이(px) |
|
|
3977
|
+
|
|
3978
|
+
**`ValueListMode` — 저장 방향**
|
|
3979
|
+
|
|
3980
|
+
| 모드 | 초기 체크 | 지목(picked) | core 연산자 |
|
|
3981
|
+
|---|---|---|---|
|
|
3982
|
+
| `'exclude'` (기본) | 전체 체크 | **체크 해제한 값** | `notIn` |
|
|
3983
|
+
| `'include'` | 전체 해제 | **체크한 값** | `in` |
|
|
3984
|
+
|
|
3985
|
+
> **왜 모드가 필요한가 — 한쪽만 두면 반대쪽에서 터집니다.**
|
|
3986
|
+
> `include` 만 있으면 고유값 3만짜리 컬럼에서 **하나만 해제해도** 나머지 29,999개를 담아야 합니다. 게다가 목록 수집에는 상한이 있어 그 배열이 상한까지밖에 안 차고, **수집되지 않은 값의 행이 조용히 사라집니다**(실측: 고유값 30,000 · 상한 10,000 에서 1개 해제 → 기대 29,999행 / 실제 9,999행, 20,000행 유실). `exclude` 만 있으면 반대로 "3개만 골라 보기"에서 29,997개를 담게 됩니다.
|
|
3987
|
+
>
|
|
3988
|
+
> 그래서 **사용자가 의도를 고릅니다.** 상용 그리드의 표준 패턴입니다 — DevExtreme DataGrid 는 헤더 필터에 `filterType: 'include' | 'exclude'` 가 있고, Tableau 는 필터 카드 메뉴에 Include values / Exclude values 를 둡니다.
|
|
3989
|
+
|
|
3990
|
+
**`ValueListItem`**
|
|
3991
|
+
|
|
3992
|
+
| 속성 | 타입 | 설명 |
|
|
3993
|
+
|---|---|---|
|
|
3994
|
+
| `key` | `string` | 선택 상태 관리용 키(문자열화된 값, 빈 값은 `VALUE_LIST_BLANK_KEY`) |
|
|
3995
|
+
| `label` | `string` | 화면 표시 라벨 |
|
|
3996
|
+
| `value` | `CellValue` | 필터에 실을 **원본값**(빈 값은 `null`) |
|
|
3997
|
+
| `pinned` | `boolean?` | 현재 후보 목록에는 없지만 사용자가 지목해 **목록 맨 위에 고정 표시 중**인 항목 |
|
|
3998
|
+
|
|
3999
|
+
> **`pinned` 가 왜 필요한가** — 고유값이 수집 상한을 넘으면 목록이 잘립니다. 사용자가 검색으로 상한 밖 값을 찾아 조작한 뒤 검색어를 지우면 그 값은 기본 목록에 없어 화면에서 사라지는데 **지목은 살아 있습니다**. 그러면 필터에는 실려 있는데 화면 어디에도 없는 값이 되어 되돌릴 방법이 없습니다.
|
|
4000
|
+
>
|
|
4001
|
+
> 위젯은 팝오버가 닫힐 때 파괴되므로 지목도 함께 사라집니다. 다시 열 때 소비처가 저장된 필터에서 "현재 후보 목록에 없는 지목"을 추려 **`setValues(..., { offListPicks })`** 로 넘겨야 고정 표시가 이어집니다.
|
|
4002
|
+
|
|
4003
|
+
| 상수 | 값 | 설명 |
|
|
4004
|
+
|---|---|---|
|
|
4005
|
+
| `DEFAULT_VALUE_LIST_LIMIT` | `10_000` | 값 수집 기본 상한 (Excel AutoFilter 와 동일) |
|
|
4006
|
+
| `VALUE_LIST_BLANK_KEY` | — | 빈 값(null/undefined/'')을 나타내는 선택 키 |
|
|
4007
|
+
|
|
4008
|
+
**수집 결과 타입** (`grid.getFilterValues` 반환)
|
|
4009
|
+
|
|
4010
|
+
```typescript
|
|
4011
|
+
interface ColumnValuesResult {
|
|
4012
|
+
values: CellValue[]; // 표시 라벨 (mask 적용)
|
|
4013
|
+
rawValues: CellValue[]; // values[i] 에 대응하는 **원본값** — 필터는 이걸로 만든다
|
|
4014
|
+
total: number;
|
|
4015
|
+
truncated: boolean; // 상한 도달로 조기 종료 → "검색으로 좁혀 주세요" 안내
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
interface ColumnValuesProgress {
|
|
4019
|
+
values: CellValue[]; // **이번 청크의 델타** — 소비처가 누적 목록에 append
|
|
4020
|
+
rawValues: CellValue[];
|
|
4021
|
+
total: number; // 누적 고유값 개수
|
|
4022
|
+
truncated: boolean;
|
|
4023
|
+
done: boolean;
|
|
4024
|
+
}
|
|
4025
|
+
```
|
|
4026
|
+
|
|
4027
|
+
> **왜 `rawValues` 를 함께 돌려주나** — 표시값으로 필터링하면 행마다 `applyMask` 를 불러야 하는데 `toLocaleString()` 자체가 비싸서 5M 행에서 **1,903ms(전체의 92%)** 를 먹습니다(실측). 원본끼리 비교하면 62ms 로 **30.6배** 빠릅니다. 게다가 함수형 mask 는 Worker 로 복제되지 않아 워커 모드가 크래시합니다. 문자열 `parser` 역변환은 mask 가 다대일이라 왕복이 깨집니다(실측: `NaN` → `"NaN"` → `0`).
|
|
4028
|
+
|
|
4029
|
+
---
|
|
4030
|
+
|
|
4031
|
+
### formatRow 컨텍스트 — FormatRowInfo
|
|
4032
|
+
|
|
4033
|
+
`formatRow` 콜백과 `formatRow.addHandler(id, cb)`에 전달되는 정보. Discriminated union이므로 `info.type`으로 분기합니다.
|
|
4034
|
+
|
|
4035
|
+
```typescript
|
|
4036
|
+
type FormatRowInfo =
|
|
4037
|
+
| { type: 'data'; ctx: DataRowContext }
|
|
4038
|
+
| { type: 'group-row'; ctx: GroupRowContext }
|
|
4039
|
+
| { type: 'subtotal'; ctx: SubtotalContext }
|
|
4040
|
+
| { type: 'grand-total'; ctx: SubtotalContext };
|
|
4041
|
+
|
|
4042
|
+
type FormatRowCallback = (info: FormatRowInfo) => void;
|
|
4043
|
+
|
|
4044
|
+
interface DataRowContext {
|
|
4045
|
+
viewIndex: number; // 가상화 기준 인덱스
|
|
4046
|
+
dataIndex: number; // 원본 데이터 기준
|
|
4047
|
+
rowId?: string | number;
|
|
4048
|
+
data: Row;
|
|
4049
|
+
groupPath: string[];
|
|
4050
|
+
rowState: RowState; // 'pristine' | 'added' | 'modified' | 'deleted'
|
|
4051
|
+
originalData?: Row; // modified일 때만
|
|
4052
|
+
changedFields?: Set<string>; // modified일 때만
|
|
4053
|
+
rowElement: HTMLElement;
|
|
4054
|
+
/** 컬럼 ID → 셀 정보 맵 */
|
|
4055
|
+
cells: Record<string, CellInfo>;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
interface GroupRowContext {
|
|
4059
|
+
viewIndex: number;
|
|
4060
|
+
groupId: string; // 토글용 ID (= grid.rowGroup.getGroupRows 인자)
|
|
4061
|
+
column: string;
|
|
4062
|
+
value: CellValue;
|
|
4063
|
+
level: number;
|
|
4064
|
+
itemCount: number;
|
|
4065
|
+
collapsed: boolean;
|
|
4066
|
+
aggregates: Record<string, CellValue>;
|
|
4067
|
+
element: HTMLElement;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
interface SubtotalContext {
|
|
4071
|
+
viewIndex: number;
|
|
4072
|
+
level: number;
|
|
4073
|
+
aggregates: Record<string, CellValue>;
|
|
4074
|
+
element: HTMLElement;
|
|
4075
|
+
cells: Record<string, CellInfo>;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
interface CellInfo {
|
|
4079
|
+
element: HTMLElement;
|
|
4080
|
+
value: CellValue;
|
|
4081
|
+
originalValue?: CellValue; // modified일 때만
|
|
4082
|
+
isModified: boolean;
|
|
4083
|
+
}
|
|
4084
|
+
```
|
|
4085
|
+
|
|
4086
|
+
```typescript
|
|
4087
|
+
// addHandler 등록 옵션
|
|
4088
|
+
grid.formatRow.addHandler('highlight', (info) => {
|
|
4089
|
+
if (info.type !== 'data') return;
|
|
4090
|
+
const { data, cells, rowElement } = info.ctx;
|
|
4091
|
+
if ((data.price as number) > 80000) {
|
|
4092
|
+
rowElement.style.backgroundColor = 'rgba(255, 193, 7, 0.12)';
|
|
4093
|
+
}
|
|
4094
|
+
if (data.stock as number <= 20) {
|
|
4095
|
+
cells['stock']?.element.style.color = '#d32f2f';
|
|
4096
|
+
}
|
|
4097
|
+
}, { priority: 50 });
|
|
4098
|
+
```
|
|
4099
|
+
|
|
4100
|
+
---
|
|
4101
|
+
|
|
4102
|
+
### formatHeader 컨텍스트 — FormatHeaderContext
|
|
4103
|
+
|
|
4104
|
+
`formatHeader` 콜백과 `formatHeader.addHandler(id, cb)` 에 전달되는 정보.
|
|
4105
|
+
`formatRow` 의 헤더 카운터파트.
|
|
4106
|
+
|
|
4107
|
+
#### 타입
|
|
4108
|
+
|
|
4109
|
+
```typescript
|
|
4110
|
+
type FormatHeaderCallback = (ctx: FormatHeaderContext) => void;
|
|
4111
|
+
|
|
4112
|
+
interface FormatHeaderContext {
|
|
4113
|
+
/**
|
|
4114
|
+
* columnId → 헤더 셀 정보 맵.
|
|
4115
|
+
* - Flat: 모든 컬럼 헤더 셀
|
|
4116
|
+
* - Pivot: rowHeader 컬럼 셀 + center leaf 셀 (valueField 리프 컬럼)
|
|
4117
|
+
* 중간 그룹 셀(예: year, quarter 노드)은 여기 들어가지 않고 `pivotMeta.allCells` 에서 접근.
|
|
4118
|
+
*/
|
|
4119
|
+
cells: Record<string, HeaderCellInfo>;
|
|
4120
|
+
/** 헤더 컨테이너 DOM */
|
|
4121
|
+
element: HTMLElement;
|
|
4122
|
+
/** 현재 전체 정렬 상태 (다중 정렬 시 우선순위 순서, sequence enrich 됨) */
|
|
4123
|
+
sortStates: SortState[];
|
|
4124
|
+
/** 현재 전체 필터 상태 */
|
|
4125
|
+
filterStates: FilterState[];
|
|
4126
|
+
/**
|
|
4127
|
+
* Pivot 전용 메타 (Flat 에서는 undefined).
|
|
4128
|
+
* `allCells` 는 rowHeader + center leaf + center 그룹(non-leaf) 셀 전체.
|
|
4129
|
+
* 그룹 셀 서식은 `cell.nodeId` 매칭으로 찾는다 (`data-column-node-id` 와 동일).
|
|
4130
|
+
*/
|
|
4131
|
+
pivotMeta?: {
|
|
4132
|
+
/** rowHeader + center 모든 셀 (leaf + non-leaf 그룹 셀 포함) */
|
|
4133
|
+
allCells: HeaderCellInfo[];
|
|
4134
|
+
/** 헤더 총 레벨 수 (columnFields.length + 1[valueField 레벨]) */
|
|
4135
|
+
levelCount: number;
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
interface HeaderCellInfo {
|
|
4140
|
+
/** 컬럼 ID — leaf 셀(Flat/rowHeader/center-leaf)에서만 의미. Pivot 그룹 셀은 빈 문자열 */
|
|
4141
|
+
columnId: string;
|
|
4142
|
+
/** 표시되는 헤더 텍스트 (label ?? header ?? id 합성 — 항상 비어있지 않음) */
|
|
4143
|
+
header: string;
|
|
4144
|
+
/** 헤더 셀 DOM (인터랙티브 요소 포함, 정렬 아이콘 등 자식 노드 있음) */
|
|
4145
|
+
element: HTMLElement;
|
|
4146
|
+
/** 핀 위치 */
|
|
4147
|
+
pinned: 'left' | 'right' | 'none';
|
|
4148
|
+
/** 이 컬럼의 현재 정렬 상태 (없으면 undefined). index 는 다중 정렬 시 우선순위 */
|
|
4149
|
+
sortState?: { direction: 'asc' | 'desc'; index?: number };
|
|
4150
|
+
|
|
4151
|
+
// ─── Pivot 전용 메타 (Flat 에서는 undefined) ───
|
|
4152
|
+
/** PivotHeaderNode.columnNodeId (= `data-column-node-id`). 중간 그룹 노드 셀에만 설정 */
|
|
4153
|
+
nodeId?: string;
|
|
4154
|
+
/** leaf 셀 여부 — Flat / rowHeader / center leaf 는 true, 그룹 셀만 false */
|
|
4155
|
+
isLeaf?: boolean;
|
|
4156
|
+
/** Pivot 헤더 레벨 (0-based) — Flat 에서는 undefined */
|
|
4157
|
+
level?: number;
|
|
4158
|
+
/** colspan (Pivot 그룹 셀의 자식 leaf 수). Flat / leaf 는 1 */
|
|
4159
|
+
colspan?: number;
|
|
4160
|
+
/** rowspan (Pivot rowHeader 셀은 levelCount, 레벨 갭 셀은 갭 크기). Flat / 일반 leaf 는 1 */
|
|
4161
|
+
rowspan?: number;
|
|
4162
|
+
}
|
|
4163
|
+
```
|
|
4164
|
+
|
|
4165
|
+
#### 발화 매트릭스 — 정확히 언제 호출되나
|
|
4166
|
+
|
|
4167
|
+
| 액션 | invoke 호출? | 비고 |
|
|
4168
|
+
|---|---|---|
|
|
4169
|
+
| `new PureSheet(...)` 후 `loadData()` | ✓ | 초기 렌더 |
|
|
4170
|
+
| 헤더 클릭으로 정렬 변경 | ✓ | sort 아이콘/`ps-sorted` 갱신 |
|
|
4171
|
+
| `columns.sort(...)` 호출 | ✓ | 위와 동일 |
|
|
4172
|
+
| `columns.filter(...)` | ✓ | 필터 인디케이터 갱신 |
|
|
4173
|
+
| `columns.pinLeft / pinRight / unpin` | ✓ | 헤더 cell 재구성 (`render`) — 자동 invoke |
|
|
4174
|
+
| `columns.setWidth` / 사용자 헤더 드래그 리사이즈 | ✓ | per-column path (`render` 없이 인라인 width 패치) — `GridRenderer.setColumnWidth` 끝에서 명시 invoke |
|
|
4175
|
+
| `columns.setOrder / move / moveGroup` | ✓ | 헤더 cell 재구성 (`render`) — 자동 invoke |
|
|
4176
|
+
| `columns.hide / show / setHidden / toggle` | ✓ | 헤더 cell 재구성 (`render`) — 자동 invoke |
|
|
4177
|
+
| `columns.update(...)` (헤더 영향 키 — 아래 표 참고) | ✓ | refresh 동반 |
|
|
4178
|
+
| `columns.update(...)` (예: `width`만 변경) | ✓ | refresh 안 거치지만 헤더 재렌더는 됨 |
|
|
4179
|
+
| `columns.reset / resetAll` | ✓ | refresh 동반 |
|
|
4180
|
+
| `headers.toggle(...)` (컬럼 그룹 접기/펼치기) | ✓ | |
|
|
4181
|
+
| `grid.refresh()` | ✓ | 풀 리렌더 |
|
|
4182
|
+
| 테마 변경 (`setTheme`) | ✓ | |
|
|
4183
|
+
| 로케일 변경 | ✓ | |
|
|
4184
|
+
| **수직 스크롤** (body 가상화) | ✗ | 헤더 sticky — 재렌더 없음 |
|
|
4185
|
+
| **수평 스크롤** (center 가상화 범위 변경) | ✓ | `renderCenterHeader` 끝에서 invoke (center 영역 재생성 시 사용자 서식 보존) |
|
|
4186
|
+
| 셀 편집/CRUD/Undo | ✗ | body 만 영향 |
|
|
4187
|
+
|
|
4188
|
+
> **per-column setter 의 자동 invoke**: `setColumnWidth` 는 헤더 cell DOM 을 재구성하지 않고 인라인 width 만 패치 → `HeaderRenderer.render()` 가 호출되지 않으므로 일반 경로로는 `formatHeader` 가 발화되지 않습니다. 코어가 setter 끝에서 명시적으로 `headerRenderer.invokeFormatHeader()` 를 호출해서 사용자 layer (예: 사용자 컬럼 서식) 가 헤더 inline style/class 를 잃지 않도록 보장합니다.
|
|
4189
|
+
>
|
|
4190
|
+
> **수평 가상화**: `renderCenterHeader` 도 끝에서 invoke 를 호출하므로 가상화된 center 컬럼 헤더에 사용자 서식이 보존됩니다. 컬럼 가시성 변경 시 stale 한 `horizontalVirtualRange` 로 인한 center 비어 보이는 이슈 방지를 위해 `HeaderRenderer.updateColumns` 가 진입 시 range 를 invalidate 함.
|
|
4191
|
+
|
|
4192
|
+
#### `columns.update` 의 어떤 키가 헤더 재렌더를 일으키나
|
|
4193
|
+
|
|
4194
|
+
| 키 | 헤더 재렌더? | 헤더 서식에 영향? |
|
|
4195
|
+
|---|---|---|
|
|
4196
|
+
| `header` / `label` / `headerAttributes` / `headerRenderer` | ✓ refresh | 직접 영향 — 핸들러 재계산 필요 |
|
|
4197
|
+
| `align` / `mask` / `cellAttributes` / `cellRenderer` / `aggregate` / 등 body 영향 | ✓ refresh | 헤더 메타는 그대로지만 invoke 는 됨 (헤더도 같이 재렌더되므로) |
|
|
4198
|
+
| `width` / `pinned` / `hidden` / `autoFit` | ✓ per-column path 만 | 헤더 셀 위치/크기/표시 영향 |
|
|
4199
|
+
| `sortable` / `filterable` / `editable` 등 mutate-only | ✗ | 다음 인터랙션 시 자동 반영 |
|
|
4200
|
+
|
|
4201
|
+
#### 다중 owner 패턴 — wrapper / plugin / 검색 하이라이트 공존
|
|
4202
|
+
|
|
4203
|
+
```typescript
|
|
4204
|
+
// 1) wrapper UI — 사용자 정의 헤더 서식 (정적 객체)
|
|
4205
|
+
const userHeaderFmt = new Map<string, { class?: string; style?: string }>();
|
|
4206
|
+
|
|
4207
|
+
grid.formatHeader.addHandler('user-header-fmt', (ctx) => {
|
|
4208
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
4209
|
+
const fmt = userHeaderFmt.get(colId);
|
|
4210
|
+
if (!fmt) continue;
|
|
4211
|
+
if (fmt.class) cell.element.classList.add(...fmt.class.split(' '));
|
|
4212
|
+
if (fmt.style) cell.element.style.cssText += ';' + fmt.style;
|
|
4213
|
+
}
|
|
4214
|
+
}, { priority: 80 });
|
|
4215
|
+
|
|
4216
|
+
// 2) 검색 하이라이트 plugin — 검색 매칭 컬럼 헤더에 인디케이터
|
|
4217
|
+
const matchedColumns = new Set<string>();
|
|
4218
|
+
|
|
4219
|
+
grid.formatHeader.addHandler('search-match', (ctx) => {
|
|
4220
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
4221
|
+
cell.element.classList.toggle('search-matched', matchedColumns.has(colId));
|
|
4222
|
+
}
|
|
4223
|
+
}, { priority: 60 });
|
|
4224
|
+
|
|
4225
|
+
// 3) 정렬 시각 강조 — 다중 정렬 순위 뱃지
|
|
4226
|
+
grid.formatHeader.addHandler('sort-rank', (ctx) => {
|
|
4227
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
4228
|
+
cell.element.dataset.sortRank = cell.sortState
|
|
4229
|
+
? String((cell.sortState.index ?? 0) + 1)
|
|
4230
|
+
: '';
|
|
4231
|
+
}
|
|
4232
|
+
}, { priority: 50 });
|
|
4233
|
+
```
|
|
4234
|
+
|
|
4235
|
+
세 핸들러 모두 등록 시 priority 순(50 → 60 → 80)으로 실행. 각 owner 는 자기 key 로 add/remove 독립 관리.
|
|
4236
|
+
|
|
4237
|
+
#### priority 가이드
|
|
4238
|
+
|
|
4239
|
+
`formatRow` 와 동일한 체계를 공유합니다 (RenderHookManager 같은 클래스).
|
|
4240
|
+
|
|
4241
|
+
| 범위 | 용도 |
|
|
4242
|
+
|---|---|
|
|
4243
|
+
| 0–9 | Core 초기화 (스타일/속성 리셋) |
|
|
4244
|
+
| 10–29 | 정렬 아이콘, 필터 인디케이터 등 core 시각 마커 |
|
|
4245
|
+
| 30–69 | 일반 기능 (사용자 코드 권장) |
|
|
4246
|
+
| 70–89 | wrapper-managed user UI (CellSettings 등) |
|
|
4247
|
+
| 90–99 | 최종 오버라이드 (디버그/QA 도구) |
|
|
4248
|
+
|
|
4249
|
+
핸들러는 모두 **같은 ctx 객체**를 공유하므로, 같은 cell 의 `style`/`class` 를 여러 핸들러가 누적할 수 있습니다.
|
|
4250
|
+
|
|
4251
|
+
#### cleanup 패턴 — Row pool 누수 방지
|
|
4252
|
+
|
|
4253
|
+
`formatRow` 와 달리 헤더 셀은 가상화로 인한 재사용이 적어 누수 위험이 작지만, **핸들러 자체에서 idempotent 하게 작성**하는 게 안전합니다.
|
|
4254
|
+
|
|
4255
|
+
```typescript
|
|
4256
|
+
// 안티패턴 — 누적되어 누수
|
|
4257
|
+
grid.formatHeader.addHandler('bad', (ctx) => {
|
|
4258
|
+
for (const cell of Object.values(ctx.cells)) {
|
|
4259
|
+
cell.element.classList.add('mark'); // 매 호출마다 add (한 번이면 OK이지만 toggle 가시성 등엔 누적)
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
|
|
4263
|
+
// 권장 — 상태 토글
|
|
4264
|
+
grid.formatHeader.addHandler('good', (ctx) => {
|
|
4265
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
4266
|
+
cell.element.classList.toggle('mark', shouldMark(colId));
|
|
4267
|
+
}
|
|
4268
|
+
});
|
|
4269
|
+
```
|
|
4270
|
+
|
|
4271
|
+
핸들러 제거 시 잔존 스타일은 wrapper 가 명시 정리:
|
|
4272
|
+
|
|
4273
|
+
```typescript
|
|
4274
|
+
function cleanup() {
|
|
4275
|
+
grid.formatHeader.removeHandler('user-header-fmt');
|
|
4276
|
+
// 잔존 인라인 스타일 제거 (한 번 적용해두면 핸들러 제거해도 남아있음)
|
|
4277
|
+
for (const c of grid.columns.getAll()) {
|
|
4278
|
+
const cell = grid.columns.getById(c.id);
|
|
4279
|
+
// 또는 직접 DOM 조회
|
|
4280
|
+
document.querySelector(`[data-column-id="${c.id}"]`)?.removeAttribute('style');
|
|
4281
|
+
}
|
|
4282
|
+
grid.refresh(); // 핸들러 제거 후 1회 재렌더
|
|
4283
|
+
}
|
|
4284
|
+
```
|
|
4285
|
+
|
|
4286
|
+
#### `formatHeader` vs `columns.update({ headerAttributes })` — 언제 어느 걸?
|
|
4287
|
+
|
|
4288
|
+
| 시나리오 | 권장 |
|
|
4289
|
+
|---|---|
|
|
4290
|
+
| 사용자 UI 가 컬럼별 동적 헤더 서식 (자주 변함) | **formatHeader.addHandler** — wrapper state 자동 추적, devSnap 불필요 |
|
|
4291
|
+
| 컬럼 옵션을 영구 변경 (label, title 등) | **columns.update({ header / headerAttributes })** + `updateSnapshot: true` — config snapshot 에 반영 |
|
|
4292
|
+
| 정렬/필터 메타 기반 시각 마커 | **formatHeader.addHandler** — sortState/filterState 가 ctx 에 들어옴 |
|
|
4293
|
+
| 1회성 외부 attr 부여 (분석 도구 data-*) | **columns.update({ headerAttributes })** — 정적이라 hook 불필요 |
|
|
4294
|
+
| dev 정적 attr 보존하면서 user 변경분 얹기 | **formatHeader.addHandler** — owner 분리, 충돌 없음 |
|
|
4295
|
+
|
|
4296
|
+
#### 직렬화 / Config Snapshot 관계
|
|
4297
|
+
|
|
4298
|
+
`formatHeader` 콜백은 **함수**이므로 직렬화 불가:
|
|
4299
|
+
- `grid.getConfig()` 가 반환하는 `GridConfigSnapshot` 에 **포함되지 않음**
|
|
4300
|
+
- `applyConfig(snapshot)` 후에는 핸들러 재등록 필요
|
|
4301
|
+
- 사용자 헤더 서식 데이터(`userHeaderFmt` Map 등)는 wrapper 가 별도 저장 (localStorage 등)
|
|
4302
|
+
|
|
4303
|
+
`columns.update({ headerAttributes })` 로 박은 정적 객체는 `GridConfigSnapshot.fields[*].headerAttributes` 에 직렬화됩니다.
|
|
4304
|
+
|
|
4305
|
+
#### `formatRow` 와의 비교
|
|
4306
|
+
|
|
4307
|
+
| | `formatRow` | `formatHeader` |
|
|
4308
|
+
|---|---|---|
|
|
4309
|
+
| 발화 빈도 | 분당 수천 회 (스크롤마다) | 분당 0~20회 (사용자 액션) |
|
|
4310
|
+
| 발화 트리거 | 스크롤, 데이터 변경, 행 dirty | 사용자 헤더 관련 액션 |
|
|
4311
|
+
| 가상화 대상 | ✓ viewport 내 행만 | ✗ 모든 헤더 셀 (헤더 sticky) |
|
|
4312
|
+
| ctx 행 데이터 | ✓ `data`, `originalData`, `cells[].value` | ✗ 행 데이터 없음 (헤더 메타만) |
|
|
4313
|
+
| ctx type 분기 | data/group-row/subtotal/grand-total 4종 | 1종 (flat) |
|
|
4314
|
+
| 직렬화 | ✗ | ✗ |
|
|
4315
|
+
| Render hook 패턴 | RenderHookManager | RenderHookManager (동일 클래스) |
|
|
4316
|
+
|
|
4317
|
+
라이프사이클이 다르니 분리된 hook 으로 노출. 같은 일을 두 곳에서 하지 마세요.
|
|
4318
|
+
|
|
4319
|
+
---
|
|
4320
|
+
|
|
4321
|
+
### RowHeaderConfig
|
|
4322
|
+
|
|
4323
|
+
좌측 **행 헤더 컬럼**(`_rowHeader`)의 설정. `config.rowHeader`에 `boolean` 대신 객체를 주면
|
|
4324
|
+
너비·본문 셀·헤더(코너) 셀을 커스터마이즈합니다. 행 헤더는 일급 컬럼(`ROW_HEADER_COLUMN_ID = '_rowHeader'`)이며
|
|
4325
|
+
본문 셀은 `formatRow`의 `data` 행 `cells['_rowHeader']`, 코너(좌상단) 셀은 `formatHeader`의 `cells['_rowHeader']`로 접근합니다.
|
|
4326
|
+
|
|
4327
|
+
```typescript
|
|
4328
|
+
interface RowHeaderConfig {
|
|
4329
|
+
/** 행 헤더 컬럼 너비(px). @default 40 */
|
|
4330
|
+
width?: number;
|
|
4331
|
+
/**
|
|
4332
|
+
* 본문 행 헤더 셀 렌더러. 일반 컬럼 cellRenderer와 동일 시그니처(Simple | Lifecycle).
|
|
4333
|
+
* params.selected 로 해당 행 선택 상태 스냅샷을 받아 체크박스 등을 그릴 수 있다
|
|
4334
|
+
* (선택 "변경" 반영은 CSS `.ps-selected` / `repaintRows` 중 하나로 — CellRendererParams.selected JSDoc 참조).
|
|
4335
|
+
* 실제 <input>을 그린다면 `ps-row-header-checkbox-input` 클래스를 붙이면 built-in 선택 동기화가
|
|
4336
|
+
* checked를 자동 갱신한다. rowSelection.trigger 'checkbox' 활성 시 이 renderer가 built-in 체크박스를 대체한다.
|
|
4337
|
+
*/
|
|
4338
|
+
cellRenderer?: CellRenderer;
|
|
4339
|
+
/** 코너(좌상단) 헤더 셀 렌더러. 반환은 HTMLElement 또는 문자열 */
|
|
4340
|
+
headerRenderer?: (params: { columnId: string; header: string }) => HTMLElement | string;
|
|
4341
|
+
}
|
|
4342
|
+
```
|
|
4343
|
+
|
|
4344
|
+
**셀 접근 헬퍼** (import 가능 — `ROW_HEADER_COLUMN_ID`, `getRowHeaderCell`, `getCornerCell`)
|
|
4345
|
+
|
|
4346
|
+
| 헬퍼 | 시그니처 | 설명 |
|
|
4347
|
+
|------|----------|------|
|
|
4348
|
+
| `ROW_HEADER_COLUMN_ID` | `'_rowHeader'` | 행 헤더 컬럼 ID 상수 |
|
|
4349
|
+
| `getRowHeaderCell(ctx)` | `(DataRowContext) => CellInfo \| undefined` | `formatRow`의 data 행 컨텍스트에서 행 헤더 본문 셀 |
|
|
4350
|
+
| `getCornerCell(ctx)` | `(FormatHeaderContext) => HeaderCellInfo \| undefined` | `formatHeader` 컨텍스트에서 좌상단 코너 셀 |
|
|
4351
|
+
|
|
4352
|
+
```js
|
|
4353
|
+
import { getRowHeaderCell, getCornerCell } from '@vmscloud/moz-ui-grid-core';
|
|
4354
|
+
|
|
4355
|
+
// 본문: 행마다 상태 배지 그리기
|
|
4356
|
+
grid.formatRow.addHandler('rh', (info) => {
|
|
4357
|
+
if (info.type !== 'data') return;
|
|
4358
|
+
const cell = getRowHeaderCell(info.ctx);
|
|
4359
|
+
if (cell) cell.element.dataset.state = info.ctx.rowState;
|
|
4360
|
+
});
|
|
4361
|
+
// 코너: 좌상단에 전체선택 체크박스 등
|
|
4362
|
+
grid.formatHeader.addHandler('corner', (ctx) => {
|
|
4363
|
+
const corner = getCornerCell(ctx);
|
|
4364
|
+
if (corner) corner.element.classList.add('my-corner');
|
|
4365
|
+
});
|
|
4366
|
+
```
|
|
4367
|
+
|
|
4368
|
+
> 참고: `cellRenderer`를 주면 커스텀 렌더가 그려집니다(예: 체크박스 컬럼 대체). `rowHeader: true`(구 `showRowHeader: true`)는 **빈 셀**(너비 40, 체크박스 트리거 시 체크박스)만 그리는 축약형입니다 — 행 번호를 렌더하지 않습니다.
|
|
4369
|
+
|
|
4370
|
+
---
|
|
4371
|
+
|
|
4372
|
+
### 셀 속성 — CellAttributes / CellAttributesFn
|
|
4373
|
+
|
|
4374
|
+
`FieldDef.cellAttributes`, `FieldDef.headerAttributes`에 사용.
|
|
4375
|
+
|
|
4376
|
+
```typescript
|
|
4377
|
+
/** HTML 속성 맵 (정적) */
|
|
4378
|
+
type CellAttributes = Record<string, string>;
|
|
4379
|
+
|
|
4380
|
+
/** 동적 셀 속성 콜백 — undefined 반환 시 속성 미적용 */
|
|
4381
|
+
type CellAttributesFn = (params: CellAttributeParams) => CellAttributes | undefined;
|
|
4382
|
+
|
|
4383
|
+
interface CellAttributeParams {
|
|
4384
|
+
value: CellValue;
|
|
4385
|
+
rowData: Row;
|
|
4386
|
+
rowIndex: number;
|
|
4387
|
+
columnId: string;
|
|
4388
|
+
rowId?: string | number;
|
|
4389
|
+
}
|
|
4390
|
+
```
|
|
4391
|
+
|
|
4392
|
+
```typescript
|
|
4393
|
+
// 정적 속성
|
|
4394
|
+
{ id: 'name', cellAttributes: { class: 'cell-name', 'data-priority': 'high' } }
|
|
4395
|
+
|
|
4396
|
+
// 동적 속성
|
|
4397
|
+
{
|
|
4398
|
+
id: 'amount',
|
|
4399
|
+
cellAttributes: ({ value }) => ({
|
|
4400
|
+
style: Number(value) < 0 ? 'color: red' : 'color: blue',
|
|
4401
|
+
class: 'amount-cell',
|
|
4402
|
+
}),
|
|
4403
|
+
}
|
|
4404
|
+
```
|
|
4405
|
+
|
|
4406
|
+
---
|
|
4407
|
+
|
|
4408
|
+
### DataLoader 클래스 — 워커 기반 커스텀 데이터 로딩
|
|
4409
|
+
|
|
4410
|
+
`LoaderConfig.module`이 default export하는 클래스의 베이스. 워커 안에서 실행되므로 DOM/localStorage 접근은 불가하고, `fetch`/`WebSocket`/`XMLHttpRequest` 등 네트워크 API는 사용 가능.
|
|
4411
|
+
|
|
4412
|
+
```typescript
|
|
4413
|
+
abstract class DataLoader {
|
|
4414
|
+
/** 메인스레드에서 전달받은 파라미터 (this.params로 접근) */
|
|
4415
|
+
protected params: Record<string, unknown>;
|
|
4416
|
+
|
|
4417
|
+
/** 라이브러리 내부에서 호출 — grid.updateLoaderParams() 시 자동 동기화 (Object.assign 머지) */
|
|
4418
|
+
updateParams(newParams: Record<string, unknown>): void;
|
|
4419
|
+
|
|
4420
|
+
/** [필수] 초기 데이터 로드. 플랫/트리 공통 */
|
|
4421
|
+
abstract fetchData(): Promise<Row[]>;
|
|
4422
|
+
|
|
4423
|
+
/** [선택] 트리 자식 노드 lazy load (TreeConfig.lazyLoad=true 시 호출) */
|
|
4424
|
+
fetchChildren?(nodeId: string | number): Promise<Row[]>;
|
|
4425
|
+
}
|
|
4426
|
+
```
|
|
4427
|
+
|
|
4428
|
+
```typescript
|
|
4429
|
+
// my-loader.ts (별도 ES 모듈)
|
|
4430
|
+
import { DataLoader } from '@vmscloud/moz-ui-grid-core';
|
|
4431
|
+
import type { Row } from '@vmscloud/moz-ui-grid-core';
|
|
4432
|
+
|
|
4433
|
+
export default class MyLoader extends DataLoader {
|
|
4434
|
+
async fetchData(): Promise<Row[]> {
|
|
4435
|
+
const res = await fetch('https://api.example.com/data', {
|
|
4436
|
+
headers: { Authorization: `Bearer ${this.params.authToken}` },
|
|
4437
|
+
});
|
|
4438
|
+
return res.json();
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
async fetchChildren(nodeId: string | number): Promise<Row[]> {
|
|
4442
|
+
const res = await fetch(`/api/tree/${nodeId}/children`);
|
|
4443
|
+
return res.json();
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
// 그리드 생성
|
|
4448
|
+
new PureSheet(container, {
|
|
4449
|
+
loader: {
|
|
4450
|
+
module: new URL('./my-loader.ts', import.meta.url),
|
|
4451
|
+
params: { authToken: getToken() },
|
|
4452
|
+
},
|
|
4453
|
+
fields: [...],
|
|
4454
|
+
});
|
|
4455
|
+
|
|
4456
|
+
// 런타임 파라미터 갱신 (예: 토큰 리프레시)
|
|
4457
|
+
await grid.updateLoaderParams({ authToken: newToken });
|
|
4458
|
+
await grid.refreshLoaderData();
|
|
4459
|
+
```
|
|
4460
|
+
|
|
4461
|
+
> **`InlineDataLoader`**: 동일 모듈 내 인라인 클래스를 사용하고 싶을 때(URL이 아닌 클래스 자체 전달) 내부적으로 사용되는 변형. 일반 사용 시에는 `LoaderConfig.module` URL 패턴 권장.
|
|
4462
|
+
|
|
4463
|
+
---
|
|
4464
|
+
|
|
4465
|
+
### UndoStack 클래스 — 공유 Undo/Redo
|
|
4466
|
+
|
|
4467
|
+
`PureSheetConfig.undoStack`으로 외부 인스턴스를 주입할 때 사용. 여러 그리드가 하나의 스택을 공유 가능.
|
|
4468
|
+
|
|
4469
|
+
```typescript
|
|
4470
|
+
class UndoStack {
|
|
4471
|
+
constructor(options?: { maxSize?: number }); // @default 100
|
|
4472
|
+
|
|
4473
|
+
// ── Undo/Redo ──
|
|
4474
|
+
undo(): boolean;
|
|
4475
|
+
redo(): boolean;
|
|
4476
|
+
readonly canUndo: boolean;
|
|
4477
|
+
readonly canRedo: boolean;
|
|
4478
|
+
readonly undoCount: number;
|
|
4479
|
+
readonly redoCount: number;
|
|
4480
|
+
peekUndo(): Command | undefined;
|
|
4481
|
+
peekRedo(): Command | undefined;
|
|
4482
|
+
clear(): void;
|
|
4483
|
+
|
|
4484
|
+
// ── Batch 모드 ──
|
|
4485
|
+
beginBatch(description?: string, options?: { skipUndo?: boolean }): void;
|
|
4486
|
+
endBatch(options?: { silent?: boolean }): void;
|
|
4487
|
+
readonly isBatching: boolean;
|
|
4488
|
+
|
|
4489
|
+
// ── 키보드 바인딩 ──
|
|
4490
|
+
/** 컨테이너에 Ctrl+Z/Y 바인딩 (편집 중인 input/textarea/contenteditable에선 무시) */
|
|
4491
|
+
attachKeyboard(container: HTMLElement): void;
|
|
4492
|
+
detachKeyboard(): void;
|
|
4493
|
+
|
|
4494
|
+
// ── 라이프사이클 ──
|
|
4495
|
+
destroy(): void;
|
|
4496
|
+
|
|
4497
|
+
// ── 이벤트 구독 ──
|
|
4498
|
+
on(event: 'push' | 'undo' | 'redo' | 'clear' | 'stateChange' | 'batchEnd', handler: Function): Unsubscribe;
|
|
4499
|
+
subscribe(handlers: Partial<UndoStackEvents>): Unsubscribe;
|
|
4500
|
+
}
|
|
4501
|
+
```
|
|
4502
|
+
|
|
4503
|
+
```typescript
|
|
4504
|
+
// 여러 그리드 간 공유
|
|
4505
|
+
const sharedUndo = new UndoStack({ maxSize: 200 });
|
|
4506
|
+
sharedUndo.attachKeyboard(document.getElementById('page'));
|
|
4507
|
+
|
|
4508
|
+
const grid1 = new PureSheet(el1, { undoStack: sharedUndo, fields, data });
|
|
4509
|
+
const grid2 = new PureSheet(el2, { undoStack: sharedUndo, fields, data });
|
|
4510
|
+
|
|
4511
|
+
// 한 곳에서 Ctrl+Z → 어느 그리드든 마지막 작업 복원
|
|
4512
|
+
// 두 그리드에서 일어난 변경이 동일 시간순으로 스택에 쌓임
|
|
4513
|
+
```
|
|
4514
|
+
|
|
4515
|
+
> 외부 주입 시 그리드의 개별 Ctrl+Z/Y는 비활성됨 — 공유 스택의 `attachKeyboard`로 일원화 권장. 공유 emitter 안전 규칙은 [docs/RULES.md](docs/RULES.md) 참조.
|
|
4516
|
+
|
|
4517
|
+
---
|
|
4518
|
+
|
|
4519
|
+
### Row 클래스 정적 팩토리
|
|
4520
|
+
|
|
4521
|
+
`new Row(config)`로 인스턴스를 만들 수 있지만, 흔한 패턴은 정적 팩토리 메서드입니다.
|
|
4522
|
+
|
|
4523
|
+
```typescript
|
|
4524
|
+
class Row {
|
|
4525
|
+
constructor(config: RowConfig);
|
|
4526
|
+
|
|
4527
|
+
/** 집계 행 생성 (variant: 'subtotal' | 'grandtotal'). 데이터 변경 시 자동 재계산 */
|
|
4528
|
+
static createAggregateRow(
|
|
4529
|
+
aggregates: AggregateConfig[],
|
|
4530
|
+
options?: {
|
|
4531
|
+
id?: string;
|
|
4532
|
+
pinned?: 'top' | 'bottom'; // @default 'bottom'
|
|
4533
|
+
variant?: 'subtotal' | 'grandtotal'; // @default 'subtotal'
|
|
4534
|
+
height?: number;
|
|
4535
|
+
className?: string;
|
|
4536
|
+
labelColumn?: string; // 라벨 표시 컬럼
|
|
4537
|
+
label?: string; // @default '소계' | '총합계'
|
|
4538
|
+
}
|
|
4539
|
+
): Row;
|
|
4540
|
+
|
|
4541
|
+
/** 총합계 행 단축 (variant='grandtotal' 고정) */
|
|
4542
|
+
static createGrandTotalRow(aggregates: AggregateConfig[], options?: …): Row;
|
|
4543
|
+
|
|
4544
|
+
/** 소계 행 단축 (variant='subtotal' 고정) */
|
|
4545
|
+
static createSubtotalRow(aggregates: AggregateConfig[], options?: …): Row;
|
|
4546
|
+
|
|
4547
|
+
// 인스턴스 속성
|
|
4548
|
+
readonly id: string;
|
|
4549
|
+
readonly variant: RowVariant;
|
|
4550
|
+
readonly structural: boolean;
|
|
4551
|
+
pinned: 'top' | 'bottom' | null;
|
|
4552
|
+
data: Record<string, unknown>;
|
|
4553
|
+
height: number | null;
|
|
4554
|
+
// ... (그 외 내부 메서드는 BodyRenderer가 호출)
|
|
4555
|
+
}
|
|
4556
|
+
```
|
|
4557
|
+
|
|
4558
|
+
```typescript
|
|
4559
|
+
import { Row, PureSheet } from '@vmscloud/moz-ui-grid-core';
|
|
4560
|
+
|
|
4561
|
+
const totalRow = Row.createGrandTotalRow([
|
|
4562
|
+
{ columnId: 'salary', formatter: v => '$' + (v?.toLocaleString() ?? '0') },
|
|
4563
|
+
{ columnId: 'bonus' }, // FieldDef.aggregate 사용
|
|
4564
|
+
{ columnId: 'performance', func: 'avg' },
|
|
4565
|
+
], { pinned: 'bottom', labelColumn: 'name', label: '총합계' });
|
|
4566
|
+
|
|
4567
|
+
grid.rows.addPinnedBottom(totalRow);
|
|
4568
|
+
|
|
4569
|
+
// 또는 `grid.rows.addGrandTotal(aggregates)` 단축 API 사용 시 위 코드가 내부에서 호출됨
|
|
4570
|
+
```
|
|
4571
|
+
|
|
4572
|
+
---
|
|
4573
|
+
|
|
4574
|
+
### 에러 코드 전체 레퍼런스
|
|
4575
|
+
|
|
4576
|
+
`psError(code)` / `psWarn(code)`로 발생하는 에러/경고. 자동완성 지원을 위한 상수는 `PS` 열거형.
|
|
4577
|
+
|
|
4578
|
+
```typescript
|
|
4579
|
+
import { PS, psError, psWarn, ErrorMessages } from '@vmscloud/moz-ui-grid-core';
|
|
4580
|
+
```
|
|
4581
|
+
|
|
4582
|
+
| 코드 | PS 키 | 의미 |
|
|
4583
|
+
|---|---|---|
|
|
4584
|
+
| **파라미터 검증 (PS001~)** | | |
|
|
4585
|
+
| `PS001` | `PS.REQUIRED` | 필수 값 누락 |
|
|
4586
|
+
| `PS002` | `PS.INVALID_TYPE` | 타입 불일치 |
|
|
4587
|
+
| `PS003` | `PS.EMPTY_FIELD` | 빈 문자열 거부 |
|
|
4588
|
+
| `PS004` | `PS.NON_EMPTY_ARRAY` | 비어있지 않은 배열 요구 |
|
|
4589
|
+
| `PS005` | `PS.INVALID_ENUM` | 허용 값 외 |
|
|
4590
|
+
| `PS006` | `PS.NON_NEGATIVE_INT` | 0 이상 정수 요구 |
|
|
4591
|
+
| `PS007` | `PS.POSITIVE_NUMBER` | 양수 요구 |
|
|
4592
|
+
| **데이터 (PS101~)** | | |
|
|
4593
|
+
| `PS101` | `PS.DATA_NOT_ARRAY` | data가 배열이 아님 |
|
|
4594
|
+
| `PS102` | `PS.ROW_NOT_OBJECT` | row가 객체가 아님 |
|
|
4595
|
+
| `PS103` | `PS.ROWID_REQUIRED` | rowId 필수 |
|
|
4596
|
+
| `PS104` | `PS.UPDATES_NOT_OBJECT` | updates가 객체가 아님 |
|
|
4597
|
+
| **피벗 (PS201~)** | | |
|
|
4598
|
+
| `PS201` | `PS.PIVOT_ROW_FIELDS` | rowFields 빈 배열 |
|
|
4599
|
+
| `PS202` | `PS.PIVOT_VALUE_FIELDS` | valueFields 빈 배열 |
|
|
4600
|
+
| **설정/충돌 (PS301~)** | | |
|
|
4601
|
+
| `PS301` | `PS.CONFIG_CONFLICT` | columnGroups + rowTemplate 충돌 |
|
|
4602
|
+
| `PS302~306` | `PS.SORTS_NOT_ARRAY` 등 | sorts/filters/rowIds/columnId/columns 검증 |
|
|
4603
|
+
| `PS307` | `PS.NO_COLUMN_GROUPS` | moveGroup인데 columnGroups 없음 |
|
|
4604
|
+
| `PS308` | `PS.SETORDER_NEEDS_GROUPS` | setOrder에 그룹 ID 누락 |
|
|
4605
|
+
| `PS309` | `PS.PIVOT_CONFIG_NULL` | pivotConfig 없음 |
|
|
4606
|
+
| `PS310` | `PS.SINGLE_EXPAND_ALL` | singleExpand에서 expandAll 호출 |
|
|
4607
|
+
| `PS311` | `PS.INVALID_LEVEL` | level < 0 |
|
|
4608
|
+
| **Worker (PS501~)** | | |
|
|
4609
|
+
| `PS501` | `PS.WORKER_SYNC_API` | Worker에서 getAllDataSync 호출 |
|
|
4610
|
+
| `PS502` | `PS.WORKER_DATA_DIRECT` | Worker 모드에서 `data` 직접 전달 불가 — `loader`를 사용할 것 |
|
|
4611
|
+
| **행 ID / 캐시 (PS601~)** | | |
|
|
4612
|
+
| `PS601` | `PS.ROW_ID_REQUIRED` | keyFields 미지정 + id 필드 없음 |
|
|
4613
|
+
| `PS602` | `PS.REFER_CACHE_REQUIRED` | 행 객체 API에 enableReferCache 필요 |
|
|
4614
|
+
| `PS603` | `PS.KEY_CONFLICT_COMMITTED` | 커밋된 데이터와 키 중복 |
|
|
4615
|
+
| `PS606` | `PS.KEY_LOAD_DUPLICATES` | loadData 중복 키 |
|
|
4616
|
+
| **IO (PS700~)** | | |
|
|
4617
|
+
| `PS700~715` | `PS.IO_*` / `PS.COMMIT_INVALID_CELLS` | export/import/commit 가드 (→ [grid.io 에러 코드 표](#에러-코드)) |
|
|
4618
|
+
|
|
4619
|
+
```typescript
|
|
4620
|
+
// 자체 코드에서 PS 발화
|
|
4621
|
+
import { psError, psWarn, PS } from '@vmscloud/moz-ui-grid-core';
|
|
4622
|
+
|
|
4623
|
+
if (!rowId) {
|
|
4624
|
+
psError(PS.REQUIRED, 'myUpdate', { name: 'rowId' });
|
|
4625
|
+
// throws Error('[PureSheet] PS001 myUpdate: \'rowId\'은(는) 필수 값입니다.')
|
|
4626
|
+
}
|
|
4627
|
+
```
|
|
4628
|
+
|
|
4629
|
+
---
|
|
4630
|
+
|
|
4631
|
+
### 기타 export 항목
|
|
4632
|
+
|
|
4633
|
+
| Export | 위치 | 용도 |
|
|
4634
|
+
|---|---|---|
|
|
4635
|
+
| `createReactiveRow(deps, rowId)` | `@vmscloud/moz-ui-grid-core` | `ReactiveRow` 직접 생성 (래퍼/테스트용). 보통 `grid.rows.observeById(id)` 사용 권장 |
|
|
4636
|
+
| `InlineDataLoader` | `@vmscloud/moz-ui-grid-core` | 클래스 자체를 인라인 전달하는 변형 (URL 모듈 대신). 일반적으론 `LoaderConfig.module` 권장 |
|
|
4637
|
+
| `fieldToColumn(fieldDef)` | `@vmscloud/moz-ui-grid-core` | `FieldDef` → 내부 `ColumnDef` 변환. 외부 컬럼 메타 생성용 |
|
|
4638
|
+
| `configToInternalOptions(config)` | `@vmscloud/moz-ui-grid-core` | 외부 `PureSheetConfig` → 내부 옵션 객체 변환 |
|
|
4639
|
+
| `getGridMode(config)` | `@vmscloud/moz-ui-grid-core` | `'flat' \| 'pivot'` 판별 |
|
|
4640
|
+
| `getPivotConfig(config)` | `@vmscloud/moz-ui-grid-core` | `PivotConfig \| null` 추출 |
|
|
4641
|
+
| `RenderHookManager` | `@vmscloud/moz-ui-grid-core` | `grid.formatRow`이 반환하는 핸들러 매니저 클래스 (직접 사용 드뭄) |
|
|
4642
|
+
| `BatchCommand` | `@vmscloud/moz-ui-grid-core` | UndoStack 배치용 Command. 보통 `undoStack.batch()` 사용 권장 |
|
|
4643
|
+
| `MergedRange`, `CellMergeInfo` | `@vmscloud/moz-ui-grid-core` | 셀 병합 결과 메타 타입. `__merge` sidecar 필드 형태 |
|
|
4644
|
+
| `ClipboardCopyPayload`, `ClipboardPastePayload` | `@vmscloud/moz-ui-grid-core` | clipboard 이벤트 페이로드 타입 |
|
|
4645
|
+
| `TranslationMap`, `TranslationKey`, `PartialTranslationMap` | `@vmscloud/moz-ui-grid-core` | i18n 번역 키/맵 타입 (전체 키 목록은 `packages/core/src/i18n/translations/` 참조) |
|
|
4646
|
+
| `VirtualScroller`, `GridRenderer`, `BodyRenderer`, `HeaderRenderer`, `RowPool`, `HeaderCell`, `SelectionManager`, `EditorManager`, `ColumnManager`, `ClipboardManager`, `GroupManager`, `TreeManager`, `MasterDetailManager`, `MultiRowRenderer`, `PivotHeaderRenderer` | `@vmscloud/moz-ui-grid-core` | 저수준 UI 클래스. 보통 그리드가 내부에서 자체 인스턴스화 — 직접 인스턴스 생성은 매우 드문 고급 케이스 |
|
|
4647
|
+
| `grid.getDragReorderManager()` | PureSheet 인스턴스 | 저수준 DragReorder 매니저 직접 접근 (escape hatch). 보통 `grid.dragDrop.setConfig()` 사용 권장 |
|
|
4648
|
+
|
|
4649
|
+
---
|
|
4650
|
+
|
|
4651
|
+
## API 네이밍 규칙
|
|
4652
|
+
|
|
4653
|
+
moz-ui-grid-core의 행 조작 API는 3가지 변형(variant) 패턴을 따릅니다:
|
|
4654
|
+
|
|
4655
|
+
| 접미사 | 인자 | 설명 | 요구사항 |
|
|
4656
|
+
|--------|------|------|----------|
|
|
4657
|
+
| (없음) | 행 객체 (`Row`) | 행 객체 참조 기반 | `enableReferCache: true` 필수 |
|
|
4658
|
+
| `ById` | `rowId` (`string \| number`) | rowId 기반 | — |
|
|
4659
|
+
| `ByIndex` | `sourceIndex` (`number`) | 원본 데이터 인덱스 기반 O(1) | — |
|
|
4660
|
+
|
|
4661
|
+
#### 예시
|
|
4662
|
+
|
|
4663
|
+
```typescript
|
|
4664
|
+
// 행 객체 기반 (enableReferCache 필요)
|
|
4665
|
+
const row = await grid.getRowById('row-1');
|
|
4666
|
+
await grid.changes.updateCell(row, 'name', '수정됨');
|
|
4667
|
+
await grid.changes.deleteRow(row);
|
|
4668
|
+
await grid.changes.discard(row);
|
|
4669
|
+
const state = grid.changes.getRowState(row);
|
|
4670
|
+
|
|
4671
|
+
// ById — rowId 기반
|
|
4672
|
+
await grid.changes.updateCellById('row-1', 'name', '수정됨');
|
|
4673
|
+
await grid.changes.deleteRowById('row-1');
|
|
4674
|
+
await grid.changes.discardById('row-1');
|
|
4675
|
+
const state2 = grid.changes.getRowStateById('row-1');
|
|
4676
|
+
|
|
4677
|
+
// ByIndex — sourceIndex O(1)
|
|
4678
|
+
await grid.changes.updateCellByIndex(0, 'name', '수정됨');
|
|
4679
|
+
await grid.changes.deleteRowByIndex(0);
|
|
4680
|
+
await grid.changes.discardByIndex(0);
|
|
4681
|
+
const state3 = grid.changes.getRowStateByIndex(0);
|
|
4682
|
+
```
|
|
4683
|
+
|
|
4684
|
+
> `enableReferCache`를 켜면 내부적으로 행 객체 → sourceIndex 매핑 캐시를 유지합니다. 이 캐시는 `loadData()` 호출 시 자동으로 재구성됩니다.
|
|
4685
|
+
|
|
4686
|
+
> **참고 — ChangesAPI vs RowsAPI 선택 행 삭제 이름 차이**: `grid.changes.deleteSelectedRows()` ↔ `grid.rows.deleteSelectedRows()`. 동작은 동일하지만 두 네임스페이스의 컨벤션 차이로 이름이 다릅니다.
|
|
4687
|
+
|
|
4688
|
+
---
|
|
4689
|
+
|
|
4690
|
+
## 사용 예제
|
|
4691
|
+
|
|
4692
|
+
다양한 시나리오의 코드 예제는 별도 파일로 분리되어 있습니다:
|
|
4693
|
+
|
|
4694
|
+
➡ **[examples.md](examples.md)** — 약 30개 시나리오 (기본 그리드, Sub-API별 사용, Reactive React 통합, 이벤트, 선택 모드, 셀 병합, 피벗, Excel I/O 등)
|
|
4695
|
+
|