day39 - 0920
페이징(Paging)
1) 현재 페이지 게시글 rownum의 시작과 끝 번호 관련
- Top N Query를 이용하여 작성일 기준 내림차순(최신순) 조회 결과의 갯수를 제한하여 View에 표시한다
- Top N Query의 rownum rn의 범위에 들어갈 바인드 변수의 값은 startRow()와 endRow() 메소드의 반환값으로 한다
- 현재 페이지의 첫 게시글 rownum과 마지막 게시글 rownum은 각각 startRow(), endRow()의 반환값과 같다
- 현재 페이지의 번호(page)와 페이지에 표시할 게시글의 수(size)에 대하여 startRow, endRow와의 관계는 다음과 같다
- startRow = endRow - (size - 1)
- endRow = p * size
** 2)와의 통일성을 위해 startRow를 먼저 구한 후 endRow를 구하는 방법도 있다
- startRow = (p - 1) / size * size + 1
- endRow = startRow + (size - 1)
2) 현재 페이지 블럭의 시작과 끝 번호 관련
- 한 페이지에 표시할 블럭의 갯수(블럭 구간의 크기, blockSize)는 10으로 한다
- 현재 페이지에 표시할 블럭의 시작 번호의 값은 startBlock() 메소드의 반환값으로 한다
- 현재 페이지에 표시할 블럭의 끝 번호의 값은 endBlock() 메소드의 반환값으로 한다
- 현재 페이지에 표시할 블럭의 끝 번호가 될 수 있는 값은 두 종류이며 두 값중 작은 값으로 한다
i) 다음 블럭 구간이 존재할 경우
- 마지막 블럭 구간의 끝 블럭 번호가 마지막 블럭 번호(lastBlock)보다 크다
- 이 때 블럭의 끝 번호 = 현재 페이지에 표시할 블럭의 시작 번호(startBlock)에 (blockSize - 1)을 더한 값으로 한다
ii) 다음 블럭 구간이 존재하지 않을 경우
- 마지막 블럭 구간의 끝 블럭 번호가 마지막 블럭 번호(lastBlock)와 같다
- 이 때 블럭의 끝 번호 = 블럭의 마지막 번호(lastBlock)으로 한다
- 블럭 구간의 크기(blockSize)에 대하여 starBlock, endBlock와의 관계는 다음과 같다
- startBlock = (p - 1) / blockSize * blockSize + 1
- endBlock = Math.min( startBlock + (blockSize - 1), lastBlock() )
3) 페이지 블럭 전체의 시작과 끝 번호 관련
- 페이지 블럭 전체의 시작 블럭 번호는 firstBlock() 메소드의 반환값으로 한다
- 페이지 블럭 전체의 끝 블럭 번호는 lastBlock() 메소드의 반환값으로 한다
- 페이지 블럭 전체의 시작 블럭 번호는 1로 한다
- 페이지 블럭 전체의 끝 블럭 번호는 마지막 게시글이 속한 블럭 번호(마지막 페이지 번호)와 같다
- firstBlock = 1
- lastBlock = pageCount
4) 페이지 이동시 표시할 블럭의 번호 관련
- 현재 페이지에 표시할 시작 번호 블럭의 이전 블럭 번호는 prevBlock() 메소드의 반환값으로 한다
- 현재 페이지에 표시할 끝 번호 블럭의 다음 블럭 번호는 nextBlock() 메소드의 반환값으로 한다
- 이전 블럭 번호는 현재 페이지에 표시할 시작 번호 블럭의 번호에서 1을 뺀 값이다
- 다음 블럭 번호는 현재 페이지에 표시할 끝 번호 블럭의 번호에서 1을 더한 값이다
- prevBlock = startBlock - 1
- nextBlock = endBlock + 1
BoardListSearchVO
1) 현재 페이지 게시글 rownum의 시작과 끝 번호 관련
// 1) 현재 페이지 게시글 rownum의 시작과 끝 번호 관련
// 필드
private int p = 1; // 현재 페이지 번호 (기본값이 1이 되도록)
private int size = 10; // 페이지에 표시할 게시글의 수
// 메소드
// 현재 페이지 첫 게시글 rownum
public int startRow() {
return endRow() - (size - 1);
}
// 현재 페이지 마지막 게시글 rownum
public int endRow() {
return p * size;
}
- 필드
int page | 현재 페이지 번호 (기본값이 1이 되도록 초기값을 1로 정한다) |
int size | 페이지에 표시할 게시글의 수 |
- 메소드
int | startRow() | 현재 페이지의 첫 번째 게시글 rownum을 반환 |
int | endRow() | 현재 페이지의 마지막 게시글 rownum을 반환 |
ex) 2페이지에 표시할 게시글
2페이지에 표시할 게시글의 rownum은 11부터 20까지이다
2페이지에 대하여 page = 2, size = 10이므로 2페이지의 마지막 게시글 rownum인 20(endRow)과의 관계식은 다음과 같다
20(endRow) = 2(page) * 10(size)
∴ endRow = page * size
2페이지에 표시할 게시글의 수(size)와 11(startRow), 20(endRow) 사이의 관계식은 다음과 같다
2페이지에 표시할 게시글의 수(size) = 20(endRow) - 11(startRow) + 1
∴ size = endRow - startRow + 1
따라서 2페이지의 첫 번째 게시글 rownum은 다음과 같은 관계식으로 나타낼 수 있다
∴ startRow = endRow - size + 1
∴ startRow = endRow - (size - 1)
2) 현재 페이지 블럭의 시작과 끝 번호 관련
// 2) 현재 페이지 블럭의 시작과 끝 번호 관련
// 현재 페이지에 표시할 블럭의 시작 번호
public int startBlock() {
return (p - 1) / blockSize * blockSize + 1;
}
// 현재 페이지에 표시할 블럭의 끝 번호
public int endBlock() {
int value = startBlock() + blockSize - 1;
return Math.min(value, lastBlock());
}
- 필드 없음
- 메소드
int | startBlock() | 현재 페이지에 표시할 블럭의 시작 번호 반환 |
int | endBlock() | 현재 페이지에 표시할 블럭의 끝 번호 반환 |
3) 페이지 블럭 전체의 시작과 끝 번호 관련
// 3) 페이지 블럭 전체의 시작과 끝 번호 관련
// 필드
// 총 게시글의 수 (= 마지막 게시글의 rownum)
private int count;
// 페이지에 표시할 블럭 구간의 크기
private int blockSize = 10;
// 메소드
// 마지막 게시글이 속한 블럭 번호(마지막 페이지 번호)
public int pageCount() {
return (count + (size - 1)) / size;
}
// 블럭의 시작 번호(첫 페이지 번호)
public int firstBlock() {
return 1;
}
// 블럭의 끝 번호 (마지막 페이지 번호 = 마지막 게시글이 속한 블럭 번호)
public int lastBlock() {
return pageCount();
}
- 필드
int count | 총 게시글의 수 (= 마지막 게시글의 rownum) |
int blockSize | 페이지에 표시할 블럭 구간의 크기 (10개의 블럭을 표시할 예정) |
- 메소드
int | pageCount() | 마지막 게시글이 속한 블럭 번호(마지막 페이지 번호) |
int | firstBlock() | 블럭의 시작 번호 반환 (첫 페이지 번호) |
int | lastBlock() | 블럭의 끝 번호 반환 (마지막 페이지 번호 = 마지막 게시글이 속한 블럭 번호) |
4) 페이지 이동시 표시할 블럭의 번호 관련
// 4) 페이지 이동시 표시할 블럭의 번호 관련
// 이전 블럭의 번호 반환
public int prevBlock() {
return startBlock() - 1;
}
// 다음 블럭의 번호 반환
public int nextBlock() {
return endBlock() + 1;
}
// 현재 블럭이 첫 번째 블럭인지의 여부를 반환
public boolean isFirst() {
return p == 1;
}
// 현재 블럭이 마지막 블럭인지의 여부를 반환
public boolean isLast() {
return endBlock() == lastBlock();
}
// 이전 블럭 구간이 존재하는지의 여부를 반환
public boolean hasPrev() {
return startBlock() > 1;
}
// 다음 블럭 구간이 존재하는지의 여부를 반환
public boolean hasNext() {
return endBlock() < lastBlock();
}
- 필드 없음
- 메소드
int | prevBlock() | 이전 블럭의 번호 반환(이전 페이지 번호) |
int | nextBlock() | 다음 블럭의 번호 반환(다음 페이지 번호) |
boolean | isFirst() | 해당 블럭이 시작 번호 블럭인지(첫 페이지인지)의 여부 반환 |
boolean | isLast() | 해당 블럭이 끝 번호 블럭인지(마지막 페이지인지)의 여부 반환 |
boolean | hasPrev() | 이전 블럭 구간이 존재하는지의 여부 반환 |
boolean | hasNext() | 다음 블럭 구간이 존재하는지의 여부 반환 |
5) 검색 조회시에도 페이징을 유지할 수 있도록 주소 뒤에 붙일 Query String 생성
- p(페이지 번호)를 제외한 나머지 항목들에 대한 파라미터명과 그 값을 반환
- 검색 조회일 경우 파라미터명인 size(페이지에 표시할 게시글의 수), type(검색 분류), keyword(검색어)와 그 값을 반환
- 전체 조회일 경우 size(페이지)와 그 값을 반환
// 5) 검색 조회시에도 페이징을 유지할 수 있도록 주소 뒤에 붙일 Query String을 반환
// - p(페이지 번호)를 제외한 나머지 항목들에 대한 파라미터명과 그 값을 반환
public String parameter() {
if(isSearch()) { // 검색 조회일 경우
return "size=" + size + "&type=" + type + "&keyword=" + keyword;
}
else { // 전체 조회일 경우
return "size=" + size;
}
}
- 필드 없음
- 메소드
String | parameter() | 조회 유형(검색 조회/전체 조회)에 따라 다른 Query String을 반환 |
BoardListSearchVO (완성)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoardListSearchVO {
// 필드
private String type; // 검색 분류
private String keyword; // 검색어
// 메소드 - 조회 유형이 검색 조회인지 여부 반환
public boolean isSearch() {
return type != null && keyword != null;
}
// 1) 현재 페이지 게시글 rownum의 시작과 끝 번호 관련
// 필드
private int p = 1; // 현재 페이지 번호 (기본값이 1이 되도록)
private int size = 10; // 페이지에 표시할 게시글의 수
// 메소드
// 현재 페이지 첫 게시글 rownum
public int startRow() {
return endRow() - (size - 1);
}
// 현재 페이지 마지막 게시글 rownum
public int endRow() {
return p * size;
}
// 2) 현재 페이지 블럭의 시작과 끝 번호 관련
// 현재 페이지에 표시할 블럭의 시작 번호
public int startBlock() {
return (p - 1) / blockSize * blockSize + 1;
}
// 현재 페이지에 표시할 블럭의 끝 번호
public int endBlock() {
int value = startBlock() + blockSize - 1;
return Math.min(value, lastBlock());
}
// 3) 페이지 블럭 전체의 시작과 끝 번호 관련
// 필드
// 총 게시글의 수 (= 마지막 게시글의 rownum)
private int count;
// 페이지에 표시할 블럭 구간의 크기
private int blockSize = 10;
// 메소드
// 마지막 게시글이 속한 블럭 번호(마지막 페이지 번호)
public int pageCount() {
return (count + (size - 1)) / size;
}
// 블럭의 시작 번호(첫 페이지 번호)
public int firstBlock() {
return 1;
}
// 블럭의 끝 번호 (마지막 페이지 번호 = 마지막 게시글이 속한 블럭 번호)
public int lastBlock() {
return pageCount();
}
// 4) 페이지 이동시 표시할 블럭의 번호 관련
// 이전 블럭의 번호 반환
public int prevBlock() {
return startBlock() - 1;
}
// 다음 블럭의 번호 반환
public int nextBlock() {
return endBlock() + 1;
}
// 현재 블럭이 첫 번째 블럭인지의 여부를 반환
public boolean isFirst() {
return p == 1;
}
// 현재 블럭이 마지막 블럭인지의 여부를 반환
public boolean isLast() {
return endBlock() == lastBlock();
}
// 이전 블럭 구간이 존재하는지의 여부를 반환
public boolean hasPrev() {
return startBlock() > 1;
}
// 다음 블럭 구간이 존재하는지의 여부를 반환
public boolean hasNext() {
return endBlock() < lastBlock();
}
// 5) 검색 조회시에도 페이징을 유지할 수 있도록 주소 뒤에 붙일 Query String을 반환
// - p(페이지 번호)를 제외한 나머지 항목들에 대한 파라미터명과 그 값을 반환
public String parameter() {
if(isSearch()) { // 검색 조회일 경우
return "size=" + size + "&type=" + type + "&keyword=" + keyword;
}
else { // 전체 조회일 경우
return "size=" + size;
}
}
}
- 필드
String typ | 검색 분류 |
String keyword | 검색어 |
int p | 현재 페이지 번호 (기본값이 1이 되도록) |
int size | 페이지에 표시할 게시글의 수 |
int count | 총 게시글의 수 (= 마지막 게시글의 rownum) |
int blockSize | 페이지에 표시할 블럭 구간의 크기 |
- 메소드
boolean | isSearch() | 조회 유형이 검색 조회인지의 여부 반환 |
int | startRow() | 현재 페이지 첫 게시글 rownum 반환 |
int | endRow() | 현재 페이지 마지막 게시글 rownum 반환 |
int | startBlock() | 현재 페이지에 표시할 블럭의 시작 번호 반환 |
int | endBlock() | 현재 페이지에 표시할 블럭의 끝 번호 반환 |
int | pageCount() | 마지막 게시글이 속한 페이지 번호 반환 (마지막 페이지 번호) |
int | firstBlock() | 블럭의 시작 번호 반환 (첫 페이지 번호) |
int | lastBlock() | 블럭의 끝 번호 반환 (마지막 페이지 번호 = 마지막 게시글이 속한 블럭 번호) |
int | prevBlock() | 이전 블록의 번호 반환(이전 페이지 번호) |
int | nextBlock() | 다음 블록의 번호 반환(다음 페이지 번호) |
boolean | isFirst() | 현재 블럭이 시작 번호 블럭인지(첫 페이지인지)의 여부 반환 |
boolean | isLast() | 현재 블럭이 끝 번호 블럭인지(마지막 페이지인지)의 여부 반환 |
boolean | hasPrev() | 이전 블럭 구간이 존재하는지의 여부 반환 |
boolean | hasNext() | 다음 블럭 구간이 존재하는지의 여부 반환 |
String | parameter() | 조회 유형(검색 조회/전체 조회)에 따라 다른 Query String을 반환 |
BoardDao
public interface BoardDao {
// 추상 메소드 - 게시글 조회
// 3) BoardListSearchVO를 이용한 통합 조회
List<BoardDto> selectList(BoardListSearchVO vo);
// - 전체 조회
List<BoardDto> list(BoardListSearchVO vo);
// - 검색 조회
List<BoardDto> search(BoardListSearchVO vo);
// 추상 메소드 - 전체 조회와 검색 조회 시 총 조회 결과 갯수 (마지막 게시글의 rownum)
// - 조회 유형에 따른 총 조회 결과 갯수
int count(BoardListSearchVO vo);
// - 전체 조회시 총 조회 결과 갯수
int listCount(BoardListSearchVO vo);
// - 검색 조회시 총 조회 결과 갯수
int searchCount(BoardListSearchVO vo);
}
BoardDaoImpl
- 조회 유형 판정과 실행시킬 메소드를 BoardDaoImpl에서 결정하도록 변경
@Repository
public class BoardDaoImpl implements BoardDao {
// 의존성 주입
@Autowired
JdbcTemplate jdbcTemplate;
// 게시글 조회를 위한 RowMapper
private RowMapper<BoardDto> mapper = new RowMapper<>() {
@Override
public BoardDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return BoardDto.builder()
.boardNo(rs.getInt("board_no"))
.boardWriter(rs.getString("board_writer"))
.boardTitle(rs.getString("board_title"))
.boardContent(rs.getString("board_content"))
.boardWritetime(rs.getDate("board_writetime"))
.boardUpdatetime(rs.getDate("board_updatetime"))
.boardRead(rs.getInt("board_read"))
.boardLike(rs.getInt("board_like"))
.boardHead(rs.getString("board_head"))
.build();
}
};
// 추상 메소드 오버라이딩 - 게시글 조회
// 3) BoardListSearchVO를 이용한 통합 조회
@Override
public List<BoardDto> selectList(BoardListSearchVO vo) {
// 조회 유형
if(vo.isSearch()) { // 검색 조회일 경우
return search(vo);
}
else { // 그렇지 않다면
return list(vo);
}
}
// - 전체 조회
@Override
public List<BoardDto> list(BoardListSearchVO vo) {
String sql = "select * from ("
+ "select rownum rn, TMP.* from ("
+ "select * from board order by board_no desc"
+ ")TMP"
+ ") where rn between ? and ?";
Object[] param = {vo.startRow(), vo.endRow()};
return jdbcTemplate.query(sql, mapper, param);
}
// - 검색 조회
@Override
public List<BoardDto> search(BoardListSearchVO vo) {
String sql = "select * from ("
+ "select rownum rn, TMP.* from ("
+ "select * from board "
+ "where instr(#1, ?) > 0 "
+ "order by board_no desc"
+ ")TMP"
+ ") where rn between ? and ?";
sql = sql.replace("#1", vo.getType());
Object[] param = {vo.getKeyword(), vo.startRow(), vo.endRow()};
return jdbcTemplate.query(sql, mapper, param);
}
// 추상 메소드 - 전체 조회와 검색 조회 시 조회 결과의 총 갯수 (마지막 게시글의 rownum)
// - 조회 유형에 따른 총 조회 결과 갯수
@Override
public int count(BoardListSearchVO vo) {
if(vo.isSearch()) { // 검색 조회일 경우
return searchCount(vo); // 검색 조회의 조회 결과 총 갯수 반환
}
else { // 그렇지 않을 경우(전체 조회)
return listCount(vo); // 전체 조회의 조회 결과 총 갯수 반환
}
}
// - 전체 조회시 조회 결과의 총 갯수
@Override
public int listCount(BoardListSearchVO vo) {
String sql = "select count(*) from board";
return jdbcTemplate.queryForObject(sql, int.class);
}
// - 검색 조회시 조회 결과의 총 갯수
@Override
public int searchCount(BoardListSearchVO vo) {
String sql = "select count(*) from board where instr(#1, ?) > 0";
sql = sql.replace("#1", vo.getType());
Object[] param = new Object[] {vo.getKeyword()};
return jdbcTemplate.queryForObject(sql, int.class, param);
}
}
BoardController
- BoardListSearchVO에는 총 6개의 필드가 있다
String typ | 검색 분류 |
String keyword | 검색어 |
int p | 현재 페이지 번호 (기본값을 1로 한다) |
int size | 페이지에 표시할 게시글의 수 |
int count | 총 게시글의 수 |
int blockSize | 페이지에 표시할 블럭의 수 (블럭 묶음) |
- 총 게시글의 수(count)는 BoardDao의 count(BoardListSearchVO vo) 메소드의 반환값으로 설정 (setter)
- 설정이 끝난 vo를 매개변수로 하여 조회 유형(전체 조회/검색 조회)에 따른 조회를 실행한 후 그 결과를 model에 첨부
@Controller
@RequestMapping("/board")
public class BoardController {
// 의존성 주입
@Autowired
BoardDao boardDao;
// 2. 게시글 목록
// 게시글 목록 Mapping
@GetMapping("/list")
public String selectList(Model model, @ModelAttribute(name = "vo") BoardListSearchVO vo) {
// 조회 유형 판정과 실행시킬 메소드를 BoardDaoImpl에서 결정하도록 변경
// 조회 유형에 따른 조회 결과의 총 갯수를 반환
int count = boardDao.count(vo);
// 반환한 조회 결과의 총 갯수(count)를 vo의 count 필드의 값으로 설정
vo.setCount(count);
// model에 조회 유형에 따른 조회 결과를 첨부
model.addAttribute("list", boardDao.selectList(vo));
// 게시글 목록(list.jsp)로 연결
return "board/list";
}
}
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt" uri = "http://java.sun.com/jsp/jstl/fmt" %>
<%-- 오늘 날짜를 구해서 today라는 변수로 만든다 --%>
<jsp:useBean id = "now" class = "java.util.Date"></jsp:useBean>
<c:set var = "today">
<fmt:formatDate value = "${now}" pattern = "yyyy-MM-dd"/>
</c:set>
<%-- 테스트용 데이터 출력 --%>
<h3>${vo}</h3>
<jsp:include page = "/WEB-INF/views/template/header.jsp">
<jsp:param value = "게시글 목록" name = "title"/>
</jsp:include>
<div align = "center">
<h1>게시글 목록</h1>
<table border = "1" width = "900">
<thead>
<%-- 로그인 상태에서만 글쓰기 항목이 보이도록 설정 --%>
<c:if test = "${loginId != null}">
<tr>
<td align = "right" colspan = "5">
<a href = "write">글쓰기</a>
</td>
</tr>
</c:if>
<tr>
<th>번호</th>
<th width = "45%">제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
</thead>
<tbody align = "center">
<c:forEach var = "boardDto" items = "${list}">
<tr>
<td>${boardDto.boardNo}</td>
<td align = "left">
<%-- 차수만큼 띄어쓰기 반복 --%>
<c:forEach var="i" begin="1" end="${boardDto.boardDepth}" step="1">
</c:forEach>
<%-- 말머리가 있을 경우에만 출력 --%>
<c:if test = "${boardDto.boardHead != null}">
[${boardDto.boardHead}]
</c:if>
<%-- 제목을 누르면 해당 게시글의 상세 페이지로 이동하도록 --%>
<a href = "detail?boardNo=${boardDto.boardNo}">
${boardDto.boardTitle}
</a>
</td>
<td>${boardDto.boardWriter}</td>
<td>
<%-- 작성 날짜를 current라는 변수로 만든다 --%>
<c:set var = "current">
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
</c:set>
<c:choose>
<%-- today(오늘 날짜)와 current(작성 날짜)가 같다면 시간과 분만 표시--%>
<c:when test = "${today == current}">
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "HH:mm"/>
</c:when>
<%-- 그렇지 않다면 년도-월-일로 표시 --%>
<c:otherwise>
<fmt:formatDate value = "${boardDto.boardWritetime}" pattern = "yyyy-MM-dd"/>
</c:otherwise>
</c:choose>
</td>
<td>${boardDto.boardRead}</td>
</tr>
</c:forEach>
</tbody>
<c:if test = "${loginId != null}">
<tfoot>
<tr>
<td align = "right" colspan = "5">
<a href = "write">글쓰기</a>
</td>
</tr>
</tfoot>
</c:if>
</table>
<%-- 페이지 네비게이터(변경 전) --%>
<h4> « < 1 2 3 4 5 6 7 8 9 10 > » </h4>
<%-- 페이지 네비게이터(변경 후) --%>
<h4>
<%-- 맨 처음 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isFirst()}"> <%-- 맨 처음 페이지가 아니라면 --%>
<a href = "list?p=${vo.firstBlock()}&${vo.parameter()}">«</a> <%-- 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">«</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 이전 구간의 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasPrev()}"> <%-- 이전 페이지가 있다면 --%>
<a href = "list?p=${vo.prevBlock()}&${vo.parameter()}"><</a> <%-- 이전 구간의 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = ""><</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 현재 구간의 페이지 이동 --%>
<%-- 변수명을 i로 하며 시작과 끝은 vo의 startBlock(), endBlock()의 반환값으로, 간격은 1로 한다 --%>
<c:forEach var = "i" begin = "${vo.startBlock()}" end = "${vo.endBlock()}" step = "1">
<a href = "list?p=${i}&${vo.parameter()}">${i}</a>
</c:forEach>
<%-- 다음 구간의 첫 번째 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasNext()}"> <%-- 다음 페이지가 있다면 --%>
<a href = "list?p=${vo.nextBlock()}&${vo.parameter()}">></a> <%-- 다음 구간의 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">></a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 맨 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isLast()}"> <%-- 맨 마지막 페이지가 아니라면 --%>
<a href = "list?p=${vo.lastBlock()}&${vo.parameter()}">»</a> <%-- 맨 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise>
<a href = "">»</a>
</c:otherwise>
</c:choose>
</h4>
<%-- 검색창 --%>
<form action = "list" method = "get">
<input type = "hidden" name = "size" value = "${vo.size}">
<select name = "type" required>
<option value = "board_title" <c:if test = "${vo.type == 'board_title'}">selected</c:if>>제목</option>
<option value = "board_content" <c:if test = "${vo.type == 'board_content'}">selected</c:if>>내용</option>
<option value = "board_writer" <c:if test = "${vo.type == 'board_writer'}">selected</c:if>>작성자</option>
</select>
<input type = "search" name = "keyword" placeholder = "검색어" required value = "${vo.keyword}">
<button type = "submit">검색</button>
</form>
</div>
<jsp:include page = "/WEB-INF/views/template/footer.jsp"></jsp:include>
- 페이지 이동을 할 수 있도록 페이지 네비게이터의 각 번호에 하이퍼링크 부여
<%-- 페이지 네비게이터 --%>
<h4>
<%-- 맨 처음 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isFirst()}"> <%-- 맨 처음 페이지가 아니라면 --%>
<a href = "list?p=${vo.firstBlock()}">«</a> <%-- 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">«</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 이전 구간의 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasPrev()}"> <%-- 이전 페이지가 있다면 --%>
<a href = "list?p=${vo.prevBlock()}"><</a> <%-- 이전 구간의 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = ""><</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 현재 구간의 페이지 이동 --%>
<%-- 변수명을 i로 하며 시작과 끝은 vo의 startBlock(), endBlock()의 반환값으로, 간격은 1로 한다 --%>
<c:forEach var = "i" begin = "${vo.startBlock()}" end = "${vo.endBlock()}" step = "1">
<a href = "list?p=${i}">${i}</a>
</c:forEach>
<%-- 다음 구간의 첫 번째 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasNext()}"> <%-- 다음 페이지가 있다면 --%>
<a href = "list?p=${vo.nextBlock()}">></a> <%-- 다음 구간의 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">></a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 맨 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isLast()}"> <%-- 맨 마지막 페이지가 아니라면 --%>
<a href = "list?p=${vo.lastBlock()}">»</a> <%-- 맨 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise>
<a href = "">»</a>
</c:otherwise>
</c:choose>
</h4>
- vo는 총 게시글의 수(count)의 설정이 끝난 BoardListSearchVO vo 이다
- 현재 구간의 페이지 이동
<%-- 현재 구간의 페이지 이동 --%>
<%-- 변수명을 i로 하며 시작과 끝은 vo의 startBlock(), endBlock()의 반환값으로, 간격은 1로 한다 --%>
<c:forEach var = "i" begin = "${vo.startBlock()}" end = "${vo.endBlock()}" step = "1">
<a href = "list?p=${i}">${i}</a>
</c:forEach>
int | startBlock() | 현재 페이지에 표시할 블럭의 시작 번호 반환 |
int | endBlock() | 현재 페이지에 표시할 블럭의 끝 번호 반환 |
- 이전 구간의 마지막 페이지로 이동 & 다음 구간의 첫 번째 페이지로 이동
<%-- 이전 구간의 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasPrev()}"> <%-- 이전 페이지가 있다면 --%>
<a href = "list?p=${vo.prevBlock()}"><</a> <%-- 이전 구간의 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = ""><</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 다음 구간의 첫 번째 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasNext()}"> <%-- 다음 페이지가 있다면 --%>
<a href = "list?p=${vo.nextBlock()}">></a> <%-- 다음 구간의 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">></a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
boolean | hasPrev() | 이전 블럭 구간이 존재하는지의 여부 반환 |
int | prevBlock() | 이전 블록의 번호 반환 |
boolean | hasNext() | 다음 블럭 구간이 존재하는지의 여부 반환 |
int | nextBlock() | 다음 블록의 번호 반환 |
- 맨 처음 페이지로 이동 & 맨 마지막 페이지로 이동
<%-- 맨 처음 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isFirst()}"> <%-- 맨 처음 페이지가 아니라면 --%>
<a href = "list?p=${vo.firstBlock()}">«</a> <%-- 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">«</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 맨 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isLast()}"> <%-- 맨 마지막 페이지가 아니라면 --%>
<a href = "list?p=${vo.lastBlock()}">»</a> <%-- 맨 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise>
<a href = "">»</a>
</c:otherwise>
</c:choose>
boolean | isFirst() | 현재 블럭이 시작 번호 블럭인지(첫 페이지인지)의 여부 반환 |
int | firstBlock() | 블럭의 시작 번호 반환(첫 페이지 번호) |
boolean | isLast() | 현재 블럭이 끝 번호 블럭인지(마지막 페이지인지)의 여부 반환 |
int | lastBlock() | 블럭의 끝 번호 반환(마지막 페이지 번호 = 마지막 게시글이 속한 블럭 번호) |
- 검색 조회시에도 페이징을 유지할 수 있도록 하이퍼링크에 parameter() 추가
<%-- 페이지 네비게이터(변경 후) --%>
<h4>
<%-- 맨 처음 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isFirst()}"> <%-- 맨 처음 페이지가 아니라면 --%>
<a href = "list?p=${vo.firstBlock()}&${vo.parameter()}">«</a> <%-- 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">«</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 이전 구간의 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasPrev()}"> <%-- 이전 페이지가 있다면 --%>
<a href = "list?p=${vo.prevBlock()}&${vo.parameter()}"><</a> <%-- 이전 구간의 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = ""><</a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 현재 구간의 페이지 이동 --%>
<%-- 변수명을 i로 하며 시작과 끝은 vo의 startBlock(), endBlock()의 반환값으로, 간격은 1로 한다 --%>
<c:forEach var = "i" begin = "${vo.startBlock()}" end = "${vo.endBlock()}" step = "1">
<a href = "list?p=${i}">${i}</a>
</c:forEach>
<%-- 다음 구간의 첫 번째 페이지로 이동 --%>
<c:choose>
<c:when test = "${vo.hasNext()}"> <%-- 다음 페이지가 있다면 --%>
<a href = "list?p=${vo.nextBlock()}&${vo.parameter()}">></a> <%-- 다음 구간의 첫 번째 페이지로 이동 --%>
</c:when>
<c:otherwise> <%-- 그렇지 않다면 --%>
<a href = "">></a> <%-- 아무런 페이지 변화가 없도록 --%>
</c:otherwise>
</c:choose>
<%-- 맨 마지막 페이지로 이동 --%>
<c:choose>
<c:when test = "${not vo.isLast()}"> <%-- 맨 마지막 페이지가 아니라면 --%>
<a href = "list?p=${vo.lastBlock()}&${vo.parameter()}">»</a> <%-- 맨 마지막 페이지로 이동 --%>
</c:when>
<c:otherwise>
<a href = "">»</a>
</c:otherwise>
</c:choose>
</h4>
- 검색시 크기 유지
<%-- 검색창 --%>
<form action = "list" method = "get">
<input type = "hidden" name = "size" value = "${vo.size}">
<select name = "type" required>
<option value = "board_title" <c:if test = "${vo.type == 'board_title'}">selected</c:if>>제목</option>
<option value = "board_content" <c:if test = "${vo.type == 'board_content'}">selected</c:if>>내용</option>
<option value = "board_writer" <c:if test = "${vo.type == 'board_writer'}">selected</c:if>>작성자</option>
</select>
<input type = "search" name = "keyword" placeholder = "검색어" required value = "${vo.keyword}">
<button type = "submit">검색</button>
</form>