테이블 노드 타입

카테고리: 복잡 • 뷰 연동 노트를 포함한 상세 구현 가이드

스키마 정의

테이블 노드 타입의 스키마 정의:

{
  table: {
    content: 'tableRow+',
    group: 'block',
    attrs: {
      colCount: { default: 0 }
    }
  },
  tableRow: {
    content: '(tableCell | tableHeader)+'
  },
  tableCell: {
    content: 'block+',
    attrs: {
      colspan: { default: 1 },
      rowspan: { default: 1 }
    }
  },
  tableHeader: {
    content: 'block+',
    attrs: {
      colspan: { default: 1 },
      rowspan: { default: 1 }
    }
  }
}

모델 표현

모델 표현 예제:

{
  type: 'table',
  attrs: { colCount: 3 },
  children: [
    {
      type: 'tableRow',
      children: [
        {
          type: 'tableHeader',
          attrs: { colspan: 1, rowspan: 1 },
          children: [
            {
              type: 'paragraph',
              children: [{ type: 'text', text: '헤더' }]
            }
          ]
        }
      ]
    }
  ]
}

HTML 직렬화

모델을 HTML로 변환:

function serializeTable(node) {
  let html = '<table><tbody>';
  node.children.forEach(row => {
    html += serializeTableRow(row);
  });
  html += '</tbody></table>';
  return html;
}

function serializeTableRow(node) {
  let html = '<tr>';
  node.children.forEach(cell => {
    html += serializeTableCell(cell);
  });
  html += '</tr>';
  return html;
}

function serializeTableCell(node) {
  const tag = node.type === 'tableHeader' ? 'th' : 'td';
  const attrs = [];
  if (node.attrs.colspan > 1) {
    attrs.push('colspan="' + node.attrs.colspan + '"');
  }
  if (node.attrs.rowspan > 1) {
    attrs.push('rowspan="' + node.attrs.rowspan + '"');
  }
  return '<' + tag + (attrs.length ? ' ' + attrs.join(' ') : '') + '>' +
         serializeChildren(node.children) +
         '</' + tag + '>';
}

HTML 역직렬화

HTML을 모델로 파싱:

function parseTable(domNode) {
  const tbody = domNode.querySelector('tbody') || domNode;
  const rows = Array.from(tbody.querySelectorAll('tr'))
    .map(row => parseTableRow(row));
  
  // 열 개수 계산
  const colCount = Math.max(...rows.map(row => 
    row.children.reduce((sum, cell) => sum + (cell.attrs.colspan || 1), 0)
  ));
  
  return {
    type: 'table',
    attrs: { colCount },
    children: rows
  };
}

function parseTableRow(domNode) {
  const cells = Array.from(domNode.childNodes)
    .filter(node => node.tagName === 'TD' || node.tagName === 'TH')
    .map(cell => parseTableCell(cell));
  
  return {
    type: 'tableRow',
    children: cells
  };
}

function parseTableCell(domNode) {
  const type = domNode.tagName === 'TH' ? 'tableHeader' : 'tableCell';
  const colspan = parseInt(domNode.getAttribute('colspan')) || 1;
  const rowspan = parseInt(domNode.getAttribute('rowspan')) || 1;
  
  return {
    type,
    attrs: { colspan, rowspan },
    children: parseChildren(domNode.childNodes)
  };
}

뷰 연동

뷰 연동 노트: 이 노드 타입을 뷰 레이어에서 구현할 때 contenteditable 동작, 선택 처리, 이벤트 관리에 특히 주의하세요.

뷰 연동 코드:

// 렌더링
const table = document.createElement('table');
const tbody = document.createElement('tbody');
node.children.forEach(row => {
  const tr = renderTableRow(row);
  tbody.appendChild(tr);
});
table.appendChild(tbody);

// 셀 편집
function makeCellEditable(cell) {
  cell.contentEditable = 'true';
  cell.addEventListener('input', handleCellInput);
  cell.addEventListener('blur', handleCellBlur);
}

// 테이블 조작
function insertRow(table, index) {
  const rowCount = table.children[0].children.length;
  const newRow = createEmptyRow(rowCount);
  // 인덱스에 삽입
}

function insertColumn(table, index) {
  // 각 행의 인덱스에 셀 추가
}

일반적인 문제

일반적인 함정: 이 노드 타입을 구현할 때 자주 발생하는 문제들입니다. 구현 전에 주의 깊게 검토하세요.

일반적인 문제 및 해결 방법:

// 문제: 테이블 구조 검증
// 해결: 일관된 열 개수 보장
function validateTable(table) {
  const colCount = table.attrs.colCount;
  table.children.forEach(row => {
    const actualCols = row.children.reduce((sum, cell) => 
      sum + (cell.attrs.colspan || 1), 0
    );
    if (actualCols !== colCount) {
      // 열 개수 불일치 수정
    }
  });
}

// 문제: 빈 셀
// 해결: 항상 최소 하나의 블록 유지
if (cell.children.length === 0) {
  cell.children.push({
    type: 'paragraph',
    children: []
  });
}

// 문제: Colspan/rowspan 계산
// 해결: 병합된 셀 추적
function getCellAt(table, row, col) {
  // colspan/rowspan 고려
}

구현

완전한 구현 예제:

class TableNode {
  constructor(attrs, children) {
    this.type = 'table';
    this.attrs = { colCount: attrs?.colCount || 0 };
    this.children = children || [];
  }
  
  toDOM() {
    const table = document.createElement('table');
    const tbody = document.createElement('tbody');
    
    this.children.forEach(row => {
      tbody.appendChild(row.toDOM());
    });
    
    table.appendChild(tbody);
    return table;
  }
  
  static fromDOM(domNode) {
    const tbody = domNode.querySelector('tbody') || domNode;
    const rows = Array.from(tbody.querySelectorAll('tr'))
      .map(row => TableRowNode.fromDOM(row));
    
    const colCount = Math.max(...rows.map(row => 
      row.getColumnCount()
    ));
    
    return new TableNode({ colCount }, rows);
  }
}