Javascript pagination 기능 구현
개발환경 : SpringBoot, Thymeleaf, JQuery, javascript, DataTables
JQuery 의 Datatables 를 사용하여 화면 구현 중 DataTables 옵션에 있는 페이징 기능을 사용하지 않고
직접 페이징 기능 구현이 필요하여 개발을 진행했다.
필요한 pagination 조건
- 한 그룹의 선택할 수 있는 페이지 수는 10개이다.
- 한 페이지의 게시물 개수는 select box 를 사용하여 보여준다.(select box option : 10개, 100개, 1000개, 10000개)
- 페이지 버튼 이외에 이전, 다음, 맨 앞쪽이동, 맨 뒤쪽이동 버튼이 존재한다.
Pagination 기능 구현
일단 페이징 기능을 구현하기 위해선 총 4가지의 값이 필요하다.
- 현재 페이지의 페이지 그룹
- 화면에 보여질 첫번째 페이지
- 화면에 보여질 마지막 페이지
- 데이터의 총 개수에 따른 총 페이지 수
총 페이지 수
- 총 페이지 수 = (데이터 총 개수 / 한 페이지에 보여질 목록 개수)
총 페이지 수는 데이터의 총 개수를 가지고 한 페이지에 보여질 목록 개수로 나누면 총 페이지 수가 나온다.
예시)
총 데이터 수 = 492
한 페이지에 보여질 목록 개수 = 20
// 총 데이터 수
const totalCount = 492;
// 한 페이지에 보여질 목록 개수
const selectCount = 20;
// 총 페이지 수
const totalPage = Math.ceil(totalCount / selectCount);
Math 의 ceil 함수는 소수점 자리를 올림해주는 함수이다.
=> 492 / 20 = 24.6
=> 24.6 를 올림 하게 되면 총 페이지 수는 25페이지가 나온다.
현재 페이지 기준 페이지 그룹
- 현재 페이지 기준 페이지 그룹 = (현재 페이지 / 화면에 보여질 페이지 그룹 수)
현재 페이지 기준 페이지 그룹은 선택된 현재 페이지를 기준으로 보여질 페이지 그룹 수를 나누면 페이지 그룹이 나온다.
예시)
현재 페이지 = 1 (선택된 페이지)
보여질 페이지 그룹 수 = 10 (아래 이미지 기준 1~10까지의 페이지 개수가 존재)
// 현재 페이지
const currentPage = 1;
// 현재 페이지 기준 페이지 그룹
const pageGroup = Math.ceil(currentPage / 10);
=> 1 / 10 = 0.1
=> 0.1 올림하면 페이지그룹은 1이 된다. 1그룹은 1~10 페이지가 존재한다.
예를 들어 선택된 페이지가 12 라면 12 / 10 = 1.2 가 나오고 올림 시 현재 페이지 기준 페이지 그룹은 2이다.
페이지 그룹 2는 11 ~ 20 까지이다.
화면에 보여질 마지막 페이지
- 화면에 보여질 마지막 페이지 = 화면에 보여질 페이지 그룹 * 페이지 그룹 수
화면에 보여질 마지막 페이지는 현재 페이지 기준 페이지 그룹 * 화면에 보여질 페이지 그룹 수를 하면 나온다.
현재 페이지 기준 페이지 그룹은 위에서 구했으며
화면에 보여질 페이지 그룹 수는 페이지 그룹마다 총 10개씩이므로 10이다.
예를 들어 페이지 그룹 수가 5 라고 한다면 1 ~ 5개가 된다.
//현재 페이지
let currentPage = 1;
// 현재 페이지 기준 페이지 그룹
let pageGroup = Math.ceil(currentPage / 10);
// 화면에 보여질 마지막 페이지
let last = pageGroup * 10;
현재 선택된 페이지가 1 이고 화면에 보여질 페이지 그룹 수(1 ~ 10페이지)가 10이면
현재 페이지 기준 페이지 그룹은 > 1 / 10 = 0.1 올림하면 페이지 그룹 수는 1이다 (1 ~ 10 페이지)
화면에 보여질 마지막 페이지는 현재 페이지 기준 페이지 그룹이 1 이고 페이지 그룹 수가 10이므로 아래와 같다.
=> 1 * 10
=> 화면에 보여질 마지막 페이지는 10 페이지이다.
만약 현재 페이지가 1이고 화면에 보여질 페이지 그룹 수가 5 (1 ~ 5페이지) 라면
=> 1(현재 페이지 기준 페이지 그룹) * 5
화면에 보여질 첫번째 페이지
- 화면에 보여질 첫번째 페이지 = 화면에 보여질 마지막 페이지 - ( 화면에 보여질 페이지 그룹 수 - 1 )
화면에 보여질 페이지 그룹수 - 1 한 숫자를 화면에 보여질 마지막 페이지 값에 빼면 첫번째 페이지 수이다.
예시)
현재페이지 = 1
화면에 보여질 페이지 그룹 수(1 ~ 10페이지) = 10
화면에 보여질 마지막 페이지 = 10
=> 10 - (10 - 1) = 1 페이지
=> 화면에 보여질 첫번쨰 페이지는 1이다.
만약 현재 페이지가 12라고 한다면 페이지 그룹은 2, 화면에 보여질 마지막 페이지는 20 이다.
=> 20 - (10 - 1) = 11
=> 화면에 보여질 첫번째 페이지는 11이다.
//현재 페이지
let currentPage = 1;
// 현재 페이지 기준 페이지 그룹
let pageGroup = Math.ceil(currentPage / 10);
// 화면에 보여질 마지막 페이지
let last = pageGroup * 10;
// 화면에 보여질 첫번째 페이지
let first = last - (pageGroup * 10);
이전, 다음, 가장 앞쪽 이동 버튼, 맨 뒤쪽 버튼
- 이전 버튼(이전 페이지 그룹 이동) : 화면에 보여질 첫번째 페이지 - 1
- 다음 버튼(다음 페이지 그룹 이동) : 화면에 보여질 마지막 페이지 + 1
- 가장 앞쪽 이동 버튼 : 1
- 맨 뒤쪽 이동 버튼 : 총 페이지 수
// 다음 버튼
let next = last + 1;
let prev = first - 1;
let allnext = 25(총 페이지 수)
let allprev = 1 (가장 첫번째 선택 페이지)
현재 선택 페이지 = 1
화면에 보여질 첫번째 페이지 = 1
화면에 보여질 마지막 페이지 = 10
현재 페이지 기준 페이지 그룹 = 1 그룹
총 페이지 수 = 25
이제 페이징에서 필요한 4가지의 값을 모두 구했으며, 화면을 그린다.
Javascript
function deotisPagination(desc, totalCount, currentPage, selectCount){
let htmlStr = '';
const bootstrap_version = '4';
if(totalCount <= 10) { return; }
// 전체 개수에서 페이지에서 보여질 개수로 나누어 총 페이지를 계산
const totalPage = Math.ceil(totalCount / selectCount);
// 10개씩 보여질 페이지 그룹
const pageGroup = Math.ceil(currentPage / 10);
let last = pageGroup * 10;
if(last > totalPage) last = totalPage;
let first = last - (10 - 1) <= 0 ? 1 : last - (10 - 1);
let next = last + 1;
let prev = first - 1;
htmlStr += `<nav>`;
htmlStr += `<ul class="pagination justify-content-center">`;
htmlStr += prev > 0 ? "<li class=\"page-item\">" : "<li class=\"page-item disabled\">";
htmlStr += `<a class="page-link" href="#" id="allprev" data-move="allprev" tabindex="-1"><<</a>`;
htmlStr += "</li>";
htmlStr += prev > 0 ? "<li class=\"page-item\">" : "<li class=\"page-item disabled\">";
htmlStr += `<a class="page-link" href="#" id="prev" data-move="prev" tabindex="-1"><</a>`;
htmlStr += "</li>";
for (var i = first; i <= last; i++) {
htmlStr += `<li id='pageLi-${i}' class="page-item">`;
htmlStr += `<a class="page-link" href='#' id='page-${i}' data-move="no" data-num='${i}'>${i}</a>`;
htmlStr += "</li>";
}
htmlStr += last < totalPage ? "<li class=\"page-item\">" : "<li class=\"page-item disabled\">";
htmlStr += `<a class="page-link" href="#" id="next" data-move="next" tabindex="-1">></a>`;
htmlStr += "</li>";
htmlStr += last < totalPage ? "<li class=\"page-item\">" : "<li class=\"page-item disabled\">";
htmlStr += `<a class="page-link" href="#" id="allnext" data-move="allnext" tabindex="-1">>></a>`;
htmlStr += "</li>";
htmlStr += "</ul>";
htmlStr += `</nav>`;
$("#"+desc.page_id).html(htmlStr);
if(bootstrap_version == '4'){
$('#'+desc.page_id+' ul li').removeClass('active');
$('#'+desc.page_id+' ul li#pageLi-'+currentPage).addClass('active');
}else if(bootstrap_version == '5'){
$('#'+desc.page_id+' ul li a').removeClass('active');
$('#'+desc.page_id+' ul li a#page-'+currentPage).addClass('active');
}
let pid = "#"+desc.page_id;
$(pid+" a").on('click', function (e) {
const page = $(this).data().num;
const pageSize = $("#"+desc.select_id).val();
if($(this).data().move == 'no'){
paginationClickEvent(desc, page, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
}else {
let moveValue = $(this).data().move;
switch(moveValue){
case "allprev":
if(prev > 0){
paginationClickEvent(desc, 1, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
}
break;
case "prev":
if(prev > 0){
paginationClickEvent(desc, prev, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
}
break;
case "next":
if(last < totalPage){
paginationClickEvent(desc, next, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
}
break;
case "allnext":
if(last < totalPage){
paginationClickEvent(desc, totalPage, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
}
break;
}
}
});
$("#"+desc.select_id).change(function () {
const pageSize = $("#"+desc.select_id).val();
paginationClickEvent(desc, 1, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
});
}
const paginationClickEvent = function(desc, page, pageSize, ajaxUrl, totalCount){
let ajaxStr = '';
const startDateArr = desc.str_search3.split(':');
const startH = startDateArr[0];
const startM = startDateArr[1];
const startS = startDateArr[2];
const endDateArr = desc.str_search4.split(':');
const endH = endDateArr[0];
const endM = endDateArr[1];
const endS = endDateArr[2];
ajaxStr += 'dateStart=' + desc.str_search1
+ '&' + 'dateEnd=' + desc.str_search2
+ '&' + 'startH=' + startH
+ '&' + 'startM=' + startM
+ '&' + 'startS=' + startS
+ '&' + 'endH=' + endH
+ '&' + 'endM=' + endM
+ '&' + 'endS=' + endS;
if(desc.str_search5 != null && desc.str_search5 != ""){
ajaxStr += '&' + 'ani=' + desc.str_search5
}
if(desc.str_search6 != null && desc.str_search6 != ""){
ajaxStr += '&' + 'usid=' + desc.str_search6
}
if(desc.list_search1.length > 0){
desc.list_search1.forEach((data, index) => {
ajaxStr += '&' + 'holiday=' + data;
});
}
if(desc.list_search2.length > 0){
desc.list_search2.forEach((data, index) => {
ajaxStr += '&' + 'week=' + data;
});
}
if(desc.list_search3.length > 0){
desc.list_search3.forEach((data, index) => {
ajaxStr += '&' + 'dnis=' + data;
});
}
ajaxStr += '&' + 'page=' + page
+ '&' + 'pageSize=' + pageSize;
$.ajax({
url : ajaxUrl,
type: "POST",
data: ajaxStr,
dataType: "html",
cache: false,
success: function(data) {
let table = $('#'+desc.table_id).DataTable();
table.clear();
table.rows.add(JSON.parse(data)).draw();
deotisPagination(desc, totalCount, page, pageSize);
}
});
}
HTML
<div th:id="${desc.page_id}"></div>
deotisPagination 함수
실제적으로 페이지를 그리는 함수
let pid = "#"+desc.page_id;
$(pid+" a").on('click', function (e) {}
desc.page_id 는 id 값 구분을 위해 Controller 에서 랜덤값으로 던졌으며 그냥 id 값이라고 생각하면 된다.
이렇게 한 이유는 프로젝트 화면 자체가 탭 화면으로 여러개를 불러오는 구조라 구분을 위해 랜덤 id 값을 사용했다.
별 신경 안써도 된다.
$(div id 값).on('click') 이벤트를 통해 페이지 번호를 클릭 했을 때 처리하기 위한 함수이다.
$("#"+desc.page_id).html(htmlStr);
htmlStr 변수에 넣은 html 코드 값을 화면에 그리기
if(bootstrap_version == '4'){
$('#'+desc.page_id+' ul li').removeClass('active');
$('#'+desc.page_id+' ul li#pageLi-'+currentPage).addClass('active');
}else if(bootstrap_version == '5'){
$('#'+desc.page_id+' ul li a').removeClass('active');
$('#'+desc.page_id+' ul li a#page-'+currentPage).addClass('active');
}
bootstrap4 와 5 기준으로 active 가 달라 구분한것이다.
별 다를점은 없고 active 가 들어가는 위치만 다르다.
// 페이지네이션에서 페이지 버튼을 클릭했을 때 처리
paginationClickEvent(desc, page, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
// 가장 맨 앞쪽 페이지 이동 버튼
paginationClickEvent(desc, 1, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
// 맨 뒤쪽 페이지 이동 버튼
paginationClickEvent(desc, totalPage, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
// 다음 페이지 그룹 이동 버튼
paginationClickEvent(desc, next, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
// 이전 페이지 그룹 이동 버튼
paginationClickEvent(desc, prev, pageSize, contextPath+'/reporter/callList/ajax/list/data', totalCount);
위 함수 중 중요한 파라미터는 2번째 page(선택된 페이지 버튼), 3번째 pageSize(화면에 보여질 목록 개수),
totalCount(총 데이터 개수) 이다.
맨 앞쪽 페이지로 이동하기 위해서는 선택된 페이지 값을 1로 줬다.
맨 뒤쪽 페이지로 이동하기 위해서는 총 페이지 수를 2번째 파라미터로 줬다.
paginationClickEvent 함수
페이지 버튼을 클릭 했을 때 이벤트 처리를 위한 함수
$.ajax({
url : ajaxUrl,
type: "POST",
data: ajaxStr,
dataType: "html",
cache: false,
success: function(data) {
let table = $('#'+desc.table_id).DataTable();
table.clear();
table.rows.add(JSON.parse(data)).draw();
deotisPagination(desc, totalCount, page, pageSize);
}
});
본인은 DataTables 를 사용하여 테이블을 그렸으며, 페이지 버튼을 따로 구성했기 때문에 ajax 를 날렸다.
페이지 버튼을 클릭 했을 때 ajax 요청을 보내 데이터를 불러오고
DataTables 내용을 삭제하는 .clear(), 데이터를 다시 넣기 위한 rows.add(JSON 데이터).draw() 를 통해
다시 그렸다.
이후에 pagination 함수를 호출해 데이터를 넘겨 그린다.