paginationInfoVO

package kr.or.ddit.vo;

import java.util.List;

import lombok.Data;

@Data
public class PaginationInfoVO<T> {
	private int totalRecord;		// 총 게시글 수
	private int totalPage;			// 총 페이지 수
	private int currentPage;		// 현재 페이지
	private int screenSize = 10;			// 페이지 당 게시글 수
	private int blockSize = 5;			// 페이지 블록 수
	private int startRow;			// 시작row
	private int endRow;				// 끝 row
	private int startPage;			// 시작 페이지
	private int endPage;			// 끝 페이지
	private List<T> dataList ;		// 결과를 넣을 데이터 리스트
	private String searchType ;		// 검색 타입(제목, 내용, 작성자, 작성일 등등)
	private String searchWord ;		// 검색 단어(키워드)
	
	public PaginationInfoVO() {}

	// PaginationInfoVO 객체를 만들 때, 한 페이지당 게시글 수와 페이지 블록 수를
	// 원하는 값으로 초기화 할 수 있도록 Custom 생성자 생성
	public PaginationInfoVO(int screenSize, int blockSize) {
		this.screenSize = screenSize;
		this.blockSize = blockSize;
	}

	public void setTotalRecord(int totalRecord) {
		this.totalRecord = totalRecord;
		// 총 페이지 수 구하기
		// ceil 올림
		totalPage = (int)Math.ceil(totalRecord / (double)screenSize);
	}

	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;				// 현재 페이지
		endRow = currentPage * screenSize; 			// 끝 row = 현재 페이지 * 한 페이지당 게시글 수
		startRow = endRow - (screenSize - 1 ); 		// 시작 row = 끝 row - (한 페이지당 게시글 수 - 1)
		// 마지막 페이지 = (현재 페이지 + (페이지 블록 사이즈 - 1)) / 페이지 블록 사이즈 * 페이지 블록 사이즈
		endPage = (currentPage + (blockSize - 1)) / blockSize * blockSize;
		startPage = endPage - (blockSize - 1); 		// 시작 페이지 = 마지막 페이지 - (페이지 블록 사이즈 - 1)
	}

	public String getPagingHTML() {
		StringBuffer html = new StringBuffer();
		
		// 부트스트랩을 이용하면 여러 css요소를 이용해서 페이징 UI를 구현하는 곳입니다!
		html.append("<ul class=\\"pagination justify-content-center\\">");
		
		if(startPage > 1) {
			html.append("<li class='page-item'><a href='' class='page-link' data-page='"
					+(startPage - blockSize)+"'><span class=\\"material-icons\\">keyboard_arrow_left</span></a></li>");
		}
		
		for(int i = startPage; i <= (endPage < totalPage ? endPage : totalPage); i++) {
			if(i == currentPage) {
				html.append("<li class='page-item active'><span class='page-link'>"
						+ i + "</span></li>");
			}else {
				html.append("<li class='page-item'><a href='' class='page-link' data-page='"
						+ i + "'>" + i + "</a></li>");
			}
		}
		
		if(endPage < totalPage) {
			html.append("<li class='page-item'><a href='' class='page-link' data-page='"
						+ (endPage + 1) + "'><span class=\\"material-icons\\">keyboard_arrow_right</span></a></li>");
		}
		
		html.append("</ul>");
		return html.toString();
	}
	
	
}

Controller

package kr.or.ddit.controller.noticeboard.web;

import java.util.List;

import javax.inject.Inject;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.or.ddit.controller.noticeboard.service.INoticeService;
import kr.or.ddit.vo.NoticeVO;
import kr.or.ddit.vo.PaginationInfoVO;
import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping("/notice")
@Slf4j
public class NoticeRetrieveController {
	
	@Inject
	private INoticeService noticeService;

**// 페이징 및 검색 리스트**
	@RequestMapping(value="/list.do")
	public String noticeList(
			@RequestParam(name="page", required = false, defaultValue = "1") int currentPage,
			@RequestParam(required = false, defaultValue = "title") String searchType,
			@RequestParam(required = false) String searchWord,
			Model model
			) {
		log.info("noticeList() 실행...!");
		
		PaginationInfoVO<NoticeVO> pagingVO = new PaginationInfoVO<NoticeVO>();
		
		//검색 기능 추가 시 활용
		if(StringUtils.isNotBlank(searchWord)) {   //검색 됨
			pagingVO.setSearchType(searchType);
			pagingVO.setSearchWord(searchWord);
			model.addAttribute("searchType", searchType);
			model.addAttribute("searchWord", searchWord);
		}
		
		pagingVO.setCurrentPage(currentPage);	// startRow, endRow, startPage, endPage 가 결정
		int totalRecord = noticeService.selectNoticeCount(pagingVO); // 총 게시글 수
		pagingVO.setTotalRecord(totalRecord);
		
		List<NoticeVO> dataList = noticeService.selectNoticeList(pagingVO);
		pagingVO.setDataList(dataList);
		
		model.addAttribute("pagingVO", pagingVO);
		
		// 해당경로는 tiles 에서 설정한 경로 이름 
		return "notice/list";
	}
	
	
	@RequestMapping(value="/detail.do")
	public String noticeDetail(int boNo, Model model) {
		NoticeVO noticeVO = noticeService.selectNotice(boNo);
		model.addAttribute("notice", noticeVO);
		return "notice/detail";
	}
}

Service

package kr.or.ddit.controller.noticeboard.service;

import java.util.List;

import javax.inject.Inject;

import org.apache.commons.io.input.TaggedInputStream;
import org.springframework.stereotype.Service;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.controller.noticeboard.web.TelegramSendController;
import kr.or.ddit.mapper.NoticeMapper;
import kr.or.ddit.vo.NoticeVO;
import kr.or.ddit.vo.PaginationInfoVO;

@Service
public class NoticeServiceImpl implements INoticeService {
	
	@Inject
	private NoticeMapper noticeMapper;
	
	TelegramSendController tst = new TelegramSendController();
	
	@Override
	public ServiceResult insertNotice(NoticeVO noticeVO) {
		ServiceResult result = null;
		int status = noticeMapper.insertNotice(noticeVO);
		if(status > 0) {
			// 성공 했다는 메시지를 텔레그램 BOT API를 이용하여 알림!
			try {
				tst.sendGet("조성희", noticeVO.getBoTitle());
			} catch (Exception e) {
				e.printStackTrace();
			}
			result = ServiceResult.OK;
		}else {
			result = ServiceResult.FAILED;
		}
		return result;
	}

	@Override
	public NoticeVO selectNotice(int boNo) {
		noticeMapper.incrementHit(boNo);  //조회수 증가
		return noticeMapper.selectNotice(boNo);
	}

	@Override
	public ServiceResult updateNotice(NoticeVO noticeVO) {
		ServiceResult result = null;
		int status = noticeMapper.updateNotice(noticeVO);
		if(status > 0) { // 성공
			result = ServiceResult.OK;
		}else {
			result = ServiceResult.FAILED;
		}
		return result;
	}

	@Override
	public ServiceResult deleteNotice(int boNo) {
		ServiceResult result = null;
		int status = noticeMapper.deleteNotice(boNo);
		if(status > 0) { // 성공
			result = ServiceResult.OK;
		}else {
			result = ServiceResult.FAILED;
		}
		return result;
	}
**// 게시글 개수 세는 서비스**
	@Override
	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO) {
		return noticeMapper.selectNoticeCount(pagingVO);
	}
**// 페이징, 검색 서비스**
	@Override
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO) {
		return noticeMapper.selectNoticeList(pagingVO);
	}

}

Mapper

package kr.or.ddit.mapper;

import java.util.List;

import kr.or.ddit.vo.NoticeVO;
import kr.or.ddit.vo.PaginationInfoVO;

public interface NoticeMapper {
	public int insertNotice(NoticeVO noticeVO);
	public void incrementHit(int boNo);
	public NoticeVO selectNotice(int boNo);
	public int updateNotice(NoticeVO noticeVO);
	public int deleteNotice(int boNo);
	public int selectNoticeCount(PaginationInfoVO<NoticeVO> pagingVO);
	public List<NoticeVO> selectNoticeList(PaginationInfoVO<NoticeVO> pagingVO);
}

NoticeMapper (SQL)

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "<https://mybatis.org/dtd/mybatis-3-mapper.dtd>">
 <mapper namespace="kr.or.ddit.mapper.NoticeMapper">
 	
<!-- 검색을위한 동적 쿼리 -->
<sql id="noticeSearch">
 		<if test="searchType != null and searchType == 'title'">
 			and (bo_title like '%'||#{searchWord}||'%')
 		</if>
 		<if test="searchType != null and searchType == 'writer'">
 			and (bo_writer like '%'||#{searchWord}||'%')
 		</if>
 	</sql>

 	<!-- 공지사항 등록하는 쿼리 -->
 	<insert id="insertNotice" parameterType="noticeVO" useGeneratedKeys="true">
 		<selectKey keyProperty="boNo" resultType="int" order="BEFORE">
 			select seq_notice.nextval from dual
 		</selectKey>
 		insert into notice(
 			bo_no, bo_title, bo_content, bo_writer, bo_date
 		)values(
 			#{boNo}, #{boTitle}, #{boContent}, #{boWriter}, sysdate
 		)
 	</insert>
 	
 	<!-- 조회수 증가 쿼리 -->
 	<update id="incrementHit" parameterType="int">
 		update notice
 		set
 			bo_hit = bo_hit + 1
 		where bo_no = #{boNo}
 	</update>
 	
 	<!-- 상세보기 쿼리 -->
 	<select id="selectNotice" parameterType="int" resultType="noticeVO">
 		select
 			bo_no, bo_title, bo_content, bo_writer,
 			to_char(bo_date, 'yy-mm-dd hh24:mi:ss') bo_date, bo_hit
 		from notice
 		where bo_no = #{boNo}
 	</select>
 	
 	<!-- 수정하기 쿼리 -->
 	<update id="updateNotice" parameterType="noticeVO">
 		update notice
 		set
 			bo_title = #{boTitle},
 			bo_content = #{boContent},
 			bo_date = sysdate
 		where bo_no = #{boNo}
 	</update>
 	
 	<delete id="deleteNotice" parameterType="int">
		delete from notice
		where bo_no = #{boNo} 	
 	</delete>
 	
**<!-- 게시글 개수 세는 쿼리 -->**
 	<select id="selectNoticeCount" parameterType="pagingVO" resultType="int">
 		select count(bo_no)
 		from notice
 		where 1=1
		<include refid="noticeSearch"/>
 	</select>
 	
**<!-- 검색 및 페이징 위한 쿼리-->**
 	<select id="selectNoticeList" parameterType="pagingVO" resultType="noticeVO">
	 	select
	 		b.*
	 	from(
	 		select
	 			a.*, row_number() over (order by a.bo_no desc) rnum
	 		from(
		 		select
		 			bo_no, bo_title, bo_content, bo_writer,
		 			to_char(bo_date, 'yy-mm-dd hh24:mi:ss') bo_date, bo_hit
		 		from notice
		 		where 1=1
		 		<include refid="noticeSearch"/>
		 		order by bo_no desc
		 	)a
		 ) b
		 <![CDATA[
		 	where b.rnum >= #{startRow} and b.rnum <= #{endRow}
		 ]]>
 	</select>
 </mapper>

JSP 페이지

<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="<http://tiles.apache.org/tags-tiles>" prefix="tiles" %>
<%@ taglib uri="<http://java.sun.com/jsp/jstl/core>" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AdminLTE 3 | Simple Tables</title>

<link rel="stylesheet" href="<https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback>">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/dist/css/adminlte.min.css">
<script src="${pageContext.request.contextPath }/resources/plugins/jquery/jquery.min.js"></script>
<script src="${pageContext.request.contextPath }/resources/ckeditor/ckeditor.js"></script>
</head>
<c:if test="${not empty message }">
<script type="text/javascript">
	alert("${message}");
	<c:remove var="message" scope="request"/>
	<c:remove var="message" scope="session"/>
</script>
</c:if>
<body class="hold-transition sidebar-mini">
	<div class="wrapper">
		<!-- header 영역 -->
		<tiles:insertAttribute name="header"/>
		<div class="content-wrapper">
			<!-- content 영역 -->
			<tiles:insertAttribute name="content"/>
		</div>
		<!--  footer 영역 -->
		<tiles:insertAttribute name="footer"/>

		<aside class="control-sidebar control-sidebar-dark">
		</aside>
	</div>

	<script src="${pageContext.request.contextPath }/resources/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
	<script src="${pageContext.request.contextPath }/resources/dist/js/adminlte.min.js"></script>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="<http://java.sun.com/jsp/jstl/core>" prefix="c" %>
<!-- Content Header (Page header) -->
<section class="content-header">
	<div class="container-fluid">
		<div class="row mb-2">
			<div class="col-sm-6">
				<h1>공지사항 게시판</h1>
			</div>
			<div class="col-sm-6">
				<ol class="breadcrumb float-sm-right">
					<li class="breadcrumb-item"><a href="#">DDIT HOME</a></li>
					<li class="breadcrumb-item active">공지사항 게시판</li>
				</ol>
			</div>
		</div>
	</div>
	<!-- /.container-fluid -->
</section>

<!-- Main content -->
<section class="content">
	<div class="container-fluid">
		<div class="row">
			<div class="col-md-12">
				<div class="card">
					<div class="card-header">
						<div class="card-tools">
							<form method="post" id="searchForm" class="input-group input-group-sm" style="width: 440px;">
								<input type="hidden" name="page" id="page">
								<select class="form-control" name="searchType">
									<option value="title" <c:if test="${searchType eq 'title' }">selected</c:if>>제목</option>
									<option value="writer" <c:if test="${searchType eq 'writer' }">selected</c:if>>작성자</option>
								</select> 
								<input type="text" name="searchWord" value="${searchWord }" class="form-control float-right" placeholder="Search">
								<div class="input-group-append">
									<button type="submit" class="btn btn-default">
										<i class="fas fa-search"></i>검색
									</button>
								</div>
							</form>
						</div>
						<h3 class="card-title">공지사항</h3>
					</div>
					<!-- /.card-header -->
					<div class="card-body">
						<table class="table table-bordered">
							<thead>
								<tr>
									<th style="width: 6%">#</th>
									<th style="width: px">제목</th>
									<th style="width: 12%">작성자</th>
									<th style="width: 12%">작성일</th>
									<th style="width: 10%">조회수</th>
								</tr>
							</thead>
							<tbody>
								<c:set value="${pagingVO.dataList }" var="noticeList"/>
								<c:choose>
									<c:when test="${empty noticeList }">
										<tr>
											<td colspan="5">조회하신 게시글이 존재하지 않습니다.</td>
										</tr>
									</c:when>
									<c:otherwise>
										<c:forEach items="${noticeList }" var="notice">
											<tr>
												<td>${notice.boNo }</td>
												<td>
													<a href="/notice/detail.do?boNo=${notice.boNo }">
														 ${notice.boTitle }
													</a>
												</td>
												<td>${notice.boWriter }</td>
												<td>${notice.boDate }</td>
												<td>${notice.boHit }</td>
											</tr>
										</c:forEach>
									</c:otherwise>
								</c:choose>
							</tbody>
						</table>
					</div>
					<div class="card-body">
						<button type="button" class="btn btn-primary" id="newBtn">등록</button>
					</div>
					<!-- /.card-body -->
					<div class="card-footer clearfix" id="pagingArea">
						${pagingVO.pagingHTML }
					</div>
				</div>
			</div>
		</div>
	</div>
</section>
<script>
$(function(){
	var searchForm = $("#searchForm");
	var pagingArea = $("#pagingArea");
	var newBtn = $("#newBtn");
	
	pagingArea.on("click", "a", function(event){
		event.preventDefault();
		var pageNo = $(this).data("page");  // key를이용하여 페이지 번호가 넘어옴
		searchForm.find("#page").val(pageNo);
		searchForm.submit();
	});
	
	newBtn.on("click", function(){
		location.href = "/notice/form.do";
	});
});
</script>