@vmscloud/moz-ui-grid-react 0.1.19
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 +148 -0
- package/dist/MozGrid.d.ts +136 -0
- package/dist/bridge/ReactRenderer.d.ts +45 -0
- package/dist/bridge/fieldBuilder.d.ts +9 -0
- package/dist/bridge/templateRegistry.d.ts +11 -0
- package/dist/index.cjs +25 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +96086 -0
- package/dist/legacy/components/common/Dragging.d.ts +30 -0
- package/dist/legacy/components/common/custom-cell/FormulaEngine.d.ts +256 -0
- package/dist/legacy/components/common/custom-cell/FormulaInput.d.ts +25 -0
- package/dist/legacy/components/common/custom-cell/GridCustomCell.d.ts +17 -0
- package/dist/legacy/components/common/custom-cell/useFormulaAutocomplete.d.ts +54 -0
- package/dist/legacy/components/common/excel/ClientExcelUploadPopup.d.ts +6 -0
- package/dist/legacy/components/common/filter/GridFilter.d.ts +13 -0
- package/dist/legacy/components/common/footer/GridFooter.d.ts +17 -0
- package/dist/legacy/components/common/sort/GridSort.d.ts +12 -0
- package/dist/legacy/components/common/toolbar/GridSearchInput.d.ts +26 -0
- package/dist/legacy/components/grid/CellEditor.d.ts +9 -0
- package/dist/legacy/components/grid/CellTemplate.d.ts +21 -0
- package/dist/legacy/components/grid/DetailTemplate.d.ts +8 -0
- package/dist/legacy/components/grid/GroupTemplate.d.ts +8 -0
- package/dist/legacy/components/grid/HeaderTemplate.d.ts +9 -0
- package/dist/legacy/components/grid/MozGridReact.d.ts +12 -0
- package/dist/legacy/components/grid/MozGridReactCore.d.ts +28 -0
- package/dist/legacy/components/grid/bulk-editor/GridBulkEditor.d.ts +11 -0
- package/dist/legacy/components/grid/context-menu/GridContextMenu.d.ts +22 -0
- package/dist/legacy/components/grid/group-panel/GroupPanel.d.ts +5 -0
- package/dist/legacy/components/grid/toolbar/ColumnPicker.d.ts +18 -0
- package/dist/legacy/components/grid/toolbar/GridOptionModal.d.ts +30 -0
- package/dist/legacy/components/grid/toolbar/GridSetupPopover.d.ts +25 -0
- package/dist/legacy/components/grid/toolbar/GridToolBox.d.ts +49 -0
- package/dist/legacy/components/pivot/MozPivotReact.d.ts +47 -0
- package/dist/legacy/components/pivot/PivotContextMenu.d.ts +23 -0
- package/dist/legacy/components/pivot/PivotFieldChip.d.ts +17 -0
- package/dist/legacy/components/pivot/PivotFieldDropZone.d.ts +30 -0
- package/dist/legacy/components/pivot/PivotFieldPanel.d.ts +26 -0
- package/dist/legacy/components/pivot/PivotOptionModal.d.ts +36 -0
- package/dist/legacy/components/pivot/PivotToolBox.d.ts +56 -0
- package/dist/legacy/components/pivot/chart/EChart.d.ts +18 -0
- package/dist/legacy/components/pivot/chart/MozPivotChart.d.ts +23 -0
- package/dist/legacy/components/pivot/chart/PivotChartDetail.d.ts +17 -0
- package/dist/legacy/components/pivot/chart/PivotChartMini.d.ts +19 -0
- package/dist/legacy/components/pivot/chart/PivotChartMultiple.d.ts +16 -0
- package/dist/legacy/components/pivot/chart/PivotChartToolbar.d.ts +32 -0
- package/dist/legacy/components/pivot/chart/chartUtils.d.ts +45 -0
- package/dist/legacy/contexts/index.d.ts +31 -0
- package/dist/legacy/contracts/notification.contract.d.ts +34 -0
- package/dist/legacy/hooks/index.d.ts +16 -0
- package/dist/legacy/hooks/useCellTooltip.d.ts +8 -0
- package/dist/legacy/hooks/useClientExcelExport.d.ts +27 -0
- package/dist/legacy/hooks/useClientExcelStore.d.ts +18 -0
- package/dist/legacy/hooks/useContextMenu.d.ts +62 -0
- package/dist/legacy/hooks/useGridBulkEditor.d.ts +16 -0
- package/dist/legacy/hooks/useGridChanges.d.ts +10 -0
- package/dist/legacy/hooks/useGridColumns.d.ts +11 -0
- package/dist/legacy/hooks/useGridCustomCell.d.ts +32 -0
- package/dist/legacy/hooks/useGridCustomCellModule.d.ts +9 -0
- package/dist/legacy/hooks/useGridEvents.d.ts +13 -0
- package/dist/legacy/hooks/useGridFilter.d.ts +19 -0
- package/dist/legacy/hooks/useGridInstance.d.ts +9 -0
- package/dist/legacy/hooks/useGridSelect.d.ts +10 -0
- package/dist/legacy/hooks/useGridSelection.d.ts +6 -0
- package/dist/legacy/hooks/useGridSort.d.ts +16 -0
- package/dist/legacy/hooks/useGridUndoRedo.d.ts +10 -0
- package/dist/legacy/hooks/useGridValidation.d.ts +9 -0
- package/dist/legacy/hooks/useSearchHighlight.d.ts +9 -0
- package/dist/legacy/icons/index.d.ts +36 -0
- package/dist/legacy/index.d.ts +71 -0
- package/dist/legacy/locale/MozConfigProvider.d.ts +31 -0
- package/dist/legacy/locale/contextWrapper.d.ts +19 -0
- package/dist/legacy/locale/en.d.ts +2 -0
- package/dist/legacy/locale/index.d.ts +8 -0
- package/dist/legacy/locale/ja.d.ts +2 -0
- package/dist/legacy/locale/ko.d.ts +2 -0
- package/dist/legacy/locale/types.d.ts +328 -0
- package/dist/legacy/locale/useMozLocale.d.ts +15 -0
- package/dist/legacy/locale/zh.d.ts +2 -0
- package/dist/legacy/stories/MozGridReact.stories.d.ts +7 -0
- package/dist/legacy/stories/MozPivotReact.stories.d.ts +44 -0
- package/dist/legacy/stories/_helpers/sampleData.d.ts +54 -0
- package/dist/legacy/types/pivot-props.d.ts +62 -0
- package/dist/legacy/types/props.d.ts +118 -0
- package/dist/legacy/types/toolbar.d.ts +117 -0
- package/dist/legacy/ui/button/Button.d.ts +18 -0
- package/dist/legacy/ui/button/index.d.ts +1 -0
- package/dist/legacy/ui/checkbox/CheckBox.d.ts +17 -0
- package/dist/legacy/ui/checkbox/icons.d.ts +9 -0
- package/dist/legacy/ui/checkbox/index.d.ts +1 -0
- package/dist/legacy/ui/context-menu/ContextMenu.d.ts +36 -0
- package/dist/legacy/ui/context-menu/index.d.ts +2 -0
- package/dist/legacy/ui/context-menu/types.d.ts +26 -0
- package/dist/legacy/ui/date-input/DateInput.d.ts +25 -0
- package/dist/legacy/ui/date-input/index.d.ts +1 -0
- package/dist/legacy/ui/empty-state/EmptyState.d.ts +28 -0
- package/dist/legacy/ui/empty-state/index.d.ts +1 -0
- package/dist/legacy/ui/input/Input.d.ts +27 -0
- package/dist/legacy/ui/input/index.d.ts +1 -0
- package/dist/legacy/ui/multi-select/MultiSelect.d.ts +43 -0
- package/dist/legacy/ui/multi-select/index.d.ts +1 -0
- package/dist/legacy/ui/number-input/NumberInput.d.ts +22 -0
- package/dist/legacy/ui/number-input/index.d.ts +1 -0
- package/dist/legacy/ui/popup/Popup.d.ts +50 -0
- package/dist/legacy/ui/popup/index.d.ts +1 -0
- package/dist/legacy/ui/select/Select.d.ts +56 -0
- package/dist/legacy/ui/select/index.d.ts +1 -0
- package/dist/legacy/ui/toggle/Toggle.d.ts +21 -0
- package/dist/legacy/ui/toggle/index.d.ts +1 -0
- package/dist/legacy/utils/cn.d.ts +5 -0
- package/dist/legacy/utils/file.d.ts +17 -0
- package/dist/legacy/utils/index.d.ts +2 -0
- package/dist/legacy/utils/misc.d.ts +17 -0
- package/dist/stories/MozGrid.stories.d.ts +18 -0
- package/dist/style.css +1 -0
- package/dist/types/slots.d.ts +49 -0
- package/docs/manual/examples.md +2385 -0
- package/docs/manual/reference.md +4695 -0
- package/docs/manual/reference_react.md +720 -0
- package/package.json +104 -0
|
@@ -0,0 +1,2385 @@
|
|
|
1
|
+
# moz-ui-grid-core 사용 예제
|
|
2
|
+
|
|
3
|
+
> 본 문서는 [API 레퍼런스(reference.md)](reference.md)의 사용 예제 모음입니다. 타입·시그니처·옵션 표는 [reference.md](reference.md)에서 확인하세요.
|
|
4
|
+
>
|
|
5
|
+
> 패키지: `@vmscloud/moz-ui-grid-core` (클래스명은 호환을 위해 `PureSheet` 유지)
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [기본 플랫 그리드](#기본-플랫-그리드)
|
|
10
|
+
- [grid.rows 사용 예제](#gridrows-사용-예제)
|
|
11
|
+
- [grid.columns 사용 예제](#gridcolumns-사용-예제)
|
|
12
|
+
- [grid.changes 사용 예제](#gridchanges-사용-예제)
|
|
13
|
+
- [grid.undoManager 사용 예제](#gridundomanager-사용-예제)
|
|
14
|
+
- [grid.rowGroup 사용 예제](#gridrowgroup-사용-예제)
|
|
15
|
+
- [grid.tree 사용 예제](#gridtree-사용-예제)
|
|
16
|
+
- [트리 드래그 reorder 사용 예제](#트리-드래그-reorder-사용-예제)
|
|
17
|
+
- [grid.pivot 사용 예제](#gridpivot-사용-예제)
|
|
18
|
+
- [grid.masterDetail 사용 예제](#gridmasterdetail-사용-예제)
|
|
19
|
+
- [grid.validation 사용 예제](#gridvalidation-사용-예제)
|
|
20
|
+
- [grid.clipboard 사용 예제](#gridclipboard-사용-예제)
|
|
21
|
+
- [grid.headers 사용 예제](#gridheaders-사용-예제)
|
|
22
|
+
- [grid.io 사용 예제](#gridio-사용-예제)
|
|
23
|
+
- [Reactive API 전체 사용 예제](#reactive-api-전체-사용-예제)
|
|
24
|
+
- [이벤트 전체 사용 예제](#이벤트-전체-사용-예제)
|
|
25
|
+
- [정렬/필터 사용 예제](#정렬필터-사용-예제)
|
|
26
|
+
- [필터 그룹 사용 예제](#필터-그룹-사용-예제)
|
|
27
|
+
- [값 목록 필터 사용 예제 (엑셀식 체크박스)](#값-목록-필터-사용-예제-엑셀식-체크박스)
|
|
28
|
+
- [헤더 아이콘 이벤트 사용 예제](#헤더-아이콘-이벤트-사용-예제)
|
|
29
|
+
- [저장본 마이그레이션 사용 예제](#저장본-마이그레이션-사용-예제)
|
|
30
|
+
- [선택 모드 사용 예제](#선택-모드-사용-예제)
|
|
31
|
+
- [셀 병합 사용 예제](#셀-병합-사용-예제)
|
|
32
|
+
- [동적 행 높이 사용 예제](#동적-행-높이-사용-예제)
|
|
33
|
+
- [데이터 조회 사용 예제](#데이터-조회-사용-예제)
|
|
34
|
+
- [스크롤/네비게이션 사용 예제](#스크롤네비게이션-사용-예제)
|
|
35
|
+
- [국제화 사용 예제](#국제화-사용-예제)
|
|
36
|
+
- [포맷팅 사용 예제](#포맷팅-사용-예제)
|
|
37
|
+
- [헤더/상태바 사용 예제](#헤더상태바-사용-예제)
|
|
38
|
+
- [배치 작업 사용 예제](#배치-작업-사용-예제)
|
|
39
|
+
- [데이터 내보내기 사용 예제](#데이터-내보내기-사용-예제)
|
|
40
|
+
- [스트리밍/로더 사용 예제](#스트리밍로더-사용-예제)
|
|
41
|
+
- [성능 조회 사용 예제](#성능-조회-사용-예제)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### 기본 플랫 그리드
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const grid = new PureSheet(container, {
|
|
49
|
+
mode: 'flat',
|
|
50
|
+
fields: [
|
|
51
|
+
// id = 컬럼 **식별자**(가리킬 때: 정렬/필터 상태, 폭, 저장 레이아웃).
|
|
52
|
+
// field = 원본 데이터 **키**(읽을 때: row[field]). 생략하면 id 와 같은 값으로 정규화.
|
|
53
|
+
{ id: 'name', header: '이름', dataType: 'string', width: 200 },
|
|
54
|
+
{ id: 'age', header: '나이', dataType: 'number', width: 100, align: 'right' },
|
|
55
|
+
{ id: 'email', header: '이메일', dataType: 'string', flex: 1 },
|
|
56
|
+
|
|
57
|
+
// 같은 원본 컬럼을 두 번 배치 — id 로 구분한다 (ITSM-2026-002188)
|
|
58
|
+
{ id: 'dept_raw', field: 'dept', header: '부서(원본)', dataType: 'string' },
|
|
59
|
+
{ id: 'dept_short', field: 'dept', header: '부서(약칭)', dataType: 'string' },
|
|
60
|
+
],
|
|
61
|
+
data: employees,
|
|
62
|
+
rowSelection: { mode: 'multi' },
|
|
63
|
+
editable: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 초기화 완료 대기
|
|
67
|
+
await grid.waitForReady();
|
|
68
|
+
|
|
69
|
+
// 데이터 로드
|
|
70
|
+
await grid.loadData(newData);
|
|
71
|
+
|
|
72
|
+
// 새로고침
|
|
73
|
+
await grid.refresh();
|
|
74
|
+
|
|
75
|
+
// 리소스 해제
|
|
76
|
+
grid.destroy();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### grid.rows 사용 예제
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// ── 행 조회 ──
|
|
85
|
+
const row = grid.rows.getById('row-1'); // ID로 단일 행
|
|
86
|
+
const rowByIdx = grid.rows.getByIndex(0); // sourceIndex로 단일 행 O(1)
|
|
87
|
+
const allRows = grid.rows.all(); // 전체 행
|
|
88
|
+
const filtered = grid.rows.where( // 조건부
|
|
89
|
+
(data, state) => data.age > 30 && state !== 'deleted'
|
|
90
|
+
);
|
|
91
|
+
const batch = grid.rows.getByIds([1, 2, 3]); // ID 목록
|
|
92
|
+
const byIndices = grid.rows.getByIndices([0, 1, 2]); // sourceIndex 목록
|
|
93
|
+
|
|
94
|
+
// ── 반응형 ──
|
|
95
|
+
const reactiveRow = grid.rows.observeById('row-1'); // ID 기반
|
|
96
|
+
const reactiveByIdx = grid.rows.observeByIndex(0); // sourceIndex 기반
|
|
97
|
+
|
|
98
|
+
// ── 선택 (행 쓰기) ──
|
|
99
|
+
grid.rows.selectRowsByIds([1, 2, 3]); // rowId 배열로 행 선택
|
|
100
|
+
grid.rows.selectRowsByViewIndices([0, 1, 2]); // viewIndex(화면 물리 순서) 배열로 — 정렬/필터 정확
|
|
101
|
+
grid.rows.selectRowRangeByViewIndex(2, 6); // 화면상 N~M번째 범위 선택
|
|
102
|
+
grid.rows.selectRowRangeByIds('r2', 'r7'); // rowId 범위 선택(정렬/필터 안정)
|
|
103
|
+
grid.rows.selectAllRows(); // 전체 선택
|
|
104
|
+
grid.rows.clearRowSelection(); // 행 선택 해제
|
|
105
|
+
|
|
106
|
+
// ── 선택 (셀 쓰기) ──
|
|
107
|
+
grid.cells.selectCellsByIds([{ rowId: 5, columnId: 'name' }]); // rowId+열
|
|
108
|
+
grid.cells.selectCellsByViewIndices([{ viewIndex: 3, columnId: 'name' }]); // viewIndex+열
|
|
109
|
+
grid.cells.selectCellRangeByViewIndex( // 셀 사각형 범위 (viewIndex)
|
|
110
|
+
{ viewIndex: 2, columnId: 'name' },
|
|
111
|
+
{ viewIndex: 6, columnId: 'salary' },
|
|
112
|
+
);
|
|
113
|
+
grid.cells.selectCellRangeByIds( // 셀 사각형 범위 (rowId, 정렬/필터 안정)
|
|
114
|
+
{ rowId: 'r2', columnId: 'name' },
|
|
115
|
+
{ rowId: 'r7', columnId: 'salary' },
|
|
116
|
+
);
|
|
117
|
+
grid.cells.clearCellSelection(); // 셀 선택만 해제
|
|
118
|
+
|
|
119
|
+
// ── 선택 조회 (행 — grid.rows) ──
|
|
120
|
+
const ids = grid.rows.getSelectedRowIds(); // 선택된 행 rowId
|
|
121
|
+
const srcIdx = await grid.rows.getSelectedRowIndices(); // sourceIndex
|
|
122
|
+
const viewIdx = await grid.rows.getSelectedRowViewIndices(); // viewIndex(화면 물리 순서)
|
|
123
|
+
const dispIdx = await grid.rows.getSelectedRowDisplayedIndices(); // displayedIndex(화면 표시 순서)
|
|
124
|
+
|
|
125
|
+
// ── 선택 조회 (셀 — grid.cells) ──
|
|
126
|
+
const cells = grid.cells.getSelectedCellIds(); // [{ rowId, columnId }, ...]
|
|
127
|
+
const cellRowIds = grid.cells.getSelectedCellRowIds(); // 걸친 행 rowId (중복 제거)
|
|
128
|
+
const cellCols = grid.cells.getSelectedCellColumnIds(); // 걸친 컬럼 columnId (중복 제거)
|
|
129
|
+
|
|
130
|
+
// ── 선택 상태 (RowHandle 기반 — grid.selection) ──
|
|
131
|
+
const selRows = grid.selection.getSelectedRows(); // RowHandle[] (viewIndex/sourceIndex 채워짐)
|
|
132
|
+
const activeRow = grid.selection.getActiveRow(); // RowHandle | null (traversal 앵커)
|
|
133
|
+
const selCells = grid.selection.getSelectedCells(); // [{ row: RowHandle, columnId }, ...]
|
|
134
|
+
const activeCell = grid.selection.getActiveCell(); // { row: RowHandle, columnId } | null
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// ── 다음/이전 이동 (traversal) — Drawer 상세 네비게이션 패턴 ──
|
|
138
|
+
// 현재 선택 앵커에서 displayed 순서로 ±1 이동. 접힌 그룹의 자식은 자동 스킵된다.
|
|
139
|
+
async function move(dir: 1 | -1) {
|
|
140
|
+
const anchor = grid.selection.getActiveRow(); // 마지막 상호작용 행
|
|
141
|
+
const cur = anchor ? await grid.rows.getDisplayedIndexById(anchor.id) : -1;
|
|
142
|
+
const total = grid.rows.displayedCount();
|
|
143
|
+
for (let i = cur + dir; i >= 0 && i < total; i += dir) {
|
|
144
|
+
const info = await grid.rows.getByDisplayedIndex(i);
|
|
145
|
+
if (info?.type === 'data' && info.rowId != null) {
|
|
146
|
+
grid.rows.selectRowsByIds([info.rowId]); // 그 행으로 이동·선택
|
|
147
|
+
return info.rowId; // Drawer에 이 rowId의 detail 표시
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null; // 경계 도달
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── 숨은(접힌 그룹 속) 행으로 이동 ──
|
|
154
|
+
// getDisplayedIndexById가 -1이면 조상 그룹을 펼친 뒤 이동
|
|
155
|
+
async function goToRow(rowId: string | number) {
|
|
156
|
+
if (await grid.rows.getDisplayedIndexById(rowId) < 0) {
|
|
157
|
+
await grid.rowGroup.expandToRow(rowId); // 조상 그룹 모두 펼침
|
|
158
|
+
}
|
|
159
|
+
grid.rows.selectRowsByIds([rowId]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
// ── 집계 ──
|
|
164
|
+
const result = grid.rows.selectionAggregates();
|
|
165
|
+
// → {
|
|
166
|
+
// columns: [
|
|
167
|
+
// { columnId: 'salary', header: 'Salary', aggregate: 'sum', value: 325000, formattedValue: '$325,000', count: 5 },
|
|
168
|
+
// ],
|
|
169
|
+
// total: { sum: 325000, avg: 65000, count: 5, totalCount: 5, min: 50000, max: 90000 }
|
|
170
|
+
// }
|
|
171
|
+
|
|
172
|
+
// ── 선택 일괄 ──
|
|
173
|
+
const deletedCount = await grid.rows.deleteSelectedRows();
|
|
174
|
+
const updatedCount = await grid.rows.updateSelectedRows('status', 'approved');
|
|
175
|
+
const discardedCount = await grid.rows.discardSelectedRows();
|
|
176
|
+
|
|
177
|
+
// ── 데이터 행 고정 ──
|
|
178
|
+
grid.rows.pinTopById('row-1'); // ID로 기존 행을 상단 고정
|
|
179
|
+
grid.rows.pinTopByIndex(0); // sourceIndex로 기존 행을 상단 고정
|
|
180
|
+
grid.rows.pinBottomById('row-2'); // ID로 기존 행을 하단 고정
|
|
181
|
+
grid.rows.pinBottomByIndex(1); // sourceIndex로 기존 행을 하단 고정
|
|
182
|
+
|
|
183
|
+
// ── 구조적 행 고정 ──
|
|
184
|
+
const pinnedRow = grid.rows.addPinnedTop({ id: 'summary', label: '요약', amount: 9999 });
|
|
185
|
+
grid.rows.addPinnedBottom({ id: 'footer', label: '합계' });
|
|
186
|
+
|
|
187
|
+
// ── 고정 관리 ──
|
|
188
|
+
grid.rows.unpinById('summary');
|
|
189
|
+
const { top, bottom } = grid.rows.getPinned();
|
|
190
|
+
grid.rows.clearPinned();
|
|
191
|
+
grid.rows.refreshPinned();
|
|
192
|
+
|
|
193
|
+
// ── 구조적 행 ──
|
|
194
|
+
grid.rows.addGrandTotal([
|
|
195
|
+
{ field: 'amount', aggregate: 'sum' },
|
|
196
|
+
{ field: 'quantity', aggregate: 'avg' },
|
|
197
|
+
]);
|
|
198
|
+
grid.rows.addFilter();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### grid.columns 사용 예제
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// ── 고정 ──
|
|
207
|
+
grid.columns.pinLeft('name');
|
|
208
|
+
grid.columns.pinRight('total');
|
|
209
|
+
grid.columns.unpin('name');
|
|
210
|
+
|
|
211
|
+
// ── 너비 ──
|
|
212
|
+
grid.columns.setWidth('name', 300);
|
|
213
|
+
grid.columns.autoFit('name', true);
|
|
214
|
+
grid.columns.autoFitAll(); // 일회성
|
|
215
|
+
grid.columns.autoFitAll(true); // 영구 적용
|
|
216
|
+
|
|
217
|
+
// ── 순서 (일반 헤더) ──
|
|
218
|
+
grid.columns.setOrder(['id', 'name', 'email', 'age']);
|
|
219
|
+
|
|
220
|
+
// ── 순서 (컬럼 그룹) ── 그룹 ID를 포함하여 전달
|
|
221
|
+
grid.columns.setOrder(['personal', 'work', 'age'], {
|
|
222
|
+
leafOrders: { personal: ['name', 'email'] } // 그룹 내 리프 순서 지정 (선택)
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ── 단일 컬럼 이동 ──
|
|
226
|
+
grid.columns.move('email', 'name', 'before'); // email을 name 앞으로
|
|
227
|
+
grid.columns.move('email', 'name', 'after'); // email을 name 뒤로
|
|
228
|
+
// 컬럼 그룹에서 lockToGroup=true이면 같은 그룹 내에서만 이동 가능
|
|
229
|
+
|
|
230
|
+
// ── 그룹 통째로 이동 (같은 레벨 형제 간) ──
|
|
231
|
+
grid.columns.moveGroup('personal', 'work', 'after'); // personal 그룹을 work 뒤로
|
|
232
|
+
|
|
233
|
+
// ── 가시성 ──
|
|
234
|
+
grid.columns.hide('email');
|
|
235
|
+
grid.columns.show('email');
|
|
236
|
+
grid.columns.setHidden('email', true);
|
|
237
|
+
grid.columns.toggle('email');
|
|
238
|
+
|
|
239
|
+
// ── 상태 저장/복원 ──
|
|
240
|
+
const json = grid.columns.save();
|
|
241
|
+
grid.columns.load(json); // JSON에서 복원
|
|
242
|
+
|
|
243
|
+
// ── 조회 (모두 ColumnInfo[] / ColumnInfo — 동일한 키 집합) ──
|
|
244
|
+
// ColumnInfo = 정적 ColumnDef + 런타임 ColumnState 합본
|
|
245
|
+
// header/type/align/mask/cellAttributes/headerAttributes/aggregate ... + width/pinned/visible/order/offset ...
|
|
246
|
+
const all = grid.columns.getAll(); // 전체, 정의 순서
|
|
247
|
+
const map = grid.columns.getMap(); // getAll 의 별칭 (하위호환)
|
|
248
|
+
const states = grid.columns.getState(); // 전체, state 배열 순서
|
|
249
|
+
const visible = grid.columns.getVisibleColumns(); // 보이는 것만, order 순
|
|
250
|
+
const col = grid.columns.getById('name'); // 단건
|
|
251
|
+
|
|
252
|
+
// ── 통합 변경: columns.update(colId, partial, options?) ──
|
|
253
|
+
// FieldDef/ColumnDef 키들을 한 번에 부분 갱신. 변경 키별로 적절한 렌더 자동 발화.
|
|
254
|
+
grid.columns.update('amount', {
|
|
255
|
+
align: 'right',
|
|
256
|
+
header: '금액',
|
|
257
|
+
mask: { type: 'numeric', pattern: '#,##0', prefix: '$' },
|
|
258
|
+
cellAttributes: { class: 'highlight', style: 'background:#fffbe6' },
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// updateSnapshot: 변경분을 초기 스냅샷에도 미러 → 이후 reset 시 이 변경을 포함한 새 baseline 으로 복귀
|
|
262
|
+
grid.columns.update('amount', { header: '판매금액' }, { updateSnapshot: true });
|
|
263
|
+
|
|
264
|
+
// ── 복귀: columns.reset / resetAll ──
|
|
265
|
+
grid.columns.reset('amount'); // 1개 컬럼 — 초기 스냅샷 복귀 (정적 + 런타임 양쪽)
|
|
266
|
+
grid.columns.resetAll(); // 전체 복귀. 컬럼 그룹 없을 때 컬럼 순서도 함께 복원
|
|
267
|
+
|
|
268
|
+
// ── 이벤트 ──
|
|
269
|
+
grid.on('column:update', ({ columnId, changedKeys }) => {
|
|
270
|
+
console.log(`${columnId} 변경:`, changedKeys);
|
|
271
|
+
});
|
|
272
|
+
grid.on('column:reset', ({ columnId }) => {
|
|
273
|
+
// columnId === null 이면 resetAll
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### grid.changes 사용 예제
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// ── CRUD (Dirty State) ──
|
|
283
|
+
const newId = await grid.changes.addRow({ name: '신규', age: 0 });
|
|
284
|
+
await grid.changes.addRow({ name: '삽입', age: 25 }, 3); // 인덱스 3에 삽입
|
|
285
|
+
|
|
286
|
+
// ById 패턴 (rowId 기반)
|
|
287
|
+
await grid.changes.updateCellById('row-1', 'name', '수정됨');
|
|
288
|
+
await grid.changes.deleteRowById('row-1');
|
|
289
|
+
await grid.changes.undeleteRowById('row-1');
|
|
290
|
+
const restoredCount = await grid.changes.undeleteRowsById([1, 2, 3]);
|
|
291
|
+
|
|
292
|
+
// 행 객체 패턴 (enableReferCache 필요)
|
|
293
|
+
const row = await grid.getRowById('row-1');
|
|
294
|
+
await grid.changes.updateCell(row, 'name', '수정됨');
|
|
295
|
+
await grid.changes.deleteRow(row);
|
|
296
|
+
await grid.changes.undeleteRow(row);
|
|
297
|
+
|
|
298
|
+
// ByIndex 패턴 (sourceIndex O(1))
|
|
299
|
+
await grid.changes.updateCellByIndex(0, 'name', '수정됨');
|
|
300
|
+
await grid.changes.deleteRowByIndex(0);
|
|
301
|
+
|
|
302
|
+
// ── 선택 행 삭제 ──
|
|
303
|
+
const count = await grid.changes.deleteSelectedRows();
|
|
304
|
+
// (RowsAPI 쪽은 `grid.rows.deleteSelectedRows()` — 이름이 다름에 주의)
|
|
305
|
+
|
|
306
|
+
// ── 변경 취소 ──
|
|
307
|
+
await grid.changes.discardById('row-1'); // ID로 단일 행
|
|
308
|
+
await grid.changes.discard(row); // 행 객체로
|
|
309
|
+
await grid.changes.discardByIndex(0); // sourceIndex로
|
|
310
|
+
const discarded = await grid.changes.discardSelectedRows(); // 선택 행
|
|
311
|
+
await grid.changes.discardImmediateById('row-1'); // undo 스택 미경유, 즉시 반영
|
|
312
|
+
await grid.changes.discardImmediate(row); // 행 객체로 즉시 반영
|
|
313
|
+
await grid.changes.discardImmediateByIndex(0); // sourceIndex로 즉시 반영
|
|
314
|
+
await grid.changes.discardAll(); // 전체
|
|
315
|
+
|
|
316
|
+
// ── 커밋 ──
|
|
317
|
+
try {
|
|
318
|
+
await grid.changes.commit(); // invalid 있으면 PS715 throw + 첫 invalid로 자동 점프
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.warn(err.message); // 사용자가 invalid 셀을 확인 가능
|
|
321
|
+
}
|
|
322
|
+
await grid.changes.commit({ allowInvalid: true }); // 검증 무시하고 강제 commit
|
|
323
|
+
|
|
324
|
+
// ── 조회 ──
|
|
325
|
+
const dirty = grid.changes.hasChanges;
|
|
326
|
+
const summary = grid.changes.getAll();
|
|
327
|
+
// → { added: [...], modified: [...], deleted: [...], reorderedOrder: [...] | null }
|
|
328
|
+
const state = grid.changes.getRowStateById('row-1');
|
|
329
|
+
// → 'pristine' | 'added' | 'modified' | 'deleted'
|
|
330
|
+
const stateByIdx = grid.changes.getRowStateByIndex(0);
|
|
331
|
+
const stateByRow = grid.changes.getRowState(row);
|
|
332
|
+
|
|
333
|
+
// ── 대량 업데이트 ──
|
|
334
|
+
const updated = await grid.changes.bulkUpdate('status', 'done', 'selected');
|
|
335
|
+
const updatedAll = await grid.changes.bulkUpdate('flag', true, 'all');
|
|
336
|
+
|
|
337
|
+
// ── 행 재정렬 ──
|
|
338
|
+
await grid.changes.reorderRow(2, 5); // 인덱스 2→5
|
|
339
|
+
const order = grid.changes.getRowOrder();
|
|
340
|
+
const reordered = grid.changes.isReordered();
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### grid.undoManager 사용 예제
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// ── Undo/Redo ──
|
|
349
|
+
grid.undoManager.undo();
|
|
350
|
+
grid.undoManager.redo();
|
|
351
|
+
console.log(grid.undoManager.canUndo); // true/false
|
|
352
|
+
console.log(grid.undoManager.canRedo); // true/false
|
|
353
|
+
|
|
354
|
+
// ── 배치 (여러 작업을 하나의 Undo 단위로) ──
|
|
355
|
+
grid.undoManager.beginBatch('대량 수정');
|
|
356
|
+
await grid.changes.updateCellById(1, 'name', 'A');
|
|
357
|
+
await grid.changes.updateCellById(2, 'name', 'B');
|
|
358
|
+
await grid.changes.updateCellById(3, 'name', 'C');
|
|
359
|
+
await grid.undoManager.endBatch();
|
|
360
|
+
// → Undo 1번으로 3개 셀 모두 복원
|
|
361
|
+
|
|
362
|
+
// ── 콜백 배치 ──
|
|
363
|
+
await grid.undoManager.batch(async () => {
|
|
364
|
+
await grid.changes.addRow({ name: '행1' });
|
|
365
|
+
await grid.changes.addRow({ name: '행2' });
|
|
366
|
+
}, { description: '행 2개 추가' });
|
|
367
|
+
|
|
368
|
+
// ── skipUndo 옵션 ──
|
|
369
|
+
await grid.undoManager.batch(async () => {
|
|
370
|
+
await grid.changes.updateCellById(1, 'timestamp', Date.now());
|
|
371
|
+
}, { skipUndo: true }); // Undo 스택에 기록하지 않음
|
|
372
|
+
|
|
373
|
+
// ── undoThrough — 특정 command까지 cascade undo (낙관적 파이프라인 롤백) ──
|
|
374
|
+
// e.commandId는 row:reordered / changes:undone 이벤트가 주는 값 (= BatchCommand.id).
|
|
375
|
+
// target + 그 위(더 최신) 전부를 top→target 순으로 되돌리고, 되돌린 id 배열을 반환.
|
|
376
|
+
grid.on('row:reordered', async (e) => {
|
|
377
|
+
try {
|
|
378
|
+
await api.applyReorder(e.patches); // 낙관적: UI는 이미 반영됨
|
|
379
|
+
} catch {
|
|
380
|
+
// 실패한 그 작업 + 그 위에 쌓인 것 전부 롤백 (위치 기반 undo()는 엉뚱한 top을 되돌림)
|
|
381
|
+
const rolledBack = grid.undoManager.undoThrough(e.commandId);
|
|
382
|
+
console.log('cascade 롤백:', rolledBack); // [topId, ..., targetId]
|
|
383
|
+
// 백엔드 롤백은 changes:undone이 cascade 개수만큼 자동 발화 → 별도 핸들러가 처리
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
// 미존재/빈 스택/이미 롤백된 id → [] 반환 (idempotent, throw 없음)
|
|
387
|
+
|
|
388
|
+
// ── 스택 접근 ──
|
|
389
|
+
const stack = grid.undoManager.getStack();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### grid.rowGroup 사용 예제
|
|
395
|
+
|
|
396
|
+
> 정식 네임스페이스는 `grid.rowGroup`입니다. 구 `grid.group`은 deprecated 별칭으로 계속 동작합니다(다음 major에서 제거 예정).
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// ── 그룹화 설정 ──
|
|
400
|
+
await grid.rowGroup.setConfig({
|
|
401
|
+
columns: ['region', 'category'],
|
|
402
|
+
aggregates: { amount: 'sum', quantity: 'avg' },
|
|
403
|
+
defaultCollapsed: false,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ── 간편 그룹화 ──
|
|
407
|
+
await grid.rowGroup.groupBy(['region', 'category']);
|
|
408
|
+
|
|
409
|
+
// ── 그룹 설정 조회 ──
|
|
410
|
+
const groupConfig = grid.rowGroup.getConfig();
|
|
411
|
+
|
|
412
|
+
// ── 그룹 토글/펼침/접힘 ──
|
|
413
|
+
await grid.rowGroup.toggle('region:서울');
|
|
414
|
+
await grid.rowGroup.expand('region:서울');
|
|
415
|
+
await grid.rowGroup.collapse('region:서울');
|
|
416
|
+
console.log(grid.rowGroup.isCollapsed('region:서울')); // true/false
|
|
417
|
+
await grid.rowGroup.expandAll();
|
|
418
|
+
await grid.rowGroup.collapseAll();
|
|
419
|
+
|
|
420
|
+
// ── 레벨별 펼침/접힘 ──
|
|
421
|
+
await grid.rowGroup.expandToLevel(0); // 최상위만 펼침 — ⚠️ 이벤트 미발화(알려진 갭)
|
|
422
|
+
await grid.rowGroup.collapseToLevel(0); // 최상위만 남기고 접기 — ⚠️ 이벤트 미발화
|
|
423
|
+
await grid.rowGroup.expandAtLevel(1); // 정확히 레벨 1 그룹만 펼침 — 다른 레벨 보존
|
|
424
|
+
await grid.rowGroup.collapseAtLevel(1); // 정확히 레벨 1 그룹만 접힘 — 다른 레벨 보존
|
|
425
|
+
|
|
426
|
+
// ── 그룹 컬럼 추가/제거 ──
|
|
427
|
+
await grid.rowGroup.addColumn('product');
|
|
428
|
+
await grid.rowGroup.removeColumn('category');
|
|
429
|
+
|
|
430
|
+
// ── 그룹 멤버 rowId 조회 (groupId = "col:val/col:val" 경로 = GroupRowContext.groupId) ──
|
|
431
|
+
// 소스 전체 스캔이라 접힘/필터 무관, 중첩 그룹이면 하위 전체 포함.
|
|
432
|
+
// 메서드명은 getGroupRows 그대로 유지.
|
|
433
|
+
grid.rowGroup.getGroupRows('region:서울'); // 서울 하위 모든 데이터 행 rowId[]
|
|
434
|
+
grid.rowGroup.getGroupRows('region:서울/category:가전'); // 중첩 경로
|
|
435
|
+
// 예: 그룹 단위 일괄 수정
|
|
436
|
+
const ids = grid.rowGroup.getGroupRows('region:서울');
|
|
437
|
+
grid.rows.getByIds(ids).setValue('flag', true); // enableReferCache: true 필요
|
|
438
|
+
|
|
439
|
+
// ── 그룹화 해제 ──
|
|
440
|
+
await grid.rowGroup.clear();
|
|
441
|
+
|
|
442
|
+
// ── 토글 이벤트 구독 (구 onGroupToggle config 콜백 대체) ──
|
|
443
|
+
// 헤더 클릭은 source:'user', 프로그래매틱 API 호출은 source:'api'로 발화.
|
|
444
|
+
grid.on('rowGroup:toggle', (e) => {
|
|
445
|
+
if (e.source === 'api') return; // 프로그래매틱 재진입 무한루프 방지
|
|
446
|
+
console.log('single toggle', e.groupId, e.collapsed);
|
|
447
|
+
});
|
|
448
|
+
grid.on('rowGroup:batchToggle', (e) => {
|
|
449
|
+
// 여러 그룹 일괄 토글. e.scope로 구분 — 화면 밖 포함 전체 groupIds가 1회로 요약 통지됨.
|
|
450
|
+
switch (e.scope) {
|
|
451
|
+
case 'level': // 같은 depth 일괄(Ctrl/Cmd+클릭 또는 expandAtLevel/collapseAtLevel). e.level 존재.
|
|
452
|
+
console.log('level toggle', e.level, e.groupIds, e.collapsed); break;
|
|
453
|
+
case 'toLevel': // 특정 depth까지 누적(expandToLevel/collapseToLevel). e.level 존재.
|
|
454
|
+
console.log('toLevel toggle', e.level, e.groupIds, e.collapsed); break;
|
|
455
|
+
case 'all': // 전체(expandAll/collapseAll). e.level은 undefined.
|
|
456
|
+
console.log('all toggle', e.groupIds, e.collapsed); break;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
// 그룹 체크박스 cascade (구 group:checkbox 에서 리네임)
|
|
460
|
+
grid.on('rowGroup:checkbox', (e) => console.log(e.groupId, e.checked, e.memberCount));
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### grid.tree 사용 예제
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// ── 트리 설정 (flat 구조 — parentId 기반) ──
|
|
469
|
+
await grid.tree.setConfig({
|
|
470
|
+
idField: 'id',
|
|
471
|
+
parentField: 'parentId',
|
|
472
|
+
treeColumn: 'name',
|
|
473
|
+
defaultExpandLevel: 1,
|
|
474
|
+
aggregates: { amount: 'sum' },
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ── 트리 설정 (중첩 구조 — children 배열) ──
|
|
478
|
+
await grid.tree.loadData(nestedData, {
|
|
479
|
+
idField: 'id',
|
|
480
|
+
childrenField: 'children',
|
|
481
|
+
treeColumn: 'name',
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ── 노드 조작 ──
|
|
485
|
+
await grid.tree.toggle('node-1');
|
|
486
|
+
await grid.tree.expand('node-1');
|
|
487
|
+
await grid.tree.collapse('node-1');
|
|
488
|
+
await grid.tree.expandAll();
|
|
489
|
+
await grid.tree.collapseAll();
|
|
490
|
+
await grid.tree.expandToLevel(2); // 레벨 2까지 펼침
|
|
491
|
+
await grid.tree.collapseToLevel(0); // 루트만 남기고 접기
|
|
492
|
+
await grid.tree.expandAtLevel(1); // 정확히 레벨 1 노드만 펼침 — 다른 레벨 보존
|
|
493
|
+
await grid.tree.collapseAtLevel(1); // 정확히 레벨 1 노드만 접힘 — 다른 레벨 보존
|
|
494
|
+
console.log(grid.tree.isCollapsed('node-1')); // true/false
|
|
495
|
+
|
|
496
|
+
// ── 경로 펼치기 (검색 결과 표시용) ──
|
|
497
|
+
await grid.tree.expandPath('deep-node-42'); // 조상 노드를 모두 펼쳐서 대상 노드를 보이게 함
|
|
498
|
+
|
|
499
|
+
// ── 프로그램 방식 노드 이동 (드래그 없이 코드로) ──
|
|
500
|
+
// moveNode(nodeId, newParentId, newIndex?) — newIndex는 "이동 후 최종 위치".
|
|
501
|
+
// flat 그리드 changes.reorderRow의 tree 대칭 API. 원자 undo 단위 + row:reordered{kind:'tree'} 발화.
|
|
502
|
+
await grid.tree.moveNode('report.pdf', 'work', 0); // report.pdf를 work 폴더의 첫 자식으로
|
|
503
|
+
await grid.tree.moveNode('work', null, 1); // work를 root 레벨 index 1로 승격
|
|
504
|
+
await grid.tree.moveNode('leaf', 'archive'); // newIndex 생략 → archive 맨 끝에 append
|
|
505
|
+
// throw: PS401(nodeId 부재) / PS402(부모 부재) / PS403(cycle — 자기 자신·후손으로 이동)
|
|
506
|
+
|
|
507
|
+
// ── 트래버설 (idField/parentField 값 기준 — 접힘/필터/정렬 무관, 소스 전체 스캔) ──
|
|
508
|
+
grid.tree.getChildren('work'); // 직계 자식 rowId[] (소스 순서)
|
|
509
|
+
grid.tree.getDescendants('work'); // 모든 자손 rowId[] (BFS)
|
|
510
|
+
grid.tree.getParent('report.pdf'); // 부모 rowId | null
|
|
511
|
+
grid.tree.getAncestors('report.pdf'); // 조상 rowId[] (가까운 부모 → root)
|
|
512
|
+
|
|
513
|
+
// ── 부모↔자손 체크박스 cascade (boolean 컬럼 + editCommitted 트리거) ──
|
|
514
|
+
// enableReferCache: true 필요 (rows.getByIds/getById는 idMap 기반 조회)
|
|
515
|
+
let cascading = false;
|
|
516
|
+
grid.on('cell:editCommitted', (e) => {
|
|
517
|
+
if (e.columnId !== 'checked' || cascading || e.rowId == null) return;
|
|
518
|
+
cascading = true;
|
|
519
|
+
try {
|
|
520
|
+
const next = !!e.newValue;
|
|
521
|
+
// ① 하향: 자손 전체를 같은 값으로
|
|
522
|
+
const descendants = grid.tree.getDescendants(e.rowId);
|
|
523
|
+
if (descendants.length) grid.rows.getByIds(descendants).setValue('checked', next);
|
|
524
|
+
// ② 상향: 가까운 조상부터 "자식 전부 체크 = 부모 체크" 재계산
|
|
525
|
+
const cur = new Map([[String(e.rowId), next], ...descendants.map(d => [String(d), next])]);
|
|
526
|
+
const valueOf = (id) => cur.has(String(id)) ? cur.get(String(id)) : !!grid.rows.getById(id).data?.checked;
|
|
527
|
+
for (const anc of grid.tree.getAncestors(e.rowId)) {
|
|
528
|
+
const all = grid.tree.getChildren(anc).every(valueOf);
|
|
529
|
+
grid.rows.getById(anc).setValue('checked', all);
|
|
530
|
+
cur.set(String(anc), all);
|
|
531
|
+
}
|
|
532
|
+
} finally { cascading = false; }
|
|
533
|
+
grid.refresh(); // 값이 안 바뀐 조상까지 재렌더 (부분체크 indeterminate 재계산 등)
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ── 설정 조회 ──
|
|
537
|
+
const config = grid.tree.getConfig();
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
> **부분 체크 표시(indeterminate)** — boolean 셀은 native `<input>`이 아니라 `.ps-cell-checkbox` span이라
|
|
541
|
+
> `formatRow(type:'data')`에서 `getDescendants` 체크 비율을 계산해 CSS 클래스로 `−`를 그립니다.
|
|
542
|
+
> 전체 동작 데모는 `apps/demo/examples/tree-checkbox-cascade.html` 참조.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
### 트리 드래그 reorder 사용 예제
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
// ── 트리 드래그 reorder 활성화 (treeConfig + reorderableRows) ──
|
|
550
|
+
const grid = new PureSheet(container, {
|
|
551
|
+
treeConfig: { idField: 'id', parentField: 'parentId', treeColumn: 'name' },
|
|
552
|
+
dragDropConfig: {
|
|
553
|
+
reorderableRows: true, // treeConfig와 함께 → 트리 인식 드래그 자동 활성
|
|
554
|
+
dragTrigger: 'row', // 행 본문 어디서나 드래그 (파일 탐색기 UX)
|
|
555
|
+
showDragHandleColumn: false, // 핸들 컬럼 숨김
|
|
556
|
+
treeReorder: {
|
|
557
|
+
allowReparent: true, // 다른 부모로 재부모화 허용
|
|
558
|
+
expandOnHover: 700, // collapsed 부모 위 700ms hover → 자동 펼침
|
|
559
|
+
// 폴더 위 dwell → drop-onto(자식화), 파일 위 → insert-between(형제 사이)
|
|
560
|
+
resolveDropMode: (row) => (row.type === 'folder' ? 'drop-onto' : 'insert-between'),
|
|
561
|
+
isDroppableAsChild: (row) => row.type === 'folder', // 폴더만 자식 수용
|
|
562
|
+
dropOntoEdgeThreshold: 2, // 행 가장자리 2px는 drop-onto여도 형제 삽입으로 fall-back
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// ── order 필드 자동 관리 + updatedAt 갱신 (flat/tree 공통) ──
|
|
568
|
+
await grid.dragDrop.setConfig({
|
|
569
|
+
...grid.dragDrop.getConfig(),
|
|
570
|
+
rowOrderField: { field: 'order', strategy: 'dense' }, // tree에서도 형제 순서 자동 저장
|
|
571
|
+
mutateOnDirty: (row, ctx) => {
|
|
572
|
+
if (ctx.type === 'tree-reorder' || ctx.type === 'reorder') {
|
|
573
|
+
return { updatedAt: new Date().toISOString() };
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ── 완료 통지: row:reordered {kind:'tree'} ──
|
|
579
|
+
grid.on('row:reordered', (e) => {
|
|
580
|
+
if (e.kind !== 'tree') return;
|
|
581
|
+
console.log('트리 이동:', e.patches, 'commandId:', e.commandId);
|
|
582
|
+
// 낙관적 백엔드 sync + 실패 시 undoThrough(e.commandId) → 이벤트 예제 섹션 참조
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ── 드래그 없이 프로그램 방식 이동 ──
|
|
586
|
+
await grid.tree.moveNode('report.pdf', 'work', 0); // work 폴더 첫 자식으로
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
> 낙관적 백엔드 동기화(실패 시 `undoThrough` cascade 롤백) + undo/redo 시 `changes:undone`/`changes:redone` 처리 전체 흐름은 [이벤트 전체 사용 예제](#이벤트-전체-사용-예제)와 [grid.undoManager 사용 예제](#gridundomanager-사용-예제)를 참조하세요.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
### grid.pivot 사용 예제
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// ── 모드 관리 ──
|
|
597
|
+
console.log(grid.pivot.getMode()); // 'flat' | 'pivot'
|
|
598
|
+
await grid.pivot.setMode('pivot');
|
|
599
|
+
|
|
600
|
+
// ── 피벗 설정 ──
|
|
601
|
+
await grid.pivot.setConfig({
|
|
602
|
+
rowFields: [
|
|
603
|
+
{ field: 'region', header: '지역' },
|
|
604
|
+
{ field: 'category', header: '카테고리' },
|
|
605
|
+
],
|
|
606
|
+
columnFields: [
|
|
607
|
+
{ field: 'quarter', header: '분기' },
|
|
608
|
+
],
|
|
609
|
+
valueFields: [
|
|
610
|
+
{ field: 'sales', header: '매출', aggregate: 'sum', showValueAs: 'none' },
|
|
611
|
+
{ field: 'profit', header: '이익', aggregate: 'sum', showValueAs: 'percentOfGrandTotal' },
|
|
612
|
+
],
|
|
613
|
+
showRowGrandTotals: true,
|
|
614
|
+
showColumnGrandTotals: true,
|
|
615
|
+
showRowSubTotals: true,
|
|
616
|
+
rowSubTotalPosition: 'after',
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// ⚠️ 같은 field 를 값 필드로 **여러 번** 넣을 때는 불변 id 를 명시한다 (ITSM-2026-002188).
|
|
620
|
+
// id 가 없으면 'sales-0' 같은 **순서 기반** 키로 폴백하는데, 값 필드 순서를 바꾸거나
|
|
621
|
+
// 하나를 지우면 인덱스가 밀려 같은 저장본이 다른 필드를 가리킨다(수식 필터·조건부 서식이
|
|
622
|
+
// 조용히 어긋남). 콘솔 경고도 함께 나온다.
|
|
623
|
+
await grid.pivot.setConfig({
|
|
624
|
+
rowFields: [{ field: 'region', header: '지역' }],
|
|
625
|
+
valueFields: [
|
|
626
|
+
{ field: 'sales', header: '매출 합계', aggregate: 'sum', id: 'sales_sum' },
|
|
627
|
+
{ field: 'sales', header: '매출 평균', aggregate: 'avg', id: 'sales_avg' },
|
|
628
|
+
],
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const config = grid.pivot.getConfig(); // 깊은 복사본 반환
|
|
632
|
+
const result = grid.pivot.getResult();
|
|
633
|
+
|
|
634
|
+
// ── 편의 메서드: 소계/총합계 설정 ──
|
|
635
|
+
await grid.pivot.setSubTotals({
|
|
636
|
+
showRowSubTotals: true,
|
|
637
|
+
rowSubTotalFields: ['category'],
|
|
638
|
+
rowSubTotalPosition: 'after',
|
|
639
|
+
});
|
|
640
|
+
await grid.pivot.setGrandTotals({
|
|
641
|
+
showRowGrandTotals: true,
|
|
642
|
+
showColumnGrandTotals: false,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// ── 편의 메서드: 필드 변경 ──
|
|
646
|
+
await grid.pivot.setRowFields([{ field: 'region', header: '지역' }]);
|
|
647
|
+
await grid.pivot.setColumnFields([{ field: 'year', header: '연도' }]);
|
|
648
|
+
await grid.pivot.setValueFields([{ field: 'sales', header: '매출', aggregate: 'sum' }]);
|
|
649
|
+
|
|
650
|
+
// ── 상태 저장/복원 ──
|
|
651
|
+
const state = grid.pivot.getState();
|
|
652
|
+
// → { config: {...}, collapsedRowNodes: ['region:서울'], collapsedColumnNodes: ['quarter:Q1'] }
|
|
653
|
+
await grid.pivot.setState(state);
|
|
654
|
+
|
|
655
|
+
// ── 행 노드 네비게이션 ──
|
|
656
|
+
await grid.pivot.toggleRow('region:서울');
|
|
657
|
+
await grid.pivot.expandRow('region:서울');
|
|
658
|
+
await grid.pivot.collapseRow('region:서울');
|
|
659
|
+
await grid.pivot.expandAllRows();
|
|
660
|
+
await grid.pivot.collapseAllRows();
|
|
661
|
+
await grid.pivot.expandRowAtLevel(0); // 특정 레벨만 펼침
|
|
662
|
+
await grid.pivot.collapseRowAtLevel(1); // 특정 레벨만 접기
|
|
663
|
+
await grid.pivot.expandRowToLevel(2); // 레벨 2까지 펼침
|
|
664
|
+
await grid.pivot.collapseRowToLevel(0); // 레벨 0까지 접기
|
|
665
|
+
console.log(grid.pivot.isRowCollapsed('region:서울'));
|
|
666
|
+
const collapsedRows = grid.pivot.getCollapsedRowNodes();
|
|
667
|
+
|
|
668
|
+
// ── 피벗 정렬/필터 (viewState 단일 채널 — field-level sort 옵션 없음) ──
|
|
669
|
+
// 라벨 기준 정렬
|
|
670
|
+
await grid.sort([{ id: 'region', direction: 'asc', sequence: 1 }]);
|
|
671
|
+
// 값(집계) 기준 정렬 — byValue 로 집계 값 기준 정렬
|
|
672
|
+
await grid.sort([{ id: 'region', direction: 'desc', sequence: 1, byValue: 'sales' }]);
|
|
673
|
+
|
|
674
|
+
// 차원(행/열 필드) 필터 → core 가 자동으로 pre(소스 축소 후 재집계)로 분류
|
|
675
|
+
await grid.filter([{ id: 'region', operator: 'eq', filterValue: '서울', sequence: 1 }]);
|
|
676
|
+
// 값(measure) 필터 → 자동으로 post(피벗 값 기준 행 필터). excludeUnmatchedColumns 로 빈 컬럼까지 제거
|
|
677
|
+
await grid.filter([
|
|
678
|
+
{ id: 'sales', operator: 'gte', filterValue: 1000, sequence: 1, excludeUnmatchedColumns: true },
|
|
679
|
+
]);
|
|
680
|
+
// 명시 mode 로 분류 override (예: dimension 필터를 소스 축소 없이 post 로)
|
|
681
|
+
await grid.filter([{ id: 'region', operator: 'eq', filterValue: '서울', sequence: 1, mode: 'post' }]);
|
|
682
|
+
|
|
683
|
+
// ── 열 노드 네비게이션 (Row 메서드와 대칭) ──
|
|
684
|
+
await grid.pivot.toggleColumn('quarter:Q1');
|
|
685
|
+
await grid.pivot.expandAllColumns();
|
|
686
|
+
await grid.pivot.collapseColumnToLevel(0);
|
|
687
|
+
const collapsedCols = grid.pivot.getCollapsedColumnNodes();
|
|
688
|
+
|
|
689
|
+
// ── 소스 행 매핑 (trackSourceRows: true 필요) ──
|
|
690
|
+
const cellSources = await grid.pivot.getSourceRowsForCell(0, 'Q1_sales');
|
|
691
|
+
const rowSources = await grid.pivot.getSourceRowsForRow(0);
|
|
692
|
+
const selSources = await grid.pivot.getSourceRowsForSelection();
|
|
693
|
+
|
|
694
|
+
// ── 메타/차트 ──
|
|
695
|
+
const meta = grid.pivot.getPivotMeta(); // 동기, 경량 스냅샷
|
|
696
|
+
const series = await grid.pivot.getSeriesData(); // 차트용 시리즈
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
### grid.masterDetail 사용 예제
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// ── 디테일 설정 ──
|
|
705
|
+
await grid.masterDetail.setConfig({
|
|
706
|
+
expandColumn: 'name',
|
|
707
|
+
singleExpand: false,
|
|
708
|
+
keepExpandedOnDataChange: false,
|
|
709
|
+
keepMasterDetailRows: true,
|
|
710
|
+
keepMasterDetailRowsCount: 10,
|
|
711
|
+
minHeight: 100,
|
|
712
|
+
maxHeight: 400,
|
|
713
|
+
isExpandable: (data) => data.hasDetails === true,
|
|
714
|
+
renderer: (container, parentData, dataIndex) => {
|
|
715
|
+
// 디테일 패널 렌더링
|
|
716
|
+
const div = document.createElement('div');
|
|
717
|
+
div.innerHTML = `<h3>${parentData.name} 상세</h3><p>인덱스: ${dataIndex}</p>`;
|
|
718
|
+
container.appendChild(div);
|
|
719
|
+
|
|
720
|
+
// cleanup 함수 반환 (선택)
|
|
721
|
+
return () => {
|
|
722
|
+
div.remove();
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const config = grid.masterDetail.getConfig();
|
|
728
|
+
|
|
729
|
+
// ── 디테일 조작 ──
|
|
730
|
+
await grid.masterDetail.toggle(0); // dataIndex 0 토글
|
|
731
|
+
await grid.masterDetail.expand(3); // dataIndex 3 펼침
|
|
732
|
+
await grid.masterDetail.collapse(3); // dataIndex 3 접기
|
|
733
|
+
await grid.masterDetail.expandAll(); // 전체 펼침
|
|
734
|
+
await grid.masterDetail.collapseAll(); // 전체 접기
|
|
735
|
+
console.log(grid.masterDetail.isExpanded(0)); // true/false
|
|
736
|
+
|
|
737
|
+
// ── 설정 제거 ──
|
|
738
|
+
await grid.masterDetail.setConfig(null);
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
### grid.validation 사용 예제
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
// ── 검증 실행 ──
|
|
747
|
+
const errors = grid.validation.validateAll(); // trigger 기본 'commit' (['blur', 'commit'])
|
|
748
|
+
const blurErrors = grid.validation.validateAll('blur'); // 특정 trigger만
|
|
749
|
+
|
|
750
|
+
// ── 에러 조회 ──
|
|
751
|
+
const allErrors = grid.validation.getErrors();
|
|
752
|
+
// → [{ rowId: 1, field: 'name', messages: ['필수 입력'] }, ...]
|
|
753
|
+
console.log(grid.validation.hasErrors()); // true/false
|
|
754
|
+
|
|
755
|
+
// ── 에러 초기화 ──
|
|
756
|
+
await grid.validation.clear();
|
|
757
|
+
|
|
758
|
+
// ── 셀/행 단위 조회 ──
|
|
759
|
+
const cellErrors = grid.validation.getCellErrors('user-5', 'email');
|
|
760
|
+
// → ['이메일 형식 불일치'] | undefined
|
|
761
|
+
const has = grid.validation.hasCellError('user-5', 'email'); // true/false
|
|
762
|
+
|
|
763
|
+
const rowErrors = grid.validation.getRowErrors('user-5');
|
|
764
|
+
// → Map { 'email' => ['...'], 'phone' => ['...'] } | undefined
|
|
765
|
+
if (rowErrors) {
|
|
766
|
+
for (const [field, messages] of rowErrors) {
|
|
767
|
+
console.log(`${field}: ${messages.join(', ')}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ── 외부 에러 주입 (백엔드 응답 반영) ──
|
|
772
|
+
// 시나리오: commit 전 사전 검증 후 백엔드에 보냈는데 서버가 reject
|
|
773
|
+
async function saveDirtyChanges() {
|
|
774
|
+
// 1) 클라이언트 검증 먼저
|
|
775
|
+
const clientErrors = grid.validation.validateAll();
|
|
776
|
+
if (clientErrors.length > 0) {
|
|
777
|
+
grid.validation.nextInvalid({ fromStart: true });
|
|
778
|
+
return alert(`${clientErrors.length}개 오류를 먼저 수정하세요.`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 2) 백엔드 전송
|
|
782
|
+
const dirty = grid.changes.getAll();
|
|
783
|
+
const response = await fetch('/api/save', {
|
|
784
|
+
method: 'POST',
|
|
785
|
+
body: JSON.stringify(dirty),
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
if (response.ok) {
|
|
789
|
+
// 3-a) 성공 → 검증 skip하고 commit (이미 검증 완료)
|
|
790
|
+
await grid.changes.commit({ allowInvalid: true });
|
|
791
|
+
} else {
|
|
792
|
+
// 3-b) 서버가 field-level 에러 반환
|
|
793
|
+
const { errors } = await response.json();
|
|
794
|
+
// errors: [{ rowId, field, messages }, ...]
|
|
795
|
+
for (const e of errors) {
|
|
796
|
+
grid.validation.setCellErrors(e.rowId, e.field, e.messages);
|
|
797
|
+
}
|
|
798
|
+
// 에러가 있는 첫 셀로 자동 이동
|
|
799
|
+
grid.validation.nextInvalid({ fromStart: true });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ── 특정 행의 모든 에러 제거 (사용자가 해당 행을 다시 편집할 때) ──
|
|
804
|
+
grid.validation.clearRowErrors('user-5');
|
|
805
|
+
|
|
806
|
+
// ── 개별 셀 에러 제거 (입력 중 실시간으로 특정 제약 해소됨을 감지) ──
|
|
807
|
+
grid.validation.clearCellErrors('user-5', 'email');
|
|
808
|
+
|
|
809
|
+
// ── Invalid 셀 네비게이션 ──
|
|
810
|
+
// 첫 invalid 셀로 이동 (anchor 무시)
|
|
811
|
+
const first = grid.validation.nextInvalid({ fromStart: true });
|
|
812
|
+
// → { rowId: 'user-5', field: 'email' } | null
|
|
813
|
+
|
|
814
|
+
// 현재 선택 셀 이후의 다음 invalid
|
|
815
|
+
const next = grid.validation.nextInvalid();
|
|
816
|
+
|
|
817
|
+
// 이전 invalid
|
|
818
|
+
const prev = grid.validation.prevInvalid();
|
|
819
|
+
|
|
820
|
+
// wrap 없이 순회 (끝에서 멈춤)
|
|
821
|
+
const lastInRange = grid.validation.nextInvalid({ wrap: false });
|
|
822
|
+
if (lastInRange === null) {
|
|
823
|
+
console.log('더 이상 뒤에 invalid 셀이 없습니다.');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ── commit 시 자동 점프 활용 ──
|
|
827
|
+
try {
|
|
828
|
+
await grid.changes.commit(); // 실패 시 자동으로 첫 invalid로 점프
|
|
829
|
+
} catch (err) {
|
|
830
|
+
// 에러 메시지 표시. anchor는 이미 첫 invalid 셀로 이동돼 있음.
|
|
831
|
+
alert(err.message); // "[PureSheet] PS715 commit: error.PS715"
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── 검증 무시하고 강제 commit ──
|
|
835
|
+
await grid.changes.commit({ allowInvalid: true });
|
|
836
|
+
|
|
837
|
+
// ── 커스텀 "Fix all errors" 워크플로우 ──
|
|
838
|
+
async function fixAllErrors() {
|
|
839
|
+
let current = grid.validation.nextInvalid({ fromStart: true });
|
|
840
|
+
while (current) {
|
|
841
|
+
// UI로 사용자에게 수정 요청
|
|
842
|
+
await promptUserToFix(current);
|
|
843
|
+
current = grid.validation.nextInvalid({ wrap: false });
|
|
844
|
+
}
|
|
845
|
+
if (!grid.validation.hasErrors()) {
|
|
846
|
+
await grid.changes.commit();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
### grid.clipboard 사용 예제
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
// ── 복사 ──
|
|
857
|
+
// 데이터만 복사
|
|
858
|
+
const text = await grid.clipboard.copy();
|
|
859
|
+
|
|
860
|
+
// 헤더 포함 복사
|
|
861
|
+
const text = await grid.clipboard.copy({ includeHeaders: true });
|
|
862
|
+
|
|
863
|
+
// ── 붙여넣기 ──
|
|
864
|
+
const pastedCount = await grid.clipboard.paste();
|
|
865
|
+
// → 6 (붙여넣기된 셀 수)
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
### grid.headers 사용 예제
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
// ── 컬럼 그룹 조작 ──
|
|
874
|
+
grid.headers.collapse('personal');
|
|
875
|
+
grid.headers.expand('personal');
|
|
876
|
+
grid.headers.toggle('personal');
|
|
877
|
+
console.log(grid.headers.isCollapsed('personal')); // true/false
|
|
878
|
+
|
|
879
|
+
grid.headers.collapseAll();
|
|
880
|
+
grid.headers.expandAll();
|
|
881
|
+
|
|
882
|
+
// ── 특정 레벨만 (Ctrl/Cmd+클릭 "같은 depth 일괄" 제스처와 동일) ──
|
|
883
|
+
grid.headers.collapseAtLevel(0); // level 0 그룹만 접기
|
|
884
|
+
grid.headers.expandAtLevel(0); // level 0 그룹만 펼치기
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
### grid.io 사용 예제
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
// ── 1) 기본 export (메모리 Blob) ──
|
|
893
|
+
const blob = await grid.io.toExcel({
|
|
894
|
+
scope: 'visible',
|
|
895
|
+
includeHeaders: true,
|
|
896
|
+
preserveFormatting: true,
|
|
897
|
+
});
|
|
898
|
+
const url = URL.createObjectURL(blob);
|
|
899
|
+
const a = document.createElement('a');
|
|
900
|
+
a.href = url;
|
|
901
|
+
a.download = 'data.xlsx';
|
|
902
|
+
a.click();
|
|
903
|
+
URL.revokeObjectURL(url);
|
|
904
|
+
|
|
905
|
+
// ── 2) 디스크 직접 저장 (FSA) + 분할 ──
|
|
906
|
+
const abort = new AbortController();
|
|
907
|
+
const result = await grid.io.saveExcelToFile({
|
|
908
|
+
suggestedName: 'huge-dataset.xlsx',
|
|
909
|
+
scope: 'all',
|
|
910
|
+
sheetSplit: 'auto', // 1,048,576 행 한계 자동 분할
|
|
911
|
+
sheetNamePattern: 'Data ({n})',
|
|
912
|
+
freezeHeader: true,
|
|
913
|
+
autoFilter: true,
|
|
914
|
+
useWorker: true,
|
|
915
|
+
signal: abort.signal,
|
|
916
|
+
onProgress: (p) => {
|
|
917
|
+
if (p.currentSheet) {
|
|
918
|
+
console.log(`Sheet ${p.currentSheet}/${p.totalSheets} ${p.percent}%`);
|
|
919
|
+
} else {
|
|
920
|
+
console.log(`${p.phase}: ${p.percent}%`);
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
console.log(`Saved via ${result.mode}: ${result.fileName}`);
|
|
925
|
+
|
|
926
|
+
// ── 3) 시트 정보 미리 조회 ──
|
|
927
|
+
const sheets = await grid.io.getSheetInfo(file);
|
|
928
|
+
// → [{ index: 0, name: 'Sheet1', estimatedRowCount: 5_000_000 }]
|
|
929
|
+
|
|
930
|
+
// ── 4) 미리보기 (처음 20행만) ──
|
|
931
|
+
const preview = await grid.io.previewExcel(file, { rows: 20 });
|
|
932
|
+
console.log(preview.headers, preview.rows);
|
|
933
|
+
|
|
934
|
+
// ── 5) Import: replace + 수동 검토 후 commit ──
|
|
935
|
+
const importResult = await grid.io.fromExcel(file, {
|
|
936
|
+
mode: 'replace',
|
|
937
|
+
autoCommit: false,
|
|
938
|
+
onProgress: (p) => updateProgressBar(p.percent),
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
if (importResult.warnings?.includes('LARGE_IMPORT')) {
|
|
942
|
+
const ok = confirm(
|
|
943
|
+
`Import 결과 미리보기:\n` +
|
|
944
|
+
`+ 추가 ${importResult.addedCount}\n` +
|
|
945
|
+
`~ 수정 ${importResult.updatedCount}\n` +
|
|
946
|
+
`- 삭제 ${importResult.deletedCount}\n` +
|
|
947
|
+
`= 동일 ${importResult.unchangedCount}\n\n` +
|
|
948
|
+
`이대로 commit할까요?`
|
|
949
|
+
);
|
|
950
|
+
if (ok) await grid.changes.commit();
|
|
951
|
+
else await grid.changes.discardAll();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── 6) Import: append + skip 전략 ──
|
|
955
|
+
const appendResult = await grid.io.fromExcel(file, {
|
|
956
|
+
mode: 'append',
|
|
957
|
+
conflictStrategy: 'skip',
|
|
958
|
+
autoCommit: true,
|
|
959
|
+
});
|
|
960
|
+
if (appendResult.conflicts?.length) {
|
|
961
|
+
console.warn(`충돌 ${appendResult.skippedCount}개 건너뜀:`, appendResult.conflicts);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ── 7) Dry-run으로 변경 요약만 확인 (실제 dirty 반영 없음) ──
|
|
965
|
+
const dry = await grid.io.fromExcel(file, {
|
|
966
|
+
mode: 'upsert',
|
|
967
|
+
dryRun: true,
|
|
968
|
+
});
|
|
969
|
+
console.log(`예상 변경: +${dry.addedCount} ~${dry.updatedCount} -${dry.deletedCount}`);
|
|
970
|
+
|
|
971
|
+
// ── 8) Import 취소 ──
|
|
972
|
+
const abortImport = new AbortController();
|
|
973
|
+
setTimeout(() => abortImport.abort(), 5000); // 5초 후 취소
|
|
974
|
+
try {
|
|
975
|
+
await grid.io.fromExcel(file, { signal: abortImport.signal });
|
|
976
|
+
} catch (err) {
|
|
977
|
+
if (err.name === 'AbortError') console.log('Import 취소됨');
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ── 9) 이벤트 구독 ──
|
|
981
|
+
grid.on('io:import:complete', ({ rowCount, duration }) => {
|
|
982
|
+
console.log(`Import 완료: ${rowCount}행, ${duration.toFixed(0)}ms`);
|
|
983
|
+
});
|
|
984
|
+
grid.on('io:export:complete', ({ rowCount, fileSize }) => {
|
|
985
|
+
console.log(`Export 완료: ${rowCount}행, ${(fileSize / 1024 / 1024).toFixed(1)}MB`);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// ── 10) 피벗 모드 export (default) ──
|
|
989
|
+
// 기본값 `pivotExpandCollapsed: true`로 접힘 무시 + 다단계 컬럼 헤더 + rowMergeInfo 병합까지 복원.
|
|
990
|
+
// columnFields 2+level인 경우 `<mergeCells>`로 상위 헤더 colspan 병합됨.
|
|
991
|
+
await grid.io.toExcel({
|
|
992
|
+
includeHeaders: true,
|
|
993
|
+
// pivotExpandCollapsed: true, // default
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// ── 11) 피벗 "화면에 보이는 것만" export ──
|
|
997
|
+
// 현재 접힘 상태 그대로 export. row merge는 생략 (v1 안전 가드).
|
|
998
|
+
await grid.io.toExcel({
|
|
999
|
+
pivotExpandCollapsed: false,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// ── 12) 원본 데이터 export (모든 모드 공통) ──
|
|
1003
|
+
// `exportSource: true`는 flat/pivot/tree/grouping 어떤 모드에서든 동작.
|
|
1004
|
+
// `sourceColumns` 옵션으로 포함 범위 선택:
|
|
1005
|
+
|
|
1006
|
+
// 12-a) 원본의 모든 컬럼 (default) — 피벗에선 rowFields/columnFields 라벨 + 타입 추론 적용
|
|
1007
|
+
await grid.io.toExcel({
|
|
1008
|
+
exportSource: true,
|
|
1009
|
+
// sourceColumns: 'all', // default
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// 12-b) 피벗에 참여하는 필드만 — rowFields + columnFields + valueFields 교집합
|
|
1013
|
+
// 피벗 차원 + 집계 대상 컬럼만 필요할 때. flat 모드에서는 'all'과 동일.
|
|
1014
|
+
await grid.io.toExcel({
|
|
1015
|
+
exportSource: true,
|
|
1016
|
+
sourceColumns: 'used',
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// 12-c) 미배치 필드까지 header 라벨로 — sourceFields 카탈로그 주입
|
|
1020
|
+
// 피벗 모드는 배치된 필드(rowFields/columnFields)에만 header가 있어, 행/열/값에
|
|
1021
|
+
// 안 들어간 원본 필드는 raw id로 새어 나간다. sourceFields로 모든 원본 필드의
|
|
1022
|
+
// header/type을 시드하면 미배치 필드도 사람이 읽는 header로 export된다.
|
|
1023
|
+
// (피벗 rowFields/columnFields의 header가 카탈로그를 override, valueFields는 제외.
|
|
1024
|
+
// 미지정 시 originalConfig.fields로 폴백.)
|
|
1025
|
+
await grid.io.toExcel({
|
|
1026
|
+
exportSource: true,
|
|
1027
|
+
sourceColumns: 'all', // default
|
|
1028
|
+
sourceFields: [
|
|
1029
|
+
{ field: 'region', header: '지역', dataType: 'string' },
|
|
1030
|
+
{ field: 'amount', header: '매출액', dataType: 'number' },
|
|
1031
|
+
{ field: 'memo', header: '비고' }, // 피벗 미배치 필드도 header로
|
|
1032
|
+
],
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// ── 13) 플랫 셀 병합 (MergeStrategy) 보존 ──
|
|
1036
|
+
// grid.merge.setConfig({...})로 설정된 병합이 Excel <mergeCells>로 그대로 반영 (default true).
|
|
1037
|
+
await grid.io.toExcel({
|
|
1038
|
+
preserveMerges: true, // default
|
|
1039
|
+
});
|
|
1040
|
+
// 레거시 동작(평탄 export):
|
|
1041
|
+
await grid.io.toExcel({ preserveMerges: false });
|
|
1042
|
+
|
|
1043
|
+
// ── 14) 플랫 컬럼 그룹 다단계 헤더 export ──
|
|
1044
|
+
// `columnGroups: [{ header: '개인정보', children: [...] }]` 같은 트리가 다단계 행 + merge로 복원.
|
|
1045
|
+
// default `preserveColumnGroups: true`이므로 별도 옵션 없이 동작.
|
|
1046
|
+
await grid.io.toExcel({});
|
|
1047
|
+
|
|
1048
|
+
// ── 15) 플랫 그룹 행 3가지 모드 ──
|
|
1049
|
+
// 'include' (default): 접힘 무시, 모든 group header + data
|
|
1050
|
+
await grid.io.toExcel({ groupRowMode: 'include' });
|
|
1051
|
+
// 'visible': 현재 접힘 상태 반영 (collapsed 자식 제외)
|
|
1052
|
+
await grid.io.toExcel({ groupRowMode: 'visible' });
|
|
1053
|
+
// 'dataOnly': group header 행 제외, data 행만
|
|
1054
|
+
await grid.io.toExcel({ groupRowMode: 'dataOnly' });
|
|
1055
|
+
|
|
1056
|
+
// ── 16) 조합: columnGroups + grouping + merge 동시 ──
|
|
1057
|
+
// 세 기능은 직교 조합 가능. 헤더는 컬럼 그룹 다단계, 데이터는 group row 섞임,
|
|
1058
|
+
// 각 data 행의 cell merge는 스트리밍 감지로 누적.
|
|
1059
|
+
await grid.io.toExcel({
|
|
1060
|
+
preserveMerges: true,
|
|
1061
|
+
preserveColumnGroups: true,
|
|
1062
|
+
groupRowMode: 'include',
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// ── 17) 트리 그리드 export ──
|
|
1066
|
+
// 기본: 모든 노드 + Excel 네이티브 outline (+/- 버튼).
|
|
1067
|
+
await grid.io.toExcel({ treeRowMode: 'include', treeOutline: true });
|
|
1068
|
+
// 현재 접힘 상태 그대로 (collapsed 자식은 xlsx에 없음).
|
|
1069
|
+
await grid.io.toExcel({ treeRowMode: 'visible' });
|
|
1070
|
+
// outline 없이 평탄한 트리 행만 내보내고 싶다면:
|
|
1071
|
+
await grid.io.toExcel({ treeRowMode: 'include', treeOutline: false });
|
|
1072
|
+
// 원본 평탄 데이터(트리 구조 전혀 없이)만 추출:
|
|
1073
|
+
await grid.io.toExcel({ exportSource: true });
|
|
1074
|
+
|
|
1075
|
+
// ── 18) 그룹 행 + Excel outline ──
|
|
1076
|
+
// 기본: 그룹 행 + 데이터 행 + outline(+/- 버튼). groupOutline default true.
|
|
1077
|
+
await grid.io.toExcel({ groupRowMode: 'include' });
|
|
1078
|
+
// outline 없이 평탄 그룹 행만:
|
|
1079
|
+
await grid.io.toExcel({ groupRowMode: 'include', groupOutline: false });
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
### Reactive API 전체 사용 예제
|
|
1085
|
+
|
|
1086
|
+
#### grid.select() — GridState 주요 필드
|
|
1087
|
+
|
|
1088
|
+
```typescript
|
|
1089
|
+
// ── 데이터 & 컬럼 ──
|
|
1090
|
+
const columns = grid.select(s => s.columns);
|
|
1091
|
+
columns.subscribe(() => console.log('컬럼 정의 변경:', columns.getSnapshot()));
|
|
1092
|
+
|
|
1093
|
+
const columnStates = grid.select(s => s.columnStates);
|
|
1094
|
+
columnStates.subscribe(() => console.log('컬럼 UI 상태 변경:', columnStates.getSnapshot()));
|
|
1095
|
+
|
|
1096
|
+
// ── 행 수 ──
|
|
1097
|
+
const totalRowCount = grid.select(s => s.totalRowCount);
|
|
1098
|
+
totalRowCount.subscribe(() => console.log('전체 행 수:', totalRowCount.getSnapshot()));
|
|
1099
|
+
|
|
1100
|
+
const visibleRowCount = grid.select(s => s.visibleRowCount);
|
|
1101
|
+
visibleRowCount.subscribe(() => console.log('표시 행 수:', visibleRowCount.getSnapshot()));
|
|
1102
|
+
|
|
1103
|
+
// ── 뷰 상태 ──
|
|
1104
|
+
const viewState = grid.select(s => s.viewState);
|
|
1105
|
+
viewState.subscribe(() => console.log('뷰 상태 변경:', viewState.getSnapshot()));
|
|
1106
|
+
|
|
1107
|
+
// ── Feature 모듈 설정 (반응형 관측 계약) ──
|
|
1108
|
+
const pivotConfig = grid.select(s => s.pivotConfig);
|
|
1109
|
+
pivotConfig.subscribe(() => console.log('피벗 설정 변경:', pivotConfig.getSnapshot()));
|
|
1110
|
+
|
|
1111
|
+
const pivotMode = grid.select(s => s.pivotMode); // 'flat' | 'pivot'
|
|
1112
|
+
const treeConfig = grid.select(s => s.treeConfig);
|
|
1113
|
+
const masterDetailConfig = grid.select(s => s.masterDetailConfig);
|
|
1114
|
+
const groupConfig = grid.select(s => s.groupConfig);
|
|
1115
|
+
const dragDropConfig = grid.select(s => s.dragDropConfig);
|
|
1116
|
+
const mergeConfig = grid.select(s => s.mergeConfig);
|
|
1117
|
+
|
|
1118
|
+
// ── 변경 추적 ──
|
|
1119
|
+
const hasChanges = grid.select(s => s.hasChanges);
|
|
1120
|
+
hasChanges.subscribe(() => console.log('변경 여부:', hasChanges.getSnapshot()));
|
|
1121
|
+
|
|
1122
|
+
const changeVersion = grid.select(s => s.changeVersion);
|
|
1123
|
+
changeVersion.subscribe(() => console.log('변경 버전:', changeVersion.getSnapshot()));
|
|
1124
|
+
|
|
1125
|
+
// ── 비동기 상태 ──
|
|
1126
|
+
const activity = grid.select(s => s.activity);
|
|
1127
|
+
activity.subscribe(() => {
|
|
1128
|
+
const act = activity.getSnapshot();
|
|
1129
|
+
if (act === 'idle') console.log('대기 중');
|
|
1130
|
+
else console.log('작업 중:', act);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
const isSearching = grid.select(s => s.isSearching); // background 검색 진행 여부 (activity와 독립)
|
|
1134
|
+
isSearching.subscribe(() => console.log('검색 중:', isSearching.getSnapshot()));
|
|
1135
|
+
|
|
1136
|
+
// ── 선택 ──
|
|
1137
|
+
const selectedRowCount = grid.select(s => s.selectedRowCount);
|
|
1138
|
+
selectedRowCount.subscribe(() => console.log('선택 행 수:', selectedRowCount.getSnapshot()));
|
|
1139
|
+
|
|
1140
|
+
const selectedCellCount = grid.select(s => s.selectedCellCount);
|
|
1141
|
+
selectedCellCount.subscribe(() => console.log('선택 셀 수:', selectedCellCount.getSnapshot()));
|
|
1142
|
+
|
|
1143
|
+
// ── 편집 ──
|
|
1144
|
+
const isEditing = grid.select(s => s.isEditing);
|
|
1145
|
+
isEditing.subscribe(() => console.log('편집 중:', isEditing.getSnapshot()));
|
|
1146
|
+
|
|
1147
|
+
const editingCell = grid.select(s => s.editingCell);
|
|
1148
|
+
editingCell.subscribe(() => console.log('편집 셀:', editingCell.getSnapshot()));
|
|
1149
|
+
|
|
1150
|
+
// ── Undo/Redo ──
|
|
1151
|
+
const canUndo = grid.select(s => s.canUndo);
|
|
1152
|
+
canUndo.subscribe(() => console.log('Undo 가능:', canUndo.getSnapshot()));
|
|
1153
|
+
|
|
1154
|
+
const canRedo = grid.select(s => s.canRedo);
|
|
1155
|
+
canRedo.subscribe(() => console.log('Redo 가능:', canRedo.getSnapshot()));
|
|
1156
|
+
|
|
1157
|
+
// ── 검증 ──
|
|
1158
|
+
const hasValidationErrors = grid.select(s => s.hasValidationErrors);
|
|
1159
|
+
hasValidationErrors.subscribe(() => console.log('검증 에러:', hasValidationErrors.getSnapshot()));
|
|
1160
|
+
|
|
1161
|
+
const validationErrorCount = grid.select(s => s.validationErrorCount);
|
|
1162
|
+
validationErrorCount.subscribe(() => console.log('에러 수:', validationErrorCount.getSnapshot()));
|
|
1163
|
+
|
|
1164
|
+
// ── 스크롤 ──
|
|
1165
|
+
const scrollTop = grid.select(s => s.scrollTop);
|
|
1166
|
+
scrollTop.subscribe(() => console.log('스크롤:', scrollTop.getSnapshot()));
|
|
1167
|
+
|
|
1168
|
+
const visibleStartIndex = grid.select(s => s.visibleStartIndex);
|
|
1169
|
+
visibleStartIndex.subscribe(() => console.log('보이는 첫 행:', visibleStartIndex.getSnapshot()));
|
|
1170
|
+
|
|
1171
|
+
const visibleEndIndex = grid.select(s => s.visibleEndIndex);
|
|
1172
|
+
visibleEndIndex.subscribe(() => console.log('보이는 끝 행:', visibleEndIndex.getSnapshot()));
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
#### 파생 셀렉터 (복합 상태)
|
|
1176
|
+
|
|
1177
|
+
```typescript
|
|
1178
|
+
// 여러 상태를 조합한 파생 셀렉터
|
|
1179
|
+
const toolbar = grid.select(s => ({
|
|
1180
|
+
canSave: s.hasChanges && !s.hasValidationErrors,
|
|
1181
|
+
canUndo: s.canUndo,
|
|
1182
|
+
canRedo: s.canRedo,
|
|
1183
|
+
isBusy: s.activity !== 'idle',
|
|
1184
|
+
rowCount: s.visibleRowCount,
|
|
1185
|
+
selectedCount: s.selectedRowCount,
|
|
1186
|
+
}));
|
|
1187
|
+
|
|
1188
|
+
toolbar.subscribe(() => {
|
|
1189
|
+
const state = toolbar.getSnapshot();
|
|
1190
|
+
saveButton.disabled = !state.canSave;
|
|
1191
|
+
undoButton.disabled = !state.canUndo;
|
|
1192
|
+
redoButton.disabled = !state.canRedo;
|
|
1193
|
+
spinner.hidden = !state.isBusy;
|
|
1194
|
+
statusLabel.textContent = `${state.rowCount}건 (${state.selectedCount}건 선택)`;
|
|
1195
|
+
});
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
#### grid.rows.observeById() — ReactiveRow 전체
|
|
1199
|
+
|
|
1200
|
+
```typescript
|
|
1201
|
+
const row = grid.rows.observeById('row-1');
|
|
1202
|
+
|
|
1203
|
+
// ── 구독 ──
|
|
1204
|
+
const unsub = row.subscribe(() => {
|
|
1205
|
+
console.log('행 변경됨');
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// ── 속성 읽기 (getter) ── 총 11개
|
|
1209
|
+
console.log(row.id); // 'row-1'
|
|
1210
|
+
console.log(row.data); // { id: 'row-1', name: '홍길동', age: 30 }
|
|
1211
|
+
console.log(row.exists); // true
|
|
1212
|
+
console.log(row.state); // 'pristine' | 'added' | 'modified' | 'deleted'
|
|
1213
|
+
console.log(row.selected); // true/false
|
|
1214
|
+
console.log(row.readOnly); // true/false
|
|
1215
|
+
console.log(row.visible); // true/false
|
|
1216
|
+
console.log(row.pinned); // 'top' | 'bottom' | null
|
|
1217
|
+
console.log(row.cssClass); // 'highlight' | null
|
|
1218
|
+
console.log(row.height); // 40 | null
|
|
1219
|
+
console.log(row.changedFields); // ['name', 'age'] | null
|
|
1220
|
+
|
|
1221
|
+
// ── Subscribable 인터페이스 ──
|
|
1222
|
+
const snapshot = row.getSnapshot(); // Row | undefined
|
|
1223
|
+
|
|
1224
|
+
// ── 셀 접근 ──
|
|
1225
|
+
const nameCell = row.cell('name');
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
#### ReactiveCell 전체
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
const cell = grid.rows.observeById('row-1').cell('amount');
|
|
1232
|
+
|
|
1233
|
+
// ── 구독 ──
|
|
1234
|
+
const unsub = cell.subscribe(() => {
|
|
1235
|
+
console.log('셀 값 변경:', cell.getSnapshot());
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// ── 값 읽기 ──
|
|
1239
|
+
const value = cell.getSnapshot(); // CellValue
|
|
1240
|
+
|
|
1241
|
+
// ── 값 쓰기 ──
|
|
1242
|
+
cell.set(1500);
|
|
1243
|
+
cell.set('변경됨');
|
|
1244
|
+
cell.set(null);
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
#### React useSyncExternalStore 통합
|
|
1248
|
+
|
|
1249
|
+
```tsx
|
|
1250
|
+
import { useSyncExternalStore, useMemo } from 'react';
|
|
1251
|
+
|
|
1252
|
+
// GridState 구독 훅
|
|
1253
|
+
function useGridState<T>(grid: PureSheet, selector: (state: GridState) => T): T {
|
|
1254
|
+
const subscribable = useMemo(() => grid.select(selector), [grid, selector]);
|
|
1255
|
+
return useSyncExternalStore(subscribable.subscribe, subscribable.getSnapshot);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// 행 구독 훅
|
|
1259
|
+
function useRow(grid: PureSheet, rowId: string | number) {
|
|
1260
|
+
const row = useMemo(() => grid.rows.observeById(rowId), [grid, rowId]);
|
|
1261
|
+
return useSyncExternalStore(row.subscribe, row.getSnapshot);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// 셀 구독 훅
|
|
1265
|
+
function useCellValue(grid: PureSheet, rowId: string | number, field: string) {
|
|
1266
|
+
const cell = useMemo(() => grid.rows.observeById(rowId).cell(field), [grid, rowId, field]);
|
|
1267
|
+
return useSyncExternalStore(cell.subscribe, cell.getSnapshot);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// 사용 예
|
|
1271
|
+
function GridToolbar({ grid }) {
|
|
1272
|
+
const rowCount = useGridState(grid, s => s.visibleRowCount);
|
|
1273
|
+
const hasChanges = useGridState(grid, s => s.hasChanges);
|
|
1274
|
+
const canUndo = useGridState(grid, s => s.canUndo);
|
|
1275
|
+
const canRedo = useGridState(grid, s => s.canRedo);
|
|
1276
|
+
const isBusy = useGridState(grid, s => s.activity !== 'idle');
|
|
1277
|
+
const selectedCount = useGridState(grid, s => s.selectedRowCount);
|
|
1278
|
+
const isEditing = useGridState(grid, s => s.isEditing);
|
|
1279
|
+
const hasErrors = useGridState(grid, s => s.hasValidationErrors);
|
|
1280
|
+
const errorCount = useGridState(grid, s => s.validationErrorCount);
|
|
1281
|
+
|
|
1282
|
+
return (
|
|
1283
|
+
<div>
|
|
1284
|
+
<span>{rowCount}건 ({selectedCount}건 선택)</span>
|
|
1285
|
+
<button disabled={!canUndo} onClick={() => grid.undoManager.undo()}>Undo</button>
|
|
1286
|
+
<button disabled={!canRedo} onClick={() => grid.undoManager.redo()}>Redo</button>
|
|
1287
|
+
<button disabled={!hasChanges || hasErrors} onClick={() => grid.changes.commit()}>저장</button>
|
|
1288
|
+
{isBusy && <Spinner />}
|
|
1289
|
+
{isEditing && <span>편집 중</span>}
|
|
1290
|
+
{hasErrors && <span style={{ color: 'red' }}>{errorCount}개 에러</span>}
|
|
1291
|
+
</div>
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function RowStatus({ grid, rowId }) {
|
|
1296
|
+
const row = useRow(grid, rowId);
|
|
1297
|
+
if (!row) return null;
|
|
1298
|
+
return <span>{row.name} ({row.state})</span>;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function CellDisplay({ grid, rowId, field }) {
|
|
1302
|
+
const value = useCellValue(grid, rowId, field);
|
|
1303
|
+
return <span>{String(value)}</span>;
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
---
|
|
1308
|
+
|
|
1309
|
+
### 이벤트 전체 사용 예제
|
|
1310
|
+
|
|
1311
|
+
```typescript
|
|
1312
|
+
// ═══════════════════════════════════════════════════
|
|
1313
|
+
// 데이터 이벤트
|
|
1314
|
+
// ═══════════════════════════════════════════════════
|
|
1315
|
+
|
|
1316
|
+
grid.on('data:loaded', ({ rowCount, columnCount }) => {
|
|
1317
|
+
console.log(`데이터 로드 완료: ${rowCount}행, ${columnCount}열`);
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// ═══════════════════════════════════════════════════
|
|
1321
|
+
// 행 이벤트
|
|
1322
|
+
// ═══════════════════════════════════════════════════
|
|
1323
|
+
|
|
1324
|
+
grid.on('row:click', ({ row, rowIndex, event }) => {
|
|
1325
|
+
console.log(`행 클릭: index=${rowIndex}, id=${row.id}`);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
grid.on('row:dblclick', ({ row, rowIndex, event }) => {
|
|
1329
|
+
console.log(`행 더블클릭: index=${rowIndex}`);
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
grid.on('row:pinned', ({ row, position }) => {
|
|
1333
|
+
console.log(`행 고정: ${row.id} → ${position}`); // 'top' | 'bottom'
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
grid.on('row:unpinned', ({ rowId, position }) => {
|
|
1337
|
+
console.log(`행 고정 해제: ${rowId}`);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
grid.on('row:reordered', (e) => {
|
|
1341
|
+
// discriminated union — flat 그리드 vs 트리 그리드
|
|
1342
|
+
if (e.kind === 'flat') {
|
|
1343
|
+
console.log(`행 재정렬: ${e.fromIndex} → ${e.toIndex}`);
|
|
1344
|
+
} else {
|
|
1345
|
+
// kind === 'tree' — 트리 노드 재정렬
|
|
1346
|
+
console.log(`트리 이동: ${e.patches.length}개, dropMode=${e.dropMode}`);
|
|
1347
|
+
}
|
|
1348
|
+
// e.commandId(= BatchCommand.id)는 undoManager.undoThrough(commandId) 롤백 키
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// ═══════════════════════════════════════════════════
|
|
1352
|
+
// 셀 이벤트
|
|
1353
|
+
// ═══════════════════════════════════════════════════
|
|
1354
|
+
|
|
1355
|
+
grid.on('cell:click', ({ row, columnId, value, event }) => {
|
|
1356
|
+
console.log(`셀 클릭: [${row.id}].${columnId} = ${value}`);
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
grid.on('cell:dblclick', ({ row, columnId, value, event }) => {
|
|
1360
|
+
console.log(`셀 더블클릭: [${row.id}].${columnId} = ${value}`);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
grid.on('cell:change', ({ rowIndex, columnId, oldValue, newValue }) => {
|
|
1364
|
+
console.log(`셀 변경: row[${rowIndex}].${columnId}: ${oldValue} → ${newValue}`);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
grid.on('cell:beforeEdit', (payload) => {
|
|
1368
|
+
// 편집 시작 전 — cancel()로 취소 가능
|
|
1369
|
+
if (payload.columnId === 'locked_field') {
|
|
1370
|
+
payload.cancel();
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
grid.on('cell:editCommitting', (payload) => {
|
|
1375
|
+
// 편집 커밋 전 — cancel()로 취소 가능
|
|
1376
|
+
if (payload.newValue === '') {
|
|
1377
|
+
payload.cancel(); // 빈 값 커밋 방지
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
grid.on('cell:editCommitted', ({ rowIndex, dataIndex, columnId, oldValue, newValue }) => {
|
|
1382
|
+
console.log(`편집 완료: row[${rowIndex}].${columnId}: ${oldValue} → ${newValue}`);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
grid.on('cell:tooltip', (payload) => {
|
|
1386
|
+
// 셀·컬럼 헤더 공통 이벤트 — payload.area ('cell' | 'columnHeader') 로 구분.
|
|
1387
|
+
// payload.tooltipText: ColumnDef.headerTooltip/cellTooltip 로 지정한 커스텀 문구(있으면 우선).
|
|
1388
|
+
console.log('툴팁:', payload.area, payload.tooltipText ?? payload.formattedValue);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// ═══════════════════════════════════════════════════
|
|
1392
|
+
// 선택 이벤트
|
|
1393
|
+
// ═══════════════════════════════════════════════════
|
|
1394
|
+
|
|
1395
|
+
grid.on('selection:changed', ({ selectedRows, selectedCells }) => {
|
|
1396
|
+
console.log(`선택 변경: ${selectedRows.length}행, ${selectedCells.length}셀`);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
// ═══════════════════════════════════════════════════
|
|
1400
|
+
// 컬럼 이벤트
|
|
1401
|
+
// ═══════════════════════════════════════════════════
|
|
1402
|
+
|
|
1403
|
+
grid.on('column:resize', ({ columnId, width }) => {
|
|
1404
|
+
console.log(`컬럼 리사이즈: ${columnId} → ${width}px`);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
grid.on('column:reorder', ({ order, source, sourceId }) => {
|
|
1408
|
+
console.log(`컬럼 순서 변경 (${source}):`, order);
|
|
1409
|
+
// source: 'order' | 'column' | 'group' | 'groupOrder'
|
|
1410
|
+
// sourceId: move/moveGroup 시 이동된 컬럼/그룹 ID
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
grid.on('column:pin', ({ columnId, position }) => {
|
|
1414
|
+
console.log(`컬럼 고정: ${columnId} → ${position}`); // 'left' | 'right' | 'none'
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
grid.on('column:visibility', ({ columnId, visible }) => {
|
|
1418
|
+
console.log(`컬럼 가시성: ${columnId} → ${visible}`);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
// ═══════════════════════════════════════════════════
|
|
1422
|
+
// 정렬/필터 이벤트
|
|
1423
|
+
// ═══════════════════════════════════════════════════
|
|
1424
|
+
|
|
1425
|
+
grid.on('sort:changed', ({ sorts }) => {
|
|
1426
|
+
console.log('정렬 변경:', sorts);
|
|
1427
|
+
// → [{ id: 'name', direction: 'asc', sequence: 1 }]
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
grid.on('filter:changed', ({ filters }) => {
|
|
1431
|
+
console.log('필터 변경:', filters);
|
|
1432
|
+
// → [{ id: 'age', operator: 'gte', filterValue: 30, sequence: 1 }]
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
// 헤더 클릭 = 정렬 정책은 소비처가 소유한다.
|
|
1436
|
+
// core는 header:click만 발행하고 자동 정렬하지 않으므로,
|
|
1437
|
+
// 클릭-정렬을 원하면 직접 toggleSort()를 호출한다.
|
|
1438
|
+
grid.on('header:click', ({ columnId, event }) => {
|
|
1439
|
+
// event는 원본 DOM MouseEvent — 모디파이어를 직접 읽는다.
|
|
1440
|
+
// shift+클릭이면 기존 정렬에 추가(다중 정렬), 아니면 단일 정렬.
|
|
1441
|
+
grid.toggleSort(columnId, { additive: event.shiftKey });
|
|
1442
|
+
// 사이클: none → asc → desc → none (sortable:false 컬럼은 무시)
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
// 헤더 아이콘 클릭은 셀 본체 클릭과 **분리**된다 (ITSM-2026-002185).
|
|
1446
|
+
// 아이콘 클릭은 stopPropagation 되므로 header:click 과 동시에 발생하지 않는다.
|
|
1447
|
+
grid.on('header:sortClick', ({ columnId, element }) => {
|
|
1448
|
+
openSortPanel(element, columnId); // element = 아이콘 DOM (팝오버 앵커)
|
|
1449
|
+
});
|
|
1450
|
+
grid.on('header:filterClick', ({ columnId, element }) => {
|
|
1451
|
+
openFilterPopover(element, columnId);
|
|
1452
|
+
// 필드에 showFilterIcon:false 면 아이콘 DOM 자체가 없어 이 이벤트도 발생하지 않는다.
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// ═══════════════════════════════════════════════════
|
|
1456
|
+
// Undo/Redo 이벤트
|
|
1457
|
+
// ═══════════════════════════════════════════════════
|
|
1458
|
+
|
|
1459
|
+
grid.on('undo:stateChange', (state) => {
|
|
1460
|
+
console.log('Undo 상태:', state); // { canUndo, canRedo } — 버튼 활성화 토글용
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// changes:undone / changes:redone — undo/redo 완료 시 구조화된 변경 payload.
|
|
1464
|
+
// ⚠️ row:reordered만 구독하면 undo/redo 시 백엔드와 어긋난다 (row:reordered는 정방향만 발화).
|
|
1465
|
+
// ⚠️ 트리/플랫 reorder는 항상 kind:'batch'로 감싸져 오므로, iterateLeafChanges로 평탄화한 뒤 분기.
|
|
1466
|
+
import { iterateLeafChanges } from '@solasheet/core';
|
|
1467
|
+
|
|
1468
|
+
function syncChange(rootChange, direction) {
|
|
1469
|
+
// children은 이미 direction-ordered → 헬퍼 방출 순서 그대로 순차 적용 (재-reverse 금지)
|
|
1470
|
+
for (const leaf of iterateLeafChanges(rootChange)) {
|
|
1471
|
+
switch (leaf.kind) {
|
|
1472
|
+
case 'cell-edit':
|
|
1473
|
+
api.patch(leaf.rowId, { [leaf.field]: direction === 'undo' ? leaf.oldValue : leaf.newValue });
|
|
1474
|
+
break;
|
|
1475
|
+
case 'tree-reorder':
|
|
1476
|
+
for (const p of leaf.patches) {
|
|
1477
|
+
api.setParent(p.rowId, direction === 'undo' ? p.oldParentId : p.newParentId);
|
|
1478
|
+
}
|
|
1479
|
+
for (const [parentId, order] of leaf.siblingsAfterFinal) api.reorderChildren(parentId, order);
|
|
1480
|
+
break;
|
|
1481
|
+
// 'flat-move' / 'add-row' / 'delete-row' 등도 동일 패턴
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
grid.on('changes:undone', (e) => syncChange(e.change, 'undo'));
|
|
1486
|
+
grid.on('changes:redone', (e) => syncChange(e.change, 'redo'));
|
|
1487
|
+
|
|
1488
|
+
// ═══════════════════════════════════════════════════
|
|
1489
|
+
// 클립보드 이벤트
|
|
1490
|
+
// ═══════════════════════════════════════════════════
|
|
1491
|
+
|
|
1492
|
+
grid.on('clipboard:copy', ({ text, rowCount, colCount }) => {
|
|
1493
|
+
console.log(`복사: ${rowCount}행 × ${colCount}열`);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
grid.on('clipboard:paste', ({ cellCount }) => {
|
|
1497
|
+
console.log(`붙여넣기: ${cellCount}셀`);
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
// ═══════════════════════════════════════════════════
|
|
1501
|
+
// 검증 이벤트
|
|
1502
|
+
// ═══════════════════════════════════════════════════
|
|
1503
|
+
|
|
1504
|
+
grid.on('validation:changed', (payload) => {
|
|
1505
|
+
console.log('검증 상태 변경:', payload);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
// ═══════════════════════════════════════════════════
|
|
1509
|
+
// 컬럼 그룹 이벤트
|
|
1510
|
+
// ═══════════════════════════════════════════════════
|
|
1511
|
+
|
|
1512
|
+
grid.on('columnGroup:toggle', ({ groupId, collapsed }) => {
|
|
1513
|
+
console.log(`컬럼 그룹: ${groupId} → ${collapsed ? '접힘' : '펼침'}`);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// ═══════════════════════════════════════════════════
|
|
1517
|
+
// 컨텍스트 메뉴 이벤트
|
|
1518
|
+
// ═══════════════════════════════════════════════════
|
|
1519
|
+
|
|
1520
|
+
grid.on('contextmenu', ({
|
|
1521
|
+
area, // 'cell' | 'header' | 'rowHeader' | 'empty'
|
|
1522
|
+
rowIndex, // 행 인덱스 (cell 영역)
|
|
1523
|
+
dataIndex, // 데이터 인덱스
|
|
1524
|
+
rowId, // 행 ID
|
|
1525
|
+
columnId, // 컬럼 ID
|
|
1526
|
+
value, // 셀 값
|
|
1527
|
+
selectedRowIds, // 현재 선택된 행 ID 배열
|
|
1528
|
+
selectedCells, // 현재 선택된 셀 배열
|
|
1529
|
+
originalEvent, // 원본 MouseEvent
|
|
1530
|
+
}) => {
|
|
1531
|
+
originalEvent.preventDefault();
|
|
1532
|
+
showContextMenu(originalEvent.clientX, originalEvent.clientY, {
|
|
1533
|
+
area, rowId, columnId, value, selectedRowIds, selectedCells,
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
// ═══════════════════════════════════════════════════
|
|
1538
|
+
// 이벤트 구독 해제
|
|
1539
|
+
// ═══════════════════════════════════════════════════
|
|
1540
|
+
|
|
1541
|
+
// 방법 1: on() 반환값 사용
|
|
1542
|
+
const unsubscribe = grid.on('cell:click', handler);
|
|
1543
|
+
unsubscribe();
|
|
1544
|
+
|
|
1545
|
+
// 방법 2: off() 사용
|
|
1546
|
+
grid.off('cell:click', handler);
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
---
|
|
1550
|
+
|
|
1551
|
+
### 정렬/필터 사용 예제
|
|
1552
|
+
|
|
1553
|
+
```typescript
|
|
1554
|
+
// ── 정렬 (SortState[] — id/direction/sequence) ──
|
|
1555
|
+
await grid.sort([{ id: 'name', direction: 'asc', sequence: 1 }]);
|
|
1556
|
+
await grid.sort([
|
|
1557
|
+
{ id: 'age', direction: 'desc', sequence: 1 },
|
|
1558
|
+
{ id: 'name', direction: 'asc', sequence: 2 }, // 다중 정렬
|
|
1559
|
+
]);
|
|
1560
|
+
|
|
1561
|
+
// ── 조건식 필터 (FilterState[] — id/operator/filterValue/sequence) ──
|
|
1562
|
+
await grid.filter([
|
|
1563
|
+
{ id: 'age', operator: 'gte', filterValue: 30, sequence: 1 },
|
|
1564
|
+
{ id: 'name', operator: 'contains', filterValue: '김', sequence: 2 },
|
|
1565
|
+
]);
|
|
1566
|
+
|
|
1567
|
+
// OR 조건 (logical: 'or')
|
|
1568
|
+
await grid.filter([
|
|
1569
|
+
{ id: 'status', operator: 'eq', filterValue: 'active', sequence: 1 },
|
|
1570
|
+
{ id: 'status', operator: 'eq', filterValue: 'pending', sequence: 2, logical: 'or' },
|
|
1571
|
+
]);
|
|
1572
|
+
|
|
1573
|
+
// ── 값 집합 필터 (엑셀식 체크박스 — filterValue 가 **배열**) ──
|
|
1574
|
+
await grid.filter([
|
|
1575
|
+
{ id: 'dept', operator: 'in', filterValue: ['영업', '개발'], sequence: 1 },
|
|
1576
|
+
]);
|
|
1577
|
+
// notIn = 제외 방향 (exclude 모드가 만드는 형태)
|
|
1578
|
+
await grid.filter([
|
|
1579
|
+
{ id: 'dept', operator: 'notIn', filterValue: ['임시'], sequence: 1 },
|
|
1580
|
+
]);
|
|
1581
|
+
|
|
1582
|
+
// ── 수식 필터 (행 전체를 식 하나로 판정 — 괄호·컬럼 간 비교 가능) ──
|
|
1583
|
+
await grid.filter([{
|
|
1584
|
+
id: '__formula__', sequence: 1, operator: 'formula',
|
|
1585
|
+
expression: '([급여] > 50000 AND [부서] == "Engineering") OR [도시] == "서울"',
|
|
1586
|
+
}]);
|
|
1587
|
+
|
|
1588
|
+
// ── 뷰 상태 저장/복원 ──
|
|
1589
|
+
const viewState = grid.getViewState(); // { sorts, filters, filterGroups }
|
|
1590
|
+
await grid.setViewState(viewState);
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
> ⚠️ `FilterState` 는 `operator` 로 갈라지는 **판별 유니온**입니다. `in`/`notIn` 에 스칼라를 넣거나 `formula` 에 `expression` 을 빠뜨리면 런타임에서 **조용히 전 행 통과**가 됩니다 — 타입이 이를 컴파일 시점에 막습니다. → [reference.md SortState / FilterState](reference.md#sortstate--filterstate)
|
|
1594
|
+
|
|
1595
|
+
---
|
|
1596
|
+
|
|
1597
|
+
### 필터 그룹 사용 예제
|
|
1598
|
+
|
|
1599
|
+
여러 UI(툴바 필터 창, 컬럼 헤더 팝오버)가 **같은 필터 배열을 공유하지 않도록** 소스별로 그룹을 나눕니다.
|
|
1600
|
+
|
|
1601
|
+
```typescript
|
|
1602
|
+
import type { FilterGroupMap } from '@vmscloud/moz-ui-grid-core';
|
|
1603
|
+
|
|
1604
|
+
// 그룹 맵으로 한 번에 적용 — 그룹 간은 항상 AND, 그룹 안은 각 항목의 logical
|
|
1605
|
+
const groups: FilterGroupMap = {
|
|
1606
|
+
toolbar: {
|
|
1607
|
+
type: 'conditional', priority: 0, applied: true,
|
|
1608
|
+
states: [{ id: 'dept', operator: 'eq', filterValue: 'Sales', sequence: 0 }],
|
|
1609
|
+
},
|
|
1610
|
+
'headerPopover:name': {
|
|
1611
|
+
type: 'values', priority: 1, applied: true,
|
|
1612
|
+
states: [{ id: 'name', operator: 'in', filterValue: ['강호동', '김태희'], sequence: 0 }],
|
|
1613
|
+
},
|
|
1614
|
+
};
|
|
1615
|
+
await grid.filter(groups);
|
|
1616
|
+
// → (dept = Sales) AND (name ∈ {강호동, 김태희})
|
|
1617
|
+
|
|
1618
|
+
// 그룹 하나만 교체 — 다른 그룹은 건드리지 않는다
|
|
1619
|
+
await grid.setFilterGroup('headerPopover:name', {
|
|
1620
|
+
type: 'values', priority: 1, applied: true,
|
|
1621
|
+
states: [{ id: 'name', operator: 'in', filterValue: ['강호동'], sequence: 0 }],
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
// 그룹 삭제 (팝오버에서 "지우기")
|
|
1625
|
+
await grid.setFilterGroup('headerPopover:name', null);
|
|
1626
|
+
|
|
1627
|
+
// 임시로 끄기 — 상태는 남기고 필터링에만 미참여
|
|
1628
|
+
const g = grid.getFilterGroup('toolbar');
|
|
1629
|
+
if (g) await grid.setFilterGroup('toolbar', { ...g, applied: false });
|
|
1630
|
+
|
|
1631
|
+
// 전체 조회
|
|
1632
|
+
const all = grid.getFilterGroups();
|
|
1633
|
+
const flat = grid.getViewState().filters; // 평탄화된 파생값 (엔진이 읽는 형태)
|
|
1634
|
+
```
|
|
1635
|
+
|
|
1636
|
+
> **그룹 경계 = 괄호.** 평면 배열 하나로 두면 좌→우 fold 가 그룹을 넘나들어, 뒤 그룹의 OR 이 앞 그룹 조건을 통째로 무력화합니다(실측: `dept=Sales` + `name∈{강호동,김태희}` 에서 HR/김태희 누출). 평탄화 시 `groupKey` 가 자동 부착되어 이를 막습니다.
|
|
1637
|
+
>
|
|
1638
|
+
> 컬럼 간 OR 은 그룹으로 표현할 수 없습니다(그룹 간 AND 고정) — **수식 그룹**(`type: 'formula'`)이 담당합니다.
|
|
1639
|
+
|
|
1640
|
+
---
|
|
1641
|
+
|
|
1642
|
+
### 값 목록 필터 사용 예제 (엑셀식 체크박스)
|
|
1643
|
+
|
|
1644
|
+
```typescript
|
|
1645
|
+
import { ValueCheckList, DEFAULT_VALUE_LIST_LIMIT } from '@vmscloud/moz-ui-grid-core';
|
|
1646
|
+
|
|
1647
|
+
const GROUP_KEY = 'headerPopover:dept';
|
|
1648
|
+
let selectedKeys: string[] = [];
|
|
1649
|
+
|
|
1650
|
+
const list = new ValueCheckList(popoverBody, {
|
|
1651
|
+
mode: 'exclude', // 전체 체크로 시작 → notIn 저장
|
|
1652
|
+
onSelectionChange: (keys) => { selectedKeys = keys; },
|
|
1653
|
+
// 검색을 소비처가 소유 — 상한 밖 값은 로컬 필터링으로 못 찾으므로 엔진에 재조회한다
|
|
1654
|
+
onKeywordChange: async (keyword) => {
|
|
1655
|
+
const r = await grid.getFilterValues('dept', { keyword });
|
|
1656
|
+
list.setValues(r.values, selectedKeys, { truncated: r.truncated, rawValues: r.rawValues });
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// 1) 후보 수집 — 자기 컬럼의 값 목록 조건(in/notIn)만 제외한 나머지 필터가 적용된다
|
|
1661
|
+
list.setLoading(true);
|
|
1662
|
+
const r = await grid.getFilterValues('dept', { limit: DEFAULT_VALUE_LIST_LIMIT });
|
|
1663
|
+
list.setValues(r.values, undefined, {
|
|
1664
|
+
truncated: r.truncated, // true 면 위젯이 "검색으로 좁혀 주세요" 안내
|
|
1665
|
+
limit: DEFAULT_VALUE_LIST_LIMIT,
|
|
1666
|
+
rawValues: r.rawValues, // 필터에 실을 원본값 (표시값 아님)
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
// 2) [적용] 클릭 — 선택 항목을 원본값으로 필터에 싣는다
|
|
1670
|
+
const picked = list.getSelectedItems();
|
|
1671
|
+
await grid.setFilterGroup(GROUP_KEY, {
|
|
1672
|
+
type: 'values', priority: 1, applied: true,
|
|
1673
|
+
states: [{ id: 'dept', operator: 'notIn', sequence: 0,
|
|
1674
|
+
filterValue: picked.map((i) => i.value) }],
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// 3) 팝오버를 닫을 때
|
|
1678
|
+
list.destroy();
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
> **표시값과 원본값을 구분하세요.** `values` 는 mask 가 적용된 **표시 라벨**(검색·필터가 표시값 기준이므로 목록도 같아야 합니다), `rawValues` 는 **필터에 실을 원본값**입니다. 표시값으로 필터링하면 5M 행에서 30.6배 느리고 함수형 mask 는 Worker 복제가 안 됩니다.
|
|
1682
|
+
>
|
|
1683
|
+
> 원본값 하나를 화면 표기로 되돌릴 때는 `grid.formatColumnValue(columnId, raw)` 를 쓰세요 — mask 해석 규칙은 core 가 소유하므로 소비처가 재현하면 어긋납니다.
|
|
1684
|
+
>
|
|
1685
|
+
> **팝오버 재오픈 시** 저장된 필터에서 "현재 후보 목록에 없는 지목"을 추려 `setValues(..., { offListPicks })` 로 넘겨야, 상한 밖 값이 화면에서 사라지지 않습니다.
|
|
1686
|
+
|
|
1687
|
+
---
|
|
1688
|
+
|
|
1689
|
+
### 헤더 아이콘 이벤트 사용 예제
|
|
1690
|
+
|
|
1691
|
+
헤더의 **셀 본체 / 정렬 아이콘 / 필터 아이콘** 클릭은 서로 배타적인 세 이벤트로 분리됩니다 (ITSM-2026-002185).
|
|
1692
|
+
|
|
1693
|
+
```typescript
|
|
1694
|
+
// 셀 본체 클릭 = 정렬 토글 (core 는 자동 정렬하지 않는다 — 정책은 소비처 소유)
|
|
1695
|
+
grid.on('header:click', ({ columnId, event }) => {
|
|
1696
|
+
grid.toggleSort(columnId, { additive: event.shiftKey });
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
// 정렬 아이콘 클릭 = 정렬 패널 열기 (element 를 앵커로)
|
|
1700
|
+
grid.on('header:sortClick', ({ columnId, element }) => {
|
|
1701
|
+
openSortPanel(element, columnId);
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
// 필터 아이콘 클릭 = 값 목록 팝오버 열기
|
|
1705
|
+
grid.on('header:filterClick', ({ columnId, element }) => {
|
|
1706
|
+
openValueListPopover(element, columnId);
|
|
1707
|
+
});
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
> 아이콘 클릭은 `HeaderCell` 에서 `stopPropagation` 되므로 `header:click` 과 **동시에 발생하지 않습니다**.
|
|
1711
|
+
>
|
|
1712
|
+
> 필드에 `showFilterIcon: false` 를 주면 아이콘 span 자체가 생성되지 않아 그 컬럼에서는 `header:filterClick` 이 **발생하지 않습니다**. 이것은 표시 축일 뿐이며 툴바 필터 창·수식 등 다른 경로로는 여전히 조건을 걸고 해제할 수 있습니다.
|
|
1713
|
+
|
|
1714
|
+
---
|
|
1715
|
+
|
|
1716
|
+
### 저장본 마이그레이션 사용 예제
|
|
1717
|
+
|
|
1718
|
+
```typescript
|
|
1719
|
+
import {
|
|
1720
|
+
GRID_CONFIG_VERSION, compareConfigVersion,
|
|
1721
|
+
migrateFormulaToStorage, conditionFiltersToFormulaState,
|
|
1722
|
+
} from '@vmscloud/moz-ui-grid-core';
|
|
1723
|
+
|
|
1724
|
+
const grid = new PureSheet(el, {
|
|
1725
|
+
fields,
|
|
1726
|
+
// 옛 저장본을 현재 스키마로 올린다. core 는 호출 시점·순서만 책임지고,
|
|
1727
|
+
// 어떤 버전에서 무엇을 바꿀지는 스키마를 아는 소비처가 정한다.
|
|
1728
|
+
migrateConfig: (cfg, fromVersion) => {
|
|
1729
|
+
let out = cfg;
|
|
1730
|
+
// 0.1 → 0.2: 수식 컬럼 참조를 [라벨] → $id 로
|
|
1731
|
+
if (compareConfigVersion(fromVersion, '0.2') < 0) out = to02(out);
|
|
1732
|
+
// 0.2 → 0.3: 소유자 없는 옛 조건식을 고급 필터 수식 그룹으로
|
|
1733
|
+
if (compareConfigVersion(fromVersion, '0.3') < 0) out = to03(out);
|
|
1734
|
+
return { ...out, version: GRID_CONFIG_VERSION };
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
// 저장 — getConfig() 가 항상 현재 버전을 실어 준다
|
|
1739
|
+
const saved = grid.getConfig(); // { version: '0.3', current, snapshot }
|
|
1740
|
+
localStorage.setItem('layout', JSON.stringify(saved));
|
|
1741
|
+
|
|
1742
|
+
// 복원 — version 이 없으면 '0.1' 로 간주하고 체인을 처음부터 태운다
|
|
1743
|
+
await grid.applyConfig(JSON.parse(localStorage.getItem('layout')!));
|
|
1744
|
+
```
|
|
1745
|
+
|
|
1746
|
+
> **마이그레이션은 누적 적용입니다.** `0.1` 저장본이면 `0.1 → 0.2 → 0.3` 을 순서대로 전부 거칩니다. 최신 변환만 돌리면 중간 단계를 건너뛰어 조용히 어긋납니다.
|
|
1747
|
+
>
|
|
1748
|
+
> 변환 중 예외가 나도 복원 자체는 막지 않고 경고(`PS901`)만 남깁니다 — 옛 저장본이라도 최대한 살립니다.
|
|
1749
|
+
|
|
1750
|
+
---
|
|
1751
|
+
|
|
1752
|
+
### 선택 모드 사용 예제
|
|
1753
|
+
|
|
1754
|
+
moz-ui-grid-core는 행 선택과 셀 선택을 **독립된 두 옵션**(`rowSelection` + `cellSelection`)으로 분리합니다.
|
|
1755
|
+
하나의 `selectionMode` 옵션이 아니라 각 도메인을 따로 설정하므로, 행 식별(identity) 모델과
|
|
1756
|
+
좌표(position) 모델이 정합성 보호 하에 공존할 수 있습니다.
|
|
1757
|
+
|
|
1758
|
+
- **`rowSelection`**: `rowId` 기반 (정렬/필터/dirty 변경에도 같은 식별자 추종)
|
|
1759
|
+
- **`cellSelection`**: `viewIndex` 기반 (자리 추종 — Excel 모델)
|
|
1760
|
+
|
|
1761
|
+
둘 다 미지정 시 기본은 `cellSelection: { mode: 'range' }` (셀 전용 Excel 스타일).
|
|
1762
|
+
|
|
1763
|
+
> 각 옵션의 타입 정의는 [reference.md → RowSelectionConfig/CellSelectionConfig](reference.md#열거형유니온-타입) 참조.
|
|
1764
|
+
|
|
1765
|
+
#### `RowSelectionConfig` 필드
|
|
1766
|
+
|
|
1767
|
+
| 필드 | 타입 | 기본 | 의미 |
|
|
1768
|
+
|---|---|---|---|
|
|
1769
|
+
| `mode` | `'none' \| 'single' \| 'multi'` | `'none'` | 행 선택 모드 |
|
|
1770
|
+
| `trigger` | `'cellClick' \| 'rowHeader' \| 'checkbox' \| Array<...>` | 자동 결정(아래 표 참조) | 어느 입력 이벤트가 행 선택을 발화시키는지 |
|
|
1771
|
+
| `includeInSelectAll` | `boolean` | `false` | **Ctrl+A 시 행도 함께 fill 할지** — 아래 섹션 참조 |
|
|
1772
|
+
|
|
1773
|
+
#### `CellSelectionConfig` 필드
|
|
1774
|
+
|
|
1775
|
+
| 필드 | 타입 | 기본 | 의미 |
|
|
1776
|
+
|---|---|---|---|
|
|
1777
|
+
| `mode` | `'none' \| 'single' \| 'range'` | (둘 다 미지정 시 `'range'`) | 셀 선택 모드 |
|
|
1778
|
+
|
|
1779
|
+
#### 분리 API 예제
|
|
1780
|
+
|
|
1781
|
+
```typescript
|
|
1782
|
+
// ── 행 전용 (단순 list 그리드) ──
|
|
1783
|
+
new PureSheet(container, {
|
|
1784
|
+
rowSelection: { mode: 'multi' },
|
|
1785
|
+
});
|
|
1786
|
+
// trigger 미지정 → 자동 'cellClick' (셀 클릭으로 행 선택)
|
|
1787
|
+
|
|
1788
|
+
// ── 셀 전용 (Excel 스타일) ──
|
|
1789
|
+
new PureSheet(container, {
|
|
1790
|
+
cellSelection: { mode: 'range' },
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
// ── 셀+행 분리: 행 헤더 클릭으로 행 선택 (정합성 보호) ──
|
|
1794
|
+
new PureSheet(container, {
|
|
1795
|
+
rowSelection: { mode: 'multi' }, // trigger 자동 = 'rowHeader'
|
|
1796
|
+
cellSelection: { mode: 'range' },
|
|
1797
|
+
});
|
|
1798
|
+
// _rowHeader 컬럼 자동 주입. 셀 클릭은 셀만 선택 → 행/셀 독립.
|
|
1799
|
+
|
|
1800
|
+
// ── 체크박스 트리거 ──
|
|
1801
|
+
new PureSheet(container, {
|
|
1802
|
+
rowSelection: { mode: 'multi', trigger: 'checkbox' },
|
|
1803
|
+
});
|
|
1804
|
+
// _rowHeader 컬럼 자동 주입 + CSS-only 체크박스 자동 렌더.
|
|
1805
|
+
|
|
1806
|
+
// ── 트리거 복수 지정 (행 헤더 + 체크박스) ──
|
|
1807
|
+
new PureSheet(container, {
|
|
1808
|
+
rowSelection: { mode: 'multi', trigger: ['rowHeader', 'checkbox'] },
|
|
1809
|
+
cellSelection: { mode: 'range' },
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// ── 셀 클릭이 행도 선택 (legacy 'all' 동작) ──
|
|
1813
|
+
new PureSheet(container, {
|
|
1814
|
+
rowSelection: { mode: 'multi', trigger: 'cellClick' }, // 명시 지정
|
|
1815
|
+
cellSelection: { mode: 'range' },
|
|
1816
|
+
});
|
|
1817
|
+
// 셀 mutation이 행 selection을 자동 동기화 (drag/click 한정).
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
#### `rowSelection.trigger` 자동 결정
|
|
1821
|
+
|
|
1822
|
+
| `rowSelection.mode` | `cellSelection.mode` | `trigger` 자동값 | 의미 |
|
|
1823
|
+
|---|---|---|---|
|
|
1824
|
+
| 활성 | 미지정/`'none'` | `'cellClick'` | 셀 클릭으로 행 선택 (단순 list 그리드) |
|
|
1825
|
+
| 활성 | 활성 | `'rowHeader'` | 정합성 보호 — 셀과 행 독립 (수동 명시로 `'cellClick'` 가능) |
|
|
1826
|
+
|
|
1827
|
+
#### `rowSelection.includeInSelectAll` — Ctrl+A 동작 제어
|
|
1828
|
+
|
|
1829
|
+
`Ctrl+A`(전체 선택) 시점에 활성된 selection 도메인을 어떻게 채울지 제어합니다.
|
|
1830
|
+
AG Grid의 `ctrlASelectsRows` 옵션과 같은 의미.
|
|
1831
|
+
|
|
1832
|
+
| `cellSelection` | `rowSelection` | `includeInSelectAll` | Ctrl+A 결과 |
|
|
1833
|
+
|---|---|---|---|
|
|
1834
|
+
| 활성 | 비활성 | (무시) | 모든 셀 |
|
|
1835
|
+
| 비활성 | 활성 | (무시) | 모든 행 |
|
|
1836
|
+
| 활성 | 활성 | `false` (기본) | 모든 셀만 — 행은 그대로 |
|
|
1837
|
+
| 활성 | 활성 | `true` | 모든 셀 + 모든 행 |
|
|
1838
|
+
|
|
1839
|
+
```typescript
|
|
1840
|
+
// 기본 — 셀만 전체 선택 (AG Grid와 동일)
|
|
1841
|
+
new PureSheet(container, {
|
|
1842
|
+
rowSelection: { mode: 'multi' },
|
|
1843
|
+
cellSelection: { mode: 'range' },
|
|
1844
|
+
// rowSelection.includeInSelectAll: false (기본)
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
// Ctrl+A로 행도 함께 fill
|
|
1848
|
+
new PureSheet(container, {
|
|
1849
|
+
rowSelection: { mode: 'multi', includeInSelectAll: true },
|
|
1850
|
+
cellSelection: { mode: 'range' },
|
|
1851
|
+
});
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
> **`trigger: 'cellClick'`과의 분리** — `trigger='cellClick'`은 **마우스 드래그/클릭** 시
|
|
1855
|
+
> 셀 mutation이 행을 동반 동기화하는 규칙. Ctrl+A는 별개의 명시적 단축키라서
|
|
1856
|
+
> `includeInSelectAll`이 단독으로 제어합니다. 즉 `trigger='cellClick'`이어도
|
|
1857
|
+
> `includeInSelectAll: false`(기본)면 Ctrl+A는 셀만 전체 선택합니다.
|
|
1858
|
+
|
|
1859
|
+
#### `cellSelectionImpliesRows` — 행 액션 fallback
|
|
1860
|
+
|
|
1861
|
+
행 단위 액션(Ctrl+H 행 숨김, 행 삭제, 행 복사 등)이 **셀 선택만 있을 때** 어떻게
|
|
1862
|
+
작동할지 결정합니다. `includeInSelectAll`과 별개의 옵션 — 이쪽은 Ctrl+A와 무관하게
|
|
1863
|
+
**암묵적으로 셀 → 행을 도출**할지 정합니다.
|
|
1864
|
+
|
|
1865
|
+
```typescript
|
|
1866
|
+
// 기본 (Policy A — 엄격 분리)
|
|
1867
|
+
new PureSheet(container, {
|
|
1868
|
+
rowSelection: { mode: 'multi' },
|
|
1869
|
+
cellSelection: { mode: 'range' },
|
|
1870
|
+
// cellSelectionImpliesRows: false (기본)
|
|
1871
|
+
});
|
|
1872
|
+
// → cell-only 사용자가 Ctrl+H를 누르면 작동 안 함 (행 선택이 없으므로).
|
|
1873
|
+
|
|
1874
|
+
// Policy B — Excel/Google Sheets 동작
|
|
1875
|
+
new PureSheet(container, {
|
|
1876
|
+
rowSelection: { mode: 'multi' },
|
|
1877
|
+
cellSelection: { mode: 'range' },
|
|
1878
|
+
cellSelectionImpliesRows: true,
|
|
1879
|
+
});
|
|
1880
|
+
// → cell-only 사용자가 Ctrl+H를 누르면 셀이 걸친 행들이 숨겨짐.
|
|
1881
|
+
// 시각(`ps-selected` 클래스)은 옵션과 무관하게 명시적 행 선택만 표시.
|
|
1882
|
+
```
|
|
1883
|
+
|
|
1884
|
+
| 정책 | 명시적 행 선택만 있음 | 셀만 선택됨 (행 비어있음) |
|
|
1885
|
+
|---|---|---|
|
|
1886
|
+
| Policy A (`false`, 기본) | 행 액션 작동 | 행 액션 작동 안 함 |
|
|
1887
|
+
| Policy B (`true`) | 행 액션 작동 | 셀의 행 자동 도출 → 행 액션 작동 |
|
|
1888
|
+
|
|
1889
|
+
#### 세 옵션 정리 — 어떤 동작이 어떤 옵션에 묶이나
|
|
1890
|
+
|
|
1891
|
+
| 시나리오 | 제어 옵션 |
|
|
1892
|
+
|---|---|
|
|
1893
|
+
| 셀 클릭/드래그 시 행도 함께 선택될지 | `rowSelection.trigger`에 `'cellClick'` 포함 여부 |
|
|
1894
|
+
| **Ctrl+A** 시 셀 + 행 둘 다 채울지 | `rowSelection.includeInSelectAll` |
|
|
1895
|
+
| 셀만 선택된 상태에서 **행 액션**이 작동할지 | `cellSelectionImpliesRows` |
|
|
1896
|
+
|
|
1897
|
+
세 가지가 모두 독립적이므로, 예를 들어:
|
|
1898
|
+
- `trigger: 'rowHeader'` + `includeInSelectAll: true` → 평소엔 셀/행 분리지만 Ctrl+A는 둘 다 fill.
|
|
1899
|
+
- `trigger: 'cellClick'` + `includeInSelectAll: false` → 평소엔 셀이 행을 끌고가지만 Ctrl+A는 셀만.
|
|
1900
|
+
|
|
1901
|
+
#### 그룹 행과 선택
|
|
1902
|
+
|
|
1903
|
+
그룹 행(그룹 배너)은 데이터가 없는 구조적 행이라 **선택 대상이 아닙니다**.
|
|
1904
|
+
|
|
1905
|
+
- **드래그 range** — 커서를 그룹 경계 너머로 드래그하면 그룹 행을 기점으로 선택이 **분리(split)** 됩니다. 그룹 배너 셀은 range에서 제외되고, 선택 오버레이도 그룹 행 위치에서 끊어 여러 조각으로 그립니다.
|
|
1906
|
+
- **Ctrl+A / `selectAll()` / `selectAllCells()`** — 그룹 행을 제외한 **모든 데이터 행**을 덮습니다. 그룹 행이 사이에 끼어 있어도 맨 아래 데이터 행까지 빠짐없이 포함됩니다.
|
|
1907
|
+
- 그룹 배너 자체에는 마우스 hover 하이라이트만 표시되고 선택 하이라이트(`ps-selected`)는 붙지 않습니다.
|
|
1908
|
+
|
|
1909
|
+
> 트리 노드는 실제 데이터 행이므로 위 규칙과 무관하게 정상적으로 선택됩니다(그룹 배너·소계 같은 구조적 행에만 해당).
|
|
1910
|
+
|
|
1911
|
+
#### SelectionManager 직접 조회 API (저수준)
|
|
1912
|
+
|
|
1913
|
+
대부분의 사용자는 `grid.rows.getSelectedRowIds()` 같은 RowsAPI를 쓰면 충분. 좌표계 변환이
|
|
1914
|
+
필요한 고급 케이스(커스텀 렌더러/집계/익스텐션)에서만 SelectionManager에 직접 접근.
|
|
1915
|
+
|
|
1916
|
+
```typescript
|
|
1917
|
+
const sm = (grid as any).selectionManager;
|
|
1918
|
+
sm.getSelectedRows(); // Set<RowId> — pinned/regular 통합, identity 추종
|
|
1919
|
+
sm.getSelectedCells(); // Set<`${rowId}\x1F${col}`> — pinned 셀 통합 (외부 표준)
|
|
1920
|
+
sm.getSelectedCellsByView(); // Set<`${viewIndex}\x1F${col}`> — viewIndex 좌표 (자리 추종)
|
|
1921
|
+
sm.getSelectedPinnedCells(); // Set<`${top|bottom}:${idx}\x1F${col}`> — pinned 좌표
|
|
1922
|
+
sm.getSourceIndex(rowId); // number | undefined — sourceIndex 캐시 lookup (idMap 회피용)
|
|
1923
|
+
sm.getRanges(); // SelectionRange[] — drag 1회 = 1 range. isAllVisible flag로 Ctrl+A 빠른 판정
|
|
1924
|
+
sm.isAllVisibleSelected(); // boolean — ranges에 isAllVisible flag 포함 여부 (5M행 O(1) 판정)
|
|
1925
|
+
sm.selectAll(); // 활성된 도메인 채우기 (includeInSelectAll에 따라 분기)
|
|
1926
|
+
sm.selectAllCells(); // 모든 셀만 (isAllVisible 빠른 path)
|
|
1927
|
+
sm.clearSelection(); // 전체 선택 해제 (selectedRows + cells + ranges 모두)
|
|
1928
|
+
sm.clearCellRanges(); // 셀 range만 해제 (row identity 선택은 보존) — 피벗 collapse 등 view 구조 변경 시
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
| 메서드 | 키/반환 형식 | 추종 | 주 사용처 |
|
|
1932
|
+
|---|---|---|---|
|
|
1933
|
+
| `getSelectedRows()` | `Set<RowId>` | identity | 외부 일반 — 행 ID 조회 |
|
|
1934
|
+
| `getSelectedCells()` | `Set<rowIdKey>` | identity | 외부 일반 — 셀 (pinned 통합) |
|
|
1935
|
+
| `getSelectedCellsByView()` | `Set<viewIndexKey>` | position | 렌더러 / 집계 / 클립보드 (regular 영역) |
|
|
1936
|
+
| `getSelectedPinnedCells()` | `Set<pinnedCoordKey>` | position | pinned 영역 paint / 집계 |
|
|
1937
|
+
| `getSourceIndex(rowId)` | `number \| undefined` | — | idMap O(N) 회피 캐시 lookup |
|
|
1938
|
+
| `getRanges()` | `SelectionRange[]` | position | drag/range 단위 순회 — `isAllVisible` flag로 unbounded full range 판정 |
|
|
1939
|
+
| `isAllVisibleSelected()` | `boolean` | — | Ctrl+A 후 ClipboardManager / aggregates 빠른 path 분기 |
|
|
1940
|
+
| `clearSelection()` | `void` | — | 전체 선택 해제 |
|
|
1941
|
+
| `clearCellRanges()` | `void` | — | 셀 range만 해제, row identity 보존. view 구조가 변하여 viewIndex 기반 range가 stale일 때 (피벗 collapse 등) |
|
|
1942
|
+
|
|
1943
|
+
> **identity vs position** — `getSelectedRows`/`getSelectedCells`는 rowId 기반이라
|
|
1944
|
+
> 정렬/필터에 안정적. `getSelectedCellsByView`/`getSelectedPinnedCells`는 좌표 기반이라
|
|
1945
|
+
> 정렬 시 같은 화면 자리의 셀이 선택된 상태로 유지(Excel 모델).
|
|
1946
|
+
|
|
1947
|
+
> **`isAllVisible` 패턴** — Ctrl+A 같은 unbounded full selection은 `selectedCells` Set에
|
|
1948
|
+
> 모든 셀을 채우지 않고 ranges에 `isAllVisible: true` flag만 둡니다. 5M 행 × N 컬럼
|
|
1949
|
+
> Set.add 회피로 O(1)에 즉시 표시. ag-grid의 unbounded cell range, MUI X DataGrid의
|
|
1950
|
+
> exclude model과 동일 패턴.
|
|
1951
|
+
|
|
1952
|
+
#### 선택된 행 ID 조회
|
|
1953
|
+
|
|
1954
|
+
```typescript
|
|
1955
|
+
// 선택된 행 ID 조회
|
|
1956
|
+
const selectedRowIds = grid.rows.getSelectedRowIds();
|
|
1957
|
+
// 또는 reactive: grid.rows.getSelectedRowsAccessor().rows()
|
|
1958
|
+
```
|
|
1959
|
+
|
|
1960
|
+
---
|
|
1961
|
+
|
|
1962
|
+
### 셀 병합 사용 예제
|
|
1963
|
+
|
|
1964
|
+
```typescript
|
|
1965
|
+
// content 병합: 인접한 같은 값을 자동 병합 (계층 X)
|
|
1966
|
+
grid.setMergeConfig({
|
|
1967
|
+
type: 'content',
|
|
1968
|
+
columns: ['region'],
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// hierarchical 병합: 상위 컬럼이 같은 범위 안에서만 하위 컬럼도 병합 (계층 O)
|
|
1972
|
+
grid.setMergeConfig({
|
|
1973
|
+
type: 'hierarchical',
|
|
1974
|
+
columns: ['region', 'category'],
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
// custom 병합(세로): 콜백으로 셀별 병합 범위 직접 결정 (Worker 모드 불가)
|
|
1978
|
+
// startCol/endCol 생략 → 현재 컬럼만(세로 전용).
|
|
1979
|
+
grid.setMergeConfig({
|
|
1980
|
+
type: 'custom',
|
|
1981
|
+
columns: ['dept', 'team'],
|
|
1982
|
+
getMergedRange: ({ rowIndex, columnId, value, getRowData, totalRowCount }) => {
|
|
1983
|
+
let start = rowIndex, end = rowIndex;
|
|
1984
|
+
while (start > 0 && getRowData(start - 1)?.[columnId] === value) start--;
|
|
1985
|
+
while (end < totalRowCount - 1 && getRowData(end + 1)?.[columnId] === value) end++;
|
|
1986
|
+
return start === end ? null : { startRow: start, endRow: end };
|
|
1987
|
+
},
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// custom 병합(가로): startCol/endCol 지정 → 한 행에서 여러 컬럼을 가로 병합.
|
|
1991
|
+
// anchor(colIndex 0)에서만 영역을 반환하고, 컬럼 인덱스는 columnsInOrder(렌더 순서) 기준.
|
|
1992
|
+
grid.setMergeConfig({
|
|
1993
|
+
type: 'custom',
|
|
1994
|
+
columns: ['q1', 'q2', 'q3', 'q4'],
|
|
1995
|
+
getMergedRange: ({ rowIndex, colIndex, columnsInOrder, getCellValue }) => {
|
|
1996
|
+
if (colIndex !== 0) return null; // 첫 참여 컬럼에서만
|
|
1997
|
+
if (getCellValue(rowIndex, 'status') !== 'TOTAL') return null;
|
|
1998
|
+
return {
|
|
1999
|
+
startRow: rowIndex, endRow: rowIndex, // 한 행
|
|
2000
|
+
startCol: columnsInOrder.indexOf('q1'),
|
|
2001
|
+
endCol: columnsInOrder.indexOf('q4'), // q1..q4 가로 병합
|
|
2002
|
+
};
|
|
2003
|
+
},
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
// custom 병합(2D): 세로(같은 값 연속) × 가로(참여 컬럼) 사각형 블록
|
|
2007
|
+
grid.setMergeConfig({
|
|
2008
|
+
type: 'custom',
|
|
2009
|
+
columns: ['region', 'category'],
|
|
2010
|
+
getMergedRange: ({ rowIndex, colIndex, columnId, value, getRowData, totalRowCount, columnsInOrder }) => {
|
|
2011
|
+
if (colIndex !== 0) return null;
|
|
2012
|
+
let start = rowIndex, end = rowIndex;
|
|
2013
|
+
while (start > 0 && getRowData(start - 1)?.[columnId] === value) start--;
|
|
2014
|
+
while (end < totalRowCount - 1 && getRowData(end + 1)?.[columnId] === value) end++;
|
|
2015
|
+
if (start === end) return null;
|
|
2016
|
+
return {
|
|
2017
|
+
startRow: start, endRow: end,
|
|
2018
|
+
startCol: columnsInOrder.indexOf('region'),
|
|
2019
|
+
endCol: columnsInOrder.indexOf('category'), // region..category 2D 사각형
|
|
2020
|
+
};
|
|
2021
|
+
},
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
// getMergedRanges(배치) — 여러 셀을 한 번에 처리(성능). getMergedRange보다 우선.
|
|
2025
|
+
|
|
2026
|
+
// 병합 해제
|
|
2027
|
+
grid.setMergeConfig(null);
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
자세한 타입은 [reference.md → MergeConfig](reference.md#셀-병합--mergeconfig) 참조.
|
|
2031
|
+
|
|
2032
|
+
---
|
|
2033
|
+
|
|
2034
|
+
### 동적 행 높이 사용 예제
|
|
2035
|
+
|
|
2036
|
+
```typescript
|
|
2037
|
+
grid.setDynamicRowHeights(true);
|
|
2038
|
+
console.log(grid.isDynamicRowHeightsEnabled()); // true
|
|
2039
|
+
grid.setDynamicRowHeights(false);
|
|
2040
|
+
```
|
|
2041
|
+
|
|
2042
|
+
---
|
|
2043
|
+
|
|
2044
|
+
### 데이터 조회 사용 예제
|
|
2045
|
+
|
|
2046
|
+
```typescript
|
|
2047
|
+
const allData = await grid.getAllData();
|
|
2048
|
+
const visibleData = await grid.getVisibleData(); // async — 반드시 await
|
|
2049
|
+
const totalCount = grid.getTotalRowCount();
|
|
2050
|
+
const visibleCount = grid.getVisibleRowCount();
|
|
2051
|
+
const rowById = await grid.getRowById('row-1');
|
|
2052
|
+
const rowIndex = await grid.getRowIndexById('row-1');
|
|
2053
|
+
const virtualIndex = await grid.getRowVirtualIndexById('row-1');
|
|
2054
|
+
// sourceIndex로 단일 행 조회 — 최상위 getRowByIndex는 제거됨. grid.rows.getByIndex(idx) 사용
|
|
2055
|
+
const rowByIdx = grid.rows.getByIndex(0).data; // sourceIndex 기반 O(1) (data는 readonly 속성)
|
|
2056
|
+
const fields = grid.getFields(); // flat 모드: FieldDef[]
|
|
2057
|
+
|
|
2058
|
+
// 피벗 모드에서 필드 조회 (카테고리별)
|
|
2059
|
+
const pivotFields = grid.getPivotFields();
|
|
2060
|
+
// { rowFields: FieldDef[], columnFields: FieldDef[], valueFields: FieldDef[], filterFields: FieldDef[] }
|
|
2061
|
+
|
|
2062
|
+
// 모드 무관하게 flat array로 필드 조회
|
|
2063
|
+
const pf = grid.getPivotFields();
|
|
2064
|
+
const anyFields = grid.getFields()
|
|
2065
|
+
?? (pf ? [...pf.rowFields, ...pf.columnFields, ...pf.valueFields, ...pf.filterFields] : null);
|
|
2066
|
+
|
|
2067
|
+
// ── 필드 정의 교체 (updateFields) ──
|
|
2068
|
+
// 기존 컬럼의 레이아웃(너비, 순서, 가시성, 고정)을 보존하면서 필드 교체.
|
|
2069
|
+
// 새 필드는 추가, 삭제된 필드는 제거됨.
|
|
2070
|
+
await grid.updateFields([
|
|
2071
|
+
{ id: 'name', header: '이름', width: 200 },
|
|
2072
|
+
{ id: 'email', header: '이메일', width: 250 }, // 기존 컬럼 — 사용자가 조정한 너비/순서 유지
|
|
2073
|
+
{ id: 'phone', header: '전화번호', width: 150 }, // 새 컬럼 — 기본 상태로 추가
|
|
2074
|
+
// 기존에 있던 'address' 필드가 빠짐 — 자동 제거
|
|
2075
|
+
]);
|
|
2076
|
+
|
|
2077
|
+
// getFields() 가 반환하는 **원본 필드 목록도 함께 갱신**된다(ITSM-2026-002177).
|
|
2078
|
+
// 이전에는 갱신되지 않아 "화면에는 있는데 필드 목록에는 없는" 동적 컬럼이 생겼고,
|
|
2079
|
+
// getFields().find(id) 로 필드를 찾는 소비처(필터 팝오버·툴바 필터 창·정렬 패널)가
|
|
2080
|
+
// 에러 없이 조용히 무시했다(실측: 동적 컬럼의 필터 아이콘 무반응).
|
|
2081
|
+
grid.getFields()?.some((f) => f.id === 'phone'); // → true
|
|
2082
|
+
```
|
|
2083
|
+
|
|
2084
|
+
---
|
|
2085
|
+
|
|
2086
|
+
### 스크롤/네비게이션 사용 예제
|
|
2087
|
+
|
|
2088
|
+
```typescript
|
|
2089
|
+
grid.scrollToRow(0); // 인덱스 기반
|
|
2090
|
+
grid.ensureCellVisible(5, 'email'); // dataIndex + columnId
|
|
2091
|
+
```
|
|
2092
|
+
|
|
2093
|
+
---
|
|
2094
|
+
|
|
2095
|
+
### 국제화 사용 예제
|
|
2096
|
+
|
|
2097
|
+
```typescript
|
|
2098
|
+
grid.setLocale('en-US');
|
|
2099
|
+
console.log(grid.getLocale()); // 'en-US'
|
|
2100
|
+
```
|
|
2101
|
+
|
|
2102
|
+
---
|
|
2103
|
+
|
|
2104
|
+
### 셀 렌더러 사용 예제
|
|
2105
|
+
|
|
2106
|
+
컬럼별 셀 커스텀 렌더링은 `FieldDef.cellRenderer`(및 `rowHeader.cellRenderer`)로 합니다.
|
|
2107
|
+
`function.length`(arity)로 **Simple**(1인자)과 **Lifecycle**(2인자)이 자동 구분됩니다.
|
|
2108
|
+
|
|
2109
|
+
```typescript
|
|
2110
|
+
// Simple (arity 1) — 매 렌더마다 호출, 반환값으로 셀 내용 교체 (vanilla JS)
|
|
2111
|
+
const fields = [{
|
|
2112
|
+
id: 'status', header: '상태', dataType: 'string',
|
|
2113
|
+
cellRenderer: ({ value }) => `<span class="badge badge-${value}">${value}</span>`,
|
|
2114
|
+
}];
|
|
2115
|
+
|
|
2116
|
+
// Lifecycle (arity 2) — mount/update/destroy 훅. React/Vue 등 프레임워크 래핑용.
|
|
2117
|
+
const chartField = {
|
|
2118
|
+
id: 'trend', header: '추이', dataType: 'number',
|
|
2119
|
+
cellRenderer: (container, params) => {
|
|
2120
|
+
const inst = mountMyWidget(container, params.value); // 최초 1회 mount
|
|
2121
|
+
return {
|
|
2122
|
+
// 재사용 셀의 데이터가 바뀔 때. false 반환 → 그리드가 destroy 후 re-mount 강제.
|
|
2123
|
+
update: (p) => { inst.setValue(p.value); return true; },
|
|
2124
|
+
// 셀 풀 반환 / 컬럼 영구 제거 / 그리드 destroy 시 호출 (스크롤 재사용 시점엔 호출 X — dormant 유지).
|
|
2125
|
+
destroy: () => inst.unmount(),
|
|
2126
|
+
};
|
|
2127
|
+
},
|
|
2128
|
+
};
|
|
2129
|
+
```
|
|
2130
|
+
|
|
2131
|
+
`rowHeader.cellRenderer`도 동일 `CellRenderer` 타입이며, params에 `selected`(행 선택 스냅샷)가 추가로 전달됩니다. 실제 `<input>`에 `ps-row-header-checkbox-input` 클래스를 붙이면 built-in 선택 동기화가 `checked`를 자동 갱신합니다.
|
|
2132
|
+
|
|
2133
|
+
```typescript
|
|
2134
|
+
new PureSheet(el, {
|
|
2135
|
+
fields,
|
|
2136
|
+
rowSelection: { mode: 'multi', trigger: 'checkbox' },
|
|
2137
|
+
rowHeader: {
|
|
2138
|
+
width: 44,
|
|
2139
|
+
cellRenderer: ({ selected }) => // built-in 체크박스를 대체
|
|
2140
|
+
`<input type="checkbox" class="ps-row-header-checkbox-input" ${selected ? 'checked' : ''}>`,
|
|
2141
|
+
headerRenderer: () => '☑', // 좌상단 코너(TopLeft) 셀
|
|
2142
|
+
},
|
|
2143
|
+
});
|
|
2144
|
+
```
|
|
2145
|
+
|
|
2146
|
+
> 데모: `apps/demo/examples/cell-renderer-lifecycle.html`. 타입 상세는 [reference.md → CellRendererParams / CellRenderer](reference.md) 참조.
|
|
2147
|
+
|
|
2148
|
+
---
|
|
2149
|
+
|
|
2150
|
+
### 포맷팅 사용 예제
|
|
2151
|
+
|
|
2152
|
+
```typescript
|
|
2153
|
+
// 행 포맷 핸들러 등록 (네임드 키 + priority)
|
|
2154
|
+
grid.formatRow.addHandler('highlight', (info) => {
|
|
2155
|
+
if (info.type === 'data') {
|
|
2156
|
+
const { data, rowElement, cells } = info.ctx;
|
|
2157
|
+
// 조건부 행 스타일
|
|
2158
|
+
if ((data as Record<string, number>).price >= 80000) {
|
|
2159
|
+
rowElement.style.backgroundColor = 'rgba(255, 193, 7, 0.12)';
|
|
2160
|
+
} else {
|
|
2161
|
+
rowElement.style.backgroundColor = '';
|
|
2162
|
+
}
|
|
2163
|
+
// 조건부 셀 스타일
|
|
2164
|
+
const stockCell = cells.stock;
|
|
2165
|
+
if (stockCell && (data as Record<string, number>).stock <= 20) {
|
|
2166
|
+
stockCell.element.style.color = '#d32f2f';
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
// 여러 핸들러를 priority로 순서 제어
|
|
2172
|
+
grid.formatRow.addHandler('reset', (info) => {
|
|
2173
|
+
if (info.type !== 'data') return;
|
|
2174
|
+
// 모든 셀 스타일 초기화 (Row 재사용 시 누수 방지)
|
|
2175
|
+
for (const cell of Object.values(info.ctx.cells)) {
|
|
2176
|
+
cell.element.style.backgroundColor = '';
|
|
2177
|
+
cell.element.style.color = '';
|
|
2178
|
+
}
|
|
2179
|
+
}, { priority: 0 }); // priority 0 = 가장 먼저 실행
|
|
2180
|
+
|
|
2181
|
+
// 핸들러 해제
|
|
2182
|
+
grid.formatRow.removeHandler('highlight');
|
|
2183
|
+
```
|
|
2184
|
+
|
|
2185
|
+
**priority 가이드:**
|
|
2186
|
+
|
|
2187
|
+
| 범위 | 용도 |
|
|
2188
|
+
|------|------|
|
|
2189
|
+
| 0-9 | Core 초기화 (스타일 리셋) |
|
|
2190
|
+
| 10-19 | Core 기능 (검색 하이라이트, 커스텀 셀) |
|
|
2191
|
+
| 50 | 기본값 (사용자 formatRow) |
|
|
2192
|
+
| 90-99 | 최종 오버라이드 |
|
|
2193
|
+
|
|
2194
|
+
> `info.ctx`의 전체 형태(DataRowContext/GroupRowContext/SubtotalContext)는 [reference.md → FormatRowInfo](reference.md#formatrow-컨텍스트--formatrowinfo) 참조.
|
|
2195
|
+
|
|
2196
|
+
#### 헤더 행 포맷팅 — formatHeader
|
|
2197
|
+
|
|
2198
|
+
`formatRow` 의 헤더 카운터파트. 헤더가 (재)렌더될 때만 발화 — 스크롤로는 발화 X.
|
|
2199
|
+
|
|
2200
|
+
##### 기본 사용 — config 슬롯
|
|
2201
|
+
|
|
2202
|
+
```typescript
|
|
2203
|
+
new PureSheet(el, {
|
|
2204
|
+
fields,
|
|
2205
|
+
formatHeader: (ctx) => {
|
|
2206
|
+
// 정렬된 컬럼에 data-* 마커
|
|
2207
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
2208
|
+
cell.element.dataset.sortDir = cell.sortState?.direction ?? '';
|
|
2209
|
+
}
|
|
2210
|
+
},
|
|
2211
|
+
});
|
|
2212
|
+
```
|
|
2213
|
+
|
|
2214
|
+
##### wrapper-managed 사용자 헤더 서식 (완전 패턴)
|
|
2215
|
+
|
|
2216
|
+
`devSnap` 보관, `column:update` 이벤트 구독, self-suppress 가드 모두 **불필요**.
|
|
2217
|
+
핸들러가 매 invoke 마다 wrapper state 를 직접 읽으므로 자연 일관성.
|
|
2218
|
+
|
|
2219
|
+
```typescript
|
|
2220
|
+
// 클로저 캡처용 wrapper state
|
|
2221
|
+
const userHeaderFmt = new Map<string, { class?: string; style?: string }>();
|
|
2222
|
+
|
|
2223
|
+
// (a) 핸들러 1개 등록 — 모든 컬럼을 한 번에 처리
|
|
2224
|
+
grid.formatHeader.addHandler('mzc-user-header', (ctx) => {
|
|
2225
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
2226
|
+
const fmt = userHeaderFmt.get(colId);
|
|
2227
|
+
// toggle 식 적용 — idempotent (여러 번 호출해도 안전)
|
|
2228
|
+
cell.element.classList.toggle('mzc-user-header', !!fmt);
|
|
2229
|
+
if (fmt?.class) {
|
|
2230
|
+
for (const c of fmt.class.split(/\s+/)) cell.element.classList.add(c);
|
|
2231
|
+
}
|
|
2232
|
+
if (fmt?.style) {
|
|
2233
|
+
// 인라인 style 누적 방지 — 우선 제거 후 재적용
|
|
2234
|
+
cell.element.removeAttribute('style');
|
|
2235
|
+
cell.element.style.cssText = fmt.style;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}, { priority: 80 });
|
|
2239
|
+
|
|
2240
|
+
// (b) 사용자 UI 액션 — 노란 배경 적용
|
|
2241
|
+
function applyHeaderStyle(colId, style) {
|
|
2242
|
+
userHeaderFmt.set(colId, style);
|
|
2243
|
+
grid.refresh(); // 헤더 재렌더 트리거 → 핸들러 invoke
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// (c) 단일 컬럼 해제
|
|
2247
|
+
function clearHeaderStyle(colId) {
|
|
2248
|
+
userHeaderFmt.delete(colId);
|
|
2249
|
+
grid.refresh();
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// (d) 전체 해제 + 핸들러 제거 (cleanup, e.g. wrapper destroy 시)
|
|
2253
|
+
function destroyHeaderFormatter() {
|
|
2254
|
+
userHeaderFmt.clear();
|
|
2255
|
+
grid.formatHeader.removeHandler('mzc-user-header');
|
|
2256
|
+
grid.refresh(); // 잔존 클래스/스타일 정리
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// (e) 직렬화 — localStorage 등
|
|
2260
|
+
function serializeUserHeader() {
|
|
2261
|
+
return Object.fromEntries(userHeaderFmt);
|
|
2262
|
+
}
|
|
2263
|
+
function restoreUserHeader(saved) {
|
|
2264
|
+
userHeaderFmt.clear();
|
|
2265
|
+
for (const [colId, fmt] of Object.entries(saved)) userHeaderFmt.set(colId, fmt);
|
|
2266
|
+
grid.refresh();
|
|
2267
|
+
}
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
##### 다중 핸들러 공존 — 검색 하이라이트 / 정렬 강조 / wrapper
|
|
2271
|
+
|
|
2272
|
+
```typescript
|
|
2273
|
+
// 정렬 순위 뱃지 (priority 50)
|
|
2274
|
+
grid.formatHeader.addHandler('sort-rank', (ctx) => {
|
|
2275
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
2276
|
+
cell.element.dataset.sortRank = cell.sortState
|
|
2277
|
+
? String((cell.sortState.index ?? 0) + 1)
|
|
2278
|
+
: '';
|
|
2279
|
+
}
|
|
2280
|
+
}, { priority: 50 });
|
|
2281
|
+
|
|
2282
|
+
// 검색 매칭 (priority 60)
|
|
2283
|
+
const matched = new Set<string>();
|
|
2284
|
+
grid.formatHeader.addHandler('search-match', (ctx) => {
|
|
2285
|
+
for (const [colId, cell] of Object.entries(ctx.cells)) {
|
|
2286
|
+
cell.element.classList.toggle('search-matched', matched.has(colId));
|
|
2287
|
+
}
|
|
2288
|
+
}, { priority: 60 });
|
|
2289
|
+
|
|
2290
|
+
// wrapper UI (priority 80) — 위의 mzc-user-header 와 동일
|
|
2291
|
+
```
|
|
2292
|
+
|
|
2293
|
+
priority 순(50 → 60 → 80)으로 누적 적용. 같은 cell 의 class/style 을 여러 핸들러가 안전하게 누적할 수 있습니다.
|
|
2294
|
+
|
|
2295
|
+
##### 발화 시점
|
|
2296
|
+
|
|
2297
|
+
- **자동 발화**: 초기 렌더 / 정렬 / 필터 / 핀 / 리사이즈 / 순서 / 가시성 변경 / 컬럼 그룹 토글 / `columns.update`(헤더 영향 키) / 테마·로케일 변경
|
|
2298
|
+
- **명시 트리거**: 사용자 state 변경 후엔 `grid.refresh()` 호출 필요 (헤더만 따로 재렌더하는 API 는 미제공)
|
|
2299
|
+
- **발화 안 함**: 스크롤, body CRUD, undo/redo 등 헤더 메타가 변하지 않는 액션
|
|
2300
|
+
|
|
2301
|
+
> 발화 매트릭스 / priority 가이드 / 직렬화 등 상세는 [reference.md → FormatHeaderContext](reference.md#formatheader-컨텍스트--formatheadercontext) 참조.
|
|
2302
|
+
|
|
2303
|
+
---
|
|
2304
|
+
|
|
2305
|
+
### 헤더/상태바 사용 예제
|
|
2306
|
+
|
|
2307
|
+
```typescript
|
|
2308
|
+
grid.setHeaderVisible(false);
|
|
2309
|
+
console.log(grid.isHeaderVisible()); // false
|
|
2310
|
+
grid.setStatusBarVisible(true);
|
|
2311
|
+
grid.updateStatusBarRowCount();
|
|
2312
|
+
```
|
|
2313
|
+
|
|
2314
|
+
---
|
|
2315
|
+
|
|
2316
|
+
### 배치 작업 사용 예제
|
|
2317
|
+
|
|
2318
|
+
```typescript
|
|
2319
|
+
// 콜백 배치
|
|
2320
|
+
await grid.batch(async () => {
|
|
2321
|
+
await grid.changes.addRow({ name: 'A' });
|
|
2322
|
+
await grid.changes.addRow({ name: 'B' });
|
|
2323
|
+
await grid.changes.updateCellById(1, 'status', 'done');
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
// 명시적 배치
|
|
2327
|
+
grid.beginBatch('대량 작업');
|
|
2328
|
+
// ... 여러 작업 ...
|
|
2329
|
+
await grid.endBatch();
|
|
2330
|
+
|
|
2331
|
+
// 트랜잭션
|
|
2332
|
+
grid.applyTransaction((tx) => {
|
|
2333
|
+
tx.add({ name: '행1' });
|
|
2334
|
+
tx.add({ name: '행2' });
|
|
2335
|
+
tx.update(1, { status: 'done' });
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
await grid.applyTransactionAsync(async (tx) => {
|
|
2339
|
+
// 비동기 트랜잭션
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
// 비동기 트랜잭션 flush
|
|
2343
|
+
await grid.flushAsyncTransactions();
|
|
2344
|
+
```
|
|
2345
|
+
|
|
2346
|
+
---
|
|
2347
|
+
|
|
2348
|
+
### 데이터 내보내기 사용 예제
|
|
2349
|
+
|
|
2350
|
+
```typescript
|
|
2351
|
+
const exportData = grid.getExportData();
|
|
2352
|
+
const exportFiltered = grid.getExportData({ includeHidden: false });
|
|
2353
|
+
```
|
|
2354
|
+
|
|
2355
|
+
---
|
|
2356
|
+
|
|
2357
|
+
### 스트리밍/로더 사용 예제
|
|
2358
|
+
|
|
2359
|
+
```typescript
|
|
2360
|
+
// 스트리밍 설정 (StreamingOptions)
|
|
2361
|
+
grid.setStreamingOptions({
|
|
2362
|
+
asyncTransactionWaitMillis: 50, // 비동기 트랜잭션 배치 간격 (ms, 기본 50)
|
|
2363
|
+
enableCellFlash: true, // 값 변경 시 셀 플래시 (기본 false)
|
|
2364
|
+
cellFlashDuration: 500, // 플래시 지속 (ms, 기본 500)
|
|
2365
|
+
cellFadeDuration: 1000, // 페이드 지속 (ms, 기본 1000)
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
// 로더 파라미터 업데이트
|
|
2369
|
+
grid.updateLoaderParams({ authToken: 'new-token', page: 2 });
|
|
2370
|
+
|
|
2371
|
+
// 로더 데이터 새로고침
|
|
2372
|
+
await grid.refreshLoaderData();
|
|
2373
|
+
```
|
|
2374
|
+
|
|
2375
|
+
> DataLoader 클래스 작성법은 [reference.md → DataLoader 클래스](reference.md#dataloader-클래스--워커-기반-커스텀-데이터-로딩) 참조.
|
|
2376
|
+
|
|
2377
|
+
---
|
|
2378
|
+
|
|
2379
|
+
### 성능 조회 사용 예제
|
|
2380
|
+
|
|
2381
|
+
```typescript
|
|
2382
|
+
const timings = grid.getTimings();
|
|
2383
|
+
console.log(timings);
|
|
2384
|
+
// → PerformanceTiming[]: [{ name: 'dataLoad', duration: 150 }, { name: 'render', duration: 30 }, ...]
|
|
2385
|
+
```
|