# 워크시트 데이터 범위 

## Content

SpreadJS에서 데이터 범위 기능은 시트의 축 시스템 내에서 특정 워크시트 영역을 정의하고 관리할 수 있도록 해줍니다. 이 기능은 특히 복잡한 데이터 구조를 표시하고 상호작용할 때 유용하며, 시트 축과 데이터 범위 축 사이에서 셀 좌표(Row/Col)를 변환하여, 좌표를 수동으로 처리하는 대신 비즈니스 로직에 집중할 수 있도록 합니다.
데이터 범위는 내부 워크시트의 데이터 저장 및 스타일 저장을 추상화하여 사용자 정의 인터페이스를 통해 처리할 수 있도록 해줍니다. 이를 통해 사용자는 일반적인 워크시트 렌더링 및 상호작용 기능을 그대로 활용하면서, 자신의 특화된 데이터 구조를 워크시트에 직접 렌더링할 수 있습니다. 예를 들어, 피벗 테이블은 데이터 분석을 위해 설계된 특수한 데이터 저장 구조이며, 데이터 범위를 사용해 데이터와 스타일을 워크시트에 렌더링합니다. 이를 바탕으로 사용자는 분석 리포트, 재무 리포트, 가상 서버 데이터 뷰 등 피벗 테이블과 유사한 사용자 정의 솔루션을 설계할 수 있습니다.
데이터를 효율적으로 조회하고 집계한 후, 데이터 범위를 통해 렌더링함으로써 워크시트 셀에 데이터를 수동으로 작성하지 않아도 되며, 성능과 사용성 모두를 향상시킬 수 있습니다.

>type=warning
> **참고**: 데이터 범위는 서로 **겹쳐지면 안 되며**, **고유한 이름**을 가져야 합니다.

## 주요 기능

* **데이터 관리(Data Management)**: 대규모 데이터셋 내에서 **논리적인 범위를 정의**하여 시각화 또는 분석 등 **대상 작업을 효율적으로 수행**할 수 있습니다.
* **사용자 정의 데이터 표시(Customizable Data Display)**: 스타일 지정, 병합, 조건부 서식 등을 지원하여 **가독성 향상**에 기여합니다.
* **동적 데이터 상호작용(Dynamic Data Interaction)**: 이벤트 기반 훅, 고정 행(sticky row), JSON 직렬화를 통해 **실시간 업데이트**가 가능합니다.
* **향상된 사용자 경험(Improved User Experience)**: 고정 헤더, 교차 행 스타일, 키보드 탐색 등의 기능으로 **최적의 사용성 제공**합니다:
* **데이터 범위 IO(Data Range IO)**: 데이터 범위를 JSON 또는 Excel 형식으로 내보내기 가능하며, 일관된 데이터 표현을 유지합니다.

## 데이터 범위 사용 단계

다음 단계를 따라 워크시트에서 데이터 범위를 사용할 수 있습니다:

1. `IDataProvider` 인터페이스를 구현하여 사용자 정의 데이터 공급자(Data Provider) 생성
2. 워크시트 내에서 대상 범위 선택
3. Data Range Manager를 사용하여 데이터 범위를 워크시트에 추가

## 데이터 공급자 설정

데이터 공급자(Data Provider)는 사용자 정의 인스턴스로, `GC.Spread.Sheets.DataRange.IDataProvider` 인터페이스를 구현하면 값 설정, 값 가져오기, 변경 취소(Undo), 셀 값 커스터마이징 등을 처리할 수 있습니다.

### 값 설정

셀 값의 읽기 및 쓰기는 데이터 범위 내에서 데이터 공급자에 의해 제어되며, 이 기능을 활용하여 데이터 소스에 편리하게 연결할 수 있습니다.
시트가 데이터 범위 내의 셀을 렌더링할 때는 `getValue` 메서드를 사용하여 값을 가져옵니다.
마찬가지로, `setValue` 메서드를 사용하면 데이터 범위 내 셀에 값을 할당할 수 있습니다.다음은 데이터 소스로부터 값을 가져오고 설정하는 고객용 데이터 공급자 예제입니다.

```javascript
 class CustomerDataProvider {
     // 값
     dataSource = [
         ["ID", "Name", "Age"],
         ["1", "Mike", "18"],
         ["2", "Joe", "19"],
         ["3", "Nick", "30"],
         ["4", "Sam", "26"]
     ]
     getValue(row, col) {
         return this.dataSource[row][col];
     }
     setValue(row, col, value) {
         this.dataSource[row][col] = value;
         return true;
     }
 }
```

그러나 변경된 내용을 실행 취소/다시 실행(Undo/Redo)하고 싶다면, 변경 내용을 `changes` 매개변수에 기록해야 하며, 그런 다음 `changes` 매개변수를 사용하여 데이터 공급자에서 `undo` 메서드를 구현해야 합니다.

```javascript
   class CustomerDataProvider {
       // 값
       dataSource = [
           ["ID", "Name", "Age"],
           ["1", "Mike", "18"],
           ["2", "Joe", "19"],
           ["3", "Nick", "30"],
           ["4", "Sam", "26"]
       ]
       getValue(row, col) {
           return this.dataSource[row][col];
       }
       setValue(row, col, value, changes) {
           var oldValue = this.getValue(row, col);
           if (changes) {
               // 실행 취소 시 상태를 복원할 수 있도록 필요한 정보를 포함한 change 객체를 생성합니다.
               var change = {
                   row: row,
                   col: col,
                   oldValue: oldValue,
                   type: "value" // 실행 취소 시 식별하기 쉽도록 change에 키를 지정하여 마킹합니다.
               };
               changes.push(change);
           }
           this.dataSource[row][col] = value;
           return true;
       }
       undo(change) {
           if (change.type === "value") {// setValue 메서드 내에서 마킹
               var row = change.row;
               var col = change.col;
               var oldValue = change.oldValue;
               this.setValue(row, col, oldValue);
           }
       }
   }
```

### 데이터 범위 스타일 지정

`getStyle` 메서드를 사용하면 테이블 헤더, 본문 스타일, 교차 행 스타일 등 데이터 범위의 외형을 사용자 정의할 수 있습니다.
또한 필요한 경우, `GC.Spread.Sheets.Style` 클래스를 사용하여 `formatter`와 같은 공용 스타일 속성을 데이터 공급자에서 사용할 수 있습니다.

```javascript
 class CustomerDataProvider {
     //  Styles
     headerRowCount = 1;
     styles = {
         header: () => {
             var style = new GC.Spread.Sheets.Style();
             style.backColor = "#217346";
             style.foreColor = "white";
             return style;
         },
         body: () => {
             var style = new GC.Spread.Sheets.Style();
             style.foreColor = "#217346";
             style.backColor = "lightgray";
             return style;
         }
     }

     step = 2;
     alternatingRowStyle = () => {
         var style = new GC.Spread.Sheets.Style();
         style.backColor = "#EEEEEE";
         return style;
     }
     getStyle(row, col) {
         // when row index is at the row header area
         if (row < this.headerRowCount) {
             return this.styles.header();
         }

         // Alternating Row Styles
         if (row % this.step === 0) {
             return this.alternatingRowStyle();
         } else {
             return this.styles.body();
         }
     }
 }
```

### 조건부 서식

`applyColumnIndex` 속성을 사용하여 데이터 범위의 열에 조건부 서식을 적용할 수도 있으며, 이를 통해 데이터의 가독성을 향상시킬 수 있습니다.

```javascript
class CustomerDataProvider {
    // Conditional Formatting
    headerRowCount = 2;
    applyColumnIndex = 2; // the applied conditional format column index
    condition = (value) => value > 18;
    highlightStyle = () => {
        var style = new GC.Spread.Sheets.Style();
        style.foreColor = "red";
        style.cellPadding = "10 0 0 0";
        style.watermark = "ADULT";
        style.labelOptions = { visibility: 0, foreColor: "#CCCCCC", font: '10px Arial' };
        style.vAlign = GC.Spread.Sheets.VerticalAlign.center;
        return style;
    }
    normalStyle = () => {
        var style = new GC.Spread.Sheets.Style();
        style.vAlign = GC.Spread.Sheets.VerticalAlign.center;
        return style;
    }
    getStyle(row, col) {
        if (row < this.headerRowCount) {
            var style = new GC.Spread.Sheets.Style();
            style.backColor = "#217346";
            style.foreColor = "white";
            return style;
        }
        if (row >= this.headerRowCount) {
            // if match the condition
            if (col === this.applyColumnIndex && this.condition(this.getValue(row, col))) {
                return this.highlightStyle();
            }
            return this.normalStyle();
        }
    }
}
```

### 셀 병합

데이터 공급자에서 `getSpans` 메서드를 사용하여 여러 행 또는 열에 걸친 헤더를 생성할 수 있습니다.

```javascript
class CustomerDataProvider{
    headerRowCount = 2;

    // Spans
    spans = [new GC.Spread.Sheets.Range(0, 0, 2, 1), new GC.Spread.Sheets.Range(0, 1, 1, 2)]
    getSpans(row, col, rowCount, colCount) {
        const spans = [];
        for (let span of this.spans) {
            if (span.intersect(row, col, rowCount, colCount)) {
                spans.push(span);
            }
        }
        return spans;
    }
}
```

### 훅(Hook) 및 이벤트

아래 표는 다양한 후크 동작과 해당 반환 파라미터를 보여줍니다. 필요에 따라 데이터 공급자에 구현할 수 있습니다.

| **Hooks** | **입력 파라미터** | **반환값** |
| ----- | ------- | --- |
| `onRowChange` | row: number;<br>rowCount: number;<br>changeType: "add" \| "delete";<br>changes: any[]; |  |
| `onColumnChange` | col: number;<br>colCount: number;<br>changeType: "add" \| "delete";<br>changes: any[]; |  |
| `onCopy` | name: string; | {<br> name: string,<br> dataProvider: GC\.Spread\.Sheet\.DataRange\.IDataProvider \| boolean;<br>}<br>값이 false인 경우, 데이터 범위 복사를 방지합니다. 기본값은 false입니다. |
| `onClear` | row: number;<br>col: number;<br>rowCount: number;<br>colCount: number;<br>changes: any[]; | 값이 false인 경우, 데이터 범위 지우기를 방지합니다. 기본값은 false입니다.<br>이 훅은 콘텐츠를 지울 때 실행됩니다 |
| `onDestory` |  | 이 훅은 데이터 범위가 제거될 때 실행됩니다. |
| `onMouseDown`<br>`onMouseMove`<br>`onMouseUp`<br>`onContextMenu`<br>`onDoubleClick` | row: number;<br>col: number;<br>e: MouseEvent; | `boolean`<br>`true`는 해당 동작이 이미 데이터 공급자에 의해 실행되었음을 의미하며, 이후에 발생할 수 있는 시트 동작을 방지합니다. |
| `onMouseWheel` | deltaX: number;<br>deltaY: number;<br>e: MouseEvent; | `boolean`<br>`true`는 해당 동작이 이미 데이터 공급자에 의해 실행되었음을 의미하며, 따라서 이후에 발생할 수 있는 시트 동작을 방지합니다. |
| `onKeyDown`<br>`onKeyUp` | row: number;<br>col: number;<br>e: KeyboardEvent; | `boolean`<br>`true`는 해당 동작이 이미 데이터 공급자에 의해 실행되었음을 의미하며, 따라서 이후에 발생할 수 있는 시트 동작을 방지합니다. |
| `toJSON` |  | {<br> typeName: string;<br> \[prop: string\]: any;<br>}<br>역직렬화를 원할 경우, `typeName`은 데이터 공급자 클래스 이름과 동일해야 합니다. |
| `fromJSON` | {<br> typeName: string;<br> \[prop: string\]: any;<br>} |  |

## 데이터 범위 관리자 사용

`sheet.dataRanges` 속성은 워크시트 내에서 여러 데이터 범위를 관리하는 데 사용됩니다. 아래 섹션에 언급된 작업을 수행할 수 있습니다.
새로운 데이터 범위를 이름, 데이터 공급자 인스턴스, 범위와 함께 추가하려면 `dataRanges` 클래스의 `add` 메서드를 사용합니다.

```javascript
sheet.dataRanges.add(
  "DataRangeName",
  CustomDataProvider,
  new GC.Spread.Sheets.Range(2, 2, 4, 4)
);
```

데이터 범위 이름을 기준으로 데이터 범위를 제거하려면, `dataRanges` 클래스의 `remove` 메서드를 사용합니다.

```javascript
sheet.dataRanges.remove("DataRangeName");
```

시트의 모든 데이터 범위를 지우려면, `dataRanges` 클래스의 `clear` 메서드를 사용합니다.

```javascript
sheet.dataRanges.clear();
```

데이터 범위 이름을 기준으로 데이터 범위를 가져오려면, `dataRanges` 클래스의 `get` 메서드를 사용하며, 이 메서드는 데이터 범위의 이름을 매개변수로 받습니다.

```javascript
var dataRange = sheet.dataRanges.get("DataRangeName");
```

시트의 모든 데이터 범위를 가져오려면, `dataRanges` 클래스의 `all` 메서드를 사용합니다.

```javascript
var dataRanges = sheet.dataRanges.all();
```

데이터 소스가 변경된 후에는 필요에 따라 데이터 범위의 크기와 위치를 변경하고 다시 렌더링할 수 있습니다.
데이터 범위 영역 자체를 다시 그리려면 `repaint` 메서드를 사용합니다.

```javascript
dataRange.repaint();
```

데이터 범위의 크기를 변경하려면, `range` 메서드에 **새로운 `rowCount`와 `colCount`** 값을 전달하면 됩니다.

```javascript
var oldRange = dataRange.range();
dataRange.range(oldRange.row, oldRange.col, oldRange.rowCount + 1, oldRange.colCount + 1);
```

데이터 범위의 위치를 변경하려면, `range` 메서드에 **새로운 `rowIndex`와 `colIndex`** 값을 전달하면 됩니다.

```javascript
var oldRange = dataRange.range();
dataRange.range(oldRange.row + 1, oldRange.col + 1, oldRange.rowCount, oldRange.colCount);
```

## 데이터 범위 옵션 관리

데이터 범위의 상단 행을 시트에서 항상 보이도록 고정(stick)할 수 있으며, 고정할 행 수는 `sticky` 속성을 사용하여 정의할 수 있고, 이후에 변경할 수도 있습니다.
`IDataRangeOptions` 인터페이스를 사용하여 고정 행(sticky rows) 과 같은 추가 옵션을 구성할 수 있습니다.

```javascript
sheet.dataRanges.add("DataRangeName", CustomDataProvider, new GC.Spread.Sheets.Range(2, 2, 4, 4), {
  sticky: { top: 2 }
});
```

## 데이터 범위 내보내기

SpreadJS는 데이터 범위를 **JSON, SJS, Excel 형식으로 내보내는 기능**을 제공하여, **데이터의 일관된 표현을 보장**합니다. 다음은 데이터 범위 내보내기 시 기본적으로 고려해야 할 사항들입니다.
**JSON 형식**
사용자 정의 데이터 공급자 클래스는 직렬화(serialize)를 직접 수행할 수 없습니다. 따라서, 데이터 공급자 내에 클래스 이름과 동일한 문자열 값을 가진 `typeName`을 정의해야 합니다.
또한, 역직렬화(deserialize) 시 해당 사용자 정의 데이터 공급자 클래스를 찾을 수 있도록, 데이터 공급자 클래스가 `window`와 같은 전역 인스턴스에 등록되어 있어야 합니다.

```javascript
class CustomDataProvider {
  constructor () {
    this.typeName = "CustomDataProvider";
  }
}
window.CustomDataProvider = CustomDataProvider;
```

**SJS 형식**
SpreadJS 애플리케이션에서 사용하기 위해 데이터 범위를 SpreadJS 형식(SJS) 으로 내보낼 때, 모든 JSON 데이터는 손실 없이 저장되는 형식(`lossless` format) 으로 유지됩니다.
**Excel 형식**
Excel은 데이터 범위 기능을 지원하지 않기 때문에, 이러한 범위는 값과 스타일이 포함된 일반 셀 형식으로 내보내집니다.

## 데이터 범위 정책

SpreadJS에서 데이터 범위를 사용할 때 고려해야 할 중요한 정책은 다음과 같습니다:

* 행과 열 인덱스는 반드시 0 이상이며, 시트의 전체 행/열 수 이내여야 합니다.
* 행과 열 수에 대한 제한은 없지만, 워크시트 범위를 벗어나는 데이터 범위 영역은 무시됩니다.
* 데이터 공급자 후크에 전달되는 모든 입력 파라미터는 시트 축이 아닌 데이터 범위 축을 기준으로 합니다 (예: row, column).
* 데이터 범위 앞에 행 또는 열이 삽입되거나 삭제되면 데이터 범위가 이동됩니다.
    반대로, 데이터 범위 외부에서 삽입 또는 삭제가 발생한 경우, 데이터 범위에는 아무런 영향도 주지 않습니다.
* 전체 데이터 범위를 포함하는 셀 범위를 Clear(지우기) 할 경우, 해당 데이터 범위는 제거됩니다.
* 필터 대화상자 및 컨텍스트 메뉴에서의 정렬 옵션(Sort options) 은 현재 컨텍스트가 데이터 범위일 경우 숨겨집니다.
* 데이터 공급자에 `toJSON` 후크가 구현되어 있지 않은 경우, SpreadJS는 사용자 정의 데이터 공급자의 `typeName` 속성을 기반으로 기본 JSON 출력을 생성합니다.
* `fromJSON` 사용 시, 일치하는 데이터 공급자가 존재하지 않으면 해당 데이터 범위는 생성되지 않습니다.