구조화된 데이터를 위한 sgcHTML 신규 컴포넌트 4종: TreeGrid, PivotTable, ActivityFeed, Splitter | eSeGeCe 블로그

구조화된 데이터를 위한 sgcHTML 신규 컴포넌트 4종: TreeGrid, PivotTable, ActivityFeed, Splitter

· 컴포넌트

sgcHTML에 25개의 컴포넌트가 새로 추가되었으며, 이 글은 주제별로 이를 살펴보는 다섯 편의 시리즈 중 첫 번째입니다. 먼저 구조화되고 계속 변화하는 데이터를 다루는 네 가지 컴포넌트부터 시작합니다: 계층형 행을 위한 TreeGrid, 그룹 집계를 위한 PivotTable, 실시간 이벤트 스트림을 위한 ActivityFeed, 그리고 이 모든 것을 담아야 하는 경우가 많은 크기 조절 2단 레이아웃을 위한 Splitter입니다. 아래의 모든 컴포넌트는 평범한 Delphi/C++Builder/.NET 클래스입니다: 속성을 설정하고 HTML을 호출하면 Bootstrap 5 마크업이 반환됩니다. JavaScript 프레임워크도, 빌드 단계도 필요하지 않습니다.

TreeGrid — 접고 펼칠 수 있는 계층형 테이블

TsgcHTMLComponent_TreeGrid는 행을 접고 펼칠 수 있는 Bootstrap 테이블을 렌더링하며, 단순한 Id / ParentId 노드 쌍으로 구성됩니다 — 조직도, 카테고리 트리, 중첩된 자재명세서(BOM) 데이터에 이미 사용하고 있는 것과 같은 구조입니다. 화면에 표시할 필드마다 TsgcHTMLTreeGridColumn을 선언한 다음 노드를 추가하면, 부모/자식 관계에 따라 트리와 토글 화살표가 자동으로 그려집니다.

uses
  sgcHTML_Component_TreeGrid;

var
  oTreeGrid: TsgcHTMLComponent_TreeGrid;
  oColumn: TsgcHTMLTreeGridColumn;
begin
  oTreeGrid := TsgcHTMLComponent_TreeGrid.Create(nil);
  try
    oTreeGrid.TreeGridID := 'orgchart';
    oTreeGrid.Sortable := True;

    oColumn := oTreeGrid.Columns.Add;
    oColumn.Caption := 'Department';
    oColumn.FieldName := 'Name';

    oColumn := oTreeGrid.Columns.Add;
    oColumn.Caption := 'Headcount';
    oColumn.FieldName := 'Headcount';
    oColumn.Align := tgaRight;

    oTreeGrid.AddNode('eng', '', ['Engineering', '42']);
    oTreeGrid.AddNode('eng-be', 'eng', ['Backend', '18']);
    oTreeGrid.AddNode('eng-fe', 'eng', ['Frontend', '15']);

    WebModule.Response := oTreeGrid.HTML;   // hierarchical table + scoped CSS + toggle script
  finally
    oTreeGrid.Free;
  end;
end;

// Or bind it straight to a self-referencing dataset (Id / ParentId columns):
oTreeGrid.LoadFromDataSet(qryDepartments, 'Id', 'ParentId');

데이터가 이미 자기 참조(self-referencing) 테이블에 있다면 수동으로 노드를 추가하는 과정을 모두 건너뛰고, 부모/자식 필드 이름을 LoadFromDataSet에 전달하면 됩니다 — 데이터셋을 순회하며 트리 전체를 한 번에 구성합니다.

PivotTable — 그룹화, 집계, 합계

TsgcHTMLComponent_PivotTable은 평평한(flat) 행 데이터를 받아 행 합계, 열 합계, 총합계가 포함된 그룹화·집계 테이블을 생성합니다. 그렇지 않으면 스프레드시트에서 직접 만들어야 했을 보고서입니다. 그룹화 기준이 되는 RowFieldsColumnFields를 선언하고, 집계할 하나 이상의 Measures(합계, 평균, 개수, 최소값, 최대값)를 지정하며, 실제로 읽어들일 원본 열을 알려주는 DataFields 목록을 지정합니다.

uses
  sgcHTML_Component_PivotTable;

var
  oPivot: TsgcHTMLComponent_PivotTable;
  oMeasure: TsgcHTMLPivotMeasure;
begin
  oPivot := TsgcHTMLComponent_PivotTable.Create(nil);
  try
    oPivot.Caption := 'Revenue by Region and Quarter';

    oPivot.RowFields.Add.FieldName := 'Region';
    oPivot.ColumnFields.Add.FieldName := 'Quarter';

    oMeasure := oPivot.Measures.Add;
    oMeasure.SourceField := 'Amount';
    oMeasure.Caption := 'Revenue';
    oMeasure.Aggregation := paSum;
    oMeasure.Format := '#,##0.00';

    oPivot.DataFields.Add('Region');
    oPivot.DataFields.Add('Quarter');
    oPivot.DataFields.Add('Amount');

    oPivot.AddRow(['EMEA', 'Q1', '12000']);
    oPivot.AddRow(['EMEA', 'Q2', '15500']);
    oPivot.AddRow(['APAC', 'Q1', '9800']);

    WebModule.Response := oPivot.HTML;   // aggregated table + row/column/grand totals
  finally
    oPivot.Free;
  end;
end;

// Or bind it straight to a dataset (reads only RowFields/ColumnFields/Measures fields):
oPivot.LoadFromDataSet(qrySales);

AddRow(또는 LoadFromDataSet) 이후의 모든 과정은 자동으로 계산됩니다: 컴포넌트가 원본 행을 그룹화하고, 셀마다 선택한 집계 방식을 적용한 다음, 행/열/총합계 행과 열을 자동으로 추가합니다.

ActivityFeed — 실시간으로 푸시 가능한 이벤트 스트림

TsgcHTMLComponent_ActivityFeed는 "누가 무엇을 했는지"를 나타내는 항목들을 최신순으로 렌더링하며, 상대 시간("3분 전"처럼 클라이언트 측에서 실시간으로 갱신됨)을 함께 표시합니다. 그리고 단순히 한 번 렌더링되는 것이 아니라 처음부터 WebSocket을 통해 푸시되도록 설계되었습니다. MaxItems는 링 버퍼처럼 동작합니다 — 한도에 도달하면 가장 오래된 항목이 자동으로 제거됩니다.

uses
  sgcHTML_Enums, sgcHTML_Component_ActivityFeed;

var
  oFeed: TsgcHTMLComponent_ActivityFeed;
begin
  oFeed := TsgcHTMLComponent_ActivityFeed.Create(nil);
  try
    oFeed.FeedID := 'timeline';
    oFeed.Title := 'Recent Activity';
    oFeed.MaxItems := 30;

    oFeed.AddActivity('Ana', 'closed', 'ticket #482', hcSuccess, 'bi bi-check-circle');
    oFeed.AddActivity('Marc', 'commented on', 'invoice #1190', hcPrimary);

    WebModule.Response := oFeed.HTML;   // feed list + scoped CSS + live relative-time script
  finally
    oFeed.Free;
  end;
end;

// Push a new item live to connected clients (out-of-band htmx fragment):
oFeed.AddActivity('Ana', 'approved', 'purchase order #77', hcSuccess);
Engine.BroadcastFragment(oFeed.GetLastItemFragmentHTML);

흥미로운 부분은 마지막 줄입니다: GetLastItemFragmentHTML은 최신 항목만을 out-of-band htmx 프래그먼트로 렌더링하고, BroadcastFragment는 이미 실행 중인 sgcWebSockets 서버를 통해 이를 열려 있는 모든 브라우저로 푸시합니다. 폴링도 필요 없고, 별도의 실시간 레이어를 구성할 필요도 없습니다.

Splitter — 모든 것을 담는 2단 셸

TsgcHTMLComponent_Splitter는 레이아웃을 담당하는 컴포넌트입니다: 드래그 가능한 거터(gutter)로 나뉜 크기 조절 가능한 두 개의 패널을 가로 또는 세로 방향으로 배치하며, 양쪽 모두 픽셀 단위의 최소 크기를 지정할 수 있어 어느 쪽 패널도 완전히 사라지도록 드래그할 수 없습니다. TreeGrid와 상세 패널을 함께 배치하거나 파일 목록과 편집기를 나란히 배치하는 화면에 정확히 필요한 셸입니다.

uses
  sgcHTML_Component_Splitter;

var
  oSplitter: TsgcHTMLComponent_Splitter;
begin
  oSplitter := TsgcHTMLComponent_Splitter.Create(nil);
  try
    oSplitter.SplitterID := 'workspace';
    oSplitter.Orientation := soHorizontal;
    oSplitter.InitialSplit := 30;
    oSplitter.MinSizeA := 180;
    oSplitter.MinSizeB := 320;
    oSplitter.GutterSize := 8;
    oSplitter.Height := '560px';
    oSplitter.PersistKey := 'workspace-split';

    oSplitter.AddPaneA('<div class="p-3"><h5>Files</h5>...</div>');
    oSplitter.AddPaneB('<div class="p-3"><h5>Editor</h5>...</div>');

    WebModule.Response := oSplitter.HTML;   // two panes + gutter + scoped CSS + drag script
  finally
    oSplitter.Free;
  end;
end;

특히 짚고 넘어갈 부분은 PersistKey입니다: 이 값을 설정하면 방문자가 드래그한 거터 위치가 클라이언트 측에 저장되어 다음 방문 시 그대로 복원되므로, 별도의 상태 관리 코드를 작성하지 않아도 작업 공간 레이아웃이 세션 간에 그대로 유지됩니다.

직접 사용해보기

네 가지 컴포넌트 모두 현재 sgcWebSockets 릴리스에 포함되어 있으며, 각 컴포넌트 페이지에서 전체 문서, 주요 속성 레퍼런스, Delphi/C++Builder/.NET 코드 예제를 확인할 수 있습니다: TreeGrid, PivotTable, ActivityFeed, Splitter. 전체 기능 매트릭스에는 이제 9개 제품군에 걸쳐 83개의 컴포넌트가 등재되어 있습니다 — 이 시리즈의 다음 글에서는 새로운 차트와 시각 코드를 다룹니다.

질문, 피드백, 마이그레이션 도움이 필요하신가요? 문의하기 — 실제로 코드를 작성한 사람들에게서 답변을 받으실 수 있습니다.