💡 전체 작업 예제는 GitHub에서 확인할 수 있습니다:
metadata-diff-doc-versions-using-groupdocs-metadata-nodejs

소개

Metadata 비교는 GroupDocs.Metadata 기능으로, 문서 버전 간에 숨겨진 변화를 드러내어 감사자가 신뢰성을 빠르게 검증할 수 있게 합니다. 법무팀이 계약서가 변조되지 않았음을 입증해야 할 때, 저자, 편집자, 수정 타임스탬프와 같은 보이지 않는 속성이 실제 이야기를 전달합니다.

저는 공급업체 계약서 일괄 검토 중에 이 문제를 겪었습니다. 각 파일은 동일한 생성 날짜를 주장했지만, 숨겨진 저자 필드는 서로 달랐으며, 수동 감사를 수행하면 몇 시간이 걸렸을 것입니다.

법무팀에게 숨겨진 메타데이터가 중요한 이유

법률 및 컴플라이언스 전문가들은 변조할 수 없는 증거에 의존합니다. 눈에 보이는 텍스트가 동일하더라도 Creator 또는 LastPrinted 필드의 변경은 무단 편집을 나타낼 수 있습니다. 이러한 변화를 조기에 감지하면 비용이 많이 드는 분쟁을 예방하고 규제 감사 추적을 충족시킬 수 있습니다.

GroupDocs.Metadata를 활용한 솔루션

Node.js용 GroupDocs.Metadata는 모든 지원 속성을 추출하고 두 개의 리비전을 비교하며 구조화된 diff 보고서를 생성하는 단일 API를 제공합니다. SDK는 DOCX, PDF, XLSX 등 다양한 형식을 기본적으로 지원하므로 파일 형식마다 별도의 파서를 만들 필요가 없습니다.

1단계 – 문서에서 모든 메타데이터 추출

아래 헬퍼는 파일을 열고 전체 속성 트리를 순회하여 속성 이름 → 값 형태의 순수 JavaScript 맵을 반환합니다. AnySpecification을 사용해 기본 및 사용자 정의 필드를 모두 캡처합니다.

/** Extract all metadata from a file */
function extractAllMetadata(documentPath) {
  const result = {};
  const metadata = new groupdocs.Metadata(documentPath);
  try {
    const properties = metadata.findProperties(new groupdocs.AnySpecification());
    for (let i = 0; i < properties.getCount(); i++) {
      const p = properties.get_Item(i);
      const name = p.getName();
      const value = valueToString(p);
      result[name] = value;
    }
  } finally {
    metadata.close();
  }
  return result;
}

Performance note: 5 MB DOCX 파일에서 메타데이터를 추출하는 데 일반적인 2 GHz CPU [Docs][DOCS_URL] 기준 200 ms 미만이 소요됩니다.

2단계 – 두 리비전 간 차이점 찾기

두 버전을 순수 객체로 표현한 뒤, diff 루틴은 added, removed, changed 세 개의 버킷을 만들고 편리한 totalChanges getter를 제공합니다.

/** Compare two metadata maps */
function compareMetadataSets(pathV1, pathV2) {
  const v1 = extractAllMetadata(pathV1);
  const v2 = extractAllMetadata(pathV2);

  const added = {};
  const removed = {};
  const changed = {};

  for (const k of Object.keys(v2)) {
    if (!(k in v1)) added[k] = v2[k];
    else if (v1[k] !== v2[k]) changed[k] = { from: v1[k], to: v2[k] };
  }
  for (const k of Object.keys(v1)) {
    if (!(k in v2)) removed[k] = v1[k];
  }

  return {
    added,
    removed,
    changed,
    get totalChanges() {
      return Object.keys(this.added).length + Object.keys(this.removed).length + Object.keys(this.changed).length;
    },
  };
}

3단계 – 소유권 및 리비전 인사이트 (선택)

소유권 변경 감지

법률 감사자는 문서를 누가 생성하거나 편집했는지에 주목합니다. 이 헬퍼는 해당 필드만 추출해 변동이 있으면 표시합니다.

/** Ownership change detection */
function detectOwnershipChanges(pathV1, pathV2) {
  const v1 = readOwnership(pathV1);
  const v2 = readOwnership(pathV2);
  const all = new Set([...Object.keys(v1), ...Object.keys(v2)]);
  const changes = {};
  for (const k of all) {
    const oldV = v1[k] !== undefined ? v1[k] : '<missing>';
    const newV = v2[k] !== undefined ? v2[k] : '<missing>';
    if (oldV !== newV) changes[k] = { from: oldV, to: newV };
  }
  return changes;
}

리비전 히스토리 변경 감지

리비전 번호, 총 편집 시간, 마지막 인쇄 타임스탬프는 숨겨진 활동을 드러냅니다.

/** Revision history change detection */
function detectRevisionHistory(pathV1, pathV2) {
  const v1 = readRevision(pathV1);
  const v2 = readRevision(pathV2);
  const all = new Set([...Object.keys(v1), ...Object.keys(v2)]);
  const changes = {};
  for (const k of all) {
    const oldV = v1[k] !== undefined ? v1[k] : '<missing>';
    const newV = v2[k] !== undefined ? v2[k] : '<missing>';
    if (oldV !== newV) changes[k] = { from: oldV, to: newV };
  }
  return changes;
}

4단계 – 감사 보고서 내보내기

CSV 내보내기

CSV 형식은 Excel이나 SIEM에 적합합니다. 필드에 쉼표와 개행이 포함될 경우 이스케이프합니다.

/** Export diff to CSV */
function exportDiffToCsv(diff, outputPath) {
  const rows = ['change_type,property,old_value,new_value'];
  for (const [k, v] of Object.entries(diff.added)) rows.push(`added,${esc(k)},,${esc(v)}`);
  for (const [k, v] of Object.entries(diff.removed)) rows.push(`removed,${esc(k)},${esc(v)},`);
  for (const [k, o] of Object.entries(diff.changed)) rows.push(`changed,${esc(k)},${esc(o.from)},${esc(o.to)}`);
  fs.writeFileSync(outputPath, rows.join('\n') + '\n', 'utf-8');
}

JSON 내보내기

JSON은 계층 구조를 보존하여 프로그램에서 바로 활용할 수 있습니다.

/** Export diff to JSON */
function exportDiffToJson(diff, outputPath) {
  const payload = { added: diff.added, removed: diff.removed, changed: diff.changed };
  fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2), 'utf-8');
}

전체 실행 스크립트

스크립트는 모든 단계를 연결하고 간단한 요약을 출력한 뒤 CSV와 JSON 보고서를 모두 작성합니다.

const path = require('path');
const fs = require('fs');
const groupdocs = require('@groupdocs/groupdocs.metadata');

function esc(txt) { return /[",\n]/.test(txt) ? `"${txt.replace(/"/g, '""')}"` : txt; }

const diff = compareMetadataSets('resources/document-v1.docx', 'resources/document-v2.docx');
console.log(`Total metadata changes: ${diff.totalChanges}`);

exportDiffToCsv(diff, path.resolve('metadata-diff.csv'));
exportDiffToJson(diff, path.resolve('metadata-diff.json'));

node index.js를 실행하면 프로젝트 루트에 metadata-diff.csvmetadata-diff.json이 생성됩니다.

두 버전의