2024년 10월 29일

코딩 - Vue3 : AG-Grid 커뮤니티 버전 기반의 페이징 그리드 컴포넌트 만들기

 AG Grid Community 버전은 매우 인기 있는 오픈소스 그리드 컴포넌트로, 다양한 기능을 제공하지만 상업적인 용도로 사용할 때는 프로 버전으로의 업그레이드가 필요할 수도 있다. 커뮤니티 버전의 이점과 한계를 살펴보면 다음과 같다.

AG Grid Community  버전의 이점

  1. 무료 사용: 커뮤니티 버전은 오픈소스로 제공되므로 무료로 사용할 수 있다. 중소규모의 프로젝트나 상업적 용도가 아닌 경우에 적합.
  2. 강력한 성능:  AG Grid 는 많은 양의 데이터를 효율적으로 처리할 수 있는 그리드 컴포넌트로 기본적인 CRUD 작업을 포함하여 빠른 렌더링 성능을 지원.
  3. 광범위한 기능:
    • 기본적인 정렬, 필터링, 페이징, 그룹핑, 편집 등 그리드에서 필요한 대부분의 기능을 포함
    • 다양한 컬럼 레이아웃과 사용자 정의 셀 렌더러 등 유연한 데이터 표현이 지원
  4. 활발한 커뮤니티: AG Grid는 대규모 사용자 커뮤니티를 보유하고 있어 문제 발생 시 도움을 얻기 용의.
  5. 다양한 프레임워크 지원: React, Angular, Vue, 순수 자바스크립트 등 여러 프레임워크에서 사용할 수 있는 유연함

AG Grid Community 버전의 한계

  1. 고급 기능 제한: 커뮤니티 버전에는 고급 기능이 포함되어 있지 않음. 특히 아래 기능들이 프로 버전에만 포함.
    • 서버 사이드 모델(Server-Side Model): 대규모 데이터 집합에서 서버와 연동하여 그리드 데이터를 관리하는 기능.
    • 익스포트 기능: CSV, Excel 등의 형식으로 데이터를 내보내는 기능이 제한.
    • Tree Data 구조: 트리 구조의 데이터를 처리할 수 있는 고급 데이터 표현 방식 미지원.
    • 엔터프라이즈 레벨 필터링: 고급 필터링 기능은 프로 버전에만 포함.
  2. 기술 지원 부족: 커뮤니티 버전에서는 공식적인 기술 지원을 받을 수 없음. 문제 해결 시 커뮤니티에 의존해야 함.
  3. 사용 제한: 상업적 프로젝트에서 고급 기능이 필요하거나 성능 최적화 및 유지보수가 중요한 경우, 커뮤니티 버전만으로는 한계가 있을 수 있음.
AG Grid Community 버전은 무료로 사용할 수 있고, 기본적인 그리드 기능을 모두 제공하기 때문에 많은 프로젝트에서 유용하다. 그러나 고급 기능이나 대규모 데이터 처리가 필요한 경우 프로 버전으로 업그레이드하는 것이 필요하다. 

유료 버전에서는 rowModelType을 "serverSide" 로 설정하고 그리드가 준비되면 getRows 함수 내에서 서버 요청을 수행하고 결과를 그리드에 전달하는 데이터소스를 api.setServerSideDatasource() 함수를 사용하여 설정하는 방식으로 쉽게 구현할 수 있다.

AG Grid 커뮤니티 버전에서 서버 페이징은 (무한 스크롤을 구현하는 방법) rowModelType 값으로  "infinite" 을 사용하여 비슷한 방법으로 구현할 수 있다. 
  1. rowModelType을 "infinite"로 설정하여 그리드가 데이터를 한 번에 모두 로드하지 않고, 필요한 만큼 서버에서 요청하도록 한다.
  2. getRows 함수는 그리드가 추가 데이터를 요청할 때마다 서버에 데이터를 가져오는 역할을 한다.
  3. dataSource는그리드가 준비될 때 설정하고, 그리드가 데이터를 필요로 할 때마다 자동으로 서버에 데이터를 요청한다. ( 그리드 준비 여부는 onGridReady 이벤트를 사용)
개발 단계를 구체적으로 설명하면 아래와 같다.

설명 코드 예시
1 프로젝트에 AG Grid 설치 npm install --save ag-grid-community ag-grid-vue
2 컬럼 정의 및 그리드 옵션 설정 columnDefsgridOptions<script setup lang="ts">에서 정의하고 ref로 선언
3 Infinite Row Model 활성화 gridOptions에서 rowModelType'infinite'로 설정하고, paginationPageSizecacheBlockSize를 정의
4 onGridReady 함수에서 서버 데이터 소스 설정 onGridReady에서 getRows 함수가 포함된 dataSource를 설정
5 getRows 함수에서 서버 데이터 요청 params.startRow, params.endRow를 기반으로 페이지와 페이지 크기를 서버에 요청
6 서버 응답 처리 서버의 데이터 응답을 받아 params.successCallback(data.rows, data.totalCount)을 호출하여 그리드에 전달
7 Vue 컴포넌트 마운트 시 그리드 참조 설정 onMounted에서 gridRef를 통해 그리드 API에 접근하여 초기 설정 완료


매번 서버 페이징이 필요한 경우 위와 같이 코딩하는 것은 여간 불편한 것이 아니다. 이런 이유에서 추상 형태의 DataSource 을 정의하고 공통으로 적용할 설정을 미리 정의하고 필요한 값만 전달하여 페이징 그리드를 구현하는 컴포넌트를 만들어 보았다.

// plugins/ag-gird/ag-grid-options.ts
import type { GridOptions } from 'ag-grid-community';
// 날짜 변활을 위한 포맷터
import { DateFormatter, SimpleDateFormatter } from '@/util/helpers';
// 사용자 정의 렌더러
import CheckboxRenderer from '@/components/ag-grid/renderer/CheckboxRenderer.vue';
import CustomBooleanFilter from '@/components/ag-grid/renderer/CustomBooleanFilter.vue';
import CustomLoadingOverlay from '@/components/ag-grid/CustomLoadingOverlay.vue';
const gridOptions : GridOptions = {
loadingOverlayComponent: CustomLoadingOverlay,
defaultColDef: {
flex: 1,
minWidth: 100,
suppressMovable: true ,
resizable: true,
sortable: true,
filter: true,
cellStyle: {
display: "flex",
alignItems: "center",
},
},
columnTypes: {
string: {
filter: 'agTextColumnFilter',
filterParams: { maxNumConditions: 1 },
cellStyle: { textAlign: 'center' },
},
number: {
filter: 'agNumberColumnFilter',
filterParams: { maxNumConditions: 1 },
cellStyle: { textAlign: 'right' },
},
boolean: {
cellRenderer: CheckboxRenderer,
cellStyle: { textAlign: 'center' },
filter: CustomBooleanFilter,
filterParams: {values: [true, false], maxNumConditions: 1}
},
date: {
filter: 'agDateColumnFilter',
filterParams: { maxNumConditions: 1},
valueFormatter: DateFormatter,
cellStyle: { textAlign: 'center' },
},
simpledate: {
filter: 'agDateColumnFilter',
filterParams: { maxNumConditions: 1},
valueFormatter: SimpleDateFormatter,
cellStyle: { textAlign: 'center' },
}
},
};
export default gridOptions;
// types/index.ts
import type { SortModelItem } from 'ag-grid-community';
import type { Ref } from 'vue';
type Datasource = {
isLoaded: Ref<boolean>;
dataItems: Ref<any[]>;
total: Ref<number>;
pageSize: Ref<number>;
setSort(newValue: SortModelItem[]): void;
setFilter(newValue: any): void;
setPage(newVal: number):void;
fetch(): Promise<void>;
}
type PageableDataSource = {
isLoaded: Ref<boolean>;
dataItems: Ref<any[]>;
total: Ref<number>;
pageSize: Ref<number>;
setSort(newValue: SortModelItem[]): void;
setFilter(newValue: any): void;
setPage(newVal: number):void;
fetch(): Promise<void>;
}
type EnableActions = {
enable(): Promise<void>;
disable(): Promise<void>;
isEnabled(): Promise<boolean>;
}
export type { Datasource, EnableActions, PageableDataSource };
view raw index.ts hosted with ❤ by GitHub
<template>
<div ref="gridContainer"
class="ag-theme-alpine" :style="{ width: '100%', height: gridHeight + 'px' }">
<AgGridVue class="ag-theme-quartz" style="width: 100%; height: 100%;"
:rowModelType="'infinite'"
:gridOptions="gridOptionsDefs"
:columnDefs="columnDefs"
:pagination="true"
:paginationPageSize="pageSize"
:cacheBlockSize="cacheBlockSize"
@selectionChanged="onSelectionChanged"
@grid-ready="onGridReady"></AgGridVue>
</div>
</template>
<script setup lang="ts">
import gridOptions from '@/plugins/ag-grid/ag-grid-options';
import type {
PageableDataSource,
} from "@/types/index";
import type { ColDef, GridApi, GridOptions, IGetRowsParams, SelectionChangedEvent } from 'ag-grid-community';
import { AgGridVue } from "ag-grid-vue3"; // Vue Data Grid Component
import { computed, defineProps, onMounted, ref, watch, } from 'vue';
// Event Listener 설정
interface Listener {
type: string,
listener : (event: any) => void
}
// Props 설정
const props = defineProps<{
columns?: ColDef[],
options?: GridOptions,
events?: Listener[],
datasource: PageableDataSource;
}>();
// 페이지 크기 및 캐시 크기 설정
const pageSize = ref(props.datasource?.pageSize);
const cacheBlockSize = ref(props.datasource?.pageSize);
// 로딩 및 데이터 관리
const loader = ref(false);
const gridData = ref<any[]>([]);
const total = ref<number>(0);
// Grid options 설정
// 프로퍼티로 전달된 옵션을 사용하거나 기본값 사용
const gridOptionsDefs:GridOptions = computed(() => props.options||gridOptions );
// Grid columns 설정
// 프로퍼티로 전달된 컬럼을 사용하거나 빈 배열 사용
const columnDefs:ColDef[] = computed(() => props.columns || []);
// 그리드 동적 크기 조정
const gridContainer = ref<HTMLElement | null>(null);
const gridHeight = ref(400); // initial height
const resizeGrid = () => {
if (gridContainer.value) {
gridHeight.value = window.innerHeight - gridContainer.value.getBoundingClientRect().top - 20; // Adjust as needed
gridApi.value?.sizeColumnsToFit();
}
};
// Grid API 설정
const gridApi = ref<GridApi | null>();
const onGridReady = (params: any) => {
gridApi.value = params.api;
const dataSource = {
getRows: async (params: IGetRowsParams) => {
// 페이지 및 페이지 크기 설정
const page = Math.floor(params.startRow / pageSize.value);
const pageSizeValue = params.endRow - params.startRow;
// 필터 및 정렬 설정
const filterModel = params.filterModel;
const sortModel = params.sortModel;
try {
props.datasource?.setSort(sortModel);
props.datasource?.setFilter(filterModel);
props.datasource?.setPage(page);
// 필터가 설정되어 있는 경우
filtersActive.value = Object.keys(filterModel).length > 0;
await getData(true);
params.successCallback(gridData.value, total.value);
} catch (error) {
params.failCallback();
}
}
};
params.api.setGridOption('datasource', dataSource);
// 이벤트 리스너 설정
props.events?.forEach( item => params.api.addEventListener(item.type, item.listener) );
resizeGrid();
};
// 필터 관리
const emit = defineEmits(['filterActived']);
const filtersActive = ref(false);
const onFilterChanged = () => {
const filterModel = gridApi.value?.getFilterModel();
filtersActive.value = Object.keys(filterModel||{}).length > 0;
};
// filtersActive 값이 변경되면 filtersActive 이벤트 발생
watch(filtersActive, (val, oldVal) => {
emit('filterActived', val);
});
// 데이터 새로고침
function refresh() {
gridApi.value?.refreshInfiniteCache();
}
//필터 초기화
const clearFilters = () => {
gridApi.value?.setFilterModel({});
gridApi.value?.onFilterChanged();
};
// 선택된 행 관리
const selectedItems = ref<[]>([]);
const onSelectionChanged = (params: SelectionChangedEvent) => {
selectedItems.value = params.api.getSelectedRows() as [];
};
const selectedRows = () => {
return selectedItems.value;
};
const tooltipShowDelay = ref(null);
// 데이터 가져오기
// force : true 인 경우 데이터를 강제로 가져옴
async function getData(force: boolean = false) {
loader.value = true;
gridApi.value?.setGridOption("loading", true);
if (!props.datasource?.isLoaded || force)
await props.datasource?.fetch();
gridData.value = props.datasource?.dataItems ;
total.value = props.datasource?.total as number;
gridApi.value?.setGridOption("loading", false);
if (total.value === 0)
gridApi.value?.showNoRowsOverlay();
loader.value = false;
}
onMounted(async () => {
// Initial resize
resizeGrid();
});
defineExpose({
refresh, clearFilters, selectedRows
});
</script>

아래는 PageableGridContent.vue 에서 사용하는 서버에서 데이터를 가져오는 PageableDataSource 에 대한 추상 AbstractPageDataSource 이다. 이를 통하여 좀더 쉽게 서버에서 데이터를 가져오는 데이터소스 구현을 가능하게 한다. ( 구현체에서는 규칙을 벗어 나지 않는 경우라면 서버 엔트포인트 값을 리턴하는 getPatchUrl () 만 구현하면 된다. 서버 프로그램은 스프링의 Pageable  형식으로 데이터를 리턴하는 것을 가정 하여 데이터소스 추상을 구현 했다.

import { PageableDataSource } from "@/types/studio";
import { Ref, ref } from "vue";
import type { SortModelItem } from "ag-grid-community";
import { API_HEADERS, authHeader, convertAgGridFilterToKendo } from "@/util/helpers";
import { useAlertStore } from "../alert.store";
import axios from "axios";
export abstract class AbstractDataSource implements PageableDataSource {
alertStore = useAlertStore();
// 공통 상태 정의
isLoaded: Ref<boolean> = ref(false);
dataItems: Ref<any[]> = ref([]);
total: Ref<number> = ref(0);
page: Ref<number> = ref<number>(1);
pageSize: Ref<number> = ref<number>(20);
sort: Ref<SortModelItem[]> = ref<SortModelItem[]>([]);
filter:any = ref({ logic: "and", filters: [] });
// 공통 메서드 구현
setPage(newVal: number): void {
if (this.page.value === newVal) return;
this.page.value = newVal;
this.isLoaded.value = false;
}
setSort(newValue: SortModelItem[]): void {
this.sort.value = newValue;
}
setFilter(newValue: any): void {
this.filter.value = convertAgGridFilterToKendo(newValue||{});
}
pagableParams() {
return {
page: this.page.value,
size: this.pageSize.value,
sort: this.sort_string(),
};
}
sort_string() {
if (this.sort.value.length > 0) {
const field = this.sort_field() || "";
const dir = this.sort_dir() || "";
return `${field},${dir}`;
}
return null;
}
sort_field() {
if (this.sort.value.length > 0) {
return this.sort.value[0].colId;
}
return null;
}
sort_dir() {
if (this.sort.value.length > 0) {
return this.sort.value[0].sort;
}
return null;
}
/**
* 데이터를 가져오는 메서드
* fetch 에서 호출하는 endpoint 는 필터가 있는 경우는 POST 요청, 없는 경우는 GET 요청을 사용한다.
*/
async fetch(): Promise<void> {
const headers = { ...API_HEADERS, ...authHeader() };
try {
let response;
if (this.filter.value.filters && this.filter.value.filters.length > 0) {
// 필터가 있을 때는 POST 요청
response = await axios.post(
this.getFetchUrl(),
JSON.stringify(this.filter.value),
{
params: { ...this.pagableParams() },
headers: headers,
}
);
} else {
// 필터가 없을 때는 GET 요청
response = await axios.get(this.getFetchUrl(), {
params: { ...this.pagableParams() },
headers: headers,
});
}
// 공통 처리 로직
const data = response.data;
this.dataItems.value = data.items || [];
this.total.value = data.totalCount || 0;
this.isLoaded.value = true;
} catch (error) {
// 에러 처리
this.alertStore.error(error);
}
}
// 추상 메서드: 하위 클래스에서 반드시 구현해야 함
abstract getFetchUrl(): string;
}
import { useAuthStore } from "@/store/auth.store";
import axios from "axios";
import { format } from 'date-fns';
const API_HEADERS = {
Accept: "application/json",
"Content-Type": "application/json",
};
export function authHeader() {
// return authorization header with jwt token
const auth = useAuthStore();
if (auth.user != null && auth.user.jwtToken) {
let user = auth.user;
return { Authorization: "Bearer " + user.jwtToken };
} else {
return {};
}
}
////////////////////
// AG-GRIID UTILS
export const convertAgGridFilterToKendo = (agFilterModel:any) => {
const kendoFilters = Object.keys(agFilterModel).map((colId) => {
const filter = agFilterModel[colId];
let operator;
switch (filter.filterType) {
case 'text':
operator = convertNumberFilterType(filter.type);
break;
case 'number':
operator = convertNumberFilterType(filter.type);
break;
case 'boolean':
operator = 'eq';
break;
case 'date':
operator = convertNumberFilterType(filter.type);;
break;
default:
operator = 'eq';
}
return {
field: colId,
operator: operator,
value: filter.filter
};
});
return {
logic: 'and',
filters: kendoFilters
};
};
// 숫자 필터 타입 변환 함수
const convertNumberFilterType = (type:string) => {
switch (type) {
case 'equals':
return 'eq';
case 'notEqual':
return 'ne';
case 'lessThan':
return 'lt';
case 'lessThanOrEqual':
return 'lte';
case 'greaterThan':
return 'gt';
case 'greaterThanOrEqual':
return 'gte';
case 'contains':
return 'contains'
default:
return 'eq';
}
};
view raw helper.ts hosted with ❤ by GitHub

위의 컴포넌트를 사용하여 1) 컴포넌트 임포트 2)  그리드 컬럼 정의 3) 태그를 사용하여 컴포넌트를 정의하는 것으로 그리드를 간단하게 구현 할 수 있다. 또한 컴포넌트에 직접 제어가능한 함수를 사용하여 새로고침, 선택된 데이터 접근, 필터 초기화 등을 제어할 수 있다. 그리드 컬럼에서 단수 데이터 출력이 아닌 버튼과 또는 이미지 출력 등의 작업이 필요한 경우는 사용자 정의 더러를 정의하고 버튼 클릭등의 이벤트는 이벤트 리스트너를 통하여 구현하였다. 

<template>
<v-container :fluid="true">
<v-breadcrumbs :items="['Security', 'Roles']" density="compact"></v-breadcrumbs>
<PageToolbar title="Roles" @refresh="refresh" @custom="onClearFilters" @create="edit( { roleId : 0, name: null } )" :divider="false" :items="[
{ icon: 'mdi-plus', event: 'create' },
{ icon: 'mdi-filter-variant-remove', event:'custom', visible:filtersActive },
{ icon: 'mdi-refresh', event: 'refresh' },
]"
></PageToolbar>
<v-row>
<v-col cols="12">
<PageableGridContent
@filter-actived="onPageableGridFilterActived"
ref="pageableGridContentRef"
:datasource="rolesStore"
:events="[
{ type: 'roleCellRenderer:edit', listener: handleEdit }
]"
:columns="columnDefs">
</PageableGridContent>
</v-col>
</v-row>
</v-container>
<RoleDialog v-model="roleDialog.visible" :roleId="roleDialog.roleId" @close="roleDialog.visible = false"></RoleDialog>
<Alert></Alert>
</template>
<script setup lang="ts">
import Alert from '@/components/Alert.vue';
import PageToolbar from '@/components/PageToolbar.vue';
import PageableGridContent from '@/components/ag-grid/PageableGridContent.vue';
import EditRoleCellRenderer from '@/components/ag-grid/renderer/EditRoleCellRenderer.vue';
import { usePageableRolesStore } from '@/store/studio/mgmt/mgmt.pageable.roles.store';
import { RoleModel } from '@/types/studio/RoleModel';
import type { ColDef } from 'ag-grid-community';
import { ref } from 'vue';
import RoleDialog from './RoleDialog.vue';
// START : define pagable grid content .
const rolesStore = usePageableRolesStore();
const columnDefs:ColDef[] = [
{ field: 'roleId', headerName: 'ID', type: 'number', width: 100, sortable: true , flex:.5 },
{ field: 'name', headerName: 'Name', sortable: true , type:"string", flex:2 , cellRenderer : EditRoleCellRenderer },
{ field: 'description', headerName: 'Description', type: 'string', flex:1, sortable: false , filter:false},
{ field: 'creationDate', headerName: 'Created', type: 'date', width: 100 },
{ field: 'modifiedDate', headerName: 'Modified', type: 'date', width: 100 },
];
const pageableGridContentRef = ref<InstanceType<typeof PageableGridContent> | null>(null);
const filtersActive = ref(false);
function onPageableGridFilterActived(event: any) {
filtersActive.value = event;
}
const refresh = () => {
pageableGridContentRef.value?.refresh();
}
const onClearFilters = () => {
pageableGridContentRef.value?.clearFilters();
};
// END : define pagable grid content .
function handleEdit( event : any ){
const item = event.data as RoleModel;
edit(item);
}
// role dialog
const roleDialog = ref({
visible: false,
roleId : 0,
});
function edit(role: RoleModel) {
roleDialog.value.visible = true;
roleDialog.value.roleId = role.roleId;
}
function closeRoleDialg() {
roleDialog.value.visible = false;
roleDialog.value.roleId = 0;
}
</script>
view raw RolesPage.vue hosted with ❤ by GitHub

다음은 위의 AbstractPageDataSource 활용하여 구현한 예이다. 아래는 목록을 조회하는 기능과 추가로 여러 함수를 포함하고 있다.

구현 클래스는 추가 기능이 필요하면 PageableDataSource 을 포함하여 새로운 타입을 정의하고 이를 구현하는 클래스를 AbstractPageableDataSource 상속하여 만든다. 구현 클래스는 getFetchUrl 함수를 구현하는 것으로 완성되며 this 이슈를 해결하기 위하여 반인딩을 사용하여 스토어를 생성하게 한다.
  1. 필요한 새로운 PageableDataSource 을 포함하는 type 생성
  2. getFetchUrl 을 포함 새로운 type 을 구현하는 클래스 생성 
  3. 구현 클래스를 사용하여 스토어 생성

import { RoleModel } from "@/types/studio/RoleModel";
import {
API_HEADERS,
authHeader
} from "@/util/helpers";
import axios from "axios";
import { defineStore } from "pinia";

import type {
PageableDataSource
} from "@/types/studio/index";
import { AbstractPageDataSource } from "../AbstractPageDataSource";

// getById,getByName,loadById,saveOrUpdate 추가을 추가하여 IPageableRoleDataSource를 정의
type IPageableRoleDataSource = PageableDataSource & {
getById: (roleId: number) => RoleModel;
getByName: (name: string) => RoleModel;
loadById: (roleId: number) => Promise<RoleModel>;
saveOrUpdate: (item: RoleModel) => Promise<void>;
};
const baseUrl = `${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/roles:find`;

class PageableRoleDataSource
extends AbstractPageDataSource
implements IPageableRoleDataSource
{
getById(roleId: number) {
return this.dataItems.value.find(
(item: RoleModel) => item.roleId === roleId
);
}

getByName(name: string) {
return this.dataItems.value.find((item: RoleModel) => item.name === name);
}

async loadById(roleId: number) {
const headers = { ...API_HEADERS, ...authHeader() };
const category = await axios
.get(
`${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/roles/${roleId}`,
{ headers: headers }
)
.then((response) => {
const data = response.data;
return data;
})
.catch((err) => {
this.alertStore.error(err);
});

return category;
}

async saveOrUpdate(item: RoleModel) {
const headers = { ...API_HEADERS, ...authHeader() };
await axios
.post(
`${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/roles/${item.roleId}`,
JSON.stringify(item),
{ headers: headers }
)
.then((response) => {
const dataItems = response.data;
})
.catch((err) => {
this.alertStore.error(err);
});
}

// API 엔드포인트 URL을 제공. 이 클래스만 구현하면 그리드는 정상 동작한다.
getFetchUrl(): string {
return baseUrl;
}
}

// 필수로 표기된 부분은 반드시 동일하게 리턴해야 한다.
export const usePageableRolesStore = defineStore("mgmt-pageable-roles-store", () => {
const dataSource = new PageableRoleDataSource();
return {
isLoaded: dataSource.isLoaded, // 필수
dataItems: dataSource.dataItems, // 필수
total: dataSource.total, // 필수
pageSize: dataSource.pageSize,// 필수
getById: dataSource.getById.bind(dataSource),
getByName: dataSource.getByName.bind(dataSource),
loadById: dataSource.loadById.bind(dataSource),
saveOrUpdate: dataSource.saveOrUpdate.bind(dataSource),
setPage: dataSource.setPage.bind(dataSource),// 필수
setSort: dataSource.setSort.bind(dataSource),// 필수
setFilter: dataSource.setFilter.bind(dataSource),// 필수
fetch: dataSource.fetch.bind(dataSource), // 필수
};
});

예제 프로그램 실행 화면은 아래와 같다.


댓글 없음:

댓글 쓰기