2024년 6월 26일

코딩 - Vue3 : AG-Grid 사용하기

 ◼︎ 환경

  • Model : MacBook Pro (14-inch, 2021)
  • CPU : Apple M1 Pro
  • MENORY : 16GB
  • DISK : 512 GB SSD
  • OS : macOS 13.2.4 (22F66)
  • TOOLS : Visual Studio Code, Java 11, Gradle, Docker
  • Version Control : GitHub
  • Programming Language : Java, Vue3
  • Front-End Framework : Vue 3.4.30, Vuetify 3.6.10, ag-grid-vue3 31.3.2
  • Back-End Framework : Spring Boot 2.7.12, Spring Security 5.7.7 
  • DBMS : MySql 8.0.33
  • Cloud : OCI (free tier account)

Kendo UI 을 이용했지만 1년 단위 개발자 라이선스가 부담이 되어 Vue 3에서 사용할 수 있는 그리드들 중에서 AG Grid Community 무료 버전을 사용해보기로 했다.

AG Grid Community는 필터링, 정렬, 그룹핑 등 다양한 기능을 제공하며 대규모 데이터 처리에 특히 유용하다. 추가로 무료로 제공되기 때문에 많은 프로젝트에서 널리 사용된다고 한다. 



1. 설치하기

설치는 간단하게 아래 명령을 사용하여 패키지를 설치할 수 있다. 사용하는 것 역시 별도의 설정이 없어도 바로 사용할 수 있다. 

npm install --save ag-grid-vue3 

가장 힘들었던 부분이 특정 컬럼(셀)에 사용자 정의 렌더러을 지정하여 버튼들을 그리고 해당 버튼을 클릭하면 해당 이벤트에 따라 저장/삭제 동작을 구현하는 것이었다.


Actions 에 해당 하는 셀은 ActionCellRenderer.vue 만들어 구현을 했다. 문제는 row 데이터에 따라 버튼을 제어( 노출/숨김) 하고 클릭시 이벤트를 받아 서버 통신을 통해서 삭제/저장을 구현하는 화면을 구현하려고 했다. 

1) defineProps 함수를 사용하여 전달된 params 값을 바인딩 하려고 했으나 이부분이 해결되지 않아 params 을 직접 접근하는 방식으로 코드를 작생했다. 

const props = defineProps<{
data?: { [key: string]: any };
api?: any;
node?: any;
params: {
data: { [key: string]: any };
api: any;
node: any;
}
}>();

2) 버튼을 클릭하면 defineEmits 사용하여 정의한 이벤트를 발생하는 것으로 코드를 만들었지만 부모 즉 ag-grid 구현 화면에 해당 이벤트를 전달하는 방법을 찾지 못해 prams.api 로 전달되는 GridApi.dispatchEvent() 함수를 사용했다.  (-> 이부분을 알아내는 것이 가장 힘들었던 것 같다)

const saveRow = () => {
props.params.api.dispatchEvent({
type: 'actionCellRenderer:save',
data: props.params.data
});
};
const deleteRow = () => {
emits('delete', props.params.data);
props.params.api.dispatchEvent({
type: 'actionCellRenderer:delete',
data: props.params.data
});
};

GridApi.dispatchEvent() 함수를 사용하여 전달된 이벤트를 ga-grid 화면을 그리는 페이지에서 받기 위해서는 GridApi.addEventListener() 함수를 사용하여 리스트너를 등록해줘야 한다.  @grid-ready="onGridReady" 구문을 사용해서 grid 가 준비됨을 알리는 이벤트가 발생하면 onGridReady 를 실행하고 해당 코드에서 리스트너를 등록했다. 

const onGridReady = (params: any) => {
gridApi.value = params.api;
gridApi.value.addEventListener('actionCellRenderer:save', handleSave);
gridApi.value.addEventListener('actionCellRenderer:delete', handleDelete);
};


아래는 관련된 소스이다.

<template>
<div>
<v-btn v-if="isEdit || isNew" variant="tonal" size="small" color="primary" @click="saveRow" prepend-icon="mdi-content-save" style="margin-right: 2px;">Save</v-btn>
<v-btn v-if="!isNew" variant="tonal" size="small" color="red" @click="deleteRow" prepend-icon="mdi-close">Delete</v-btn>
</div>
</template>
<script setup lang="ts">
import { defineProps, computed } from 'vue';
const props = defineProps<{
data?: { [key: string]: any };
api?: any;
node?: any;
params: {
data: { [key: string]: any };
api: any;
node: any;
}
}>();
const isEdit = computed(() => {
if (props.params && props.params.data && props.params.data.isEdit) {
return props.params.data.isEdit;
}
return false;
});
const isNew = computed(() => {
if (props.params && props.params.data && props.params.data.isNew) {
return props.params.data.isNew;
}
return false;
});
const emits = defineEmits(['save', 'delete']);
const saveRow = () => {
props.params.api.dispatchEvent({
type: 'actionCellRenderer:save',
data: props.params.data
});
};
const deleteRow = () => {
emits('delete', props.params.data);
props.params.api.dispatchEvent({
type: 'actionCellRenderer:delete',
data: props.params.data
});
};
</script>
<template>
<v-container :fluid="true">
<v-breadcrumbs :items="['Setting', 'Properties']" density="compact"></v-breadcrumbs>
<PageToolbar title="Application Properties" @refresh="refresh" @create="addRow()" :divider="false"
:items="[{ icon: 'mdi-plus', event: 'create' }, { icon: 'mdi-refresh', event: 'refresh', }]">
</PageToolbar>
<v-row>
<v-col cols="12" md="12">
<div class="ag-theme-quartz" style="min-height:600px;max-height:100%;">
<AgGridVue class="ag-theme-quartz" :columnDefs="columnDefs" :rowData="gridData"
style="height:600px;"
:gridOptions="gridOptions"
:defaultColDef="defaultColDef"
:frameworkComponents="frameworkComponents"
@grid-ready="onGridReady"
@actionCellRenderer:save="handleSave"
@actionCellRenderer:delete="handleDelete"
@cellEditingStarted="onCellEditingStarted"
@cellEditingStopped="onCellEditingStopped"
@cellValueChanged="onCellValueChanged">
</AgGridVue>
</div>
</v-col>
</v-row>
<Alert></Alert>
</v-container>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
// define grid componet : ag-gird
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
import { AgGridVue } from "ag-grid-vue3"; // Vue Data Grid Component
import type { GridApi } from 'ag-grid-community';
import type { GridOptions, ColDef } from 'ag-grid-community';
import ActionCellRenderer from '@/components/ag-grid/ActionCellRenderer.vue';
import Alert from '@/components/Alert.vue';
import { useApplicationPropertiesStore } from '@/store/studio/mgmt/mgmt.settting.properties.store';
import PageToolbar from '@/components/PageToolbar.vue';
import { PropertyModel } from '@/types/models/PropertyModel';
// grid data setting
const applicationProperties = useApplicationPropertiesStore();
const frameworkComponents = ref({
ActionCellRenderer,
});
const gridOptions : GridOptions = {};
const columnDefs : ColDef[]= [
{ field: 'name', headerName: 'Name', flex: 1, editable: (params: any) => isEditable(params.data) },
{ field: 'value', headerName: 'Value', flex: 2, editable: true },
{
headerName: 'Actions',
field: 'actions',
editable: false,
filter: false,
sortable: false,
cellRenderer: ActionCellRenderer,
},
]
const defaultColDef = ref({
sortable: true,
filter: true,
resizable: true,
});
const gridApi = ref<GridApi|null>();
const onGridReady = (params: any) => {
gridApi.value = params.api;
gridApi.value.addEventListener('actionCellRenderer:save', handleSave);
gridApi.value.addEventListener('actionCellRenderer:delete', handleDelete);
};
const onCellEditingStarted = (event: any) => {
//event.data.isEdit = true;
console.log( "onCellEditingStarted" , event)
};
const onCellEditingStopped = (event: any) => {
const data = event.data;
console.log( "onCellEditingStopped" , data)
if (!data.name && data.name === null) {
gridApi.value.applyTransaction({ remove: [data] });
}
};
const onCellValueChanged = (event: any) => {
console.log( "onCellValueChanged" , event)
if(event.oldValue != event.newValue){
event.data.isEdit = true;
gridApi.value.refreshCells({ force: true });
}
};
const handleSave = async(event: any) => {
console.log("handleSave", event);
gridApi.value.showLoadingOverlay();
await applicationProperties.saveOrUpdate(event.data);
gridApi.value.hideOverlay();
gridApi.value.refreshCells({ force: true });
};
const handleDelete = async(event: any) => {
console.log("handleDelete", event);
gridApi.value.showLoadingOverlay();
await applicationProperties.remove(event.data);
gridApi.value.hideOverlay();
gridApi.value.applyTransaction({ remove: [event.data] });
};
const total = ref(0);
const gridData = ref([]);
const updatedData = ref([]);
const loader = ref(false);
const sort = ref([{ field: 'name', dir: 'asc' }]);
const filter = ref([]);
/**
* 생성시에는 Name 입력이 가능하나 이미 저장된 경우에는 수정이 불가능하며 이를 검사하어 UI 을 제어하기 위한 상태를 반환합니다.
*/
function isEditable( data : any ){
if( data.hasOwnProperty('isNew') && data.isNew === true )
return true;
return false;
}
async function addRow() {
const newItem = { name: null, value: null , isEdit: false, isNew: true };
const newRow = gridApi.value.applyTransaction({ add: [newItem] });
if (newRow.add) {
const newRowIndex = newRow.add[0].rowIndex;
gridApi.value.startEditingCell({
rowIndex: newRowIndex,
colKey: 'name',
});
}
}
async function refresh() {
getData(true);
}
async function getData(force: boolean = false) {
loader.value = true;
if (gridApi.value) {
gridApi.value.showLoadingOverlay();
}
if (!applicationProperties.isLoaded||force){
await applicationProperties.fetch();
}
if( applicationProperties.isLoaded){
updatedData.value = [...applicationProperties.properties];
gridData.value = [...applicationProperties.properties];
total.value = gridData.value.length;
}
if( gridApi.value )
gridApi.value.hideOverlay();
if (total.value === 0)
gridApi.value.showNoRowsOverlay();
loader.value = false;
}
onMounted(async () => {
getData();
})
</script>

참고자료


댓글 없음:

댓글 쓰기