Table 테이블
열 설정형 테이블입니다. 정렬, 고정 열, 얼룩무늬 줄 무늬, 셀 슬롯을 지원하며, 대용량 데이터를 위한 가상 스크롤도 지원합니다 (core 순수 함수로 가시 창을 계산, 자체 구현이며 라이브러리를 사용하지 않습니다).
기본 사용법
<script setup>
const cols = [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
]
const rows = [
{ id: 1, name: '심연', role: '아키텍처', city: '항저우' },
{ id: 2, name: '소만', role: '프런트엔드', city: '쑤저우' },
{ id: 3, name: '육리', role: '디자인', city: '베이징' },
]
</script>
<template>
<LhTable :data="rows" :columns="cols" />
</template><script>
export default {
data() {
return {
cols: [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
],
rows: [
{ id: 1, name: '심연', role: '아키텍처', city: '항저우' },
{ id: 2, name: '소만', role: '프런트엔드', city: '쑤저우' },
{ id: 3, name: '육리', role: '디자인', city: '베이징' },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="cols" />
</template>정렬과 고정 열
<script setup>
const sortCols = [
{ prop: 'name', label: '이름', width: 140, fixed: 'left' },
{ prop: 'age', label: '나이', width: 100, sortable: true, align: 'center' },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
]
</script>
<template>
<LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template><script>
export default {
data() {
return {
sortCols: [
{ prop: 'name', label: '이름', width: 140, fixed: 'left' },
{ prop: 'age', label: '나이', width: 100, sortable: true, align: 'center' },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template>가상 스크롤 (대용량 데이터)
height를 설정하면 활성화됩니다. 가시 창 내의 행만 렌더링하며, rowHeight는 실제 행 높이와 일치해야 합니다.
<script setup lang="ts">
const cols = [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
]
const bigRows = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `행 ${i + 1}`,
role: i % 2 === 0 ? '아키텍처' : '프런트엔드',
city: '항저우',
}))
</script>
<template>
<LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template><script>
export default {
data() {
return {
cols: [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
],
bigRows: Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `행 ${i + 1}`,
role: i % 2 === 0 ? '아키텍처' : '프런트엔드',
city: '항저우',
})),
}
},
}
</script>
<template>
<LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template>커스텀 셀
<script setup lang="ts">
const rows = [
{ id: 1, name: '심연', age: 34, role: '아키텍처', city: '항저우' },
{ id: 2, name: '소만', age: 28, role: '프런트엔드', city: '쑤저우' },
{ id: 3, name: '육리', age: 31, role: '디자인', city: '베이징' },
]
const slotCols = [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'role', label: '직무', slot: 'role' },
]
</script>
<template>
<!-- 직무 열 슬롯 커스텀 -->
<LhTable :data="rows" :columns="slotCols">
<template #role="{ value }">
<LhTag :type="value === '아키텍처' ? 'primary' : 'info'">{{ value }}</LhTag>
</template>
</LhTable>
</template><script>
export default {
data() {
return {
rows: [
{ id: 1, name: '심연', age: 34, role: '아키텍처', city: '항저우' },
{ id: 2, name: '소만', age: 28, role: '프런트엔드', city: '쑤저우' },
{ id: 3, name: '육리', age: 31, role: '디자인', city: '베이징' },
],
slotCols: [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'role', label: '직무', slot: 'role' },
],
}
},
}
</script>
<template>
<!-- 직무 열 슬롯 커스텀 -->
<LhTable :data="rows" :columns="slotCols">
<template #role="{ value }">
<LhTag :type="value === '아키텍처' ? 'primary' : 'info'">{{ value }}</LhTag>
</template>
</LhTable>
</template>다단 헤더
열 설정에 children을 중첩하면 다단 헤더가 되며, 무한히 중첩할 수 있습니다. 표 본체는 잎 열에 맞춰 정렬됩니다.
<script setup>
const nestedCols = [
{ prop: 'name', label: '이름', width: 140, fixed: 'left' },
{
label: '프로젝트 정보',
children: [
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
],
},
{ prop: 'age', label: '나이', sortable: true, align: 'center' },
]
</script>
<template>
<LhTable :data="rows" :columns="nestedCols" border />
</template><script>
export default {
data() {
return {
nestedCols: [
{ prop: 'name', label: '이름', width: 140, fixed: 'left' },
{
label: '프로젝트 정보',
children: [
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120 },
],
},
{ prop: 'age', label: '나이', sortable: true, align: 'center' },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="nestedCols" border />
</template>필터
열 설정에 filters를 지정하면 헤더에 필터 트리거가 나타납니다. 옵션을 선택하고「확인」을 누르면 적용되고,「초기화」를 누르면 비워집니다. filterMultiple: false는 단일 선택이며, filterMethod로 판정을 커스터마이징할 수 있습니다. 제어되는 사용법은 아래 예시를 참조하세요.
현재 필터: {}
<script setup>
import { ref } from 'vue'
const filteredValues = ref({})
const filterCols = [
{ prop: 'name', label: '이름', width: 140 },
{
prop: 'role', label: '직무', filters: [
{ text: '아키텍처', value: '아키텍처' },
{ text: '프런트엔드', value: '프런트엔드' },
{ text: '디자인', value: '디자인' },
],
},
]
function onFilterChange({ prop, values }) {
console.log('filterChange', prop, values)
}
</script>
<template>
<!-- 제어되는 필터 -->
<LhTable
:data="rows"
:columns="filterCols"
v-model:filtered-values="filteredValues"
@filter-change="onFilterChange"
/>
</template><script>
export default {
data() {
return {
filteredValues: {},
filterCols: [
{ prop: 'name', label: '이름', width: 140 },
{
prop: 'role', label: '직무', filters: [
{ text: '아키텍처', value: '아키텍처' },
{ text: '프런트엔드', value: '프런트엔드' },
{ text: '디자인', value: '디자인' },
],
},
],
}
},
methods: {
onFilterChange({ prop, values }) {
console.log('filterChange', prop, values)
},
},
}
</script>
<template>
<!-- 제어되는 필터 -->
<LhTable
:data="rows"
:columns="filterCols"
v-model:filtered-values="filteredValues"
@filter-change="onFilterChange"
/>
</template>열 너비 드래그
resizable (마스터 스위치)를 켜고 열에 resizable: true를 표시합니다 (잎 열이며 숫자 width여야 함). 헤더 오른쪽 가장자리에 드래그 핸들이 나타나며, 포커스 후 ←/→로 매번 ±10px씩 조정할 수 있고 드래그 하한은 40px입니다.
최근 조정: (조정 없음)
<script setup>
import { ref } from 'vue'
const resizeLog = ref('(조정 없음)')
const resizeCols = [
{ prop: 'name', label: '이름', width: 140, resizable: true, fixed: 'left' },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120, resizable: true },
]
function onColumnResize({ prop, width }) {
resizeLog.value = `${prop} → ${width}px`
}
</script>
<template>
<!-- 열 너비 드래그 -->
<LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template><script>
export default {
data() {
return {
resizeLog: '(조정 없음)',
resizeCols: [
{ prop: 'name', label: '이름', width: 140, resizable: true, fixed: 'left' },
{ prop: 'role', label: '직무' },
{ prop: 'city', label: '도시', width: 120, resizable: true },
],
}
},
methods: {
onColumnResize({ prop, width }) {
this.resizeLog = `${prop} → ${width}px`
},
},
}
</script>
<template>
<!-- 열 너비 드래그 -->
<LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template>행 키, 최대 높이, 기본 정렬
row-key는 행의 고유 키를 지정합니다 (권장). max-height로 높이를 제한하면 헤더가 상단에 고정되고 표 본체가 스크롤됩니다. sorter는 커스텀 정렬 함수, default-sort-prop / default-sort-order는 초기 정렬, default-filtered-values는 초기 필터를 설정합니다.
<script setup>
const sorterCols = [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'age', label: '나이', sortable: true, sorter: (a, b) => a.age - b.age },
{ prop: 'role', label: '직무' },
]
</script>
<template>
<LhTable
:data="rows"
:columns="sorterCols"
row-key="name"
max-height="200"
default-sort-prop="age"
default-sort-order="desc"
/>
</template><script>
export default {
data() {
return {
sorterCols: [
{ prop: 'name', label: '이름', width: 140 },
{ prop: 'age', label: '나이', sortable: true, sorter: (a, b) => a.age - b.age },
{ prop: 'role', label: '직무' },
],
}
},
}
</script>
<template>
<LhTable
:data="rows"
:columns="sorterCols"
row-key="name"
max-height="200"
default-sort-prop="age"
default-sort-order="desc"
/>
</template>API
LhTable Props
| 프로퍼티 | 설명 | 타입 | 기본값 |
|---|---|---|---|
| data | 행 데이터 | [LhTableRow](#type-LhTableRow)[] | — |
| columns | 열 설정 | [LhTableColumn](#type-LhTableColumn)[] | — |
| height | 표 본체 높이 (설정하면 가상 스크롤 활성화, 숫자는 px) | string | number | — |
| maxHeight | 가상 스크롤 모드가 아닐 때의 최대 높이 | string | number | — |
| rowHeight | 가상 스크롤 행 높이 (실제 행 높이와 일치해야 함) | number | [LH_TABLE_ROW_HEIGHT](#const-LH_TABLE_ROW_HEIGHT) |
| stripe | 얼룩무늬 줄 무늬 | boolean | false |
| border | 셀 테두리 | boolean | false |
| size | 크기 | "small" | "default" | "large" | 'default' |
| rowKey | 행 키 필드명 | string | 'id' |
| defaultSortProp | 기본 정렬 열 | string | — |
| defaultSortOrder | 기본 정렬 방향 | "asc" | "desc" | — |
| sorter | 커스텀 정렬 함수 | (a: LhTableRow, b: LhTableRow) => number | — |
| resizable | 열 너비 드래그 활성화 (열 수준 resizable 설정도 필요) | boolean | false |
| defaultFilteredValues | 기본 필터 상태 (제어되지 않음, 키는 열 prop) | { [x: string]: (string | number)[]; } | () => ({}) |
| filteredValues | 제어되는 필터 상태 (키는 열 prop) | { [x: string]: (string | number)[]; } | — |
| sx | sx 빠른 레이아웃 속성 | LhSxProps | — |
Events
| 이벤트명 | 설명 | 콜백 매개변수 |
|---|---|---|
| sortChange | 정렬이 변경될 때 트리거 (열을 바꾸면 asc부터 다시 시작, 같은 열은 asc → desc → 취소 순환) | (payload: { prop: string, order: 'asc' | 'desc' | undefined }) |
| rowClick | 행을 클릭할 때 트리거 | (row: LhTableRow, index: number) |
| filterChange | 필터「확인」또는「초기화」제출 후 트리거 | (payload: { prop: string, values: (string | number)[] }) |
| update:filteredValues | 필터 상태가 변경될 때 트리거 (v-model:filtered-values) | (values: Record<string, (string | number)[]>) |
| columnResize | 열 너비 조정 완료 후 트리거 (드래그 종료 또는 키보드 ←/→) | (payload: { prop: string, width: number }) |
슬롯: empty, <column.slot> (범위 매개변수 { row, column, index, value }).
LhTableColumn
| 필드 | 설명 | 타입 |
|---|---|---|
| prop | 데이터 필드명 | string |
| label | 열 제목 | string |
| width | 열 너비 (숫자는 px, 설정하지 않으면 균등 분배) | number | string |
| align | 정렬 | 'left' | 'center' | 'right' |
| fixed | 고정 열 (width 필요) | 'left' | 'right' |
| sortable | 정렬 가능 | boolean |
| slot | 커스텀 셀 슬롯명 | string |
| formatter | 값 형식화 | (row, value) => string |
| children | 하위 열 (설정하면 다단 헤더) | LhTableColumn[] |
| filters | 열 필터 옵션 (잎 열만 유효) | LhTableFilterOption[] |
| filterMethod | 커스텀 필터 판정 (기본은 prop 완전 일치 비교) | (value, row) => boolean |
| filterMultiple | 필터 패널 다중 선택 여부 | boolean |
| resizable | 이 열의 너비 드래그 가능 (숫자 width인 잎 열이어야 함) | boolean |
무장애
role="grid"+aria-rowcount/aria-colcount, 행은row, 열 헤더는columnheader, 셀은gridcell- 정렬 열 헤더는
aria-sort(none/ascending/descending)를 노출하며, 정렬 트리거는 실제<button>입니다 - 고정 열은 CSS
position: sticky를 사용합니다 (자체 흡착 계산을 하지 않음) - 필터 트리거는
aria-haspopup+aria-expanded이며, 패널은role="group"입니다 - 열 너비 핸들은
role="separator"이며 포커스할 수 있고 ←/→ 키로 ±10px 조정할 수 있습니다
타입 정의
이 컴포넌트가 외부로 노출하는 TypeScript 타입 구조는 다음과 같습니다 (packages/components/table/src/types.ts와 @lh-ui/share/core/* 소스에서 자동 추출, 각 항목을 펼쳐 전체 정의를 확인하세요. 프로퍼티 설명은 위쪽 Props 표를 참조하세요):
LhTableProps
export interface LhTableProps {
/** 행 데이터 */
data: LhTableRow[]
/** 열 설정 */
columns: LhTableColumn[]
/** 표 본체 높이 (설정하면 가상 스크롤 활성화, 숫자는 px) */
height?: number | string
/** 가상 스크롤 모드가 아닐 때의 최대 높이 */
maxHeight?: number | string
/** 가상 스크롤 행 높이 (실제 행 높이와 일치해야 함) */
rowHeight?: number
/** 얼룩무늬 줄 무늬 */
stripe?: boolean
/** 셀 테두리 */
border?: boolean
/** 크기 */
size?: 'small' | 'default' | 'large'
/** 행 키 필드명 */
rowKey?: string
/** 기본 정렬 열 */
defaultSortProp?: string
/** 기본 정렬 방향 */
defaultSortOrder?: LhTableSortOrder
/** 커스텀 정렬 함수 */
sorter?: (a: LhTableRow, b: LhTableRow) => number
/** 열 너비 드래그 활성화 (열 수준 resizable 설정도 필요, 열은 숫자 width여야 함) */
resizable?: boolean
/** 기본 필터 상태 (제어되지 않음, 키는 열 prop) */
defaultFilteredValues?: Record<string, (string | number)[]>
/** 제어되는 필터 상태 (키는 열 prop) */
filteredValues?: Record<string, (string | number)[]>
/** sx 빠른 레이아웃 속성 */
sx?: LhSxProps
}LhTableSortOrder (출처: @lh-ui/share/core/table)
/** 정렬 방향 */
export type LhTableSortOrder = 'asc' | 'desc'LhTableRow (출처: @lh-ui/share/core/table)
/** 행 데이터 타입 */
export type LhTableRow = Record<string, any>LhTableColumn (출처: @lh-ui/share/core/table)
/** 열 설정 */
export interface LhTableColumn {
/** 데이터 필드명 */
prop: string
/** 열 제목 */
label: string
/** 열 너비 (숫자는 px) */
width?: number | string
/** 정렬 */
align?: LhTableAlign
/** 고정 열 (width 필요) */
fixed?: 'left' | 'right'
/** 정렬 가능 */
sortable?: boolean
/** 커스텀 셀 슬롯명 */
slot?: string
/** 값 형식화 */
formatter?: (row: LhTableRow, value: any) => string
/** 하위 열 (설정하면 다단 헤더, 무한 중첩 가능) */
children?: LhTableColumn[]
/** 열 필터 옵션 (잎 열만 유효) */
filters?: LhTableFilterOption[]
/** 커스텀 필터 판정 (기본은 prop 완전 일치 비교) */
filterMethod?: (value: string | number, row: LhTableRow) => boolean
/** 필터 패널 다중 선택 여부 (기본 true) */
filterMultiple?: boolean
/** 이 열의 너비 드래그 가능 (숫자 width인 잎 열이어야 함) */
resizable?: boolean
}LhTableFilterOption (출처: @lh-ui/share/core/table)
/** 필터 옵션 */
export interface LhTableFilterOption {
/** 옵션 문구 */
text: string
/** 옵션 값 (filterMethod 비교에 참여) */
value: string | number
}LhTableAlign (출처: @lh-ui/share/core/table)
/** 셀 정렬 */
export type LhTableAlign = 'left' | 'center' | 'right'상수
이 컴포넌트에서 사용하는 공개 상수는 다음과 같습니다 (Props 표의 「타입 / 기본값」 열에서 동일한 이름의 항목을 클릭하면 이곳으로 이동합니다):
LH_TABLE_ROW_HEIGHT (출처: @lh-ui/share/core/table)
/** 가상 스크롤 기본 행 높이 */
export const LH_TABLE_ROW_HEIGHT = 48LH_TABLE_OVERSCAN (출처: @lh-ui/share/core/table)
/** 창 상하에 각각 몇 행 더 렌더링 (스크롤 빈 화면 버퍼) */
export const LH_TABLE_OVERSCAN = 3LH_TABLE_MIN_COL_WIDTH (출처: @lh-ui/share/core/table)
// ============================================================================
// 열 너비 드래그
// ============================================================================
/** 드래그 하한 (px), 이보다 좁아지면 셀 콘텐츠를 담을 수 없음 */
export const LH_TABLE_MIN_COL_WIDTH = 40