기술 문서

HotXLS Delphi Component: Delphi에서 template-based report generation

Delphi에서 스타일이 적용된 Excel 보고서를 안정적으로 만드는 방법은 디자이너가 이미 만들어 둔 워크북에서 출발하는 것입니다. 재무팀의 누군가가 Excel에서 청구서 레이아웃을 잡습니다. 로고, 열 헤더, 명세 밴드의 테두리, 굵은 합계 행, 통화 서식이 그것입니다. 여러분의 코드는 그 파일을 열고, 디자이너가 예약해 둔 셀에 실시간 데이터를 떨어뜨린 다음 결과를 저장합니다. 외관은 그들의 것이고, 숫자는 여러분의 것입니다. Excel을 구동하지 않고 XLS와 XLSX 워크북을 읽고 쓰는 네이티브 Delphi 및 C++Builder 라이브러리인 HotXLS는 이 접근 방식에 필요한 세 가지 연산을 제공합니다. 텍스트로 셀을 검색하는 것, 스타일과 수식을 그대로 유지한 채 범위를 복사하는 것, 그리고 데이터와 함께 아래의 모든 것이 밀려 내려가도록 행을 삽입하는 것입니다

템플릿 수정을 견뎌내는 생성기와 첫 번째 수정에서 무너지는 생성기를 가르는 단 하나의 규칙은 셀을 절대 리터럴 행/열 번호로 주소 지정하지 않는 것입니다. 템플릿은 다른 사람들이 수정하는 문서입니다. 재무팀은 세금 항목 줄을 추가하고, 로고 행의 높이를 올리고, 주소 블록의 순서를 바꾸지만, 파일 형식은 여러분을 조금도 도와주지 않습니다. 10행이 지난 분기와 같은 의미를 여전히 지니든 아니든, BIFF나 OOXML 저장은 성공합니다. 첫 번째 명세 라인을 하드코딩된 10행에 쓰는 생성기는, 누군가 명세 구역 위에 블록을 삽입하는 순간 잘못된 셀 위에 항목을 찍고, 더 이상 데이터를 덮지 않는 합계 범위를 더하게 됩니다. 아무것도 예외를 던지지 않고, 모든 저장은 성공을 반환하며, 유일한 신호는 고객이 잘못된 청구서를 발견하는 것뿐입니다

Delphi에서 HotXLS 템플릿 파이프라인 다이어그램: FindText로 토큰 앵커를 찾고, 상세 밴드를 확장하고, 계산된 합계를 검증한 뒤 저장
Delphi의 템플릿 보고서 생성은 네 가지 HotXLS 단계로 돕니다. 토큰을 고정하고, 상세 대역을 확장하고, 계산된 합계를 검증한 뒤 전달합니다

모든 좌표를 플레이스홀더 토큰에 고정하기

해결책은 템플릿이 자신의 좌표를 스스로 지니게 만드는 것입니다. 디자이너는 생성기가 건드려야 하는 셀에 {{CUSTOMER}}, {{DATE}}, {{DETAIL_START}} 같은 토큰을 써 넣고, 생성기는 이 토큰들을 찾은 위치로부터 실행 시점에 모든 위치를 계산해 냅니다. 이제 레이아웃 수정은 더 이상 문제가 되지 않습니다. 토큰이 자신이 자리한 셀과 함께 움직이기 때문입니다. 계약의 나머지 절반은 실패 규칙입니다. 필수 토큰이 없다면 어떤 고객 데이터도 파일에 도달하기 전에 작업이 멈춰야 합니다. 어긋난 템플릿은 전달된 문서가 아니라 실패한 작업 티켓을 만들어내야 합니다

토큰 찾기: FindText와 ReplaceText

두 HotXLS 클래스 군 모두 워크시트 수준의 검색을 제공합니다. FindText는 텍스트가 일치하는 첫 번째 셀의 행과 열을 반환하며, 대소문자 구분을 추가하는 오버로드도 있습니다. ReplaceText는 모든 발생 위치를 바꾸고 몇 개를 바꿨는지 반환합니다. 이 둘은 여러분이 흔히 가지게 되는 두 종류의 토큰을 커버합니다. 고객 이름처럼 한 번 찾아서 그 옆에 쓰는 단일 앵커, 그리고 보고서 날짜처럼 정확히 한 번만 나타나야 하는 토큰은 교체하고 개수를 확인하면 됩니다. XLSX 쪽에서 이런 식으로 스스로를 고정하는 채우기는 다음과 같습니다:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R, C: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('invoice-template.xlsx') <> 1 then
      raise Exception.Create('Cannot open invoice template');
    Sheet := Book.Sheets[0];               // TXLSXSheets.Items는 0부터 시작

    if not Sheet.FindText('{{CUSTOMER}}', R, C) then
      raise Exception.Create('Template drift: {{CUSTOMER}} anchor missing');
    Sheet.Cells[R, C].Value := 'ACME Corp';

    if Sheet.ReplaceText('{{DATE}}',
         FormatDateTime('yyyy-mm-dd', Date)) = 0 then
      raise Exception.Create('Template drift: {{DATE}} token missing');
    // 명세 확장과 저장은 아래에 이어집니다
  finally
    Book.Free;
  end;
end;

중요한 세부사항이 둘 있습니다. 첫째, FindTextReplaceText는 셀의 텍스트 값에 대해서만 일치를 확인합니다. 수식 문자열 안에 박혀 있는 토큰은 이들에게 보이지 않으므로, 플레이스홀더 토큰은 평범한 셀에만 두어야 하며 절대 수식 안에 두어서는 안 됩니다. 둘째, 교체 개수가 여러분의 드리프트 감지기입니다. 정확히 하나의 {{DATE}} 토큰을 포함해야 하는 템플릿이 0건의 교체를 보고한다면 수정이 가해진 것이며, 그 순간 예외를 던지는 것이야말로 조용한 레이아웃 드리프트를 눈에 보이는 실패로 바꾸는 방법입니다

스타일이나 수식을 잃지 않고 명세 행을 복제하기

청구서의 명세 구역은 데이터와 함께 자라납니다. 견본 라인 아래 빈 행에 값을 곧바로 쓰면 디자이너가 준비한 모든 것, 즉 테두리, 숫자 서식, 행별 수식이 사라집니다. 이 모든 것을 유지하는 패턴은 완전히 서식이 적용된 견본 행을 템플릿에 하나 남겨두고 항목마다 이를 복제하는 것입니다. CopyRange는 한 번의 호출로 스타일과 수식을 복제하며, 그런 다음 생성기는 값 셀만 덮어씁니다

HotXLS Delphi 템플릿의 토큰 앵커 다이어그램: 빠진 플레이스홀더가 데이터가 쓰이기 전에 작업을 실패시킴
템플릿 토큰은 자체 좌표를 지니며, 빠진 토큰은 어떤 데이터도 쓰이기 전에 작업을 멈춥니다
const
  DetailRow = 10;            // 템플릿의 서식이 적용된 견본 행
var
  I: Integer;
begin
  // 먼저 합계 블록 앞에 공간을 열어서, 명세 밴드 아래의
  // SUM 범위가 데이터와 함께 늘어나게 합니다.
  if Length(Items) > 1 then
    Sheet.InsertRows(DetailRow + 1, Length(Items) - 1);

  for I := 0 to High(Items) do
  begin
    if I > 0 then              // 견본 행에서 스타일 + 수식을 복제
      Sheet.CopyRange(DetailRow, 1, DetailRow, 5, DetailRow + I, 1);
    Sheet.Cells[DetailRow + I, 1].Value := Items[I].Name;
    Sheet.Cells[DetailRow + I, 2].Value := Items[I].Qty;
    Sheet.Cells[DetailRow + I, 3].Value := Items[I].UnitPrice;
    Sheet.Cells[DetailRow + I, 4].Formula :=
      Format('B%d*C%d', [DetailRow + I, DetailRow + I]);  // '=' 접두사 없음
  end;
end;

수식 대입을 주의 깊게 살펴보십시오. XLSX의 Formula 속성은 등호 접두사 없이 표현식을 받는 반면, XLS 파사드는 Value를 통해 대입되는 '=B10*C10'을 기대합니다. 두 관례를 섞는 것은 두 클래스 군 사이에서 가장 흔한 이식 실수이며, 아무 불평 없이 실패합니다. 셀은 그저 Excel이 텍스트로 보여주는 리터럴 문자열을 담게 될 뿐입니다. 템플릿이 병합된 제목 행으로 명세 밴드를 꾸미고 있다면, 병합 영역의 좌측 상단 셀만 값을 지닌다는 점을 기억하십시오. 레이아웃 기반 보고서 템플릿에서의 병합 셀에 관한 관련 문서는 왜 병합 영역이 데이터 밴드 완전히 바깥에 있어야 하는지를 설명합니다

InsertRows가 옮기는 것과 뒤에 남기는 것

합계 블록 앞에 행을 삽입하는 것이 명세 구역이 자랄 때 SUM 범위가 함께 늘어나게 만드는 방법입니다. XLSX 쪽에서 InsertRows는 셀과 함께 긴 목록의 종속 구조를 함께 끌고 내려갑니다. 병합된 범위, 행 높이, 하이퍼링크, 코멘트, 고정 창, 자동 필터 범위, 조건부 서식, 데이터 유효성 검사, 표, 정의된 이름, 이미지와 차트 앵커가 그것입니다. 이 목록에는 기억해 둘 가치가 있는 경계가 하나 있습니다. 수식 재작성은 같은 시트 안의 참조에만 미칩니다. 요약 시트의 수식이 이동한 영역을 가리키고 있다면 옛 좌표를 그대로 유지한 채 조용히 잘못된 셀을 읽으며, 이것이 시트를 넘나드는 합계를 워크북 수준의 이름을 통해 표현하는 것이 더 안전한 이유입니다. 정의된 이름과 시트 간 수식에 관한 관련 문서가 이 패턴을 자세히 다룹니다

레거시 XLS 형식은 더 단단한 곳에 선을 긋습니다. HotXLS는 피벗 테이블, 쿼리 테이블, 외부 데이터 연결을 BIFF 파일 안에 원시 바이트 블록으로 유지합니다. 이들은 열기와 저장을 거치며 변하지 않은 채 살아남지만 모델링되지는 않으므로, 행 삽입은 이들을 절대 건드리지 않습니다. 확장되는 명세 블록 아래에 피벗 테이블을 배치한 템플릿은 아무 경고도 없이 저장되지만 피벗 원본 사각형은 데이터에서 점점 멀어집니다. 빠져나갈 길은 방어적인 처리가 아니라 구조적인 처리입니다. 피벗과 쿼리 콘텐츠를 생성기가 절대 삽입하지 않는 시트에 두면 이런 노후화는 애초에 일어날 수 없습니다

HotXLS InsertRows가 XLSX에서 무엇을 옮기는지와, Delphi 제너레이터가 존중해야 하는 교차 시트 수식과 BIFF 피벗 경계 다이어그램
InsertRows는 XLSX에서 종속 구조를 아래로 끌고 가는 반면, 시트 간 수식과 BIFF 원시 블록이 경계를 표시합니다

전달 전에 재계산하거나, 왜 건너뛰었는지 알기

HotXLS는 SaveAs 도중에 수식을 평가하지 않습니다. 사람이 파일을 열면 Excel이 모든 것을 재계산하므로(이를 제어해야 한다면 XLS 파사드가 CalculationModeRecalcOnSave를 노출합니다), 사람의 받은 편지함으로 향하는 보고서라면 여러분이 더 할 일은 없습니다. 워크북이 다른 프로그램에 데이터를 공급하는 순간 상황은 달라집니다. CSV 내보내기는 수식을 리터럴 텍스트로 기록할 뿐 절대 계산하지 않으며, 캐시된 값을 신뢰하는 다운스트림 파서는 무엇이든 오래된 숫자나 빈 값을 읽게 됩니다. 그런 경로를 위해서는 서버에서 Calculate로 계산하십시오. 이는 로드된 워크북에 대해 임의의 표현식을 평가하고 결과를 돌려줍니다:

var
  Total: Variant;
  LastDetail: Integer;
begin
  LastDetail := DetailRow + Length(Items) - 1;
  Total := Book.Calculate(Format('SUM(Invoice!D%d:D%d)',
    [DetailRow, LastDetail]));
  if (not VarIsNumeric(Total)) or
     (Abs(Total - ExpectedTotal) > 0.005) then
    raise Exception.Create('Invoice total does not match the order record');

  if Book.SaveAs('invoice-2026-0611.xlsx') <> 1 then
    raise Exception.Create('Save failed: check output path and permissions');
end;

저장 전에 계산된 합계를 주문 기록과 대조하는 것은 저렴한 보험이면서 보상도 좋습니다. 잘못된 청구서를 실패한 작업으로 바꿔주기 때문입니다. 운영자는 실패한 작업을 몇 초 만에 재시도할 수 있지만, 이미 고객의 메일함에 들어간 잘못된 청구서는 계정 담당자에게 사과와 정정 작업을 치르게 합니다

두 클래스 군, 하나의 알고리즘

같은 논리가 형식 사이에서 이식되지만, 같은 코드는 아닙니다. 레거시 .xls를 위한 TXLSWorkbook은 인터페이스 기반이자 참조 카운트 방식이며 1부터 시작하는 시트 인덱싱을 가지고, 손으로 절대 해제하지 않습니다. .xlsx를 위한 TXLSXWorkbooktry..finally에서 반드시 해제해야 하는 일반 객체이며, 0부터 시작하는 시트 인덱싱과 위에서 본 수식 관례를 가집니다. FindText, ReplaceText, CopyRange, InsertRows 모두 양쪽에 존재하므로, 앵커-복제-재계산이라는 형태는 깔끔하게 그대로 이어집니다. 실용적인 조언은 파이프라인마다 하나의 형식을 고수하거나, 그 차이를 생성기 전반에 흩뿌리는 대신 자신만의 얇은 어댑터 뒤에 두 객체 생명주기를 숨기는 것입니다

이 패턴이 만들어내는 종류의 보고서에서 크기는 거의 문제가 되지 않습니다. 스타일이 적용된 행을 수천 번 복제하는 것은 현재 하드웨어에게는 아무것도 아닙니다. 저장 경로가 병목이 되는 것은 명세 밴드가 6자리 행 수에 도달할 때뿐이며, 그 시점에서 StreamingWrite를 설정하면 워크시트 XML을 버퍼링하는 대신 곧바로 출력 패키지로 보냅니다. 서버 배치 작업을 위한 스트리밍 쓰기 문서가 이 교환이 언제 가치 있는지를 다룹니다. 차트는 레이아웃의 나머지 부분과 같은 방식으로 동작합니다. XLSX 쪽에서는 InsertRows가 위에서 실행될 때 차트 앵커와 그 계열 참조 모두 함께 이동하므로, 합계 행 아래의 차트는 올바른 데이터에 계속 묶여 있습니다. 반면 XLS 쪽에서는 차트가 자신만의 차트 시트에 자리하며, 피벗 테이블처럼 절대 이동하지 않습니다. 이는 생성기가 확장하는 시트로부터 프레젠테이션 시트를 떨어뜨려 두어야 할 또 하나의 이유입니다

이 앵커-복제-재계산 접근 방식은 디자이너가 워크북의 모양을 소유하고 여러분의 코드가 그 내용을 소유하게 해 줍니다. 대체로 이것이 생성된 Excel 출력물을 유지 보수할 가치가 있게 만드는 요인입니다. 여기서 소개한 검색, 복사, 삽입 호출은 전달 전 합계 확인에 사용되는 수식 엔진과 함께 Delphi 및 C++Builder용 HotXLS Delphi Component와 함께 제공됩니다