mycrm-ui
v0.1.1
Published
> A headless, fully-typed React table component library. > 헤드리스 방식의 완전한 타입 지원 React 테이블 컴포넌트 라이브러리.
Readme
mycrm-ui
A headless, fully-typed React table component library. 헤드리스 방식의 완전한 타입 지원 React 테이블 컴포넌트 라이브러리.
mycrm-ui provides a powerful and flexible <Table> component for React applications.
It is headless — no styles are bundled. You own the CSS entirely via classNames.
mycrm-ui는 React 애플리케이션을 위한 강력하고 유연한 <Table> 컴포넌트를 제공합니다.
헤드리스 방식으로 번들된 스타일이 없으며, classNames를 통해 CSS를 완전히 직접 제어합니다.
Features / 특징
- Headless / 헤드리스 — 스타일 없음, CSS 완전 제어
- Fully typed / 완전한 타입 지원 — TypeScript 제네릭 기반
- Sorting / 정렬 — 단일 정렬 및 다중 정렬
- Selection / 행 선택 — 체크박스 기반 다중 선택
- Inline editing / 인라인 편집 — 셀 직접 편집 및 유효성 검사
- Row actions / 행 액션 — 행 추가 및 삭제
- Filters / 필터 — text, select, dateRange, numberRange
- Virtual scroll / 가상 스크롤 — 대용량 데이터 성능 최적화
- Infinite load / 무한 로드 — 스크롤 기반 데이터 추가 로드
- Column manager / 컬럼 매니저 — 숨김, 순서 변경, 좌우 고정, 너비 조절
- Grouped rows / 그룹 행 — 다단계 중첩 그룹 지원
- Keyboard navigation / 키보드 탐색 — 방향키 셀 탐색
- Tooltip — 셀 오버플로우 시 자동 툴팁
- Context menu / 컨텍스트 메뉴 — 셀 우클릭 메뉴
Requirements / 요구사항
- React >= 18
Installation / 설치
npm install mycrm-uiQuick Start / 빠른 시작
import { Table, type ColumnDef } from 'mycrm-ui'
interface User {
id: string
name: string
email: string
}
const columns: ColumnDef<User>[] = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
]
const data: User[] = [
{ id: '1', name: 'Alice', email: '[email protected]' },
{ id: '2', name: 'Bob', email: '[email protected]' },
]
export default function App() {
return (
<Table
columns={columns}
data={data}
rowKey={(row) => row.id}
/>
)
}Table Props
| Prop | Type | Description / 설명 |
|------|------|---------------------|
| columns | ColumnDef<T>[] | Column definitions / 컬럼 정의 목록 |
| data | T[] | Row data / 행 데이터 |
| rowKey | (row: T) => string | Unique key extractor per row / 행 고유 키 추출 함수 |
| classNames | TableClassNames | CSS class overrides for each element / 각 요소별 CSS 클래스 지정 |
| sorting | SortingProps | Sort state and change handlers / 정렬 상태 및 변경 핸들러 |
| selection | SelectionProps | Checkbox row selection / 체크박스 행 선택 |
| editing | EditingProps | Inline cell editing / 셀 인라인 편집 |
| rowActions | RowActionProps | Row add / delete actions / 행 추가·삭제 액션 |
| columnManager | ColumnManagerProps | Hide, reorder, pin, resize columns / 컬럼 숨김·순서·고정·너비 조절 |
| filter | FilterProps | Per-column filtering / 컬럼별 필터 |
| loading | LoadingProps | Skeleton loading and empty state / 스켈레톤 로딩 및 빈 상태 |
| scroll | ScrollProps | Virtual scroll and infinite load / 가상 스크롤·무한 로드 |
| expand | ExpandProps<T> | Grouped and nested rows / 그룹·중첩 행 |
| headerMenuItems | { label: string; onClick: () => void }[] | Custom header dropdown menu items / 헤더 드롭다운 메뉴 항목 |
| headerMenuIcon | React.ReactNode | Custom header menu trigger icon / 헤더 메뉴 트리거 아이콘 |
| onRowClick | (row, rowKey, event) => void | Row single-click handler / 행 클릭 핸들러 |
| onRowDoubleClick | (row, rowKey, event) => void | Row double-click handler / 행 더블클릭 핸들러 |
| rowClassName | (row, rowKey) => string \| undefined | Dynamic CSS class per row / 행별 동적 CSS 클래스 |
| tooltip | boolean | Show tooltip on overflowing cell content / 셀 오버플로우 시 툴팁 표시 |
| onCellCopy | (rowKey, colKey, value) => void | Called when a cell value is copied / 셀 값 복사 시 콜백 |
| cellContextMenuItems | { label: string; onClick: (rowKey, colKey, value) => void }[] | Right-click context menu for cells / 셀 우클릭 컨텍스트 메뉴 |
| keyboardNavigation | boolean | Enable arrow-key cell navigation / 방향키 셀 탐색 활성화 |
| onFocusedCellChange | (cell: { rowKey: string; colKey: string } \| null) => void | Called when focused cell changes / 포커스 셀 변경 시 콜백 |
ColumnDef
각 컬럼의 동작과 표현 방식을 정의합니다. Defines the behavior and appearance of each column.
interface ColumnDef<T> {
key: string // 컬럼 식별자 / column identifier
label: string // 헤더에 표시될 텍스트 / header label text
render?: (row: T) => React.ReactNode // 셀 렌더 함수 / custom cell renderer
width?: string // 컬럼 너비 (e.g. '120px') / column width
minWidth?: number // 최소 너비 (px) / minimum width in px
align?: 'left' | 'center' | 'right' // 셀 정렬 / cell text alignment
sortable?: boolean // 정렬 가능 여부 / enable sorting
editable?: boolean // 인라인 편집 가능 여부 / enable inline editing
insertable?: boolean // 행 추가 폼에 포함 여부 / include in add-row form
copyable?: boolean // 셀 복사 가능 여부 / enable cell copy
filterType?: 'text' | 'select' | 'dateRange' | 'numberRange'
filterOptions?: { label: string; value: string }[] // select 타입 옵션 목록
filterPlaceholder?: string // 필터 입력 placeholder
renderEditCell?: (props: EditCellProps) => React.ReactNode // 커스텀 편집 셀 렌더러
validate?: (value: string) => string | null // 편집값 유효성 검사
onValidationError?: (error: string) => void // 유효성 오류 핸들러
}Usage / 사용법
Sorting / 정렬
다중 정렬(multi-sort)과 단일 정렬(single sort) 두 가지 방식을 지원합니다. Both multi-sort and single sort modes are supported.
Multi-sort / 다중 정렬
const [sorts, setSorts] = useState<SortState[]>([])
<Table
sorting={{ sorts, onSortsChange: setSorts }}
...
/>Single sort / 단일 정렬
const [sort, setSort] = useState<SortState | null>(null)
<Table
sorting={{ sort, onSortChange: setSort }}
...
/>Selection / 행 선택
체크박스를 통해 여러 행을 선택할 수 있습니다. Supports multi-row selection via checkboxes.
const [selectedKeys, setSelectedKeys] = useState<string[]>([])
<Table
selection={{ enabled: true, keys: selectedKeys, onChange: setSelectedKeys }}
...
/>
// 선택된 행 처리 / handling selected rows
<button
onClick={() => console.log('selected:', selectedKeys)}
disabled={selectedKeys.length === 0}
>
Delete ({selectedKeys.length})
</button>Inline Editing / 인라인 편집
ColumnDef에서 editable: true로 설정한 컬럼은 셀 클릭 시 편집 가능한 상태로 전환됩니다.
Columns with editable: true become editable on cell click.
renderCell로 편집 UI를 완전히 커스터마이징할 수 있습니다.
Use renderCell to fully customize the editing UI.
<Table
editing={{
onCellChange: (rowKey, colKey, value) => {
// 변경된 값을 서버 또는 상태에 반영 / persist the updated value
console.log(rowKey, colKey, value)
},
renderCell: ({ value, onChange, onSave, onCancel, error }) => (
<span style={{ display: 'flex', flexDirection: 'column' }}>
<input
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onSave()
if (e.key === 'Escape') onCancel()
}}
autoFocus
/>
{error && <span style={{ color: 'red', fontSize: 12 }}>{error}</span>}
</span>
),
}}
...
/>컬럼별로 renderEditCell을 설정하면 해당 컬럼만 별도의 편집 UI를 사용할 수 있습니다.
Per-column renderEditCell overrides the global renderCell for that column only.
const columns: ColumnDef<Row>[] = [
{
key: 'memo',
label: '메모',
editable: true,
renderEditCell: ({ value, onChange, onSave, onCancel }) => (
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') onCancel()
if (e.key === 'Enter' && e.metaKey) onSave()
}}
autoFocus
/>
),
},
]유효성 검사는 컬럼별로 validate 함수로 정의합니다.
Per-column validation is defined via the validate function.
{
key: 'name',
label: '이름',
editable: true,
validate: (value) => {
if (!value.trim()) return '이름을 입력해주세요'
if (value.trim().length < 2) return '2글자 이상 입력해주세요'
return null
},
onValidationError: (error) => alert(error),
}Row Actions / 행 추가·삭제
insertable: true로 설정된 컬럼이 행 추가 폼에 포함됩니다.
Columns with insertable: true are included in the add-row form.
const [isAdding, setIsAdding] = useState(false)
<Table
rowActions={{
adding: isAdding,
onAdd: (values) => {
// values: Record<string, string> — insertable 컬럼의 입력값
console.log('추가할 데이터:', values)
setIsAdding(false)
},
onAddCancel: () => setIsAdding(false),
deletable: true,
onDelete: (rowKey) => console.log('삭제:', rowKey),
}}
...
/>Filters / 필터
ColumnDef.filterType으로 컬럼별 필터 유형을 지정합니다.
Set ColumnDef.filterType to enable per-column filtering.
'text'— 텍스트 검색 / text search input'select'— 드롭다운 선택 / dropdown (requiresfilterOptions)'dateRange'— 날짜 범위 (시작일 ~ 종료일) / date range input'numberRange'— 숫자 범위 / number range input
const [filters, setFilters] = useState<Record<string, string>>({})
const [showFilter, setShowFilter] = useState(false)
<Table
filter={{
enabled: showFilter,
values: filters,
onChange: (key, val) => setFilters((prev) => ({ ...prev, [key]: val })),
debounce: 300, // 입력 후 반영까지의 딜레이(ms) / debounce delay in ms
}}
headerMenuItems={[
{ label: showFilter ? '필터 숨기기' : '필터 보기', onClick: () => setShowFilter((v) => !v) },
{ label: '필터 초기화', onClick: () => setFilters({}) },
]}
...
/>Virtual Scroll + Infinite Load / 가상 스크롤 + 무한 로드
수만 건의 데이터도 부드럽게 렌더링됩니다. rowHeight는 각 행의 고정 높이(px)입니다.
Renders tens of thousands of rows smoothly. rowHeight is the fixed height (px) per row.
<Table
scroll={{
virtual: true,
rowHeight: 44,
stickyHeader: true,
onLoadMore: fetchNextPage, // 다음 페이지 로드 함수
hasMore: !!hasNextPage, // 추가 데이터 존재 여부
loadingMore: isFetchingNextPage, // 로딩 중 여부
}}
...
/>Column Manager / 컬럼 매니저
헤더 메뉴를 통해 컬럼 숨김, 순서 변경, 좌우 고정, 너비 조절이 가능합니다. The header menu allows hiding, reordering, pinning, and resizing columns.
const [hiddenKeys, setHiddenKeys] = useState<string[]>([])
const [columnOrder, setColumnOrder] = useState(() => columns.map((c) => c.key))
const [pinnedKeys, setPinnedKeys] = useState<{ left?: string[]; right?: string[] }>({})
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
<Table
columnManager={{
hiddenKeys,
onHiddenKeysChange: setHiddenKeys,
order: columnOrder,
onOrderChange: setColumnOrder,
pinned: pinnedKeys,
onPinnedChange: setPinnedKeys,
pinnedBg: { header: '#1e293b', body: '#0f172a' }, // 고정 컬럼 배경색
resizable: true,
widths: columnWidths,
onWidthChange: (key, width) =>
setColumnWidths((prev) => ({ ...prev, [key]: width })),
}}
...
/>Grouped / Nested Rows / 그룹·중첩 행
ExpandDef로 그룹 행을 정의하고 다단계 중첩도 지원합니다.
Define group rows with ExpandDef. Multi-level nesting is supported via childExpandDef.
interface Group {
label: string
items: Item[]
}
const expandDef: ExpandDef<Group, Item> = {
children: (group) => group.items,
childRowKey: (item) => item.id,
childColumns: itemColumns,
renderGroupLabel: (group) => (
<strong>{group.label} ({group.items.length}건)</strong>
),
}
const [expandedKeys, setExpandedKeys] = useState<string[]>([])
<Table<Group>
columns={[]}
data={groups}
rowKey={(g) => g.label}
expand={{
def: expandDef,
keys: expandedKeys,
onKeysChange: setExpandedKeys,
childSelection: {
enabled: true,
keys: childSelectedKeys,
onChange: setChildSelectedKeys,
},
childDeletable: true,
onChildDelete: (groupKey, childKey) => console.log(groupKey, childKey),
}}
/>Loading / Empty State / 로딩·빈 상태
<Table
loading={{
enabled: isLoading,
rowCount: 10, // 스켈레톤 행 수 / skeleton row count
emptyText: '데이터가 없습니다.', // 빈 상태 텍스트 / empty state text
renderEmpty: () => <p>데이터 없음</p>, // 커스텀 빈 상태 렌더러 / custom empty renderer
}}
...
/>Styling / 스타일링
컴포넌트는 헤드리스 방식으로, 번들된 스타일이 없습니다.
classNames prop으로 각 요소에 CSS 클래스를 지정하세요.
The component is headless — no styles are bundled.
Pass CSS classes to each element via the classNames prop.
<Table
classNames={{
wrap: styles.tableWrap,
table: styles.table,
th: styles.th,
tr: styles.tr,
td: styles.td,
tdFocused: styles.tdFocused, // 키보드 포커스 셀
tdEditing: styles.tdEditing, // 편집 중인 셀
filterRow: styles.filterRow,
filterInput: styles.filterInput,
filterSelect: styles.filterSelect,
skeletonRow: styles.skeletonRow,
skeletonCell: styles.skeletonCell,
groupRow: styles.groupRow,
childRow: styles.childRow,
contextMenu: styles.contextMenu,
tooltip: styles.tooltip,
// ... 전체 목록은 TableClassNames 타입 참고
// ... see TableClassNames type for the full list
}}
...
/>Versioning / 버전 관리
이 라이브러리는 Semantic Versioning을 따릅니다. This library follows Semantic Versioning.
| 변경 유형 | 버전 변경 |
|-----------|-----------|
| 버그 수정 | patch (0.1.0 → 0.1.1) |
| 기능 추가 (하위 호환) | minor (0.1.0 → 0.2.0) |
| 하위 호환 불가 변경 | major (0.1.0 → 1.0.0) |
License
MIT
