Operations

모델 기반 에디터에서 transaction으로 조합할 수 있는 operation 타입에 대한 포괄적인 가이드입니다.

개요

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)
  };
}