Table#
CoTable은 Flutter와 Web 양쪽에서 동일한 API로 렌더링되는 통일 테이블 컴포넌트다. CoreTableRowData<W>
/ CoreTableCellData<W> 구조를 받아 셀 단위 호버/선택, 행/열 병합(rowSpan/columnSpan), 유연한 사이즈 전략(CoreFlexTableSize/CoreFixedTableSize/CoreIntrinsicTableSize), frozen cells, 스크롤 오프셋을 지원한다. 드래그로 열/행 크기를 바꿔야 할 때는
CoResizableTable + CoResizableTableController를 양쪽 플랫폼에서 동일하게 사용한다.
Live Preview#
class TableDefaultExample extends StatelessComponent {
const TableDefaultExample({super.key});
@override
Component build(BuildContext context) {
return CoTable(
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: CoBadge(
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: CoBadge(
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 CoTable(
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: CoBadge(
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: CoBadge(
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 CoResizableTableController _controller;
@override
void initState() {
super.initState();
_controller = CoResizableTableController(
defaultColumnWidth: 300,
defaultRowHeight: 56,
defaultWidthConstraint: const CoreConstrainedTableSize(min: 80),
defaultHeightConstraint: const CoreConstrainedTableSize(min: 44),
);
}
@override
Component build(BuildContext context) {
return CoResizableTable(
controller: _controller,
rows: [/* ... */],
);
}
}
class TableResizableExample extends StatefulWidget {
const TableResizableExample({super.key});
@override
State<TableResizableExample> createState() => _TableResizableExampleState();
}
class _TableResizableExampleState extends State<TableResizableExample> {
late final CoResizableTableController _controller;
@override
void initState() {
super.initState();
_controller = CoResizableTableController(
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 CoResizableTable(
controller: _controller,
rows: [/* ... */],
);
}
}
class TableScrollableExample extends StatelessComponent {
const TableScrollableExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
CoScrollableClient(
diagonalDragBehavior: CoreDiagonalDragBehavior.free,
builder: (context, offset, viewportSize, child) {
return CoTable(
horizontalOffset: offset.dx,
verticalOffset: offset.dy,
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: CoScrollableClient(
diagonalDragBehavior: CoreDiagonalDragBehavior.free,
builder: (context, offset, viewportSize, child) {
return CoTable(
horizontalOffset: offset.dx,
verticalOffset: offset.dy,
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일 때
대신 다른 컴포넌트를 사용하세요:
CoCard리스트: 각 항목이 복잡한 레이아웃을 가질 때Accordion: 행별 상세 정보를 펼쳐서 보여줄 때
기본 사용법 (Basic Usage)#
CoTable(
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: CoBadge(
variant: CoreBadgeVariant.primary,
child: Text('Active'),
),
),
CoreTableCellData<Widget>(child: Text('Admin')),
]),
],
)
CoTable(
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: CoBadge(
variant: CoreBadgeVariant.primary,
child: Component.text('Active'),
),
),
CoreTableCellData<Component>(child: Component.text('Admin')),
]),
],
)
Props#
CoTable / CoResizableTable#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
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) |
viewportWidth / viewportHeight |
double? |
null | 뷰포트 크기 (스크롤 사용 시) |
theme | CoreTableTheme? | null | 위젯 레벨 테마 오버라이드 |
tableStyle |
CoreTableStyle<Color>? / CoreTableStyle<CoreColor>? |
null | 인스턴스 스타일 (Style 시스템 참조) |
CoResizableTable에는 위 파라미터에 더해 controller: CoResizableTableController와 columnResizeMode
/ rowResizeMode (CoreTableCellResizeMode)가 추가된다.
스타일 시스템 (Style System)#
Table 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreTableStyle<Clr> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 시맨틱/구조 (rows,
columnWidths, rowHeights, frozenCells, viewport*,
clipBehavior) 는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 / behaviour / 구조: 위젯 파라미터로 직접 (
rows,columnWidths,rowHeights,frozenCells,horizontalOffset,verticalOffset,viewportWidth,viewportHeight,clipBehavior,controller,columnResizeMode,rowResizeMode) -
chrome / dimensional / 슬롯 스타일:
CoreTableStyle한 곳으로 (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 15 필드)#
| 필드 | 타입 (Flutter) | 적용 영역 |
|---|---|---|
headerBackgroundColor | Color? | 헤더 행 배경 |
headerHeight | double? | 헤더 행 높이 (px) |
headerPaddingH | double? | 헤더 셀 좌우 패딩 (px) |
headerPaddingV | double? | 헤더 셀 위/아래 패딩 (px) |
rowBackgroundColor | Color? | 바디 행 배경 |
rowAlternateBackgroundColor |
Color? |
짝수 행 zebra-stripe 배경 |
rowHoverColor | Color? | 행 호버 배경 |
rowSelectedColor | Color? | 행 선택 배경 |
rowHeight | double? | 바디 행 높이 (px) |
cellPaddingH | double? | 바디 셀 좌우 패딩 (px) |
cellPaddingV | double? | 바디 셀 위/아래 패딩 (px) |
borderColor | Color? | 셀/행 구분선 색 |
borderWidth | double? | 셀/행 구분선 두께 (px) |
headerTextStyle |
CoreTextStyle<Color>? |
헤더 텍스트 스타일 |
cellTextStyle |
CoreTextStyle<Color>? |
바디 셀 텍스트 스타일 |
Migration#
기존 CoreTableTheme 의 평면 chrome (backgroundColor / borderColor /
borderWidth / borderRadius / cellPadding / headerBackgroundColor
/ footerBackgroundColor / rowHoverColor / rowSelectedColor 등) 는 호환을 위해 유지되지만, 새 코드는
tableStyle 슬롯을 사용하세요:
| 기존 (legacy theme 필드) | 새 위치 |
|---|---|
CoreTableTheme.headerBackgroundColor |
tableStyle.headerBackgroundColor |
CoreTableTheme.rowHoverColor | tableStyle.rowHoverColor |
CoreTableTheme.rowSelectedColor |
tableStyle.rowSelectedColor |
CoreTableTheme.borderColor | tableStyle.borderColor |
CoreTableTheme.borderWidth | tableStyle.borderWidth |
CoreTableTheme.cellPadding |
tableStyle.cellPaddingH / cellPaddingV |
| 인스턴스 헤더 텍스트 스타일 (불가능) | tableStyle.headerTextStyle |
| 인스턴스 셀 텍스트 스타일 (불가능) | tableStyle.cellTextStyle |
사용 예 (Flutter)#
CoTable(
rows: rows,
tableStyle: CoreTableStyle<Color>(
headerBackgroundColor: Color(0xFFF7F7F7),
rowHoverColor: Colors.blueGrey.shade50,
cellPaddingH: 16,
cellPaddingV: 8,
borderColor: Colors.grey.shade300,
borderWidth: 1,
),
)
사용 예 (Web)#
CoTable(
rows: rows,
tableStyle: CoreTableStyle<CoreColor>(
headerPaddingH: 16,
cellPaddingH: 16,
cellPaddingV: 8,
borderWidth: 1,
),
)
CoreTableCellData<W>#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 셀 콘텐츠 |
columnSpan | int | 1 | 병합 열 수 |
rowSpan | int | 1 | 병합 행 수 |
enabled |
bool |
true |
포인터/hover 이벤트 수신 여부 |
selected |
bool |
false |
선택 상태 강제 적용 |
columnHover |
bool |
false |
호버 시 같은 열 강조 |
rowHover |
bool |
true |
호버 시 같은 행 강조 |
onPressed | void Function()? | null | 셀 탭/클릭 콜백 |
테마 (CoreTableTheme)#
CoreComponentTheme.table 또는 위젯 theme 파라미터로 아래 필드를 오버라이드할 수 있다.
| 필드 | 설명 |
|---|---|
backgroundColor | 테이블 컨테이너 배경색 |
borderColor | 셀 경계선 및 외곽선 색 |
borderWidth | 경계선 두께 (px) |
borderRadius | 컨테이너 모서리 반경 (px) |
cellPadding | 셀 기본 패딩 (px) |
headerBackgroundColor / footerBackgroundColor | 헤더/푸터 행 배경 |
rowHoverColor / rowSelectedColor | 행 hover/선택 배경 |
resizerHoverColor / resizerDragColor / resizerThickness |
리사이저 색/두께 (CoResizableTable 전용) |
크로스 플랫폼 차이점#
| 항목 | 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로 구현 |
| 드래그 리사이즈 | CoResizableTable + CoResizableTableController |
동일 — 각 셀 우측/하단에 drag handle overlay |
| 스크롤 | CoScrollableClient(builder) — 2D TwoDimensionalScrollable 기반 |
동일 API — overflow: auto + onscroll로 offset 추적해 builder 재호출 |
관련 컴포넌트#
- Card: 카드 리스트
- Pagination: 페이지 네비게이션
- Accordion: 접이식 행별 상세