@vmscloud/moz-ui-grid-vue 0.2.16 → 0.2.18
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/dist/MozGrid.vue.d.ts +0 -1
- package/dist/bridge/VueRenderer.d.ts +2 -1
- package/dist/compat/MozGridVue.vue.d.ts +0 -1
- package/dist/compat/MozPivotVue.vue.d.ts +1 -1
- package/dist/composables/useGridPreset.d.ts +6 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +62 -16
- package/dist/stories/DataLoader.stories.d.ts +53 -0
- package/dist/stories/ObjectValue.stories.d.ts +34 -0
- package/dist/stories/_helpers/localData.worker.d.ts +1 -0
- package/dist/stories/_helpers/localDataLoader.d.ts +3 -3
- package/dist/stories/_helpers/serverRowsLoader.d.ts +55 -0
- package/dist/stories/_helpers/serverTreeLoader.d.ts +18 -0
- package/dist/style.css +1 -1
- package/dist/types/pivot-props.d.ts +4 -1
- package/dist/types/props.d.ts +6 -7
- package/dist/types/toolbar.d.ts +31 -11
- package/docs/manual/examples.md +131 -2
- package/docs/manual/reference.md +1042 -147
- package/docs/manual/reference_vue.md +259 -48
- package/docs/manual/runtime-contracts.md +23 -17
- package/package.json +3 -2
package/docs/manual/reference.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# moz-ui-grid-core 레퍼런스 (Config · 타입 · API · 이벤트)
|
|
2
2
|
|
|
3
3
|
> 패키지: `@vmscloud/moz-ui-grid-core` (export 클래스명은 호환을 위해 `PureSheet` 유지)
|
|
4
|
-
>
|
|
5
|
-
>
|
|
4
|
+
> 정적 소스 기준: `c945b9f5db6a930d07bc32f46ea2ebc90f455e46` (해당 Git object의 core manifest: `0.1.59`).
|
|
5
|
+
> 이 기준은 로컬 Git object이며 원격 최신·npm 게시 상태를 뜻하지 않습니다. 예제의 타입 검사와 실제 브라우저/Worker·파일 저장 동작 검증은 별개입니다.
|
|
6
6
|
|
|
7
7
|
> 📋 **사용 예제 코드는 별도 파일**: [examples.md](examples.md) — Sub-API별 사용, Reactive React 통합, 이벤트, 선택 모드, Excel I/O 등 약 30개 시나리오
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
`onTelemetry`는 해당 그리드와 소유 processor/Worker의 진단만 받습니다. 다른 그리드의 생성·폐기는 콜백에 영향을 주지 않으며, 이 그리드 폐기 뒤의 진단은 전달되지 않습니다. 콜백의 동기 예외·비동기 rejection은 격리됩니다. Worker 진단의 `ctx.error`는 `{ name, message }` 형태이며 host 초기화 실패는 원래 Error를 유지합니다. Worker에서 대기하던 진단은 host 수신 시점의 콜백으로 전달합니다. `psWarn`/`psError` 검증 경고와 독립적이며 전역 로그 수집 옵션이 아닙니다.
|
|
14
14
|
|
|
15
|
-
`keyFields`, `enableReferCache`, `engine`, `
|
|
15
|
+
`keyFields`, `enableReferCache`, `engine`, `loader`, `undoStack` 등 생성/소유권 경계의 runtime 지원을 뜻하지 않습니다. 실제 인스턴스 교체 시에는 어댑터가 `GridChrome.captureReinitStateAsync()`를 기다린 뒤 이전 인스턴스를 파기합니다. 동기 `captureReinitState()`는 기존 직접 호출자용으로 남습니다.
|
|
16
16
|
|
|
17
17
|
## 날짜 시간대 계약 (`timeZone`)
|
|
18
18
|
|
|
@@ -40,6 +40,9 @@ await grid.io.fromExcel(legacyFile, { timeZone: 'UTC' });
|
|
|
40
40
|
- [FlatModeConfig](#flatmodeconfig)
|
|
41
41
|
- [PivotModeConfig](#pivotmodeconfig)
|
|
42
42
|
- [FieldDef (필드 정의)](#fielddef-필드-정의)
|
|
43
|
+
- [점 표기 필드 경로 (`field: 'owner.team'`)](#점-표기-필드-경로-field-ownerteam)
|
|
44
|
+
- [객체 값 열 (`dataType: 'object'`)](#객체-값-열-datatype-object)
|
|
45
|
+
- [계산 필드](#계산-필드)
|
|
43
46
|
- [PureSheet 최상위 API](#puresheet-최상위-api)
|
|
44
47
|
- [Sub-API](#sub-api)
|
|
45
48
|
- [grid.rows — 행 API](#gridrows--행-api)
|
|
@@ -66,8 +69,8 @@ await grid.io.fromExcel(legacyFile, { timeZone: 'UTC' });
|
|
|
66
69
|
- [ColumnGroupDef](#columngroupdef)
|
|
67
70
|
- [TreeConfig](#treeconfig)
|
|
68
71
|
- [MasterDetailConfig](#masterdetailconfig)
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
72
|
+
- [RowGroupConfig](#rowgroupconfig)
|
|
73
|
+
- [RowGroupAggregate](#rowgroupaggregate)
|
|
71
74
|
- [EditorConfig](#editorconfig)
|
|
72
75
|
- [MaskConfig](#maskconfig)
|
|
73
76
|
- [피벗 필드 타입](#피벗-필드-타입)
|
|
@@ -89,11 +92,14 @@ await grid.io.fromExcel(legacyFile, { timeZone: 'UTC' });
|
|
|
89
92
|
- [컬럼 식별자 유틸 — columnKeys](#컬럼-식별자-유틸--columnkeys)
|
|
90
93
|
- [수식 엔진 — FormulaEngine](#수식-엔진--formulaengine)
|
|
91
94
|
- [값 목록 위젯 — ValueCheckList](#값-목록-위젯--valuechecklist)
|
|
95
|
+
- [객체 값·필드 경로 헬퍼](#객체-값필드-경로-헬퍼)
|
|
92
96
|
- [formatRow 컨텍스트 — FormatRowInfo](#formatrow-컨텍스트--formatrowinfo)
|
|
93
97
|
- [formatHeader 컨텍스트 — FormatHeaderContext](#formatheader-컨텍스트--formatheadercontext)
|
|
94
98
|
- [RowHeaderConfig](#rowheaderconfig)
|
|
95
99
|
- [셀 속성 — CellAttributes / CellAttributesFn](#셀-속성--cellattributes--cellattributesfn)
|
|
96
100
|
- [DataLoader 클래스](#dataloader-클래스--워커-기반-커스텀-데이터-로딩)
|
|
101
|
+
- [서버 행 모델 (SSRM)](#서버-행-모델-ssrm--rowmodel-server--loader)
|
|
102
|
+
- [객체 값 열·점 표기 필드의 서버 계약](#객체-값-열점-표기-필드의-서버-계약)
|
|
97
103
|
- [UndoStack 클래스](#undostack-클래스--공유-undoredo)
|
|
98
104
|
- [Row 클래스 정적 팩토리](#row-클래스-정적-팩토리)
|
|
99
105
|
- [에러 코드 전체 레퍼런스](#에러-코드-전체-레퍼런스)
|
|
@@ -203,14 +209,12 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
203
209
|
| 속성 | 타입 | 기본값 | 설명 |
|
|
204
210
|
|------|------|--------|------|
|
|
205
211
|
| `data` | `Row[]` | — | 데이터 배열 |
|
|
206
|
-
| `loader` | `LoaderConfig` | — | Worker
|
|
212
|
+
| `loader` | `LoaderConfig` | — | 데이터 로더. `{ use: DataLoader 클래스 }`(메인 스레드) 또는 `{ worker: () => Worker }`(Worker 모드) 중 하나. 지정하면 `data`는 무시 (→ [LoaderConfig](#데이터-로더--loaderconfig)) |
|
|
207
213
|
| `fields` | `FieldDef[]` | 필수 | 필드(컬럼) 정의 |
|
|
208
214
|
| `keyFields` | `string[]` | — | 업무 키 필드 목록. 복합 키도 가능(예: `['exchange', 'symbol']`). 생략 시 `enableReferCache: true`가 필요하며 내부 자동 행 ID를 사용한다. 빈 배열은 허용하지 않는다. 데이터의 `id` 필드를 암묵적으로 업무 키로 사용하지 않는다. |
|
|
209
215
|
| `enableReferCache` | `boolean` | `false` | 행 객체 참조 캐시 활성화 — 행 객체 기반 API 및 `keyFields` 생략 시 필수. 키와 캐시 설정을 모두 생략하면 초기화 오류가 발생한다. 자동 ID는 원본 데이터 필드에 주입하지 않으며 전체 데이터 교체 시 새로 발급된다. |
|
|
210
216
|
| **엔진** | | | |
|
|
211
217
|
| `engine` | `'native'` | `'native'` | 데이터 처리 엔진. **`'aq'`(Arquero)는 제거됨** — 단일 엔진(native)만 남았습니다(ITSM-2026-002190) |
|
|
212
|
-
| `useWorker` | `boolean` | `false` | Web Worker 사용 여부 |
|
|
213
|
-
| `workerFactory` | `() => Worker` | — | `useWorker: true`에서 앱이 빌드한 신뢰 Worker entry 생성. `@vmscloud/moz-ui-grid-core/worker`의 `registerWorkerFunctions`와 `installProcessorWorker`로 함수·loader를 정적으로 등록한다. |
|
|
214
218
|
| **저장본 호환** | | | |
|
|
215
219
|
| `migrateConfig` | `GridConfigMigrator` | — | `applyConfig()` 가 옛 버전 저장본을 받았을 때 호출할 변환기. core 는 **호출 시점·순서만** 책임지고 변환 내용은 소비처(wrapper)가 소유 (→ [저장 스키마 버전](#저장-스키마-버전--gridconfigmigrator)) |
|
|
216
220
|
| **UI** | | | |
|
|
@@ -235,11 +239,12 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
235
239
|
| `cellSelection` | `CellSelectionConfig` | — | 셀 선택 설정 (자리 추종, viewIndex 기반). 서브필드: `mode`. 둘 다 미지정 시 기본 `{mode:'range'}` (셀 전용 Excel 스타일) |
|
|
236
240
|
| `cellSelectionImpliesRows` | `boolean` | `false` | 셀만 선택된 상태에서 행 액션(Ctrl+H 등) 작동 여부 — Policy B fallback. Ctrl+A 동작은 `rowSelection.includeInSelectAll`이 별도 제어 |
|
|
237
241
|
| **서버/수동 row-model** | | | |
|
|
238
|
-
| `rowModel` | `'client' \| 'server'` | `'client'` | `'server'`이면 `sort()`/`filter()`는 정렬·필터 상태와 이벤트만 갱신하고
|
|
242
|
+
| `rowModel` | `'client' \| 'server'` | `'client'` | `'server'`이면 `sort()`/`filter()`는 정렬·필터 상태와 이벤트만 갱신하고 행 순서·구성을 로컬에서 바꾸지 않습니다. `loader` 가 없으면 소비처가 서버 정본으로 재조회/append 합니다. `loader` 가 있으면 SSRM — 그리드가 `DataLoader.fetchRange()` 로 필요한 구간만 받습니다(→ [서버 행 모델](#서버-행-모델-ssrm--rowmodel-server--loader)). `'client'` + `loader` 는 `fetchAll()` 로 전체를 받습니다. |
|
|
243
|
+
| `serverSide` | `ServerSideConfig` | — | SSRM 설정 `{ blockSize: 100, maxBlocksInCache: 50, maxConcurrentRequests: 2, debounceMs: 50, sendChangesToAggregates: false }` |
|
|
239
244
|
| `endReachedThresholdRows` | `number` | `0` | `scroll:endReached` 선발화 행 임계값. 현재 가시 범위 끝에서 남은 행 수가 이 값 이하이면 끝 도달로 간주합니다. |
|
|
240
245
|
| **편집** | | | |
|
|
241
246
|
| `editable` | `boolean` | `false` | 전역 편집 가능 여부 |
|
|
242
|
-
| `preventKeyEdit` | `boolean` | `true` | 키 컬럼 편집 방지 — `keyFields` 컬럼은 신규 행(added)에서만 편집 가능, 기존 행(pristine/modified)에서는 자동 readonly |
|
|
247
|
+
| `preventKeyEdit` | `boolean` | `true` | 키 컬럼 편집 방지 — `keyFields` 컬럼은 신규 행(added)에서만 편집 가능, 기존 행(pristine/modified)에서는 자동 readonly. 키 컬럼은 열의 `field`로 판정합니다(`{ id:'cv', field:'code' }`도 `keyFields:['code']`면 키 컬럼 — 편집·붙여넣기 차단, 머리글 키 표시) |
|
|
243
248
|
| **드래그앤드롭** | | | |
|
|
244
249
|
| `dragDropConfig` | `DragDropConfig` | — | 행 드래그앤드롭 설정 (같은 그리드 reorder, 크로스-그리드 드롭, `rowOrderField`, `mutateOnDirty` 등). 런타임 변경은 `grid.dragDrop.setConfig()` |
|
|
245
250
|
| **툴팁** | | | |
|
|
@@ -267,6 +272,8 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
267
272
|
- **업무 키 모드**: `keyFields: ['id']`처럼 비어 있지 않은 배열을 명시합니다. `enableReferCache` 기본값은 `false`이며, 자동 ID 모드가 누락된 업무 키 값을 대신 채워주지는 않습니다.
|
|
268
273
|
- **자동 ID 모드**: `keyFields`를 **생략**하고 `enableReferCache: true`를 지정합니다. `keyFields: []`는 캐시 설정과 관계없이 초기화 오류이며, 키 생략 + 캐시 미지정/`false`도 빈 데이터부터 초기화 오류입니다. 생성 후 `await grid.waitForReady()`로 초기화 성공/실패를 확인하세요.
|
|
269
274
|
- ID는 값이나 원본 객체가 아니라 **행 occurrence(배열에 등장한 각각의 행)** 에 속합니다. 값이 같거나 동일 객체를 두 번 넣어도 서로 다른 ID가 발급됩니다. frozen 원본 객체에도 ID 필드를 주입하지 않습니다.
|
|
275
|
+
- **ID 없는 중첩 트리**: 자동 ID 모드에서 모든 `idField` 값이 null/undefined인 nested 입력은 occurrence별 부모 관계를 내부에서 관리합니다. 생성자 `data + treeConfig`, `tree.loadData`, 등록 Worker loader가 같은 규칙을 사용합니다. 명시 ID 또는 일부만 ID가 있는 입력은 기존 raw nodeId 계약을 유지하며 자동 보정하지 않습니다. Worker 직접 배열 load는 여전히 PS502입니다.
|
|
276
|
+
- 자동 트리의 `moveNode`/`setParent`는 도메인 `parentId`를 합성하지 않습니다. 기존 이동 이벤트·reordered/hasChanges·undo/redo·commit/discard·DataSession으로 관계를 관리합니다. 일반 `addRow`는 root로 추가한 뒤 `setParent`로 연결할 수 있습니다. 새 저장용 공개 API나 서버 lazy 업무키 변환은 제공하지 않습니다. 일반 reload는 새 IDs/관계, 같은 DataSession 복원은 기존 IDs/관계/기준 상태를 사용합니다.
|
|
270
277
|
- 정렬·필터·재정렬, dirty 편집·추가·삭제 복원·discard, undo/redo 및 commit은 같은 occurrence의 ID를 유지합니다. 반면 `loadData()`로 전체 데이터를 교체하면 동일 객체를 다시 넘겨도 새 ID가 발급됩니다. 서버 키나 재조회 간 영속 식별자로 저장하지 마세요.
|
|
271
278
|
- 자동 identity는 원본/조회 데이터의 속성이나 Symbol이 아니라 내부 메타데이터로 관리됩니다. JSON 및 도메인 데이터 export에 자동 ID 필드가 추가되지 않습니다. 행 접근자의 `id`, 선택 ID 또는 `addRow()` 반환값으로 ID를 얻고, 원본 객체에 ID를 만들어 넣거나 문자열 형태를 해석하지 마세요.
|
|
272
279
|
- 자동 모드의 객체 기반 API에는 그리드가 반환한 행 데이터를 사용합니다. 원본 객체나 `{ ...row }`/JSON 복사본은 occurrence를 식별하는 핸들이 아닙니다. ID를 별도로 가진 `changes.updateCellById(id, field, value)` 등으로 수정하세요. raw keyless patch만으로 대상을 추론하지 않습니다([트랜잭션](#트랜잭션--rowtransaction--transactionoptions--transactionresult)).
|
|
@@ -292,8 +299,7 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
292
299
|
| 속성 | 타입 | 기본값 | 설명 |
|
|
293
300
|
|------|------|--------|------|
|
|
294
301
|
| `mode` | `'flat'` | `'flat'` | 그리드 모드 |
|
|
295
|
-
| `
|
|
296
|
-
| `group` | `GroupConfig` | — | **deprecated 하위호환 별칭** — `rowGroup`과 동일. 둘 다 주면 `rowGroup`이 우선. 신규 코드는 `rowGroup` 사용 |
|
|
302
|
+
| `rowGroupConfig` | `RowGroupConfig` | — | 초기 그룹화 설정. 런타임 `grid.rowGroup.setConfig()`와 같은 타입이며 `aggregates`는 `{ column, func, id?, scope? }[]` (→ [RowGroupConfig](#rowgroupconfig)) |
|
|
297
303
|
| `rowTemplate` | `RowTemplate` | — | Multi-Row 레이아웃 템플릿 |
|
|
298
304
|
| `columnGroups` | `ColumnGroupDef[]` | — | 컬럼 그룹(다단계 컬럼 헤더) 정의 (→ [ColumnGroupDef](#columngroupdef)). (구 `headerGroups`) |
|
|
299
305
|
| ~~`onColumnGroupToggle`~~ | — | — | **제거됨.** 컬럼 그룹 토글은 `columnGroup:toggle` 이벤트로 노출됩니다. `grid.on('columnGroup:toggle', ...)`로 구독 (→ [이벤트](#이벤트)) |
|
|
@@ -322,9 +328,13 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
322
328
|
| 속성 | 타입 | 필수 | 설명 |
|
|
323
329
|
|------|------|------|------|
|
|
324
330
|
| `id` | `string` | O | 컬럼 **식별자** — 이 컬럼을 *가리킬* 때 쓴다. 정렬/필터 상태(`SortState.id`/`FilterState.id`), 컬럼 폭 상태, DOM dataset, 저장 레이아웃이 전부 이 값을 참조 |
|
|
325
|
-
| `field` | `string` | — | 원본 데이터 객체의 **키** — 값을 *읽을* 때만 쓴다(`row[field]`). 생략 시 `id` 와 같은 값으로
|
|
331
|
+
| `field` | `string` | — | 원본 데이터 객체의 **키** — 값을 *읽을* 때만 쓴다(`row[field]`). 생략 시 `id` 와 같은 값으로 정규화. 점 표기(`'owner.team'`)는 중첩 경로 → [점 표기 필드 경로](#점-표기-필드-경로-field-ownerteam) |
|
|
326
332
|
| `header` | `string` | O | 헤더 표시명 |
|
|
327
|
-
| `dataType` | `DataType` | O | 데이터 타입 (`'string' \| 'number' \| 'boolean' \| 'date' \| 'array'`) |
|
|
333
|
+
| `dataType` | `DataType` | O | 데이터 타입 (`'string' \| 'number' \| 'boolean' \| 'date' \| 'array' \| 'object'`). `'object'` 는 셀에 객체를 그대로 두는 열 → [객체 값 열](#객체-값-열-datatype-object) |
|
|
334
|
+
| `objectKey` | `ObjectKeyAccessor` — 경로 문자열 \| 함수 \| `WorkerFunctionRef` | — | 객체 값 열의 "같은 값" 키 (묶기·값 목록·in 필터·병합·select 비교) |
|
|
335
|
+
| `displayKey` | `ObjectKeyAccessor` — 경로 문자열 \| 함수 \| `WorkerFunctionRef` | — | 객체 값 열의 보이는 값 (`mask.formatter` 가 없을 때 표시·라벨·복사·내보내기, 정렬 다음 순위, 글자 필터, 경로 문자열이면 편집) |
|
|
336
|
+
| `sortValue` | `SortValueAccessor` — 경로 문자열 \| 함수 \| `WorkerFunctionRef` | — | 객체 값 열의 정렬 값. 배열이면 앞 값부터 비교. 생략하면 `displayKey`, 그것도 없으면 `objectKey` |
|
|
337
|
+
| `equals` | `(a, b) => boolean` | — | 객체 값 열의 변경 감지 비교. 생략하면 내용 비교 |
|
|
328
338
|
|
|
329
339
|
> **`id` ≠ `field` — 이중 역할 해소 (ITSM-2026-002188)**
|
|
330
340
|
> 예전에는 `id` 하나가 "식별자"와 "데이터 키"를 겸했습니다("필드 ID = 데이터 객체의 키"). 이제 두 축이 분리되어 **가리킬 때는 `id`, 읽을 때는 `field`** 입니다.
|
|
@@ -337,10 +347,177 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
337
347
|
> { id: 'dept_short', field: 'dept', header: '부서(약칭)', dataType: 'string' }
|
|
338
348
|
> ```
|
|
339
349
|
>
|
|
350
|
+
> 엔진 결과도 같은 규칙입니다: `grid.search` 매치 `columnId`, 수식 필터 `$dept_short`·`[dept_short]`, 그룹 집계 키 `sum_<열 id>`, 트리 집계(`treeConfig.aggregates`)·트리 체크 수 키는 모두 **열 id** 로 나오고 값은 `row.dept` 로 읽습니다 — 같은 `field` 를 쓰는 두 열도 따로 나옵니다(ITSM-2026-004445).
|
|
351
|
+
>
|
|
340
352
|
> ⚠️ 계산 필드(`formula`)는 원본 데이터에 존재하지 않으므로 `field` 가 의미 없습니다 — `id` 만 지정하고 값 조회는 `FormulaEngine` 이 담당합니다.
|
|
341
353
|
>
|
|
342
354
|
> 정규화 헬퍼는 core 가 export 합니다: `normalizeColumnKeys` / `getColumnId` / `getColumnField` / `resolveFieldKey` (→ [컬럼 식별자 유틸](#컬럼-식별자-유틸--columnkeys)).
|
|
343
355
|
|
|
356
|
+
### 점 표기 필드 경로 (`field: 'owner.team'`)
|
|
357
|
+
|
|
358
|
+
`field` 에 점이 있으면 **중첩 경로**로 읽습니다. 따로 켜는 옵션은 없습니다(기본 켬).
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// 데이터: { no: 1, owner: { id: 'u2', name: '김철수', team: 'B' } }
|
|
362
|
+
{ id: 'ownerTeam', header: '담당 팀', field: 'owner.team' } // → 'B'
|
|
363
|
+
{ id: 'ownerName', header: '담당자명', field: 'owner.name' } // → '김철수'
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
| 규칙 | 입력 → 결과 |
|
|
367
|
+
|---|---|
|
|
368
|
+
| 읽기 | `field: 'owner.team'` → `row.owner?.team`. 중간 값이 `null`/`undefined` 면 `undefined`(셀은 빈 칸) |
|
|
369
|
+
| 점이 든 키가 행에 그대로 있으면 그 키 우선 | `{ 'owner.team': 'literal', owner: { team: 'nested' } }` → `'literal'` (기존 데이터 호환) |
|
|
370
|
+
| 경로로 보지 않는 이름 | 점으로 시작·끝나거나 빈 조각이 있는 이름(`'.a'`, `'a.'`, `'a..b'`)은 일반 키 |
|
|
371
|
+
| 쓰기 (편집·붙여넣기·`changes.updateCellById`·`bulkUpdate`) | **최상위 객체를 복사한 뒤 교체**합니다. `owner.team` 을 `'Z'` 로 고치면 `row.owner` 를 얕게 복사해 `team` 만 바꾼 새 객체를 `row.owner` 에 넣습니다. 같은 `owner` 객체를 여러 행이 공유해도 다른 행은 바뀌지 않습니다 |
|
|
372
|
+
| 중간 객체가 없을 때 쓰기 | `owner: null` 인 행에 `owner.team = 'Q'` → `owner: { team: 'Q' }` |
|
|
373
|
+
| 변경 기록 | `changes.getAll()`·`data:changed`·undo 에는 **최상위 키**(`owner`)와 새 객체가 남습니다. `'owner.team'` 키는 생기지 않습니다. 같은 값으로 되돌리면 dirty 가 풀립니다 |
|
|
374
|
+
| 점이 든 키가 행에 그대로 있을 때 쓰기 | 그 키(`row['owner.team']`)에 씁니다. 변경 기록의 필드도 `'owner.team'` |
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// 5행과 6행이 같은 owner 객체 { id: 'u1', name: '홍길동', team: 'A' } 를 공유
|
|
378
|
+
await grid.changes.updateCellById(5, 'owner.team', 'Z');
|
|
379
|
+
grid.changes.getAll().modified;
|
|
380
|
+
// → 5행: fields ['owner'], data.owner = { id: 'u1', name: '홍길동', team: 'Z' }, original.owner.team = 'A'
|
|
381
|
+
// 6행의 owner.team 은 'A' 그대로
|
|
382
|
+
await grid.changes.updateCellById(5, 'owner.team', 'A'); // 되돌리면 modified 에서 빠진다
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- 표시·정렬·필터·값 목록·묶기·병합·고정 행·`cellRenderer`/`cellAttributes` 의 `value`·CSV/XLSX 내보내기·선택 집계(상태바)·수식이 모두 같은 경로 규칙으로 읽습니다. 메인 스레드와 Worker 모드 모두 같습니다.
|
|
386
|
+
- 경로 열(`owner.team`)의 정렬·필터는 최상위 키(`owner`)가 바뀌어도 다시 계산됩니다.
|
|
387
|
+
- 그리드와 같은 규칙으로 직접 읽고 쓰려면 [객체 값·필드 경로 헬퍼](#객체-값필드-경로-헬퍼)의 `getFieldValue`·`setFieldValue` 를 씁니다.
|
|
388
|
+
|
|
389
|
+
### 객체 값 열 (`dataType: 'object'`)
|
|
390
|
+
|
|
391
|
+
셀에 객체(`{ id: 'u2', name: '김철수' }`)를 그대로 두는 열입니다. 그리드는 기능마다 객체에서 단순 값을 하나씩 뽑아 씁니다.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const users = [
|
|
395
|
+
{ id: 'u1', name: '홍길동', team: 'A', rank: 1 },
|
|
396
|
+
{ id: 'u2', name: '김철수', team: 'B', rank: 2 },
|
|
397
|
+
{ id: 'u3', name: '김철수', team: 'C', rank: 3 }, // 동명이인
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
id: 'owner', header: '담당자', dataType: 'object',
|
|
402
|
+
objectKey: 'id', // 같은 id = 같은 값
|
|
403
|
+
displayKey: 'name', // 보이는 값 (mask 가 없을 때 화면 글자, 글자 필터·편집)
|
|
404
|
+
sortValue: (v) => [v.rank, v.name], // 정렬: rank → name
|
|
405
|
+
equals: (a, b) => a?.id === b?.id, // 변경 감지 (생략하면 내용 비교)
|
|
406
|
+
mask: {
|
|
407
|
+
type: 'function',
|
|
408
|
+
formatter: (v) => v?.name ?? '', // 화면 글자
|
|
409
|
+
parser: (text) => users.find((u) => u.id === text || u.name === text), // 글자 → 객체
|
|
410
|
+
},
|
|
411
|
+
editor: { type: 'dropdown', options: users }, // 선택 사항
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**기능별로 쓰는 값**
|
|
416
|
+
|
|
417
|
+
| 기능 | 쓰는 값 | 예 (`{ id: 'u2', name: '김철수', rank: 2 }`) |
|
|
418
|
+
|---|---|---|
|
|
419
|
+
| 셀 표시·값 목록 라벨·글자 검색·복사·내보내기·그룹 배너 라벨 | `mask.formatter` (함수형 mask) → 없으면 `displayKey` 값의 문자열(`null`/`undefined` → `''`) → 둘 다 없으면 JSON | `'김철수'` |
|
|
420
|
+
| 묶기·groupId·`in`/`notIn` 필터·값 목록 항목 키·셀 병합·select/dropdown/multiselect 선택 비교 | `objectKey` | `'u2'` → groupId `owner:u2` |
|
|
421
|
+
| 정렬 (열 정렬·그룹 순서)·크기 비교 필터 | `sortValue` → `displayKey` → `objectKey` | `[2, '김철수']` (배열은 앞 값부터) |
|
|
422
|
+
| 변경 감지 (dirty·같은 값 확정은 변경 아님) | `equals` → 없으면 내용 비교 | 같은 내용의 새 객체 = 변경 아님 |
|
|
423
|
+
| 텍스트 편집 확정·붙여넣기 | 아래 **편집 우선순위** | `'u2'` → 객체 |
|
|
424
|
+
|
|
425
|
+
- 객체가 아닌 값(문자열·숫자·`null`·Date·배열)은 위 규칙을 타지 않고 기존 동작 그대로입니다.
|
|
426
|
+
- `objectKey`·`sortValue` 문자열은 객체 **안의** 경로입니다(`'id'`, `'dept.code'`). 함수도 됩니다(Worker 모드 제약은 아래).
|
|
427
|
+
- 같은 id 의 **서로 다른 객체 참조**(서버 JSON 을 행마다 따로 파싱한 경우)도 한 그룹·한 값 목록 항목입니다. 동명이인(이름이 같고 id 가 다른 객체)은 따로입니다 — 값 목록 라벨은 둘 다 `김철수` 로 보이지만 항목은 둘입니다.
|
|
428
|
+
- `in` 필터 값은 키 배열(`['u2']`)이든 객체 배열(`[{ id: 'u3', … }]`)이든 됩니다 — 그리드가 `objectKey` 로 바꿔 비교합니다. `[null]` 은 값이 `null` 인 행입니다.
|
|
429
|
+
- `eq`/`neq` 행 필터는 셀이나 필터 값이 객체일 때 아래 **그룹 결과 필터**와 같은 규칙입니다: 필터 값이 객체면 `objectKey` 비교(같은 id 의 다른 참조도 같고, 동명이인은 다름), 필터 값이 문자열이고 필터에 mask(함수형 `formatter`)가 있으면 기존처럼 **표시값** 비교(`'김철수'` → 동명이인 둘 다), mask 가 없고 열에 `displayKey` 가 있으면 그 **보이는 글자**와 대소문자 무시 비교, 둘 다 없이 문자열·숫자면 `objectKey` 와 비교(`'u2'`). 셀도 필터 값도 객체가 아니면(`eq null` 등) 기존 그대로입니다. 서버로 보내는 `filterModel` 의 객체 필터 값은 키로 바뀌어 갑니다(`in` 과 같음).
|
|
430
|
+
- 글자 조건(`contains`·`notContains`·`startsWith`·`endsWith`·`like`·`notLike`)은 셀이 객체면 보이는 글자(mask 가 실린 필터면 기존 표시값, 아니면 `displayKey` 글자, 그것도 없으면 JSON)로 비교합니다.
|
|
431
|
+
- 크기 비교(`gt`·`gte`·`lt`·`lte`·`between`)는 셀이 객체면 정렬 기준 값(`sortValue` → `displayKey` → `objectKey`)과 비교합니다. `sortValue` 가 배열이면 첫 값과 비교합니다.
|
|
432
|
+
- 위 필터 규칙은 그리드 엔진(메인·Worker)·그룹 결과 필터·`createRowFilterPredicate`(SSRM 받아 둔 행 판정 포함)가 같은 함수 하나로 씁니다.
|
|
433
|
+
- `grid.getFilterValues('owner')` 는 키로 중복을 없애고 `values` 에 라벨, `rawValues` 에 원본 객체를 줍니다. 필터는 `rawValues` 로 만드세요.
|
|
434
|
+
- mask 도 `displayKey` 도 없으면 셀은 JSON 문자열로 보입니다(`{"id":"u1","name":"홍길동"}`). 사람이 읽는 글자를 보이려면 `displayKey: 'name'`(경로 하나면 충분) 또는 `mask: { type: 'function', formatter }` 를 주세요. 둘 다 있으면 `mask.formatter` 가 보입니다.
|
|
435
|
+
- `displayKey` 는 `objectKey` 와 같은 형태(경로 문자열·함수·등록 함수 참조)이고 Worker 규칙도 같습니다. `objectKey` 가 없을 때 그룹 키로 `displayKey` 를 대신 쓰지 않습니다(`PS314` 경고 + JSON 키 그대로).
|
|
436
|
+
- `objectKey` 가 `null`/`undefined` 를 내는 객체는 `null` 값과 같은 그룹이 됩니다.
|
|
437
|
+
- `equals` 는 변경 감지용입니다. 점 표기 경로(`owner.team`)로 쓴 변경은 `equals` 대신 **내용 비교**로 판정합니다 — `equals: id 비교` 여도 `team` 변경이 묻히지 않습니다. Date·배열 값의 변경 감지는 기존 규칙(참조 비교) 그대로입니다.
|
|
438
|
+
|
|
439
|
+
**편집 우선순위** (위에서부터 먼저 맞는 것)
|
|
440
|
+
|
|
441
|
+
| 순위 | 열 | 편집 |
|
|
442
|
+
|---|---|---|
|
|
443
|
+
| 1 | `editor` 지정(`select`·`dropdown`·`multiselect`·`custom` 등, 또는 `editorResolver` 결과) | 그 에디터 |
|
|
444
|
+
| 2 | `mask.parser` 있음 | 글자 → `parser` (아래 **편집 규칙**) |
|
|
445
|
+
| 3 | `displayKey` 가 **경로 문자열** | 입력 글자를 그 경로 값으로 기록: `owner = { ...owner, name: 글자 }` (최상위 복사 후 교체 — 점 표기 쓰기와 같음, `changes` 는 최상위 키 `owner`, `objectKey` 그대로). `owner` 가 `null` 이면 `{ name: 글자 }` 로 새로 만듦. 빈 글자 확정 → 그 경로 값 `''`(`null` 로 지우지 않음). 시작 글자는 그 경로 값, type-to-edit 은 일반 문자열 열처럼 누른 글자로 시작. 이름 변경은 열 `equals`(id 비교 등)가 아닌 내용 비교로 판정 |
|
|
446
|
+
| — | `displayKey` 가 함수·등록 함수 (parser·editor 없음) | **편집 불가**(쓸 경로가 없음) |
|
|
447
|
+
| 4 | `displayKey`·`parser`·`editor` 모두 없음 | **JSON 편집** (한 줄 입력칸) — 아래 |
|
|
448
|
+
|
|
449
|
+
**JSON 편집** (4순위)
|
|
450
|
+
|
|
451
|
+
| 상황 | 결과 |
|
|
452
|
+
|---|---|
|
|
453
|
+
| 시작 글자 | 한 줄 JSON(`null` → `''`). `mask.formatter` 가 있어도 JSON |
|
|
454
|
+
| type-to-edit | 원래 JSON **끝에 이어 붙임**(IME 는 원래 JSON 끝에서 조합) |
|
|
455
|
+
| 빈 글자(공백만) 확정 | `null` |
|
|
456
|
+
| JSON 객체 확정 | 그 객체. 기존 값과 `equals`(없으면 내용)가 같으면 기록 안 함 |
|
|
457
|
+
| 문법 오류·배열·원시값 + Enter/Tab | 편집 유지 + 오류 표시(`ps-editor-invalid`, 숫자 칸 오류와 같음) + `validationError`. 문구는 i18n `editor.objectJsonInvalid`(ko: `JSON 객체({ … })만 입력할 수 있습니다`) + 문법 오류 내용 |
|
|
458
|
+
| 문법 오류 등 + 그리드가 확정(창 밖 클릭·다른 셀 클릭/편집·`commitEdit()` API) | **원래 값 유지**(편집 취소 — `editCancel`) |
|
|
459
|
+
|
|
460
|
+
여러 줄 편집기·문법 색이 필요하면 래퍼의 JSON 편집기(`editor: jsonEditor()` — 래퍼 문서)를 쓰거나 `custom` 에디터를 지정하세요(1순위).
|
|
461
|
+
|
|
462
|
+
**편집 규칙** (2순위 `mask.parser`)
|
|
463
|
+
|
|
464
|
+
| 상황 | 결과 |
|
|
465
|
+
|---|---|
|
|
466
|
+
| 편집 시작 글자 | 열 표시값(`mask.formatter` → `displayKey` → JSON). type-to-edit 은 누른 글자로 시작 |
|
|
467
|
+
| 시작 글자를 바꾸지 않고 확정 | 원래 값 유지(`parser` 를 부르지 않음) |
|
|
468
|
+
| 빈 글자로 확정 | `null` (`parser` 를 부르지 않음) |
|
|
469
|
+
| `parser` 가 `undefined` 를 돌려줌 | 편집 유지 + 오류 표시 + `validationError`. 문구는 i18n `editor.objectValueNotFound`(ko: `값을 찾을 수 없습니다`) |
|
|
470
|
+
| `parser` 가 `throw new Error(msg)` | 편집 유지 + 그 `msg` 를 오류 문구로 표시 (예: 동명이인 안내) |
|
|
471
|
+
| `parser` 가 객체를 돌려줌 | 그 객체로 확정. 기존 값과 `equals`(또는 내용)가 같으면 변경 아님 |
|
|
472
|
+
| `mask.parser` 도, `select`·`dropdown`·`multiselect`·`custom` 에디터도 없음 | 위 **편집 우선순위** 3·4순위(displayKey 경로 / JSON). `displayKey` 가 함수면 편집 불가 |
|
|
473
|
+
| `editorResolver` 가 있음 | 편집 시작 때 그 행의 에디터로 다시 판단 |
|
|
474
|
+
|
|
475
|
+
- **선택 에디터** — `editor.options` 에 **객체 배열만** 줘도 됩니다. 라벨은 열의 `mask.formatter`(없으면 `String(객체)` = `[object Object]`), 선택 표시와 "같은 값" 비교는 `objectKey` 입니다. 같은 키를 다시 고르면 변경이 아니고, 고른 옵션의 객체가 그대로 저장됩니다. `{ value: 객체, label }` 모양도 그대로 됩니다. 타입은 `options?: EditorOption[] | CellObject[]` 입니다.
|
|
476
|
+
- `multiselect` 는 `dataType: 'array'` 열에 `objectKey` 를 주면 객체 배열 값의 체크 표시를 키로 맞춥니다. 처음과 같은 선택으로 확정하면 변경이 아닙니다.
|
|
477
|
+
- `custom` 에디터(`CustomEditorParams.value`)는 원본 객체를 받습니다. 값을 읽는 규칙은 [EditorConfig](#editorconfig) 의 `getValue` 를 보세요.
|
|
478
|
+
- **붙여넣기** — 객체 열 칸은 편집 우선순위를 따릅니다. `mask.parser` 열은 글자를 객체로 바꾸고, 실패(`undefined`·throw)한 칸은 **건너뜁니다**(붙여넣은 칸 수에도 안 셈, 옆 칸은 그대로 붙음). 블록 안의 빈 칸은 `null`. `displayKey` 경로 열(3순위)은 글자를 그 경로 값으로 씁니다(`null` 이면 새 객체). JSON 열(4순위)은 JSON 객체 글자만 넣고(빈 칸 → `null`) 아니면 그 칸을 건너뜁니다 — 따옴표가 든 칸은 TSV 규칙대로 `"{""id"":…}"` 로 이스케이프돼 있어야 합니다(그리드·엑셀 복사는 그렇게 씀). `editor` 만 있고 `parser` 가 없는 열·`displayKey` 가 함수인 열은 건너뜁니다. 삽입 붙여넣기(`mode: 'insert'`)의 실패 칸은 비워 둡니다(글자를 그대로 쓰지 않음).
|
|
479
|
+
- **복사** — 보이는 글자(`mask.formatter` → `displayKey` → JSON)가 복사됩니다. formatter 와 parser 가 서로 되돌릴 수 있으면(예: id ↔ 객체) 복사 → 붙여넣기 왕복이 같은 내용의 객체를 만듭니다.
|
|
480
|
+
|
|
481
|
+
**경고 코드**
|
|
482
|
+
|
|
483
|
+
| 코드 | 언제 | 그리드 동작 |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| `PS314` | `dataType: 'object'` 열인데 `objectKey` 가 없음 | 객체 내용을 속성 순서와 무관한 JSON 으로 만들어 키로 씀. 열마다 1회 경고 |
|
|
486
|
+
| `PS315` | `dataType` 이 `'object'` 가 아닌 열에 순수 객체 값이 있음 | 처음 그려지는 행 중 최대 50행을 보고 열마다 1회 경고. 계산 열·Date·배열 값은 대상 아님. 묶기·필터 키는 위와 같은 JSON |
|
|
487
|
+
|
|
488
|
+
**Worker 모드 (`loader: { worker }`)**
|
|
489
|
+
|
|
490
|
+
- `objectKey`·`displayKey`·`sortValue` 는 **경로 문자열** 또는 **등록 함수 참조**(`{ workerFunction: '키', params? }`, → [runtime-contracts](runtime-contracts.md))만 됩니다. 인라인 함수면 조용히 무시하지 않고 오류입니다: `Worker objectValue.<열>.objectKey requires { workerFunction: string, params? }; inline functions are not supported` (`displayKey` 면 `…<열>.displayKey …`). `displayKey` 는 셀 표시처럼 메인 스레드에서도 읽으므로, 등록 함수 참조로 주면 그 함수를 메인 스레드에도 등록하거나 경로 문자열을 쓰세요.
|
|
491
|
+
- `equals` 는 Worker 로 보내지 않습니다(변경 감지는 메인 스레드) — 인라인 함수여도 오류가 나지 않습니다.
|
|
492
|
+
- `mask.formatter`·`mask.parser` 는 화면 표시·편집·붙여넣기·래퍼 일괄 편집이 **메인 스레드**에서 부르므로 Worker 모드에서도 **일반 함수**로 주세요. 등록 함수 참조로 주면 메인 스레드에서 부를 수 없어(`Registered mask formatter requires Worker execution`) 라벨·입력 변환이 동작하지 않습니다.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// Worker 파일
|
|
496
|
+
setupGridWorker({ loader: MyLoader, functions: { 'objectValue:rankName': (v) => [v?.rank, v?.name] } });
|
|
497
|
+
// 메인 설정
|
|
498
|
+
{ id: 'owner', dataType: 'object', objectKey: 'id', sortValue: { workerFunction: 'objectValue:rankName' } }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**수식에서** — `[owner]` 처럼 객체 자체를 `&`·`+` 로 이으면 `[object Object]` 가 됩니다. `[owner.name]` 처럼 속성 경로를 쓰거나 함수형 `formula` 를 쓰세요(→ [계산 필드](#계산-필드)).
|
|
502
|
+
|
|
503
|
+
**그룹 결과 필터** — `groupFilters` 의 `basis: 'groupValue'` 조건도 행 필터와 같습니다. `in`/`notIn` 은 `objectKey` 로 비교하고(키 배열·객체 배열 모두), `eq`/`neq` 는 그룹 값이나 필터 값이 객체일 때 `objectKey` 로 비교합니다(mask 또는 `displayKey` 가 있고 필터 값이 문자열이면 보이는 글자 비교). 글자 조건·크기 비교도 행 필터와 같은 규칙입니다. 서버용 `createGroupFilterPredicate(model, groupColumns, { objectSpecs, fieldOf })` 도 `objectSpecs`(`buildObjectFieldSpecs(열 정의)`)를 주면 같은 규칙입니다.
|
|
504
|
+
|
|
505
|
+
**엑셀 가져오기 (`fromExcel`)**
|
|
506
|
+
|
|
507
|
+
| 칸 | 결과 |
|
|
508
|
+
|---|---|
|
|
509
|
+
| 객체 열의 JSON 객체 글자 (앞뒤 공백 제외 `{` 로 시작, `JSON.parse` 결과가 순수 객체) | 그 객체를 값으로 씀(`parser` 를 부르지 않음, `parser` 가 없는 열도). 원본 내보내기(`exportSource: true`) 파일 왕복용. 깨진 JSON·배열·원시값 JSON 은 아래 `parser` 경로. 가져오기에만 적용(붙여넣기·셀 편집은 그대로) |
|
|
510
|
+
| 객체 열의 글자 | 붙여넣기와 같은 규칙으로 `mask.parser` 변환. 빈 칸 → `null` |
|
|
511
|
+
| 변환 실패(`undefined`·throw) | 그 칸만 건너뜀(기존 값 유지, 새 행이면 값 없음). `result.conflicts` 에 `reason: 'parse_failed'`(행 번호·키·`field`·`column`·원문 `text`·throw 문구 `message`) |
|
|
512
|
+
| `parser` 가 없고 `displayKey` 가 경로 문자열인 객체 열 | JSON 객체 글자가 아니면 글자를 그 경로 값으로 씀(편집 3순위와 같음 — 기존 행은 `{ ...owner, name: 글자 }`, 값이 없으면 `{ name: 글자 }`). 경고 없음 |
|
|
513
|
+
| `parser` 도 경로 `displayKey` 도 없는 객체 열 | JSON 객체 글자가 아닌 칸은 쓰지도 지우지도 않음. 결과 경고 `OBJECT_PARSER_MISSING:<열 id>` 1회 |
|
|
514
|
+
| 같은 사람을 다시 가져옴 | `equals`(없으면 내용)가 같으면 변경 아님 |
|
|
515
|
+
| 점 표기 열(`owner.team`) | 기존 행·새 행 모두 `owner: { … }` 중첩 객체에 씀. 같은 행의 `owner` 객체 열을 먼저 쓰고 경로 열을 그 위에 덮어씀(파일 열 순서 무관) |
|
|
516
|
+
|
|
517
|
+
XLSX 내보내기는 객체 열을 `preserveFormatting` 과 관계없이 `mask.formatter` 글자(없으면 `displayKey` 글자)로 씁니다. 그래서 formatter·parser 가 서로 되돌릴 수 있으면 내보내기 → 가져오기 왕복의 변경이 0건입니다. 원본 내보내기(`exportSource: true`)는 함수 mask 를 쓰지 않는 정책이라 객체 열이 JSON 으로 나가고, 가져오기가 그 JSON 을 객체로 되돌리므로 이 왕복도 변경이 0건입니다(동명이인·`parser` 없는 열 포함).
|
|
518
|
+
|
|
519
|
+
**알려진 한계** — 객체 열을 피벗 축(`rowFields`/`columnFields`)으로 쓰는 것은 지원하지 않습니다(점 표기 열 `owner.name` 을 축으로 쓰세요). 행 필터의 `eq`/`neq` 는 위 규칙(객체면 `objectKey`)으로 비교합니다 — 여러 값은 `in` 필터를 쓰세요(래퍼 우클릭 "선택 값으로 필터"는 객체 셀에서 `in` 필터를 겁니다).
|
|
520
|
+
|
|
344
521
|
### 너비/크기
|
|
345
522
|
|
|
346
523
|
| 속성 | 타입 | 설명 |
|
|
@@ -356,7 +533,7 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
356
533
|
|
|
357
534
|
| 속성 | 타입 | 설명 |
|
|
358
535
|
|------|------|------|
|
|
359
|
-
| `align` | `'left' \| 'center' \| 'right'` | 셀 정렬 (dataType에서 자동
|
|
536
|
+
| `align` | `'left' \| 'center' \| 'right'` | 셀 정렬 (dataType에서 자동 추론: number→right, boolean→center, 그 외 left). 병합 셀(본문·고정 행)도 앵커 컬럼 정렬을 같은 규칙으로 따름 |
|
|
360
537
|
| `hidden` | `boolean` | 숨김 필드 |
|
|
361
538
|
| `lockVisible` | `boolean` | 가시성 잠금 (기본 `false`). 아래 참조 |
|
|
362
539
|
| `pinned` | `'left' \| 'right'` | 컬럼 고정 위치 |
|
|
@@ -385,6 +562,8 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
385
562
|
>
|
|
386
563
|
> ⚠️ 이것은 **표시 축**이며 필터 **능력**을 막지 않습니다. 툴바 필터 창·수식 등 다른 경로로는 여전히 조건을 걸고 **해제**할 수 있습니다 — 능력까지 막으면 이미 걸어 둔 조건을 되돌릴 수 없습니다. (AG Grid 도 `suppressHeaderFilterButton`(표시)과 `filter: false`(능력)를 분리합니다.)
|
|
387
564
|
>
|
|
565
|
+
> wrapper 우클릭 메뉴의 `필터 추가`·`선택 값으로 필터` 도 이 값을 보지 않습니다(ITSM-2026-004445). 두 항목의 활성 여부는 **툴바 고급 필터가 그 열을 후보로 주는지**와 같습니다 — 필드 목록(flat `getFields()`, 피벗은 행/열/값/필터 필드)에 있는 열이면 숨긴 열·아이콘 없는 열도 활성, 필드 목록에 없는 열(행 헤더·드래그 핸들 같은 합성 열)은 비활성입니다. `필터 해제` 는 이 판정과 무관하게 걸린 조건이 있으면 활성입니다.
|
|
566
|
+
>
|
|
388
567
|
> 정렬 쪽에는 대응 옵션이 **없습니다.** 정렬 진입점은 아이콘과 **헤더 셀 클릭** 둘이라 아이콘만 숨기면 "정렬은 되는데 상태 표시가 없는" 상태가 됩니다. 정렬을 막으려면 `sortable` 을 쓰세요.
|
|
389
568
|
|
|
390
569
|
> **⚠️ `filterable` 은 현재 미동작입니다 (ITSM-2026-002167, 실측)**
|
|
@@ -408,7 +587,7 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
408
587
|
| 속성 | 타입 | 설명 |
|
|
409
588
|
|------|------|------|
|
|
410
589
|
| `mask` | `MaskConfig` | 마스킹 설정 |
|
|
411
|
-
| `aggregate` | `AggregateFunc \| (values, ctx) => CellValue` | 그룹/피벗 집계 함수.
|
|
590
|
+
| `aggregate` | `AggregateFunc \| (values, ctx) => CellValue` | 그룹/피벗 집계 함수. `summaryRow` 가 켜져 있으면 하단 합계 행 집계에도 사용한다(기본 `'none'`). 표시를 꺼도 함수는 유지된다. 함수형은 그룹행 자기 칸·고정 합계 행 값을 계산한다 — [그룹 집계 표시 자리와 함수형 집계](#그룹-집계-표시-자리와-함수형-집계) |
|
|
412
591
|
|
|
413
592
|
### 그룹 행
|
|
414
593
|
|
|
@@ -434,6 +613,35 @@ await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
|
434
613
|
}
|
|
435
614
|
```
|
|
436
615
|
|
|
616
|
+
**문자열 수식의 이름 해석** — 이름은 **열 id 우선, 없으면 데이터 키**로 읽습니다(수식 필터·조건부 서식의 `$열id`와 같은 기준).
|
|
617
|
+
|
|
618
|
+
- 다른 계산 열 id → 그 계산 값 (`'total * 0.1'`)
|
|
619
|
+
- 열 id → 그 열의 `field` 값(화면에 보이는 값). 별칭 열 `{ id: 'price', field: 'cost' }`이면 `price`는 `row.cost`
|
|
620
|
+
- 열이 아닌 이름 → 데이터 키 그대로 (`'extra + 1'` → `row.extra`)
|
|
621
|
+
- 함수형 `formula: (row) => …`는 원본 행 객체를 받습니다(데이터 키 그대로).
|
|
622
|
+
|
|
623
|
+
예) 데이터 `{ qty: 3, cost: 10, price: 999, extra: 5 }`, 열 `qsum{field:'qty'}`·`price{field:'cost'}` → `'qty * price'` = 30, `'qsum * 2'` = 6, `'extra + 1'` = 6.
|
|
624
|
+
이름이 열 id이면서 다른 데이터 키(데이터에 같은 이름 키가 있거나 다른 열의 `field`)이기도 하면 열 id 쪽을 읽고 콘솔에 `PS313` 경고를 수식·이름 조합마다 한 번 냅니다(위 예의 `price`). 데이터 키를 읽으려면 함수형을 쓰거나 열 id를 바꾸세요.
|
|
625
|
+
|
|
626
|
+
**점 표기 경로 참조** — 이름에 점이 있으면 데이터 경로로 읽습니다. 규칙은 위와 같이 **열 id 우선 → 없으면 데이터 경로**입니다.
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// 데이터: { owner: { id: 'u2', name: '김철수', rank: 2 } }, 열 owner{dataType:'object'}, ownerName{field:'owner.name'}
|
|
630
|
+
{ id: 'label', formula: '[owner.name] & "님"', dataType: 'string' } // → '김철수님' (열이 아닌 이름 → 경로)
|
|
631
|
+
{ id: 'byColumn', formula: 'ownerName & "님"', dataType: 'string' } // → '김철수님' (열 id → 그 열 field 경로)
|
|
632
|
+
{ id: 'rank10', formula: 'owner.rank * 10' } // → 20 (괄호 없는 점 이름)
|
|
633
|
+
{ id: 'chain', formula: 'rank10 + 1' } // → 21 (계산 → 계산)
|
|
634
|
+
// owner 가 null 인 행: label = '님', owner.rank = undefined
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
- 행에 `'owner.name'` 키가 그대로 있으면 그 키가 경로보다 먼저입니다.
|
|
638
|
+
- 이름이 점 표기인 **열 id** 가 있으면 그 열이 이깁니다 — 열 `{ id: 'owner.name', field: 'nick' }` 이 있으면 `[owner.name]` 은 `row.nick` 을 읽습니다.
|
|
639
|
+
- 의존성은 최상위 키(`owner`)입니다. `owner` 를 새 객체로 바꾸면 경로 수식이 다시 계산됩니다(같은 객체를 공유하던 다른 행은 그대로).
|
|
640
|
+
- 객체 자체(`[owner]`)를 `&`·`+` 로 이으면 `[object Object]` 입니다 — 속성 경로(`[owner.name]`)를 쓰세요.
|
|
641
|
+
- 메인 스레드와 Worker 모드가 같은 규칙입니다.
|
|
642
|
+
|
|
643
|
+
지원 문법: 이름은 영문·숫자·`_`(첫 글자 숫자 불가), 점 표기 경로 `owner.name`(점 뒤 조각도 영문·`_` 로 시작해야 이어짐 — 숫자 조각은 `[items.0]` 처럼 괄호로 쓰면 경로로 읽음), `[이름]`(괄호 안 글자 그대로 — 공백·점 허용, 함수로 보지 않음: `[unit price] * 2`), 숫자·문자열(`'…'`) 리터럴, `+ - * / %`, `&` 문자열 결합(Excel 과 같음 — `null` 은 빈 글자, `+`·`-` 보다 나중·비교보다 먼저: `qty + 1 & "개"` → `'3개'`), 비교 `> < >= <= == !=`, `&& || !`, `조건 ? 값 : 값`, 함수 `IF YEAR MONTH DAY ABS ROUND FLOOR CEIL UPPER LOWER CONCAT LEN MIN MAX`. 닫는 `]` 가 없거나(`[owner.name`) 점으로 끝나는 이름(`owner.`)은 수식 오류입니다.
|
|
644
|
+
|
|
437
645
|
### 렌더링
|
|
438
646
|
|
|
439
647
|
| 속성 | 타입 | 설명 |
|
|
@@ -486,10 +694,15 @@ PureSheet 인스턴스에서 직접 호출하는 메서드들입니다. Sub-API
|
|
|
486
694
|
| 메서드 | 반환 | 설명 |
|
|
487
695
|
|--------|------|------|
|
|
488
696
|
| `waitForReady()` | `Promise<void>` | 초기 렌더링 완료까지 대기 |
|
|
697
|
+
| `waitForSettled(signal?: AbortSignal)` | `Promise<void>` | 초기화와 core가 추적하는 activity·undo 작업 대기. 취소는 `AbortError`, disposed·초기화 실패는 rejection |
|
|
698
|
+
| `captureDataSession()` | `DataSessionSnapshot \| null` | 준비된 live·settled 인스턴스의 동기 세션 캡처 ([지원 범위](runtime-contracts.md)) |
|
|
699
|
+
| `captureDataSessionAsync()` | `Promise<DataSessionSnapshot \| null>` | Worker 소스 캡처 포함. **완료 후** 기존 인스턴스를 destroy |
|
|
489
700
|
| `refresh()` | `Promise<void>` | 데이터 재처리 + 렌더링 갱신 |
|
|
490
701
|
| `notifyResize()` | `void` | 컨테이너 가시성/크기 변경을 호스트가 명시적으로 알리는 훅. viewport 재측정 + 스크롤 재동기화 + 가상 가시범위 재계산 → 재렌더. 데이터/컬럼 변경 없이 레이아웃만 다시 맞추므로 `refresh()`보다 가벼움 (ResizeObserver로 감지 못 한 컨테이너 표시/크기 변경 시나리오용) |
|
|
491
702
|
| `destroy()` | `void` | 리소스 해제, DOM 정리 |
|
|
492
703
|
|
|
704
|
+
`waitForSettled()`는 임의의 호스트 fetch·renderer의 모든 외부 비동기 작업을 기다리지 않습니다. `captureDataSessionAsync()` 자체는 준비/settled 대기를 대신하지 않습니다. 먼저 `await grid.waitForSettled()`를 호출하고 캡처 중 새 작업을 시작하지 마세요. 준비 전·disposed·진행 중 작업이 있거나 열린 편집을 확정하지 못하면 거절하며 캡처 중 상태 변경도 거절될 수 있습니다. 토큰은 같은 프로세스의 불투명 세션 이전용이며 JSON 프리셋이 아닙니다. [지원 범위와 복원 계약](runtime-contracts.md)을 참조하세요.
|
|
705
|
+
|
|
493
706
|
### 데이터
|
|
494
707
|
|
|
495
708
|
| 메서드 | 반환 | 설명 |
|
|
@@ -499,9 +712,10 @@ PureSheet 인스턴스에서 직접 호출하는 메서드들입니다. Sub-API
|
|
|
499
712
|
| `getVisibleData()` | `Promise<RowData[]>` | 필터/정렬 적용된 processor 데이터 조회. 하위 호환 API라 rowGroup/tree collapse로 renderer에서 숨긴 자식 행은 제외하지 않음. 커밋 전 드래그 재정렬도 반영하지 않음(원래 순서) — 화면 순서는 `getDisplayedData()` |
|
|
500
713
|
| `getDisplayedData()` | `Promise<RowData[]>` | 현재 renderer view-index 순서의 순수 도메인 데이터 행. rowGroup/tree collapse 반영(접힌 자식 제외), group header/subtotal/grand-total/master-detail 구조 행 제외, DOM/viewport 가상화와 무관 |
|
|
501
714
|
| `getDisplayedRowIds()` | `Promise<RowId[]>` | `getDisplayedData()`와 같은 순서의 도메인 rowId 목록 |
|
|
715
|
+
| `isServerRowModel()` | `boolean` | 서버 행 모델(`rowModel: 'server'`)인지. 받지 않은 행이 있어 "보이는 행 전체" 작업은 행 ID 목록 대신 `'all'` 범위로 해야 합니다 |
|
|
502
716
|
| `rows.displayedCount()` | `number` | displayed 좌표계 총 행 수(그룹/트리 collapse 반영). traversal 경계 계산용 |
|
|
503
717
|
| `rows.getByDisplayedIndex(i)` | `Promise<DisplayedRowInfo \\| null>` | displayed 좌표 i의 단일 행 정보(O(1)~O(log g), 배열 materialize 없음). 다음/이전 이동 등 traversal용. 범위 밖 `null` |
|
|
504
|
-
| `rows.getDisplayedIndexById(id)` | `Promise<number>` | rowId → displayed
|
|
718
|
+
| `rows.getDisplayedIndexById(id)` | `Promise<number>` | rowId → displayed 인덱스(`getByDisplayedIndex`의 역: 정렬·필터·저장 전 추가 행을 반영한 화면 순번. 예: 원본 [1,2] 앞에 추가한 행 `'9'` → `'9'`=0, `1`=1). 접힘/필터로 숨겨졌으면 `-1`. 피벗 모드의 rowId는 피벗 결과 행의 `nodeId`(`getActiveCell().row.id`·`getSelectedRowIds()`와 같은 값, 예: `'g:G1\|i:I1'`·소계 `'g:G1'`·`'__grandtotal__'`)이며, 자기 행이 없는 그룹 id(소계 숨김)는 `-1`. rowId만으로 호출 시 idMap O(N) 유발 가능하므로 대용량 traversal은 index 보관 권장 |
|
|
505
719
|
| `getRowById(rowId)` | `Promise<RowData \| undefined>` | ID로 단일 행 조회 |
|
|
506
720
|
| `getRowIndexById(rowId)` | `Promise<number>` | ID → 데이터 인덱스 |
|
|
507
721
|
| `getRowVirtualIndexById(rowId)` | `Promise<number>` | ID → 가상 인덱스 (그룹 행 포함) |
|
|
@@ -524,7 +738,10 @@ await grid.changes.commit();
|
|
|
524
738
|
|
|
525
739
|
| 메서드 | 반환 | 설명 |
|
|
526
740
|
|--------|------|------|
|
|
527
|
-
| `sort(
|
|
741
|
+
| `sort(input: SortState[] \| SortGroupMap)` | `Promise<void>` | 정렬 전체 교체. 다른 소유자 보존은 `setSortGroup` 사용 |
|
|
742
|
+
| `setSortGroup(key: string, group: SortGroupMap[string] \| null)` | `Promise<void>` | 한 소유자의 정렬만 교체. `null`은 해당 소유자 삭제, 나머지 보존 |
|
|
743
|
+
| `getSortGroup(key: string)` | `SortGroupMap[string] \| undefined` | 소유자별 정렬 조회 |
|
|
744
|
+
| `getSortGroups()` | `SortGroupMap` | 전체 소유자 정렬 맵의 복사본 조회 |
|
|
528
745
|
| `filter(filters)` | `Promise<void>` | 필터 적용. `FilterState[]`(평면, 하위 호환) 또는 **`FilterGroupMap`**(소스별 그룹) 둘 다 받음 |
|
|
529
746
|
| `setFilterGroup(key, group)` | `Promise<void>` | 그룹 하나만 교체/삭제(`null`=삭제). 다른 그룹은 건드리지 않음 |
|
|
530
747
|
| `getFilterGroup(key)` | `FilterGroup \| undefined` | 그룹 하나 조회 (sync) |
|
|
@@ -578,7 +795,7 @@ await grid.filter([{ id: 'dept', operator: 'in', filterValue: res.rawValues, seq
|
|
|
578
795
|
| 메서드 | 반환 | 설명 |
|
|
579
796
|
|--------|------|------|
|
|
580
797
|
| `updateLoaderParams(params)` | `Promise<void>` | 로더 파라미터 변경 + 재조회 |
|
|
581
|
-
| `refreshLoaderData()` | `Promise<void>` | 로더 데이터
|
|
798
|
+
| `refreshLoaderData()` | `Promise<void>` | 로더 데이터 재조회. 성공하면 `data:loaded` 발행(SSRM 제외) |
|
|
582
799
|
|
|
583
800
|
### 스크롤 / 내비게이션
|
|
584
801
|
|
|
@@ -616,6 +833,17 @@ grid.clearSearch(); // 종료
|
|
|
616
833
|
|
|
617
834
|
### 레이아웃 / UI
|
|
618
835
|
|
|
836
|
+
추가 편의 API:
|
|
837
|
+
|
|
838
|
+
| 메서드 | 반환 | 설명 |
|
|
839
|
+
|---|---|---|
|
|
840
|
+
| `repaintRow(rowId: RowId)` | `void` | `repaintRows([rowId])`의 단일 행 편의 함수 |
|
|
841
|
+
| `getSummaryRow()` | `SummaryRowPosition` | 현재 합계 행 위치: `'none' \| 'pinnedTop' \| 'pinnedBottom'` |
|
|
842
|
+
| `registerShortcut(combo: string, handler: () => void \| Promise<void>)` | `Unsubscribe` | 단축키 등록. 같은 combo는 기존/기본 단축키를 덮어씀. 반환 해제 함수를 소비처 lifecycle에서 호출 |
|
|
843
|
+
| `hasShortcut(combo: string)` | `boolean` | 해당 단축키 등록 여부 |
|
|
844
|
+
|
|
845
|
+
단축키는 `'ctrl+shift+v'`처럼 정규화한 문자열이며 수식어 순서는 `ctrl → meta → shift → alt`입니다.
|
|
846
|
+
|
|
619
847
|
| 메서드 | 반환 | 설명 |
|
|
620
848
|
|--------|------|------|
|
|
621
849
|
| `setDynamicRowHeights(enabled)` | `void` | 동적 행 높이 켜기/끄기 |
|
|
@@ -663,7 +891,7 @@ grid.clearSearch(); // 종료
|
|
|
663
891
|
|
|
664
892
|
| 메서드 | 반환 | 설명 |
|
|
665
893
|
|--------|------|------|
|
|
666
|
-
| `getTotalRowCount()` | `number` | 전체 행 수 (필터 전) |
|
|
894
|
+
| `getTotalRowCount()` | `number` | 전체 행 수 (필터 전). 피벗은 화면 피벗 행 수(소계·총합계 포함) — 원본 행 수는 `pivot.getSourceRowCount()` |
|
|
667
895
|
| `getVisibleRowCount()` | `number` | 가시 행 수 (필터 후) |
|
|
668
896
|
|
|
669
897
|
### 내보내기
|
|
@@ -937,21 +1165,22 @@ PureSheet 인스턴스는 기능별 Sub-API 네임스페이스를 제공합니
|
|
|
937
1165
|
| `selectRowsByViewIndices(viewIndices)` | `void` | viewIndex(화면 물리 순서) 배열로 행 선택. 정렬/필터 후에도 "화면상 그 위치"를 선택(sourceIndex 아님), data 행만 |
|
|
938
1166
|
| `selectRowRangeByViewIndex(startViewIndex, endViewIndex)` | `void` | viewIndex 범위로 행 선택. "화면상 N~M번째" 범위, 구조행 자동 제외 |
|
|
939
1167
|
| `selectRowRangeByIds(startRowId, endRowId)` | `void` | rowId 범위로 행 선택(정렬/필터 안정). 한쪽이라도 숨김이면 no-op |
|
|
940
|
-
| `
|
|
941
|
-
| `
|
|
1168
|
+
| `selectAllRows()` | `void` | 전체 행 선택 (셀 선택과 구분) |
|
|
1169
|
+
| `clearRowSelection()` | `void` | 행 선택만 해제 (셀 선택 유지) |
|
|
1170
|
+
| `clearSelection()` | `void` | 행·셀 선택 모두 해제 |
|
|
942
1171
|
|
|
943
1172
|
#### 선택 조회 (행 — grid.rows)
|
|
944
1173
|
|
|
945
1174
|
| 메서드 | 반환 | 설명 |
|
|
946
1175
|
|--------|------|------|
|
|
947
|
-
| `
|
|
1176
|
+
| `getSelectedRowsAccessor()` | `MultiRowAccessor` | 선택된 행 접근자 |
|
|
948
1177
|
| `getSelectedRowIds()` | `(string \| number)[]` | 선택된 행 rowId |
|
|
949
1178
|
| `getSelectedRowIndices()` | `Promise<number[]>` | 선택된 행 sourceIndex |
|
|
950
1179
|
| `getSelectedRowViewIndices()` | `Promise<number[]>` | 선택된 행 viewIndex(화면 물리 순서) |
|
|
951
1180
|
| `getSelectedRowDisplayedIndices()` | `Promise<number[]>` | 선택된 행 displayedIndex(화면 표시 순서, 접힘/필터 반영). 숨김 행 제외 |
|
|
952
|
-
| `selectionAggregates()` | `SelectionAggregateResult \| null
|
|
1181
|
+
| `selectionAggregates()` | `Promise<SelectionAggregateResult \| null>` | 선택 범위 집계 (컬럼별 + 총 집계). `const aggregates = await grid.rows.selectionAggregates();`로 조회. 반환값은 칸 수와 무관하지만, 내장 상태 표시줄·wrapper 하단 푸터는 `total.totalCount`(선택 칸 수) **2 이상**일 때만 집계를 보이고, 값이 `null`인 집계는 `-`로 표시한다. 셀 값은 화면과 같이 `field ?? id`로 읽어(`{ id: 'nmin', field: 'name' }` → `name` 값) 드래그·Shift·Ctrl+A·행 선택·함수형 `ctx`가 같은 값이다 |
|
|
953
1182
|
|
|
954
|
-
> **displayed 좌표계 조회**(`displayedCount`/`getByDisplayedIndex`/`getDisplayedIndexById`)는 위 [행 조회](#
|
|
1183
|
+
> **displayed 좌표계 조회**(`displayedCount`/`getByDisplayedIndex`/`getDisplayedIndexById`)는 위 [행 조회](#gridrows--행-api) 표 참조 — 다음/이전(traversal) 이동에 사용.
|
|
955
1184
|
|
|
956
1185
|
### grid.cells — 셀 선택 API
|
|
957
1186
|
|
|
@@ -965,13 +1194,14 @@ PureSheet 인스턴스는 기능별 Sub-API 네임스페이스를 제공합니
|
|
|
965
1194
|
| `selectCellsByViewIndices(cells)` | `void` | `{viewIndex, columnId}` 배열로 셀 선택 |
|
|
966
1195
|
| `selectCellRangeByViewIndex(start, end)` | `void` | `{viewIndex, columnId}` 두 점으로 사각형 범위 선택 |
|
|
967
1196
|
| `selectCellRangeByIds(start, end)` | `void` | `{rowId, columnId}` 두 점으로 사각형 범위 선택(정렬/필터 안정) |
|
|
1197
|
+
| `selectAllCells()` | `void` | 전체 셀 선택 (행 선택 유지) |
|
|
968
1198
|
| `clearCellSelection()` | `void` | 셀 선택만 해제 (행 선택 유지) |
|
|
969
1199
|
|
|
970
1200
|
#### 선택 조회 (셀 — grid.cells)
|
|
971
1201
|
|
|
972
1202
|
| 메서드 | 반환 | 설명 |
|
|
973
1203
|
|--------|------|------|
|
|
974
|
-
| `getSelectedCellIds()` | `{rowId, columnId}[]` | 선택된 셀 목록 (셀 API 에는 `selected()` 가 없다 — 행 API `grid.rows.
|
|
1204
|
+
| `getSelectedCellIds()` | `{rowId, columnId}[]` | 선택된 셀 목록 (셀 API 에는 `selected()` 가 없다 — 행 API `grid.rows.getSelectedRowsAccessor()` 와 다름) |
|
|
975
1205
|
| `getSelectedCells()` | `SelectedCell[]` | 선택된 셀 상세 (행 핸들·값 포함) |
|
|
976
1206
|
| `getActiveCell()` | `SelectedCell \| null` | 현재 활성 셀 — `selection.getActiveCell()`과 같음(화면 자리 기준, 아래 표 참고) |
|
|
977
1207
|
| `getFocusCell()` | `SelectedCell \| null` | 논리적 선택 끝점 — `selection.getFocusCell()`과 같음. [끝점·데이터 세션 상세](runtime-contracts.md) |
|
|
@@ -992,10 +1222,12 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
|
|
|
992
1222
|
| `selection.getSelectedCells()` | `{ row: RowHandle; columnId }[]` | 현재 선택된 셀들 |
|
|
993
1223
|
| `selection.getActiveCell()` | `{ row: RowHandle; columnId } \| null` | 마지막 상호작용 셀. traversal 컬럼 유지용. 행은 **화면 자리 기준** — 셀 선택(`getSelectedCellIds`)·화면 강조·키보드 이동과 같은 행이다. 정렬·필터·그룹/트리 접기·디테일 펼침·드래그 재정렬로 그 자리에 다른 행이 오면 그 행, 자리가 그룹 배너·디테일 행이거나 사라지면 `null`. 행 선택 모드(셀 선택 없음)의 앵커와 고정(pinned) 행은 클릭한 행 그대로 |
|
|
994
1224
|
| `selection.getFocusCell()` | `SelectedCell \| null` | 현재 유효한 논리적 선택 끝점 `{ row: RowHandle, columnId }`. traversal 앵커인 `getActiveCell()`과 별개. 전체 선택 열거·비동기 fetch 없이 조회하며 확인 불가능한 identity는 null. [상세 계약](runtime-contracts.md) |
|
|
995
|
-
| `selection.getSelectedGroups()` | `{ groupId: string;
|
|
1225
|
+
| `selection.getSelectedGroups()` | `{ groupId: string; groupKeys: CellValue[]; rowCount: number; rowIds?: RowId[] }[]` | 선택된 그룹 (`rowGroupConfig.selectable` — [그룹 배너 선택](#그룹-배너-선택-selectable)). `groupKeys`는 그룹 값 경로, `rowCount`는 그룹의 데이터 행 수. `rowIds`는 리프 멤버 전체(접힘 무관, 중첩이면 하위 리프 전부)이며 `rowModel: 'server'` 서버 그룹에서는 없음(멤버를 다 받지 않음 — `groupKeys`와 현재 필터로 서버에서 처리). 옵션이 꺼져 있으면 `[]`. 행 선택 결과와 별개 |
|
|
996
1226
|
| `selection.selectGroupsByIds(groupIds, { addToExisting? })` | `string[]` | 그룹을 코드로 선택 — 배너 본문 클릭과 같은 의미([그룹 배너 선택](#그룹-배너-선택-selectable)). 적용된 groupId(입력 순서, 중복 제거)를 반환. 화면에 없는 그룹·옵션 꺼짐은 무시 |
|
|
997
1227
|
| `selection.selectGroupsByViewIndices(viewIndices, { addToExisting? })` | `string[]` | viewIndex로 그룹 선택. 그룹행이 아닌 위치는 무시. 나머지는 `selectGroupsByIds`와 같음 |
|
|
998
1228
|
|
|
1229
|
+
> **범위 선택의 시작·끝 셀**: SA_02에서 SA_03까지 드래그(또는 Shift+클릭·Shift+↓)하면 `getActiveCell()` = SA_02(시작), `getFocusCell()` = SA_03(끝)이고, 아래→위로 고르면 둘이 바뀝니다. 끝 행: `grid.selection.getFocusCell()?.row.data`. 방향과 관계없이 아래쪽 행(Wijmo `selection.bottomRow`에 해당)이 필요하면 `getSelectedCells()`에서 `row.viewIndex`가 가장 큰 셀을 고릅니다.
|
|
1230
|
+
|
|
999
1231
|
> **피벗 모드의 행(RowHandle)**: `row.id`는 피벗 행 nodeId(`'g:G1|i:I1'`, 소계 `'g:G1'`, 총합계 `'__grandtotal__'`), `row.data`는 **화면의 피벗 행** — `cell:click`/`cell:dblclick`의 `row`, `contextmenu`의 `rowData`와 같은 객체입니다. 내용은 `pivot.getAllRows()` 한 행의 `rowHeaders`와 `values`를 한 객체로 펼친 것에 메타 필드(`__pivotType: 'data' | 'subtotal' | 'grandtotal'`, `__nodeId`, `__parentNodeId`, `__level`, `__hasChildren`)를 더한 것입니다. 소계 행은 하위 행 필드에 `'Subtotal'`이, 총합계 행은 첫 행 필드에 `'Grand Total'`이 들어갑니다(화면 표시와 같음). 원본 행이 아닙니다. 접힘으로 화면에 없는 피벗 행은 `data`가 비어 있습니다. 예: 드래그 시작 셀의 행축 값 `const d = grid.selection.getActiveCell()?.row.data; d?.__pivotType === 'data' && d.g`.
|
|
1000
1232
|
|
|
1001
1233
|
|
|
@@ -1011,7 +1243,7 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
|
|
|
1011
1243
|
|
|
1012
1244
|
> `pinTop*`/`pinBottom*`은 본문 데이터 행을 고정 영역으로 **옮깁니다** — `rows.getById(id).setProps({ pinned: 'top' | 'bottom' })`와 같은 동작입니다. 호출 후 그 행은 본문에서 빠지고 고정 영역에 한 번만 보이며, 고정 영역에서의 편집은 원본 데이터에 반영됩니다(`changes` 포함). `unpin`/`unpinById`/`clearPinned`로 본문 원래 위치로 돌아옵니다. 같은 행을 다시 고정해도 중복되지 않고, `pinTop` ↔ `pinBottom`은 영역 이동입니다. 정렬·필터·그룹·트리 상태에서도 동작합니다.
|
|
1013
1245
|
> `pinTop`/`pinBottom`은 **데이터 행 객체**를 받아 키(`keyFields`)로 본문 행을 찾습니다. 본문에 없는 객체/ID를 주면 기존처럼 화면 전용 고정 행으로 추가됩니다(`addPinnedTop`/`addPinnedBottom`과 동일). `Row` 인스턴스는 그대로 꽂힙니다. 새로운 행/구성을 추가하려면 [구조적 행 고정](#구조적-행-고정) 섹션의 `addPinnedTop` / `addPinnedBottom`을 사용하세요.
|
|
1014
|
-
> Worker 모드(`
|
|
1246
|
+
> Worker 모드(`loader.worker`)에서는 메인 스레드에 원본 데이터가 없어 `pinTop`/`pinTopById`/`pinBottom`/`pinBottomById`가 본문 행을 동기로 찾지 못합니다. 이때는 화면 전용 고정 행으로 추가되고 `PS503` 경고가 콘솔에 나옵니다(본문 행 이동·편집 반영·해제 시 본문 복귀 없음). `pin*ByIndex`는 Worker 모드에서 행을 찾지 못해 예외를 던집니다.
|
|
1015
1247
|
> `enableReferCache: false`에서도 동작합니다(`*ByIndex`는 O(1), 객체·ID는 원본 데이터를 한 번 훑어 찾음). `setProps({ pinned })`로 직접 고정한 행도 같은 방식으로 원본 위치를 찾아 표시·클릭·우클릭·편집이 동작합니다.
|
|
1016
1248
|
|
|
1017
1249
|
| 메서드 | 반환 | 설명 |
|
|
@@ -1188,7 +1420,11 @@ grid.columns.update('amount', { header: '판매금액' }, { updateSnapshot: true
|
|
|
1188
1420
|
|
|
1189
1421
|
| 메서드 | 반환 | 설명 |
|
|
1190
1422
|
|--------|------|------|
|
|
1191
|
-
| `addRow(
|
|
1423
|
+
| `addRow(newRow, anchorRow?, options?)` | `Promise<string \| number>` | 행 객체(`anchorRow`) **바로 앞**에 새 행 추가 (dirty). 생략하면 맨 끝. 두 번째 인자에 숫자를 넘기면 `PS805` — 위치로 넣으려면 `addRowByIndex` |
|
|
1424
|
+
| `addRowById(newRow, rowId, sourceIndex?, options?)` | `Promise<string \| number>` | 그 ID 행 **바로 앞**에 추가. 저장 전 새 행도 기준이 될 수 있습니다. 원래 행을 기준으로 할 때 `sourceIndex` 힌트를 주면 `enableReferCache` 없이도 찾습니다(힌트가 틀리면 버리고 ID로 다시 찾음, 못 찾으면 `PS804`) |
|
|
1425
|
+
| `addRowByIndex(newRow, sourceIndex?, options?)` | `Promise<string \| number>` | 원본 행 사이 틈 `sourceIndex`의 **맨 앞**에 추가. `0`=진짜 맨 위, `1`=원본 0번 행 바로 뒤, 생략·행 수 이상=맨 끝. 같은 값을 반복하면 나중 행이 위에 옵니다(`A, X2, X1, B`). 음수·소수는 `PS006` |
|
|
1426
|
+
| `addRowBeforeSelection(newRow, options?)` | `Promise<string \| number>` | 선택한 행(셀 선택 포함) 중 화면에서 가장 위 행 **앞**에 추가. 선택이 없으면 맨 위. `enableReferCache` 불필요 |
|
|
1427
|
+
| `addRowAfterSelection(newRow, options?)` | `Promise<string \| number>` | 선택한 행 중 화면에서 가장 아래 행 **뒤**에 추가. 선택이 없으면 맨 끝. `enableReferCache` 불필요 |
|
|
1192
1428
|
| `updateCell(row, field, value)` | `Promise<void>` | 행 객체로 셀 수정 (`enableReferCache` 필요) |
|
|
1193
1429
|
| `updateCellById(id, field, value, sourceIndex?)` | `Promise<void>` | ID로 셀 수정. 호출자가 sourceIndex를 알고 있으면 4번째 인자로 전달하여 idMap fallback을 회피할 수 있음 |
|
|
1194
1430
|
| `updateCellByIndex(idx, field, value)` | `Promise<void>` | sourceIndex로 셀 수정 O(1) |
|
|
@@ -1202,6 +1438,21 @@ grid.columns.update('amount', { header: '판매금액' }, { updateSnapshot: true
|
|
|
1202
1438
|
| `undeleteRowsById(ids)` | `Promise<number>` | ID 배열로 다중 삭제 복원 |
|
|
1203
1439
|
| `deleteSelectedRows()` | `Promise<number>` | 선택 행 삭제 (삭제된 건수 반환). 참고: `grid.rows.deleteSelectedRows()`와 동작은 동일하지만 ChangesAPI 쪽 메서드 이름은 `deleteSelectedRows`로 다르다 |
|
|
1204
1440
|
|
|
1441
|
+
`add*` 공통 `options`: `{ startEdit?: boolean | columnId }`(기본 꺼짐). 켜면 추가 직후 그 행의 첫 편집 가능 칸(또는 지정 컬럼)에서 편집을 시작합니다 — `grid.editing.startEditById(새 id, columnId)`와 같고, undo batch 안에서는 무시됩니다.
|
|
1442
|
+
|
|
1443
|
+
- 새 행의 `sourceIndex`는 `undefined`입니다(원본 배열에 없음). 선택 행 기준으로 추가할 때는 `getSelectedRows()[0]?.sourceIndex`를 넘기지 말고 `addRowBeforeSelection` / `addRowAfterSelection`을 쓰세요.
|
|
1444
|
+
- 정렬이 켜져 있으면 새 행은 위치와 관계없이 맨 위에 보입니다.
|
|
1445
|
+
|
|
1446
|
+
```ts
|
|
1447
|
+
// 선택 행 바로 아래에 추가하고 이름 칸 편집 시작
|
|
1448
|
+
await grid.changes.addRowAfterSelection({ name: '' }, { startEdit: 'name' });
|
|
1449
|
+
// 원래 행 기준(힌트 포함) — enableReferCache 없이 동작
|
|
1450
|
+
const r = grid.selection.getSelectedRows()[0];
|
|
1451
|
+
await grid.changes.addRowById({}, r.id, r.sourceIndex);
|
|
1452
|
+
// 진짜 맨 위
|
|
1453
|
+
await grid.changes.addRowByIndex({}, 0);
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1205
1456
|
#### 변경 취소
|
|
1206
1457
|
|
|
1207
1458
|
| 메서드 | 반환 | 설명 |
|
|
@@ -1260,7 +1511,7 @@ await grid.changes.commit({ allowInvalid: true });
|
|
|
1260
1511
|
|
|
1261
1512
|
| 메서드 | 반환 | 설명 |
|
|
1262
1513
|
|--------|------|------|
|
|
1263
|
-
| `bulkUpdate(columnId, value, scope?)` | `Promise<number>` | 일괄 셀
|
|
1514
|
+
| `bulkUpdate(columnId, value, scope?)` | `Promise<number>` | 일괄 셀 업데이트. `columnId`는 열 id이고 값은 그 열의 `field`에 씁니다(예: `{ id:'nm', field:'name' }` → `name`). `scope`: `'selected'`(기본)·`'all'`·행 ID 배열(문자열로 비교) — Worker 모드도 같습니다. `rowModel: 'server'` 는 받지 않은 행까지 범위 수정(`rangeUpdates`)으로 남깁니다 |
|
|
1264
1515
|
|
|
1265
1516
|
#### 행 재정렬
|
|
1266
1517
|
|
|
@@ -1272,6 +1523,22 @@ await grid.changes.commit({ allowInvalid: true });
|
|
|
1272
1523
|
|
|
1273
1524
|
---
|
|
1274
1525
|
|
|
1526
|
+
### grid.editing — 코드로 셀 편집
|
|
1527
|
+
|
|
1528
|
+
| 메서드 | 반환 | 설명 |
|
|
1529
|
+
|--------|------|------|
|
|
1530
|
+
| `startEditById(rowId, columnId?, options?)` | `Promise<boolean>` | 그 행·칸에서 편집 시작. `columnId` 생략 시 편집 가능한 첫 보이는 칸 |
|
|
1531
|
+
| `startEditByIndex(sourceIndex, columnId?, options?)` | `Promise<boolean>` | 원본 위치로 행을 찾아 편집 시작 |
|
|
1532
|
+
| `startEditActiveCell(options?)` | `Promise<boolean>` | 활성 칸에서 편집 시작 (F2와 같음) |
|
|
1533
|
+
| `commitEdit()` | `Promise<boolean>` | 열린 편집 확정. 검증 등으로 막히면 `false`, 열린 편집이 없으면 `true` |
|
|
1534
|
+
| `cancelEdit()` | `void` | 열린 편집 취소 |
|
|
1535
|
+
|
|
1536
|
+
- `options`: `{ initialValue?: string; scroll?: boolean = true }`. `initialValue`는 문자를 친 것처럼 그 값으로 시작합니다.
|
|
1537
|
+
- 순서: 열린 편집 확정(막히면 `false`) → 그 칸 선택 → 보이게 스크롤 → `cell:beforeEdit`(취소 가능) → 편집기 열기.
|
|
1538
|
+
- `false`가 되는 경우: 편집 가능한 칸이 없음(readonly·`editable:false`·`editableWhen`·키 컬럼 정책), 필터·접힌 그룹으로 숨겨짐, 전역 편집 불가, 피벗 모드, 행을 못 찾음, `scroll:false`인데 화면 밖.
|
|
1539
|
+
|
|
1540
|
+
---
|
|
1541
|
+
|
|
1275
1542
|
### grid.undoManager — Undo/Redo API
|
|
1276
1543
|
|
|
1277
1544
|
| 메서드/속성 | 반환 | 설명 |
|
|
@@ -1325,7 +1592,7 @@ await grid.changes.commit({ allowInvalid: true });
|
|
|
1325
1592
|
| `setConfig(config)` | `Promise<void>` | 그룹화 설정 (`null`로 해제) |
|
|
1326
1593
|
| `groupBy(columns)` | `Promise<void>` | 컬럼으로 그룹화 |
|
|
1327
1594
|
| `clear()` | `Promise<void>` | 그룹화 해제 |
|
|
1328
|
-
| `getConfig()` | `
|
|
1595
|
+
| `getConfig()` | `RowGroupConfig \| null` | 현재 그룹화 설정 조회 (내부 참조) |
|
|
1329
1596
|
| `toggle(groupId)` | `Promise<void>` | 그룹 접힘/펼침 토글 |
|
|
1330
1597
|
| `expand(groupId)` | `Promise<void>` | 특정 그룹 펼침 |
|
|
1331
1598
|
| `collapse(groupId)` | `Promise<void>` | 특정 그룹 접힘 |
|
|
@@ -1356,7 +1623,7 @@ await grid.changes.commit({ allowInvalid: true });
|
|
|
1356
1623
|
| Shift+배너 클릭 | 마지막으로 클릭한 그룹 ~ 클릭한 그룹 사이의 그룹 전부 |
|
|
1357
1624
|
| ▾ 버튼 클릭 / Ctrl+▾ | 접기/펼치기 / 같은 depth 일괄 접기/펼치기 (선택은 바뀌지 않음) |
|
|
1358
1625
|
|
|
1359
|
-
- 결과: `grid.selection.getSelectedGroups()` → `[{ groupId, rowIds }]`, `selection:changed`의 `selectedGroups`. `rowIds`는 그룹의 리프 멤버 전체(접혀 있어도, 중첩 그룹이면 하위 리프 전부)이며 표기는 `getGroupRows`와 같습니다(단일 키 원시값).
|
|
1626
|
+
- 결과: `grid.selection.getSelectedGroups()` → `[{ groupId, groupKeys, rowCount, rowIds }]`, `selection:changed`의 `selectedGroups`. 서버 그룹(SSRM)에서는 `rowIds` 가 없습니다. `rowIds`는 그룹의 리프 멤버 전체(접혀 있어도, 중첩 그룹이면 하위 리프 전부)이며 표기는 `getGroupRows`와 같습니다(단일 키 원시값).
|
|
1360
1627
|
- 행 선택(`rowSelection`) 결과는 바뀌지 않습니다 — 그룹 멤버를 행 선택에 넣으려면 소비처가 `rowIds`를 사용합니다.
|
|
1361
1628
|
- 배너에서 고른 그룹은 배너와 그룹행 셀을 행 선택 색으로 칠합니다(`.ps-group-banner-selected`, `.ps-group-row-selected`). 범위가 지나간 그룹은 결과에만 기록되고 화면 표시는 없습니다(사각형은 배너에서 분할).
|
|
1362
1629
|
- 배너 커서: selectable이면 배너 본문·그룹행은 데이터 셀과 같은 커서이고 ▾ 버튼만 pointer입니다. 옵션(`selectable`/`defaultCollapsed`/`defaultExpandLevel`)은 그룹 컬럼을 모두 뺐다가(`removeColumn`/`groupBy([])`/`setConfig({ columns: [] })`, `getConfig()`는 `null`) 다시 넣어도 유지되며, 생략 시 이어받고 명시값(`false` 포함)이 이깁니다. `clear()`/`setConfig(null)`은 옵션까지 지웁니다.
|
|
@@ -1416,7 +1683,7 @@ interface AggregateContext {
|
|
|
1416
1683
|
|
|
1417
1684
|
- `ctx.sum('x')` 등은 엔진이 계산한 값을 읽으므로 **그 조합이 선언돼 있어야** 합니다 — 컬럼 정의 `aggregate: 'sum'`(숨김 컬럼 가능) 또는 `rowGroupConfig.aggregates`의 `{ column: 'x', func: 'sum' }`. 선언되지 않은 조합을 읽으면 `PS312` 오류입니다(그룹행: `refresh()`가 거절, 고정 합계 행: `console.error` 후 빈 값). 고정 합계 행은 컬럼 정의 `aggregate`만 선언으로 봅니다.
|
|
1418
1685
|
- 다단 그룹은 단계마다 그 단계 집계로 다시 계산하고, 필터·편집·행 추가 후에도 다시 계산합니다.
|
|
1419
|
-
- 결과가 `null`/`undefined`/`NaN`이면 그룹행 칸은 빈
|
|
1686
|
+
- 결과가 `null`/`undefined`/`NaN`이면 그룹행 칸은 빈 칸, 고정 합계 행은 `-`입니다. 0 나누기는 함수에서 처리합니다. (내장 함수의 빈 데이터 결과는 아래 `AggregateFunc` 규칙 — `sum`·개수류 `0`, 그 외 `null`.)
|
|
1420
1687
|
- 일반 Main 모드는 기존 inline 함수를 그대로 사용합니다. Worker 모드는 `{ workerFunction: 'app:aggregate', params: ... }` 참조를 사용하며, 앱 Worker entry에 정적으로 등록된 함수가 실제 멤버 `values`와 `ctx`를 Worker 안에서 받습니다. 메인 계산 왕복·fallback은 없고 inline 함수·미등록 ID는 오류입니다. 함수의 마지막 인수는 descriptor의 `params`입니다.
|
|
1421
1688
|
- 함수형 컬럼 aggregate가 하나라도 있으면 선언된 문자열 컬럼 aggregate를 모두(숨김·배너 아래 컬럼 포함) 계산만 합니다. 이 값들은 배너 글자에 나오지 않습니다.
|
|
1422
1689
|
- 함수형 aggregate는 프리셋(JSON)에 저장되지 않고 코드가 소유합니다 — `applyConfig`가 지우거나 문자열로 바꾸지 않으며, wrapper 집계 탭은 "사용자 함수"로 표시하고 변경을 막습니다.
|
|
@@ -1447,14 +1714,15 @@ summaryRow: 'pinnedBottom', // 고정 합계 행도 전체 Σ/Σ
|
|
|
1447
1714
|
같은 ID를 사용할 수 있지만, 모든 레벨 선언과 특정 레벨 선언의 scope가 겹치면 충돌합니다.
|
|
1448
1715
|
- `setConfig` 및 그룹 컬럼 변경 시 충돌을 검사합니다. 충돌하면 예외가 발생하며 기존 그룹
|
|
1449
1716
|
설정은 변경되지 않습니다. 그룹 해제 중 보관한 선언은 그룹 복원 시 다시 검사합니다.
|
|
1450
|
-
- 공개 ID는 내부 계산 alias와 별개입니다.
|
|
1717
|
+
- 공개 ID는 내부 계산 alias와 별개입니다. 비충돌 항목의 기존 alias는 유지됩니다. 같은 컬럼의 서로 다른 custom 집계는 내부 계산 슬롯을 분리하며, 그룹 정렬·필터는 공개 ID에 해당하는 계산값을 사용합니다.
|
|
1718
|
+
- 서로 다른 ID의 항목은 같은 컬럼·같은 계산이라도 배너에 각각 표시됩니다. 내보내기도 동일하며 컬럼 정의 `aggregate`의 자기 칸 배치 원칙은 변하지 않습니다.
|
|
1451
1719
|
- 콜백 기본값은 함수의 영속 식별자가 아닙니다. 같은 컬럼에 여러 콜백을 쓰면 ID를 명시해야 하며,
|
|
1452
1720
|
콜백 자체의 JSON 직렬화를 보장하지 않습니다. 수식에서 참조 가능한 문법 역시 별도 계약입니다.
|
|
1453
1721
|
|
|
1454
1722
|
#### 그룹행 정렬 기준
|
|
1455
1723
|
|
|
1456
1724
|
core의 `RowGroupConfig.sorts`는 같은 부모 아래 그룹행을 그룹 컬럼별로 정렬합니다.
|
|
1457
|
-
각 규칙은 `{
|
|
1725
|
+
각 `RowGroupSort` 규칙은 `{ id, direction, basis?, by? }`이며 `id`는 해당 그룹 레벨의 컬럼 ID,
|
|
1458
1726
|
`direction`은 `'asc' | 'desc'`입니다.
|
|
1459
1727
|
|
|
1460
1728
|
- `id`: 정렬할 그룹 레벨의 컬럼 ID (집계 데이터 컬럼이 아님).
|
|
@@ -1509,6 +1777,8 @@ wrapper 정렬 패널의 **그룹 정렬 → 정렬 기준**에서도
|
|
|
1509
1777
|
| `expandAtLevel(level)` | `Promise<void>` | 정확히 그 레벨의 노드만 펼침 — 다른 레벨 보존 (Ctrl/Cmd+클릭 제스처와 동일) |
|
|
1510
1778
|
| `collapseAtLevel(level)` | `Promise<void>` | 정확히 그 레벨의 노드만 접힘 — 다른 레벨 보존 |
|
|
1511
1779
|
| `isCollapsed(nodeId)` | `boolean` | 접힘 상태 조회 |
|
|
1780
|
+
| `getTreeLevel(nodeId: string \| number)` | `number` | 마지막 트리 조회 결과 기준 깊이. root=0, 미존재/조회 전=-1 |
|
|
1781
|
+
| `getNodeMeta(nodeId: string \| number)` | `{ level: number; hasChildren: boolean; descendantCount: number; aggregates?: Record<string, CellValue> } \| null` | 마지막 트리 조회 결과의 노드 메타데이터. 없으면 `null` |
|
|
1512
1782
|
| `expandPath(nodeId)` | `Promise<void>` | 조상 전체 펼치기 (검색 결과 표시용) |
|
|
1513
1783
|
| `moveNode(nodeId, newParentId, newIndex?)` | `Promise<void>` | **프로그램 방식 트리 노드 이동** (드래그 없이 코드로). flat 그리드 `changes.reorderRow`의 tree 대칭 API. 아래 참조 |
|
|
1514
1784
|
| `getChildren(nodeId)` | `RowId[]` | 직계 자식 rowId 목록 (소스 순서). 리프/미존재 시 `[]` |
|
|
@@ -1516,6 +1786,8 @@ wrapper 정렬 패널의 **그룹 정렬 → 정렬 기준**에서도
|
|
|
1516
1786
|
| `getParent(nodeId)` | `RowId \| null` | 부모 rowId. root/미존재 시 `null` |
|
|
1517
1787
|
| `getAncestors(nodeId)` | `RowId[]` | 조상 rowId 목록 (가까운 부모부터 root 방향). root/미존재 시 `[]` |
|
|
1518
1788
|
|
|
1789
|
+
`getNodeMeta().aggregates`는 `treeConfig.aggregates` 설정 시 **자기+자손 서브트리**의 집계이며 키는 `${func}_${columnId}`(예: `sum_amount`)입니다. 노드 셀 값을 자동 대체하지 않으므로 표시는 소비자 렌더러가 결정합니다.
|
|
1790
|
+
|
|
1519
1791
|
> **트래버설 (`getChildren` / `getDescendants` / `getParent` / `getAncestors`)** — `treeConfig.idField`/`parentField` 값 기준으로 **소스 전체를 스캔**하므로 접힘·필터·정렬 상태와 무관하게 논리적 트리 구조를 반환합니다. 반환값은 rowId 배열(원형 유지)이며 `grid.rows.getByIds(...)`와 조합해 일괄 수정에 사용합니다 (부모↔자손 체크박스 cascade 등). `treeConfig` 미설정 시 모두 빈 결과. 조회당 O(N) — 프레임 내 반복 호출은 피하세요.
|
|
1520
1792
|
|
|
1521
1793
|
> **`moveNode(nodeId, newParentId, newIndex?)`** — `nodeId`를 `newParentId`의 `newIndex`번째 자식으로 이동합니다.
|
|
@@ -1613,6 +1885,7 @@ wrapper 정렬 패널의 **그룹 정렬 → 정렬 기준**에서도
|
|
|
1613
1885
|
| `getSourceRowsForSelection()` | `Promise<number[]>` | 선택 영역의 소스 행 (`trackSourceRows: true` 필요) |
|
|
1614
1886
|
| `getGrandTotals()` | `Promise<PivotGrandTotal[]>` | 값 필드별 전체 총합계(코너). 현재 필터 반영, 표시 on/off 무관 상시 계산. 비피벗이면 `[]` |
|
|
1615
1887
|
| `getColumnGrandTotals(columnIds?)` | `Promise<PivotColumnGrandTotal[]>` | 컬럼별 행 총합계. `columnIds` 생략 시 전체 데이터 컬럼. 표시 on/off 무관. 비피벗이면 `[]` |
|
|
1888
|
+
| `getSourceRowCount()` | `{ total: number; filtered: number } \| null` | 피벗 **원본 행 수**(소계·총합계·접기 무관). `total` = 필터 전, `filtered` = 차원·값 필터 반영(값 필터는 통과한 피벗 행에 속한 원본 행 수). `filtered` 가 0 이면 총합계 행도 없는 빈 표. 화면 피벗 행 수는 `getTotalRowCount()`/`getVisibleRowCount()`. 예) 원본 5행·소계+총합계 → 피벗 7행, `{ total: 5, filtered: 5 }`. 비피벗이면 `null` |
|
|
1616
1889
|
| `getFieldValues(fieldId, options?)` | `Promise<CellValue[]>` | 필드의 값 목록 (행/열 자동 판별). `options.unique` 기본 `true` |
|
|
1617
1890
|
| `getAllRows()` | `Promise<PivotRow[]>` | lazy 모드에서도 전체 피벗 행을 가져옴 |
|
|
1618
1891
|
| `getDataMatrix(options?)` | `Promise<DataMatrix \| null>` | 피벗 결과를 2D 표로 변환 |
|
|
@@ -1811,7 +2084,10 @@ interface InvalidCell {
|
|
|
1811
2084
|
|--------|------|------|
|
|
1812
2085
|
| `copy()` | `Promise<string \| null>` | 선택 셀 복사 |
|
|
1813
2086
|
| `copy({ includeHeaders: true })` | `Promise<string \| null>` | 헤더 포함 복사 |
|
|
1814
|
-
| `paste()` | `Promise<number>` |
|
|
2087
|
+
| `paste(options?: PasteOptions)` | `Promise<number>` | 값이 쓰인 셀 수 반환. `options.mode`는 `'overwrite'`(기본) 또는 `'insert'` |
|
|
2088
|
+
| `pasteInsert()` | `Promise<number>` | `paste({ mode: 'insert' })` 편의 함수 |
|
|
2089
|
+
|
|
2090
|
+
`overwrite`는 앵커 셀부터 기존 값을 덮어쓰며 행 수를 늘리지 않습니다. `insert`는 앵커 행 위치에 새 행을 삽입하고 기존 행을 아래로 밉니다. 정렬·필터·행 그룹·트리·피벗이 적용된 view에서는 삽입 위치를 확정할 수 없어 수행하지 않고 `clipboard:paste`에 `rejected: true`를 전달합니다. 클립보드 접근에는 브라우저 권한/보안 컨텍스트 제약이 적용됩니다.
|
|
1815
2091
|
|
|
1816
2092
|
#### 키보드 단축키
|
|
1817
2093
|
|
|
@@ -1863,6 +2139,52 @@ interface InvalidCell {
|
|
|
1863
2139
|
| `getSheetInfo(file)` | `Promise<SheetInfo[]>` | 시트 목록과 예상 행 수 조회 (import 전 미리보기용) |
|
|
1864
2140
|
| `previewExcel(file, options?)` | `Promise<PreviewResult>` | 헤더 + 첫 N행 반환 (그리드에 로드하지 않음) |
|
|
1865
2141
|
|
|
2142
|
+
`grid.io`는 그리드에 로드된 데이터를 내보냅니다. 아직 로드하지 않은 서버 조회 전체를 내보내려면 아래 `XlsxExporter`에 호출자가 전체 페이지를 공급해야 합니다.
|
|
2143
|
+
|
|
2144
|
+
#### 전체 조회 스트리밍 — XlsxExporter
|
|
2145
|
+
|
|
2146
|
+
`XlsxExporter`, `MemoryWriteSink`, `FileWriteSink`, `canUseFileSystemAccess` 및 타입 `XlsxWriteSink`, `ExportColumnMeta`는 core 루트에서 import할 수 있습니다. 이 진입점은 그리드 인스턴스 없이 외부 조회 페이지를 XLSX로 작성합니다. 서버 조회·페이지 순서·컬럼 메타데이터·총 행 수는 소비처 책임이며 그리드의 필터나 표시 서식이 자동 적용되지는 않습니다.
|
|
2147
|
+
|
|
2148
|
+
| 진입점 | 반환 | 계약 |
|
|
2149
|
+
|---|---|---|
|
|
2150
|
+
| `new XlsxExporter(columns: ExportColumnMeta[], options?)` | `XlsxExporter` | 옵션 타입은 `ConstructorParameters<typeof XlsxExporter>[1]` 참조. `totalRows`, `rowsPerSheet`, `includeHeaders`, `sheetName`, `compression` 등을 받으며 `ExcelExportOptions`와 다름 |
|
|
2151
|
+
| `beginStream(sink: XlsxWriteSink)` | `Promise<void>` | 작성 시작. 시트 수는 `totalRows`로 사전 결정 |
|
|
2152
|
+
| `writeChunkStream(rows)` | `Promise<void>` | 데이터 청크 순차 작성. 일반 `Record<string, unknown>[]` 페이지를 전달 가능 |
|
|
2153
|
+
| `finalizeStream()` | `Promise<void>` | 마지막 시트·ZIP 마무리와 sink.close까지 대기 |
|
|
2154
|
+
| `abortStream(reason?: unknown)` | `Promise<void>` | sink가 abort를 제공하면 호출. 이미 작성한 모든 외부 효과의 롤백을 보장하지 않음 |
|
|
2155
|
+
| `getProgress()` | `ExportProgress` | 현재 행 작성 진행률 조회 ([진행률 타입](#진행률--importprogress--exportprogress)). 100%만으로 finalize/저장 완료를 판정하지 않음 |
|
|
2156
|
+
| `exportAllToBlob(rows: Record<string, unknown>[])` | `Promise<Blob>` | 이미 메모리에 있는 전체 배열을 한 번에 변환하는 편의 API |
|
|
2157
|
+
|
|
2158
|
+
`XlsxWriteSink`는 `readonly position: number`, `write(data: Uint8Array): Promise<void>`, `close(): Promise<void>` 계약입니다. `MemoryWriteSink`는 바이트를 메모리에 누적하며 `toBlob(type?: string): Blob`, `dispose(): void`를 제공합니다. 메모리 사용량이 파일 크기에 비례하므로 대용량에서는 파일 sink를 고려하세요. `FileWriteSink`는 호출자가 연 writable file stream을 생성자로 받아 직접 기록하며 `abort(reason?)`도 제공합니다.
|
|
2159
|
+
|
|
2160
|
+
`canUseFileSystemAccess(): boolean`은 `showSaveFilePicker` 존재 여부만 확인합니다. 저장 권한이나 성공을 보장하지 않으며 file picker 호출·사용자 제스처·취소 처리는 호스트 책임입니다. 직접 `XlsxExporter`를 사용하면 `grid.io.saveExcelToFile`의 다운로드 폴백/Worker 실행/AbortSignal 처리가 자동으로 붙지 않습니다.
|
|
2161
|
+
|
|
2162
|
+
스트림마다 새 exporter/sink를 만들고 `beginStream → writeChunkStream(각 페이지를 await) → finalizeStream` 순서를 지키세요. 시트 분할을 쓸 때는 정확한 `totalRows`를 제공하고 `rowsPerSheet`에 헤더 행도 포함하세요. 실패 시 `abortStream(error)`로 정리하며 메모리 sink는 별도로 `dispose()`합니다.
|
|
2163
|
+
|
|
2164
|
+
```typescript
|
|
2165
|
+
import { XlsxExporter, MemoryWriteSink, type ExportColumnMeta } from '@vmscloud/moz-ui-grid-core';
|
|
2166
|
+
|
|
2167
|
+
async function exportPages(
|
|
2168
|
+
columns: ExportColumnMeta[],
|
|
2169
|
+
pages: AsyncIterable<Record<string, unknown>[]>,
|
|
2170
|
+
totalRows: number,
|
|
2171
|
+
): Promise<Blob> {
|
|
2172
|
+
const sink = new MemoryWriteSink();
|
|
2173
|
+
const exporter = new XlsxExporter(columns, { totalRows, rowsPerSheet: 1_048_576 });
|
|
2174
|
+
try {
|
|
2175
|
+
await exporter.beginStream(sink);
|
|
2176
|
+
for await (const rows of pages) await exporter.writeChunkStream(rows);
|
|
2177
|
+
await exporter.finalizeStream();
|
|
2178
|
+
return sink.toBlob(); // 다운로드/파일 저장은 호출자가 처리
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
await exporter.abortStream(error);
|
|
2181
|
+
throw error;
|
|
2182
|
+
} finally {
|
|
2183
|
+
sink.dispose();
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
```
|
|
2187
|
+
|
|
1866
2188
|
#### Export 옵션 — `ExcelExportOptions`
|
|
1867
2189
|
|
|
1868
2190
|
`timeZone?: string`은 해당 작업만 코어 시간대보다 우선합니다. `saveExcelToFile`과 Worker 경로에도 적용됩니다.
|
|
@@ -1874,7 +2196,7 @@ interface InvalidCell {
|
|
|
1874
2196
|
| `includeHeaders` | `boolean` | `true` | 헤더 행 포함 |
|
|
1875
2197
|
| `sheetName` | `string` | `'Sheet1'` | 시트 이름 |
|
|
1876
2198
|
| `preserveFormatting` | `boolean` | `false` | 숫자 decimalFormat 및 구조화된 날짜 mask → 컬럼별 Excel numFmt 매핑 |
|
|
1877
|
-
| `
|
|
2199
|
+
| `includePinnedRows` | `boolean` | `false` | 일반 flat XLSX·CSV의 화면에 보이는 구조/데이터 고정 행 포함 여부. 범위/제외 규칙은 [아래 설명](#고정-행-xlsxcsv-export-includepinnedrows) 참조 |
|
|
1878
2200
|
| `chunkSize` | `number` | `10000` | 청크당 행 수 |
|
|
1879
2201
|
| `compression` | `'deflate' \| 'store'` | `'deflate'` | 압축 방식 |
|
|
1880
2202
|
| `freezeHeader` | `boolean` | `false` | 헤더 행 고정 (freeze pane) |
|
|
@@ -1888,8 +2210,14 @@ interface InvalidCell {
|
|
|
1888
2210
|
|
|
1889
2211
|
##### XLSX Worker 기본 경로와 실패 처리
|
|
1890
2212
|
|
|
1891
|
-
- `toExcel()`과 `saveExcelToFile()`은 한 행짜리 export도 기본 공통 XLSX Worker 경로를 사용합니다.
|
|
2213
|
+
- `toExcel()`과 `saveExcelToFile()`은 한 행짜리 export도 기본 공통 XLSX Worker 경로를 사용합니다. 그리드의 데이터 처리 Worker(`loader.worker`)와 이 export 옵션은 별개입니다.
|
|
2214
|
+
- 내보낼 행이 Proxy(예: Vue `reactive` 데이터)라 Worker로 그대로 못 보내면 평범한 값으로 깊은 복사해 보내고 `[PureSheet]` 경고를 냅니다(한 내보내기 안에서는 1회, export 결과는 같음). 함수·클래스 인스턴스 값은 여전히 보낼 수 없습니다.
|
|
1892
2215
|
- export 옵션에 `useWorker: false`를 명시한 경우만 메인 스레드 생성 경로를 선택합니다. Worker 실행 실패 시 메인 스레드에서 자동 재시도하지 않고 export Promise가 reject됩니다.
|
|
2216
|
+
- **함수 서식 열**(`mask: { type: 'function', formatter: v => … }` inline 함수, 피벗 값 필드의 사용자 `mask` 포함)은 `preserveFormatting: true`(래퍼 `ExcelController` 기본)일 때 표시 문자열 글자 셀로 나가며, 기본 Worker 경로로도 내보낼 수 있습니다. 화면 표시와 같은 규칙으로, 메인이 행 청크를 Worker로 보내기 직전에 그 청크의 표시 문자열을 계산해 함께 보내고 Worker는 그 글자를 글자 셀로 씁니다(ZIP/XML 생성은 Worker). 결과는 `useWorker: false`와 셀 단위로 같습니다. 예: `formatter: v => '₩' + v.toLocaleString()` 열의 값 `1000` → 글자 셀 `₩1,000`.
|
|
2217
|
+
- formatter 인자는 화면과 같은 `(value, rowData, columnKey, timeZone)`입니다(`useWorker: false`도 같음). 값이 `null`/`undefined`인 셀은 formatter를 부르지 않고 빈 셀로 둡니다. 빈 문자열 반환도 빈 셀입니다.
|
|
2218
|
+
- formatter가 예외를 던지면 export Promise가 그 오류로 reject됩니다(`useWorker: false`와 같음). 메인 재생성으로 숨기지 않습니다.
|
|
2219
|
+
- 등록 참조(`formatter: { workerFunction: 'app:money', params }`)는 그대로 XLSX Worker 안에서 실행합니다. Worker가 메인에 함수 실행을 요청하는 왕복 콜백은 없습니다.
|
|
2220
|
+
- 메인 추가 비용은 함수 서식 셀마다 formatter 1회(메인 생성과 같은 호출 수)이며, 청크(`chunkSize`, 기본 10,000행) 단위로 나뉘어 실행됩니다.
|
|
1893
2221
|
- `saveExcelToFile()`의 FSA 미지원 다운로드 폴백은 저장 방식의 전환입니다. 내부 `toExcel(options)`에도 같은 export 옵션을 전달하므로 Worker 실패를 메인 생성으로 우회하는 폴백이 아닙니다. `fallbackToDownload: false`이면 FSA 미지원 시 오류를 반환합니다.
|
|
1894
2222
|
- 이 실패 정책은 **XLSX export**에 해당합니다. `fromExcel()`의 import는 별도 크기 기반 Worker 선택을 사용하며, Worker 오류(취소 `AbortError` 제외) 시 메인 스레드 파싱으로 폴백합니다.
|
|
1895
2223
|
|
|
@@ -1918,6 +2246,7 @@ XLSX·CSV의 행 순서는 **화면에 보이는 순서**입니다(원본 Wijmo
|
|
|
1918
2246
|
고정 행 데이터의 `__merge` 같은 사용자 필드는 `includePinned` 여부와 관계없이 병합으로 해석하지 않습니다(고정 행 병합은 위 설정에서만 나옴).
|
|
1919
2247
|
|
|
1920
2248
|
- `exportSource: true`는 항상 원본 데이터만 내보냅니다. 피벗 결과 export와 CSV는 이 옵션을 무시합니다.
|
|
2249
|
+
- `exportSource: true`의 원본은 `getAllDataSync()` 값입니다. `keyFields`가 있는 그리드는 **저장(commit)된 원본**이라 저장 전 편집·추가 행이 빠집니다(예: qty를 4→7로 고치고 저장 전 내보내면 파일은 4). `keyFields`가 없는 메인 스레드 그리드는 저장 전 편집·추가가 반영된 값입니다. 편집 내용을 포함하려면 먼저 `changes.commit()` 하세요(ITSM-2026-004445).
|
|
1921
2250
|
- 구조 고정 행의 집계는 현재 supplier 집계 요청 완료를 기다린 뒤 **숫자 등 원래 타입**으로 내보냅니다. 같은 `field`를 쓰는 서로 다른 컬럼의 sum/avg도 컬럼 ID별로 분리됩니다. 원본 row 데이터와 집계값을 단순 spread로 합치지 않습니다.
|
|
1922
2251
|
- 구조 고정 행의 라벨은 pinned 행의 `data`에 **데이터 키(field)** 로 선언합니다. 예: `{ data: { name: '합계' }, aggregates: [{ columnId: 'total', func: 'sum' }] }`. 집계가 선언된 컬럼은 그 집계값이 우선합니다.
|
|
1923
2252
|
- 데이터 고정 행은 본문 행과 같은 셀 값 변환(날짜·숫자, `preserveFormatting` 서식)으로 현재 데이터(편집 반영)를 씁니다.
|
|
@@ -2330,7 +2659,7 @@ const b = store.select(s => s.canRedo);
|
|
|
2330
2659
|
| `pivotMode` | `'flat' \| 'pivot'` | 현재 피벗 모드 (`setMode()`의 권위 있는 신호) |
|
|
2331
2660
|
| `treeConfig` | `Readonly<TreeConfig> \| null` | 현재 트리 설정 |
|
|
2332
2661
|
| `masterDetailConfig` | `Readonly<MasterDetailConfig> \| null` | 현재 마스터-디테일 설정 |
|
|
2333
|
-
| `
|
|
2662
|
+
| `rowGroupConfig` | `Readonly<RowGroupConfig> \| null` | 현재 그룹화 설정 (`viewState.groups`는 이로부터 합성됨) |
|
|
2334
2663
|
| `dragDropConfig` | `Readonly<DragDropConfig> \| null` | 현재 드래그-드롭 설정 |
|
|
2335
2664
|
| `mergeConfig` | `Readonly<MergeConfig> \| null` | 현재 셀 병합 설정 |
|
|
2336
2665
|
|
|
@@ -2503,7 +2832,7 @@ grid.off('cell:click', handler);
|
|
|
2503
2832
|
|
|
2504
2833
|
| 이벤트 | 페이로드 | 설명 |
|
|
2505
2834
|
|--------|---------|------|
|
|
2506
|
-
| `data:loaded` | `{ rowCount, columnCount }` | 데이터 로드
|
|
2835
|
+
| `data:loaded` | `{ rowCount, columnCount }` | 데이터 로드 완료. `rowCount` 는 불러온 원본 행 수(`loadData`·`loader` 공통, 피벗이어도 총합계·소계를 포함한 결과 행 수가 아님 — 예: 피벗+행 총합계+0행 → `0`). `loader` 는 첫 로드와 `refreshLoaderData()` 재조회가 **성공할 때마다 한 번씩** 발행(필터 중이어도 필터 전 원본 수, 실패하면 발행 안 함). `rowModel: 'server'`(SSRM)의 `refreshLoaderData()` 는 발행하지 않음 |
|
|
2507
2836
|
|
|
2508
2837
|
#### 행
|
|
2509
2838
|
|
|
@@ -2805,7 +3134,7 @@ grid.on('changes:redone', (e) => syncChange(e.change, 'redo'));
|
|
|
2805
3134
|
|
|
2806
3135
|
| 이벤트 | 페이로드 | 설명 |
|
|
2807
3136
|
|--------|---------|------|
|
|
2808
|
-
| `scroll` | `
|
|
3137
|
+
| `scroll` | `ScrollPayload` | 세로 range 변경 시 발행. [스크롤 이벤트 상세](#스크롤-1)의 payload 참조. 수평 이동만으로 발행된다는 보장은 없음 |
|
|
2809
3138
|
|
|
2810
3139
|
#### 검색 / 구조 변경
|
|
2811
3140
|
|
|
@@ -2854,11 +3183,14 @@ interface TreeConfig {
|
|
|
2854
3183
|
defaultCollapsed?: boolean; // 기본 접힘 상태
|
|
2855
3184
|
defaultExpandLevel?: number; // 기본 확장 레벨 (0=루트만, Infinity=전체)
|
|
2856
3185
|
aggregates?: Record<string, AggregateFn>; // 집계 설정
|
|
2857
|
-
|
|
2858
|
-
fetchChildren?: (nodeId: string | number) => Promise<Row[]>; // 자식 fetch 함수
|
|
3186
|
+
childCountField?: string; // rowModel: 'server' 트리 — 자식 여부 필드 (기본: 'childCount'). 숫자=자식 수, true=있음(수 모름)
|
|
2859
3187
|
}
|
|
2860
3188
|
```
|
|
2861
3189
|
|
|
3190
|
+
재로드(`loadData`) 때 `defaultCollapsed`/`defaultExpandLevel`은 행 그룹과 같이 **처음 보는 부모 노드**(자식이 생긴 노드 포함)에 적용하고, 이미 본 노드는 지금 상태(사용자가 펴거나 접은 것)를 유지합니다. 예: `defaultCollapsed: true`로 빈 데이터에서 시작해 조회하면 부모 노드가 모두 접힌 채 나오고, `A`를 편 뒤 재조회로 `C`가 새로 생기면 `A`는 펼친 채, `C`는 접힌 채입니다. `grid.tree.setConfig`로 설정을 바꾸면 모든 노드에 다시 적용합니다.
|
|
3191
|
+
|
|
3192
|
+
필드 옵션 `checkboxIncludesDescendants: true`(boolean 열) — 트리에서 **부모가 자손을 포함**하는 체크박스. 부모 칸은 자손 상태(전부 checked / 일부 indeterminate / 없음 unchecked)로 보이고, 부모를 누르면 부모와 자손 전체(필터로 숨은 자손 포함)가 같은 값이 됩니다(실행 취소 한 번). 잎은 평소처럼 자기 값만 바뀝니다. 예전 이름 `checkboxTriState`(표시만 하고 퍼뜨리기는 소비처 몫이던 옵션)를 대신합니다. 클라이언트 트리에서 자손 값을 읽으려면 `enableReferCache: true` 가 필요합니다. `rowModel: 'server'` 트리는 [서버 행 모델 — 트리](#서버-행-모델-ssrm--rowmodel-server--loader) 참조.
|
|
3193
|
+
|
|
2862
3194
|
### MasterDetailConfig
|
|
2863
3195
|
|
|
2864
3196
|
```typescript
|
|
@@ -2893,19 +3225,37 @@ interface MasterDetailConfig {
|
|
|
2893
3225
|
- `visible:false`·Ctrl+H·데이터 행 고정·그룹/트리 접힘으로 안 보이는 동안 디테일은 표시하지 않습니다. 기본(`true`)은 행 키의 펼침 상태를 기억하여 다시 보일 때 복원하고, `false`는 기존처럼 접습니다.
|
|
2894
3226
|
- `singleExpand: true`와 함께 쓰면 마지막으로 펼친 한 행만 유지됩니다.
|
|
2895
3227
|
|
|
2896
|
-
###
|
|
3228
|
+
### RowGroupConfig
|
|
2897
3229
|
|
|
2898
|
-
|
|
3230
|
+
생성자의 `rowGroupConfig`와 런타임 `grid.rowGroup.setConfig(config)`는 같은 공개 타입을 사용합니다. 구 `rowGroup`/`group` 키는 옛 저장본 마이그레이션 대상이지 현행 생성 옵션 별칭이 아닙니다.
|
|
2899
3231
|
|
|
2900
3232
|
```typescript
|
|
2901
|
-
interface
|
|
2902
|
-
/** 그룹핑 기준 컬럼 (계층 순서) */
|
|
3233
|
+
interface RowGroupConfig {
|
|
2903
3234
|
columns: string[];
|
|
2904
|
-
|
|
2905
|
-
|
|
3235
|
+
aggregates?: RowGroupAggregate[];
|
|
3236
|
+
sorts?: RowGroupSort[];
|
|
3237
|
+
defaultCollapsed?: boolean;
|
|
3238
|
+
defaultExpandLevel?: number;
|
|
3239
|
+
selectable?: boolean;
|
|
2906
3240
|
}
|
|
2907
3241
|
```
|
|
2908
3242
|
|
|
3243
|
+
`defaultCollapsed: true`는 `defaultExpandLevel`보다 우선합니다. `defaultExpandLevel`은 0이면 최상위 그룹행만, 1이면 그 아래 한 단계까지 펼치며 생략하면 모두 펼칩니다. `selectable`은 기본 `false`입니다.
|
|
3244
|
+
|
|
3245
|
+
재로드(`loadData`) 때 기본 접힘 규칙은 **처음 보는 그룹**에 적용하고, 이미 본 그룹은 지금 상태(사용자가 펴거나 접은 것)를 유지합니다(서버 그룹과 같은 규칙). 예: `defaultCollapsed: true`로 빈 데이터에서 시작해 조회하면 도착한 그룹이 모두 접힌 채 나오고, `HR`을 편 뒤 재조회로 `Ops`가 새로 생기면 `HR`은 펼친 채, `Ops`는 접힌 채입니다. 그룹 열이 바뀌거나 `defaultCollapsed`/`defaultExpandLevel`을 바꾸면 모든 그룹에 다시 적용합니다. Vue/React 래퍼의 `data`는 참조로 비교하므로 같은 배열을 다시 넣으면 재로드(`data:loaded`)가 일어나지 않습니다 — 조회마다 새 배열을 넘기세요.
|
|
3246
|
+
|
|
3247
|
+
```typescript
|
|
3248
|
+
const rowGroupConfig: RowGroupConfig = {
|
|
3249
|
+
columns: ['region', 'product'],
|
|
3250
|
+
aggregates: [
|
|
3251
|
+
{ column: 'amount', func: 'sum', scope: ['region'] },
|
|
3252
|
+
{ column: 'amount', func: 'avg', scope: ['region', 'product'] },
|
|
3253
|
+
],
|
|
3254
|
+
};
|
|
3255
|
+
// 생성자: new PureSheet(container, { fields, data, keyFields: ['id'], rowGroupConfig })
|
|
3256
|
+
await grid.rowGroup.setConfig(rowGroupConfig);
|
|
3257
|
+
```
|
|
3258
|
+
|
|
2909
3259
|
#### 그룹 행 rowHeader 체크박스
|
|
2910
3260
|
|
|
2911
3261
|
`rowSelection.trigger === 'checkbox'` 와 함께 쓰면 그룹 행 rowHeader 에도 체크박스가 표시됩니다(기본 활성).
|
|
@@ -2935,25 +3285,29 @@ rowSelection: { mode: 'multi', trigger: 'checkbox', groupRowCheckbox: false } /
|
|
|
2935
3285
|
⚠️ `rowHeader.cellRenderer` 로 커스텀 렌더러를 지정하면 이 기능은 비활성입니다 — 그 칸의 내용은
|
|
2936
3286
|
소비처 소유이므로 그리드가 덮어쓰지 않습니다.
|
|
2937
3287
|
|
|
2938
|
-
###
|
|
2939
|
-
|
|
2940
|
-
`grid.rowGroup.setConfig(config)`로 런타임에 사용. 집계 함수와 기본 접힘 상태를 직접 지정할 수 있습니다.
|
|
3288
|
+
### RowGroupAggregate
|
|
2941
3289
|
|
|
2942
3290
|
```typescript
|
|
2943
|
-
interface
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
/** 기본 접힘 상태 */
|
|
2949
|
-
defaultCollapsed?: boolean;
|
|
2950
|
-
/** 그룹 배너 선택 허용 (기본 false) — [그룹 배너 선택](#그룹-배너-선택-selectable) */
|
|
2951
|
-
selectable?: boolean;
|
|
3291
|
+
interface RowGroupAggregate {
|
|
3292
|
+
id?: string;
|
|
3293
|
+
column: string;
|
|
3294
|
+
func: AggregateFn;
|
|
3295
|
+
scope?: string[];
|
|
2952
3296
|
}
|
|
2953
3297
|
```
|
|
2954
3298
|
|
|
3299
|
+
`func`는 반드시 명시하며 `FieldDef.aggregate`에서 빌려오지 않습니다. `scope`는 그룹 컬럼 ID 경로이며 생략하면 모든 레벨, 지정하면 그 경로의 레벨별 그룹에 적용합니다. 기본 집계 ID는 내장 함수의 경우 `${func}_${column}`, 콜백은 `column`입니다. 같은 레벨에 적용되는 ID는 유일해야 하며 여러 콜백은 명시 ID로 구분하세요. 집계 설정은 Record 맵이나 `subtotals` 배열이 아닙니다.
|
|
3300
|
+
|
|
2955
3301
|
### EditorConfig
|
|
2956
3302
|
|
|
3303
|
+
가상 스크롤은 편집 확정/취소 요청이 아니다. 편집 중인 행 shell과 입력 DOM은 연결된 상태로 재사용을 유예하며, viewport에 필요한 다른 행은 별도로 확보한다. 화면 밖 입력창을 고정 floating editor로 옮기지 않는다. dropdown/datepicker/multiselect popup도 스크롤만으로 닫지 않으며, 선택·검색·표시 월 상태를 유지한다. 일반 blur/다른 셀 편집으로 종료할 때는 시작 시점의 행 ID/열 ID에 적용하며 기존 입력 제약·확정 거부를 우회하지 않는다.
|
|
3304
|
+
|
|
3305
|
+
**Esc 2단계**: 편집 중 Esc는 편집만 취소한다(선택 유지, `selection:changed` 없음 — text·dropdown·datepicker·multiselect 공통). 취소로 포커스가 body로 떨어지면 그리드로 되돌리므로, 한 번 더 Esc를 누르면 선택이 해제된다. 편집 중이 아닐 때 Esc 1번은 선택 해제다.
|
|
3306
|
+
|
|
3307
|
+
커스텀 에디터의 body portal은 `CustomEditorParams.registerOwnedSurface?.(portalElement)`로 해당 편집의 focus surface임을 등록할 수 있다. 반환 함수는 등록을 해제하며 편집 종료 시 core 등록도 해제된다. core는 portal DOM 자체를 제거하지 않는다. portal 안으로 이동한 focus는 외부 blur로 확정하지 않고, 그 surface 밖으로 이동하면 기존 `getValue`/확정 경로를 사용한다. `destroy`는 정상 blur를 포함한 종료에서 호출되므로 portal DOM/사용자 listener 정리는 그 hook에서 수행한다. 소비자가 독자적으로 등록한 `blur → onCancel`의 의미를 core가 재정의하지 않는다. 등록하지 않은 외부 surface를 전역 CSS class로 추정하지 않는다.
|
|
3308
|
+
|
|
3309
|
+
이 보존은 필터 정책·삭제 정책 변경과 별개다. 데이터 전체 교체는 기존 설치 경계에서 이전 편집을 무효화하며, 이전 dataset의 비동기 확정이 새 dataset에 쓰이지 않도록 검사한다. native IME 후보창/브라우저별 focus 수용 검증과 framework/Worker 통합은 별도 검증 대상이다.
|
|
3310
|
+
|
|
2957
3311
|
```typescript
|
|
2958
3312
|
interface EditorConfig {
|
|
2959
3313
|
type: EditorType;
|
|
@@ -2963,17 +3317,29 @@ interface EditorConfig {
|
|
|
2963
3317
|
max?: number; // number 전용, 포함하는 최댓값
|
|
2964
3318
|
step?: number; // number 전용, 양수 간격 (기준: min 또는 0)
|
|
2965
3319
|
inputMask?: string; // text 전용, 입력 마스크 (예: '99:99') — 표시 mask와 별개
|
|
2966
|
-
options?: EditorOption[];
|
|
3320
|
+
options?: EditorOption[] | CellObject[]; // dropdown/select/multiselect 옵션 (객체 값 열은 객체 배열만 줘도 됨 → FieldDef 의 "객체 값 열" 절)
|
|
2967
3321
|
searchable?: boolean; // 검색 활성화 (dropdown, multiselect)
|
|
2968
3322
|
placeholder?: string; // placeholder 텍스트
|
|
2969
3323
|
dateFormat?: string; // datepicker 포맷 (기본: 'yyyy-MM-dd')
|
|
2970
3324
|
separator?: string; // multiselect 구분자 (기본: ',')
|
|
2971
3325
|
renderer?: (params: CustomEditorParams) => HTMLElement; // 커스텀 에디터 렌더러
|
|
2972
|
-
getValue?: (el: HTMLElement) => CellValue; //
|
|
3326
|
+
getValue?: (el: HTMLElement) => CellValue; // 커스텀: 그리드가 값을 가져가는 함수 (아래)
|
|
2973
3327
|
destroy?: (el: HTMLElement) => void; // 커스텀 정리
|
|
2974
3328
|
}
|
|
2975
3329
|
```
|
|
2976
3330
|
|
|
3331
|
+
**커스텀 에디터의 `getValue`** — 그리드가 값을 **가져가는** 함수입니다. 에디터가 `onCommit` 을 부르지 않은 채 그리드가 스스로 편집을 확정할 때(창 밖 클릭·다른 셀 클릭/편집 시작·Enter/Tab 이 그리드까지 올라옴·`commitEdit()` API·설정 적용 전 확정) renderer 가 돌려준 요소를 받아 **셀에 저장할 데이터 값**을 돌려줍니다.
|
|
3332
|
+
|
|
3333
|
+
| `getValue` | 결과 |
|
|
3334
|
+
|---|---|
|
|
3335
|
+
| 값을 돌려줌(`null` 포함) | 그 값으로 확정(값이 같으면 기록 안 함, `cell:editCommitting` 취소 가능) |
|
|
3336
|
+
| `undefined` 를 돌려줌 — 지금 내용을 값으로 읽을 수 없음(입력 오류 등) | **원래 값 유지**(편집 취소, `editCancel`). 확정 호출은 막히지 않음(다른 셀로 이동됨) |
|
|
3337
|
+
| 예외를 던짐 | 원래 값 유지 + `console.warn` 1회 |
|
|
3338
|
+
| 지정 안 함 + renderer 요소가 `input`/`select`/`textarea` | 그 요소의 값(기존 규칙) |
|
|
3339
|
+
| 지정 안 함 + 그 밖의 요소(`div` 등) | 원래 값 유지 (예전에는 `null` 이 저장됐다) |
|
|
3340
|
+
|
|
3341
|
+
에디터 안에서 Enter/Tab/Esc 를 직접 쓰려면(여러 줄 입력 등) keydown 의 `stopPropagation()` 으로 그리드에 올라가지 않게 하세요 — 올라가면 그리드가 위 규칙으로 확정·취소합니다.
|
|
3342
|
+
|
|
2977
3343
|
#### 숫자 입력 제약
|
|
2978
3344
|
|
|
2979
3345
|
`min`/`max`/`step`은 **`type: 'number'` 전용**입니다. 생략 시 범위 제한이 없고 소수 입력도 허용됩니다(`step` 생략 시 HTML input의 `step='any'`). `step`을 지정하면 `min ?? 0`을 기준으로 그 간격에 맞는 값만 확정할 수 있습니다. 소수 간격도 지원하며 값을 반올림하거나 범위에 맞춰 자동 보정하지 않습니다.
|
|
@@ -3086,7 +3452,7 @@ interface FunctionMaskConfig {
|
|
|
3086
3452
|
type: 'function';
|
|
3087
3453
|
/** 포맷 함수 — 추가 context(rowData, columnKey)는 피벗 등에서 자동 전달 */
|
|
3088
3454
|
formatter: (value: unknown, ...context: any[]) => string;
|
|
3089
|
-
/** 역변환 함수 (스마트 필터링용, 선택) */
|
|
3455
|
+
/** 역변환 함수 (스마트 필터링용, 선택). 객체 값 열(`dataType: 'object'`)에서는 편집 확정·붙여넣기(글자 → 객체)에 쓴다 */
|
|
3090
3456
|
parser?: (displayValue: string) => unknown;
|
|
3091
3457
|
}
|
|
3092
3458
|
```
|
|
@@ -3115,6 +3481,17 @@ interface FunctionMaskConfig {
|
|
|
3115
3481
|
}
|
|
3116
3482
|
```
|
|
3117
3483
|
|
|
3484
|
+
#### Worker 그리드의 inline 함수 mask (검색·필터·값 목록)
|
|
3485
|
+
|
|
3486
|
+
`loader.worker` 그리드에서 열 `mask` 가 inline 함수(`{ type: 'function', formatter: v => … }`, 등록 참조 아님)여도 **검색(`grid.search`/툴바 검색), 표시값 필터(`eq`·`neq`·`contains`·`notContains`·`startsWith`·`endsWith`·`like`·`notLike`), 필터 값 목록(`getFilterValues`)** 이 메인 스레드 그리드와 같은 결과로 동작합니다. 그룹·트리·집계·피벗 쿼리의 필터, 그룹 결과 필터(`basis: 'groupValue'`), 피벗 값 필드의 사용자 `mask`(검색)도 같습니다. 예: `amt` 열 `formatter: v => v.toLocaleString() + '원'`, 데이터 `1000`·`2500` → 검색 `'1,0'` → 1000 행, `'원'` → 두 행, `contains '500원'` → 2500 행, 값 목록 `'1,000원'`·`'2,500원'`.
|
|
3487
|
+
|
|
3488
|
+
- **계약: inline mask 는 값만으로 표시 글자가 정해져야 합니다.** Worker 모드에서는 Worker 가 그 열의 고유값(중복 제거)만 메인으로 보내고, 메인이 `formatter(value)` 로 만든 '값 → 표시 글자 대조표'를 Worker 가 씁니다. 그래서 formatter 가 두 번째 이후 인수(`rowData`, `columnKey`)나 바깥 상태에 따라 다른 글자를 내면 이 경로의 결과가 화면과 어긋날 수 있습니다(화면 셀 표시는 지금처럼 메인에서 행 정보와 함께 실행). 메인 스레드 그리드의 검색·필터·값 목록도 `formatter(value)` 하나만 부르므로 같은 결과입니다. 행에 따라 달라지는 서식이 필요하면 등록 참조(`{ workerFunction }`, [Worker 실행 계약](runtime-contracts.md))를 쓰세요.
|
|
3489
|
+
- 대조표는 **청크(5,000 값) 단위**로 주고받고, 메인의 formatter 실행도 약 8ms 마다 다음 작업으로 양보해 화면이 멈추지 않게 합니다. 표는 (formatter 함수, 열, 데이터 버전) 단위로 기억해 **두 번째 검색·필터부터는 주고받지 않습니다.** 셀 편집·행 추가 등으로 데이터가 바뀌면 다음 요청에서 **새로 생긴 값만** 추가로 받고, 데이터를 통째로 다시 불러오면(`loadData`·loader 새로 고침) 표를 버리고 새로 만듭니다. `mask` 의 formatter 함수가 바뀌면(새 함수 객체) 새 표를 만듭니다 — 렌더마다 새 화살표 함수를 만드는 설정은 매번 다시 만들게 됩니다.
|
|
3490
|
+
- **첫 검색 비용**은 대략 `고유값 수 × formatter 한 번 비용` 이 더해집니다. 측정 예(Edge, 1,000,000행·고유값 100만, `v.toLocaleString()` 약 9µs/값): 첫 검색 약 12~15초(그동안 화면은 멈추지 않으며 메인이 한 번에 붙잡는 시간은 최장 약 15ms), 같은 표를 쓰는 두 번째 검색부터는 표 왕복 없이 약 2초(Worker 검색 시간만). 가벼운 formatter(`` `${v}원` ``)면 표 왕복 추가는 100만 값에 약 0.5초입니다.
|
|
3491
|
+
- 표를 채우는 중 새 검색(또는 검색어 지우기)·새 값 목록 요청이 오면 이전 요청은 빈 결과로 끝나고(기존 검색 중단 규칙과 같음), 같은 표가 필요하면 진행 중인 채우기를 새 요청이 이어받습니다.
|
|
3492
|
+
- formatter 가 어떤 값에서 예외를 던지면, 그 값을 실제로 평가하는 검색·필터 요청이 같은 메시지로 실패합니다(메인 그리드와 같음). 문자열이 아닌 객체·함수를 반환하면 `String()` 한 글자로 둡니다.
|
|
3493
|
+
- 등록 참조 mask, 패턴 mask(`numeric`·`string`·`date`·`secure`), 숫자 열 기본 천단위 mask 는 지금처럼 Worker 안에서 바로 처리합니다(대조표 없음). 정렬은 원본 값 기준이라 해당 없습니다. 피벗 값 필드의 inline `mask` 는 피벗 계산에 쓰이지 않으므로 Worker 로 보내지 않습니다(표시는 메인).
|
|
3494
|
+
|
|
3118
3495
|
### 피벗 필드 타입
|
|
3119
3496
|
|
|
3120
3497
|
#### PivotRowField
|
|
@@ -3131,10 +3508,10 @@ interface PivotRowField {
|
|
|
3131
3508
|
formula?: string | ((row: Record<string, unknown>) => unknown); // 계산된 그룹 축 (집계 전)
|
|
3132
3509
|
headerAttributes?: CellAttributes;
|
|
3133
3510
|
cellAttributes?: CellAttributes | CellAttributesFn;
|
|
3511
|
+
align?: 'left' | 'center' | 'right'; // 행 머리글 값 셀·세로 병합 값·소계/총합계 라벨 정렬. 미지정 = dataType 기본(문자 왼쪽, 숫자 오른쪽). outline 모드에서는 무시 (ITSM-2026-004445 버그 23)
|
|
3134
3512
|
}
|
|
3135
|
-
// 참고: 피벗 정렬은
|
|
3136
|
-
// grid.sort([{ id: 'region', direction: 'asc', sequence: 1 }]).
|
|
3137
|
-
// 같은 필드를 넣어도 silently 무시된다.
|
|
3513
|
+
// 참고: 피벗 행 정렬은 grid.sort([...]) 로 지정한다 (viewState 단일 진입점, SSOT) —
|
|
3514
|
+
// grid.sort([{ id: 'region', direction: 'asc', sequence: 1 }]).
|
|
3138
3515
|
```
|
|
3139
3516
|
|
|
3140
3517
|
#### PivotColumnField
|
|
@@ -3151,8 +3528,7 @@ interface PivotColumnField {
|
|
|
3151
3528
|
formula?: string | ((row: Record<string, unknown>) => unknown); // 계산된 열 축 (집계 전)
|
|
3152
3529
|
headerAttributes?: CellAttributes;
|
|
3153
3530
|
}
|
|
3154
|
-
// 참고: 열 축 정렬도
|
|
3155
|
-
// PivotColumnField 에 sort/sortByValue 를 넣어도 무시된다.
|
|
3531
|
+
// 참고: 열 축 정렬도 grid.sort([...]) 로 지정한다 — grid.sort([{ id, direction, sequence }]).
|
|
3156
3532
|
```
|
|
3157
3533
|
|
|
3158
3534
|
> **`PivotFieldBase.id` — 값 필드 중복 시 필수 (ITSM-2026-002188)**
|
|
@@ -3246,8 +3622,8 @@ interface PivotValueField {
|
|
|
3246
3622
|
|
|
3247
3623
|
| 타입 | 값 |
|
|
3248
3624
|
|------|-----|
|
|
3249
|
-
| `DataType` | `'string' \| 'number' \| 'boolean' \| 'date' \| 'array'` |
|
|
3250
|
-
| `AggregateFunc` | `'sum' \| 'avg' \| 'min' \| 'max' \| 'count' \| 'countAll' \| 'first' \| 'last' \| 'range' \| 'stdev' \| 'stdevPop' \| 'variance' \| 'variancePop' \| 'countNumbers'` (FieldDef.aggregate / 피벗 valueField에서 사용). `count`=컬럼 non-null 값 수, `countAll`=그룹 전체 행 수(null 포함), `countNumbers`=숫자 타입 값
|
|
3625
|
+
| `DataType` | `'string' \| 'number' \| 'boolean' \| 'date' \| 'array' \| 'object'` (`'object'` → [객체 값 열](#객체-값-열-datatype-object)) |
|
|
3626
|
+
| `AggregateFunc` | `'sum' \| 'avg' \| 'min' \| 'max' \| 'count' \| 'countAll' \| 'first' \| 'last' \| 'range' \| 'stdev' \| 'stdevPop' \| 'variance' \| 'variancePop' \| 'countNumbers'` (FieldDef.aggregate / 피벗 valueField에서 사용). `count`=컬럼 non-null 값 수, `countAll`=그룹 전체 행 수(null 포함), `countNumbers`=숫자 타입 값 수. **문자열 `min`/`max`**: 정렬과 같은 문자열 순서 규칙(SortState 아래)으로 최소 = 오름차순 맨 앞, 최대 = 맨 뒤 — 예: `[cherry, Zeta, apple, Banana]` → 최소 `apple`, 최대 `Zeta`. 숫자·날짜 값과 숫자↔문자열이 섞인 비교는 원래 `<`/`>` 그대로. **빈 데이터**(대상 행 0개 또는 숫자 값 없음 — null만 있는 그룹 포함): `sum`·`count`·`countAll`·`countNumbers`는 `0`, 나머지(`avg`·`min`·`max`·`first`·`last`·`range`·`stdev` 계열·`variance` 계열)는 `null`(고정 합계 행 `-`, 그룹행 빈 칸). 예: 행 0개 → 합계 `0`, 평균 `-`. 고정 합계 행·그룹행·트리·피벗·선택 집계·Worker·내보내기 모두 같습니다 |
|
|
3251
3627
|
| `AggregateType` | `'sum' \| 'avg' \| 'count' \| 'countAll' \| 'min' \| 'max' \| 'first' \| 'last'` (그룹화/트리 aggregates에서 사용) |
|
|
3252
3628
|
| `AggregateFn` | `AggregateType \| ((values: CellValue[]) => CellValue)` |
|
|
3253
3629
|
| `ValidationTrigger` | `'input' \| 'change' \| 'blur' \| 'commit'` |
|
|
@@ -3268,7 +3644,7 @@ interface PivotValueField {
|
|
|
3268
3644
|
| `EngineType` | `'native'` — **`'aq'`(Arquero)는 제거됨.** 단일 엔진화로 유니온에 값이 하나만 남았습니다(ITSM-2026-002190) |
|
|
3269
3645
|
| `TotalPosition` | `'before' \| 'after'` |
|
|
3270
3646
|
|
|
3271
|
-
> **`AggregateFunc` vs `AggregateFn` 구분**: `AggregateFunc`는
|
|
3647
|
+
> **`AggregateFunc` vs `AggregateFn` 구분**: `AggregateFunc`는 내장 집계 이름 14종의 문자열 유니온입니다. `AggregateFn = AggregateFunc | CustomAggregateFunction | WorkerFunctionRef`이며 `CustomAggregateFunction`은 `(values: CellValue[], ctx: AggregateContext) => CellValue`입니다. 행 그룹은 `RowGroupConfig.aggregates: RowGroupAggregate[]`의 `func`에, 트리는 `TreeConfig.aggregates` 맵의 값에 지정합니다. 일반 inline 콜백은 메인 스레드에서 실행하며 Worker 모드의 `values`는 빈 배열이므로 선언된 집계를 `ctx`로 읽어야 합니다. 등록된 `WorkerFunctionRef`는 별도 Worker 실행 계약이며 함수 본문을 직렬화하는 방식이 아닙니다. [Worker 실행 계약](runtime-contracts.md)을 참조하세요.
|
|
3272
3648
|
|
|
3273
3649
|
---
|
|
3274
3650
|
|
|
@@ -3281,9 +3657,16 @@ interface PivotValueField {
|
|
|
3281
3657
|
#### CellValue
|
|
3282
3658
|
|
|
3283
3659
|
```typescript
|
|
3284
|
-
type CellValue = string | number | boolean | Date | null | undefined | CellValue[];
|
|
3660
|
+
type CellValue = string | number | boolean | Date | null | undefined | CellValue[] | CellObject;
|
|
3661
|
+
|
|
3662
|
+
/** 객체 셀 값 — `dataType: 'object'` 열에 들어가는 순수 객체 */
|
|
3663
|
+
interface CellObject {
|
|
3664
|
+
readonly [key: string]: unknown;
|
|
3665
|
+
}
|
|
3285
3666
|
```
|
|
3286
3667
|
|
|
3668
|
+
> `CellObject` 는 [객체 값 열](#객체-값-열-datatype-object)용입니다. 묶기·필터·병합은 열의 `objectKey`, 정렬은 `sortValue`, 표시는 `mask.formatter` 로 다룹니다. 이벤트·`cellRenderer`·`changes` 의 `value` 가 객체일 수 있으니 값을 문자열로 다루는 소비 코드는 `isCellObject(value)` 로 먼저 가르세요.
|
|
3669
|
+
|
|
3287
3670
|
#### Row / RowData
|
|
3288
3671
|
|
|
3289
3672
|
```typescript
|
|
@@ -3295,13 +3678,16 @@ interface Row {
|
|
|
3295
3678
|
type RowData = Row;
|
|
3296
3679
|
```
|
|
3297
3680
|
|
|
3681
|
+
> `__psRowId`는 저장 전(dirty) 행을 그리드 안에서 식별하는 내부 표식입니다. `getAllData()`, `getRowById()`, `getVisibleData()`, `getDisplayedData()`, `getExportData()`, `changes.getAll()`이 돌려주는 행과 행 핸들 데이터(`rows.getById/getByIndex(...).data`, `rows.where` 조건 함수 인자, `rows.getByDisplayedIndex(...).data`, `selection.getSelectedRows()`/`getActiveRow()`의 `.data`, `rows.observeById(...)`의 `getSnapshot()`/`.data`), 이벤트 행 데이터(`cell:click`·`cell:dblclick`·`row:click`의 `row`, `contextmenu`의 `rowData`)에는 들어 있지 않으며 commit 후 원본 데이터에도 남지 않습니다. 저장 전 행의 식별자는 `changes.getAll()`의 `rowId`를 사용하세요.
|
|
3682
|
+
> 단, 셀을 그릴 때마다 불리는 설정 콜백(`cellRenderer`·`cellStyle`·`cellTooltip`·`formatter`·`validator`·`editableWhen`·`editorResolver`·커스텀 에디터)이 받는 행 데이터에는 `__psRowId`가 들어 있을 수 있습니다(셀마다 복사하지 않기 위함). 이 값은 읽기 전용으로 보고 저장·전송하지 마세요(ITSM-2026-004445).
|
|
3683
|
+
|
|
3298
3684
|
#### ColumnDef
|
|
3299
3685
|
|
|
3300
3686
|
```typescript
|
|
3301
3687
|
interface ColumnDef {
|
|
3302
3688
|
id: string;
|
|
3303
3689
|
label?: string; // 헤더 표시명 (FieldDef.header와 동일 역할)
|
|
3304
|
-
type?: 'string' | 'number' | 'boolean' | 'date' | 'array';
|
|
3690
|
+
type?: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'object';
|
|
3305
3691
|
width?: number;
|
|
3306
3692
|
minWidth?: number;
|
|
3307
3693
|
maxWidth?: number;
|
|
@@ -3312,6 +3698,9 @@ interface ColumnDef {
|
|
|
3312
3698
|
sortable?: boolean;
|
|
3313
3699
|
filterable?: boolean;
|
|
3314
3700
|
align?: CellAlign;
|
|
3701
|
+
objectKey?: ObjectKeyAccessor; // 객체 값 열 규칙 (FieldDef 에서 그대로 복사)
|
|
3702
|
+
sortValue?: SortValueAccessor;
|
|
3703
|
+
equals?: ObjectEqualsFn;
|
|
3315
3704
|
// ... FieldDef와 거의 동일 (FieldDef는 사용자 입력용, ColumnDef는 정규화된 내부 형태)
|
|
3316
3705
|
}
|
|
3317
3706
|
```
|
|
@@ -3459,6 +3848,12 @@ interface SortState {
|
|
|
3459
3848
|
mode?: 'pre' | 'post';
|
|
3460
3849
|
}
|
|
3461
3850
|
|
|
3851
|
+
// 문자열 순서 규칙(정렬·행 그룹·트리·피벗 행/열 순서·sortKey 가 돌려준 문자열·조건 필터 gt/gte/lt/lte/between·수식 < >):
|
|
3852
|
+
// 영문 대소문자만 무시한 문자 코드 비교, 실행 환경 언어와 무관. 대소문자만 다르면 대문자 먼저.
|
|
3853
|
+
// 예) ['삼성전자','LG전자','현대'] → LG전자, 삼성전자, 현대 / ['Banana','Zeta','apple'] → apple, Banana, Zeta
|
|
3854
|
+
// 숫자 문자열은 글자 단위('item10' < 'item9'). 같음 판정(eq, 그룹 키, 피벗 병합)은 대소문자를 구분한다.
|
|
3855
|
+
// 문자열 min/max 집계도 같은 규칙: 최소 = 오름차순 맨 앞, 최대 = 맨 뒤. 예) [cherry, Zeta, apple, Banana] → 최소 apple, 최대 Zeta (숫자·날짜는 그대로).
|
|
3856
|
+
|
|
3462
3857
|
interface FilterStateBase {
|
|
3463
3858
|
/** 필터링할 컬럼 ID (플랫: 컬럼 ID, 피벗: 행/열 필드 ID) */
|
|
3464
3859
|
id: string;
|
|
@@ -3471,7 +3866,11 @@ interface FilterStateBase {
|
|
|
3471
3866
|
* 같은 키끼리 먼저 접고 그룹 결과끼리 AND 로 합친다.
|
|
3472
3867
|
*/
|
|
3473
3868
|
groupKey?: string;
|
|
3474
|
-
/**
|
|
3869
|
+
/**
|
|
3870
|
+
* 스마트 필터링용 mask (보통 컬럼의 mask가 자동 주입됨 — 직접 지정 불필요).
|
|
3871
|
+
* 있으면 `eq`·`neq`·`contains`·`notContains`·`startsWith`·`endsWith`·`like`·`notLike`(문자열 값)를 **화면 표시값** 기준으로 비교.
|
|
3872
|
+
* 플랫·피벗·그룹 결과 필터(그룹 값 조건 — 그룹 열의 mask 자동 주입) 모두 같은 규칙.
|
|
3873
|
+
*/
|
|
3475
3874
|
mask?: MaskConfig;
|
|
3476
3875
|
/**
|
|
3477
3876
|
* (피벗 전용) 필터 적용 단계 override. 플랫 그리드에서는 무시.
|
|
@@ -3661,7 +4060,8 @@ await grid.filter({
|
|
|
3661
4060
|
`getViewState().filters`는 실행 가능한 일반 행 필터의 평탄화 결과다.
|
|
3662
4061
|
`getConfig()`/`applyConfig()`와 `getViewState()`/`setViewState()` 왕복은 소유 맵을 보존한다.
|
|
3663
4062
|
- 피벗에서는 행 그룹 결과 필터를 실행하지 않고 보존한다. 서버 row-model도 상태만 보존하며
|
|
3664
|
-
서버가 준 행을 로컬에서 탈락시키지 않는다.
|
|
4063
|
+
서버가 준 행을 로컬에서 탈락시키지 않는다. `rowModel: 'server'` + `loader`(SSRM) 서버 그룹에서는
|
|
4064
|
+
`fetchGroups`·`fetchAggregates` 요청의 `groupFilterModel` 로 서버에 넘겨 서버가 거른다(SSRM 절 참조).
|
|
3665
4065
|
- Worker는 조건식·수식을 지원한다. 실행 대상 그룹의 함수형 `custom` predicate는 직렬화할 수 없어
|
|
3666
4066
|
명시적으로 거부한다. 비활성·대상 레벨 부재 묶음은 Worker 전송에서 제외하되 원본에서 삭제하지 않는다.
|
|
3667
4067
|
- 집계 `id`는 선택적이며 생략 시 `getRowGroupAggregateId()`의 결정적 계산 alias를 사용한다.
|
|
@@ -3723,37 +4123,38 @@ interface PerformanceTiming {
|
|
|
3723
4123
|
|
|
3724
4124
|
### 데이터 로더 — LoaderConfig
|
|
3725
4125
|
|
|
3726
|
-
|
|
4126
|
+
데이터를 어디서 가져올지 정합니다. `use`와 `worker` 중 **하나만** 지정합니다(타입으로 강제).
|
|
3727
4127
|
|
|
3728
|
-
|
|
4128
|
+
| 형태 | 실행 위치 | 가리키는 것 |
|
|
4129
|
+
|---|---|---|
|
|
4130
|
+
| `loader: { use: OrderLoader, params }` | 메인 스레드 | `DataLoader`를 상속한 클래스 |
|
|
4131
|
+
| `loader: { worker: () => new Worker(...), params }` | Worker | `setupGridWorker({ loader })`를 호출하는 앱의 Worker 파일 |
|
|
4132
|
+
| `data: [...]` | 메인 스레드 | 행 배열 (loader 없이) |
|
|
3729
4133
|
|
|
3730
4134
|
```typescript
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
/** 초기 파라미터 (직렬화 가능한 plain object). 런타임에 grid.updateLoaderParams()로 갱신 */
|
|
3735
|
-
params?: Record<string, unknown>;
|
|
3736
|
-
}
|
|
4135
|
+
type LoaderConfig =
|
|
4136
|
+
| { use: DataLoaderClass; worker?: never; params?: Record<string, unknown> }
|
|
4137
|
+
| { worker: () => Worker; use?: never; params?: Record<string, unknown> };
|
|
3737
4138
|
```
|
|
3738
4139
|
|
|
4140
|
+
- `params`는 loader 인스턴스의 `this.params`가 됩니다. Worker 모드에서는 structured-clone 가능한 값을 넘기세요. Proxy(예: Vue `reactive`/`ref`)처럼 감싸여 복제가 안 되는 값은 그리드가 평범한 값으로 깊은 복사해 보내고 `[PureSheet] … 깊은 복사한 뒤 전송했습니다` 경고를 냅니다(같은 Worker·같은 요청 종류당 1회, 행 추가·수정 값과 XLSX 내보내기 행도 같음). 함수·클래스 인스턴스는 복사해도 보낼 수 없어 여전히 `DataCloneError`입니다. 런타임에 `grid.updateLoaderParams()`로 머지 갱신합니다.
|
|
4141
|
+
- 잘못된 설정(`use`에 일반 클래스, `use`와 `worker` 동시 지정, `worker`가 함수가 아님 등)은 `waitForReady()`가 오류로 거절합니다.
|
|
4142
|
+
- Worker 모드에서는 `data`/`loadData()`를 쓸 수 없습니다(PS502). Worker 등록 규칙은 [Worker 등록·데이터 세션 계약](runtime-contracts.md)을 보세요.
|
|
4143
|
+
|
|
3739
4144
|
```typescript
|
|
3740
|
-
//
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
4145
|
+
// 메인 스레드
|
|
4146
|
+
new PureSheet(container, {
|
|
4147
|
+
keyFields: ['id'],
|
|
4148
|
+
loader: { use: OrderLoader, params: { year: 2026 } },
|
|
4149
|
+
fields: [...],
|
|
4150
|
+
});
|
|
3748
4151
|
|
|
3749
|
-
//
|
|
4152
|
+
// Worker — orders.worker.ts 에서 setupGridWorker({ loader: OrderLoader }) 호출
|
|
3750
4153
|
new PureSheet(container, {
|
|
3751
|
-
|
|
3752
|
-
useWorker: true,
|
|
3753
|
-
workerFactory: () => new Worker(new URL('./app-grid.worker.ts', import.meta.url), { type: 'module' }),
|
|
4154
|
+
keyFields: ['id'],
|
|
3754
4155
|
loader: {
|
|
3755
|
-
|
|
3756
|
-
params: {
|
|
4156
|
+
worker: () => new Worker(new URL('./orders.worker.ts', import.meta.url), { type: 'module' }),
|
|
4157
|
+
params: { year: 2026 },
|
|
3757
4158
|
},
|
|
3758
4159
|
fields: [...],
|
|
3759
4160
|
});
|
|
@@ -3858,7 +4259,7 @@ type MergeConfig = BuiltInMergeConfig | CustomMergeConfig;
|
|
|
3858
4259
|
interface BuiltInMergeConfig {
|
|
3859
4260
|
/** 'content': 인접한 같은 값 / 'hierarchical': 상위 컬럼 범위 내에서만 하위도 병합 */
|
|
3860
4261
|
type: 'content' | 'hierarchical';
|
|
3861
|
-
/** 병합 대상 컬럼 (순서대로 계층) */
|
|
4262
|
+
/** 병합 대상 컬럼 id (순서대로 계층). 값 비교는 화면 셀과 같은 `row[field ?? id]` — `{ id:'nm', field:'name' }`은 name 값으로 병합 */
|
|
3862
4263
|
columns: string[];
|
|
3863
4264
|
/** 'lazy'(동적 탐색) | 'eager'(사전 계산) @default 'lazy' */
|
|
3864
4265
|
strategy?: 'lazy' | 'eager';
|
|
@@ -3892,13 +4293,15 @@ interface MergeRangeContext {
|
|
|
3892
4293
|
rowIndex: number;
|
|
3893
4294
|
/** 현재 컬럼의 `setMergeConfig` `columns` 배열 인덱스(화면 순서 아님) — `rowIndex`의 가로 대칭. `columnsInOrder` 기준 */
|
|
3894
4295
|
colIndex: number;
|
|
4296
|
+
/** 열 id (`field`가 따로 있어도 id) */
|
|
3895
4297
|
columnId: string;
|
|
3896
4298
|
/** columns(설정) 배열 내 인덱스 (계층 레벨) */
|
|
3897
4299
|
columnLevel: number;
|
|
4300
|
+
/** 이 칸의 화면 값 = `row[field ?? id]` (예: 열 `{ id:'nm', field:'name' }` → `row.name`) */
|
|
3898
4301
|
value: CellValue;
|
|
3899
|
-
/** 인접 행 데이터 조회 (세로 경계 탐색용) */
|
|
4302
|
+
/** 인접 행 데이터 조회 (세로 경계 탐색용) — 원본 행 그대로(데이터 키) */
|
|
3900
4303
|
getRowData: (index: number) => Row | null;
|
|
3901
|
-
/** 임의
|
|
4304
|
+
/** 임의 셀의 화면 값 (2D 경계 탐색용) — 열 id를 주면 `getRowData(rowIndex)?.[그 열의 field ?? id]`. 열이 아닌 이름은 `row[이름]` */
|
|
3902
4305
|
getCellValue: (rowIndex: number, columnId: string) => CellValue;
|
|
3903
4306
|
/** 병합 대상 컬럼 ID 목록 — `setMergeConfig`의 `columns` 배열 그대로 (가로 경계 탐색용) */
|
|
3904
4307
|
columnsInOrder: readonly string[];
|
|
@@ -3920,6 +4323,8 @@ interface MergeRangeResult {
|
|
|
3920
4323
|
|
|
3921
4324
|
> **세로 / 가로 / 2D 병합** — `startCol`/`endCol`을 **생략하면 세로 병합**(현재 컬럼만, 기존 `{ startRow, endRow }` 계약 그대로), **지정하면 가로/2D 병합**입니다. 컬럼 인덱스(`colIndex`/`startCol`/`endCol`)는 **`setMergeConfig`의 `columns` 배열 순서**(= `columnsInOrder`) 기준이며 화면(가시) 컬럼 순서가 아닙니다 — 사용자가 컬럼을 숨기거나 옮겨도 인덱스는 변하지 않습니다. 단 병합 칸은 앵커 컬럼(`columns[startCol]`)부터 오른쪽으로 그려지므로, 가로 병합은 덮는 컬럼들이 **화면에서도 같은 순서로 붙어 있고 같은 파티션(center/pinned) 안에 있을 때만** 시각적으로 올바릅니다. 화면상 사이에 다른 컬럼이 끼거나 순서가 뒤바뀌면 끼어든 컬럼까지 덮어 보일 수 있으며, 이때 `console.warn`을 영역·컬럼 조합당 한 번(병합 설정이나 가시 컬럼 배치가 바뀌면 다시) 남깁니다. 숨긴 컬럼은 화면 폭이 없으므로 병합 컬럼 사이에 끼어 있어도 문제가 되지 않습니다 — 병합 칸은 덮는 병합 컬럼 중 보이는 것들의 폭 합으로 그려집니다(본문·고정 행 동일). 영역 내 아무 셀에서 같은 사각형을 반환해도 전략이 anchor=`(startRow, startCol)`를 잡고 나머지를 숨김 처리합니다. `null` 또는 단일 셀(`startRow===endRow && startCol===endCol`)이면 병합 없음.
|
|
3922
4325
|
|
|
4326
|
+
> 변경: 커스텀 병합 콜백의 `value`·`getCellValue(rowIndex, 열 id)`는 이제 `field` 별칭을 따른 화면 값입니다(예전에는 `row[열 id]`라 `{ id:'nm', field:'name' }` 열이면 `undefined`). `columnId`·`columnsInOrder`·`getRowData`와 별칭 없는 열은 그대로입니다.
|
|
4327
|
+
|
|
3923
4328
|
> **고정 행 병합 (`includePinned`)** — 기본값(`false`)은 기존 동작 그대로 본문만 병합합니다. `true`면 같은 설정이 상단/하단 고정 행 영역에도 적용되며, 병합은 **각 영역 안에서만** 일어납니다(본문↔고정 영역을 넘는 병합 없음). 콜백은 `area`로 영역을 구분하고, 반환 `startRow`/`endRow`는 그 영역 안의 행 번호입니다. 좌우 고정 컬럼은 영역이 아니므로 같은 컬럼 구역 안에서만 가로 병합되며, 경계를 넘는 범위는 경고 후 무시됩니다. 병합 셀(본문·고정 행 모두)은 앵커(좌상단) 셀이 단독으로 그려질 때와 같게 — 앵커 컬럼의 `cellRenderer`로 그려지며(집계 고정 행은 원래처럼 포맷 텍스트), 데이터 고정 행의 클릭/선택은 앵커 셀로 전달되고 선택 강조는 본문 병합 셀과 같은 규칙(덮는 셀/행 중 하나라도 선택이면 강조)을 따릅니다(복사에는 병합이 반영되지 않음. 엑셀 내보내기는 `includePinnedRows: true`일 때 고정 행 병합을 반영).
|
|
3924
4329
|
>
|
|
3925
4330
|
> ```typescript
|
|
@@ -4149,13 +4554,21 @@ interface CustomEditorParams {
|
|
|
4149
4554
|
value: CellValue;
|
|
4150
4555
|
column: ColumnDef;
|
|
4151
4556
|
rowData: Record<string, unknown>;
|
|
4557
|
+
/** type-to-edit 으로 열렸을 때 친 글자 (IME 는 조합이 끝난 글자). 더블클릭·Enter·F2 로 열면 undefined */
|
|
4558
|
+
initialText?: string;
|
|
4152
4559
|
/** 값을 확정하고 에디터 닫기 */
|
|
4153
4560
|
onCommit: (newValue: CellValue) => void;
|
|
4154
4561
|
/** 편집 취소하고 에디터 닫기 */
|
|
4155
4562
|
onCancel: () => void;
|
|
4563
|
+
/** 편집 소유 portal 등록. 반환 함수는 등록만 해제하며 DOM은 제거하지 않음. */
|
|
4564
|
+
registerOwnedSurface?: (surface: HTMLElement) => () => void;
|
|
4156
4565
|
}
|
|
4157
4566
|
```
|
|
4158
4567
|
|
|
4568
|
+
portal의 focus·해제·destroy 책임과 `getValue` 규칙은 [EditorConfig](#editorconfig)를 참조하세요. React/Vue template props에 이 멤버가 자동 전달되는 것은 아닙니다.
|
|
4569
|
+
|
|
4570
|
+
**`initialText`** — 셀을 선택한 채 글자 키를 눌러(type-to-edit) 열면 그 글자가 옵니다(`'x'`). IME 키(한글 등)로 열면 그리드가 셀에 임시 입력칸을 띄워 조합을 받고, 조합이 끝난 뒤 그 글자(`'한'`)로 renderer 를 부릅니다 — 이때 포커스는 임시 입력칸에서 빠지므로 renderer 가 돌려준 요소가 스스로 focus 해야 합니다. 조합 중 Esc 는 편집 취소입니다. 더블클릭·Enter·F2 로 열면 `undefined` 입니다. `startEditById(id, col, { initialValue })` 를 주면 그 글자입니다. 글자를 원래 값 대신 쓸지, 원래 값 끝에 이어 붙일지는 에디터가 정합니다. (예전에는 custom 에디터가 type-to-edit 으로 열리지 않았습니다.)
|
|
4571
|
+
|
|
4159
4572
|
```typescript
|
|
4160
4573
|
// 드롭다운 (검색 가능)
|
|
4161
4574
|
{
|
|
@@ -4248,26 +4661,43 @@ interface CellRendererInstance {
|
|
|
4248
4661
|
* 미정의 시: 값/행 변경 또는 명시 reload이면 destroy 후 re-mount. 그 외에는 유지.
|
|
4249
4662
|
*/
|
|
4250
4663
|
update?: (params: CellRendererParams) => boolean | void;
|
|
4251
|
-
/**
|
|
4252
|
-
|
|
4664
|
+
/** mount/update가 예약한 DOM commit 완료. 없으면 동기 renderer로 취급 */
|
|
4665
|
+
whenReady?: () => void | Promise<void>;
|
|
4666
|
+
/** 안전한 중지/복귀를 모두 보증하는 renderer만 선언 */
|
|
4667
|
+
suspension?: CellRendererSuspension;
|
|
4668
|
+
/** terminal cleanup. 비동기 정리는 실제 완료 Promise를 반환 */
|
|
4669
|
+
destroy?: () => void | Promise<void>;
|
|
4670
|
+
}
|
|
4671
|
+
interface CellRendererSuspension {
|
|
4672
|
+
suspend: () => void | Promise<void>;
|
|
4673
|
+
resume: (params: CellRendererParams) => boolean | void | Promise<boolean | void>;
|
|
4253
4674
|
}
|
|
4254
4675
|
```
|
|
4255
4676
|
|
|
4256
4677
|
명시 `loadData`의 최신 요청이 데이터를 정상 설치하면, 표시되는 custom renderer는 같은 행 객체/ID/값이어도 다시 평가됩니다. 유지된 lifecycle 인스턴스는 `update`를 받고, update가 없거나 false이면 destroy 후 재마운트됩니다. 보이지 않는 셀을 즉시 mount하지는 않으며 다음 표시 시 최신 reload 상태로 그립니다. 외부 closure 자체의 자동 추적은 제공하지 않습니다. 일반 `refresh()`/스크롤의 기존 dirty-check 정책은 이 계약으로 확대하지 않습니다. 설치 전 실패하거나 뒤늦게 완료된 오래된 load는 새 renderer invalidation 경계를 만들지 않습니다.
|
|
4257
4678
|
|
|
4679
|
+
비동기 renderer 계약의 범위:
|
|
4680
|
+
- factory와 `update`는 동기입니다. Promise-returning update/factory는 지원하지 않습니다. `whenReady`는 해당 mount/update의 framework DOM commit을 기다리며, 임의 fetch·async effect·모든 Suspense 하위 데이터 완료를 뜻하지 않습니다.
|
|
4681
|
+
- 편집 중이 아닌 pooled cell은 release 때 `suspension` 쌍이 없으면 destroy됩니다. 쌍이 있으면 suspend 완료 후에만 보존하며, 다시 사용할 때 최신 full params로 resume합니다. resume=false/실패는 종료 후 remount입니다. suspend는 외부 portal·구독·타이머까지 안전하게 중지한다는 renderer의 보증이며 CSS 숨김만으로 선언하면 안 됩니다.
|
|
4682
|
+
- destroy는 인스턴스당 한 번 호출합니다. 실패/영구 pending cleanup은 재사용 성공으로 처리하지 않습니다. renderer는 destroy 이후 늦은 비동기 작업이 UI를 되살리지 않도록 자체 terminal guard를 가져야 합니다.
|
|
4683
|
+
- row의 inline gate는 외부 업무 popup까지 닫지 않습니다. 외부 renderer-owned surface는 cleanup 완료 경계까지 정리해야 하며, release 호출 즉시 모든 portal이 제거된다는 보증은 없습니다. 동기 `PureSheet.destroy()` 반환도 비동기 cleanup 완료 신호가 아닙니다.
|
|
4684
|
+
- 기존 `() => void` destroy는 유지되지만 `destroy: () => array.push(x)`처럼 값을 반환하던 구현은 `void | Promise<void>` 타입에 맞게 `{ array.push(x); }`로 바꿔야 합니다. core와 Vue/React bridge를 함께 사용해야 completion 보증이 전달됩니다.
|
|
4685
|
+
- cached-detail은 기존 `keepDetailRows`/adapter `keepMasterDetailRows` 캐시 동작을 유지하며 새 cell lifecycle 계약에서 제외합니다. 별도 detail lifecycle 확장이나 캐시 비활성화는 하지 않습니다. 편집 중인 셀의 scroll-out 유예/취소/확정 정책 및 사용자 수동 브라우저 검증도 이 셀 계약의 완료 보증에 포함하지 않습니다.
|
|
4686
|
+
|
|
4258
4687
|
```typescript
|
|
4259
4688
|
// Simple 형태 — 매 렌더마다 새 요소 반환
|
|
4260
4689
|
{ id: 'status', cellRenderer: ({ value }) => `<span class="badge-${value}">${value}</span>` }
|
|
4261
4690
|
|
|
4262
|
-
// Lifecycle 형태 — React adapter
|
|
4691
|
+
// Lifecycle 형태 — 동기 DOM renderer (React/Vue는 각 adapter의 template bridge 사용)
|
|
4263
4692
|
{
|
|
4264
4693
|
id: 'chart',
|
|
4265
4694
|
cellRenderer: (container, params) => {
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4695
|
+
const label = document.createElement('span');
|
|
4696
|
+
container.appendChild(label);
|
|
4697
|
+
label.textContent = String(params.value ?? '');
|
|
4268
4698
|
return {
|
|
4269
|
-
update: (next) =>
|
|
4270
|
-
destroy: () =>
|
|
4699
|
+
update: (next) => { label.textContent = String(next.value ?? ''); },
|
|
4700
|
+
destroy: () => { label.remove(); },
|
|
4271
4701
|
};
|
|
4272
4702
|
},
|
|
4273
4703
|
}
|
|
@@ -4305,7 +4735,7 @@ interface GridConfigSnapshot {
|
|
|
4305
4735
|
|
|
4306
4736
|
// Flat 모드 ↓
|
|
4307
4737
|
fields?: FieldDef[]; // 런타임 width/visible/order/pinned 반영됨
|
|
4308
|
-
|
|
4738
|
+
rowGroupConfig?: RowGroupConfig | null; // null = 그룹 없음
|
|
4309
4739
|
rowTemplate?: RowTemplate;
|
|
4310
4740
|
columnGroups?: ColumnGroupDef[];
|
|
4311
4741
|
|
|
@@ -4334,8 +4764,9 @@ interface GridConfigSnapshot {
|
|
|
4334
4764
|
|
|
4335
4765
|
// 공통 view 상태 ↓
|
|
4336
4766
|
sorts?: SortState[];
|
|
4337
|
-
|
|
4338
|
-
|
|
4767
|
+
sortGroups?: SortGroupMap; // 소유자별 정렬 — 있으면 sorts보다 우선
|
|
4768
|
+
filters?: FilterState[]; // row 스코프만 평탄화한 파생값 (옛 소비처 호환)
|
|
4769
|
+
filterGroups?: FilterGroupMap; // 있으면 filters보다 우선; 그룹 결과 필터도 이 맵으로 복원
|
|
4339
4770
|
|
|
4340
4771
|
// 트리/마스터-디테일 ↓
|
|
4341
4772
|
treeConfig?: TreeConfig;
|
|
@@ -4371,11 +4802,11 @@ interface GridConfigSnapshot {
|
|
|
4371
4802
|
**제외되는 항목** (직렬화 불가):
|
|
4372
4803
|
- `data` — `loadData()`로 별도 관리
|
|
4373
4804
|
- 모든 함수 (`cellRenderer`, `cellTooltip`, `formatHeader`, validator, mask formatter, `formatRow` 등) — 사용자 코드에서 재등록
|
|
4374
|
-
- DOM/객체 참조 (`undoStack`, `loader
|
|
4805
|
+
- DOM/객체 참조 (`undoStack`, `loader`, `dragDropConfig` 등)
|
|
4375
4806
|
- dirty 상태 (`grid.changes.getAll()`로 별도 저장)
|
|
4376
4807
|
|
|
4377
4808
|
**현재 직렬화 안 되는 옵션 (향후 추가 여지)**:
|
|
4378
|
-
- `engine`, `
|
|
4809
|
+
- `engine`, `dynamicRowHeights`, `rowHeightChangeThreshold`
|
|
4379
4810
|
- `enableReferCache`, `preventKeyEdit`
|
|
4380
4811
|
- `rowStyle`, `headerStyle` (`height:Npx` 추출 alias — 일반 CSS 적용 아님)
|
|
4381
4812
|
- `PivotConfig.sorts` / `PivotConfig.filters` (피벗 전용 — `GridConfigSnapshot.sorts/filters` 와 별개)
|
|
@@ -4445,6 +4876,7 @@ const grid = new PureSheet(el, {
|
|
|
4445
4876
|
| `ColumnKeyInput` / `ColumnKeys` | 타입 | 입력/출력 형태 |
|
|
4446
4877
|
|
|
4447
4878
|
> **읽을 때 `field`, 가리킬 때 `id`.** 원본 행에서 값을 꺼내는 자리(`row[...]`)는 반드시 `field`, 정렬/필터/저장/DOM dataset 처럼 *컬럼을 지목*하는 자리는 `id` 입니다.
|
|
4879
|
+
> 예) `{ id: 'nm', field: 'name' }` 셀의 `cell:click`·`cell:dblclick`·`contextmenu`·`cell:tooltip` `value`, `cellTooltip`·`cellAttributes(fn)` 의 `value`, `formatRow` `cells.nm.value`·`originalValue`·`isModified`, 유효성 검사 validator 의 값은 모두 `row.name` 이고, 열을 가리키는 `columnId`·`cells` 키·검사 오류 `field` 는 `'nm'` 입니다(ITSM-2026-004445).
|
|
4448
4880
|
|
|
4449
4881
|
**피벗 쪽 대응** — `PivotFieldBase.id?` 가 추가됐습니다(미지정 시 `field`). 값 필드는 같은 `field` 를 합계·평균으로 **여러 번** 넣을 수 있어 `field` 만으로는 식별이 깨집니다.
|
|
4450
4882
|
|
|
@@ -4518,6 +4950,13 @@ const grid = new PureSheet(el, {
|
|
|
4518
4950
|
- 기존의 잘못된 저장본 `$변동 상태 …`는 자동으로 추측 복구하지 않습니다. 원문을 보존하고 편집기에서 오류를 표시합니다. 사용자가 컬럼을 다시 선택하거나 `$["변동 상태"]`로 명시적으로 고쳐야 합니다. 조건부 서식에서 오류가 있으면 규칙 전체 적용을 막고 이전 적용본을 유지합니다.
|
|
4519
4951
|
- 새 표기를 읽지 못하는 구버전 Core로 되돌리면 해당 수식은 동작하지 않습니다. Core와 공용 wrapper를 함께 배포하고, 소비처 업그레이드 전에는 새 저장본을 공유하지 마세요.
|
|
4520
4952
|
|
|
4953
|
+
**점 표기 경로 (수식 필터·조건부 서식)**
|
|
4954
|
+
|
|
4955
|
+
- 열 `{ id: 'ownerTeam', field: 'owner.team' }` 의 `$ownerTeam`·`[ownerTeam]` 은 `row.owner.team` 을 읽습니다(열 id 우선 규칙 그대로).
|
|
4956
|
+
- 열이 아닌 점 이름은 데이터 경로로 읽습니다: `[owner.team] == "B"`, `$["owner.name"] == "김철수"`. 경로에 값이 없으면(`[owner.missing]`) `null` 입니다.
|
|
4957
|
+
- 같은 이름의 열 id 가 있으면 열이 이깁니다(열 `{ id: 'owner.team', field: 't' }` 이면 `[owner.team]` 은 `row.t`). 행에 `'owner.team'` 키가 그대로 있으면 그 키가 경로보다 먼저입니다.
|
|
4958
|
+
- `$owner.name` 은 지원하지 않습니다 — `$` 뒤 단순 이름에는 점이 들어가지 않습니다. `[owner.name]` 또는 `$["owner.name"]` 로 쓰세요.
|
|
4959
|
+
|
|
4521
4960
|
---
|
|
4522
4961
|
|
|
4523
4962
|
### 값 목록 위젯 — ValueCheckList
|
|
@@ -4549,7 +4988,7 @@ list.destroy();
|
|
|
4549
4988
|
|
|
4550
4989
|
| 메서드 | 설명 |
|
|
4551
4990
|
|---|---|
|
|
4552
|
-
| `setValues(values, selectedKeys?, meta?)` | 목록 채우기. `meta`: `{ truncated?, limit?, rawValues?, offListPicks? }`. `selectedKeys` 생략 시 **모드 기본 체크**(`'exclude'`=전체 선택, `'include'`=전체 해제) |
|
|
4991
|
+
| `setValues(values, selectedKeys?, meta?)` | 목록 채우기. `meta`: `{ truncated?, limit?, rawValues?, offListPicks?, objectSpec? }`. `selectedKeys` 생략 시 **모드 기본 체크**(`'exclude'`=전체 선택, `'include'`=전체 해제). 객체 값 열이면 `objectSpec`(`{ objectKey: 'id' }` 등)을 넘겨 항목 키를 `objectKey` 로 만든다 — 아래 참조 |
|
|
4553
4992
|
| `setLoading(loading)` | 로딩 표시 |
|
|
4554
4993
|
| `getSelectedKeys()` / `getSelectedItems()` | 현재 선택 |
|
|
4555
4994
|
| `setMode(mode)` | 저장 방향 전환 — 지목 집합은 유지하고 의미만 뒤집으므로 **체크가 반전**됩니다 |
|
|
@@ -4581,7 +5020,7 @@ list.destroy();
|
|
|
4581
5020
|
|
|
4582
5021
|
| 속성 | 타입 | 설명 |
|
|
4583
5022
|
|---|---|---|
|
|
4584
|
-
| `key` | `string` | 선택 상태 관리용 키(문자열화된 값, 빈 값은 `VALUE_LIST_BLANK_KEY`) |
|
|
5023
|
+
| `key` | `string` | 선택 상태 관리용 키(문자열화된 값, 빈 값은 `VALUE_LIST_BLANK_KEY`). 원본이 객체면 `objectKey` 문자열(`objectSpec` 없으면 객체 JSON) |
|
|
4585
5024
|
| `label` | `string` | 화면 표시 라벨 |
|
|
4586
5025
|
| `value` | `CellValue` | 필터에 실을 **원본값**(빈 값은 `null`) |
|
|
4587
5026
|
| `pinned` | `boolean?` | 현재 후보 목록에는 없지만 사용자가 지목해 **목록 맨 위에 고정 표시 중**인 항목 |
|
|
@@ -4616,6 +5055,68 @@ interface ColumnValuesProgress {
|
|
|
4616
5055
|
|
|
4617
5056
|
> **왜 `rawValues` 를 함께 돌려주나** — 표시값으로 필터링하면 행마다 `applyMask` 를 불러야 하는데 `toLocaleString()` 자체가 비싸서 5M 행에서 **1,903ms(전체의 92%)** 를 먹습니다(실측). 원본끼리 비교하면 62ms 로 **30.6배** 빠릅니다. 게다가 함수형 mask 는 Worker 로 복제되지 않아 워커 모드가 크래시합니다. 문자열 `parser` 역변환은 mask 가 다대일이라 왕복이 깨집니다(실측: `NaN` → `"NaN"` → `0`).
|
|
4618
5057
|
|
|
5058
|
+
**객체 값 열 (`dataType: 'object'`)** — `getFilterValues` 는 `objectKey` 로 중복을 없애고, `values` 에 라벨(mask), `rawValues` 에 원본 객체를 줍니다. 동명이인은 라벨이 같아도 항목이 따로입니다. 위젯에 `objectSpec` 을 넘기면 항목 키가 `objectKey` 가 됩니다(넘기지 않으면 라벨이 같은 두 객체도 JSON 키로 나뉩니다).
|
|
5059
|
+
|
|
5060
|
+
```typescript
|
|
5061
|
+
list.setValues(['김철수', '김철수', '홍길동', null], undefined, {
|
|
5062
|
+
rawValues: [{ id: 'u2', name: '김철수' }, { id: 'u3', name: '김철수' }, { id: 'u1', name: '홍길동' }, null],
|
|
5063
|
+
objectSpec: { objectKey: 'id' },
|
|
5064
|
+
});
|
|
5065
|
+
// 항목 key: 'u2', 'u3', 'u1' (+ 빈 값) — label 은 '김철수'·'김철수'·'홍길동', value 는 원본 객체
|
|
5066
|
+
list.setSelectedKeys(['u3']);
|
|
5067
|
+
list.getSelectedItems().map(i => i.value); // → [{ id: 'u3', name: '김철수' }]
|
|
5068
|
+
// 필터: { operator: 'in', filterValue: [위 value 들] } — 그리드가 objectKey 로 비교한다
|
|
5069
|
+
```
|
|
5070
|
+
|
|
5071
|
+
저장된 필터에서 체크 상태를 되살릴 때는 같은 규칙의 `valueListKeyOf(label, raw, spec)` 로 키를 만듭니다: `valueListKeyOf('김철수', { id: 'u3', name: '김철수' }, { objectKey: 'id' })` → `'u3'`, `valueListKeyOf('1,000', 1000)` → `'1,000'`, `valueListKeyOf(null, null)` → `VALUE_LIST_BLANK_KEY`. `appendValues(values, meta)` 도 `meta.objectSpec` 을 받습니다.
|
|
5072
|
+
|
|
5073
|
+
---
|
|
5074
|
+
|
|
5075
|
+
### 객체 값·필드 경로 헬퍼
|
|
5076
|
+
|
|
5077
|
+
그리드가 [점 표기 필드 경로](#점-표기-필드-경로-field-ownerteam)와 [객체 값 열](#객체-값-열-datatype-object)에 쓰는 규칙을 소비처(래퍼·저장 로직·JS 서버)가 그대로 쓰도록 export 합니다.
|
|
5078
|
+
|
|
5079
|
+
```typescript
|
|
5080
|
+
import {
|
|
5081
|
+
getFieldValue, createFieldGetter, resolveFieldWrite, setFieldValue,
|
|
5082
|
+
isCellObject, buildObjectFieldSpecs, objectKeyOf, objectKeyString,
|
|
5083
|
+
sortValueOf, displayKeyText, objectDisplayText, compareSortKeys, valuesEqual, valueListKeyOf,
|
|
5084
|
+
type ObjectFieldSpec, type ObjectFieldSpecMap,
|
|
5085
|
+
} from '@vmscloud/moz-ui-grid-core';
|
|
5086
|
+
```
|
|
5087
|
+
|
|
5088
|
+
**필드 경로**
|
|
5089
|
+
|
|
5090
|
+
| Export | 입력 → 출력 |
|
|
5091
|
+
|---|---|
|
|
5092
|
+
| `getFieldValue(row, field)` | `({ owner: { team: 'A' } }, 'owner.team')` → `'A'`. 행에 `'owner.team'` 키가 그대로 있으면 그 값. 중간이 `null` 이면 `undefined` |
|
|
5093
|
+
| `createFieldGetter(field)` | 루프용 읽기 함수. 필드당 한 번 만들고 행마다 부른다: `const read = createFieldGetter('owner.team'); rows.map(read)` |
|
|
5094
|
+
| `resolveFieldWrite(row, field, value)` | 행은 그대로 두고 실제로 바뀔 **최상위 변경**만 계산: `({ owner: { id: 'u1', team: 'A' } }, 'owner.team', 'B')` → `{ key: 'owner', value: { id: 'u1', team: 'B' } }` |
|
|
5095
|
+
| `setFieldValue(row, field, value)` | 행에 직접 쓴다(경로면 최상위 객체를 복사해 교체). 쓴 최상위 키를 돌려준다: `'owner'` |
|
|
5096
|
+
|
|
5097
|
+
**객체 값**
|
|
5098
|
+
|
|
5099
|
+
| Export | 입력 → 출력 |
|
|
5100
|
+
|---|---|
|
|
5101
|
+
| `isCellObject(value)` | 순수 객체만 `true` (Date·배열·클래스 인스턴스·`null` 은 `false`) |
|
|
5102
|
+
| `buildObjectFieldSpecs(columns)` | `ColumnDef[]` 중 `type: 'object'` 열의 규칙을 모은다. 키는 데이터 키(`field ?? id`), 같은 데이터 키를 두 열이 쓰면 첫 열 규칙 → `ObjectFieldSpecMap` |
|
|
5103
|
+
| `objectKeyOf(value, spec?, column?)` | 객체면 `spec.objectKey` 결과(없으면 `null`), 객체가 아니면 값 그대로. `{ id: 'u2' }`, `{ objectKey: 'id' }` → `'u2'`. 숫자 키는 숫자 그대로. `objectKey` 가 없으면 안정 JSON + `PS314`(spec 있음) / `PS315`(spec 없음) 경고 1회 |
|
|
5104
|
+
| `objectKeyString(value, spec?, column?)` | `objectKeyOf` 를 문자열로(Map/Set·groupId 비교용). `null`/`undefined` 는 그대로 |
|
|
5105
|
+
| `sortValueOf(value, spec?, column?)` | 객체면 `sortValue` → `displayKey` → `objectKey` → 안정 JSON 순. `{ rank: 2, name: '김철수' }`, `{ sortValue: v => [v.rank, v.name] }` → `[2, '김철수']` |
|
|
5106
|
+
| `displayKeyText(value, spec?)` | 객체이고 `spec.displayKey` 가 있을 때만 그 값의 문자열(`null`/`undefined` → `''`), 아니면 `undefined`. `{ name: '김철수' }`, `{ displayKey: 'name' }` → `'김철수'` |
|
|
5107
|
+
| `objectDisplayText(value, spec?)` | mask 가 없을 때의 보이는 글자 — `displayKeyText`, 없으면 JSON. 객체가 아니면 `undefined` |
|
|
5108
|
+
| `compareSortKeys(a, b, cmp?)` | 오름차순 비교(-1/0/1). 배열이면 앞 값부터, 원소의 `null` 이 앞. 배열이 아니면 그리드 기본 셀 비교와 같다 |
|
|
5109
|
+
| `valuesEqual(a, b, spec?)` | 변경 감지 비교. 둘 중 하나라도 순수 객체일 때만 `spec.equals`(없으면 내용 비교), 아니면 `Object.is` |
|
|
5110
|
+
| `valueListKeyOf(label, raw?, spec?)` | 값 목록 항목 키 — [ValueCheckList](#값-목록-위젯--valuechecklist) 참조 |
|
|
5111
|
+
|
|
5112
|
+
`ObjectFieldSpec` = `{ column?, objectKey?, displayKey?, sortValue?, equals? }`, `ObjectFieldSpecMap` = `Record<데이터 키, ObjectFieldSpec>`. 열 정의 쪽 타입 `ObjectValueColumnOptions`·`ObjectKeyAccessor`·`SortValueAccessor`·`ObjectValueAccessor`·`ObjectEqualsFn` 도 export 됩니다.
|
|
5113
|
+
|
|
5114
|
+
```typescript
|
|
5115
|
+
// JS 서버에서 그리드와 같은 키로 묶기
|
|
5116
|
+
const specs = buildObjectFieldSpecs([{ id: 'owner', type: 'object', objectKey: 'id' }]);
|
|
5117
|
+
const key = objectKeyString(row.owner, specs.owner); // { id: 'u2', … } → 'u2'
|
|
5118
|
+
```
|
|
5119
|
+
|
|
4619
5120
|
---
|
|
4620
5121
|
|
|
4621
5122
|
### formatRow 컨텍스트 — FormatRowInfo
|
|
@@ -5034,63 +5535,449 @@ interface CellAttributeParams {
|
|
|
5034
5535
|
|
|
5035
5536
|
---
|
|
5036
5537
|
|
|
5037
|
-
### DataLoader 클래스 —
|
|
5538
|
+
### DataLoader 클래스 — 커스텀 데이터 로딩
|
|
5038
5539
|
|
|
5039
|
-
|
|
5540
|
+
`loader.use`(메인 스레드)와 `setupGridWorker({ loader })`(Worker)에 넘기는 클래스의 베이스입니다. 같은 클래스를 두 모드에서 그대로 쓸 수 있습니다. Worker 모드에서는 Worker 안에서 실행되므로 DOM/localStorage는 쓸 수 없고 `fetch`/`WebSocket` 등은 쓸 수 있습니다.
|
|
5040
5541
|
|
|
5041
5542
|
```typescript
|
|
5042
5543
|
abstract class DataLoader {
|
|
5043
|
-
/**
|
|
5544
|
+
/** config 의 loader.params (updateLoaderParams() 로 머지 갱신) */
|
|
5044
5545
|
protected params: Record<string, unknown>;
|
|
5045
5546
|
|
|
5046
|
-
/**
|
|
5547
|
+
/** 라이브러리가 호출 — grid.updateLoaderParams() 시 Object.assign 머지 */
|
|
5047
5548
|
updateParams(newParams: Record<string, unknown>): void;
|
|
5048
5549
|
|
|
5049
|
-
/**
|
|
5050
|
-
|
|
5550
|
+
/** rowModel 'client' — 전체 행을 한 번에. 초기화와 refreshLoaderData() 때 호출. 플랫/트리 공통 */
|
|
5551
|
+
fetchAll?(): Promise<Row[]>;
|
|
5552
|
+
|
|
5553
|
+
/** rowModel 'server' — 요청 구간의 행만. 화면에 필요한 블록마다 호출 */
|
|
5554
|
+
fetchRange?(request: FetchRangeRequest): Promise<FetchRangeResult>;
|
|
5555
|
+
|
|
5556
|
+
/** rowModel 'server' — (선택) 필터 값 목록(체크박스). 없으면 받아 둔 행에서만 모음 */
|
|
5557
|
+
fetchFilterValues?(request: FetchFilterValuesRequest): Promise<FetchFilterValuesResult>;
|
|
5051
5558
|
|
|
5052
|
-
/**
|
|
5053
|
-
|
|
5559
|
+
/** rowModel 'server' — (선택) 툴바 검색. 없으면 받아 둔 행에서만 찾음 */
|
|
5560
|
+
fetchSearch?(request: FetchSearchRequest): Promise<FetchSearchResult>;
|
|
5561
|
+
|
|
5562
|
+
/** rowModel 'server' — (선택) 집계. 하단 합계 행·선택 합계(상태 표시줄). 없으면 둘 다 표시하지 않음 */
|
|
5563
|
+
fetchAggregates?(request: FetchAggregatesRequest): Promise<FetchAggregatesResult>;
|
|
5564
|
+
|
|
5565
|
+
/** rowModel 'server' + rowGroupConfig — 그룹 행 목록. 그룹을 쓰려면 필요 */
|
|
5566
|
+
fetchGroups?(request: FetchGroupsRequest): Promise<FetchGroupsResult>;
|
|
5054
5567
|
}
|
|
5055
5568
|
```
|
|
5056
5569
|
|
|
5570
|
+
- 어떤 메서드를 부를지는 config 의 `rowModel` 이 정합니다. 그 모드에 맞는 메서드가 없으면 `waitForReady()` 가 오류로 거절합니다. 두 메서드를 모두 구현하면 `rowModel` 만 바꿔 전체 불러오기와 SSRM 을 오갈 수 있습니다.
|
|
5571
|
+
|
|
5572
|
+
- `@vmscloud/moz-ui-grid-core`와 `@vmscloud/moz-ui-grid-core/worker` 양쪽에서 export됩니다. Worker 파일에서는 `/worker`에서 import하세요(메인 스레드 UI 코드가 Worker 번들에 들어가지 않음).
|
|
5573
|
+
- 클래스 판별은 `instanceof`가 아니라 static 표시(`DataLoader.isMozGridLoader`)로 하므로 번들이 두 벌이어도 동작합니다.
|
|
5574
|
+
- 트리 입력은 `fetchAll()`가 중첩(`children` 배열) 또는 평탄(`parentField`) 행을 반환하면 됩니다. 자식 지연 로딩은 지원하지 않습니다.
|
|
5575
|
+
|
|
5057
5576
|
```typescript
|
|
5058
|
-
//
|
|
5577
|
+
// order-loader.ts
|
|
5059
5578
|
import { DataLoader } from '@vmscloud/moz-ui-grid-core';
|
|
5060
5579
|
import type { Row } from '@vmscloud/moz-ui-grid-core';
|
|
5061
5580
|
|
|
5062
|
-
export
|
|
5063
|
-
async
|
|
5064
|
-
const res = await fetch(
|
|
5065
|
-
|
|
5066
|
-
});
|
|
5581
|
+
export class OrderLoader extends DataLoader {
|
|
5582
|
+
async fetchAll(): Promise<Row[]> {
|
|
5583
|
+
const res = await fetch(`/api/orders?year=${this.params.year}`);
|
|
5584
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
5067
5585
|
return res.json();
|
|
5068
5586
|
}
|
|
5587
|
+
}
|
|
5069
5588
|
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5589
|
+
// 그리드 생성 (메인 스레드)
|
|
5590
|
+
const grid = new PureSheet(container, {
|
|
5591
|
+
keyFields: ['id'],
|
|
5592
|
+
loader: { use: OrderLoader, params: { year: 2026 } },
|
|
5593
|
+
fields: [...],
|
|
5594
|
+
});
|
|
5595
|
+
|
|
5596
|
+
// 런타임 파라미터 갱신 후 다시 불러오기
|
|
5597
|
+
await grid.updateLoaderParams({ year: 2025 });
|
|
5598
|
+
await grid.refreshLoaderData();
|
|
5599
|
+
```
|
|
5600
|
+
|
|
5601
|
+
```typescript
|
|
5602
|
+
// orders.worker.ts — 같은 클래스를 Worker 에서 사용
|
|
5603
|
+
import { setupGridWorker } from '@vmscloud/moz-ui-grid-core/worker';
|
|
5604
|
+
import { OrderLoader } from './order-loader';
|
|
5605
|
+
setupGridWorker({ loader: OrderLoader });
|
|
5606
|
+
```
|
|
5607
|
+
|
|
5608
|
+
#### 받아 오는 방식과 포맷은 자유
|
|
5609
|
+
|
|
5610
|
+
`fetchAll`·`fetchRange` 등은 소비처가 직접 쓰는 async 함수라, **그 안에서 무엇으로 어떻게 받든 상관없습니다**(fetch·WebSocket·SSE, JSON·Arrow·protobuf·CSV, 압축 등). 그리드가 요구하는 것은 **돌려주는 값의 모양**(행 객체 `Row[]`)뿐입니다.
|
|
5611
|
+
|
|
5612
|
+
```typescript
|
|
5613
|
+
// 예: 서버가 Apache Arrow IPC 로 보내는 경우 (apache-arrow 패키지는 소비처가 설치)
|
|
5614
|
+
import { tableFromIPC } from 'apache-arrow';
|
|
5615
|
+
|
|
5616
|
+
class OrderLoader extends DataLoader {
|
|
5617
|
+
async fetchRange(req: FetchRangeRequest): Promise<FetchRangeResult> {
|
|
5618
|
+
const res = await fetch(`/api/orders.arrow?start=${req.startRow}&end=${req.endRow}`, { signal: req.signal });
|
|
5619
|
+
const table = tableFromIPC(new Uint8Array(await res.arrayBuffer()));
|
|
5620
|
+
return { rows: table.toArray().map(row => row.toJSON()), rowCount: Number(res.headers.get('X-Total-Count')) };
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
```
|
|
5624
|
+
|
|
5625
|
+
---
|
|
5626
|
+
|
|
5627
|
+
### 서버 행 모델 (SSRM) — `rowModel: 'server'` + `loader`
|
|
5628
|
+
|
|
5629
|
+
행은 서버에 두고, 그리드가 **화면에 필요한 구간만** 블록 단위(기본 100행)로 요청합니다. 1천만 행이어도 브라우저에는 본 블록만 남습니다.
|
|
5630
|
+
|
|
5631
|
+
```typescript
|
|
5632
|
+
class OrderLoader extends DataLoader {
|
|
5633
|
+
async fetchRange(req: FetchRangeRequest): Promise<FetchRangeResult> {
|
|
5634
|
+
const res = await fetch('/api/orders/rows', {
|
|
5635
|
+
method: 'POST',
|
|
5636
|
+
body: JSON.stringify({ startRow: req.startRow, endRow: req.endRow, sort: req.sortModel, filter: req.filterModel, anchor: req.anchor }),
|
|
5637
|
+
signal: req.signal, // 필요 없어진 요청은 그리드가 취소
|
|
5638
|
+
});
|
|
5639
|
+
const { rows, total, nextCursor } = await res.json();
|
|
5640
|
+
return { rows, rowCount: total, nextCursor };
|
|
5073
5641
|
}
|
|
5074
5642
|
}
|
|
5075
5643
|
|
|
5076
|
-
// 그리드 생성
|
|
5077
5644
|
new PureSheet(container, {
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
module: 'app:data', // worker entry: installProcessorWorker({ loaders: { 'app:data': MyLoader } })
|
|
5083
|
-
params: { authToken: getToken() },
|
|
5084
|
-
},
|
|
5645
|
+
keyFields: ['orderNo'],
|
|
5646
|
+
rowModel: 'server',
|
|
5647
|
+
loader: { use: OrderLoader, params: { year: 2026 } },
|
|
5648
|
+
serverSide: { blockSize: 100 },
|
|
5085
5649
|
fields: [...],
|
|
5086
5650
|
});
|
|
5651
|
+
```
|
|
5087
5652
|
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5653
|
+
**요청·응답**
|
|
5654
|
+
|
|
5655
|
+
| `FetchRangeRequest` | 뜻 |
|
|
5656
|
+
|---|---|
|
|
5657
|
+
| `startRow`, `endRow` | 요청 구간 `[startRow, endRow)` — 항상 블록 경계 |
|
|
5658
|
+
| `sortModel`, `filterModel` | 현재 정렬·필터 상태(`SortState[]`, `ServerFilterState[]`). **서버가 적용**. 수식 필터(`operator: 'formula'`)에는 `expression` 과 함께 수식 트리 `ast`(JSON)가 실려, 서버가 문자열을 해석하지 않고 트리를 따라 SQL 등으로 바꿀 수 있습니다. JS 서버는 `createRowFilterPredicate(filterModel, { columnTypes })` 로 그리드와 같은 판정 함수를 쓸 수 있습니다(아래) |
|
|
5659
|
+
| `groupColumns`, `groupKeys` | 행 그룹이 있으면 그룹 열(레벨 순)과 이 행들이 속한 그룹의 값 경로(예: `['서울', '강남구']`). 서버는 이 그룹 안의 행만 돌려줍니다. 그룹이 없으면 둘 다 `[]`. 객체 값 그룹 열이면 `groupKeys` 에는 객체 대신 `objectKey` 값이 옵니다(→ [서버 계약](#객체-값-열점-표기-필드의-서버-계약)) |
|
|
5660
|
+
| `parentIds` | 트리(`treeConfig`)면 이 행들의 부모 노드 ID 경로(최상위부터). `[]` = 최상위 노드, `['A', 'A-0']` = A 아래 A-0 의 자식들. 트리가 아니면 `[]` |
|
|
5661
|
+
| `aggregateModel?` | 트리에서 `treeConfig.aggregates` 나 부모가 자손을 포함하는 체크박스 열(`checkboxIncludesDescendants`)이 있을 때만 — 노드별 **자기 + 자손**(서브트리) 집계 요청 (아래 트리 참조) |
|
|
5662
|
+
| `anchor?` | 커서 단서 `{ row, cursor }` — "행 번호 `row` 의 행은 `cursor` 바로 다음". 커서 방식 서버는 `cursor` 다음부터 `startRow - row` 행을 건너뛰면 됩니다(바로 앞 블록이면 0). 오프셋 방식 서버는 무시하고 `startRow` 로 조회 |
|
|
5663
|
+
| `signal` | 취소 신호 (`fetch` 에 그대로 전달) |
|
|
5664
|
+
|
|
5665
|
+
| `FetchRangeResult` | 뜻 |
|
|
5666
|
+
|---|---|
|
|
5667
|
+
| `rows` | 구간의 행. 끝에 닿으면 요청보다 적게 |
|
|
5668
|
+
| `rowCount?` | 전체 행 수(정렬·필터 후). 모르면 생략 → 끝(요청보다 적은 응답)에 닿을 때까지 스크롤 범위를 한 블록씩 늘립니다 |
|
|
5669
|
+
| `nextCursor?` | 이 블록 마지막 행 다음을 가리키는 커서. 다음 요청의 `anchor.cursor` 로 돌아옵니다 |
|
|
5670
|
+
| `aggregates?` | 트리 — 요청의 `aggregateModel` 에 대한 노드별 서브트리(자기 + 자손) 집계 `{ [행 ID]: { 열: { 함수: 값 } } }`. 자식 없는 행은 생략 가능 |
|
|
5671
|
+
|
|
5672
|
+
**그리드가 하는 일**
|
|
5673
|
+
|
|
5674
|
+
- 아직 안 온 행은 칸마다 회색 막대(스켈레톤)로 그립니다(`.ps-row-loading`). 선택·편집·복사 대상이 아닙니다.
|
|
5675
|
+
- 받은 블록은 캐시합니다(`maxBlocksInCache` 초과 시 가장 오래 안 본 블록부터 버림). 다시 스크롤해 온 구간은 다시 요청하지 않습니다. `maxBlocksInCache: Infinity` 면 받은 블록을 버리지 않고(메모리 사용은 받은 만큼 늘어남), `0` 이면 보이는 구간의 블록만 남깁니다(미리 받기 없음, 다시 스크롤하면 다시 요청). 정렬·필터 변경과 `refreshLoaderData()` 때는 조건이 달라지므로 항상 비웁니다.
|
|
5676
|
+
- 스크롤이 멈출 때까지 요청을 미루고(`debounceMs`), 동시 요청 수를 제한하고(`maxConcurrentRequests`), 보이는 구간 다음 한 블록을 미리 받습니다.
|
|
5677
|
+
- 정렬·필터가 바뀌면 받아 둔 블록을 버리고 맨 위부터 다시 요청합니다. 이전 조건의 응답은 버립니다.
|
|
5678
|
+
- `refreshLoaderData()` 는 같은 조건으로 지금 보이는 구간부터 다시 받습니다(행 수·스크롤 위치 유지). `updateLoaderParams()` 값은 다음 요청부터 반영됩니다.
|
|
5679
|
+
- 일부만 다시 받기: `refreshLoaderData({ parentIds: ['A'] })`(트리 — A 의 자식 목록과 그 아래 펼친 목록), `refreshLoaderData({ groupKeys: ['서울'] })`(행 그룹 — 서울 그룹 아래 하위 그룹·행). 그 목록의 `fetchRange`(그룹 목록은 `fetchGroups`)를 다시 부릅니다. 펼침·스크롤은 유지하고, 그 노드·그룹 행 자신(부모 목록에 있음)의 값·행 수·집계는 그대로입니다. client 모드에서는 범위를 무시하고 전체를 다시 불러옵니다.
|
|
5680
|
+
- 특정 행만: `refreshLoaderData({ rowIds: ['A-17', 'B-3'] })` — 평면·그룹·트리 공통. 그 행이 든(받아 둔) **블록**만 `fetchRange` 로 다시 받습니다(예: `A-17` 이 0~99 블록이면 `startRow: 0, endRow: 100`). 같은 블록의 다른 행도 새 값이 되고, 새 블록이 올 때까지 이전 값이 보입니다. 받지 않은 행 ID 는 무시합니다(다음에 받을 때 새 값이 옴). 하단 합계도 다시 묻습니다.
|
|
5681
|
+
- 블록 요청이 실패하면 그 행을 `.ps-row-load-failed` 로 표시하고 `error` 이벤트(`code: 'SERVER_ROWS_FAILED'`)를 냅니다. 실패 행을 누르면 다시 요청합니다.
|
|
5682
|
+
|
|
5683
|
+
**필터 값 목록** — 헤더 필터의 체크박스 목록은 `DataLoader.fetchFilterValues({ columnId, filterModel, keyword, limit })` 로 서버에 묻습니다(`{ values, truncated? }`). 구현하지 않으면 받아 둔 행의 값만 보이고 `truncated: true` 로 표시됩니다.
|
|
5684
|
+
|
|
5685
|
+
**편집** — 셀 수정·행 추가·행 삭제는 저장 전 표시(덧씌움)로 보관합니다. 블록이 캐시에서 빠졌다 다시 와도 유지됩니다.
|
|
5686
|
+
|
|
5687
|
+
| 동작 | 화면 |
|
|
5688
|
+
|---|---|
|
|
5689
|
+
| 셀 수정 | 받은 행에 수정값을 덧씌워 표시(`.ps-row-modified`) |
|
|
5690
|
+
| 행 추가 (`changes.addRowByIndex(row, index)` 등) | 그 위치에 임시 행으로 끼움. 서버 블록 번호는 밀리지 않음. 정렬·필터가 바뀌면 맨 위로 이동 |
|
|
5691
|
+
| 행 삭제 | 행을 지우지 않고 삭제 표시(`.ps-row-deleted`) |
|
|
5692
|
+
| `changes.commit()` | 로컬 원본을 고치지 않음 — 저장 전 표시를 지우고 서버에서 다시 받음 |
|
|
5693
|
+
|
|
5694
|
+
저장 순서: `const changes = grid.changes.getAll()` → 서버에 저장 → `await grid.changes.commit()`.
|
|
5695
|
+
|
|
5696
|
+
**전체 선택** — `selection.selectAllRows()`(헤더 체크박스·Ctrl+A 포함)는 행을 불러오지 않고 "전부 + 예외 목록"으로 표시합니다(1억 행도 즉시). 이후 해제한 행은 예외로 쌓입니다.
|
|
5697
|
+
|
|
5698
|
+
```typescript
|
|
5699
|
+
grid.selection.getRowSelectionState();
|
|
5700
|
+
// 전체 선택 뒤 2행 해제 → { selectAll: true, excludedIds: ['7', '9'] } "이 둘을 뺀 전부"
|
|
5701
|
+
// 보통 선택 → { selectAll: false, selectedIds: ['1', '2'] } "이 둘만"
|
|
5702
|
+
// 타입: RowSelectionState — selectAll 을 확인해야 excludedIds / selectedIds 를 꺼낼 수 있다
|
|
5703
|
+
```
|
|
5704
|
+
|
|
5705
|
+
`getSelectedRowIds()` 는 받아 둔 행만 돌려주므로, 선택 행 삭제·내보내기 같은 일괄 작업은 `getRowSelectionState()` 를 현재 필터와 함께 서버에 보내 처리합니다. 선택한 행 전체의 ID 목록을 서버에서 받아 오는 API 는 두지 않습니다(1억 행 선택이면 ID 도 1억 개).
|
|
5706
|
+
|
|
5707
|
+
**집계 (하단 합계·선택 합계)** — `DataLoader.fetchAggregates(request)` 로 서버에 묻고, 돌려준 값을 그대로 표시합니다(클라이언트 계산 없음). 구현하지 않으면 하단 합계 칸은 비고 선택 합계는 나오지 않습니다(오류 없음).
|
|
5708
|
+
|
|
5709
|
+
```typescript
|
|
5710
|
+
class OrderLoader extends DataLoader {
|
|
5711
|
+
async fetchAggregates({ aggregateModel, filterModel, selection, changes, signal }: FetchAggregatesRequest) {
|
|
5712
|
+
// aggregateModel: [{ columnId: 'amount', aggregate: 'sum' }, ...]
|
|
5713
|
+
// selection 없음 → 필터 결과 전체 / 있음 → 그중 선택한 행만 ({ selectAll, excludedIds } | { selectAll: false, selectedIds })
|
|
5714
|
+
// changes: serverSide.sendChangesToAggregates 가 true 일 때만 — 저장 전 편집을 반영한 것처럼 집계
|
|
5715
|
+
const res = await fetch('/api/orders/aggregates', { method: 'POST', body: JSON.stringify({ aggregateModel, filterModel, selection, changes }), signal });
|
|
5716
|
+
return res.json(); // { amount: { sum: 12345 } } — 열 ID → 함수 → 값
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
```
|
|
5720
|
+
|
|
5721
|
+
| `FetchAggregatesRequest` | 뜻 |
|
|
5722
|
+
|---|---|
|
|
5723
|
+
| `aggregateModel` | `[{ columnId, aggregate }]`. 한 열에 함수가 여러 개 올 수 있음 |
|
|
5724
|
+
| `filterModel` | 현재 필터 (정렬은 합계와 무관해 싣지 않음) |
|
|
5725
|
+
| `selection?` | 있으면 선택 합계 요청. `RowSelectionState` 그대로 |
|
|
5726
|
+
| `changes?` | 저장 전 편집 `{ added: [{ id, row }], modified: [{ id, changes: { 필드: { from, to } } }], deleted: [id] }`. `sendChangesToAggregates: true` 일 때만 |
|
|
5727
|
+
| `signal` | 취소 신호 — 같은 종류의 새 요청이 나가면 이전 요청을 취소 |
|
|
5728
|
+
|
|
5729
|
+
| 쓰임 | 언제 묻나 | 무엇을 묻나 |
|
|
5730
|
+
|---|---|---|
|
|
5731
|
+
| 하단 합계 행(`summaryRow`) | 처음, 필터 변경, `refreshLoaderData()`, 열의 집계 함수 변경. `sendChangesToAggregates: true` 면 편집할 때마다(0.3초 묶음) | 열 정의의 `aggregate`(문자열)가 있는 열 — 그 함수 하나 |
|
|
5732
|
+
| 그룹 행·트리 노드 집계 (`fetchNodeAggregates`) | `sendChangesToAggregates: true` 이고 편집할 때마다(0.3초 묶음) — 편집이 없으면 묻지 않음 | 그룹: 그룹 행 집계(`rowGroupConfig.aggregates`), 트리: `treeConfig.aggregates` |
|
|
5733
|
+
| 선택 합계(상태 표시줄) | 행 선택이 바뀔 때(전체 선택 포함) | 숫자 열과 문자열 `aggregate` 열. 숫자 열은 상태 표시줄 합계용으로 `sum`·`count`·`min`·`max` 도 함께 |
|
|
5734
|
+
|
|
5735
|
+
- 정렬만 바뀌면 합계가 같으므로 다시 묻지 않습니다.
|
|
5736
|
+
- 응답에 없는 열·함수는 빈칸입니다. 요청이 실패하면 `error` 이벤트(`code: 'SERVER_AGGREGATES_FAILED'`)를 내고 합계를 비웁니다. 다음 계기(필터 변경·refresh·편집)에 다시 묻습니다.
|
|
5737
|
+
- 셀 범위(드래그) 선택의 합계는 SSRM 에서 계산하지 않습니다(`selectionAggregates()` 가 null). 행 선택만 서버에 묻습니다.
|
|
5738
|
+
- `sendChangesToAggregates` 가 꺼져 있으면(기본) 저장 전 편집은 저장 후 다시 불러올 때 합계에 반영됩니다.
|
|
5739
|
+
|
|
5740
|
+
**저장 전 편집을 그룹 행·트리 노드 집계에 반영 (`sendChangesToAggregates` + `fetchNodeAggregates`)** — 그룹 행 집계는 `fetchGroups`, 트리 노드 집계는 `fetchRange` 가 처음 줍니다(저장된 값). 이 옵션을 켜고 `DataLoader.fetchNodeAggregates` 를 구현하면, 편집 뒤 그 편집이 바꾸는 **받아 둔** 그룹·노드만 다시 묻습니다.
|
|
5741
|
+
|
|
5742
|
+
```typescript
|
|
5743
|
+
// 트리: 'D0-T1-E4' 급여 수정 → 바뀌는 노드 = 받아 둔 조상 D0-T1, D0 (부모 노드면 자기도)
|
|
5744
|
+
async fetchNodeAggregates({ aggregateModel, filterModel, changes, treeNodes, groupColumns, groupKeys, groupFilterModel, signal }: FetchNodeAggregatesRequest) {
|
|
5745
|
+
// treeNodes: [{ id: 'D0-T1', parentIds: ['D0'] }, { id: 'D0', parentIds: [] }]
|
|
5746
|
+
return { treeNodes: { 'D0-T1': { salary: { sum: 1500300 } }, 'D0': { salary: { sum: 45000300 } } } };
|
|
5747
|
+
// 그룹: groupKeys: [['서울', '강남구'], ['서울']] → { groups: [{ groupKeys: ['서울', '강남구'], aggregates: { amount: { sum: 1200 } } }, …] }
|
|
5748
|
+
}
|
|
5749
|
+
```
|
|
5750
|
+
|
|
5751
|
+
- 대상: 편집된 행(수정·삭제)이 속한 그룹 경로·조상 노드, 새 행이 들어갈 그룹·부모. 범위 수정(그룹·트리 체크박스, `bulkUpdate`)이 있으면 받아 둔 그룹·부모 노드 전부. 나중에 받은 그룹·노드가 대상이면 그때 묻습니다.
|
|
5752
|
+
- 서버는 저장된 값에 `changes` 를 반영한 것처럼 집계합니다(`fetchAggregates` 의 `changes` 와 같은 규칙). 응답에 없는 그룹·노드는 처음 받은 값으로 보입니다.
|
|
5753
|
+
- 편집을 모두 되돌리면 묻지 않고 처음 받은 값으로 돌아갑니다. 저장(`commit`)하면 다시 불러오므로 새 저장 값이 옵니다.
|
|
5754
|
+
- 체크박스 상태(그룹 행·포함 체크박스)는 이 요청과 무관합니다 — 화면이 편집을 직접 더해 정합니다.
|
|
5755
|
+
- `fetchNodeAggregates` 가 없으면 그룹 행·노드 집계는 저장 후 다시 불러올 때 반영됩니다(하단 합계만 실시간).
|
|
5756
|
+
|
|
5757
|
+
**검색** — 툴바 검색은 `DataLoader.fetchSearch({ keyword, columnIds, caseSensitive, sortModel, filterModel, signal })` 로 서버에 묻습니다. 응답 `{ matches: [{ rowIndex, columnId }] }` 의 `rowIndex` 는 현재 정렬·필터 결과의 행 번호입니다. 다음/이전 이동 시 그 위치의 블록을 받아 강조합니다. 구현하지 않으면 받아 둔 행에서만 찾습니다. 트리는 아래 트리 항목을 보세요(`tree: true`, 매치마다 위치).
|
|
5758
|
+
|
|
5759
|
+
**행 그룹** — `rowGroupConfig`(그룹 바·보기 설정 포함)를 쓰면 그리드가 직접 묶지 않고 서버에 묻습니다. 그룹 행은 `fetchGroups`, 그룹 안의 행은 `fetchRange({ groupColumns, groupKeys })` 로 받습니다.
|
|
5760
|
+
|
|
5761
|
+
```typescript
|
|
5762
|
+
class OrderLoader extends DataLoader {
|
|
5763
|
+
async fetchGroups({ groupColumns, groupKeys, sortModel, filterModel, aggregateModel, signal }: FetchGroupsRequest) {
|
|
5764
|
+
// groupColumns: ['region', 'city'], groupKeys: ['서울'] → 서울 아래 city 그룹들 (GROUP BY city WHERE region = '서울')
|
|
5765
|
+
return {
|
|
5766
|
+
groups: [
|
|
5767
|
+
{ value: '강남구', count: 1200, aggregates: { amount: { sum: 5000 } } }, // 집계 모양은 fetchAggregates 와 같음
|
|
5768
|
+
],
|
|
5769
|
+
};
|
|
5770
|
+
}
|
|
5771
|
+
async fetchRange({ startRow, endRow, groupColumns, groupKeys, sortModel, filterModel, signal }: FetchRangeRequest) {
|
|
5772
|
+
// groupKeys: ['서울', '강남구'] → 그 그룹 안의 행만
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
```
|
|
5776
|
+
|
|
5777
|
+
| `ServerGroup` | 뜻 |
|
|
5778
|
+
|---|---|
|
|
5779
|
+
| `value` | 그룹 값. 객체 값 열이면 대표 객체 또는 `objectKey` 값 → [객체 값 열·점 표기 필드의 서버 계약](#객체-값-열점-표기-필드의-서버-계약) |
|
|
5780
|
+
| `count` | 그룹에 속한 데이터 행 수(필터 후). 펼쳤을 때의 스크롤 길이 |
|
|
5781
|
+
| `aggregates?` | 그룹 행 집계 — `rowGroupConfig.aggregates`·그룹 행에 보이는 열 `aggregate`(문자열 함수만)를 `aggregateModel` 로 묻습니다 |
|
|
5782
|
+
| `descendantGroupCounts?` | 아래 레벨마다의 하위 그룹 수(예: 그룹 열 3개의 1레벨 그룹이면 `[2레벨 수, 3레벨 수]`). 주면 하위 그룹을 받기 전에도 펼친 높이가 정확해 스크롤바가 튀지 않습니다. 없으면 `count` 로 어림잡고 하위 그룹이 오면 고칩니다 |
|
|
5783
|
+
|
|
5784
|
+
- **언제 묻나:** 최상위 그룹은 처음에 받습니다. 하위 그룹과 그룹 안의 행은 **펼쳐져 있고 화면에 보이는 그룹의 것만** 받습니다(스크롤하면 그때 그 그룹 것을). 그룹마다 블록 캐시를 따로 둡니다.
|
|
5785
|
+
- **펼침 상태:** 기존 옵션(`defaultCollapsed`, `defaultExpandLevel`, 없으면 전부 펼침)을 따릅니다. 나중에 도착한 하위 그룹에도 같은 규칙을 적용하고, 사용자가 바꾼 접힘은 유지합니다.
|
|
5786
|
+
- **정렬·필터:** 필터가 바뀌면 그룹을 처음부터 다시 받습니다. 그룹 열 정렬은 서버가 그룹 순서에 반영합니다.
|
|
5787
|
+
- **편집:** 셀 수정·삭제 표시는 평면과 같습니다. 새 행은 **그룹 열 값이 같은 그룹 맨 위**에 보이고, 서버에 없는 값이면 임시 그룹이 생깁니다(그리드가 직접 묶을 때와 같은 규칙). 저장 후 다시 불러오면 서버 위치로 갑니다. 그룹 행 집계는 서버 값 그대로입니다(저장 전 편집 미반영).
|
|
5788
|
+
- **그룹 체크박스(행 선택):** 그룹 행의 rowHeader 체크박스는 그 그룹의 행 전부를 선택/해제합니다(받지 않은 행 포함). 선택 상태는 `getRowSelectionState()` 에 그룹 경로별로 겹쳐 적힙니다(`groups`, 위 전체 선택 예 참조). 일부만 선택된 그룹은 부분 선택으로 표시됩니다. 그룹 열이 바뀌면(그룹 추가·변경·해제) 행 선택은 **전체 선택을 포함해 모두 해제**됩니다 — 선택이 그룹 경로 기준으로 저장되므로 일관되게 비웁니다.
|
|
5789
|
+
- **그룹 결과 필터(고급 필터의 "그룹 필터" 탭):** 서버가 거릅니다. `fetchGroups`·`fetchAggregates` 요청에 `groupFilterModel` 이 실립니다.
|
|
5790
|
+
```typescript
|
|
5791
|
+
groupFilterModel: {
|
|
5792
|
+
conditions: [{ type: 'formula', expression: 'GROUP("dept", "aggregate", "total") > 50', ast: { type: 'BinaryOp', operator: '>', … } }],
|
|
5793
|
+
aggregates: { total: { columnId: 'amount', aggregate: 'sum' } }, // by / GROUP(…, "aggregate", id) 가 가리키는 집계
|
|
5794
|
+
}
|
|
5795
|
+
```
|
|
5796
|
+
규칙은 그리드가 직접 거를 때와 같습니다 — 행 필터 후 그룹별 값으로 **맨 아래 그룹 경로마다** 판정하고, 살아남은 행으로 그룹 행 수·집계와 하단 합계를 다시 계산합니다(SQL 의 HAVING). 조건식(`type: 'conditional'`)도 같은 모델로 옵니다. JS 서버는 `createGroupFilterPredicate(model, groupColumns)` 로 같은 판정 함수를 쓸 수 있습니다(객체 값 그룹 열이면 세 번째 인자 `{ objectSpecs, fieldOf }` — [객체 값 열](#객체-값-열-datatype-object) 참조).
|
|
5797
|
+
|
|
5798
|
+
**행 필터 판정 함수 (`createRowFilterPredicate`, JS 서버용)** — 그리드 엔진과 **같은 코드**로 `filterModel` 을 판정합니다(값 비교·마스크·수식 필터·`groupKey` 괄호 결합).
|
|
5799
|
+
|
|
5800
|
+
```typescript
|
|
5801
|
+
import { createRowFilterPredicate } from '@vmscloud/moz-ui-grid-core';
|
|
5802
|
+
|
|
5803
|
+
async fetchRange({ filterModel, startRow, endRow }: FetchRangeRequest) {
|
|
5804
|
+
// columnTypes = 그리드 필드의 dataType — 수식 필터가 문자열 열('001', 'sales 2026')을 숫자·날짜로 오인하지 않게
|
|
5805
|
+
const matches = createRowFilterPredicate(filterModel, { columnTypes: { name: 'string', code: 'string', amount: 'number' } });
|
|
5806
|
+
const rows = ALL_ROWS.filter(matches);
|
|
5807
|
+
return { rows: rows.slice(startRow, endRow), rowCount: rows.length };
|
|
5808
|
+
}
|
|
5809
|
+
```
|
|
5810
|
+
|
|
5811
|
+
- 서버에서 판정할 수 없는 것: `custom` 필터(조건이 JS 함수라 요청에 실리지 않음 — 통과로 봄), 그리드 계산 필드에 건 필터.
|
|
5812
|
+
- 빈 셀(null)은 문자열 연산자에서 `''` 로 봅니다(`contains ''` 통과, `notContains ''` 탈락) — `isNull`·`in`·`like` 와 같은 규칙. SQL 로 옮길 때는 `COALESCE(col, '') LIKE …` 로 맞춥니다.
|
|
5813
|
+
- **그룹 배너 선택(`selectable`):** 선택 결과에 `groupKeys`·`rowCount` 가 실리고 `rowIds` 는 없습니다.
|
|
5814
|
+
- **그룹 화면에서 미지원:** 함수형 집계.
|
|
5815
|
+
|
|
5816
|
+
**범위 수정 (받지 않은 행까지 일괄 변경)** — 그룹 행의 boolean 열 체크박스, `changes.bulkUpdate(column, value, 'all' | 'selected' | 행 ID 목록)` 는 행을 하나씩 바꾸지 않고 **저장 전 범위 수정**으로 남깁니다. 모양은 두 가지입니다(`rowIds` 가 있으면 행 ID 목록 모양 — `RangeUpdate = ScopeRangeUpdate | RowIdsRangeUpdate`).
|
|
5817
|
+
|
|
5818
|
+
| 모양 | 만드는 것 | 서버 저장 |
|
|
5819
|
+
|---|---|---|
|
|
5820
|
+
| 선택 범위 (`scope`) | 그룹·트리 체크박스, `bulkUpdate(…, 'all' \| 'selected')` | `UPDATE … WHERE filterModel AND groupFilterModel AND scope` |
|
|
5821
|
+
| 행 ID 목록 (`rowIds`) | `bulkUpdate(…, ['1', '3'])` | `UPDATE … WHERE id IN (rowIds)` — 필터·그룹과 무관 |
|
|
5822
|
+
|
|
5823
|
+
```typescript
|
|
5824
|
+
grid.changes.getAll().rangeUpdates
|
|
5825
|
+
// 영업팀 그룹의 [확정] 체크 → 그 안의 17번만 다시 끔
|
|
5826
|
+
// [{ field: 'confirmed', value: true,
|
|
5827
|
+
// groupColumns: ['dept'], filterModel: [...], // 범위를 만든 순간의 맥락
|
|
5828
|
+
// scope: { selectAll: false, selectedIds: [], // RowSelectionState 와 같은 모양
|
|
5829
|
+
// groups: { '영업팀': { selectAll: true, excludedIds: ['17'] } } } }]
|
|
5830
|
+
// modified: 17번(confirmed: false) — 17번의 원래 값이 이미 false 였다면 modified 에도 없음
|
|
5831
|
+
|
|
5832
|
+
grid.changes.bulkUpdate('confirmed', true, ['3', '17', '9001']) // → 3 (중복 제외 대상 수)
|
|
5833
|
+
// [{ field: 'confirmed', value: true, rowIds: ['3', '17', '9001'] }] // 받지 않은 9001 도 포함
|
|
5834
|
+
```
|
|
5835
|
+
|
|
5836
|
+
행 ID 목록 규칙: ID 는 문자열로 비교합니다(숫자 ID 행에 `'3'` 을 줘도 같은 행). 저장 전 새 행의 ID 는 범위가 아니라 그 행의 셀 편집(`added` 의 값)으로, 저장 전 삭제한 행의 ID 는 뺍니다. 받아 둔 행은 바로 그 값으로 보이고, 그룹·트리 체크박스는 받아 둔 행만 셀 편집처럼 셉니다(받지 않은 행은 어느 그룹에 있는지 몰라 저장 후 서버 값으로 맞춰짐).
|
|
5837
|
+
|
|
5838
|
+
| 규칙 | 내용 |
|
|
5839
|
+
|---|---|
|
|
5840
|
+
| 화면 | 범위 안의 행은 받아 둔 것이든 나중에 받는 것이든 그 값으로 보입니다. 그룹 체크박스 상태는 범위·셀 편집·서버 참 개수로 정합니다 |
|
|
5841
|
+
| 그룹 체크박스 상태 | 범위가 없으면 서버에 참 개수를 묻습니다 — 그룹 행에 체크박스가 보이는 boolean 열(고정 열, center 양 끝 `unmergeOnGroupRow`)이면 `fetchGroups` 의 `aggregateModel` 에 `{ columnId, aggregate: 'sum' }`(true = 1)이 자동으로 실립니다 |
|
|
5842
|
+
| 겹침 | 같은 열·같은 맥락의 범위끼리는 겹치지 않게 정리합니다(새 범위가 겹친 부분을 가져감). 범위 안의 행을 셀 편집하면 그 행은 `scope` 에서 빠지고 `modified` 로 갑니다. 같은 그룹을 다시 체크하면 그 안의 셀 편집은 지워집니다 |
|
|
5843
|
+
| 맥락 | `groupColumns`·`filterModel`(+ `groupFilterModel`)은 범위를 만든 순간의 것입니다. 저장 전에 그룹·필터를 바꿔도 같은 행을 가리킵니다 |
|
|
5844
|
+
| 저장 | 서버는 `UPDATE … SET field = value WHERE filterModel AND groupFilterModel AND scope` 후 `modified` 를 적용합니다. 순서와 무관합니다(맥락이 다른 범위끼리는 목록 순서대로). JS 서버는 `isRowInSelectionState(scope, groupKeys, rowId)` 로 행이 범위에 드는지 판정할 수 있습니다 |
|
|
5845
|
+
| 실행 취소 | 한 번의 동작으로 되돌립니다. `commit()`·`discardAll()` 이 범위를 비웁니다. `hasChanges` 에 포함됩니다 |
|
|
5846
|
+
| 합계 | `sendChangesToAggregates: true` 면 `fetchAggregates` 의 `changes.rangeUpdates` 로도 갑니다 |
|
|
5847
|
+
|
|
5848
|
+
범위로 값이 실제로 바뀐 행에도 수정 표시(`.ps-row-modified`)가 붙습니다(원래부터 그 값이던 행은 붙지 않음). 화면에서 범위가 행을 덮는지는 범위를 만든 순간의 필터로도 판단합니다 — 그 뒤 필터를 바꿔 새로 보이게 된 행은 범위 값으로 바뀌지 않습니다(저장 때 서버가 적용하는 대상과 같음).
|
|
5849
|
+
- `fetchGroups` 가 없으면 `error` 이벤트(`code: 'SERVER_GROUPS_FAILED'`)를 내고 그룹이 비어 보입니다.
|
|
5850
|
+
|
|
5851
|
+
**트리** — `treeConfig` 를 쓰면 노드의 자식 목록을 `fetchRange({ parentIds })` 로 받습니다. 펼쳐져 있고 화면에 보이는 노드의 자식만 요청합니다. 한 노드의 자식이 수백만이어도 보이는 블록만 받습니다.
|
|
5852
|
+
|
|
5853
|
+
```typescript
|
|
5854
|
+
class DeptLoader extends DataLoader {
|
|
5855
|
+
async fetchRange({ parentIds, startRow, endRow, sortModel, filterModel, aggregateModel, signal }: FetchRangeRequest) {
|
|
5856
|
+
// parentIds: [] → 최상위, ['본사', '개발본부'] → 개발본부의 자식들 (마지막 값이 직계 부모)
|
|
5857
|
+
return {
|
|
5858
|
+
rows: [{ id: '플랫폼팀', name: '플랫폼팀', childCount: 12 }], // childCount: 자식이 있는지·몇 개인지
|
|
5859
|
+
rowCount: 40, // 이 부모의 자식 수(모르면 생략)
|
|
5860
|
+
aggregates: { '플랫폼팀': { confirmed: { sum: 3, countAll: 57 } } }, // aggregateModel 이 왔을 때만
|
|
5861
|
+
};
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
new PureSheet(container, {
|
|
5865
|
+
keyFields: ['id'], rowModel: 'server', loader: { use: DeptLoader },
|
|
5866
|
+
treeConfig: { treeColumn: 'name', childCountField: 'childCount', defaultExpandLevel: 1 },
|
|
5867
|
+
fields: [...],
|
|
5868
|
+
});
|
|
5091
5869
|
```
|
|
5092
5870
|
|
|
5093
|
-
|
|
5871
|
+
| 항목 | 동작 |
|
|
5872
|
+
|---|---|
|
|
5873
|
+
| 자식 여부 (`treeConfig.childCountField`, 기본 `'childCount'`) | 숫자 = 직계 자식 수(받기 전에도 펼친 높이가 정확), `true` = 자식은 있지만 수 모름(자리 행 1개로 시작해 받을수록 늘어남), 0·`false`·없음 = 잎 |
|
|
5874
|
+
| 펼침 | `defaultCollapsed`·`defaultExpandLevel` 을 나중에 받은 노드에도 적용합니다. 사용자가 바꾼 접힘은 유지합니다. `expandAll`·`collapseAll`·`expandToLevel`·`collapseToLevel`·`expandAtLevel`·`collapseAtLevel` 은 받아 둔 노드를 바로 바꾸고, **나중에 받는 노드에도 같은 규칙**을 적용합니다(요청은 여전히 보이는 것만) |
|
|
5875
|
+
| 정렬 | 레벨마다 같은 `sortModel` 이 실립니다 — 서버가 형제 안에서 정렬합니다 |
|
|
5876
|
+
| 필터 | 서버가 거릅니다. 규칙은 클라이언트 트리와 같게: **조건에 맞는 행과 그 조상**만 남깁니다(조건에 맞는 부모의 자식이라도 맞지 않으면 빠짐). `childCount` 도 필터 후 기준으로 돌려주세요 |
|
|
5877
|
+
| 행 추가 | 추가하는 행의 `treeConfig.parentField`(기본 `'parentId'`) 값이 부모를 정합니다. `addRow(row)` = 그 부모의 자식 맨 위, `addRow(row, index)` = 그 부모의 자식 목록 안 `index` 번째. 부모 값이 없으면 최상위. 잎이던 부모에는 펼침 아이콘이 생깁니다 |
|
|
5878
|
+
| 셀 수정·삭제·저장 | 평면 SSRM 과 같습니다 |
|
|
5879
|
+
| 노드 이동 | `tree.moveNode`·`setParent`·드래그 재정렬은 지원하지 않습니다(받지 않은 형제 사이 위치를 정할 수 없음). 서버에서 옮긴 뒤 `refreshLoaderData({ parentIds })` |
|
|
5880
|
+
| `getChildren`·`getDescendants`·`getParent`·`getAncestors` | **받아 둔 노드만** 봅니다. 받은 노드의 조상은 받지 않았어도 정확합니다(부모 경로를 앎) |
|
|
5881
|
+
|
|
5882
|
+
**부모가 자손을 포함하는 체크박스 (`checkboxIncludesDescendants`)** — 부모 칸은 자손 상태(전부/일부/없음)로 보이고, 부모를 누르면 부모와 **받지 않은 자손까지** 같은 값이 됩니다. 행을 하나씩 바꾸지 않고 트리 범위 수정으로 남깁니다(실행 취소 한 번).
|
|
5883
|
+
|
|
5884
|
+
```typescript
|
|
5885
|
+
grid.changes.getAll().rangeUpdates
|
|
5886
|
+
// A 아래 A-0 을 체크 → A-0 과 그 자손 전부 (필터와 무관)
|
|
5887
|
+
// [{ field: 'confirmed', value: true, tree: true, groupColumns: [], filterModel: [],
|
|
5888
|
+
// scope: { selectAll: false, selectedIds: [],
|
|
5889
|
+
// groups: { 'A': { selectAll: false, selectedIds: ['A-0'], // 노드 자신 = 부모 쪽 selectedIds
|
|
5890
|
+
// groups: { 'A-0': { selectAll: true, excludedIds: [] } } } } } }] // 자손 전부 = groups[노드 ID]
|
|
5891
|
+
```
|
|
5892
|
+
|
|
5893
|
+
- 부모 칸의 자손 상태는 서버에 묻습니다. 이 열이 있으면 `fetchRange` 에 `aggregateModel: [{ columnId, aggregate: 'sum' }, { columnId, aggregate: 'countAll' }]` 이 실리고, 서버는 노드마다 **서브트리(자기 + 자손)** 의 참 개수·노드 수를 `aggregates` 로 돌려줍니다(아래 노드 집계와 같은 규칙 — 그리드가 자기 값을 빼서 자손 상태를 정함). 주지 않으면 받지 않은 자손이 있는 부모 칸은 부모 자기 값으로 보입니다. 화면은 여기에 범위 수정과 받아 둔 자손의 셀 편집을 더해 정합니다.
|
|
5894
|
+
- 저장: 서버는 `scope` 가 가리키는 노드(재귀 CTE 등)를 `UPDATE` 한 뒤 `modified` 를 적용합니다. JS 서버는 `isRowInSelectionState(scope, parentIds, rowId)` 로 판정할 수 있습니다(`groupKeys` 자리에 부모 ID 경로).
|
|
5895
|
+
- 클라이언트 트리에서도 같은 옵션이 부모·자손을 함께 바꿉니다(`checkboxIncludesDescendants` — 예전 이름 `checkboxTriState`).
|
|
5896
|
+
|
|
5897
|
+
**행 선택 + `rowSelection.selectChildren`** — 부모를 고르면 받지 않은 자손까지 선택됩니다. 선택 상태는 그룹과 같은 모양이고 `groups` 의 키가 노드 ID 입니다.
|
|
5898
|
+
|
|
5899
|
+
```typescript
|
|
5900
|
+
grid.selection.getRowSelectionState()
|
|
5901
|
+
// A 를 고른 뒤 A-1 만 해제 → { selectAll: false, selectedIds: ['A'], groups: { 'A': { selectAll: true, excludedIds: ['A-1'] } } }
|
|
5902
|
+
```
|
|
5903
|
+
|
|
5904
|
+
**검색** — `fetchSearch` 에 `tree: true` 가 실립니다. 트리 전체(접힌 노드 아래 포함)에서 찾고 매치마다 위치를 돌려주세요. 그리드가 조상을 받아 펼친 뒤 그 행으로 이동합니다.
|
|
5905
|
+
|
|
5906
|
+
```typescript
|
|
5907
|
+
async fetchSearch({ keyword, columnIds, sortModel, filterModel, tree, signal }: FetchSearchRequest) {
|
|
5908
|
+
// 본사(최상위 0번) > 개발본부(본사의 자식 3번) > 그 자식 목록 7번 행
|
|
5909
|
+
return { matches: [{ parentIds: ['본사', '개발본부'], parentRowIndexes: [0, 3], rowIndex: 7, columnId: 'name' }] };
|
|
5910
|
+
}
|
|
5911
|
+
```
|
|
5912
|
+
|
|
5913
|
+
**경로 펼치기 (`tree.expandPath(id)`)** — 받아 둔 노드는 아는 경로로 펼칩니다. 받지 않은 노드는 `DataLoader.fetchPath({ id, sortModel, filterModel, signal })` 로 위치(`TreeNodeLocation` — 검색 매치와 같은 `{ parentIds, parentRowIndexes, rowIndex }`)를 물어 조상을 받은 뒤 펼칩니다. `fetchPath` 가 없으면 받은 노드만 펼칩니다.
|
|
5914
|
+
|
|
5915
|
+
**노드 집계 (`treeConfig.aggregates`)** — 클라이언트 트리처럼 화면에 그리지 않고 `grid.tree.getNodeMeta(id).aggregates` 로 돌려줍니다(키 `${함수}_${열}`, 자기 + 자손 서브트리 기준).
|
|
5916
|
+
|
|
5917
|
+
```typescript
|
|
5918
|
+
treeConfig: { treeColumn: 'name', aggregates: { salary: 'sum', id: 'count' } }
|
|
5919
|
+
// fetchRange 요청: aggregateModel: [{ columnId: 'salary', aggregate: 'sum' }, { columnId: 'id', aggregate: 'count' }]
|
|
5920
|
+
// fetchRange 응답: aggregates: { 'D0-T1': { salary: { sum: 1500000 }, id: { count: 501 } } } ← 자식 있는 노드만
|
|
5921
|
+
grid.tree.getNodeMeta('D0-T1') // { level: 1, hasChildren: true, descendantCount: 500, aggregates: { sum_salary: 1500000, count_id: 501 } }
|
|
5922
|
+
```
|
|
5923
|
+
|
|
5924
|
+
| 항목 | 동작 |
|
|
5925
|
+
|---|---|
|
|
5926
|
+
| 자식 있는 노드 | 서버 값. 노드와 같은 응답으로 오므로 노드가 보이면 값도 있습니다 |
|
|
5927
|
+
| 잎 | 서버가 주지 않아도 그리드가 자기 값으로 채웁니다(합 = 자기 값, 개수 = 1, 분산 = 0). 저장 전 편집 반영 |
|
|
5928
|
+
| 저장 전 편집 | 부모 노드는 `sendChangesToAggregates` + `fetchNodeAggregates` 일 때 반영(위 합계 항목). 아니면 저장 후 다시 불러올 때 |
|
|
5929
|
+
| 필터 | 필터 후 남은 노드 기준(서버 책임 — 클라이언트 트리와 같음) |
|
|
5930
|
+
| 함수형 집계 | 무시(클라이언트 트리와 같음) |
|
|
5931
|
+
| 받지 않은 노드 | `getNodeMeta` 가 null |
|
|
5932
|
+
| `descendantCount` | 서버가 `countAll` 집계를 주면 그 값 - 1, 아니면 직계 자식 수(`childCount`, 모르면 0) |
|
|
5933
|
+
|
|
5934
|
+
**트리에서 미지원:** 노드 이동, 트리 export 의 `treeRowMode: 'include'`(받지 않은 노드).
|
|
5935
|
+
|
|
5936
|
+
**현재 범위** — 피벗은 아직 지원하지 않습니다(호출 시 오류 또는 빈 결과). `getAllData()` 등은 받아 둔 행(+ 저장 전 새 행)만 돌려줍니다. Worker 모드(`loader.worker`) SSRM 은 지원하지 않습니다(데이터가 서버에 있어 이점이 없음).
|
|
5937
|
+
|
|
5938
|
+
> 깊은 위치의 조회 속도는 서버·DB 몫입니다. 그리드는 요청을 줄이고 `anchor` 단서를 주며, 서버는 커서(keyset)·조건별 행 번호표 등으로 깊이와 무관하게 찾을 수 있습니다.
|
|
5939
|
+
|
|
5940
|
+
#### 객체 값 열·점 표기 필드의 서버 계약
|
|
5941
|
+
|
|
5942
|
+
[객체 값 열](#객체-값-열-datatype-object)(`dataType: 'object'`)을 그룹 열·필터 열로 쓰면, 그리드는 서버에 **객체 대신 `objectKey` 값(키)** 을 보냅니다. 서버는 그 열의 `objectKey` 와 **같은 규칙**으로 묶고 거릅니다.
|
|
5943
|
+
|
|
5944
|
+
```typescript
|
|
5945
|
+
// 열: { id: 'owner', dataType: 'object', objectKey: 'id', mask: { type: 'function', formatter: v => v?.name ?? '' } }
|
|
5946
|
+
// 행: { id: 'r1', owner: { id: 'u2', name: '김철수' }, dept: { code: 7 } }
|
|
5947
|
+
|
|
5948
|
+
async fetchGroups({ groupColumns, groupKeys }: FetchGroupsRequest) {
|
|
5949
|
+
// groupColumns: ['owner', 'dept'], groupKeys: [] → GROUP BY owner.id
|
|
5950
|
+
// groupKeys: ['u2'] → WHERE owner.id = 'u2' GROUP BY dept.code
|
|
5951
|
+
return { groups: [
|
|
5952
|
+
{ value: { id: 'u2', name: '김철수' }, count: 2 }, // 대표 객체 → 그룹 행 라벨 '김철수' (열 mask)
|
|
5953
|
+
{ value: 'u3', count: 1 }, // 키만 줘도 됨 → 라벨은 'u3'
|
|
5954
|
+
{ value: null, count: 1 },
|
|
5955
|
+
] };
|
|
5956
|
+
}
|
|
5957
|
+
async fetchRange({ groupKeys }: FetchRangeRequest) {
|
|
5958
|
+
// groupKeys: ['u2', 7] — 숫자 키는 숫자 그대로
|
|
5959
|
+
}
|
|
5960
|
+
```
|
|
5961
|
+
|
|
5962
|
+
| 요청·응답 자리 | 객체 값 열에서 오가는 값 |
|
|
5963
|
+
|---|---|
|
|
5964
|
+
| `fetchGroups`·`fetchRange` 의 `groupKeys`, `fetchNodeAggregates` 의 `groupKeys`(경로 배열) | `objectKey` 값 (`['u2']`, `['u2', 7]`, `null` 그룹은 `[null]`) — 객체는 오지 않음 |
|
|
5965
|
+
| `ServerGroup.value` (응답) | 대표 객체 **또는** 키. 어느 쪽이든 묶기·펼치기·선택·부분 새로고침이 같게 동작합니다. 객체를 주면 그룹 행 라벨이 열 `mask` 로 보이고, 키만 주면 키가 보입니다 |
|
|
5966
|
+
| `filterModel` 의 `in`/`notIn` `filterValue` | 키 배열. 사용자가 객체로 걸어도 `[{ id: 'u2', … }, null, 'u3']` → `['u2', null, 'u3']` 로 바꿔 보냅니다(그룹 목록·행 블록 요청 모두). 객체가 없는 필터는 그대로 |
|
|
5967
|
+
| `filterModel` 의 `eq`/`neq` `filterValue` | 객체로 걸면 키(`{ id: 'u2', … }` → `'u2'`, 숫자 키는 숫자). 문자열·`null` 은 그대로 — 문자열에 `mask` 가 실려 있으면 표시값 비교, 아니고 열에 `displayKey` 가 있으면 그 경로 값과 대소문자 무시 비교, 둘 다 없으면 키 비교로 판정하세요(`createRowFilterPredicate` 가 이 규칙) |
|
|
5968
|
+
| `filterModel` 의 글자 조건(`contains` 등)·크기 비교(`gt`·`between` 등) | 글자 조건은 `displayKey` 경로 값(없으면 객체 JSON 글자), 크기 비교는 정렬 기준 값(`sortValue` → `displayKey` → `objectKey`, 배열이면 첫 값) |
|
|
5969
|
+
| `sortModel` 의 객체 열 | 정렬 기준 값 `sortValue` → `displayKey` → `objectKey` 경로 순서 (예: `displayKey: 'name'`, `sortValue` 없음 → `ORDER BY owner.name`). 경로는 요청에 실리지 않음 — 열 정의와 같은 경로를 서버가 씀 |
|
|
5970
|
+
| `RowSelectionState.groups` 의 키 | `objectKey` 값의 문자열(`null` 그룹은 `''`) |
|
|
5971
|
+
| `fetchFilterValues` 응답 `values` | 객체를 그대로 줘도 됩니다 — 목록 라벨은 열 `mask`, 중복은 그리드가 `objectKey` 로 한 번 더 걸러 냅니다. 요청 `filterModel` 의 `in`/`notIn` 값도 키 |
|
|
5972
|
+
|
|
5973
|
+
- 저장 전 새 행의 그룹 값이 같은 id 의 다른 객체여도 기존 그룹에 들어갑니다(임시 그룹을 만들지 않음).
|
|
5974
|
+
- 받아 둔 행으로 만드는 값 목록(`fetchFilterValues` 미구현)도 `objectKey` 로 중복을 없앱니다. `rawValues` 는 원본 객체입니다.
|
|
5975
|
+
|
|
5976
|
+
**점 표기 `field` 열** — 요청의 열 이름(`groupColumns`, `sortModel`·`filterModel` 의 `id`)은 지금처럼 **열 id** 입니다. 그 열이 `field: 'owner.team'` 이면 서버가 그 경로(`owner.team`)를 스스로 풀어 정렬·필터·묶기에 씁니다.
|
|
5977
|
+
|
|
5978
|
+
- 범위 수정(`changes.getAll().rangeUpdates`)의 `field` 에는 열의 `field` 가 **점 표기 그대로** 실립니다: `bulkUpdate('ownerTeam', 'Z', ['5'])` → `{ field: 'owner.team', value: 'Z', rowIds: ['5'] }`. 서버는 경로로 `UPDATE` 하세요. 받아 둔 행 화면은 최상위 객체를 복사해 교체한 값으로 보입니다.
|
|
5979
|
+
- 셀 편집(`modified`)은 클라이언트와 같이 **최상위 키**(`owner`)와 새 객체로 남습니다.
|
|
5980
|
+
- JS 서버는 그리드와 같은 판정을 `createRowFilterPredicate(filterModel, { fieldOf, objectSpecs })` 로 쓸 수 있습니다 — `fieldOf` 는 열 id → 데이터 키(`'ownerTeam'` → `'owner.team'`), `objectSpecs` 는 `buildObjectFieldSpecs(열 정의)` 결과(`in`/`notIn`·`eq`/`neq` 를 그리드와 같은 키 규칙으로 비교). 그룹 키는 `objectKeyString(값, specs[field])` 로 같은 규칙을 씁니다(→ [객체 값·필드 경로 헬퍼](#객체-값필드-경로-헬퍼)).
|
|
5094
5981
|
|
|
5095
5982
|
---
|
|
5096
5983
|
|
|
@@ -5144,7 +6031,7 @@ const grid2 = new PureSheet(el2, { undoStack: sharedUndo, fields, data, enableRe
|
|
|
5144
6031
|
// 두 그리드에서 일어난 변경이 동일 시간순으로 스택에 쌓임
|
|
5145
6032
|
```
|
|
5146
6033
|
|
|
5147
|
-
> 외부 주입 시 그리드의 개별 Ctrl+Z/Y는 비활성됨 — 공유 스택의 `attachKeyboard`로 일원화 권장.
|
|
6034
|
+
> 외부 주입 시 그리드의 개별 Ctrl+Z/Y는 비활성됨 — 공유 스택의 `attachKeyboard`로 일원화 권장.
|
|
5148
6035
|
|
|
5149
6036
|
---
|
|
5150
6037
|
|
|
@@ -5165,7 +6052,7 @@ class Row {
|
|
|
5165
6052
|
variant?: 'subtotal' | 'grandtotal'; // @default 'subtotal'
|
|
5166
6053
|
height?: number;
|
|
5167
6054
|
className?: string;
|
|
5168
|
-
labelColumn?: string; // 라벨 표시
|
|
6055
|
+
labelColumn?: string; // 라벨 표시 열 — 열 id 우선, 그런 열이 없으면 데이터 키(라벨은 data[그 열 field] 에 저장)
|
|
5169
6056
|
label?: string; // @default '소계' | '총합계'
|
|
5170
6057
|
}
|
|
5171
6058
|
): Row;
|
|
@@ -5201,6 +6088,10 @@ grid.rows.addPinnedBottom(totalRow);
|
|
|
5201
6088
|
// 또는 `grid.rows.addGrandTotal(aggregates)` 단축 API 사용 시 위 코드가 내부에서 호출됨
|
|
5202
6089
|
```
|
|
5203
6090
|
|
|
6091
|
+
> 합계·소계 행의 칸: 집계가 있는 열은 집계값, 없는 열은 행 `data[열 field]`(라벨 등)를 그 열 서식으로 보여 줍니다 — CSV·XLSX 내보내기와 같은 규칙. 예) 위 예시 → `name` 칸 `총합계`, `salary` 칸 합계. `{ variant: 'subtotal', data: { name: '소계' }, aggregates }` 도 같습니다.
|
|
6092
|
+
>
|
|
6093
|
+
> `labelColumn` 해석: 열 id 우선, 같은 id 의 열이 없으면 데이터 키. 예) 열 `{ id: 'nm', field: 'name' }` — `labelColumn: 'nm'`(열 id)·`'name'`(데이터 키) 둘 다 `nm` 칸에 라벨(화면·CSV·XLSX·고정 병합 글자 같음). 열 id 가 다른 열의 데이터 키와 겹치면 열 id 가 이깁니다. 해석은 행이 고정 영역에 꽂힌 뒤 현재 열 정의로 합니다.
|
|
6094
|
+
|
|
5204
6095
|
---
|
|
5205
6096
|
|
|
5206
6097
|
### 에러 코드 전체 레퍼런스
|
|
@@ -5237,6 +6128,10 @@ import { PS, psError, psWarn, ErrorMessages } from '@vmscloud/moz-ui-grid-core';
|
|
|
5237
6128
|
| `PS309` | `PS.PIVOT_CONFIG_NULL` | pivotConfig 없음 |
|
|
5238
6129
|
| `PS310` | `PS.SINGLE_EXPAND_ALL` | singleExpand에서 expandAll 호출 |
|
|
5239
6130
|
| `PS311` | `PS.INVALID_LEVEL` | level < 0 |
|
|
6131
|
+
| `PS312` | `PS.AGGREGATE_NOT_DECLARED` | 함수형 집계가 `ctx.<func>('<column>')` 로 읽은 집계가 선언돼 있지 않음 |
|
|
6132
|
+
| `PS313` | `PS.FORMULA_NAME_AMBIGUOUS` | 계산 필드 수식의 이름이 열 id 이면서 다른 데이터 키이기도 함 — 열 id 쪽을 읽음(경고) |
|
|
6133
|
+
| `PS314` | `PS.OBJECT_KEY_MISSING` | 객체 값 열(`dataType: 'object'`)에 `objectKey` 가 없어 객체 내용(JSON)을 키로 씀 — 열마다 1회 경고 (→ [객체 값 열](#객체-값-열-datatype-object)) |
|
|
6134
|
+
| `PS315` | `PS.OBJECT_VALUE_UNDECLARED` | `dataType: 'object'` 가 아닌 열에 객체 값이 있음 — `dataType: 'object'` 와 `objectKey` 를 지정하라는 경고, 열마다 1회 |
|
|
5240
6135
|
| **Worker (PS501~)** | | |
|
|
5241
6136
|
| `PS501` | `PS.WORKER_SYNC_API` | Worker에서 getAllDataSync 호출 |
|
|
5242
6137
|
| `PS502` | `PS.WORKER_DATA_DIRECT` | Worker 모드에서 `data` 직접 전달 불가 — `loader`를 사용할 것 |
|
|
@@ -5265,7 +6160,7 @@ if (!rowId) {
|
|
|
5265
6160
|
| Export | 위치 | 용도 |
|
|
5266
6161
|
|---|---|---|
|
|
5267
6162
|
| `createReactiveRow(deps, rowId)` | `@vmscloud/moz-ui-grid-core` | `ReactiveRow` 직접 생성 (래퍼/테스트용). 보통 `grid.rows.observeById(id)` 사용 권장 |
|
|
5268
|
-
| `
|
|
6163
|
+
| `setupGridWorker(options)` | `@vmscloud/moz-ui-grid-core/worker` | 앱 Worker 파일을 그리드 Worker로 설정. `{ loader, functions?, xlsxWorkerFactory? }` (→ [runtime-contracts](runtime-contracts.md)) |
|
|
5269
6164
|
| `fieldToColumn(fieldDef)` | `@vmscloud/moz-ui-grid-core` | `FieldDef` → 내부 `ColumnDef` 변환. 외부 컬럼 메타 생성용 |
|
|
5270
6165
|
| `configToInternalOptions(config)` | `@vmscloud/moz-ui-grid-core` | 외부 `PureSheetConfig` → 내부 옵션 객체 변환 |
|
|
5271
6166
|
| `getGridMode(config)` | `@vmscloud/moz-ui-grid-core` | `'flat' \| 'pivot'` 판별 |
|