◼︎ 환경
- 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)
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);
};
아래는 관련된 소스이다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
댓글 없음:
댓글 쓰기