Table#
Table은 Flutter와 Web 양쪽에서 동일한 API로 렌더링되는 통일 테이블 컴포넌트다. CoreTableRowData<W>
/ CoreTableCellData<W> 구조를 받아 셀 단위 호버/선택, 행/열 병합(rowSpan/columnSpan), 유연한 사이즈 전략(CoreFlexTableSize/CoreFixedTableSize/CoreIntrinsicTableSize), frozen cells, 스크롤 오프셋을 지원한다. 드래그로 열/행 크기를 바꿔야 할 때는
ResizableTable + ResizableTableController를 양쪽 플랫폼에서 동일하게 사용한다.
Live Preview#
class TableDefaultExample extends StatelessComponent {
const TableDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Table(
rows: [
CoreTableRowData<Component>.header(
cells: [
CoreTableCellData<Component>(child: Component.text('Name')),
CoreTableCellData<Component>(child: Component.text('Status')),
CoreTableCellData<Component>(child: Component.text('Role')),
],
),
CoreTableRowData<Component>(
cells: [
CoreTableCellData<Component>(child: Component.text('Alice')),
CoreTableCellData<Component>(
child: Badge(
variant: CoreBadgeVariant.primary,
child: Component.text('Active'),
),
),
CoreTableCellData<Component>(child: Component.text('Admin')),
],
),
CoreTableRowData<Component>(
cells: [
CoreTableCellData<Component>(child: Component.text('Bob')),
CoreTableCellData<Component>(
child: Badge(
variant: CoreBadgeVariant.secondary,
child: Component.text('Inactive'),
),
),
CoreTableCellData<Component>(child: Component.text('User')),
],
),
],
);
}
}
class TableDefaultExample extends StatelessWidget {
const TableDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Table(
rows: [
CoreTableRowData<Widget>.header(
cells: const [
CoreTableCellData<Widget>(child: Text('Name')),
CoreTableCellData<Widget>(child: Text('Status')),
CoreTableCellData<Widget>(child: Text('Role')),
],
),
CoreTableRowData<Widget>(
cells: [
const CoreTableCellData<Widget>(child: Text('Alice')),
CoreTableCellData<Widget>(
child: Badge(
variant: CoreBadgeVariant.primary,
child: const Text('Active'),
),
),
const CoreTableCellData<Widget>(child: Text('Admin')),
],
),
CoreTableRowData<Widget>(
cells: [
const CoreTableCellData<Widget>(child: Text('Bob')),
CoreTableCellData<Widget>(
child: Badge(
variant: CoreBadgeVariant.secondary,
child: const Text('Inactive'),
),
),
const CoreTableCellData<Widget>(child: Text('User')),
],
),
],
);
}
}
class TableResizableExample extends StatefulComponent {
const TableResizableExample({super.key});
@override
State<TableResizableExample> createState() => _TableResizableExampleState();
}
class _TableResizableExampleState extends State<TableResizableExample> {
late final ResizableTableController _controller;
@override
void initState() {
super.initState();
_controller = ResizableTableController(
defaultColumnWidth: 300,
defaultRowHeight: 56,
defaultWidthConstraint: const CoreConstrainedTableSize(min: 80),
defaultHeightConstraint: const CoreConstrainedTableSize(min: 44),
);
}
@override
Component build(BuildContext context) {
return ResizableTable(
controller: _controller,
rows: [/* ... */],
);
}
}
class TableResizableExample extends StatefulWidget {
const TableResizableExample({super.key});
@override
State<TableResizableExample> createState() => _TableResizableExampleState();
}
class _TableResizableExampleState extends State<TableResizableExample> {
late final ResizableTableController _controller;
@override
void initState() {
super.initState();
_controller = ResizableTableController(
defaultColumnWidth: 300,
defaultRowHeight: 56,
defaultWidthConstraint: const CoreConstrainedTableSize(min: 80),
defaultHeightConstraint: const CoreConstrainedTableSize(min: 44),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResizableTable(
controller: _controller,
rows: [/* ... */],
);
}
}
class TableScrollableExample extends StatelessComponent {
const TableScrollableExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
ScrollableClient(
diagonalDragBehavior: CoreDiagonalDragBehavior.free,
builder: (context, offset, viewportSize, child) {
return Table(
horizontalOffset: offset.dx,
verticalOffset: offset.dy,
tableStyle: CoreTableStyle(
viewportWidth: viewportSize.width,
viewportHeight: viewportSize.height,
),
defaultColumnWidth: const CoreFixedTableSize(160),
defaultRowHeight: const CoreFixedTableSize(44),
frozenCells: const CoreFrozenTableData(
frozenRows: [CoreTableRef(0)],
frozenColumns: [CoreTableRef(0)],
),
rows: [/* ... */],
);
},
),
],
styles: Styles(raw: const {'height': '320px'}),
);
}
}
class TableScrollableExample extends StatelessWidget {
const TableScrollableExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 320,
child: ScrollableClient(
diagonalDragBehavior: CoreDiagonalDragBehavior.free,
builder: (context, offset, viewportSize, child) {
return Table(
horizontalOffset: offset.dx,
verticalOffset: offset.dy,
tableStyle: CoreTableStyle(
viewportWidth: viewportSize.width,
viewportHeight: viewportSize.height,
),
defaultColumnWidth: const CoreFixedTableSize(160),
defaultRowHeight: const CoreFixedTableSize(44),
frozenCells: const CoreFrozenTableData(
frozenRows: [CoreTableRef(0)],
frozenColumns: [CoreTableRef(0)],
),
rows: [/* ... */],
);
},
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 데이터를 행과 열로 비교/분석해야 할 때
- 헤더/바디/푸터 구조가 필요한 목록일 때
- 셀 병합, frozen 열/행이 필요한 스프레드시트형 UI일 때
대신 다른 컴포넌트를 사용하세요:
Card리스트: 각 항목이 복잡한 레이아웃을 가질 때Accordion: 행별 상세 정보를 펼쳐서 보여줄 때
기본 사용법 (Basic Usage)#
Table(
rows: [
CoreTableRowData<Widget>.header(cells: [
CoreTableCellData<Widget>(child: Text('Name')),
CoreTableCellData<Widget>(child: Text('Status')),
CoreTableCellData<Widget>(child: Text('Role')),
]),
CoreTableRowData<Widget>(cells: [
CoreTableCellData<Widget>(child: Text('Alice')),
CoreTableCellData<Widget>(
child: Badge(
variant: CoreBadgeVariant.primary,
child: Text('Active'),
),
),
CoreTableCellData<Widget>(child: Text('Admin')),
]),
],
)
Table(
rows: [
CoreTableRowData<Component>.header(cells: [
CoreTableCellData<Component>(child: Component.text('Name')),
CoreTableCellData<Component>(child: Component.text('Status')),
CoreTableCellData<Component>(child: Component.text('Role')),
]),
CoreTableRowData<Component>(cells: [
CoreTableCellData<Component>(child: Component.text('Alice')),
CoreTableCellData<Component>(
child: Badge(
variant: CoreBadgeVariant.primary,
child: Component.text('Active'),
),
),
CoreTableCellData<Component>(child: Component.text('Admin')),
]),
],
)
Props#
Table / ResizableTable#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
rows |
List<CoreTableRowData<W>> |
필수 | 렌더링할 행 목록 (body/header/footer) |
defaultColumnWidth |
CoreTableSize |
CoreFlexTableSize() |
지정되지 않은 열 기본 사이즈 |
defaultRowHeight |
CoreTableSize |
CoreIntrinsicTableSize() |
지정되지 않은 행 기본 사이즈 |
columnWidths |
Map<int, CoreTableSize>? |
null | 열별 사이즈 오버라이드 |
rowHeights |
Map<int, CoreTableSize>? |
null | 행별 사이즈 오버라이드 |
frozenCells |
CoreFrozenTableData? |
null | 고정 행/열 지정 |
horizontalOffset |
double? |
null | 수평 스크롤 오프셋 (px) |
verticalOffset |
double? |
null | 수직 스크롤 오프셋 (px) |
theme | CoreTableTheme? | null | 위젯 레벨 테마 오버라이드 |
tableStyle |
CoreTableStyle<Color>? / CoreTableStyle<CoreColor>? |
null | 인스턴스 스타일 (Style 시스템 참조) |
ResizableTable에는 위 파라미터에 더해 controller: ResizableTableController와 columnResizeMode
/ rowResizeMode (CoreTableCellResizeMode)가 추가된다.
스타일 시스템 (Style System)#
Table 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreTableStyle<Clr> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 시맨틱/구조/스크롤 상태 (rows,
columnWidths, rowHeights, frozenCells, horizontalOffset,
verticalOffset, clipBehavior) 는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 / behaviour / 구조: 위젯 파라미터로 직접 (
rows,columnWidths,rowHeights,frozenCells,horizontalOffset,verticalOffset,clipBehavior,controller,columnResizeMode,rowResizeMode) -
chrome / dimensional / 슬롯 스타일:
CoreTableStyle한 곳으로 (viewportWidth/viewportHeight+ header/row/cell/border 13 chrome flat +headerTextStyle/cellTextStyle)
Resolve chain#
design system default for table
→ CoreTableTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.tableStyle // 인스턴스별
각 nested 슬롯 스타일 (headerTextStyle / cellTextStyle) 은 CoreTextStyle
자체 resolve chain 으로 다시 한 번 머지됩니다.
슬롯 매핑 (CoreTableStyle 13 필드)#
| 필드 | 타입 | 적용 영역 |
|---|---|---|
headerBackgroundColor | CoreColor? | 헤더 행 배경 |
headerHeight | double? | 헤더 행 높이 (px) |
headerPadding | CoreEdgeInsets? | 헤더 셀 패딩 |
rowBackgroundColor | CoreColor? | 바디 행 배경 |
rowAlternateBackgroundColor |
CoreColor? |
짝수 행 zebra-stripe 배경 |
rowHoverColor | CoreColor? | 행 호버 배경 |
rowSelectedColor | CoreColor? | 행 선택 배경 |
rowHeight | double? | 바디 행 높이 (px) |
cellPadding | CoreEdgeInsets? | 바디 셀 패딩 |
borderColor | CoreColor? | 셀/행 구분선 색 |
borderWidth | double? | 셀/행 구분선 두께 (px) |
headerTextStyle | CoreTextStyle? | 헤더 텍스트 스타일 |
cellTextStyle | CoreTextStyle? | 바디 셀 텍스트 스타일 |
테마 (CoreTableTheme)#
CoreComponentTheme.table 또는 위젯 theme 파라미터로 아래 필드를 오버라이드할 수 있다.
| 필드 | 설명 |
|---|---|
backgroundColor | 테이블 컨테이너 배경색 |
borderColor | 셀 경계선 및 외곽선 색 |
borderWidth | 경계선 두께 (px) |
borderRadius | 컨테이너 모서리 반경 (px) |
cellPadding | 셀 기본 패딩 (px) |
headerBackgroundColor / footerBackgroundColor | 헤더/푸터 행 배경 |
rowHoverColor / rowSelectedColor | 행 hover/선택 배경 |
resizerHoverColor / resizerDragColor / resizerThickness |
리사이저 색/두께 (ResizableTable 전용) |
크로스 플랫폼 차이점#
| 항목 | Flutter | Web |
|---|---|---|
| 레이아웃 |
TableLayoutWidget
(custom RenderBox,
RenderTableLayoutBox
) — flex/fixed/intrinsic + span + frozen + scroll
|
CSS Grid (grid-template-columns/rows, span N) |
| rowSpan/columnSpan | 엔진이 오프셋 계산 | grid-column: span N / grid-row: span N |
| frozen cells | RenderTableLayoutBox.performLayout에서 viewport 기반 adjustment |
동일 — horizontalOffset + verticalOffset + frozenCells로 구현 |
| 드래그 리사이즈 | ResizableTable + ResizableTableController |
동일 — 각 셀 우측/하단에 drag handle overlay |
| 스크롤 | ScrollableClient(builder) — 2D TwoDimensionalScrollable 기반 |
동일 API — overflow: auto + onscroll로 offset 추적해 builder 재호출 |
관련 컴포넌트#
- Card: 카드 리스트
- Pagination: 페이지 네비게이션
- Accordion: 접이식 행별 상세