저번 포스팅에서 구현한 게시판 목록에 페이지네이션 기능을 추가해봤다.
수정된 코드 아래에 첨부
개발 환경
언어: java
Spring Boot ver : 2.3.1.RELEASE
Mybatis : 2.3.0
IDE: intelliJ
SDK: JDK 17
의존성 관리툴: Maven
DB: MySQL 8.0.12
PaginateDto.java Class 추가
package org.study.board.dto;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class Board {
private Integer bno;
private String title;
private String content;
private String writer;
private Date regdate;
private int hit;
private boolean deleteYn;
private boolean noticeYn;
private Integer userIdx;
/*페이징 기능 추가*/
private Integer pageNo; // 현재 페이지 번호
private int pageSize; // 페이지 당 항목 수
private int pageOffset; // 현재 페이지 이전 항목 수
/*첨부파일*/
private List<FileDto> list;
private String filename;
private String[] uuids;
private String[] filenames;
private String[] contentTypes;
}
package org.study.board.dto;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class PaginateDto implements Serializable {
private static final long serialVersionUID = 1L;
private int totalSize; // 총 항목 수
private int totalPage; // 총 페이지 수
private int pageNo = 1; // 현재 페이지 번호
private int pageSize; // 페이지 당 항목 수
private String pageName = "page"; // 페이지 파라미터명
private boolean pageLoop = false; // 페이지 순환 여부(pageNo가 totalPage보다 클 경우 pageNo 설정 방법)
private int pageOffset; // 현재 페이지 이전 항목 수
private int pageFinal; // 현재 페이지 마지막 항목 수
private int nationSize; // 페이지 네비게이션 묶음 당 수
private int nationBegin; // 페이지 네비게이션 시작 번호
private int nationClose; // 페이지 네비게이션 끝 번호
private String params; // 파라미터(key1=value1&key2=value2)
public PaginateDto(int pageSize, int nationSize) {
this.pageSize = pageSize;
this.nationSize = nationSize;
}
public PaginateDto(int pageSize) {
this(pageSize, 0);
}
public void setPageNo(int pageNo) {
this.pageNo = pageNo;
}
public void setPageLoop(boolean pageLoop) {
this.pageLoop = pageLoop;
}
public void setTotalSize(int totalSize) {
this.totalSize = totalSize;
this._calc();
}
public void setPageName(String pageName) {
this.pageName = pageName;
}
public void setParams(String params) {
this.params = params;
}
private void _calc() {
if (pageSize <= 0) return;
// 전체 페이지 수 계산
totalPage = (int) Math.ceil(totalSize / (double) pageSize);
if (totalPage < 1) {
totalPage = 1;
}
// 현재 페이지 번호 확인
if (pageNo < 1) {
pageNo = 1;
}
if (totalPage < pageNo) {
pageNo = (pageLoop ? 1 : totalPage);
}
// 페이지 번호 계산
pageOffset = (pageNo - 1) * pageSize;
pageFinal = pageOffset + pageSize;
if (pageFinal > totalSize) {
pageFinal = totalSize;
}
// 페이지 네비게이션 계산
if (nationSize > 0) {
int share = (int) Math.floor(pageNo / (double) nationSize);
nationBegin = (share * nationSize) + 1;
nationClose = (share + 1) * nationSize;
if (nationClose > totalPage) {
nationClose = totalPage;
}
if (pageNo % nationSize == 0) {
nationBegin -= nationSize;
nationClose = nationBegin + nationSize - 1;
}
}
}
@Override
public String toString() {
return "PaginateDto [totalSize=" + totalSize + ", totalPage=" + totalPage + ", pageNo=" + pageNo + ", pageSize="
+ pageSize + ", pageOffset=" + pageOffset + ", pageFinal=" + pageFinal + ", nationSize=" + nationSize
+ ", nationBegin=" + nationBegin + ", nationClose=" + nationClose + "]";
}
}
package org.study.board.controller;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.study.board.dto.Board;
import org.study.board.dto.FileDto;
import org.study.board.dto.PaginateDto;
import org.study.board.dto.User;
import org.study.board.repository.BoardMapper;
import org.study.board.repository.UserMapper;
import org.study.board.service.BoardService;
import org.study.board.util.FileUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@Autowired
private UserMapper userMapper;
@Autowired
private HttpServletRequest request;
@RequestMapping("/main")
public String main(Board board, Model model, @RequestParam(defaultValue = "1") int page){
// 게시글 총 개수
int total = boardService.cntBoard();
model.addAttribute("cntBoard", total);
// 페이징
PaginateDto paginate = new PaginateDto(5, 3);
paginate.setPageNo(page);
paginate.setTotalSize(total);
board.setPageNo(page);
board.setPageSize(paginate.getPageSize());
board.setPageOffset(paginate.getPageOffset());
model.addAttribute("paginate", paginate);
model.addAttribute("board", boardService.getBoardlist(board));
return "board/main";
}
@GetMapping("/board/{bno}")
public String boardDetail(@PathVariable Integer bno, Model model, Board board){
Board boardDetail = boardService.getBoard(bno);
List<FileDto> file = boardService.getFile(board);
model.addAttribute("board", boardDetail);
model.addAttribute("getFile", file);
return "board/write";
}
@RequestMapping("/write")
public String write(@CookieValue(name="idx", required = false) Long idx, Model model, Board board){
User loginUser=userMapper.findById(idx);
model.addAttribute("user", loginUser);
if(board.getBno()==null){
model.addAttribute("getBoard", board);
model.addAttribute("getFile", boardService.getFile(board));
}
return "board/write";
}
@RequestMapping("/insertBoard")
public String insertBoard(@ModelAttribute Board board, @CookieValue(name="idx", required = false) Long idx, Model model) {
User loginUser=userMapper.findById(idx);
board.setWriter(loginUser.getUsername());
model.addAttribute("user", loginUser);
boardService.insertBoard(board);
return "redirect:/main";
}
@DeleteMapping("/delete/{bno}")
public ResponseEntity<String> deleteBoard(@PathVariable Integer bno) {
boolean deleted = boardService.deleteBoard(bno);
if (deleted) {
return ResponseEntity.ok("게시물이 성공적으로 삭제되었습니다.");
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("게시물 삭제 중에 오류가 발생했습니다.");
}
}
/*ajax로 첨부파일 처리*/
@RequestMapping("/ajaxFile")
@ResponseBody
public List<FileDto> ajaxFile(@RequestParam("files") MultipartFile[] uploadFile) {
// 파일 등록
List<FileDto> fileList = FileUtil.uploadFile(uploadFile);
return fileList;
}
/*파일 다운로드*/
@RequestMapping("/downloadFile")
public ResponseEntity<Resource> downloadFile(@ModelAttribute FileDto fileDto) throws IOException {
return boardService.downloadFile(fileDto);
}
}
package org.study.board.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.study.board.dto.Board;
import org.study.board.dto.FileDto;
import org.study.board.repository.BoardMapper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class BoardService {
@Autowired
private BoardMapper mapper;
public List<Board> getBoardlist(Board board) {
return mapper.getBoardList(board);
}
// 첨부파일 리스트
public List<FileDto> getFile (Board board) {return mapper.getFile(board);}
public Board getBoard(Integer bno){
return mapper.getBoard(bno);
}
public Integer cntBoard() {return mapper.cntBoard();}
public void insertBoard(Board board){
if (board.getBno() != null) {
mapper.deleteFile(board);
}
if(board.getBno()!=null){
mapper.updateBoard(board);
}else{
mapper.insertBoard(board);
}
// 파일 이름 유니크하게 생성
List<FileDto> list = new ArrayList<>();
String[] uuids = board.getUuids();
String[] fileNames = board.getFilenames();
String[] contentTypes = board.getContentTypes();
if(uuids!=null){
for(int i=0;i<uuids.length;i++){
FileDto fileDto = new FileDto();
fileDto.setFilename(fileNames[i]);
fileDto.setUuid(uuids[i]);
fileDto.setContentType(contentTypes[i]);
list.add(fileDto);
}
}
// 첨부파일 등록 (게시글이 있을경우)
if (!list.isEmpty()) {
board.setList(list);
mapper.insertFile(board);
}
}
// 파일 다운로드
public ResponseEntity<Resource> downloadFile(FileDto fileDto) throws IOException {
// 파일 저장 경로 설정
String filePath = "d:\\image";
Path path = Paths.get(filePath + "/" + fileDto.getUuid() + "_" + fileDto.getFilename());
String contentType = Files.probeContentType(path);
// header를 통해서 다운로드 되는 파일의 정보를 설정한다.
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(ContentDisposition.builder("attachment")
.filename(fileDto.getFilename(), StandardCharsets.UTF_8)
.build());
headers.add(HttpHeaders.CONTENT_TYPE, contentType);
Resource resource = new InputStreamResource(Files.newInputStream(path));
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
}
public boolean deleteBoard(Integer bno){
return mapper.deleteBoard(bno);
}
}
package org.study.board.repository;
import org.apache.ibatis.annotations.Mapper;
import org.study.board.dto.Board;
import org.study.board.dto.FileDto;
import java.util.List;
@Mapper
public interface BoardMapper {
List<Board> getBoardList(Board board);
List<FileDto> getFile(Board board);
Board getBoard(Integer bno);
Integer cntBoard();
void insertBoard(Board board);
void updateBoard(Board board);
boolean deleteBoard(Integer bno);
void insertFile(Board board);
void deleteFile(Board board);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.study.board.repository.BoardMapper">
<select id="getBoardList" resultType="org.study.board.dto.Board">
/*SELECT *
from board
where delete_yn!=1*/
SELECT b.*
, CASE WHEN COUNT(*) <![CDATA[>]]> 1
THEN CONCAT(f.filename, '외 ' , COUNT(*)-1, '건')
ELSE f.filename
END file_name
FROM board b
LEFT JOIN file f ON b.bno = f.bno
WHERE b.delete_yn != 1
GROUP BY b.bno
ORDER BY b.bno DESC
LIMIT #{pageSize} OFFSET ${pageOffset}
</select>
<select id="getBoard" parameterType="int"
resultType="org.study.board.dto.Board">
select *
from board
where delete_yn!=1 and bno=#{bno}
</select>
<select id="cntBoard" resultType="Integer">
SELECT count(*)
FROM board
WHERE delete_yn!=1
</select>
<!--첨부파일 리스트-->
<select id="getFile" resultType="org.study.board.dto.FileDto">
SELECT *
FROM file
WHERE delete_yn!=1 and bno=#{bno}
</select>
<insert id="insertBoard" parameterType="org.study.board.dto.Board">
INSERT INTO board (title, content, writer, regdate, hit, delete_yn, user_idx)
VALUES (#{title}, #{content}, #{writer}, NOW(), 0, 0, #{userIdx})
<selectKey keyProperty="bno" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>
<update id="updateBoard" parameterType="org.study.board.dto.Board">
UPDATE board
SET title=#{title}, content=#{content},regdate=NOW(), hit = #{hit}, delete_yn=0, user_idx = #{userIdx}
WHERE bno = #{bno}
</update>
<update id="deleteBoard" parameterType="int">
update board
SET delete_yn=1
WHERE bno=#{bno}
</update>
<!-- 파일 정보를 삽입하는 쿼리 -->
<insert id="insertFile" parameterType="org.study.board.dto.Board">
INSERT INTO file (bno, uuid, filename, content_type)
VALUES
<foreach collection="list" separator="," index="index" item="item">
(#{bno}, #{item.uuid}, #{item.filename}, #{item.contentType})
</foreach>
</insert>
<update id="deleteFile" parameterType="org.study.board.dto.Board">
UPDATE file
SET delete_yn=1
WHERE bno=#{bno} AND delete_yn=0
</update>
</mapper>
게시판 메인 페이지
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE HTML>
<head>
<title>게시판 메인</title>
<style>
.button-container {
margin-top: 20px;
}
.button-container input {
margin-right: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<h1>게시글 목록</h1>
게시판 총 ${cntBoard} 개
<table>
<thead>
<tr>
<th class="one wide">번호</th>
<th class="ten wide">글제목</th>
<th class="two wide">작성자</th>
<th class="three wide">작성일</th>
</tr>
</thead>
<tbody>
<c:forEach var="board" items="${board}">
<tr>
<td><span>${board.bno}</span></td>
<td><a href="/board/${board.bno}"><span>${board.title}</span></a></td>
<td><span>${board.writer}</span></td>
<td><span>${board.regdate}</span></td>
</tr>
</c:forEach>
</tbody>
</table>
<%--페이징 처리 시작--%>
<div class="col-sm-12 col-md-7" style="margin: auto">
<div class="dataTables_paginate paging_simple_numbers" id="dataTable_paginate">
<div class="paging">
<c:set var="_paginate" value="${paginate}" />
<c:if test="${not empty _paginate}">
<c:set var="_page_no" value="${_paginate.pageNo}" />
<c:set var="_page_name" value="${_paginate.pageName}" />
<c:set var="_page_total" value="${_paginate.totalPage}" />
<c:set var="_page_params" value="${fn:escapeXml(_paginate.params)}${empty _paginate.params ? '' : '&'}" />
<c:set var="_nation_size" value="${_paginate.nationSize}" />
<c:set var="_nation_begin" value="${_paginate.nationBegin}" />
<c:set var="_nation_close" value="${_paginate.nationClose}" />
<c:set var="_paging" value="" />
<%-- c:choose>
<c:when test="${_page_no gt 1}">
<c:set var="_paging">${_paging}<a href="?${_page_params}${_page_name}=1" page="1" class="first">처음</a></c:set>
</c:when>
<c:otherwise>a href="#" onclick="re
<c:set var="_paging">${_paging}<turn false" class="first">처음</a></c:set>
</c:otherwise>
</c:choose --%>
<c:choose>
<c:when test="${_nation_begin gt 1}">
<c:set var="_page_prev" value="${_nation_begin - 1}" />
<c:set var="_paging">${_paging}<a href="?${_page_params}${_page_name}=${_page_prev}" page="${_page_prev}" class="prev">이전</a></c:set>
</c:when>
<c:otherwise>
<c:set var="_paging">${_paging}<a href="#" onclick="return false" class="prev">이전</a></c:set>
</c:otherwise>
</c:choose>
<c:forEach var="__p" begin="${_nation_begin}" end="${_nation_close}">
<c:choose>
<c:when test="${__p eq _page_no}">
<c:set var="_paging">${_paging}<strong class="current">${__p}</strong></c:set>
</c:when>
<c:otherwise>
<c:set var="_paging">${_paging}<a href="?${_page_params}${_page_name}=${__p}" page="${__p}">${__p}</a></c:set>
</c:otherwise>
</c:choose>
</c:forEach>
<c:choose>
<c:when test="${_nation_close ne _page_total}">
<c:set var="_page_next" value="${_nation_begin + _nation_size}" />
<c:set var="_paging">${_paging}<a href="?${_page_params}${_page_name}=${_page_next}" page="${_page_next}" class="next">다음</a></c:set>
</c:when>
<c:otherwise>
<c:set var="_paging">${_paging}<a href="#" onclick="return false" class="next">다음</a></c:set>
</c:otherwise>
</c:choose>
<%-- c:choose>
<c:when test="${_page_no lt _page_total}">
<c:set var="_paging">${_paging}<a href="?${_page_params}${_page_name}=${_page_total}" page="${_page_total}" class="last">마지막</a></c:set>
</c:when>
<c:otherwise>
<c:set var="_paging">${_paging}<a href="#" onclick="return false" class="last">마지막</a></c:set>
</c:otherwise>
</c:choose --%>
<c:out value="${_paging}" escapeXml="false" />
</c:if>
</div>
</div>
</div>
<%--페이징 처리 끝--%>
<%
// 쿠키 배열 가져오기
Cookie[] cookies = request.getCookies();
boolean isLoggedIn = false;
// 쿠키 배열을 순회하며 idx 쿠키의 존재 여부 확인
if (cookies != null) {
for (Cookie c : cookies) {
if (c.getName().equals("idx")) {
// idx 쿠키가 존재하면 로그인 상태로 설정
isLoggedIn = true;
break; // 로그인 상태이므로 더 이상 반복할 필요가 없음
}
}
}
%>
<div class="button-container">
<%
// 로그인 상태에 따라 버튼 표시
if (isLoggedIn) {
%>
<input type="button" value="user 목록" onclick="location.href='/user/main'"><br/><br/>
<input type="button" value="글 작성" onclick="location.href='/write'"><br/><br/>
<input type="button" value="로그아웃" onclick="location.href='/logout'"><br/><br/>
<%
} else {
%>
<input type="button" value="로그인" onclick="location.href='/login'"><br/><br/>
<input type="button" value="회원가입" onclick="location.href='/join'"><br/><br/>
<%
}
%>
</div>
</body>
</html>
/main
메인 페이지 - 1페이지
메인 페이지 - 2페이지 /main?page=2