💡 전체 작업 예제는 GitHub에서 확인할 수 있습니다: office-metadata-pii-cleanup-nodejs

소개

업로드 엔드포인트는 직원이 업로드한 DOCX 파일을 받아 고객 티켓에 저장합니다. 텍스트는 문제 없지만 속성은 그렇지 않습니다. 파일에는 작성자를 나타내는 이름, 마지막 저장자를 나타내는 동료, 기업 템플릿에 포함된 부서 관리자, 그리고 SharePoint에서 가져왔기 때문에 승인자를 나타내는 정보가 들어 있습니다.

메타데이터 정화 스크립트는 파일이 저장되기 전에 이러한 속성을 삭제하고 자체 검증을 수행하는 작은 스크립트입니다. 이 튜토리얼에서는 GroupDocs.Metadata를 사용해 Node.js로 네 단계에 걸쳐 스크립트를 만드는 방법을 보여줍니다: 태그별로 속성 선택, 이름별로 속성 선택, 선택적 삭제가 도움이 안 될 때 전체 삭제, 그리고 남은 내용을 검증합니다. 각 단계는 몇 줄에 불과하며 완성된 스크립트는 100줄 미만입니다.

메타데이터 정화가 중요한 이유

데이터는 아무도 선택하지 않아도 누적됩니다. Word는 매 저장 시 운영 체제 계정으로부터 AuthorLastSavedBy를 기록하고, 수정 횟수 카운터, TotalEditingTime, LastPrinted 등을 유지합니다. 문서 서버는 워크플로 경로, 승인자 식별자, 콘텐츠 유형 URI 등을 체크인 시 추가합니다. 이러한 정보는 문서를 읽거나 인쇄할 때 나타나지 않으므로 교정 과정에서 잡히지 않습니다.

수작업이 아닌 Node.js 스크립트로 처리하는 이유는 스크립트가 숫자를 반환하기 때문입니다. 각 삭제 호출은 삭제된 속성 수를 보고하고, 이 수치를 로그에 기록하거나 테스트에서 검증하거나 문서와 연결된 레코드에 첨부할 수 있습니다.

두 번째 이유는 배치 작업을 수행할 때 더 명확해집니다. 수동 정화는 파일마다 담당자가 한 번씩 결정하기 때문에 같은 종류의 문서를 두 사람이 정화하면 결과가 달라질 수 있습니다. 스크립트는 규칙을 한 곳에 고정합니다: 동일한 네 개의 태그, 동일한 부분 문자열 목록을 사용해 큐에 파일이 3개든 3천 개든 동일하게 적용합니다.

사전 요구 사항

패키지는 Java를 통해 Node.js에서 동작하므로 머신에 Java 런타임과 Node가 모두 필요합니다.

설치

npm install @groupdocs/groupdocs.metadata

샘플 프로젝트는 버전 26.7을 고정하고 overrides 항목에 nan^2.22.0으로 설정하여 현재 Node 릴리스에서도 네이티브 바인딩이 빌드되도록 합니다. 라이선스 파일이 없으면 라이브러리는 평가 모드로 실행되며, 여기서 설명하는 모든 단계를 수행하기에 충분합니다.

1단계 – 의미에 따라 속성 선택

포맷과 패키지마다 속성 이름이 다르므로 첫 번째 규칙은 태그로 매칭합니다. ContainsTagSpecification은 태그를 받아 해당 태그가 포함된 모든 속성과 매칭하고, .or()는 여러 사양을 하나로 합칩니다.

const T = groupdocs.Tags;
const spec = new groupdocs.ContainsTagSpecification(T.getPerson().getCreator())
  .or(new groupdocs.ContainsTagSpecification(T.getPerson().getEditor()))
  .or(new groupdocs.ContainsTagSpecification(T.getPerson().getManager()))
  .or(new groupdocs.ContainsTagSpecification(T.getCorporate().getCompany()));
const affected = metadata.removeProperties(spec);
metadata.save(outputPath);

핵심 포인트:

  • 네 개의 태그가 신원 그룹을 포괄합니다: creator, editor, manager, corporate company 필드.
  • Title, Subject, Keywords는 건드리지 않으므로 이들 필드로 인덱싱된 레코드가 계속 작동합니다.
  • removeProperties는 불리언이 아니라 영향을 받은 개수를 반환합니다.

전체 코드를 try/finally 블록으로 감싸고 finally 안에서 metadata.close()를 호출하세요. 바인딩은 파일을 그때까지 열어 두며, 이를 놓치면 루프가 핸들을 다 써버립니다.

2단계 – 이름으로 속성 선택

댓글 스레드, 수정 카운터, 서버 필드에는 태그가 없습니다. 이런 경우 WithNameSpecification(needle, false)가 이름에 해당 문자열이 포함된 모든 속성과 매칭합니다. 네 줄짜리 빌더를 사용해 각 부분 문자열마다 체인합니다:

let spec = null;
for (const needle of needles) {
  const s = new groupdocs.WithNameSpecification(needle, false /* fullMatch */);
  spec = spec ? spec.or(s) : s;
}
return spec;

세 번의 패스에서 서로 다른 리스트를 사용해 이 빌더를 재활용합니다. 먼저 댓글을 처리합니다:

const affected = metadata.removeProperties(
  nameContainsSpec(['Comment', 'Reviewer', 'Reviewed']));
metadata.save(outputPath);

편집 타임라인은 종종 간과되는 그룹이며, 문서가 어떻게 제작됐는지를 설명합니다:

const affected = metadata.removeProperties(nameContainsSpec([
  'Revision', 'TrackedChange', 'LastPrinted', 'TotalEditingTime', 'EditTime',
]));
metadata.save(outputPath);

SharePoint 패스는 Server, Workflow, Approver, ContentType, Template을 추가로 지정한 동일한 호출입니다. 부분 문자열 매칭을 의도적으로 사용한 이유는 CommentsCountComment를 정확한 이름 리스트를 포맷별로 유지하지 않아도 잡아내기 위함입니다.

3단계 – 선택적 삭제가 도움이 안 될 때 전체 삭제

조직을 떠나는 복사본에 대해서는 네 개의 패스를 하나의 호출로 대체합니다:

const affected = metadata.sanitize();
metadata.save(outputPath);

sanitize()는 라이브러리가 감지하는 모든 메타데이터 패키지를, 사용자 정의 OOXML 파트까지 포함해, 모두 삭제합니다. 반환되는 개수는 보통 목표로 한 패스들의 합보다 큽니다. 또한 Title과 Subject도 삭제하므로 검토 루프가 아니라 경계 단계에서 사용해야 합니다.

4단계 – 검증 (조용히 놓친 것이 성공처럼 보일 수 있음)

스캔은 findProperties를 통해 같은 사양을 재사용합니다. findProperties는 쓰지 않고 읽기만 합니다. 결과는 Java 컬렉션이므로 인덱스로 순회합니다:

const props = metadata.findProperties(tagSpec.or(nameSpec));
for (let i = 0; i < props.getCount(); i++) {
  const p = props.get_Item(i);
  const val = p.getValue && p.getValue();
  const value = val ? String(val.getRawValue ? val.getRawValue() : val) : '';
  if (!value || value === '0' || value === '0.0') continue;
  leaks.push(`${p.getName()}=${value}`);
}

빈값·0값 필터는 그만한 이유가 있습니다. 0으로 초기화된 수정 카운터가 남아 있었을 때 이 필터를 추가했으며, 스캔이 살아있는 속성으로 정확히 보고 있었습니다.

전체 작업 예제

레포지토리는 여섯 개 함수를 index.js에 연결합니다. 여기서는 라이선스를 적용하고, resources/pii-sample.docx에 대해 각 패스를 실행하며, 모든 출력 파일이 존재하는지 확인하고, 누수 리스트가 비어 있는지 검증합니다. 검증에 실패하면 비정상 종료 코드가 반환되므로 CI에서 체크 단계로 활용할 수 있습니다.

복사본에 적용하기 전에 기억해 둘 점: 각 패스는 동일한 원본 파일을 읽고 별도의 출력 파일을 작성합니다. 이렇게 하면 영향을 받은 개수가 독립적으로 유지되어, 댓글 패스의 로그 라인은 신원 규칙 실행 후 남은 것이 아니라 댓글 규칙이 찾은 것을 보고합니다.

언제 sanitize() 대신 타깃 패스를 실행해야 할까?

문서가 아직 사용 중일 때입니다. 리뷰어 사이를 오가는 파일은 검색 및 분류를 위해 Title, Subject, Keywords를 필요로 하는데, sanitize()는 이 세 가지와 개인 데이터를 모두 삭제합니다. 협업 중에는 신원 및 댓글 패스를 실행해 설명 필드를 유지하고, 실제 배포용 복사본에 대해서만 전체 삭제를 수행하세요.

실제 적용 사례

업로드 핸들러

Express 라우트가 첨부 파일을 저장하기 전에 정화하고, 티켓에 영향을 받은 개수를 기록하며, 누수 리스트가 비어 있지 않으면 업로드를 거부합니다.

야간 내보내기 작업

워커가 내보내기 폴더를 순회하면서 신원 및 서버 패스를 적용하고, 문서에 잔여 PII가 남아 있으면 경고를 로그에 남기는 대신 작업을 실패시킵니다.

사전 공개 게이트

빌드 단계에서 문서 첨부 파일을 릴리스 전에 정화합니다. 이때는 메타데이터를 보존할 필요가 없으므로 sanitize()를 사용합니다.

모범 사례 및 팁

  • 원본이 분쟁 해결을 위해 보존될 수 있도록 항상 새로운 경로에 기록하세요.
  • 특히 루프 안에서는 finally 블록에서 메타데이터 객체를 닫으세요.
  • 배치 작업에서는 사양을 .or() 로 병합하세요; 하나의 열기와 하나의 저장이 네 번보다 효율적입니다.
  • 패스별 영향을 받은 개수를 로그에 남기고, 0도 포함하세요. 인식되지 않은 포맷을 쉽게 확인할 수 있습니다.

일반적인 문제 해결

문제가 있는 문서에서 영향을 받은 개수가 0인 경우
입력 포맷이 인식되는지 확인하세요. 읽을 수 없는 파일과 깨끗한 파일은 모두 0을 반환합니다.

누수 검사가 방금 삭제한 속성을 보고하는 경우
입력 경로가 아니라 저장된 출력 경로를 지정하세요. 스캔은 전달된 파일을 읽습니다.

Word에서 댓글 풍선이 여전히 보이는 경우
댓글 텍스트는 메타데이터 패키지가 아니라 문서 본문에 존재합니다. GroupDocs.Metadata는 댓글 관련 속성을 삭제하지만 풍선을 없애려면 Aspose.Words와 같은 콘텐츠 편집 라이브러리가 필요합니다.

결론

네 단계, 여섯 함수, 한 스크립트가 수행한 작업을 보고합니다. 태그 사양은 포맷을 가로질러 신원 그룹을 처리하고, 이름 사양은 태그가 분류하지 못하는 그룹을 커버하며, sanitize()는 경계 영역을 담당하고, 누수 스캔은 전체 과정을 검증 단계로 전환합니다. 레포지토리를 복제하고 실제 리뷰 라운드를 거친 문서에 실행해 보세요. 카운트를 확인한 뒤 파이프라인에 필요한 패스를 결정하면 됩니다.

추가 자료