@vmscloud/moz-ui-grid-vue 0.2.7 → 0.2.9
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 +2 -0
- package/dist/compat/MozGridVue.vue.d.ts +16 -2
- package/dist/compat/MozPivotVue.vue.d.ts +16 -2
- package/dist/composables/useGridPreset.d.ts +17 -3
- package/dist/index.cjs +2 -2
- package/dist/index.js +145 -62
- package/dist/style.css +1 -1
- package/dist/templates/GroupTemplate.d.ts +8 -6
- package/docs/manual/examples.md +189 -9
- package/docs/manual/reference.md +475 -54
- package/docs/manual/reference_vue.md +316 -43
- package/package.json +4 -3
package/docs/manual/reference.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# moz-ui-grid-core 레퍼런스 (Config · 타입 · API · 이벤트)
|
|
2
2
|
|
|
3
3
|
> 패키지: `@vmscloud/moz-ui-grid-core` (export 클래스명은 호환을 위해 `PureSheet` 유지)
|
|
4
|
-
> 버전: 0.1.
|
|
5
|
-
> 마지막 업데이트: 2026-
|
|
4
|
+
> 버전: 0.1.48 (소스 기준: `0481bd5`)
|
|
5
|
+
> 마지막 업데이트: 2026-10-19
|
|
6
6
|
|
|
7
7
|
> 📋 **사용 예제 코드는 별도 파일**: [examples.md](examples.md) — Sub-API별 사용, Reactive React 통합, 이벤트, 선택 모드, Excel I/O 등 약 30개 시나리오
|
|
8
8
|
|
|
9
9
|
## 날짜 시간대 계약 (`timeZone`)
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
const grid = new PureSheet(container, { fields, data, timeZone: 'Asia/Seoul' });
|
|
12
|
+
const grid = new PureSheet(container, { fields, data, enableReferCache: true, timeZone: 'Asia/Seoul' });
|
|
13
13
|
await grid.io.saveExcelToFile({ suggestedName: 'seoul.xlsx' });
|
|
14
14
|
await grid.io.fromExcel(legacyFile, { timeZone: 'UTC' });
|
|
15
15
|
```
|
|
@@ -120,6 +120,7 @@ import '@vmscloud/moz-ui-grid-wrapper/style.css';
|
|
|
120
120
|
|
|
121
121
|
const grid = new PureSheet(container, {
|
|
122
122
|
mode: 'flat',
|
|
123
|
+
enableReferCache: true, // keyFields 생략 → 내부 자동 행 ID (data.id를 키로 추론하지 않음)
|
|
123
124
|
fields: [
|
|
124
125
|
{ id: 'name', header: '이름', dataType: 'string', width: 200 },
|
|
125
126
|
{ id: 'age', header: '나이', dataType: 'number', width: 100 },
|
|
@@ -196,8 +197,8 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
196
197
|
| `data` | `Row[]` | — | 데이터 배열 |
|
|
197
198
|
| `loader` | `LoaderConfig` | — | Worker 기반 데이터 로더 설정 |
|
|
198
199
|
| `fields` | `FieldDef[]` | 필수 | 필드(컬럼) 정의 |
|
|
199
|
-
| `keyFields` | `string[]` |
|
|
200
|
-
| `enableReferCache` | `boolean` | `false` | 행 객체 참조 캐시 활성화 — 행 객체 기반 API
|
|
200
|
+
| `keyFields` | `string[]` | — | 업무 키 필드 목록. 복합 키도 가능(예: `['exchange', 'symbol']`). 생략 시 `enableReferCache: true`가 필요하며 내부 자동 행 ID를 사용한다. 빈 배열은 허용하지 않는다. 데이터의 `id` 필드를 암묵적으로 업무 키로 사용하지 않는다. |
|
|
201
|
+
| `enableReferCache` | `boolean` | `false` | 행 객체 참조 캐시 활성화 — 행 객체 기반 API 및 `keyFields` 생략 시 필수. 키와 캐시 설정을 모두 생략하면 초기화 오류가 발생한다. 자동 ID는 원본 데이터 필드에 주입하지 않으며 전체 데이터 교체 시 새로 발급된다. |
|
|
201
202
|
| **엔진** | | | |
|
|
202
203
|
| `engine` | `'native'` | `'native'` | 데이터 처리 엔진. **`'aq'`(Arquero)는 제거됨** — 단일 엔진(native)만 남았습니다(ITSM-2026-002190) |
|
|
203
204
|
| `useWorker` | `boolean` | `false` | Web Worker 사용 여부 |
|
|
@@ -210,7 +211,7 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
210
211
|
| `rowHeightChangeThreshold` | `number` | `2` | 높이 변경 감지 임계값 (px) |
|
|
211
212
|
| `headerHeight` | `number` | — | 헤더 높이 (px) |
|
|
212
213
|
| `showHeader` | `boolean` | `true` | 헤더 표시 여부 |
|
|
213
|
-
| `rowHeader` | `boolean \| RowHeaderConfig` | `false` | 좌측 행 헤더 컬럼(`_rowHeader`) 표시. `true`면 빈 셀(너비 40, 체크박스 트리거 시 체크박스), 객체면 `width`/`cellRenderer`/`headerRenderer`로 커스터마이즈 (→ [RowHeaderConfig](#rowheaderconfig)). `rowSelection.trigger`가 `'rowHeader'`/`'checkbox'`면 미지정이어도 자동 ON. (구 `showRowHeader` — deprecated alias 유지) |
|
|
214
|
+
| `rowHeader` | `boolean \| RowHeaderConfig` | `false` | 좌측 행 헤더 컬럼(`_rowHeader`) 표시. `true`면 빈 셀(너비 40, 체크박스 트리거 시 체크박스), 객체면 `width`/`cellRenderer`/`headerRenderer`/`includeGroupRows`로 커스터마이즈 (→ [RowHeaderConfig](#rowheaderconfig)). `rowSelection.trigger`가 `'rowHeader'`/`'checkbox'`면 미지정이어도 자동 ON. (구 `showRowHeader` — deprecated alias 유지) |
|
|
214
215
|
| `showStatusBar` | `boolean` | `true` | 상태 바 표시 |
|
|
215
216
|
| `summaryRow` | `'none' \| 'pinnedTop' \| 'pinnedBottom'` | `'none'` | 집계(총합계) 행 표시 위치. `FieldDef.aggregate` 가 지정된 컬럼을 모아 core 가 행을 직접 소유하며, `getConfig()` 에 실려 레이아웃 프리셋으로 저장·복원된다 |
|
|
216
217
|
| `alternatingRows` | `boolean` | `false` | 줄무늬 행 스타일 |
|
|
@@ -248,6 +249,29 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
248
249
|
| ~~`onCellClick`~~ | — | — | **제거됨.** `grid.on('cell:click', ...)` 이벤트 구독으로 대체 (→ [이벤트](#이벤트)) |
|
|
249
250
|
| ~~`onGroupToggle`~~ | — | — | **제거됨.** 로우 그룹 토글은 이벤트로 노출됩니다 — `rowGroup:toggle`(단일), `rowGroup:batchToggle`(일괄: `scope`로 `level`/`toLevel`/`all` 구분). `grid.on('rowGroup:toggle', ...)` 등으로 구독 (→ [이벤트](#이벤트)) |
|
|
250
251
|
|
|
252
|
+
#### 행 identity: 업무 키와 자동 ID
|
|
253
|
+
|
|
254
|
+
- **업무 키 모드**: `keyFields: ['id']`처럼 비어 있지 않은 배열을 명시합니다. `enableReferCache` 기본값은 `false`이며, 자동 ID 모드가 누락된 업무 키 값을 대신 채워주지는 않습니다.
|
|
255
|
+
- **자동 ID 모드**: `keyFields`를 **생략**하고 `enableReferCache: true`를 지정합니다. `keyFields: []`는 캐시 설정과 관계없이 초기화 오류이며, 키 생략 + 캐시 미지정/`false`도 빈 데이터부터 초기화 오류입니다. 생성 후 `await grid.waitForReady()`로 초기화 성공/실패를 확인하세요.
|
|
256
|
+
- ID는 값이나 원본 객체가 아니라 **행 occurrence(배열에 등장한 각각의 행)** 에 속합니다. 값이 같거나 동일 객체를 두 번 넣어도 서로 다른 ID가 발급됩니다. frozen 원본 객체에도 ID 필드를 주입하지 않습니다.
|
|
257
|
+
- 정렬·필터·재정렬, dirty 편집·추가·삭제 복원·discard, undo/redo 및 commit은 같은 occurrence의 ID를 유지합니다. 반면 `loadData()`로 전체 데이터를 교체하면 동일 객체를 다시 넘겨도 새 ID가 발급됩니다. 서버 키나 재조회 간 영속 식별자로 저장하지 마세요.
|
|
258
|
+
- 자동 identity는 원본/조회 데이터의 속성이나 Symbol이 아니라 내부 메타데이터로 관리됩니다. JSON 및 도메인 데이터 export에 자동 ID 필드가 추가되지 않습니다. 행 접근자의 `id`, 선택 ID 또는 `addRow()` 반환값으로 ID를 얻고, 원본 객체에 ID를 만들어 넣거나 문자열 형태를 해석하지 마세요.
|
|
259
|
+
- 자동 모드의 객체 기반 API에는 그리드가 반환한 행 데이터를 사용합니다. 원본 객체나 `{ ...row }`/JSON 복사본은 occurrence를 식별하는 핸들이 아닙니다. ID를 별도로 가진 `changes.updateCellById(id, field, value)` 등으로 수정하세요. raw keyless patch만으로 대상을 추론하지 않습니다([트랜잭션](#트랜잭션--rowtransaction--transactionoptions--transactionresult)).
|
|
260
|
+
- Worker도 occurrence ID를 데이터와 분리해 전송하지만 **기존 동기 조회 제한은 유지**됩니다. clean Worker 행은 `rows.getByIndex()`/`observeByIndex()`가 실패하거나 `getById()`/reactive snapshot/`all()`/`where()`가 비어 있을 수 있습니다. `await grid.getRowById(id)` 등 비동기 조회를 사용하세요. 비동기 조회가 전체 동기 캐시를 채우지는 않습니다. Worker의 기존 loader/data 전달 제한도 바뀌지 않습니다.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// 메인 스레드 예제: 행 데이터에 ID를 추가하지 않고 ID를 별도로 사용
|
|
264
|
+
const automaticGrid = new PureSheet(container, {
|
|
265
|
+
enableReferCache: true,
|
|
266
|
+
fields: [{ id: 'name', header: '이름', dataType: 'string' }],
|
|
267
|
+
data: [{ name: '홍길동' }, { name: '홍길동' }],
|
|
268
|
+
});
|
|
269
|
+
await automaticGrid.waitForReady();
|
|
270
|
+
const rowId = automaticGrid.rows.getByIndex(0).id;
|
|
271
|
+
await automaticGrid.changes.updateCellById(rowId, 'name', '김철수');
|
|
272
|
+
await automaticGrid.changes.commit(); // 같은 rowId 유지
|
|
273
|
+
```
|
|
274
|
+
|
|
251
275
|
### FlatModeConfig
|
|
252
276
|
|
|
253
277
|
`PureSheetConfigBase`에 추가:
|
|
@@ -371,7 +395,7 @@ grid.on('row:click', ({ rowIndex }) => {
|
|
|
371
395
|
| 속성 | 타입 | 설명 |
|
|
372
396
|
|------|------|------|
|
|
373
397
|
| `mask` | `MaskConfig` | 마스킹 설정 |
|
|
374
|
-
| `aggregate` | `AggregateFunc` | 그룹/피벗 집계 함수. wrapper의 `useSummaryFooter: true`에서는 컬럼 하단 집계에도 사용한다(기본 OFF). 표시를 꺼도 함수는
|
|
398
|
+
| `aggregate` | `AggregateFunc \| (values, ctx) => CellValue` | 그룹/피벗 집계 함수. wrapper의 `useSummaryFooter: true`에서는 컬럼 하단 집계에도 사용한다(기본 OFF). 표시를 꺼도 함수는 유지된다. 함수형은 그룹행 자기 칸·고정 합계 행 값을 계산한다 — [그룹 집계 표시 자리와 함수형 집계](#그룹-집계-표시-자리와-함수형-집계) |
|
|
375
399
|
|
|
376
400
|
### 그룹 행
|
|
377
401
|
|
|
@@ -459,7 +483,7 @@ PureSheet 인스턴스에서 직접 호출하는 메서드들입니다. Sub-API
|
|
|
459
483
|
|--------|------|------|
|
|
460
484
|
| `loadData(data)` | `Promise<void>` | 데이터 로드 (기존 데이터 교체) |
|
|
461
485
|
| `getAllData()` | `Promise<RowData[]>` | 전체 데이터 조회 |
|
|
462
|
-
| `getVisibleData()` | `Promise<RowData[]>` | 필터/정렬 적용된 processor 데이터 조회. 하위 호환 API라 rowGroup/tree collapse로 renderer에서 숨긴 자식 행은 제외하지 않음 |
|
|
486
|
+
| `getVisibleData()` | `Promise<RowData[]>` | 필터/정렬 적용된 processor 데이터 조회. 하위 호환 API라 rowGroup/tree collapse로 renderer에서 숨긴 자식 행은 제외하지 않음. 커밋 전 드래그 재정렬도 반영하지 않음(원래 순서) — 화면 순서는 `getDisplayedData()` |
|
|
463
487
|
| `getDisplayedData()` | `Promise<RowData[]>` | 현재 renderer view-index 순서의 순수 도메인 데이터 행. rowGroup/tree collapse 반영(접힌 자식 제외), group header/subtotal/grand-total/master-detail 구조 행 제외, DOM/viewport 가상화와 무관 |
|
|
464
488
|
| `getDisplayedRowIds()` | `Promise<RowId[]>` | `getDisplayedData()`와 같은 순서의 도메인 rowId 목록 |
|
|
465
489
|
| `rows.displayedCount()` | `number` | displayed 좌표계 총 행 수(그룹/트리 collapse 반영). traversal 경계 계산용 |
|
|
@@ -651,8 +675,8 @@ grid.clearSearch(); // 종료
|
|
|
651
675
|
|
|
652
676
|
| 메서드 | 반환 | 설명 |
|
|
653
677
|
|--------|------|------|
|
|
654
|
-
| `getConfig()` | `GridConfigExport` | 현재 그리드 상태 + reset baseline 을 `{ current, snapshot }` bundle 로
|
|
655
|
-
| `applyConfig(cfg, options?)` | `Promise<void>` | Export bundle 을
|
|
678
|
+
| `getConfig()` | `GridConfigExport` | 현재 그리드 상태 + reset baseline 을 `{ version, current, snapshot }` bundle 로 반환. 생성 시점 config 가 아니라 **현재 값**(UI 옵션·행 그룹·컬럼 그룹 포함) |
|
|
679
|
+
| `applyConfig(cfg, options?)` | `Promise<void>` | Export bundle 을 **원자적으로** 적용. 입력 전체를 먼저 검증해 잘못되면 아무것도 바꾸지 않고 `PS903`/`PS901` 을 던지고, 적용 도중 실패하면 직전 상태로 되돌린 뒤 `PS904` 를 던진다. `options`: `{ applySnapshot?: boolean; missing?: 'reset' \| 'keep' }` |
|
|
656
680
|
|
|
657
681
|
**`GridConfigExport` bundle 구조**
|
|
658
682
|
|
|
@@ -675,6 +699,48 @@ interface GridConfigExport {
|
|
|
675
699
|
- `true` (default) — `cfg.snapshot` 이 있으면 baseline 도 복원. 라운드트립 보장
|
|
676
700
|
- `false` — `cfg.snapshot` 이 있어도 무시. `current` 만 임시 layout 으로 적용 (기존 baseline 유지)
|
|
677
701
|
|
|
702
|
+
**`options.missing` — 저장본에 빠진 항목의 해석** (F04)
|
|
703
|
+
|
|
704
|
+
| 값 | 의미 | 빠진 항목 |
|
|
705
|
+
|---|---|---|
|
|
706
|
+
| `'reset'` | 전체 상태(snapshot) | **기본 보기**로 초기화 — 아래 표 |
|
|
707
|
+
| `'keep'` | 덮어쓰기(overlay) | **현재 값 유지** — 컬럼 `aggregate`/`pinned`, `summaryRow` 도 예외 없이 유지 |
|
|
708
|
+
| 미지정 | 버전 판정 | 원래(마이그레이션 전) `version` 이 `SNAPSHOT_CONFIG_VERSION`(`'0.5'`) 이상이면 `'reset'`, 아니면(버전 없음 포함) `'keep'` |
|
|
709
|
+
|
|
710
|
+
`getConfig()` 결과는 항상 `0.5` 이상이고 "없음" 도 명시값(`rowGroupConfig: null`, `columnGroups: []`, `treeConfig: null`, `masterDetailConfig: null`, 해제된 컬럼 고정·집계는 키 생략)으로 싣기 때문에 라운드트립은 정확히 그 시점 상태로 돌아갑니다. 버전 없는 부분 객체(`{ current: { mode: 'flat', rowHeight: 32 } }`)는 덮어쓰기로 해석되어 기존 호출자의 의미가 유지됩니다.
|
|
711
|
+
|
|
712
|
+
`'reset'` 의 기본값 — **앱의 기본 보기 = 그리드 생성 시점 상태**(생성자 끝에서 캡처):
|
|
713
|
+
|
|
714
|
+
| 항목 | reset 기본값 |
|
|
715
|
+
|---|---|
|
|
716
|
+
| `fields` 키 전체 누락 | 컬럼 reset baseline(`columns.getInitialSnapshotDefs()`, `applySnapshot` 이면 `cfg.snapshot.fields`)의 너비·숨김·고정·집계·순서 |
|
|
717
|
+
| 필드의 `width` / `hidden` 누락 | 같은 컬럼의 baseline 값 |
|
|
718
|
+
| 필드의 `pinned` / `aggregate` 누락 | 해제 (`getConfig` 가 해제를 키 생략으로 싣는다) |
|
|
719
|
+
| `fields` 에 없는 컬럼 | baseline 값, 순서는 저장본 컬럼 뒤(baseline 순) |
|
|
720
|
+
| `sorts`/`sortGroups`, `filters`/`filterGroups` | 생성 시점(빈 정렬·필터) |
|
|
721
|
+
| `rowGroupConfig`, `columnGroups`, `treeConfig`, `masterDetailConfig` | 생성 시 config 값 |
|
|
722
|
+
| UI 옵션(아래 목록), `summaryRow`, `locale`, `translations`, `rowModel`, `endReachedThresholdRows` | 생성 시 값(미지정이면 라이브러리 기본값) |
|
|
723
|
+
| `mode` | 생성 시 모드 |
|
|
724
|
+
| 피벗 하위 옵션(`rowFields`/`valueFields` 를 준 경우) | 라이브러리 기본값 — 피벗 정의는 `pivot.setConfig` 로 통째 교체되는 한 단위. `keep` 이면 현재 피벗 설정 값 유지 |
|
|
725
|
+
|
|
726
|
+
`rowGroupConfig: null` 은 "빠진 항목" 이 아니라 명시값입니다 — 그룹 컬럼·집계만 비우고 그룹 옵션(`selectable` 등)은 두 모드 모두 휴면 보존합니다.
|
|
727
|
+
|
|
728
|
+
**검증과 원자성** (F06)
|
|
729
|
+
|
|
730
|
+
- 진입부에서 `options` → 마이그레이션 → bundle 전체 순으로 검사하고, 계획(빠진 항목 채우기·컬럼별 목표 상태·피벗 설정 병합)을 미리 계산한 뒤에만 상태를 바꿉니다.
|
|
731
|
+
- 최소 형태 검증: `mode` ∈ `flat`/`pivot`, `fields[]` 원소는 문자열 `id` 를 가진 객체(`width` 양수, `hidden` boolean, `pinned` ∈ `left`/`right`/`none`/null), 정렬 `direction` ∈ `asc`/`desc`, `summaryRow`·`tooltipMode`·선택 `mode` 열거값, `rowGroupConfig.columns` 문자열 배열, 피벗 축 원소는 문자열 `field` 를 가진 객체, UI 옵션 타입 등. 값의 의미(모르는 컬럼 id)는 검사하지 않고 적용 단계에서 건너뜁니다.
|
|
732
|
+
- 오류: `PS903`(형태 — 메시지에 `current.fields[3].id` 같은 경로), `PS901`(마이그레이션 실패 — 예전처럼 원본으로 계속하지 않음), `PS904`(적용 도중 실패 → 직전 `getConfig()` 상태로 되돌림, 원인은 `error.cause`), 피벗 설정이 없는데 `rowFields`/`valueFields` 없이 `mode: 'pivot'` 이면 `PS903`.
|
|
733
|
+
- **셀 편집 중이면 먼저 확정**: 검증을 통과한 뒤 상태를 바꾸기 직전에, 열린 셀 편집기를 다른 셀을 클릭했을 때와 같은 경로로 확정합니다(`changes`/Undo 기록, `cell:editCommitted` 발행). 확정이 막히면(min/max/step 위반, `inputMask` 미완성, `cell:editCommitting` 에서 `cancel()`) `PS906` 으로 거절하고 아무것도 바꾸지 않습니다. 편집기와 입력값도 그대로 남습니다. 검증에 실패한 호출(`PS903`/`PS901`)은 편집을 건드리지 않습니다. 오버레이 편집기(드롭다운·날짜 선택·멀티선택)는 대상이 아닙니다.
|
|
734
|
+
- **다른 설정·뷰 API도 같은 규칙**: 행 화면을 다시 그리는 API — `columns.pinLeft`/`pinRight`/`unpin`/`setOrder`/`move`/`moveGroup`/`hide`/`show`/`toggle`/`setHidden`/`load`/`reset`/`resetAll`, `columns.update`(`hidden`·`pinned`·`frozen` 포함 시), `headers.*`(접기·펼치기, `setColumnGroups`), `updateFields`, `sort`/`toggleSort`/`setSortGroup`/`filter`/`setFilterGroup`/`setViewState`, `rowGroup.setConfig`/`groupBy`/`clear`/`addColumn`/`removeColumn`/접기·펼치기, `tree.setConfig`/`setState`/접기·펼치기, `pivot.setMode`, `rows.pinTop*`/`pinBottom*`/`unpin`/`unpinById`/`clearPinned` — 도 상태를 바꾸기 전에 열린 편집을 확정하고, 확정이 막히면 `PS906` 으로 거절합니다(동기 API는 예외를 던지고, 비동기 API는 Promise 가 거부됩니다). 너비·`autoFit`·헤더 이름처럼 행을 다시 그리지 않는 변경은 편집을 그대로 둡니다. 데이터 API(`loadData`, `applyTransaction`, `changes.addRow`/`deleteRow*`/`updateCell*`, `undoManager.undo`/`redo`)와 스크롤은 아직 대상이 아닙니다.
|
|
735
|
+
|
|
736
|
+
**UI 옵션 런타임 복원** (F02)
|
|
737
|
+
|
|
738
|
+
`getConfig()` 는 아래 옵션의 **현재 값**을 싣고 `applyConfig()` 는 런타임에 실제로 되돌립니다: `theme`, `rowHeight`(가상 스크롤·동적 높이 캐시·Multi-Row·고정 행·병합 float 재계산), `headerHeight`, `rowSelection`/`cellSelection`/`cellSelectionImpliesRows`(바뀔 때만 재설정 — 현재 선택은 비움), `rowHeader`(행 헤더 컬럼 추가/제거, 선택 트리거가 요구하면 자동 활성), `editable`(편집기·붙여넣기·Delete 단축키), `tooltipMode`, `alternatingRows`, `showHeader`, `resizableColumns`/`reorderableColumns`(헤더 재빌드), `autoFitColumns`, `showStatusBar`, `summaryRow`, `locale`, `translations`.
|
|
739
|
+
|
|
740
|
+
현재 값만 내보내고 **적용하지 않는** 키(값이 다르면 경고 `PS905`): `reorderableRows`/`showDragHandleColumn`(드래그 재정렬 배선이 생성 시점 전용 — `dragDropConfig` 소유), `keyFields`(행 식별자 — 바꾸려면 데이터 재적재 필요), `rowTemplate`(Multi-Row 런타임 전환 API 없음).
|
|
741
|
+
|
|
742
|
+
**피벗 왕복** (F01) — flat → pivot 진입 직전의 flat 런타임 상태(컬럼 너비·고정·숨김·순서, 정렬/필터, 행 그룹)를 저장해 두었다가 flat 복귀(`pivot.setMode('flat')`, pivot→flat `applyConfig`) 시 그대로 되살리고 컬럼 그룹 헤더도 재빌드합니다. flat 저장본을 함께 적용하면 저장본 값이 이깁니다.
|
|
743
|
+
|
|
678
744
|
**`current` / `snapshot` 에 포함되는 항목 — `GridConfigSnapshot` 형식**
|
|
679
745
|
|
|
680
746
|
| 카테고리 | 내용 |
|
|
@@ -683,7 +749,7 @@ interface GridConfigExport {
|
|
|
683
749
|
| **컬럼(flat)** | `fields[]` — 각 필드의 `id`, `width`, `hidden`, `pinned`, 사용자 정의 순서 |
|
|
684
750
|
| **컬럼 그룹(flat)** | `columnGroups` |
|
|
685
751
|
| **행 템플릿(flat)** | `rowTemplate` (Multi-Row 레이아웃) |
|
|
686
|
-
| **그룹핑(flat)** | `
|
|
752
|
+
| **그룹핑(flat)** | `rowGroupConfig` (현재 값, 없으면 `null`) |
|
|
687
753
|
| **피벗** | `rowFields`, `columnFields`, `valueFields`, 모든 subtotal/grandTotal 옵션, `rowDisplayMode`, `outlineColumnWidth`, `outlineIndentSize`, `showZeros`, `trackSourceRows`, `rowCollapsible`, `columnCollapsible` 등 |
|
|
688
754
|
| **뷰 상태** | `sorts`, `filters` |
|
|
689
755
|
| **서버/수동 row-model** | `rowModel`, `endReachedThresholdRows` |
|
|
@@ -702,7 +768,7 @@ interface GridConfigExport {
|
|
|
702
768
|
**특징**
|
|
703
769
|
|
|
704
770
|
- **라운드트립 보장** — `applyConfig(getConfig())` 가 항상 동일한 상태로 복원 (baseline 포함)
|
|
705
|
-
-
|
|
771
|
+
- **누락 키** — `options.missing` 에 따른다: 버전 없는 부분 객체는 덮어쓰기(누락 = 기존 값 유지), `getConfig()` 저장본(0.5+)은 전체 상태(누락 = 기본 보기)
|
|
706
772
|
- **모드 전환 자동 처리** — `current.mode` 가 현재와 다르면 `pivot.setMode()` 자동 호출
|
|
707
773
|
- **순서 복원** — flat 모드의 `current.fields` 배열 순서 그대로 컬럼이 재정렬됨
|
|
708
774
|
- **getConfig 는 sync, applyConfig 는 async** — 복원 중 엔진 재계산(정렬/필터/피벗)이 포함되기 때문
|
|
@@ -894,7 +960,7 @@ PureSheet 인스턴스는 기능별 Sub-API 네임스페이스를 제공합니
|
|
|
894
960
|
|--------|------|------|
|
|
895
961
|
| `getSelectedCellIds()` | `{rowId, columnId}[]` | 선택된 셀 목록 (셀 API 에는 `selected()` 가 없다 — 행 API `grid.rows.selected()` 와 다름) |
|
|
896
962
|
| `getSelectedCells()` | `SelectedCell[]` | 선택된 셀 상세 (행 핸들·값 포함) |
|
|
897
|
-
| `getActiveCell()` | `SelectedCell \| null` | 현재 활성 셀 |
|
|
963
|
+
| `getActiveCell()` | `SelectedCell \| null` | 현재 활성 셀 — `selection.getActiveCell()`과 같음(화면 자리 기준, 아래 표 참고) |
|
|
898
964
|
| `getSelectedCellRowIds()` | `string[]` | 선택된 셀이 걸친 행 rowId 목록(중복 제거) |
|
|
899
965
|
| `getSelectedCellColumnIds()` | `string[]` | 선택된 셀이 걸친 컬럼 columnId 목록(중복 제거) |
|
|
900
966
|
| `getSelectedCellIndices()` | `number[]` | 선택된 셀이 걸친 행 sourceIndex 목록(중복 제거) |
|
|
@@ -907,10 +973,15 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
|
|
|
907
973
|
|
|
908
974
|
| 메서드 | 반환 | 설명 |
|
|
909
975
|
|--------|------|------|
|
|
910
|
-
| `selection.getSelectedRows()` | `RowHandle[]` | 현재 선택된 행들(viewIndex
|
|
911
|
-
| `selection.getActiveRow()` | `RowHandle \| null` | 마지막 상호작용(행/셀 클릭) 행. 앵커 없으면 선택 행 마지막 폴백. traversal
|
|
976
|
+
| `selection.getSelectedRows()` | `RowHandle[]` | 현재 선택된 행들(viewIndex=화면 순번, sourceIndex=원본 데이터 위치 채워짐. 피벗 행·원본에 없는 행은 sourceIndex `undefined`) |
|
|
977
|
+
| `selection.getActiveRow()` | `RowHandle \| null` | 마지막 상호작용(행/셀 클릭) 행. 앵커 없으면 선택 행 마지막 폴백. traversal 앵커. **클릭한 행(rowId)을 계속 가리킨다** — 정렬·필터·접기 뒤에도 같은 행(`getActiveCell`과 다를 수 있음) |
|
|
912
978
|
| `selection.getSelectedCells()` | `{ row: RowHandle; columnId }[]` | 현재 선택된 셀들 |
|
|
913
|
-
| `selection.getActiveCell()` | `{ row: RowHandle; columnId } \| null` | 마지막 상호작용 셀. traversal 컬럼
|
|
979
|
+
| `selection.getActiveCell()` | `{ row: RowHandle; columnId } \| null` | 마지막 상호작용 셀. traversal 컬럼 유지용. 행은 **화면 자리 기준** — 셀 선택(`getSelectedCellIds`)·화면 강조·키보드 이동과 같은 행이다. 정렬·필터·그룹/트리 접기·디테일 펼침·드래그 재정렬로 그 자리에 다른 행이 오면 그 행, 자리가 그룹 배너·디테일 행이거나 사라지면 `null`. 행 선택 모드(셀 선택 없음)의 앵커와 고정(pinned) 행은 클릭한 행 그대로 |
|
|
980
|
+
| `selection.getSelectedGroups()` | `{ groupId: string; rowIds: RowId[] }[]` | 선택된 그룹 (`rowGroupConfig.selectable` — [그룹 배너 선택](#그룹-배너-선택-selectable)). `rowIds`는 리프 멤버 전체(접힘 무관, 중첩이면 하위 리프 전부). 옵션이 꺼져 있으면 `[]`. 행 선택 결과와 별개 |
|
|
981
|
+
| `selection.selectGroupsByIds(groupIds, { addToExisting? })` | `string[]` | 그룹을 코드로 선택 — 배너 본문 클릭과 같은 의미([그룹 배너 선택](#그룹-배너-선택-selectable)). 적용된 groupId(입력 순서, 중복 제거)를 반환. 화면에 없는 그룹·옵션 꺼짐은 무시 |
|
|
982
|
+
| `selection.selectGroupsByViewIndices(viewIndices, { addToExisting? })` | `string[]` | viewIndex로 그룹 선택. 그룹행이 아닌 위치는 무시. 나머지는 `selectGroupsByIds`와 같음 |
|
|
983
|
+
|
|
984
|
+
> **피벗 모드의 행(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`.
|
|
914
985
|
|
|
915
986
|
|
|
916
987
|
#### 선택 일괄
|
|
@@ -921,18 +992,23 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
|
|
|
921
992
|
| `updateSelectedRows(field, value)` | `Promise<number>` | 선택 **행**들의 지정 필드 일괄 업데이트 |
|
|
922
993
|
| `discardSelectedRows()` | `Promise<number>` | 선택 행 변경 취소 |
|
|
923
994
|
|
|
924
|
-
#### 데이터 행 고정 (기존 데이터 행을
|
|
995
|
+
#### 데이터 행 고정 (기존 데이터 행을 상단/하단 고정 영역으로 옮기기)
|
|
925
996
|
|
|
926
|
-
> `pinTop
|
|
997
|
+
> `pinTop*`/`pinBottom*`은 본문 데이터 행을 고정 영역으로 **옮깁니다** — `rows.getById(id).setProps({ pinned: 'top' | 'bottom' })`와 같은 동작입니다. 호출 후 그 행은 본문에서 빠지고 고정 영역에 한 번만 보이며, 고정 영역에서의 편집은 원본 데이터에 반영됩니다(`changes` 포함). `unpin`/`unpinById`/`clearPinned`로 본문 원래 위치로 돌아옵니다. 같은 행을 다시 고정해도 중복되지 않고, `pinTop` ↔ `pinBottom`은 영역 이동입니다. 정렬·필터·그룹·트리 상태에서도 동작합니다.
|
|
998
|
+
> `pinTop`/`pinBottom`은 **데이터 행 객체**를 받아 키(`keyFields`)로 본문 행을 찾습니다. 본문에 없는 객체/ID를 주면 기존처럼 화면 전용 고정 행으로 추가됩니다(`addPinnedTop`/`addPinnedBottom`과 동일). `Row` 인스턴스는 그대로 꽂힙니다. 새로운 행/구성을 추가하려면 [구조적 행 고정](#구조적-행-고정) 섹션의 `addPinnedTop` / `addPinnedBottom`을 사용하세요.
|
|
999
|
+
> Worker 모드(`useWorker: true`)에서는 메인 스레드에 원본 데이터가 없어 `pinTop`/`pinTopById`/`pinBottom`/`pinBottomById`가 본문 행을 동기로 찾지 못합니다. 이때는 화면 전용 고정 행으로 추가되고 `PS503` 경고가 콘솔에 나옵니다(본문 행 이동·편집 반영·해제 시 본문 복귀 없음). `pin*ByIndex`는 Worker 모드에서 행을 찾지 못해 예외를 던집니다.
|
|
1000
|
+
> `enableReferCache: false`에서도 동작합니다(`*ByIndex`는 O(1), 객체·ID는 원본 데이터를 한 번 훑어 찾음). `setProps({ pinned })`로 직접 고정한 행도 같은 방식으로 원본 위치를 찾아 표시·클릭·우클릭·편집이 동작합니다.
|
|
927
1001
|
|
|
928
1002
|
| 메서드 | 반환 | 설명 |
|
|
929
1003
|
|--------|------|------|
|
|
930
|
-
| `pinTop(row)` | `Row` | 데이터 행
|
|
931
|
-
| `pinTopById(rowId)` | `Row` | ID로
|
|
932
|
-
| `pinTopByIndex(sourceIndex)` | `Row` | sourceIndex로
|
|
933
|
-
| `pinBottom(row)` | `Row` | 데이터 행
|
|
934
|
-
| `pinBottomById(rowId)` | `Row` | ID로
|
|
935
|
-
| `pinBottomByIndex(sourceIndex)` | `Row` | sourceIndex로
|
|
1004
|
+
| `pinTop(row)` | `Row` | 데이터 행 객체(키로 찾은 본문 행)를 상단 고정 영역으로 옮김 |
|
|
1005
|
+
| `pinTopById(rowId)` | `Row` | ID로 본문 행을 상단 고정 영역으로 옮김 |
|
|
1006
|
+
| `pinTopByIndex(sourceIndex)` | `Row` | sourceIndex로 본문 행을 상단 고정 영역으로 옮김 |
|
|
1007
|
+
| `pinBottom(row)` | `Row` | 데이터 행 객체(키로 찾은 본문 행)를 하단 고정 영역으로 옮김 |
|
|
1008
|
+
| `pinBottomById(rowId)` | `Row` | ID로 본문 행을 하단 고정 영역으로 옮김 |
|
|
1009
|
+
| `pinBottomByIndex(sourceIndex)` | `Row` | sourceIndex로 본문 행을 하단 고정 영역으로 옮김 |
|
|
1010
|
+
|
|
1011
|
+
반환 `Row`는 화면에 그려지는 고정 행 인스턴스이며 데이터는 본문 행 데이터를 따릅니다(`rows.getPinned()`에도 포함).
|
|
936
1012
|
|
|
937
1013
|
#### 구조적 행 고정
|
|
938
1014
|
|
|
@@ -943,14 +1019,18 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
|
|
|
943
1019
|
| `addGrandTotal(aggregates)` | `Row` | **(deprecated)** 전체 합계 행 추가 — `grid.setSummaryRow('pinnedBottom')` 를 사용할 것. 이 API 로 꽂은 행은 `getConfig()` 에 실리지 않아 레이아웃 프리셋으로 저장·복원되지 않는다 |
|
|
944
1020
|
| `addFilter()` | `Row` | 필터 행 추가 |
|
|
945
1021
|
|
|
1022
|
+
> `{ id, data }`로 추가한 데이터 고정 행(본문 데이터에 없음)은 `enableReferCache` 없이도 클릭·더블클릭·우클릭(`contextmenu` 의 `rowData` 포함)이 동작합니다. 셀 편집은 그 고정 행 데이터(`row.getData()`)에 저장되고 화면에 표시되며 `cell:editCommitted`도 발행됩니다. 화면 전용 행이므로 변경 추적(`changes.getAll()`)에는 들어가지 않지만 편집은 Undo/Redo 됩니다(`enableReferCache:true` 에서 본문에 같은 id 행이 있으면 기존대로 본문 행이 편집됩니다). 고정 영역 셀을 선택하고 붙여넣기(Ctrl+V)해도 같은 규칙이며, 여러 셀은 Undo 한 번에 되돌아갑니다. 붙여넣기는 그 고정 영역의 행 순서로만 채우고 본문 행으로 넘치지 않습니다(본문 행을 고정한 행은 본문 데이터로 저장되어 변경 추적에 들어갑니다).
|
|
1023
|
+
> 화면 전용 고정 행(`addPinnedTop/Bottom`, 본문에 없는 행의 `pinTop*`/`pinBottom*` — `variant: 'custom'` 포함)의 **고정과 해제(`unpin`/`unpinById`/`clearPinned`)도 Undo/Redo** 됩니다. Undo 는 같은 `Row` 인스턴스(편집 값 포함)를 같은 영역·같은 순서로 되살리고, `clearPinned`로 여러 행을 해제했으면 Undo 한 번에 모두 복원됩니다. 본문 행의 고정·해제(`setProps({ pinned })`, 본문 행을 옮기는 `pin*`)와 `summaryRow` 총합계 행은 화면 상태라 기록하지 않습니다(컬럼 고정·정렬과 같은 기준). Undo/Redo 로 고정 상태가 바뀌면 `row:pinned`/`row:unpinned`가 `source: 'undo' | 'redo'`와 함께 다시 발행됩니다.
|
|
1024
|
+
> 고정 행(종류 무관 — 화면 전용·본문 행 고정 모두)의 키 컬럼(`keyFields`)은 편집할 수 없습니다.
|
|
1025
|
+
|
|
946
1026
|
#### 고정 관리
|
|
947
1027
|
|
|
948
1028
|
| 메서드 | 반환 | 설명 |
|
|
949
1029
|
|--------|------|------|
|
|
950
1030
|
| `unpin(row)` | `boolean` | `Row` 인스턴스로 고정 해제 |
|
|
951
|
-
| `unpinById(rowId)` | `boolean` | ID로 행 고정 해제 |
|
|
952
|
-
| `getPinned()` | `{ top: Row[]; bottom: Row[] }` | 고정된 행 조회 |
|
|
953
|
-
| `clearPinned()` | `void` | 전체 고정 해제 |
|
|
1031
|
+
| `unpinById(rowId)` | `boolean` | ID로 행 고정 해제 (데이터 고정 행은 본문으로 복귀) |
|
|
1032
|
+
| `getPinned()` | `{ top: Row[]; bottom: Row[] }` | 고정된 행 조회 — 화면 순서의 구조 고정 행 + 데이터 고정 행 |
|
|
1033
|
+
| `clearPinned()` | `void` | 전체 고정 해제 (데이터 고정 행은 본문으로 복귀, core 소유 총합계 행은 유지) |
|
|
954
1034
|
| `refreshPinned()` | `void` | 고정 행 표시 갱신 |
|
|
955
1035
|
|
|
956
1036
|
---
|
|
@@ -1248,8 +1328,98 @@ await grid.changes.commit({ allowInvalid: true });
|
|
|
1248
1328
|
|
|
1249
1329
|
> **`getGroupRows(groupId)`** — 그룹 행에서 하위 멤버 전체를 일괄 조작할 때 사용합니다 (예: 그룹 단위 선택·체크박스 cascade). 멤버십은 그룹 컬럼 값 기준 매칭(`String(row[col]) === value`)이며, `groupId`의 각 세그먼트는 **첫 `:` 만** 구분자로 사용하므로 값에 `:`가 포함돼도 안전합니다(값에 `/` 포함은 경로 모호성 — 규약상 한계). `grid.rows.getByIds(getGroupRows(id))` 조합으로 일괄 수정하세요.
|
|
1250
1330
|
|
|
1331
|
+
#### 그룹 배너 선택 (`selectable`)
|
|
1332
|
+
|
|
1333
|
+
`rowGroupConfig.selectable: true`(기본 `false`)면 그룹 배너(그룹행)를 선택할 수 있습니다. 끄면 배너는 선택되지 않고 본문 클릭 = 접기/펼치기(현행 그대로)입니다.
|
|
1334
|
+
|
|
1335
|
+
| 조작 | 결과 |
|
|
1336
|
+
|------|------|
|
|
1337
|
+
| 데이터 셀에서 시작한 드래그 / Shift+클릭 범위가 배너를 지남 | 셀 범위 폭은 그대로(배너 폭으로 넓어지지 않음), 지나간 배너 = 선택된 그룹. 선택 사각형은 selectable 여부와 관계없이 배너(그룹행)에서 끊겨 데이터 행 구간만 묶고(배너에는 사각형·강조 없음), 셀 선택 결과(`getSelectedCellIds` 등)에는 데이터 셀만 들어갑니다. Ctrl+드래그/Ctrl+Shift는 기존 그룹에 더합니다 |
|
|
1338
|
+
| 배너 본문 클릭 | 그 그룹만 선택 (셀 범위 해제, 접기/펼치기 안 함) |
|
|
1339
|
+
| 배너에서 시작해 드래그 | 지나간 **그룹행만** 선택(데이터 행은 건너뜀, 셀 범위 없음) |
|
|
1340
|
+
| Ctrl/Cmd+배너 클릭 | 그룹 추가/제외 |
|
|
1341
|
+
| Shift+배너 클릭 | 마지막으로 클릭한 그룹 ~ 클릭한 그룹 사이의 그룹 전부 |
|
|
1342
|
+
| ▾ 버튼 클릭 / Ctrl+▾ | 접기/펼치기 / 같은 depth 일괄 접기/펼치기 (선택은 바뀌지 않음) |
|
|
1343
|
+
|
|
1344
|
+
- 결과: `grid.selection.getSelectedGroups()` → `[{ groupId, rowIds }]`, `selection:changed`의 `selectedGroups`. `rowIds`는 그룹의 리프 멤버 전체(접혀 있어도, 중첩 그룹이면 하위 리프 전부)이며 표기는 `getGroupRows`와 같습니다(단일 키 원시값).
|
|
1345
|
+
- 행 선택(`rowSelection`) 결과는 바뀌지 않습니다 — 그룹 멤버를 행 선택에 넣으려면 소비처가 `rowIds`를 사용합니다.
|
|
1346
|
+
- 배너에서 고른 그룹은 배너와 그룹행 셀을 행 선택 색으로 칠합니다(`.ps-group-banner-selected`, `.ps-group-row-selected`). 범위가 지나간 그룹은 결과에만 기록되고 화면 표시는 없습니다(사각형은 배너에서 분할).
|
|
1347
|
+
- 배너 커서: selectable이면 배너 본문·그룹행은 데이터 셀과 같은 커서이고 ▾ 버튼만 pointer입니다. 옵션(`selectable`/`defaultCollapsed`/`defaultExpandLevel`)은 그룹 컬럼을 모두 뺐다가(`removeColumn`/`groupBy([])`/`setConfig({ columns: [] })`, `getConfig()`는 `null`) 다시 넣어도 유지되며, 생략 시 이어받고 명시값(`false` 포함)이 이깁니다. `clear()`/`setConfig(null)`은 옵션까지 지웁니다.
|
|
1348
|
+
- 다른 셀/행 클릭·새 범위, `clearSelection`/`clearCellSelection`, `rowGroup.setConfig`(재그룹), `loadData` 시 그룹 선택이 비워집니다.
|
|
1349
|
+
- Ctrl+A·프로그래매틱 범위 API(`selectCellRangeBy*`)는 그룹을 고르지 않습니다.
|
|
1350
|
+
- 키보드(셀 범위 모드·행 선택 모드 공통, 펼침/접힘·다단 그룹 모두 같음):
|
|
1351
|
+
|
|
1352
|
+
| 조작 | 현재 선택 | 결과 |
|
|
1353
|
+
|------|-----------|------|
|
|
1354
|
+
| ↑/↓ 로 그룹행에 닿음 | 셀·행·그룹 | `selectable` 켬: 그 그룹을 선택(배너 본문 클릭과 같음 — 배너 강조, `selection:changed`의 `selectedGroups`, 셀 범위 해제). 다음 ↑/↓는 그 그룹행에서 이어지고, 데이터 행에 닿으면 그 셀(그룹에 들어가기 전 컬럼)/행을 선택합니다. `selectable` 끔: 그룹행을 건너뛰고 같은 방향의 다음 데이터 행으로 갑니다. 그 방향에 그룹행만 남았으면 움직이지 않습니다 |
|
|
1355
|
+
| Shift+↑/↓ | 셀·행 | 그룹행을 건너뛰며 범위를 넓힙니다. 범위에는 데이터 행만 들어가고 사각형은 배너에서 끊깁니다. 그룹은 고르지 않습니다 |
|
|
1356
|
+
| Shift+↑/↓ | 그룹(배너 선택) | 같은 방향의 다음 그룹행까지 그룹 선택을 넓힙니다(배너 Shift+클릭과 같음 — 기준 그룹 ~ 새 끝 사이 그룹 전부, 데이터 행은 건너뜀). 더 갈 그룹행이 없으면 그대로 |
|
|
1357
|
+
| ←/→ | 그룹(배너 선택) | 아무 일도 하지 않습니다 |
|
|
1358
|
+
|
|
1359
|
+
선택 없이 처음 누른 ↑/↓는 0행이 그룹행이면 같은 규칙을 따릅니다(selectable이면 첫 그룹 선택, 아니면 첫 데이터 행). 그룹행의 셀에는 키보드로 셀 선택·강조가 생기지 않습니다. 행 선택 모드에서 그룹을 고르면 배너 클릭과 같이 기존 행 선택은 그대로 둡니다.
|
|
1360
|
+
- 코드로 고르기: `grid.selection.selectGroupsByIds(ids, { addToExisting? })` / `selectGroupsByViewIndices(vis, { addToExisting? })`. 배너 본문 클릭과 같습니다 — 셀 범위·이전 그룹을 비우고 그 그룹만 선택(`addToExisting: true`면 기존 셀·그룹에 추가, 토글하지 않음), 배너 강조, 이후 Shift+배너 클릭의 기준점, 변경이 있을 때 `selection:changed` 1회. 접힘 상태는 바꾸지 않습니다. 이미 같은 상태면 아무 일도 하지 않습니다. `selectable`이 꺼져 있거나 현재 화면에 없는 그룹(접힌 부모 아래, 없는 id, 데이터 행 위치)은 무시하고 반환에서 뺍니다(전부 무시되면 `[]`, 상태 변화 없음). `loadData`가 그룹 선택을 비우므로 로드가 끝난 뒤 호출합니다.
|
|
1361
|
+
|
|
1362
|
+
```ts
|
|
1363
|
+
// 로드 후 첫(접힌) 그룹을 선택하고 상세를 조회
|
|
1364
|
+
// (selection:changed 는 집계 후속으로 같은 선택을 한 번 더 보낼 수 있으니 groupId 로 중복 조회를 거른다)
|
|
1365
|
+
grid.on('selection:changed', ({ selectedGroups }) => {
|
|
1366
|
+
const first = selectedGroups[0];
|
|
1367
|
+
if (first) loadDetail(first.groupId, first.rowIds);
|
|
1368
|
+
});
|
|
1369
|
+
await grid.loadData(rows);
|
|
1370
|
+
grid.selection.selectGroupsByViewIndices([0]); // 또는 selectGroupsByIds(['region:R1'])
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1251
1373
|
---
|
|
1252
1374
|
|
|
1375
|
+
#### 그룹 집계 표시 자리와 함수형 집계
|
|
1376
|
+
|
|
1377
|
+
그룹 집계는 표시되는 자리가 둘이고 선언하는 곳도 다릅니다.
|
|
1378
|
+
|
|
1379
|
+
> **원칙(단일 규칙)** — 컬럼 정의 `aggregate`와 `rowGroupConfig.aggregates`는 서로 다른 자리입니다. 컬럼 정의 `aggregate`(문자열·함수)는 그룹행의 **그 컬럼 자기 칸**(고정 컬럼, center 양 끝 연속 `unmergeOnGroupRow`)에만 나오고, 자기 칸이 없는 컬럼이면 그룹행에 나오지 않습니다(배너 글자에도 안 나옴 — 고정 합계 행·선택 영역 집계·`ctx` 계산에는 그대로 쓰입니다). `rowGroupConfig.aggregates`(문자열·함수, `scope` 포함)는 **배너 글자에만** 나옵니다 — 그 컬럼에 자기 칸이 있어도, 같은 컬럼에 컬럼 정의 `aggregate`가 있어도 칸에 넣지 않고 배너에 냅니다. 예: `ratio`에 `aggregate: 'sum'` + `unmergeOnGroupRow`, `rowGroupConfig.aggregates: [{ column: 'ratio', func: 'max' }]` → 칸 `90`, 배너 `RATIO (Max): 50`. 내보내기(XLSX·CSV)도 화면과 같습니다(자기 칸 값 → 그 컬럼 칸, 배너 글자 → 그룹 라벨 뒤 ` · `).
|
|
1380
|
+
|
|
1381
|
+
| | `rowGroupConfig.aggregates` | 컬럼 정의 `aggregate` |
|
|
1382
|
+
|---|---|---|
|
|
1383
|
+
| 표시 자리 | 그룹 배너 글자 (`급여 (Sum): 90`)만 | 그룹행의 **자기 칸**(고정 컬럼·`unmergeOnGroupRow`)만 + 고정 합계 행(`summaryRow`) |
|
|
1384
|
+
| 개수 | 같은 컬럼에 여럿 가능 | 컬럼당 하나 |
|
|
1385
|
+
| 같은 컬럼에 둘 다 있을 때 | 배너 글자 | 자기 칸 (둘 다 각자 자리에 나옴, 함수끼리도 따로 계산) |
|
|
1386
|
+
| 자기 칸이 없는 컬럼 | 배너 글자 | 그룹행에 안 나옴 |
|
|
1387
|
+
| 함수형 | `func: (values, ctx) => 값` (배너 라벨은 `헤더: 값`) | `aggregate: (values, ctx) => 값` — 결과가 그 칸의 **진짜 셀 값**(mask/format/`cellRenderer`/XLSX 공통) |
|
|
1388
|
+
| 숨김 컬럼 | 명시했으면 배너에 표시 | **계산만** 하고 배너에 표시하지 않음 (Wijmo 동일) |
|
|
1389
|
+
|
|
1390
|
+
함수 시그니처 (두 곳 같음, 기존 `(values) => 값` 함수도 그대로 동작):
|
|
1391
|
+
|
|
1392
|
+
```ts
|
|
1393
|
+
type CustomAggregateFunction = (values: CellValue[], ctx: AggregateContext) => CellValue;
|
|
1394
|
+
interface AggregateContext {
|
|
1395
|
+
sum(col: string): CellValue; avg(col: string): CellValue; count(col: string): CellValue;
|
|
1396
|
+
min(col: string): CellValue; max(col: string): CellValue;
|
|
1397
|
+
aggregates: Readonly<Record<string, CellValue>>; // 이 그룹(또는 전체)의 문자열 집계 결과, 키 '함수_컬럼id'
|
|
1398
|
+
group?: { groupId: string; level: number; column: string; value: CellValue; itemCount: number }; // 고정 합계 행이면 없음
|
|
1399
|
+
}
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
- `ctx.sum('x')` 등은 엔진이 계산한 값을 읽으므로 **그 조합이 선언돼 있어야** 합니다 — 컬럼 정의 `aggregate: 'sum'`(숨김 컬럼 가능) 또는 `rowGroupConfig.aggregates`의 `{ column: 'x', func: 'sum' }`. 선언되지 않은 조합을 읽으면 `PS312` 오류입니다(그룹행: `refresh()`가 거절, 고정 합계 행: `console.error` 후 빈 값). 고정 합계 행은 컬럼 정의 `aggregate`만 선언으로 봅니다.
|
|
1403
|
+
- 다단 그룹은 단계마다 그 단계 집계로 다시 계산하고, 필터·편집·행 추가 후에도 다시 계산합니다.
|
|
1404
|
+
- 결과가 `null`/`undefined`/`NaN`이면 그룹행 칸은 빈 칸(고정 합계 행은 기존 null 표기 `-`). 0 나누기는 함수에서 처리합니다.
|
|
1405
|
+
- 함수는 Worker로 보낼 수 없어 엔진(Main/Worker)은 문자열 집계만 계산하고 함수는 메인 스레드에서 그 결과로 실행합니다. **`values`는 메인 스레드에 멤버 행이 있을 때만 채워지고 Worker 모드에서는 빈 배열**입니다 — 두 모드 모두에서 맞으려면 `ctx`를 쓰세요.
|
|
1406
|
+
- 함수형 컬럼 aggregate가 하나라도 있으면 선언된 문자열 컬럼 aggregate를 모두(숨김·배너 아래 컬럼 포함) 계산만 합니다. 이 값들은 배너 글자에 나오지 않습니다.
|
|
1407
|
+
- 함수형 aggregate는 프리셋(JSON)에 저장되지 않고 코드가 소유합니다 — `applyConfig`가 지우거나 문자열로 바꾸지 않으며, wrapper 집계 탭은 "사용자 함수"로 표시하고 변경을 막습니다.
|
|
1408
|
+
- **선택 영역 집계**(`rows.selectionAggregates()`·`selection:changed`의 `aggregates`·컨텍스트 메뉴): 함수형 컬럼은 그 컬럼 셀이 선택된 행들을 **하나의 그룹처럼** 보고 함수를 적용합니다. `columns[]` 항목은 `aggregate: 'custom'`, `value` = 함수 값(`formattedValue`는 그 컬럼 mask 적용), `count` = 숫자 셀 수(기존 의미)입니다. `ctx.sum('x')` 등은 선택 범위에 `x` 셀이 없어도(숨김 컬럼 포함) 선택된 **행** 기준이고 `ctx.group`은 없습니다. 읽을 수 있는 조합은 고정 합계 행과 같이 컬럼 정의의 문자열 `aggregate`이며, 미선언 조합은 `console.error`(PS312) 후 빈 값입니다. 화면 밖 선택 행·그룹/트리 모드(데이터 행만)·Worker 모드 모두 같은 값이고, 컬럼 전체 선택(Ctrl+Space/Ctrl+A)은 표시 중인 전체 데이터 행으로 계산합니다(이때만 행을 한 번 받아 옵니다). `total`(합계·평균·개수·최소·최대)은 기존대로 **선택 셀 값** 기준입니다. wrapper 컨텍스트 메뉴는 `custom`에 함수 라벨 없이 `헤더 값`만 표시합니다.
|
|
1409
|
+
- 그룹 정렬 `basis: 'aggregate'`와 그룹 결과 필터(`basis: 'aggregate'`·수식 `GROUP(id, "aggregate", by)`)는 `rowGroupConfig.aggregates`의 **함수형 항목도** 공개 ID로 참조할 수 있습니다. 엔진은 함수를 실행하지 않으므로 메인 스레드가 그룹별 함수 값을 계산해 엔진에 다시 질의합니다(필터 참조 시 필터 전 snapshot A 값 1회 + 정렬 참조 시 1회 추가 질의 — 대용량에서 그만큼 느려집니다). 오름/내림·null 위치(오름차순 맨 앞)는 문자열 집계 정렬과 같고, Main/Worker·lazy/dense 결과가 같으며 접기/펼치기·필터·편집 뒤에도 유지됩니다. 컬럼 정의 `aggregate`(자기 칸 집계)는 정렬·필터 ID로 참조할 수 없습니다(문자열·함수 모두 기존과 같음).
|
|
1410
|
+
- `scope`가 붙은 `rowGroupConfig.aggregates` 항목이 있어도 그룹행 자기 칸 집계(컬럼 정의 `aggregate` — 문자열·함수)는 모든 레벨에 남습니다. scope는 배너 집계의 표시 레벨만 정합니다(자기 칸 컬럼의 `rowGroupConfig` 항목도 scope 단계 배너에만 나옵니다).
|
|
1411
|
+
|
|
1412
|
+
```ts
|
|
1413
|
+
// 원본(Wijmo) 납기준수율: 그룹 합계끼리 나눈 가중 비율 (A그룹 (100,90),(300,150) → 60, 평균이면 70)
|
|
1414
|
+
fields: [
|
|
1415
|
+
{ id: 'demandQty', dataType: 'number', aggregate: 'sum', unmergeOnGroupRow: true },
|
|
1416
|
+
{ id: 'onTimeQty', dataType: 'number', aggregate: 'sum', hidden: true },
|
|
1417
|
+
{ id: 'onTimeRatio', dataType: 'number', unmergeOnGroupRow: true,
|
|
1418
|
+
aggregate: (v, { sum }) => sum('demandQty') ? sum('onTimeQty') / sum('demandQty') * 100 : null },
|
|
1419
|
+
],
|
|
1420
|
+
summaryRow: 'pinnedBottom', // 고정 합계 행도 전체 Σ/Σ
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1253
1423
|
#### 그룹 집계 식별자
|
|
1254
1424
|
|
|
1255
1425
|
`RowGroupConfig.aggregates`의 각 항목에는 선택적인 `id`를 지정할 수 있습니다.
|
|
@@ -1433,7 +1603,8 @@ wrapper 정렬 패널의 **그룹 정렬 → 정렬 기준**에서도
|
|
|
1433
1603
|
| `getDataMatrix(options?)` | `Promise<DataMatrix \| null>` | 피벗 결과를 2D 표로 변환 |
|
|
1434
1604
|
| `getPivotMeta()` | `PivotMeta \| null` | 읽기 전용 경량 메타 스냅샷 (필드/행수/열수) |
|
|
1435
1605
|
| `getSeriesData(options?)` | `Promise<PivotSeriesData>` | 차트/대시보드용 그룹 시리즈 데이터 |
|
|
1436
|
-
| `getRowHeaderPath(rowHeaders, categoryField?)` | `string` | 행 rowHeaders → 행 헤더 경로(= 차트 카테고리
|
|
1606
|
+
| `getRowHeaderPath(rowHeaders, categoryField?)` | `string` | 행 rowHeaders → 행 헤더 경로(= 차트 카테고리 표시 이름). `getSeriesData`와 동일 규칙(SSOT). 최상위 행 필드부터 범주 필드까지 " › " 로 이음(첫 단계 필드는 값 그대로, 중간 단계도 "서울 › 1공장" 처럼 부모 포함). (sync) |
|
|
1607
|
+
| `getRowHeaderKey(rowHeaders, categoryField?)` | `string` | 같은 경로의 식별 키(= `getSeriesData` `categories[].key`) — 경로 값을 `'|'` 로 잇고 값 안의 `'|'`·`'\'` 는 `'\'` 로 이스케이프(`'A\|B|1공장'`). (sync) |
|
|
1437
1608
|
|
|
1438
1609
|
#### getFieldValues
|
|
1439
1610
|
|
|
@@ -1481,7 +1652,33 @@ await grid.pivot.getDataMatrix({ excludeStructuralColumns: true });
|
|
|
1481
1652
|
| `subtotals` | `'all' \| 'visible' \| 'none'` | `'visible'` | 소계 행 노출 (all=숨김 포함 raw, visible=토글 ON만, none=제외) |
|
|
1482
1653
|
| `grandTotals` | `'all' \| 'visible' \| 'none'` | `'visible'` | 총합계 행 노출 (값 의미는 `subtotals` 와 동일) |
|
|
1483
1654
|
| `excludeStructuralColumns` | `boolean` | `false` | 열 소계/총합계 컬럼 제외 |
|
|
1484
|
-
| `formatted` | `boolean` | `false` |
|
|
1655
|
+
| `formatted` | `boolean` | `false` | 화면 셀과 같은 표시 문자열로 반환 — 값 칸은 값 필드 mask/decimalFormat/showZeros(빈 칸은 `''`, showZeros 면 `'0'`), 행 헤더 칸은 화면 표시값(number 필드는 천 단위 등). `headers`/`columns`/`rowHeaderValues` 는 원본 그대로 |
|
|
1656
|
+
|
|
1657
|
+
**칸 정체 메타 `columns` / 행 필드 값 `rowHeaderValues`** — `headers` 는 표시 문자열뿐이라 행 필드 칸과 값 칸의 경계, 값 칸의 열 경로·값 필드를 알 수 없습니다. `columns` 는 `headers`·`rows[i]` 와 **같은 순서·길이**(옵션 적용 후)로 각 칸의 정체를 줍니다. 기존 `headers`/`rows`/`rowTypes` 는 그대로입니다.
|
|
1658
|
+
|
|
1659
|
+
```typescript
|
|
1660
|
+
// 행 필드 region, item / 열 필드 year, quarter(열 소계·총합계 ON) / 값 qty 합계·최대(같은 field 두 번)
|
|
1661
|
+
const m = await grid.pivot.getDataMatrix();
|
|
1662
|
+
m.columns;
|
|
1663
|
+
// → [
|
|
1664
|
+
// { kind: 'row', fieldId: 'region' },
|
|
1665
|
+
// { kind: 'row', fieldId: 'item' },
|
|
1666
|
+
// { kind: 'value', path: { year: '2024', quarter: 'Q1' }, valueField: 'qtySum', structural: false },
|
|
1667
|
+
// { kind: 'value', path: { year: '2024', quarter: 'Q1' }, valueField: 'qtyMax', structural: false },
|
|
1668
|
+
// ...
|
|
1669
|
+
// { kind: 'value', path: { year: '2024' }, valueField: 'qtySum', structural: true }, // 2024 열 소계
|
|
1670
|
+
// ...
|
|
1671
|
+
// { kind: 'value', path: {}, valueField: 'qtySum', structural: true }, // 열 총합계
|
|
1672
|
+
// ]
|
|
1673
|
+
m.rowHeaderValues[0]; // → { region: '서울', item: '노트북' }
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
| 항목 | 설명 |
|
|
1677
|
+
|------|------|
|
|
1678
|
+
| `{ kind: 'row', fieldId }` | 행 필드 칸(tabular 레이아웃). `fieldId` = `getPivotMeta().rowFields[].id` |
|
|
1679
|
+
| `{ kind: 'outline', fieldIds }` | `rowDisplayMode: 'outline'` 의 합쳐진 행 헤더 칸 하나(모든 행 필드). `rows` 의 이 칸 값은 화면 outline 라벨(data 행 = 맨 하위 필드 값, 소계 = 그 레벨 필드 값, 총합계 = 총합계 라벨). 필드별 값은 `rowHeaderValues` 사용 |
|
|
1680
|
+
| `{ kind: 'value', path, valueField, structural }` | 값 칸. `path` = 열 필드 id → **열 헤더 키 문자열**(숫자 열 값도 `'2024'` 처럼 문자열), 열 필드가 없으면 `{}`. 열 소계는 덮는 상위 레벨까지만, 열 총합계는 `{}`. `valueField` = `getPivotMeta().valueFields[].id`(같은 field 중복 배치도 구분). `structural` = 열 소계/총합계 |
|
|
1681
|
+
| `rowHeaderValues[i]` | 행 i 의 행 필드 id → 값(레이아웃 무관). 소계 행은 자기 레벨까지 그룹 값, 바로 아래 필드는 소계 라벨(locale, 예 `'Subtotal'`/`'소계'`), 그 아래는 `''`. 총합계 행은 첫 필드가 총합계 라벨 |
|
|
1485
1682
|
|
|
1486
1683
|
#### getPivotMeta / getSeriesData
|
|
1487
1684
|
|
|
@@ -1498,7 +1695,9 @@ const series = await grid.pivot.getSeriesData({
|
|
|
1498
1695
|
subtotals: 'all', // 기본 'all' — 부모 카테고리 차트가 소계 토글 무관 동작
|
|
1499
1696
|
grandTotals: 'none', // 기본 'none' — 총합계 카테고리는 'all'/'visible' 지정 시 추가
|
|
1500
1697
|
});
|
|
1501
|
-
// → { xLabels: ['Q1','Q2','Q3','Q4'], categories: [{ name: '서울', series: [{ name:'sales', data:[...] }, ...] }, ...] }
|
|
1698
|
+
// → { xLabels: ['Q1','Q2','Q3','Q4'], categories: [{ name: '서울', key: '서울', path: ['서울'], series: [{ name:'sales', data:[...] }, ...] }, ...] }
|
|
1699
|
+
// 범주: name = 최상위 행 필드부터 범주 필드까지 " › " (중간 단계 'plant' → '서울 › 1공장' / '부산 › 1공장' 별도 범주),
|
|
1700
|
+
// key = path 를 '|' 로 이음('|'·'\' 이스케이프, getRowHeaderKey), path = 경로 값 배열. 총합계는 key '__grandtotal__', path []
|
|
1502
1701
|
```
|
|
1503
1702
|
|
|
1504
1703
|
---
|
|
@@ -1655,12 +1854,12 @@ interface InvalidCell {
|
|
|
1655
1854
|
|
|
1656
1855
|
| 필드 | 타입 | 기본값 | 설명 |
|
|
1657
1856
|
|------|------|--------|------|
|
|
1658
|
-
| `scope` | `'all' \| 'visible' \| 'selected'` | `'all'` | 데이터
|
|
1857
|
+
| `scope` | `'all' \| 'visible' \| 'selected'` | `'all'` | 데이터 범위. 행 순서는 [화면 순서](#내보내기-행-순서) |
|
|
1659
1858
|
| `columns` | `string[]` | 보이는 컬럼 전체 | 특정 컬럼 ID만 |
|
|
1660
1859
|
| `includeHeaders` | `boolean` | `true` | 헤더 행 포함 |
|
|
1661
1860
|
| `sheetName` | `string` | `'Sheet1'` | 시트 이름 |
|
|
1662
|
-
| `preserveFormatting` | `boolean` | `false` |
|
|
1663
|
-
| `
|
|
1861
|
+
| `preserveFormatting` | `boolean` | `false` | 숫자 decimalFormat 및 구조화된 날짜 mask → 컬럼별 Excel numFmt 매핑 |
|
|
1862
|
+
| `structuralPinnedRows` | `'exclude' \| 'include'` | `'exclude'` | flat XLSX의 구조적 고정 행 포함 여부. 범위/제외 규칙은 [아래 설명](#structural-pinned-행-xlsx-export) 참조 |
|
|
1664
1863
|
| `chunkSize` | `number` | `10000` | 청크당 행 수 |
|
|
1665
1864
|
| `compression` | `'deflate' \| 'store'` | `'deflate'` | 압축 방식 |
|
|
1666
1865
|
| `freezeHeader` | `boolean` | `false` | 헤더 행 고정 (freeze pane) |
|
|
@@ -1671,6 +1870,43 @@ interface InvalidCell {
|
|
|
1671
1870
|
| `signal` | `AbortSignal` | — | 취소 signal |
|
|
1672
1871
|
| `onProgress` | `(ExportProgress) => void` | — | 진행률 콜백 |
|
|
1673
1872
|
|
|
1873
|
+
|
|
1874
|
+
**날짜 표시 서식:** 공개 export 옵션 `dateFormat`은 제거되었습니다(날짜 편집기의 `dateFormat`은 별개이며 유지). `preserveFormatting: true`이면 `dataType: 'date'` 컬럼의 `{ type: 'date', pattern: 'YYYY-MM-DD HH:mm:ss' }` mask를 Excel 서식으로 변환합니다. 미지정/`false`, 날짜 mask 없음 또는 지원 불가 패턴이면 `yyyy-mm-dd`를 사용합니다. 지원 불가 패턴은 기존 진단 경로의 `PS005` 경고를 냅니다.
|
|
1875
|
+
|
|
1876
|
+
- 지원 토큰: `YYYY`/`yyyy`, `YY`/`yy`, `MM`(월), `DD`/`dd`, `HH`(24시간), `hh` + `A`/`a`(12시간), `mm`(분), `ss`. 분은 시간 토큰 바로 뒤 또는 초 토큰 바로 앞의 Excel 시간 문맥이어야 합니다.
|
|
1877
|
+
- 예: `YYYY/MM/DD` → `yyyy/mm/dd`, `HH:mm:ss` → `hh:mm:ss`, `hh:mm:ss A` → `hh:mm:ss AM/PM`. 구분자, 한글 단위, 따옴표 자체는 리터럴로 보존·escape합니다. 따옴표/대괄호는 별도 토큰 인용 문법이 아닙니다.
|
|
1878
|
+
- 제한: 토큰 반복/별칭 혼합, 밀리초·요일·월 이름·경과 시간, 단독 분, `A` 없는 `hh`, `HH` + `A`, 임의 영문 리터럴, mask `prefix`/`suffix`는 지원하지 않습니다. 전체 패턴을 기본 날짜 서식으로 폴백하며 문자열 변환으로 가장하지 않습니다.
|
|
1879
|
+
- 값은 날짜 serial 또는 원본 숫자로 저장하고 표시만 바꿉니다. 날짜 전용 표시도 시간·밀리초를 자르지 않습니다. 기존 `timeZone` 및 import 계약을 유지하며 ISO 원문/오프셋 표기 보존을 뜻하지 않습니다. 숫자 반올림·정수 스타일 정책도 그대로입니다.
|
|
1880
|
+
- 일반/피벗 export는 실제 컬럼의 구조화된 날짜 mask를 사용합니다. 내장 피벗 date wrapper도 원본값 병기/파생 값 변환이 없는 경우만 변환합니다. 사용자 함수/보안 mask 정책은 변경하지 않습니다. mask가 없는 피벗 행 헤더에는 기본 서식을 사용합니다.
|
|
1881
|
+
- `exportSource`는 원본 `fields`의 날짜 mask가 존재할 때만 적용하며 원본 키 헤더·숨김 필드·중복 제거 정책을 유지합니다. 피벗 값 필드 mask를 원본 컬럼에 임의로 추론하지 않습니다. 날짜처럼 보이는 문자열 컬럼을 날짜로 승격하지 않습니다.
|
|
1882
|
+
- 변환한 서식은 직렬화 가능한 메타데이터이므로 메인/Worker XLSX 경로가 동일하게 처리합니다. 사용자 지정 날짜/시간 스타일은 import에서도 날짜로 인식합니다.
|
|
1883
|
+
|
|
1884
|
+
##### 내보내기 행 순서
|
|
1885
|
+
|
|
1886
|
+
XLSX·CSV의 행 순서는 **화면에 보이는 순서**입니다(원본 Wijmo와 같음).
|
|
1887
|
+
- 정렬·필터 중에는 그 결과 순서입니다. 커밋 전 드래그 재정렬(`reorderableRows`)은 화면처럼 재정렬 순서로 나갑니다. 디테일을 펼친 상태도 같고, 디테일 행 자체는 내보내지 않습니다. 정렬·필터가 켜져 있으면 화면처럼 재정렬을 무시합니다.
|
|
1888
|
+
- 재정렬 상태의 병합(`preserveMerges`)은 화면 순서에서 이웃한, 같은 병합 블록의 행끼리만 다시 묶습니다. 예: `dept` 병합 A=[id1, id2], E=[id9, id10]에서 id10을 맨 위로 옮기면 A는 그대로 병합되고, E는 행이 떨어져 병합 없이 나갑니다. 서로 다른 병합 블록에서 온 행끼리는 같은 값이라도 합치지 않습니다. 화면의 병합 표시(병합 칸·선택·키보드·복사)도 커밋 전 재정렬 상태에서 같은 규칙을 따르므로 화면과 파일의 병합이 같습니다. 재정렬 상태에서는 엔진 가시 행 전체를 한 번 읽습니다(전체 복사 Ctrl+A와 같은 비용).
|
|
1889
|
+
- `scope: 'selected'`는 선택한 순서가 아니라 화면 순서입니다(상단 고정 → 본문 → 하단 고정, 그룹·트리·재정렬 반영). 예: id7을 선택한 뒤 id2를 Ctrl+클릭해도 [2, 7] 순서로 나갑니다. 화면에 없는 선택 행(접힌 그룹 안)은 뒤에 선택 순서대로 붙습니다.
|
|
1890
|
+
- 커밋 전에 추가하거나 수정한 행은 바뀐 값으로 나갑니다. 커밋 전에 삭제한 행은 화면에 삭제 표시로 남아 있으므로 파일에도 들어갑니다.
|
|
1891
|
+
|
|
1892
|
+
##### 고정 행 XLSX·CSV export (`includePinnedRows`)
|
|
1893
|
+
|
|
1894
|
+
`includePinnedRows?: boolean`의 기본값은 `false`이며 기존 출력을 그대로 유지합니다. `true`면 일반 flat XLSX·CSV(`saveCsvToFile`)에 화면에 보이는 고정 행을 화면과 같은 순서로 추가합니다: 상단 [구조 고정..., 데이터 고정...] → 본문 → 하단 [데이터 고정..., 구조 고정...]. **래퍼 내보내기 메뉴(컨텍스트 메뉴 XLSX/CSV, Vue `runClientExport`)는 `true`를 넘깁니다**(화면에 보이는 그대로). 끄려면 `contextMenuConfig.clientExportOptions: { includePinnedRows: false }`. 구조 고정 행은 `addPinnedTop/Bottom`·`summaryRow` 등으로 추가한 행, 데이터 고정 행은 `setProps({ pinned })` 또는 `rows.pinTop*`/`pinBottom*`으로 본문에서 옮긴 행입니다(본문에는 없으므로 한 번만 나갑니다). `scope: 'selected'`에서는 선택된 고정 행만 포함하며 선택되지 않은 합계 행을 자동 추가하지 않습니다. 기본(`false`)의 selected 경로는 기존 raw pinned 데이터 fallback을 그대로 유지합니다.
|
|
1895
|
+
`setMergeConfig({ includePinned: true })`의 고정 행 병합은 `includePinnedRows: true`일 때 화면과 같은 범위로 엑셀 병합 셀(`mergeCells`)이 됩니다(`preserveMerges: false`면 제외). 데이터 고정 행에 걸친 범위도 그대로 나갑니다. `scope: 'selected'`로 빠진 행에 걸친 범위는 내보내는 연속 행으로 잘리고, 중간 행이 빠지거나 내보내는 열이 연속이 아니면(`columns` 옵션 등) 그 병합은 생략됩니다.
|
|
1896
|
+
고정 행 데이터의 `__merge` 같은 사용자 필드는 `includePinned` 여부와 관계없이 병합으로 해석하지 않습니다(고정 행 병합은 위 설정에서만 나옴).
|
|
1897
|
+
|
|
1898
|
+
- `exportSource: true`는 항상 원본 데이터만 내보냅니다. 피벗 결과 export와 CSV는 이 옵션을 무시합니다.
|
|
1899
|
+
- 구조 고정 행의 집계는 현재 supplier 집계 요청 완료를 기다린 뒤 **숫자 등 원래 타입**으로 내보냅니다. 같은 `field`를 쓰는 서로 다른 컬럼의 sum/avg도 컬럼 ID별로 분리됩니다. 원본 row 데이터와 집계값을 단순 spread로 합치지 않습니다.
|
|
1900
|
+
- 구조 고정 행의 라벨은 pinned 행의 `data`에 **데이터 키(field)** 로 선언합니다. 예: `{ data: { name: '합계' }, aggregates: [{ columnId: 'total', func: 'sum' }] }`. 집계가 선언된 컬럼은 그 집계값이 우선합니다.
|
|
1901
|
+
- 데이터 고정 행은 본문 행과 같은 셀 값 변환(날짜·숫자, `preserveFormatting` 서식)으로 현재 데이터(편집 반영)를 씁니다.
|
|
1902
|
+
- CSV도 같은 행 순서로 나갑니다. 셀은 본문 CSV와 같은 화면 표시 서식(날짜·숫자·`mask`)이고, 구조 고정 행(합계·`summaryRow`)은 컬럼 ID별 집계값을 같은 서식으로 씁니다. CSV에는 병합이 없습니다.
|
|
1903
|
+
- `formatRow`의 DOM 텍스트, HTML, aggregate formatter 결과는 추출하지 않습니다. `AggregateConfig.label`에 새로운 의미를 부여하지 않습니다. 일반 `columns`, `includeHeaders`, `preserveFormatting` 옵션은 그대로 적용됩니다.
|
|
1904
|
+
|
|
1905
|
+
```ts
|
|
1906
|
+
await grid.io.saveExcelToFile({ includePinnedRows: true });
|
|
1907
|
+
await grid.io.saveCsvToFile({ includePinnedRows: true });
|
|
1908
|
+
```
|
|
1909
|
+
|
|
1674
1910
|
##### 원본 데이터 export 옵션
|
|
1675
1911
|
|
|
1676
1912
|
| 필드 | 타입 | 기본값 | 설명 |
|
|
@@ -1696,7 +1932,7 @@ interface InvalidCell {
|
|
|
1696
1932
|
|
|
1697
1933
|
| 필드 | 타입 | 기본값 | 설명 |
|
|
1698
1934
|
|------|------|--------|------|
|
|
1699
|
-
| `groupRowMode` | `'include' \| 'visible' \| 'dataOnly'` | `'include'` | 플랫 행 그룹화(`grid.rowGroup.setConfig`) 활성 시 export 동작. `'include'`: 접힘 상태 무시하고 모든 그룹 행 + data를 펼친 상태로 export. `'visible'`: 현재 접힘 상태 그대로 (collapsed 자식 제외). `'dataOnly'`: 그룹 행 제외하고 data
|
|
1935
|
+
| `groupRowMode` | `'include' \| 'visible' \| 'dataOnly'` | `'include'` | 플랫 행 그룹화(`grid.rowGroup.setConfig`) 활성 시 export 동작. `'include'`: 접힘 상태 무시하고 모든 그룹 행 + data를 펼친 상태로 export. `'visible'`: 현재 접힘 상태 그대로 (collapsed 자식 제외). `'dataOnly'`: 그룹 행 제외하고 data 행만. 그룹 행·순서·개수·집계는 화면과 같다(행 필터·정렬, 그룹 정렬·그룹 결과 필터 반영). 내보내기는 화면 접힘 상태를 바꾸지 않는다. 그룹 행 집계도 화면 자리 그대로 나간다: 그룹 행에서 자기 칸을 갖는 컬럼(고정 컬럼, center 양 끝의 연속 `unmergeOnGroupRow`)의 컬럼 `aggregate` 값은 그 컬럼 칸에(숫자 셀, 서식·mask 는 본문 칸과 같다), 배너 글자(`rowGroupConfig.aggregates` — 자기 칸 컬럼 항목 포함)는 그룹 라벨 칸에 화면 배너와 같은 글자로 ` · ` 를 붙여 나간다(예: `A (3) · Qty (Sum): 6,000 · Amt (Max): 7`, `scope`·숨김 컬럼 규칙도 화면과 같다). 자기 칸이 없는 컬럼의 컬럼 `aggregate`(함수형 포함)는 화면처럼 나가지 않는다. GroupTemplate 으로 배너를 바꾼 화면도 파일은 기본 배너 글자를 쓴다 |
|
|
1700
1936
|
| `groupOutline` | `boolean` | `true` | Excel 네이티브 outline (+/- 버튼) 생성. 그룹 행을 summary row로, 데이터 행에 `outlineLevel`을 부여. `groupRowMode: 'dataOnly'`이면 무시. 중첩 그룹(`columns: ['dept','role']`)은 depth별 outlineLevel 자동 계산 |
|
|
1701
1937
|
|
|
1702
1938
|
##### 트리 그리드 export 옵션
|
|
@@ -1905,6 +2141,7 @@ console.log(`변경 예상: +${summary.addedCount} ~${summary.updatedCount} -${s
|
|
|
1905
2141
|
```typescript
|
|
1906
2142
|
// 초기 설정 (생성자)
|
|
1907
2143
|
new PureSheet(container, {
|
|
2144
|
+
fields, enableReferCache: true,
|
|
1908
2145
|
dragDropConfig: {
|
|
1909
2146
|
rowOrderField: { field: 'displayOrder', strategy: 'dense' },
|
|
1910
2147
|
mutateOnDirty: (row) => ({ updatedAt: new Date().toISOString() }),
|
|
@@ -1936,6 +2173,7 @@ if (grid.dragDrop.isActive()) {
|
|
|
1936
2173
|
|
|
1937
2174
|
```typescript
|
|
1938
2175
|
new PureSheet(container, {
|
|
2176
|
+
fields, keyFields: ['id'],
|
|
1939
2177
|
treeConfig: { idField: 'id', parentField: 'parentId', treeColumn: 'name' },
|
|
1940
2178
|
dragDropConfig: {
|
|
1941
2179
|
reorderableRows: true,
|
|
@@ -2249,7 +2487,7 @@ grid.off('cell:click', handler);
|
|
|
2249
2487
|
|
|
2250
2488
|
| 이벤트 | 페이로드 | 설명 |
|
|
2251
2489
|
|--------|---------|------|
|
|
2252
|
-
| `row:click` | `{ row, rowIndex, event }` | 행
|
|
2490
|
+
| `row:click` | `{ row, rowId, viewIndex, sourceIndex, rowIndex, event }` | 행 클릭. 좌표는 아래 **셀·행 이벤트 좌표** 참조 (`rowIndex`는 하위호환 값 — 원본 위치 아님) |
|
|
2253
2491
|
| `row:dblclick` | `{ row, rowIndex, event }` | (예약) 현재 코드 경로에서는 발행되지 않음 — `cell:dblclick`을 사용하세요 |
|
|
2254
2492
|
| `row:pinned` | `{ row, position }` | 행 고정 |
|
|
2255
2493
|
| `row:unpinned` | `{ rowId, position? }` | 행 고정 해제 |
|
|
@@ -2271,8 +2509,8 @@ grid.off('cell:click', handler);
|
|
|
2271
2509
|
|
|
2272
2510
|
| 이벤트 | 페이로드 | 설명 |
|
|
2273
2511
|
|--------|---------|------|
|
|
2274
|
-
| `cell:click` | `{ row, columnId, value, event }` | 셀
|
|
2275
|
-
| `cell:dblclick` | `{ row, columnId, value, event }` | 셀 더블클릭 |
|
|
2512
|
+
| `cell:click` | `{ row, rowId, viewIndex, sourceIndex, columnId, value, event }` | 셀 클릭. `row`는 클릭한 행 데이터(피벗은 화면의 피벗 행) |
|
|
2513
|
+
| `cell:dblclick` | `{ row, rowId, viewIndex, sourceIndex, columnId, value, event }` | 셀 더블클릭 |
|
|
2276
2514
|
| `cell:change` | `{ rowIndex, rowId, columnId, oldValue, newValue }` | 에디터 편집에 따른 셀 값 변경. API·붙여넣기·undo/redo를 모두 관찰하는 이벤트가 아님. `rowId`는 정렬/필터 무관 안정 식별자 |
|
|
2277
2515
|
| `data:changed` | `DataMutationEvent` | ChangeTracker에 반영된 셀 데이터 변경. 에디터 생명주기와 독립적인 추가 이벤트 (아래 계약 참조) |
|
|
2278
2516
|
| `changes:changed` | `{ hasChanges, addedCount, deletedCount }` | 기존 dirty 상태 요약. 신규 행 셀 수정도 발행하며 상세 값은 `data:changed`로 구독 |
|
|
@@ -2281,6 +2519,17 @@ grid.off('cell:click', handler);
|
|
|
2281
2519
|
| `cell:editCommitted` | `{ rowIndex, rowId, dataIndex, columnId, oldValue, newValue }` | 편집 커밋 완료. `rowId` 포함 (위와 동일) — boolean 셀 즉시 토글도 이 이벤트로 발화되어 체크박스 cascade 트리거로 쓰인다 |
|
|
2282
2520
|
| `cell:tooltip` | `CellTooltipPayload` (아래 참조) | 셀·컬럼 헤더 호버 시 툴팁 표시 요청 (`tooltipMode` 로 발행 제어) |
|
|
2283
2521
|
|
|
2522
|
+
**셀·행 이벤트 좌표** (`cell:click`·`cell:dblclick`·`row:click`)
|
|
2523
|
+
|
|
2524
|
+
| 필드 | 의미 |
|
|
2525
|
+
|------|------|
|
|
2526
|
+
| `rowId` | 행 ID(keyFields 기준) — 행 식별은 이 값을 권장 |
|
|
2527
|
+
| `viewIndex` | 화면 순번. 그룹 배너·마스터-디테일 행 포함, 고정 행은 음수 |
|
|
2528
|
+
| `sourceIndex` | **원본 데이터 배열 위치** — `rows.getByIndex(sourceIndex)`·`changes.*ByIndex`와 같은 좌표. 정렬·필터·행 고정·그룹·트리와 무관. 원본에 없는 행(미저장 추가 행)·피벗 행·알 수 없을 때(Worker 캐시 미스 등)는 `undefined` |
|
|
2529
|
+
| `rowIndex` (`row:click`만) | 하위호환 값: 평면=필터·정렬 후 순번, 그룹·트리=flat 인덱스. 원본 위치가 아님 |
|
|
2530
|
+
|
|
2531
|
+
> 변경: 예전에는 `sourceIndex`에 평면=화면(필터·정렬 후) 순번, 그룹·트리=flat 인덱스를 그대로 담았습니다. 이 값을 `rows.getByIndex()` 등에 넘기던 코드는 정렬·필터·행 고정·그룹·트리에서 다른 행을 보고 있었으며, 이제 같은 코드가 올바른 행을 봅니다. 화면 순번이 필요하면 `viewIndex`를 쓰세요. `cell:beforeEdit`·`cell:editCommitting`·`cell:editCommitted`·`contextmenu`의 `dataIndex`는 바뀌지 않았고 원본 위치가 아닙니다(평면=필터·정렬 후 순번, 그룹·트리=flat 인덱스) — 원본 행이 필요하면 `rowId`로 찾으세요.
|
|
2532
|
+
|
|
2284
2533
|
**`DataMutationEvent` — 셀 데이터 변경 계약**
|
|
2285
2534
|
|
|
2286
2535
|
```ts
|
|
@@ -2367,7 +2616,7 @@ grid.on('cell:tooltip', ({ element, cellRect, value, errors, hasError }) => {
|
|
|
2367
2616
|
|
|
2368
2617
|
| 이벤트 | 페이로드 | 설명 |
|
|
2369
2618
|
|--------|---------|------|
|
|
2370
|
-
| `selection:changed` | `{ selectedRows, selectedCells }` | 선택
|
|
2619
|
+
| `selection:changed` | `{ selectedRows, selectedCells, selectedGroups, aggregates }` | 선택 변경. `selectedGroups` = `grid.selection.getSelectedGroups()`와 같은 값(`rowGroupConfig.selectable`이 꺼져 있으면 `[]`) |
|
|
2371
2620
|
|
|
2372
2621
|
#### 컬럼
|
|
2373
2622
|
|
|
@@ -2443,6 +2692,8 @@ grid.on('cell:tooltip', ({ element, cellRect, value, errors, hasError }) => {
|
|
|
2443
2692
|
>
|
|
2444
2693
|
> `describeChange`를 구현하지 않은 command는 `changes:undone`/`changes:redone`을 **발화하지 않습니다** (`change`가 non-null 계약).
|
|
2445
2694
|
>
|
|
2695
|
+
> **화면 전용 고정 행 편집**(`rows.addPinnedTop/Bottom({ id, data })` 행)도 같은 UndoStack에 기록되며 `cell-edit`에 `screenOnly: true`가 붙어 옵니다(`rowId`는 그 고정 행 id, 변경 추적 제외 — 서버 동기화에서 거르세요). 그 행의 고정·해제도 같은 스택에 기록되어 `add-row`(고정) / `delete-row`(해제)에 `screenOnly: true`와 `pinned: 'top' | 'bottom'`이 붙어 옵니다(`index`는 그 고정 영역의 화면 전용 고정 행 순서, `clearPinned` 여러 행은 `batch`). 그래서 편집 → 해제 → Ctrl+Z(재고정) → Ctrl+Z(편집 취소) 순서가 성립합니다.
|
|
2696
|
+
>
|
|
2446
2697
|
> **⚠️ 실질 변경(tree/flat reorder 등)은 거의 항상 `kind: 'batch'`로 래핑되어 옵니다.** `TreeReorderCoordinator`가 항상 batch로 감싸고 mutator가 추가 cell-edit(`updatedAt` 등)까지 넣기 때문입니다. `e.change.kind === 'tree-reorder'`처럼 **직접 비교하면 대부분 no-op**(조용한 실패)이 됩니다. 반드시 `@solasheet/core`의 `iterateLeafChanges(e.change)` / `findAllChangesByKind(e.change, kind)` / `findChangeByKind(e.change, kind)`로 batch를 평탄화한 뒤 분기하세요. 같은 kind가 여러 개 담길 수 있으므로(여러 tree-reorder + cell-edit) 하나만 필요한 게 아니면 `findAllChangesByKind`로 전부 수집해 유실을 막습니다.
|
|
2447
2698
|
>
|
|
2448
2699
|
> **⚠️ `batch.children`은 이미 direction-ordered입니다.** `BatchCommand.describeChange`가 undo 방향일 때 내부에서 이미 역순 정렬을 마칩니다. 소비처가 undo에서 다시 역순으로 순회하면 **이중 reverse**가 되어 순서가 깨집니다. 헬퍼가 방출한 순서를 **그대로 순차(pre-order) 적용**하세요.
|
|
@@ -2597,7 +2848,7 @@ interface MasterDetailConfig {
|
|
|
2597
2848
|
minHeight?: number; // 최소 높이 (기본: 100)
|
|
2598
2849
|
maxHeight?: number; // 최대 높이
|
|
2599
2850
|
singleExpand?: boolean; // 단일 확장 모드 (기본: false)
|
|
2600
|
-
keepExpandedOnDataChange?: boolean; // 데이터 변경 시
|
|
2851
|
+
keepExpandedOnDataChange?: boolean; // 데이터 변경 시 펼친 디테일 유지 (기본: true)
|
|
2601
2852
|
isExpandable?: (data: Row, dataIndex: number) => boolean; // 확장 가능 조건
|
|
2602
2853
|
expandColumn?: string; // 확장 아이콘 컬럼 (기본: 첫 컬럼)
|
|
2603
2854
|
keepMasterDetailRows?: boolean; // DOM 캐시 유지 (기본: true)
|
|
@@ -2605,6 +2856,20 @@ interface MasterDetailConfig {
|
|
|
2605
2856
|
}
|
|
2606
2857
|
```
|
|
2607
2858
|
|
|
2859
|
+
**펼친 디테일 유지 (`keepExpandedOnDataChange`, 기본 `true`)** — 펼침 상태는 행 키(`keyFields`)로 저장됩니다.
|
|
2860
|
+
|
|
2861
|
+
| 동작 | 기본(`true`) | `false` (예전 기본) |
|
|
2862
|
+
|---|---|---|
|
|
2863
|
+
| 정렬·필터·드래그 재정렬·`changes.commit`·값 편집·`refresh()` | 같은 행은 펼친 채 유지 | 모두 접힘 |
|
|
2864
|
+
| 필터로 숨은 행 | 펼침 기억 → 필터를 풀면 펼친 채 다시 보임 | 접힘 |
|
|
2865
|
+
| `loadData` 재조회 | 같은 키의 행은 펼친 채 유지, 새 데이터에 없는 키는 목록에서 빠짐 | 모두 접힘 |
|
|
2866
|
+
| 커밋된 행 삭제 | 그 행의 키는 목록에서 빠짐 | 모두 접힘 |
|
|
2867
|
+
|
|
2868
|
+
- 유지할 때도 디테일 내용은 `renderer`로 **다시 그립니다**(이전 cleanup 실행 후 호출) — 바뀐 데이터를 반영하기 위해서입니다. 디테일 안의 입력값·스크롤 같은 DOM 상태는 남지 않습니다.
|
|
2869
|
+
- `keyFields` 없이 자동 행 ID(`enableReferCache: true`)를 쓰면 `loadData`마다 ID가 새로 붙으므로 재조회 뒤에는 접힙니다. 재조회 후에도 펼침을 유지하려면 `keyFields`를 지정하세요.
|
|
2870
|
+
- 그룹·트리 노드를 접어 안 보이게 된 행의 디테일은 지금처럼 접힙니다(이 옵션과 무관).
|
|
2871
|
+
- `singleExpand: true`와 함께 쓰면 마지막으로 펼친 한 행만 유지됩니다.
|
|
2872
|
+
|
|
2608
2873
|
### GroupConfig
|
|
2609
2874
|
|
|
2610
2875
|
초기 그리드 생성 시 `FlatModeConfig.rowGroup`으로 전달하는 가벼운 설정 (구 `group` 필드는 deprecated 별칭). *런타임*에는 `grid.rowGroup.setConfig()`로 더 풍부한 [GroupingConfig](#groupingconfig)를 사용합니다.
|
|
@@ -2659,6 +2924,8 @@ interface GroupingConfig {
|
|
|
2659
2924
|
aggregates?: Record<string, AggregateFn>;
|
|
2660
2925
|
/** 기본 접힘 상태 */
|
|
2661
2926
|
defaultCollapsed?: boolean;
|
|
2927
|
+
/** 그룹 배너 선택 허용 (기본 false) — [그룹 배너 선택](#그룹-배너-선택-selectable) */
|
|
2928
|
+
selectable?: boolean;
|
|
2662
2929
|
}
|
|
2663
2930
|
```
|
|
2664
2931
|
|
|
@@ -2667,8 +2934,12 @@ interface GroupingConfig {
|
|
|
2667
2934
|
```typescript
|
|
2668
2935
|
interface EditorConfig {
|
|
2669
2936
|
type: EditorType;
|
|
2670
|
-
// 'text' | 'number' | 'date' | 'select' | 'checkbox'
|
|
2937
|
+
// 'text' | 'number' | 'date' | 'datetime' | 'select' | 'checkbox'
|
|
2671
2938
|
// | 'dropdown' | 'datepicker' | 'multiselect' | 'custom'
|
|
2939
|
+
min?: number; // number 전용, 포함하는 최솟값
|
|
2940
|
+
max?: number; // number 전용, 포함하는 최댓값
|
|
2941
|
+
step?: number; // number 전용, 양수 간격 (기준: min 또는 0)
|
|
2942
|
+
inputMask?: string; // text 전용, 입력 마스크 (예: '99:99') — 표시 mask와 별개
|
|
2672
2943
|
options?: EditorOption[]; // dropdown/select 옵션
|
|
2673
2944
|
searchable?: boolean; // 검색 활성화 (dropdown, multiselect)
|
|
2674
2945
|
placeholder?: string; // placeholder 텍스트
|
|
@@ -2680,6 +2951,68 @@ interface EditorConfig {
|
|
|
2680
2951
|
}
|
|
2681
2952
|
```
|
|
2682
2953
|
|
|
2954
|
+
#### 숫자 입력 제약
|
|
2955
|
+
|
|
2956
|
+
`min`/`max`/`step`은 **`type: 'number'` 전용**입니다. 생략 시 범위 제한이 없고 소수 입력도 허용됩니다(`step` 생략 시 HTML input의 `step='any'`). `step`을 지정하면 `min ?? 0`을 기준으로 그 간격에 맞는 값만 확정할 수 있습니다. 소수 간격도 지원하며 값을 반올림하거나 범위에 맞춰 자동 보정하지 않습니다.
|
|
2957
|
+
|
|
2958
|
+
- 지정한 값은 유한수여야 하고 `min <= max`, `step > 0`이어야 합니다. 잘못된 설정은 **편집 시작 시** `RangeError`를 발생시킵니다.
|
|
2959
|
+
- 입력/붙여넣기 중 오류를 표시하고, 잘못된 값의 Enter/blur 확정을 차단하여 에디터와 기존 데이터를 유지합니다. Escape는 취소합니다.
|
|
2960
|
+
- 빈 입력은 `null`이며 필수 입력을 뜻하지 않습니다. 필수 규칙은 별도 `validation`으로 지정합니다. 브라우저가 `badInput`으로 보고하는 미완성 입력은 빈 값으로 확정하지 않습니다.
|
|
2961
|
+
- 이 옵션은 셀 에디터 제약이며 API 수정·클립보드 import·서버 데이터 전체에 대한 검증 규칙이 아닙니다. 그런 검증은 별도 `validation`/업무 로직에서 처리하세요. `editorResolver`가 반환하는 `EditorConfig`에도 같은 계약이 적용됩니다.
|
|
2962
|
+
|
|
2963
|
+
#### 텍스트 입력 마스크 (`inputMask`)
|
|
2964
|
+
|
|
2965
|
+
`inputMask`는 **`type: 'text'` 전용** 셀 에디터 입력 마스크입니다. 키 입력·에디터 안 붙여넣기·삭제·type-to-edit 첫 글자에 적용되고, 고정 문자를 자동으로 넣은 **문자열**을 확정합니다(`'1230'` 입력 → `'12:30'` 저장, Date 변환 없음). 다른 에디터 타입에서는 무시됩니다.
|
|
2966
|
+
|
|
2967
|
+
> 컬럼 **표시 mask**(`mask: { type: 'string', pattern }`, 토큰 `0`/`A`/`*`)와는 다른 기능·문법입니다. 표시 mask는 셀에 보이는 형식만 바꾸고 값을 바꾸지 않으며, `inputMask`는 편집 중 입력을 제한하고 확정 값에 고정 문자를 포함합니다. 두 옵션은 함께 쓸 수 있습니다.
|
|
2968
|
+
|
|
2969
|
+
| 토큰 | 의미 (Wijmo `InputMask`/`Column.mask`와 같음) |
|
|
2970
|
+
|------|------|
|
|
2971
|
+
| `0` | 필수 숫자 |
|
|
2972
|
+
| `9` | 선택 숫자 — 숫자 또는 공백 |
|
|
2973
|
+
| `L` | 필수 영문자 (ASCII a-z, A-Z) |
|
|
2974
|
+
| `\` | 다음 문자를 고정 문자로 (`\0`, `\A`) |
|
|
2975
|
+
| 그 외 | 고정 문자 — 입력 중 자동 삽입 |
|
|
2976
|
+
|
|
2977
|
+
- 허용되지 않는 문자와 마지막 자리 뒤 문자는 버립니다. 중간 삭제·붙여넣기 뒤에도 남은 문자를 앞에서부터 다시 맞춥니다(`'12:30'`에서 `2` 삭제 → `'13:0'`).
|
|
2978
|
+
- 필수 자리(`0`, `L`)가 빈 값(`'00:00'`에서 `'12:3'`)은 숫자 제약 위반과 같이 오류를 표시하고 Enter/blur 확정을 막습니다. Escape는 취소합니다. 선택 자리(`9`)는 비어도 확정합니다(`'99:99'`에서 `'12:3'` 그대로 확정 — Wijmo도 막지 않음). 빈 입력은 일반 text 에디터처럼 `''`로 확정하고, 편집 전 값을 그대로 닫는 것도 막지 않습니다.
|
|
2979
|
+
- 편집기를 열 때 기존 값도 마스크에 맞춰 보여 줍니다(`'0900'` → `'09:00'`). 그대로 확정하면 그 값으로 바뀝니다(Wijmo 동일).
|
|
2980
|
+
- Wijmo와 달리 빈 자리를 `_` 프롬프트로 채워 보여 주지 않습니다. 값은 앞에서부터 채운 문자열이며, 마지막으로 채운 자리 뒤에 자동 삽입한 고정 문자는 남기지 않습니다(`'12'`는 `'12'`, 직접 친 `'12:'`는 유지).
|
|
2981
|
+
- IME 조합 중에는 텍스트를 건드리지 않고, 조합이 끝나면 한 번 맞춥니다.
|
|
2982
|
+
- 지원하지 않는 Wijmo 토큰(`#`, `A`, `a`, `l`, `K`, `N`, `H`, `<`, `>`, `|`, 전각 토큰)이나 입력 자리가 없는 마스크는 **편집 시작 시** `RangeError`입니다. 고정 문자로 쓰려면 `\`로 이스케이프하세요. Wijmo의 `:`·`/`·`.`·`,`·`$` 지역화 문자 치환은 하지 않고 글자 그대로 씁니다.
|
|
2983
|
+
- validator가 아닙니다. 셀 에디터 입력에만 적용되며 클립보드 셀 붙여넣기·`bulkUpdate`·API 값 변경·서버 데이터는 검사하지 않습니다. 그런 검증은 `validation`/업무 로직에서 처리하세요. `editorResolver`가 반환한 설정에도 적용됩니다.
|
|
2984
|
+
|
|
2985
|
+
```typescript
|
|
2986
|
+
{ id: 'start_break_time', header: '시작 시간',
|
|
2987
|
+
editor: { type: 'text', inputMask: '99:99' } } // Wijmo mask="99:99" 대체
|
|
2988
|
+
```
|
|
2989
|
+
|
|
2990
|
+
#### 날짜·시각 입력
|
|
2991
|
+
|
|
2992
|
+
`type: 'datetime'`은 초 단위 `datetime-local` 입력이며 **`Date | null`** 을 확정합니다. `date`/`datepicker`를 자동으로 대체하지 않으므로 날짜+시각 편집이 필요하면 명시하세요.
|
|
2993
|
+
|
|
2994
|
+
- 그리드 `timeZone`이 있으면 그 zone의 벽시계로 표시·해석하고, 생략하면 브라우저 로컬 zone을 사용합니다. offset 있는 문자열/`Date`의 절대 시각과 offset 없는 문자열의 벽시계 의미, DST `compatible` 해석은 [날짜 시간대 계약](#날짜-시간대-계약-timezone)을 따릅니다.
|
|
2995
|
+
- 빈 값은 `null`, 잘못되거나 미완성인 입력은 확정 차단입니다. 변경하지 않은 원본 `Date`를 확정할 때는 밀리초 및 DST 중복 시각의 원래 instant를 보존합니다. 문자열 입력은 확정 시 `Date`로 변환됩니다.
|
|
2996
|
+
- `min`/`max`/`step`은 datetime 범위/간격 옵션이 아닙니다. `editor.dateFormat`은 기존 datepicker 설정으로 유지되며, datetime 표시 mask나 XLSX export 옵션이 아닙니다.
|
|
2997
|
+
|
|
2998
|
+
```typescript
|
|
2999
|
+
const editorFields: FieldDef[] = [
|
|
3000
|
+
{ id: 'quantity', header: '수량', dataType: 'number',
|
|
3001
|
+
editor: { type: 'number', min: 0, max: 100, step: 0.5 } },
|
|
3002
|
+
{ id: 'scheduledAt', header: '예정 시각', dataType: 'date',
|
|
3003
|
+
editor: { type: 'datetime' },
|
|
3004
|
+
mask: { type: 'date', pattern: 'YYYY-MM-DD HH:mm:ss' } },
|
|
3005
|
+
];
|
|
3006
|
+
const editorGrid = new PureSheet(container, {
|
|
3007
|
+
enableReferCache: true, editable: true, timeZone: 'Asia/Seoul',
|
|
3008
|
+
fields: editorFields,
|
|
3009
|
+
data: [{ quantity: 1, scheduledAt: new Date('2026-08-31T15:12:34Z') }],
|
|
3010
|
+
});
|
|
3011
|
+
await editorGrid.waitForReady();
|
|
3012
|
+
// XLSX 날짜 표시에는 editor.dateFormat이 아니라 mask + preserveFormatting 사용
|
|
3013
|
+
await editorGrid.io.saveExcelToFile({ preserveFormatting: true });
|
|
3014
|
+
```
|
|
3015
|
+
|
|
2683
3016
|
### MaskConfig
|
|
2684
3017
|
|
|
2685
3018
|
5종 discriminated union. 공통 필드(`type`, `pattern`, `prefix?`, `suffix?`) + 타입별 추가 필드.
|
|
@@ -2908,7 +3241,7 @@ interface PivotValueField {
|
|
|
2908
3241
|
| `PinPosition` | `'left' \| 'right' \| 'none'` |
|
|
2909
3242
|
| `RowState` | `'pristine' \| 'added' \| 'modified' \| 'deleted'` |
|
|
2910
3243
|
| `CellState` | `'pristine' \| 'modified'` |
|
|
2911
|
-
| `EditorType` | `'text' \| 'number' \| 'date' \| 'select' \| 'checkbox' \| 'dropdown' \| 'datepicker' \| 'multiselect' \| 'custom'` |
|
|
3244
|
+
| `EditorType` | `'text' \| 'number' \| 'date' \| 'datetime' \| 'select' \| 'checkbox' \| 'dropdown' \| 'datepicker' \| 'multiselect' \| 'custom'` |
|
|
2912
3245
|
| `EngineType` | `'native'` — **`'aq'`(Arquero)는 제거됨.** 단일 엔진화로 유니온에 값이 하나만 남았습니다(ITSM-2026-002190) |
|
|
2913
3246
|
| `TotalPosition` | `'before' \| 'after'` |
|
|
2914
3247
|
|
|
@@ -3390,6 +3723,7 @@ export default class MyLoader extends DataLoader {
|
|
|
3390
3723
|
|
|
3391
3724
|
// 그리드 생성
|
|
3392
3725
|
new PureSheet(container, {
|
|
3726
|
+
enableReferCache: true,
|
|
3393
3727
|
loader: {
|
|
3394
3728
|
module: new URL('./my-loader.ts', import.meta.url),
|
|
3395
3729
|
params: { authToken: 'abc' },
|
|
@@ -3503,11 +3837,15 @@ interface BuiltInMergeConfig {
|
|
|
3503
3837
|
strategy?: 'lazy' | 'eager';
|
|
3504
3838
|
/** Lazy 모드 최대 탐색 깊이 @default 500 */
|
|
3505
3839
|
maxSearchDepth?: number;
|
|
3840
|
+
/** 고정 행(pinnedTop/pinnedBottom)에도 적용 — 각 고정 영역 안에서만 병합 @default false(본문만) */
|
|
3841
|
+
includePinned?: boolean;
|
|
3506
3842
|
}
|
|
3507
3843
|
|
|
3508
3844
|
interface CustomMergeConfig {
|
|
3509
3845
|
type: 'custom';
|
|
3510
3846
|
columns: string[];
|
|
3847
|
+
/** 고정 행에도 콜백 호출(area로 구분) @default false(본문만 — 기존 동작) */
|
|
3848
|
+
includePinned?: boolean;
|
|
3511
3849
|
/** Worker 모드에서 getRowData 행 데이터 버퍼 범위 @default 500 */
|
|
3512
3850
|
maxSearchDepth?: number;
|
|
3513
3851
|
/** 셀별 병합 범위 콜백 (null 반환 = 병합 없음) */
|
|
@@ -3521,8 +3859,11 @@ interface CustomMergeConfig {
|
|
|
3521
3859
|
// 가로: colIndex / columnsInOrder / totalColumnCount
|
|
3522
3860
|
// 2D 공용: getCellValue(rowIndex, columnId)
|
|
3523
3861
|
interface MergeRangeContext {
|
|
3862
|
+
/** 행 영역 — 본문은 항상 'body'. 고정 영역은 includePinned:true일 때만 불린다 */
|
|
3863
|
+
area: 'body' | 'pinnedTop' | 'pinnedBottom';
|
|
3864
|
+
/** area 기준 행 번호 (고정 영역은 그 영역 안의 순번, 첫 행 0). getRowData/getCellValue/totalRowCount도 같은 기준 */
|
|
3524
3865
|
rowIndex: number;
|
|
3525
|
-
/** 현재 컬럼의
|
|
3866
|
+
/** 현재 컬럼의 `setMergeConfig` `columns` 배열 인덱스(화면 순서 아님) — `rowIndex`의 가로 대칭. `columnsInOrder` 기준 */
|
|
3526
3867
|
colIndex: number;
|
|
3527
3868
|
columnId: string;
|
|
3528
3869
|
/** columns(설정) 배열 내 인덱스 (계층 레벨) */
|
|
@@ -3532,10 +3873,10 @@ interface MergeRangeContext {
|
|
|
3532
3873
|
getRowData: (index: number) => Row | null;
|
|
3533
3874
|
/** 임의 셀 값 조회 (2D 경계 탐색용) — `getRowData(rowIndex)?.[columnId]`와 동치 */
|
|
3534
3875
|
getCellValue: (rowIndex: number, columnId: string) => CellValue;
|
|
3535
|
-
/**
|
|
3876
|
+
/** 병합 대상 컬럼 ID 목록 — `setMergeConfig`의 `columns` 배열 그대로 (가로 경계 탐색용) */
|
|
3536
3877
|
columnsInOrder: readonly string[];
|
|
3537
3878
|
totalRowCount: number;
|
|
3538
|
-
/**
|
|
3879
|
+
/** 병합 대상 컬럼 수 — `totalRowCount`의 가로 대칭 (= columnsInOrder.length = columns.length) */
|
|
3539
3880
|
totalColumnCount: number;
|
|
3540
3881
|
/** 이전 컬럼의 병합 범위 (계층적 커스텀용) */
|
|
3541
3882
|
parentRange?: { startRow: number; endRow: number };
|
|
@@ -3545,12 +3886,25 @@ interface MergeRangeContext {
|
|
|
3545
3886
|
interface MergeRangeResult {
|
|
3546
3887
|
startRow: number; // inclusive
|
|
3547
3888
|
endRow: number; // inclusive
|
|
3548
|
-
startCol?: number; // inclusive,
|
|
3549
|
-
endCol?: number; // inclusive,
|
|
3889
|
+
startCol?: number; // inclusive, `columns` 배열 인덱스. 생략 시 현재 컬럼(세로 전용)
|
|
3890
|
+
endCol?: number; // inclusive, `columns` 배열 인덱스. 생략 시 현재 컬럼
|
|
3550
3891
|
}
|
|
3551
3892
|
```
|
|
3552
3893
|
|
|
3553
|
-
> **세로 / 가로 / 2D 병합** — `startCol`/`endCol`을 **생략하면 세로 병합**(현재 컬럼만, 기존 `{ startRow, endRow }` 계약 그대로), **지정하면 가로/2D 병합**입니다. 컬럼
|
|
3894
|
+
> **세로 / 가로 / 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`)이면 병합 없음.
|
|
3895
|
+
|
|
3896
|
+
> **고정 행 병합 (`includePinned`)** — 기본값(`false`)은 기존 동작 그대로 본문만 병합합니다. `true`면 같은 설정이 상단/하단 고정 행 영역에도 적용되며, 병합은 **각 영역 안에서만** 일어납니다(본문↔고정 영역을 넘는 병합 없음). 콜백은 `area`로 영역을 구분하고, 반환 `startRow`/`endRow`는 그 영역 안의 행 번호입니다. 좌우 고정 컬럼은 영역이 아니므로 같은 컬럼 구역 안에서만 가로 병합되며, 경계를 넘는 범위는 경고 후 무시됩니다. 병합 셀(본문·고정 행 모두)은 앵커(좌상단) 셀이 단독으로 그려질 때와 같게 — 앵커 컬럼의 `cellRenderer`로 그려지며(집계 고정 행은 원래처럼 포맷 텍스트), 데이터 고정 행의 클릭/선택은 앵커 셀로 전달되고 선택 강조는 본문 병합 셀과 같은 규칙(덮는 셀/행 중 하나라도 선택이면 강조)을 따릅니다(복사에는 병합이 반영되지 않음. 엑셀 내보내기는 `includePinnedRows: true`일 때 고정 행 병합을 반영).
|
|
3897
|
+
>
|
|
3898
|
+
> ```typescript
|
|
3899
|
+
> // 하단 집계 4행(sum/avg/min/max)의 앞 3열을 한 칸으로
|
|
3900
|
+
> grid.setMergeConfig({
|
|
3901
|
+
> type: 'custom',
|
|
3902
|
+
> columns: ['prod', 'site', 'grade'],
|
|
3903
|
+
> includePinned: true,
|
|
3904
|
+
> getMergedRange: ({ area }) =>
|
|
3905
|
+
> area === 'pinnedBottom' ? { startRow: 0, endRow: 3, startCol: 0, endCol: 2 } : null,
|
|
3906
|
+
> });
|
|
3907
|
+
> ```
|
|
3554
3908
|
|
|
3555
3909
|
> **Worker 모드 제약**: `type: 'custom'`은 함수 직렬화 불가로 worker에서 사용 불가. content/hierarchical만 worker 호환.
|
|
3556
3910
|
|
|
@@ -3579,6 +3933,8 @@ interface StreamingOptions {
|
|
|
3579
3933
|
|
|
3580
3934
|
`grid.applyTransaction(tx, options?)` / `applyTransactionAsync(tx, callback?)`에서 사용.
|
|
3581
3935
|
|
|
3936
|
+
`update`의 raw patch는 명시한 업무 키로 대상을 식별하는 경로입니다. 자동 ID 모드에서 `{ name: '수정' }` 같은 새 객체는 기존 occurrence를 식별하지 못합니다. ID와 값을 분리한 `grid.changes.updateCellById(rowId, field, value)`를 사용하세요. 아래 예제는 `keyFields: ['id']`로 생성한 그리드를 전제로 합니다.
|
|
3937
|
+
|
|
3582
3938
|
```typescript
|
|
3583
3939
|
interface RowTransaction {
|
|
3584
3940
|
add?: Row[];
|
|
@@ -3817,13 +4173,34 @@ interface CellRendererParams {
|
|
|
3817
4173
|
value: CellValue;
|
|
3818
4174
|
rowData: Record<string, unknown>;
|
|
3819
4175
|
columnId: string;
|
|
4176
|
+
/**
|
|
4177
|
+
* 업무 행 ID — `grid.rows.getById(rowId)`로 해석되는 값이며 `cell:click` payload의 `rowId`·`data-row-id`와 같다.
|
|
4178
|
+
* 단일 keyFields는 `String(값)`(키 resolver 규칙), 복합 키는 인코딩된 문자열, keyFields 없음은 자동 ID(`__ps_auto_N`, loadData마다 바뀜).
|
|
4179
|
+
* 고정(pinned) 데이터행은 고정 영역 cell:click과 같은 문자열 ID, 피벗은 피벗 행 nodeId.
|
|
4180
|
+
* 그룹행·구조행처럼 행 ID가 없는 셀에서는 키 자체가 없다(내부 Row 인스턴스 id `row-N`은 노출하지 않음).
|
|
4181
|
+
*/
|
|
3820
4182
|
rowId?: string | number;
|
|
3821
4183
|
/**
|
|
3822
4184
|
* 행 선택 상태 — rowHeader 셀(`rowHeader.cellRenderer`) 렌더 시에만 채워짐. 일반 데이터 셀은 undefined.
|
|
3823
4185
|
* 렌더 시점 스냅샷이라 선택 "변경"만으로는 재렌더되지 않는다 — CSS(`.ps-row.ps-selected`) /
|
|
3824
4186
|
* `ps-row-header-checkbox-input` 클래스 / `repaintRows`로 반영.
|
|
4187
|
+
* 그룹행(`rowHeader.includeGroupRows`)에서는 그룹 배너 선택(`rowGroupConfig.selectable`) 상태.
|
|
3825
4188
|
*/
|
|
3826
4189
|
selected?: boolean;
|
|
4190
|
+
/**
|
|
4191
|
+
* 그룹행 정보 — 그룹행 셀 렌더 시에만 채워짐(현재 `rowHeader.includeGroupRows: true`의 rowHeader 셀).
|
|
4192
|
+
* 데이터행에는 키가 없다. 멤버는 `grid.rowGroup.getGroupRows(group.groupId)`.
|
|
4193
|
+
*/
|
|
4194
|
+
group?: CellRendererGroupInfo;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
interface CellRendererGroupInfo {
|
|
4198
|
+
groupId: string; // "col:val/col:val" 경로 — 같은 하위값이라도 부모가 다르면 다르다
|
|
4199
|
+
level: number; // 최상위 0
|
|
4200
|
+
column: string; // 이 레벨의 그룹 컬럼 id
|
|
4201
|
+
value: CellValue; // 그룹 값
|
|
4202
|
+
collapsed: boolean; // 렌더 시점 접힘 여부
|
|
4203
|
+
itemCount: number; // 하위 데이터행 수
|
|
3827
4204
|
}
|
|
3828
4205
|
|
|
3829
4206
|
/** Simple (arity 1) — vanilla JS 사용자 */
|
|
@@ -3840,7 +4217,7 @@ interface CellRendererInstance {
|
|
|
3840
4217
|
* 셀이 재사용되며 데이터가 바뀔 때 호출.
|
|
3841
4218
|
* - 반환 `false`: instance가 처리 못함 → 그리드가 destroy 후 re-mount
|
|
3842
4219
|
* - 반환 `void`/`true`: instance가 자체 갱신함
|
|
3843
|
-
* 미정의 시:
|
|
4220
|
+
* 미정의 시: 값/행이 바뀌면(값 변경, 스크롤 재활용) destroy 후 re-mount. 그대로면 유지.
|
|
3844
4221
|
*/
|
|
3845
4222
|
update?: (params: CellRendererParams) => boolean | void;
|
|
3846
4223
|
/** 셀이 사라질 때 호출 — pool release / 컬럼 제거 / grid destroy 시 */
|
|
@@ -3981,7 +4358,8 @@ interface GridConfigSnapshot {
|
|
|
3981
4358
|
|
|
3982
4359
|
| Export | 값/시그니처 | 설명 |
|
|
3983
4360
|
|---|---|---|
|
|
3984
|
-
| `GRID_CONFIG_VERSION` | `'0.
|
|
4361
|
+
| `GRID_CONFIG_VERSION` | `'0.5'` | 현재 저장 스키마 버전. `getConfig()` 가 항상 이 값을 실어 내보냄 |
|
|
4362
|
+
| `SNAPSHOT_CONFIG_VERSION` | `'0.5'` | 이 버전 이상 저장본은 전체 상태 — `applyConfig` 가 `missing` 미지정 시 `'reset'` 으로 해석하는 경계(원래 버전 기준) |
|
|
3985
4363
|
| `LEGACY_CONFIG_VERSION` | `'0.1'` | 버전 필드가 없는 저장본을 읽을 때 가정하는 버전 |
|
|
3986
4364
|
| `compareConfigVersion(a, b)` | `number` | 버전 비교 — **숫자 단위**로 봄(`a<b` 음수, 같으면 0, `a>b` 양수) |
|
|
3987
4365
|
| `GridConfigMigrator` | `(config: GridConfigExport, fromVersion: string) => GridConfigExport` | 하위호환 변환기 타입. `PureSheetConfig.migrateConfig` 로 주입 |
|
|
@@ -3993,6 +4371,8 @@ interface GridConfigSnapshot {
|
|
|
3993
4371
|
| `0.1` | 버전 필드가 없던 시절. `preFilters`/`preSorts` 사용, 수식은 `[라벨]` 참조 |
|
|
3994
4372
|
| `0.2` | 수식 컬럼 참조가 `$id` 기준. `preFilters`/`preSorts` 폐지 |
|
|
3995
4373
|
| `0.3` | 필터 소유권(`filterGroups`) 도입. 소유자 없는 옛 조건식을 고급 필터 수식으로 이관 |
|
|
4374
|
+
| `0.4` | 행 그룹 키 `rowGroupConfig`·집계 `aggregates`, 정렬 소유권(`sortGroups`) |
|
|
4375
|
+
| `0.5` | 구조 동일. **의미 변경**: 저장본이 전체 상태(snapshot) — 현재 값·명시적 "없음" 을 싣고, 빠진 항목은 기본 보기로 초기화(`missing: 'reset'`) |
|
|
3996
4376
|
|
|
3997
4377
|
> **왜 키 존재 여부로는 부족한가** — 지금까지 하위호환은 키 존재 여부로 감지했습니다(`preFilters` 가 있으면 구버전). 그런데 **키는 그대로인데 값의 의미만 바뀌는** 변경에서는 통하지 않습니다:
|
|
3998
4378
|
> ```
|
|
@@ -4005,11 +4385,12 @@ interface GridConfigSnapshot {
|
|
|
4005
4385
|
>
|
|
4006
4386
|
> **마이그레이션은 누적 적용입니다.** `0.1` 저장본이 들어오면 `0.1 → 0.2 → …` 를 **순서대로 전부** 거칩니다. 최신 변환만 돌리면 중간 단계를 건너뛰어 조용히 어긋납니다.
|
|
4007
4387
|
>
|
|
4008
|
-
> **변환 내용은 소비처(wrapper) 소유입니다.** core 는 호출 시점과 순서만 책임집니다 — core 가 변환까지 들고 있으면 소비처 고유 필드(`customCellConfig` 등)를 다룰 수 없고, 소비처마다 다른 진화 속도를 강제하게 됩니다. 변환기가 없거나 이미 최신이면 받은 것을 그대로 돌려줍니다(부작용 없음). 변환 중 예외가
|
|
4388
|
+
> **변환 내용은 소비처(wrapper) 소유입니다.** core 는 호출 시점과 순서만 책임집니다 — core 가 변환까지 들고 있으면 소비처 고유 필드(`customCellConfig` 등)를 다룰 수 없고, 소비처마다 다른 진화 속도를 강제하게 됩니다. 변환기가 없거나 이미 최신이면 받은 것을 그대로 돌려줍니다(부작용 없음). 변환 중 예외가 나면 **아무것도 적용하지 않고** `PS901` 을 던집니다(변환 전 원본으로 진행하면 옛 스키마 값이 오해석된 채 일부만 적용됨). 변환 결과도 적용 전에 형태 검증(`PS903`)을 거칩니다.
|
|
4009
4389
|
|
|
4010
4390
|
```typescript
|
|
4011
4391
|
const grid = new PureSheet(el, {
|
|
4012
4392
|
fields,
|
|
4393
|
+
enableReferCache: true,
|
|
4013
4394
|
migrateConfig: (cfg, fromVersion) => {
|
|
4014
4395
|
let out = cfg;
|
|
4015
4396
|
if (compareConfigVersion(fromVersion, '0.2') < 0) out = to02(out);
|
|
@@ -4243,7 +4624,7 @@ interface GroupRowContext {
|
|
|
4243
4624
|
itemCount: number;
|
|
4244
4625
|
collapsed: boolean;
|
|
4245
4626
|
aggregates: Record<string, CellValue>;
|
|
4246
|
-
element: HTMLElement;
|
|
4627
|
+
element: HTMLElement; // 그룹행 요소. 예약 키 '_groupTemplate'(Vue GroupTemplate) 핸들러만 보이는 그룹 배너 안 템플릿 호스트를 받는다
|
|
4247
4628
|
}
|
|
4248
4629
|
|
|
4249
4630
|
interface SubtotalContext {
|
|
@@ -4515,6 +4896,13 @@ interface RowHeaderConfig {
|
|
|
4515
4896
|
* checked를 자동 갱신한다. rowSelection.trigger 'checkbox' 활성 시 이 renderer가 built-in 체크박스를 대체한다.
|
|
4516
4897
|
*/
|
|
4517
4898
|
cellRenderer?: CellRenderer;
|
|
4899
|
+
/**
|
|
4900
|
+
* 그룹행 rowHeader 셀에서도 cellRenderer를 호출 (opt-in). @default false
|
|
4901
|
+
* false: 기존대로 그룹행 rowHeader는 비운다. true: params.group(그룹 정보)과 함께 호출 —
|
|
4902
|
+
* rowData는 다른 그룹 셀 renderer와 같은 합성 객체(컬럼 id 키 집계값), rowId는 없음.
|
|
4903
|
+
* cellRenderer가 없으면 효과 없음.
|
|
4904
|
+
*/
|
|
4905
|
+
includeGroupRows?: boolean;
|
|
4518
4906
|
/** 코너(좌상단) 헤더 셀 렌더러. 반환은 HTMLElement 또는 문자열 */
|
|
4519
4907
|
headerRenderer?: (params: { columnId: string; header: string }) => HTMLElement | string;
|
|
4520
4908
|
}
|
|
@@ -4544,6 +4932,38 @@ grid.formatHeader.addHandler('corner', (ctx) => {
|
|
|
4544
4932
|
});
|
|
4545
4933
|
```
|
|
4546
4934
|
|
|
4935
|
+
**그룹행에도 그리기 — `includeGroupRows`** (레거시 Wijmo 행 헤더의 업무 체크박스 `is_checked` 이식 예)
|
|
4936
|
+
|
|
4937
|
+
- 그룹행 rowHeader에서도 renderer가 호출되고 `params.group`이 채워집니다. 데이터행·그룹행을 `params.group` 유무로 구분하세요.
|
|
4938
|
+
- **클릭 동작은 바뀌지 않습니다.** 그룹행 rowHeader 칸 클릭은 기존대로 `rowGroupConfig.selectable`이면 그룹 선택, 아니면 접기/펼치기이고, 데이터행은 `rowSelection.trigger` 규칙을 따릅니다. renderer가 그린 요소에서 그리드 동작(접기/펼치기·선택·드래그 선택 시작)을 막으려면 그 요소의 `mousedown`과 `click`에서 `stopPropagation()`을 호출하세요(그리드는 viewport에서 버블링으로 받습니다).
|
|
4939
|
+
- 업무 체크는 선택과 독립입니다(선택 tri-state `rowSelection.groupRowCheckbox`는 `cellRenderer`가 있으면 원래 꺼집니다). 같은 하위값이 다른 부모 아래에 있어도 `group.groupId` 경로로 구분되므로 멤버는 반드시 `getGroupRows(groupId)`로 조회합니다.
|
|
4940
|
+
- 행 재사용(접기/스크롤)으로 같은 셀이 데이터↔그룹 행을 오가면 Lifecycle 인스턴스는 `destroy` 후 새 종류로 다시 mount됩니다.
|
|
4941
|
+
|
|
4942
|
+
```ts
|
|
4943
|
+
const grid = new PureSheet(el, {
|
|
4944
|
+
fields, keyFields: ['id'], enableReferCache: true, // rows.getById 에 필요
|
|
4945
|
+
rowGroupConfig: { columns: ['parent', 'child'] },
|
|
4946
|
+
rowHeader: {
|
|
4947
|
+
includeGroupRows: true,
|
|
4948
|
+
cellRenderer: (p) => {
|
|
4949
|
+
const box = document.createElement('input');
|
|
4950
|
+
box.type = 'checkbox';
|
|
4951
|
+
const ids = p.group ? grid.rowGroup.getGroupRows(p.group.groupId) : [p.rowId!];
|
|
4952
|
+
box.checked = ids.length > 0 && ids.every(id => grid.rows.getById(id).data.is_checked === true);
|
|
4953
|
+
// 그리드 동작(접기/펼치기·그룹/행 선택·드래그 시작) 차단
|
|
4954
|
+
box.addEventListener('mousedown', e => e.stopPropagation());
|
|
4955
|
+
box.addEventListener('click', (e) => {
|
|
4956
|
+
e.stopPropagation();
|
|
4957
|
+
for (const id of ids) grid.rows.getById(id).setValue('is_checked', box.checked);
|
|
4958
|
+
});
|
|
4959
|
+
return box;
|
|
4960
|
+
},
|
|
4961
|
+
},
|
|
4962
|
+
});
|
|
4963
|
+
```
|
|
4964
|
+
|
|
4965
|
+
> 그룹 체크 상태는 멤버 값에서 파생합니다. 멤버를 `setValue`로 바꾸면 그리드가 화면 행을 다시 그리면서 그룹행 renderer도 다시 호출되어 표시가 맞춰집니다(값을 그리드 밖에서 바꿨다면 `grid.refresh()`).
|
|
4966
|
+
|
|
4547
4967
|
> 참고: `cellRenderer`를 주면 커스텀 렌더가 그려집니다(예: 체크박스 컬럼 대체). `rowHeader: true`(구 `showRowHeader: true`)는 **빈 셀**(너비 40, 체크박스 트리거 시 체크박스)만 그리는 축약형입니다 — 행 번호를 렌더하지 않습니다.
|
|
4548
4968
|
|
|
4549
4969
|
---
|
|
@@ -4625,6 +5045,7 @@ export default class MyLoader extends DataLoader {
|
|
|
4625
5045
|
|
|
4626
5046
|
// 그리드 생성
|
|
4627
5047
|
new PureSheet(container, {
|
|
5048
|
+
enableReferCache: true,
|
|
4628
5049
|
loader: {
|
|
4629
5050
|
module: new URL('./my-loader.ts', import.meta.url),
|
|
4630
5051
|
params: { authToken: getToken() },
|
|
@@ -4684,8 +5105,8 @@ class UndoStack {
|
|
|
4684
5105
|
const sharedUndo = new UndoStack({ maxSize: 200 });
|
|
4685
5106
|
sharedUndo.attachKeyboard(document.getElementById('page'));
|
|
4686
5107
|
|
|
4687
|
-
const grid1 = new PureSheet(el1, { undoStack: sharedUndo, fields, data });
|
|
4688
|
-
const grid2 = new PureSheet(el2, { undoStack: sharedUndo, fields, data });
|
|
5108
|
+
const grid1 = new PureSheet(el1, { undoStack: sharedUndo, fields, data, enableReferCache: true });
|
|
5109
|
+
const grid2 = new PureSheet(el2, { undoStack: sharedUndo, fields, data, enableReferCache: true });
|
|
4689
5110
|
|
|
4690
5111
|
// 한 곳에서 Ctrl+Z → 어느 그리드든 마지막 작업 복원
|
|
4691
5112
|
// 두 그리드에서 일어난 변경이 동일 시간순으로 스택에 쌓임
|
|
@@ -4788,7 +5209,7 @@ import { PS, psError, psWarn, ErrorMessages } from '@vmscloud/moz-ui-grid-core';
|
|
|
4788
5209
|
| `PS501` | `PS.WORKER_SYNC_API` | Worker에서 getAllDataSync 호출 |
|
|
4789
5210
|
| `PS502` | `PS.WORKER_DATA_DIRECT` | Worker 모드에서 `data` 직접 전달 불가 — `loader`를 사용할 것 |
|
|
4790
5211
|
| **행 ID / 캐시 (PS601~)** | | |
|
|
4791
|
-
| `PS601` | `PS.ROW_ID_REQUIRED` | keyFields
|
|
5212
|
+
| `PS601` | `PS.ROW_ID_REQUIRED` | 행 ID 추출 실패 진단. 키 생략 시 자동 모드는 `enableReferCache: true`가 필수이며, 빈 `keyFields`/캐시 미설정 초기화 오류는 별도 `Error`로 거부됨 (`id` 필드 자동 추론 없음) |
|
|
4792
5213
|
| `PS602` | `PS.REFER_CACHE_REQUIRED` | 행 객체 API에 enableReferCache 필요 |
|
|
4793
5214
|
| `PS603` | `PS.KEY_CONFLICT_COMMITTED` | 커밋된 데이터와 키 중복 |
|
|
4794
5215
|
| `PS606` | `PS.KEY_LOAD_DUPLICATES` | loadData 중복 키 |
|