개요
Operation은 문서 모델에 대한 원자적 변경입니다. 일관성을 보장하고 undo/redo 기능을 활성화하기 위해 transaction으로 그룹화할 수 있습니다. 각 operation 타입은 특정 속성과 동작을 가집니다.
Operation 특성:
- 불변 - operation은 기존 상태를 수정하지 않음
- 역변환 가능 - 각 operation은 리버스 Operation을 가짐
- 조합 가능 - operation은 transaction에서 조합 가능
- 검증 가능 - operation은 적용 전에 검증 가능
Operation 인터페이스
모든 operation은 공통 인터페이스를 따릅니다:
interface Operation {
type: string;
path: Path;
data?: any;
inverse?: Operation; // 효율성을 위해 사전 계산된 리버스 Operation
metadata?: {
source?: 'user' | 'programmatic';
timestamp?: number;
[key: string]: any;
};
}
// Path는 문서 트리의 위치를 나타냄
type Path = number[];
// 예제 경로:
// [0] - 루트의 첫 번째 자식
// [0, 1] - 첫 번째 자식의 두 번째 자식
// [0, 1, 2] - 첫 번째 자식의 두 번째 자식의 세 번째 자식Insert Operations
Insert operation은 특정 경로에 문서에 콘텐츠를 추가합니다.
Delete Operations
Delete operation은 문서에서 콘텐츠를 제거합니다.
Format Operations
Format operation은 콘텐츠를 변경하지 않고 텍스트 스타일을 수정합니다.
Replace Operations
Replace operation은 단일 operation에서 delete와 insert를 결합합니다.
Move Operations
Move operation은 문서 내에서 콘텐츠를 재배치합니다.
Node 구조 Operations
노드 구조와 관계를 수정하는 operation입니다.
Composite Operations
Composite operation은 복잡한 변환을 위해 여러 operation을 결합합니다.
Operation 역변환
모든 operation은 undo 기능을 위해 리버스 Operation을 가져야 합니다.
class OperationInverter {
invert(operation: Operation): Operation {
switch (operation.type) {
case 'insertText':
return {
type: 'deleteText',
path: operation.path,
length: operation.text.length
};
case 'deleteText':
return {
type: 'insertText',
path: operation.path,
text: operation.deletedContent || ''
};
case 'insertNode':
return {
type: 'deleteNode',
path: operation.path
};
case 'deleteNode':
return {
type: 'insertNode',
path: operation.path,
node: operation.deletedNode
};
case 'applyFormat':
return {
type: 'removeFormat',
path: operation.path,
length: operation.length,
format: operation.format
};
case 'removeFormat':
return {
type: 'applyFormat',
path: operation.path,
length: operation.length,
format: operation.format,
value: operation.previousValue
};
case 'replace':
return {
type: 'replace',
path: operation.path,
length: typeof operation.content === 'string'
? operation.content.length
: 1,
content: operation.deletedContent
};
case 'move':
return {
type: 'move',
fromPath: operation.toPath,
toPath: operation.fromPath
};
case 'splitNode':
return {
type: 'mergeNodes',
path: operation.path,
targetPath: [operation.path[0] + 1],
position: operation.position
};
case 'mergeNodes':
return {
type: 'splitNode',
path: operation.path,
position: operation.position
};
case 'wrap':
return {
type: 'unwrap',
path: operation.path,
wrapperType: operation.wrapper.type,
preservedWrapper: operation.wrapper
};
case 'unwrap':
return {
type: 'wrap',
path: operation.path,
wrapper: operation.preservedWrapper!
};
case 'updateAttributes':
return {
type: 'updateAttributes',
path: operation.path,
attributes: operation.previousAttributes || {},
previousAttributes: operation.attributes
};
case 'setNodeType':
return {
type: 'setNodeType',
path: operation.path,
nodeType: operation.previousType!,
previousType: operation.nodeType,
attributes: operation.previousAttributes,
previousAttributes: operation.attributes
};
default:
throw new Error(`알 수 없는 operation 타입: ${operation.type}`);
}
}
}
// 효율성을 위해 리버스 Operation 사전 계산
function createOperationWithInverse(operation: Operation): Operation {
const inverter = new OperationInverter();
return {
...operation,
inverse: inverter.invert(operation)
};
}