기술 문서

Delphi 및 HotXLS를 사용한 Excel 파일의 스레드 댓글 처리

Delphi에서 통합 문서를 생성하고 기존의 클래식 댓글 API를 사용하여 셀에 메모를 달면 Excel 365는 이를 노란색 상자 형태의 기존 메모(Notes)로 분류합니다. 이 메모에는 답장 입력란, 작성자 아바타 또는 해결 버튼이 제공되지 않습니다. HotXLS는 XLSX 엔진의 AddThreadedCommentAddThreadedReply를 통해 실제 Excel 365 스레드 댓글(threaded comments)을 작성합니다. 시트마다 전용 xl/threadedComments/threadedCommentN.xml 파트를 내보내고 통합 문서 수준의 person 파트에 모든 작성자를 등록하여, 코드가 작성한 대화 구조가 Excel에서 리뷰어가 답글을 달 수 있는 실제 스레드 댓글 객체와 정확히 동일하게 연동됩니다

두 기능은 겉보기에는 비슷하지만 완전히 다르기 때문에 명확한 구분이 필요합니다. 메모(note)는 단순히 떠 있는 텍스트 상자이지만, 스레드 댓글은 고유한 식별자, 회신 트리, 작성자 레지스트리 및 해결 완료 플래그 등을 갖는 실제 대화 기록이며 이는 리뷰 워크플로가 정상적으로 구동되는 데 필수적입니다. 본 기사에서는 먼저 OOXML 구조를 살펴보겠습니다. 두 저장소 모델을 비교해 보고 나면 Excel에서의 동작 방식 차이를 명확하게 이해할 수 있기 때문입니다

Excel 365에서 내 댓글이 메모로 표시되는 이유는 무엇입니까?

간단히 말해, 두 세대의 댓글이 하나의 스프레드시트 격자를 공유하지만 전혀 다른 패키지 파트에 저장되기 때문입니다. AddComment가 타겟팅하는 기존 모델은 텍스트를 xl/comments1.xml(콘텐츠 유형 application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml)에 저장하고, 풍선 도움말의 위치를 보조 VML 드로잉 파트인 xl/drawings/vmlDrawing1.vml을 통해 제어합니다. 이 조합은 리본 메뉴 도입 이전에 만들어진 규격입니다; Excel 365는 이를 정상적으로 읽어 들이기는 하지만 저장 구조상 스레드를 구성할 방법이 없으므로 단순 "메모"로 렌더링하고 관련 UI를 제공하지 않습니다. 작성자 정보는 단순 표시용 문자열에 불과하고 위치는 VML 도형 정보로 고정되어 기술되는 것이 구조의 전부입니다

Excel 365에 도입된 스레드 댓글은 VML 레이어를 완전히 배제합니다. 대화가 포함된 개별 시트는 application/vnd.openxmlformats-officedocument.spreadsheetml.threadedComments+xml 콘텐츠 유형 아래에 고유한 xl/threadedComments/threadedCommentN.xml 파트를 가지며, 통합 문서는 참가자 명단을 한 번씩 열거하는 단 하나의 xl/person/person.xml 파트(콘텐츠 유형 ...spreadsheetml.people+xml)를 포함합니다. HotXLS는 동일한 워크시트 상에 두 모델을 모두 제공합니다: AddComment는 기존 메모를 계속 생성하며, AddThreadedComment는 새로운 스레드 댓글 파트를 생성합니다. 만약 생성한 댓글이 메모로 나타난다면 두 번째 API 대신 첫 번째 API를 호출했기 때문입니다. 기존 모델도 특정 시나리오에서는 유용한 도구입니다 — 리뷰 워크플로에서의 셀 메모 및 하이퍼링크 기사에서 XLS 포맷 사양을 포함해 이를 심도 있게 다룹니다 — 하지만 기존 구조 자체를 스레드 댓글로 자동으로 업그레이드할 수는 없습니다

HotXLS가 패키지에 작성하는 데이터 구조

HotXLS는 각 회신 댓글을 셀 앵커(Ref), GUID 스타일 식별자(Id), 선택적인 ParentId, person 목록을 통해 해결되는 AuthorId, DisplayName 미러, 텍스트 본문, 해결 여부를 나타내는 Done 플래그 및 ISO-8601 형식의 DateTime을 포함하는 TXLSXThreadedComment 객체로 모델링합니다. 저장 시 각 항목은 t 속성에 식별자를 담고 dT 속성에 타임스탬프를 담으며, 답글에만 존재하는 parentT 속성에 상위 댓글 식별자를 명시하는 <threadedComment> 요소로 변환됩니다. Excel은 parentTt 값을 대조하여 대화 트리를 재구성합니다; XML 내부에는 중첩 구조가 없으며 단지 플랫한 리스트 정보와 참조 기호들로 구성됩니다

person 파트는 수동으로 구현할 때 가장 쉽게 누락되는 요소입니다. 스레드 댓글의 모든 authorId는 통합 문서 관계 정보와 연결되고 전용 콘텐츠 유형 재정의가 부여된 xl/person/person.xml<person> 항목과 성공적으로 매핑되어야 합니다. 그렇지 않으면 Excel에서 작성자의 표시 이름을 제대로 출력할 수 없습니다. HotXLS는 이 등록 과정을 자동으로 처리합니다: AddThreadedComment는 표시 이름을 기준으로 통합 문서의 Persons 목록에서 작성자를 검색하고, 존재하지 않는 경우 새로운 GUID와 함께 항목을 자동 생성하여 동일한 작성자가 연속해 기록한 댓글들이 하나의 ID를 공유하도록 제어합니다. 워크시트 관계에 threadedComments 관계가 추가되고, 통합 문서 관계에 person 관계가 주입되며, 콘텐츠 유형 선언 파일에 두 오버라이드 사양이 지정되는 일련의 과정이 개발자의 코드 개입 없이 자동으로 실행됩니다

AddThreadedComment 및 AddThreadedReply를 사용한 회신 체인 구축

AddThreadedComment는 대화의 최상위 루트(root)를 생성합니다: 상위 ID에 해당하는 ParentId가 비어 있으므로 최상위 스레드로 인식됩니다. AddThreadedReply는 루트가 위치한 셀 주소인 ParentRef 매개변수를 수용하여 해당 앵커에서 루트를 찾아내고, 새로 작성되는 회신 댓글의 ParentId에 루트 댓글의 Id를 부여합니다. 두 메서드 모두 새로 생성된 TXLSXThreadedComment 객체를 반환하므로, 생성 직후 객체의 타임스탬프나 해결 완료 여부 등을 지정할 수 있습니다:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Root: TXLSXThreadedComment;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('forecast.xlsx');
    Sheet := Book.Sheets[0];

    // Conversation root; ParentId stays empty
    Root := Sheet.AddThreadedComment(8, 3,
      'Q3 figure looks low against the pipeline export', 'Maria Ortiz');
    Root.DateTime := '2026-07-06T09:12:00Z';

    // Replies anchor at the same cell; ParentRef names the root cell
    Sheet.AddThreadedReply(8, 3, Root.Ref,
      'Pipeline export missed the EMEA renewals, re-running', 'Jan Kowalski');
    Sheet.AddThreadedReply(8, 3, Root.Ref,
      'Confirmed, refreshed figure lands tomorrow', 'Maria Ortiz');

    Book.SaveAs('forecast-reviewed.xlsx');
  finally
    Book.Free;
  end;
end;

위 코드 목록에서 두 가지 주목할 특징이 있습니다. 첫째, 직접 작성한 문자열 대신 자동으로 생성된 Root.Ref가 전달되었습니다: 라이브러리가 생성한 참조는 AddThreadedReply가 조회할 형식과 일치함이 보장되므로 앵커 주소가 어긋나는 오차 버그를 완벽히 막아줍니다. 둘째, 두 작성자 이름이 단순 표시용 문자열 형태로 전달되었습니다 — person 레지스트리 구성, GUID 지정 및 authorId 매핑 과정이 메서드 호출 내부에서 일괄적으로 자동 수행된 결과입니다. 저장된 파일을 Excel 365에서 열면 셀 앵커 연결, 답글 표시 순서 및 두 참가자의 정보가 정상적으로 나타나며, 사용자가 [답장] 버튼을 클릭하면 코드에서 구성한 스레드에 새로운 대화를 덧붙일 수 있습니다

스레드 대화 데이터 읽기: 라운드 트립 동작 방식

HotXLS는 버전 2.122.0부터 스레드 댓글의 라운드 트립을 지원합니다: 저장된 패키지를 다시 열면 시트별 대화 리스트와 통합 문서 참가자 목록이 저장 정보로부터 재구성되므로, 식별자, 회신 링크 및 표시 이름 등이 전체 읽기→저장 주기 동안 안정적으로 보존됩니다. 리더 모듈은 워크시트 파트를 읽기 전에 person.xml을 먼저 파싱하므로 로드 단계에서 모든 authorId가 올바른 표시 이름으로 매핑되며, 파싱된 참가자 식별자는 재생성되지 않고 기존 형식을 고수하여 Excel과 코드 간에 통합 문서를 반복해서 편집하더라도 안정적인 작성자 ID가 유지됩니다

var
  i: Integer;
  Cmt: TXLSXThreadedComment;
begin
  Book.Open('forecast-reviewed.xlsx');
  Sheet := Book.Sheets[0];
  for i := 0 to Sheet.ThreadedComments.Count - 1 do
  begin
    Cmt := Sheet.ThreadedComments[i];
    if Cmt.ParentId = '' then
      Writeln('Root at ', Cmt.Ref, ' by ', Cmt.DisplayName, ': ', Cmt.Text)
    else
      Writeln('  reply by ', Cmt.DisplayName, ': ', Cmt.Text);
  end;
  Writeln('Participants: ', Book.Persons.Count);
end;

플랫 리스트와 참조 관계 기반의 저장 구조가 API에도 직접 투영되어 나타납니다: 즉 ThreadedComments는 저장된 순서대로 답글들을 나열하며, ParentId가 비어 있는 속성 정보가 각각의 루트 스레드임을 알려줍니다. 만약 대화 트리의 계층 구조(예: 리뷰 로그 내보내기용)가 필요하다면, 먼저 Ref 주소를 기준으로 그룹화한 후 각 셀 그룹 내부에서 ParentIdId를 체인 구조로 묶어 활용하십시오. 파싱한 형식을 그대로 보존하는 이 원칙은 라이브러리 전반에 적용되는 기본 방침입니다; 테마, 확장 목록 및 계산 체인의 손실 없는 라운드 트립 기사에서 이와 동일한 설계 방식이 패키지 전반에 걸쳐 어떻게 구현되어 있는지 설명합니다

해결 상태, 타임스탬프 및 작성자 식별 데이터

리뷰 의미 정보를 전달하는 3가지 속성이 제공되며, 모두 반환되는 객체 상에서 쓰기가 가능합니다. Done은 XML의 done 속성과 매핑되어 Excel에서 해결 완료된 스레드를 축소하고 [스레드 다시 열기] 링크와 함께 숨김 처리해 줍니다. DateTime is the ISO-8601 UTC creation stamp (dT); HotXLS substitutes a fixed placeholder when you leave it empty so the part always validates, but a real timestamp is what makes the thread history readable, so set it. On the identity side, each TXLSXPerson exposes ProviderId — typically None for locally authored entries — and a UserId you can fill with a UPN or email when your application knows it:

var
  Root: TXLSXThreadedComment;
  Person: TXLSXPerson;
begin
  Root := Sheet.ThreadedComments.FindAt('D9');
  if Root <> nil then
    Root.Done := True;   // saved as done="1"; Excel shows the thread resolved

  Person := Book.Persons.FindByDisplayName('Maria Ortiz');
  if Person <> nil then
    Person.UserId := 'maria.ortiz@example.com';
end;

명확하게 명시할 한 가지 제약 조건은 HotXLS가 <mentions> 요소를 항상 빈 상태로 기록한다는 점입니다. @-mention 레코드 기능은 모델링되지 않아 @ 기호가 들어간 문자열은 단순히 일반 텍스트로 보존됩니다. 생성된 리뷰 통합 문서의 경우에는 이것이 큰 제약이 되지는 않지만, 언급 알림 메커니즘을 연동하려 한다면 사용자가 직접 Excel 내부에서 해당 멘션을 추가하는 수동 가이드라인을 수립해야 합니다. Person 파트의 작성자 식별 정보는 생성된 파일의 감사 추적(audit trail)에 활용되는 다른 메타데이터와 조화를 이루며, 이 메타데이터는 통합 문서 메타데이터 및 문서 속성 기사에서 자세히 설명합니다

이전 Excel 버전은 스레드 댓글을 어떻게 처리합니까?

Excel 2016 및 이전 버전은 스레드 모델 도입 전에 제작되었으므로 자체적으로 해석할 수 없는 파트를 무시하고 건너뜁니다. Excel 365의 경우에는 스레드 대화 내용을 저장할 때 하위 호환성 확보를 위해 "[Threaded comment]..."와 같은 안내 텍스트를 포함하는 기존의 섀도 메모(legacy shadow note)를 함께 기록하여 이전 버전 리더 프로그램에서도 셀에 메모 지시자가 출력되도록 배려합니다. HotXLS는 기존 메모 섀도 정보 없이 스레드 댓글 파트만 생성하므로, 코드에서 생성한 문서를 Excel 2016에서 열면 댓글 표시가 전혀 보이지 않습니다. 이전 365 버전 환경을 고려해야 하고 댓글 지시자가 반드시 표시되어야 하는 업무라면 기존 AddComment API를 사용하는 것이 호환성 확보에 유리합니다; 단, 동일한 셀에 두 모델을 혼용해 배치하는 방식은 Excel 버전에 따라 오작동할 우려가 있으므로 사전에 철저한 검증이 필요합니다

또 하나의 명확한 기술 한계는 파일 포맷 자체입니다. 스레드 댓글은 OOXML 기반 규격으로 BIFF8 사양에 부합하는 대응 모델이 없으므로, .xls 바이너리 기반의 HotXLS 모듈에는 스레드 댓글 관련 API가 존재하지 않으며 기존 메모(note)만 사용 가능합니다. 현실적인 선택 기준은 다음과 같이 명확합니다: Excel 365 또는 웹/모바일 Excel 리더용으로 .xlsx를 작성 중이라면 AddThreadedComment를 사용해 실제 대화 기능을 제공하십시오; Excel 2016 또는 .xls 바이너리를 타겟팅하고 있다면 AddComment를 사용해 기존 메모 형식을 적용하십시오; 수신된 파일을 점검할 때는 두 모델이 함께 탑재되어 있을 가능성이 있으므로 ThreadedCommentsComments를 모두 체크해 처리하십시오. 두 세대의 전체 API 사양은 HotXLS Delphi Excel Component 제품 문서 페이지에서 제공됩니다