💡 Full working example available on GitHub:
office-metadata-pii-cleanup-nodejs
Introduction
업로드 엔드포인트는 직원이 업로드한 DOCX 파일을 받아 고객 티켓에 저장합니다. 텍스트는 문제 없지만 속성은 그렇지 않습니다. 파일에는 작성자를 나타내는 이름, 마지막 저장자를 나타내는 이름, 기업 템플릿에 포함된 부서 관리자, 그리고 SharePoint에서 가져왔기 때문에 승인자를 나타내는 정보가 들어 있습니다.
메타데이터 정화 스크립트는 파일이 저장되기 전에 이러한 속성을 삭제하고, 자체 검증을 수행하는 작은 스크립트입니다. 이 튜토리얼에서는 Node.js와 GroupDocs.Metadata를 사용해 네 단계로 스크립트를 만드는 방법을 보여줍니다: 태그로 속성 선택, 이름으로 속성 선택, 선택적 삭제가 도움이 안 될 때 전체 삭제, 그리고 남은 내용을 검증합니다. 각 단계는 몇 줄에 불과하며, 완성된 스크립트는 100줄 미만입니다.
Why Metadata Sanitization Matters
데이터는 아무도 선택하지 않아도 누적됩니다. Word는 매 저장 시 운영 체제 계정으로부터 Author와 LastSavedBy를 기록하고, 수정 횟수 카운터, TotalEditingTime, LastPrinted 등을 유지합니다. 문서 서버는 워크플로 경로, 승인자 식별자, 콘텐츠 유형 URI 등을 체크인 시 추가합니다. 이러한 정보는 문서를 읽거나 인쇄할 때 나타나지 않으므로 교정 과정에서 잡히지 않습니다.
수동으로가 아니라 Node.js 스크립트로 처리하면 숫자를 반환합니다. 각 삭제 호출은 삭제된 속성 수를 보고하고, 이 카운트를 로그에 기록하거나 테스트에서 검증하거나 해당 문서와 연결된 레코드에 첨부할 수 있습니다.
두 번째 이유는 배치 작업이 실행될 때 더 명확해집니다. 수동 정리는 파일마다 담당자가 한 번씩 결정하기 때문에 같은 종류의 문서를 두 사람이 정화하면 결과가 달라질 수 있습니다. 스크립트는 규칙을 한 곳에 고정합니다: 동일한 네 개의 태그, 동일한 부분 문자열 목록을 사용해 큐에 3개의 파일이 있든 3천 개의 파일이 있든 동일하게 적용됩니다.
Prerequisites
패키지는 Java 기반 Node.js이므로, 머신에 Java 런타임과 Node가 모두 필요합니다.
Installation
npm install @groupdocs/groupdocs.metadata
샘플 프로젝트는 버전 26.7을 고정하고 overrides 항목에 nan을 ^2.22.0으로 설정합니다. 이는 현재 Node 릴리스에서도 네이티브 바인딩이 빌드되도록 합니다. 라이선스 파일이 없으면 라이브러리는 평가 모드로 실행되며, 여기서 설명하는 모든 단계를 수행하기에 충분합니다.
Step 1 - Select properties by what they mean
포맷과 패키지마다 속성 이름이 다르므로, 첫 번째 규칙은 태그로 매칭합니다. 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);
Key points:
- 네 개의 태그가 신원 그룹을 포괄합니다: creator, editor, manager, corporate company 필드.
- Title, Subject, Keywords는 건드리지 않으므로, 이를 키로 사용하는 레코드 인덱스가 계속 작동합니다.
removeProperties는 불리언이 아니라 영향을 받은 개수를 반환합니다.
전체 코드를 try / finally 로 감싸고 finally 블록에서 metadata.close() 를 호출하세요. 바인딩은 파일을 열어 두고 있기 때문에, 이를 닫지 않으면 루프 중에 핸들이 부족해집니다.
Step 2 - Select properties by name
댓글 스레드, 수정 카운터, 서버 필드에는 태그가 없습니다. 이런 경우 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 을 포함한 동일한 호출입니다. 부분 문자열 매칭을 의도적으로 사용한 이유는 CommentsCount 와 Comment 를 정확한 이름 리스트를 유지하지 않아도 동시에 잡아내기 위함입니다.
Step 3 - Wipe everything when selectivity stops helping
조직을 떠나는 복사본에 대해서는 네 개의 패스를 하나의 호출로 대체합니다:
const affected = metadata.sanitize();
metadata.save(outputPath);
sanitize() 는 라이브러리가 감지하는 모든 메타데이터 패키지를 삭제합니다. 커스텀 OOXML 파트도 포함되며, 반환되는 카운트는 보통 개별 패스들의 합보다 큽니다. 또한 Title과 Subject도 삭제하므로, 검토 루프가 아니라 경계 단계에서 사용해야 합니다.
Step 4 - Verify, because a silent miss looks like success
스캔은 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 으로 초기화된 수정 카운터가 남아 있었고, 스캔이 이를 살아있는 속성으로 보고했기 때문에 추가했습니다.
Complete Working Example
레포지토리는 여섯 개 함수를 index.js 에 연결합니다. 여기서는 라이선스를 적용하고, resources/pii-sample.docx 에 대해 각 패스를 실행하며, 모든 출력 파일이 존재하는지 검증하고, 누수 리스트가 비어 있는지 확인합니다. 검증에 실패하면 비정상 종료 코드가 반환되므로, 전체 흐름을 CI 체크로 활용할 수 있습니다.
한 가지 주의할 점은 각 패스가 동일한 원본 파일을 읽고 별도의 출력 파일을 만든다는 것입니다. 이렇게 하면 영향을 받은 카운트가 독립적으로 유지되어, 댓글 패스의 로그는 신원 규칙이 실행된 뒤 남은 것이 아니라 댓글 규칙이 찾은 내용을 정확히 보고합니다.
When should I run a targeted pass instead of sanitize()?
문서가 아직 사용 중일 때는 타깃 패스를 사용합니다. 리뷰어 사이를 오가는 파일은 검색 및 분류를 위해 Title, Subject, Keywords 를 필요로 하는데, sanitize() 는 이 세 가지와 개인 데이터를 모두 삭제합니다. 협업 중에는 신원 및 댓글 패스를 실행해 설명 필드를 유지하고, 실제 배포용 복사본에 대해서만 전체 삭제(sanitize())를 수행하세요.
Real-World Applications
Upload handler
Express 라우트가 첨부 파일을 저장하기 전에 정화하고, 티켓에 영향을 받은 카운트를 기록하며, 누수 리스트가 비어 있지 않으면 업로드를 거부합니다.
Nightly export job
워커가 내보내기 폴더를 순회하면서 신원 및 서버 패스를 적용하고, 문서에 남은 PII 가 있으면 경고 대신 작업을 실패시킵니다.
Pre‑publication gate
빌드 단계에서 문서 첨부 파일을 릴리스 전에 정화합니다. 여기서는 메타데이터를 보존할 필요가 없으므로 sanitize() 를 사용합니다.
Best Practices and Tips
- 원본 파일이 분쟁 해결을 위해 보존될 수 있도록 항상 새로운 경로에 기록하세요.
- 루프 안에서는 특히
finally블록에서 메타데이터 객체를 닫으세요. - 배치 작업에서는 사양을
.or()로 병합하세요. 하나의 열기와 하나의 저장이 네 번보다 효율적입니다. - 각 패스마다 영향을 받은 카운트를 로그에 남기고, 0도 포함하세요. 이렇게 하면 인식되지 않은 포맷을 쉽게 발견할 수 있습니다.
Troubleshooting Common Issues
The affected count is zero on a document you know is dirty
입력 파일 포맷이 인식되는지 먼저 확인하세요. 읽을 수 없는 파일과 깨끗한 파일은 동일하게 0을 반환합니다.
The leak check reports properties you just removed
입력 경로가 아니라 저장된 출력 경로를 스캔하도록 지정하세요. 스캔은 전달된 파일을 그대로 읽습니다.
Comment balloons are still visible in Word
댓글 텍스트는 메타데이터 패키지가 아니라 문서 본문에 존재합니다. GroupDocs.Metadata 는 댓글 관련 메타데이터만 삭제하므로, 풍선 자체를 제거하려면 Aspose.Words 와 같은 콘텐츠 편집 라이브러리가 필요합니다.
Conclusion
네 단계, 여섯 함수, 한 스크립트가 수행한 작업을 보고합니다. 태그 사양은 포맷을 가로질러 신원 그룹을 처리하고, 이름 사양은 태그가 분류하지 못하는 속성군을 커버합니다. sanitize() 는 경계 영역을 담당하며, 누수 스캔은 전체 흐름을 검증 단계로 전환합니다. 레포지토리를 복제하고 실제 리뷰 라운드를 거친 문서에 실행해 보세요. 카운트를 확인한 뒤 파이프라인에 필요한 패스를 결정하면 됩니다.