# SpreadJS Sheets Collaboration 기능

## Content

실시간 동시 작업 편집에서 **Snapshot**과 **ChangeSet**은 여러 사용자가 동시에 동일한 문서를 편집할 때 데이터 일관성, 효율성, 실시간 성능을 보장하기 위해 도입된 두 가지 핵심 개념입니다. 이 개념들은 동시 작업, 데이터 동기화, 성능 최적화와 같은 중요한 과제를 해결합니다. 아래에서는 정의, 목적, 구현 세부 사항을 설명합니다.

## 설치

```typescript
import '@mescius/spread-sheets-collaboration-addon';
```

## Collaboration 클래스

동시 작업 기능을 제공하기 위해 Workbook에 Collaboration 클래스를 얻을 수 있는 속성이 추가되며, 이 클래스는 동시 작업과 관련된 기능을 제공합니다.

```typescript
///* field GC.Spread.Sheets.@Workbook.collaboration: GC.Spread.Sheets.Collaboration.Collaboration
/**
 * Workbook용 동시 작업 관리자
 * @type {GC.Spread.Sheets.Collaboration.Collaboration}
 * //이 예제는 snapshot을 가져오는 방법을 보여줍니다.
 * @example
 * var snapshot = workbook.collaboration.toSnapshot();
 */
```

## Snapshot

**Snapshot을 사용하는 이유**

* 다중 사용자 환경에서 모든 사용자는 문서의 현재 상태를 알아야 합니다. Snapshot은 특정 버전 시점의 문서 전체 상태를 나타냅니다.
* Snapshot은 클라이언트가 최신 문서 버전을 빠르게 가져올 수 있게 하며, 이후 편집을 위한 기준선 역할을 합니다.

**동시 작업에서의 가치**

* **일관성**: Snapshot은 모든 사용자에게 동일한 시작 지점을 제공하여 동일한 문서 상태를 기반으로 편집하도록 보장합니다.
* **초기화 및 복구**: 신규 사용자나 재연결한 사용자는 Snapshot을 사용해 최신 상태로 동기화할 수 있습니다.

**Snapshot 형식**
Snapshot은 실시간 동시 작업을 위해 특별히 설계된 새로운 데이터 형식을 사용하며, `ssjson`이나 `sjs`와 같은 기존 형식과는 다릅니다.

**API**
Snapshot은 특정 버전의 Workbook 전체 상태를 캡처합니다. 다음 메서드는 Workbook 인스턴스와 Snapshot 간 변환을 지원합니다.

* [toSnapshot](/spreadjs/api/classes/GC.Spread.Sheets.Collaboration.Collaboration#tosnapshot): Workbook 상태를 Snapshot으로 저장합니다.

```typescript
/**
 * 동시 작업 시나리오에서만 사용되며, Workbook 상태를 snapshot으로 저장합니다.
 * @return {object} snapshot - snapshot 객체
 */
Workbook.collaboration.prototype.toSnapshot = function (): any;
```

* [fromSnapshot](/spreadjs/api/classes/GC.Spread.Sheets.Collaboration.Collaboration#fromsnapshot): Snapshot으로부터 Workbook 상태를 복원합니다.

```typescript
/**
 * 동시 작업 시나리오에서만 사용되며, snapshot을 workbook 상태로 복원합니다.
 * @param {object} snapshot - snapshot 객체
 */
Workbook.collaboration.prototype.fromSnapshot = function (snapshot: any): void;
```

## Op (Operation)

**Op란 무엇인가**
문서 모델에 대한 **모든 변경**은 Op(Operation)으로 기록됩니다.

**Op를 사용하는 이유**

* 편집할 때마다 전체 문서(새 Snapshot)를 전송하면 과도한 대역폭을 소모합니다. Op는 `setValue`(값 설정), `addRow`(행 추가)와 같이 새 Snapshot으로 이어지는 구체적인 변경 사항만을 설명합니다.
* Op만 전송함으로써 전체 문서를 반복 전송하지 않고도 동기화를 달성할 수 있습니다.

**API**
각 Op에는 수정 유형을 설명하는 `type` 필드가 포함됩니다.

```typescript
export interface IOpComponent{
    type: GC.Spread.Sheets.Collaboration.OpType;
}
```

`OpType`에 대한 자세한 정의는 다음 문서를 참고하세요: [OpType](/spreadjs/docs/spreadjs-collaboration-server/spreadjs-sheets-collaboration/spreadjs-sheets-collaboration-add-on/optype).

## ChangeSet

**ChangeSet이란 무엇인가**

* **ChangeSet**은 서로 관련된 Op들의 집합으로, 일반적으로 하나의 논리적 수정 단위를 나타냅니다. 문서가 한 버전에서 다음 버전으로 전환되는 과정을 설명합니다.
* SpreadJS에서는
    * 단일 모드(기본값): 하나의 명령으로 생성된 Ops가 하나의 ChangeSet으로 그룹화됩니다.
    * 배치 모드: 개발자가 여러 변경을 하나의 ChangeSet으로 병합하여 서버 부하와 네트워크 사용량을 줄입니다(동기 실행 필요). 자세한 내용은 “Batch Operations” 섹션을 참고하세요.

**ChangeSet을 사용하는 이유**

* **논리적 그룹화**: ChangeSet은 관련된 Ops를 묶어 수정 내용을 더 명확하게 표현합니다. 예를 들어 하나의 동작이 여러 모델에 영향을 미칠 수 있습니다.
* **원자성**: ChangeSet은 Ops 그룹이 전부 적용되거나 전혀 적용되지 않도록 보장하여 부분 적용 상태를 방지합니다.
* **충돌 해결**: 다중 사용자 환경에서 발생하는 충돌을 보다 상위 수준에서 비교·병합할 수 있습니다.
* **이력 관리**: 전체 수정 이력을 기록하여 버전 롤백, 디버깅, 감사에 활용할 수 있습니다.
* **성능 최적화**: 여러 Op를 하나의 ChangeSet으로 묶어 네트워크 전송과 동기화 효율을 높입니다.

**동시 작업에서의 가치**

* **효율성**: ChangeSet은 작고 빠르게 전송되어 실시간 동시 작업에 적합합니다.
* **동시성 처리**: OT(Operational Transformation)를 통해 동시에 발생한 사용자 작업을 일관되게 처리합니다.

**ChangeSet 제한 사항**
동일한 Snapshot에서 시작된 Workbook만 서로의 Op를 적용할 수 있습니다.

**API**
동시 작업은 ChangeSet을 적용하고 감지하기 위한 두 가지 API를 제공합니다.

* [applyChangeSet](/spreadjs/api/classes/GC.Spread.Sheets.Collaboration.Collaboration#applychangeset): ChangeSet을 문서에 적용합니다.

```typescript
interface IChangeSet {
    ops: IOpComponent[];
}
/**
 * 동시 작업 시나리오에서만 사용되며, changeSet을 문서에 적용합니다.
 * @param {changeSet: GC.Spread.Sheets.Collaboration.IChangeSet} changeSet - change set
 */
Workbook.collaboration.prototype.applyChangeSet = (changeSet: GC.Spread.Sheets.Collaboration.IChangeSet) => void;
```

* [onChangeSet](/spreadjs/api/classes/GC.Spread.Sheets.Collaboration.Collaboration#onchangeset): ChangeSet 생성 이벤트를 수신합니다.

```typescript
/**
 * 동시 작업 시나리오에서만 사용되며, changeset을 감시합니다.
 * @param {changeSetHandler: GC.Spread.Sheets.Collaboration.IChangeSetHandler} onOpHandler - change set 콜백
 */
Workbook.collaboration.prototype.onChangeSet = (onChangeSetHandler: GC.Spread.Sheets.Collaboration.IChangeSetHandler) => void;
```

## Batch Operations

**Batch Operations란 무엇인가**
Batch Operations는 여러 개의 동기식 Workbook 작업(예: 대량 설정 변경, 반복적인 데이터 삽입/삭제)을 하나의 ChangeSet으로 묶을 수 있게 합니다. `startBatchOp()`와 `endBatchOp()`로 감싸면 중간에 생성된 모든 Op가 하나의 원자적 변경으로 병합됩니다.

**Batch Operations를 사용하는 이유**

* **원자성**: 그룹화된 작업이 단일 변경으로 적용되어 일관성을 보장합니다.
* **효율성**: ChangeSet 수를 줄여 네트워크 사용량과 서버 부하를 감소시킵니다.
* **Undo/Redo 단순화**: 대량 업데이트를 하나의 단계로 되돌릴 수 있습니다.
* **실용적 활용**: Workbook 초기화나 대규모 업데이트에 적합합니다.

**API**

* [startBatchOp()](/spreadjs/api/classes/GC.Spread.Sheets.Collaboration.Collaboration#startbatchop): 배치 작업을 시작합니다.

```javascript
Workbook.collaboration.prototype.startBatchOp(): void
```

* [endBatchOp()](/spreadjs/api/classes/GC.Spread.Sheets.Collaboration.Collaboration#endbatchop): 배치 작업을 종료합니다.

```javascript
Workbook.collaboration.prototype.endBatchOp(): void;
```

>type=note
> **주의 사항**
>
> * startBatchOp()과 endBatchOp()은 반드시 쌍으로 올바른 순서로 사용해야 합니다.
> * 두 함수 사이의 작업은 반드시 동기식이어야 합니다.

**예제**

```javascript
spread.collaboration.startBatchOp();
sheet.setValue(0, 0, "Hello");
sheet.setFormula(0, 2, "=SUM(A1:B1)");
spread.collaboration.endBatchOp(); 
// 위 모든 작업은 하나의 ChangeSet으로 병합됩니다.
```

## 기타

### 타입 등록

동시 작업 시나리오에서는 OT\_Type을 등록해야 비정상적인 Undo 동작 등을 방지할 수 있습니다.

```typescript
export interface IOT_Type{
    uri?: string;
    transform?: (op1: GC.Spread.Sheets.Collaboration.IChangeSet, op2: GC.Spread.Sheets.Collaboration.IChangeSet, side: 'left' | 'right') => GC.Spread.Sheets.Collaboration.IChangeSet;
    transformX?: (op1: GC.Spread.Sheets.Collaboration.IChangeSet, op2: GC.Spread.Sheets.Collaboration.IChangeSet, side: 'left' | 'right') => GC.Spread.Sheets.Collaboration.IChangeSet[];
}
/**
 * 동시 작업 시나리오에서만 사용되며, 동시 작업 타입을 등록합니다.
```

**예제**

```typescript
import * as GC from '@mescius/spread-sheets'
import { type } from '@mescius/spread-sheets-collaboration-client';

const workbook = new GC.Spread.Sheets.Workbook('ss');
workbook.collaboration.registerCollaborationType(type);
```

자세한 내용은 다음을 참고하세요: [SpreadJS Sheets Collaboration packages](/spreadjs/docs/spreadjs-collaboration-server/spreadjs-sheets-collaboration/spreadjs-sheets-collaboration-packages)

### User

사용자 바인딩, 권한 설정, 동시 작업 상태 표시를 지원하기 위해 User 개념과 관련 API가 추가되었습니다. 참고: [User](/spreadjs/docs/spreadjs-collaboration-server/spreadjs-sheets-collaboration/user).

## 구현 예제

### 예제 1: 페이지 내 두 Spread 인스턴스 동기화

```typescript
var spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));
var spread2 = new GC.Spread.Sheets.Workbook(document.getElementById('ss2'));
   
spread2.collaboration.fromSnapshot(spread.collaboration.toSnapshot());
spread.collaboration.onChangeSet((changeSet) => {
  spread2.collaboration.applyChangeSet(changeSet);
});
spread2.collaboration.onChangeSet((changeSet) => {
  spread.collaboration.applyChangeSet(changeSet);
});
```

### 예제 2: 상태 저장 및 복원

```typescript
var spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));

// 초기 Snapshot 저장
const initialSnapshot = spread.collaboration.toSnapshot();
if (!localStorage.getItem('snapshot')) {
   localStorage.setItem('snapshot', JSON.stringify(initialSnapshot)); 
}
applyChangeSet(spread);

// localStorage에 저장된 snapshot과 changeSet 적용
function applyChangeSet (spread) {
    const storedSnapshot = JSON.parse(localStorage.getItem('snapshot'));
    const storedChanges = JSON.parse(localStorage.getItem('changes') || '[]');

    if (storedSnapshot && spread) {
        spread.collaboration.fromSnapshot(storedSnapshot);

        spread.suspendPaint();
        storedChanges.forEach((changeSet) => {
            spread.collaboration.applyChangeSet(changeSet);
        });
        spread.resumePaint();
    }
}

// ChangeSet 수신 및 저장
spread.collaboration.onChangeSet((changeSet) => {
    let changes = JSON.parse(localStorage.getItem('changes') || '[]');
    changes.push(changeSet);
    localStorage.setItem('changes', JSON.stringify(changes));
});
```