그룹 펼침과 업무 행 선택
명시적인 그룹 순서·집계·접힌 그룹의 선택 ID 유지를 확인합니다.
조작 순서와 결과
섹션 제목: “조작 순서와 결과”| 조작 | 기대 결과 |
|---|---|
| 초기 화면 | East 2행/합계 200, Empty 0행, West 1행/합계 240 |
| 첫 업무 행 선택 | synthetic 그룹 행을 건너뛴 첫 leaf인 Alpha를 선택합니다. |
| East 접기 | Alpha/Beta는 숨겨지고 selection의 alpha ID는 유지됩니다. 집계 200도 유지합니다. |
| getSelectedRows | 마지막 조회 결과 retrieved에 선택된 업무 데이터의 ID를 표시합니다. |
| East 펼치기 | 선택을 다시 만들지 않고 Alpha와 Beta가 나타납니다. |
| 그룹 순서 반전 | West → Empty → East로 바뀝니다. 업무 row ID와 소속 그룹은 바뀌지 않습니다. |
| 초기화 | 초기 그룹 순서·펼침 상태를 복원하고 선택을 해제합니다. |
그룹 제목의 disclosure 버튼도 직접 조작할 수 있습니다. 업무 행을 클릭하면 기존 Row Selection 규칙으로 선택합니다.
상태 소유권
섹션 제목: “상태 소유권”groups와 expandedGroupIds는 애플리케이션 상태입니다. onChangeExpandedGroupIds를 연결해야 UI와 ref의 펼침 요청이 반영됩니다. 이 예제는 setGroups로 명시적인 그룹 순서를 바꾸며 그룹 객체에 별도의 order 값을 중복 저장하지 않습니다.
setSelectedRow(0)의 인덱스는 표시되는 업무 leaf 기준입니다. 그룹 제목 행은 포함하지 않습니다. 이 값은 현재 정렬과 펼침에 따라 달라지므로 장기 저장 identity로 사용하지 않습니다. 영속적인 identity는 getRowId의 ID입니다.
접힌 그룹의 선택은 보이지 않는 상태로 유지됩니다. onChangeSelection의 row ID 목록과 getSelectedRows()의 업무 데이터를 구분하여 표시합니다. 이 예제의 선택은 Table 내부 상태이며, 표시한 ID 배열을 controlled selection prop처럼 다시 주입하지 않습니다.
집계와 빈 그룹
섹션 제목: “집계와 빈 그룹”aggregations: { amount: "sum" }을 사용합니다. 그룹을 접어도 member 데이터가 삭제되지 않으므로 집계 입력은 유지됩니다. Empty는 데이터가 없어도 groups에 정의되어 있어 표시되며 숫자 합계가 없으면 예제에서 —로 표시합니다. renderGroupContent는 내부 label·count·집계만 구성하고 외부 행·disclosure는 패키지가 소유합니다.
범위와 관련 문서
섹션 제목: “범위와 관련 문서”단일 깊이 CSR 그룹 예제입니다. Group selection, Tree, pagination, 원격 데이터 로딩은 포함하지 않습니다. 그룹/업무 Row Drag와 필터·Summary 결합은 별도 시나리오입니다.
직접 실행
섹션 제목: “직접 실행”import { useRef, useState } from 'react';import { CominsTable, type CominsTableRef, type CominsTableColumn, type CominsRowId } from 'comins-table';import 'comins-table/styles.css';type Row = { id: string; name: string; groupId: string; amount: number };type Group = { id: string; label: string };const initialGroups: Group[] = [{ id: 'east', label: 'East' }, { id: 'empty', label: 'Empty' }, { id: 'west', label: 'West' }];const rows: Row[] = [{ id: 'alpha', name: 'Alpha', groupId: 'east', amount: 120 }, { id: 'beta', name: 'Beta', groupId: 'east', amount: 80 }, { id: 'gamma', name: 'Gamma', groupId: 'west', amount: 240 }];const columns: CominsTableColumn<Row>[] = [{ id: 'name', field: 'name', label: 'Name', width: 230, sort: true }, { id: 'amount', field: 'amount', label: 'Amount', width: 170, sort: true }];export default function TableGroupingDemo({ lang = 'ko' }: { lang?: string }) { const ko = lang === 'ko'; const table = useRef<CominsTableRef<Row>>(null); const [groups, setGroups] = useState(initialGroups); const [expanded, setExpanded] = useState<CominsRowId[]>(['east', 'empty', 'west']); const [selection, setSelection] = useState<CominsRowId[]>([]); const [retrieved, setRetrieved] = useState<string[]>([]); return <div className="demo" data-demo="table-grouping"> <div className="demo-toolbar"> <button onClick={() => table.current?.setSelectedRow(0)}>{ko ? '첫 업무 행 선택' : 'Select first data row'}</button> <button onClick={() => table.current?.foldGroups(['east'])}>{ko ? 'East 접기' : 'Fold East'}</button> <button onClick={() => table.current?.expandGroups(['east'])}>{ko ? 'East 펼치기' : 'Expand East'}</button> <button onClick={() => setRetrieved(table.current?.getSelectedRows().map(row => row.id) ?? [])}>getSelectedRows</button> <button onClick={() => setGroups(current => [...current].reverse())}>{ko ? '그룹 순서 반전' : 'Reverse groups'}</button> <button onClick={() => { setGroups(initialGroups); setExpanded(['east','empty','west']); table.current?.setSelectedRows([]); setRetrieved([]); }}>{ko ? '초기화' : 'Reset'}</button> </div> <div className="table-viewport" style={{ height: 300 }}> <CominsTable<Row, Group> ref={table} data={rows} columns={columns} getRowId={row => row.id} rowSelectionOnClick onChangeSelection={next => setSelection([...next.rowIds])} rowGrouping={{ groups, getGroupId: group => group.id, getGroupLabel: group => group.label, getRowGroupId: row => row.groupId, expandedGroupIds: expanded, onChangeExpandedGroupIds: setExpanded, aggregations: { amount: 'sum' }, renderGroupContent: ({ group, rowCount, aggregateValues }) => <span data-group-label={group.id}>{group.label} · {rowCount} · {aggregateValues.amount ?? '—'}</span>, }} /> </div> <pre data-group-state>{JSON.stringify({ groups: groups.map(group => group.id), expanded, selection, retrieved }, null, 2)}</pre> </div>;}