게시물을 한 페이지에 10개씩 보이도록 하고 페이지네이션을 한 페이지에 1~10까지 표시하고자 한다.
게시물을 검색했을 때도 리스트에 똑같이 적용되게 할 것이므로 함께 코드를 적어주어야 한다.
domain package에 PagingVO class를 생성하고 내가 표현하고자 하는 페이지네이션 틀을 잡는다.
PagingVO.java
package domain;
public class PagingVO {
private int pageNo; // 화면에 출력되는 페이지네이션 번호
// qty = quantity(수량)의 약자
private int qty; // 한 페이지에 보여줄 게시글 수 (10개)
private String type; // 검색 유형
private String keyword; // 검색어
public PagingVO() {
// 처음 게시판 리스트 들어갔을 때
// 페이지네이션 클릭 하기 전의 기본값 설정
this.pageNo = 1;
this.qty = 10;
}
// 페이지네이션을 클릭하면...
public PagingVO(int pageNo, int qty, String type, String keyword) {
this.pageNo = pageNo;
this.qty = qty;
this.type = type;
this.keyword = keyword;
}
// DB에서 사용되는 시작번호 (0번지부터 시작함)
public int getPageStart() {
// 1page => 0번째 게시물 ~ 9번째 게시물, 2page => 10번째 게시물~, 3page => 20번째 게시물~
return (pageNo - 1) * qty;
}
public String[] getTypeToArray() {
// type 변수가 null 이라면 빈 문자열 배열을 반환
// 아니라면 type 변수의 각 문자를 포함한 문자열 배열을 반환
return this.type == null?
new String[] {} : this.type.split("");
}
public int getPageNo() {
return pageNo;
}
public void setPageNo(int pageNo) {
this.pageNo = pageNo;
}
public int getQty() {
return qty;
}
public void setQty(int qty) {
this.qty = qty;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
@Override
public String toString() {
return "PagingVO [pageNo=" + pageNo + ", qty=" + qty + ", type=" + type + ", keyword=" + keyword + "]";
}
}
페이지네이션은 비동기 작업처리를 하므로 handler가 필요하다.
그래서 src/main/java에 handler package를 생성해서 PagingHandler class를 생성한다.
동기 작업처리와 비동기 작업처리 그리고 비동기 작업처리와 handler의 관계에 대해 궁금하다면
https://rlog0918.tistory.com/183 여기를 참고한다.
PagingHandler.java
package handler;
import domain.PagingVO;
public class PagingHandler {
// board list.jsp 하단에 나오는 페이지네이션 핸들링 클래스
private int startPage; // 현재 화면에서 보여줄 시작 페이지네이션 번호
private int endPage; // 현재 화면에서 보여줄 끝 페이지네이션 번호
private int realEndPage; // 실제 전체 리스트의 끝 번호
// prev는 preview 약자
private boolean prev, next; // 이전, 다음 페이지의 존재 여부
// parameter로 받기
private int totalCount; // 전체 게시물 수
// pageNo / qty => 현재 사용자가 클릭한 번호와 한 화면에 표시되는 개수
private PagingVO pgvo;
public PagingHandler(PagingVO pgvo, int totalCount) {
this.pgvo = pgvo; // 생성자에 pageNo = 1, qty = 10 설정해준거 기억나지?
// boardController에서 DB조회 후 parameter로 전송
this.totalCount = totalCount; // 총 게시물 수 가져와
// 1~10 / 11~20 / 21~30
// 페이지번호 1~10을 클릭해도 1, 끝은 10
// 1 => 10 / 2 => 10 / 3 => 10 ... / 11 => 20 ...
// 페이지번호 / 한 화면의 페이지네이션 수 * 한 화면의 페이지네이션 수
// 1 / 10 = 0.1 =>(올림) 1 => 1*10 => 10
// 2 / 10 = 0.2 =>(올림) 1 => 1*10 => 10
// 11 / 10 => 1.1 => (올림) 2 => 2*10 => 20
// 21 / 10 => 2.1 => (올림) 3 => 3*10 => 30
// 정수 / 정수 => 정수
// 소수점 나오게 하려고 ceil(pgvo.get~ / pgvo.get~) 두 개 중에서 아무거나 하나를 double로 형변환
this.endPage = (int)Math.ceil(pgvo.getPageNo() / (double)pgvo.getQty())
* pgvo.getQty();
this.startPage = this.endPage - 9;
// 전체 게시글 수 / 한 화면에 게시되는 게시글 수
// 예) 101 / 10 => 10.1 => 올림 11페이지까지 나와야 함
// 나머지 게시글이 하나라도 있다면 1페이지가 더 생겨야 하기 때문
this.realEndPage = (int)Math.ceil(totalCount / (double)pgvo.getQty());
// 진짜 끝 페이지가 endPage와 같지 않을 경우 endPage를 realEndPage에 맞춤
// 예) realEndPage = 11 / endPage = 20
if(this.realEndPage < this.endPage) {
this.endPage = this.realEndPage;
}
// 이전, 다음 유무
// startPage 1, 11, 21...
this.prev = this.startPage > 1;
this.next = this.endPage < this.realEndPage;
}
public int getStartPage() {
return startPage;
}
public void setStartPage(int startPage) {
this.startPage = startPage;
}
public int getEndPage() {
return endPage;
}
public void setEndPage(int endPage) {
this.endPage = endPage;
}
public int getRealEndPage() {
return realEndPage;
}
public void setRealEndPage(int realEndPage) {
this.realEndPage = realEndPage;
}
public boolean isPrev() {
return prev;
}
public void setPrev(boolean prev) {
this.prev = prev;
}
public boolean isNext() {
return next;
}
public void setNext(boolean next) {
this.next = next;
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public PagingVO getPgvo() {
return pgvo;
}
public void setPgvo(PagingVO pgvo) {
this.pgvo = pgvo;
}
@Override
public String toString() {
return "PagingHandler [startPage=" + startPage + ", endPage=" + endPage + ", realEndPage=" + realEndPage
+ ", prev=" + prev + ", next=" + next + ", totalCount=" + totalCount + ", pgvo=" + pgvo + "]";
}
}
handler까지 작성을 완료했다면 orm > MybatisConfig.xml에 PagingVO의 alilas를 설정해준다.
mapper는 boardMapper를 사용할 거라서 별도로 추가하지 않아도 된다.
MybatisConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases> <!-- 여러개 생성 가능 / 없는 것을 만들 순 없음 -->
<typeAlias type="domain.MemberVO" alias="mvo"/>
<typeAlias type="domain.BoardVO" alias="bvo"/>
<typeAlias type="domain.PagingVO" alias="pgvo"/>
</typeAliases>
<environments default="development">
// ... (기존 코드)
</environments>
<mappers>
<mapper resource="mapper/memberMapper.xml"/>
<mapper resource="mapper/boardMapper.xml"/>
</mappers>
</configuration>
게시판 리스트가 페이지네이션이 적용되도록 BoardController를 먼저 작성한다.
BoardController.java
package controller;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import domain.BoardVO;
import domain.PagingVO;
import handler.PagingHandler;
import service.BoardService;
import service.BoardServiceImpl;
@WebServlet("/brd/*")
public class BoardController extends HttpServlet {
// ... (기존 코드)
switch(path) {
case "list":
try {
log.info("list check 1");
PagingVO pgvo = new PagingVO();
if(request.getParameter("pageNo") != null) {
int pageNo = Integer.parseInt(request.getParameter("pageNo"));
int qty = Integer.parseInt(request.getParameter("qty"));
String type = request.getParameter("type");
String keyword = request.getParameter("keyword");
pgvo = new PagingVO(pageNo, qty, type, keyword);
log.info(">>> pgvo >>> " + pageNo + " / " + qty + " / " + type + " / " + keyword);
}
// 페이지네이션을 적용하기 위해서 bsv.getList() 메서드에 pgvo 객체를 전달
List<BoardVO> list = bsv.getList(pgvo);
log.info(">>> list >>> {} ", list);
// DB에서 전체 게시글 수 가져오기 || search 값의 게시글 카운트
int totalCount = bsv.getTotal(pgvo);
log.info("totalCount >>> {}", totalCount);
PagingHandler ph = new PagingHandler(pgvo, totalCount);
// 가져온 list를 "list"라는 이름으로 list.jsp에 뿌림
request.setAttribute("list", list);
// 검색어를 반영한 리스트
request.setAttribute("ph", ph);
destPage = "/board/list.jsp";
} catch (Exception e) {
log.info("list error");
e.printStackTrace();
}
break;
}
// ... (기존 코드)
}
위의 코드에서 볼 수 있듯이 페이지네이션을 적용하기 위해서
기존의 getList() method에 pgvo 객체를 전달했다.
List<BoardVO> list = bsv.getList(pgvo);
코드가 이렇게 바뀌게 되면서 기존의 service / serviceImpl / DAO / DAOImpl에도
pgvo가 전달 받을 수 있도록 모두 추가해주어야한다.
// Board Service interface
public interface BoardService {
List<BoardVO> getList(PagingVO pgvo);
}
// BoardServiceImpl class
public class BoardServiceImpl implements BoardService {
// ... (기존 코드)
@Override
public List<BoardVO> getList(PagingVO pgvo) {
log.info(">>> list check 2");
return bdao.selectList(pgvo);
}
}
// BoardDAO interface
public interface BoardDAO {
List<BoardVO> selectList(PagingVO pgvo);
}
// BoardDAOImpl class
public class BoardDAOImpl implements BoardDAO {
// .. (기존 코드)
@Override
public List<BoardVO> selectList(PagingVO pgvo) {
log.info(">>> list check 3");
return sql.selectList("BoardMapper.list", pgvo);
}
}
그리고 DB에서 전체 게시물 수를 가져오는 getTotal() method가 수행될 수 있도록
service에서 DAOImpl까지 순차적으로 진행한다.
BoardService.interface
package service;
import java.util.List;
import domain.BoardVO;
import domain.PagingVO;
public interface BoardService {
List<BoardVO> getList(PagingVO pgvo);
int getTotal(PagingVO pgvo);
}
BoardServiceImpl.class
package service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import domain.BoardVO;
import domain.PagingVO;
import repository.BoardDAO;
import repository.BoardDAOImpl;
public class BoardServiceImpl implements BoardService {
// ... (기존 코드)
@Override
public int getTotal(PagingVO pgvo) {
log.info(">>> totalCount check 2");
return bdao.getTotal(pgvo);
}
}
BoardDAO.interface
package repository;
import java.util.List;
import domain.BoardVO;
import domain.PagingVO;
public interface BoardDAO {
List<BoardVO> selectList(PagingVO pgvo);
int getTotal(PagingVO pgvo);
}
BoardDAOImpl.class
package repository;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import domain.BoardVO;
import domain.PagingVO;
import orm.DatabaseBuilder;
public class BoardDAOImpl implements BoardDAO {
// ... (기존 코드)
@Override
public int getTotal(PagingVO pgvo) {
log.info(">>> totalCount check 3");
return sql.selectOne("BoardMapper.total", pgvo);
}
}
Mapper는 게시판 리스트에서 게시물을 검색 했을 때의 경우 때문에 동적 SQL을 실행하기 위해서
예비구문을 사용해야하는데 코드 설명에 앞서 예비구문의 전체적인 코드를 보자면 다음과 같다.
동적 SQL에 대해 궁금하다면 https://rlog0918.tistory.com/184 여기를 참고한다.
<!-- 예비 구문 -->
<sql id="search">
<if test="type != null">
<trim prefix="where (" suffix=")" prefixOverrides="or">
<foreach collection="typeToArray" item="type">
<trim prefix="or">
<choose>
<!-- concat 함수 사용하여 붙이기 -->
<when test="type == 't'.toString()"> <!-- .toString은 문자로 인지시켜주려고 붙임 -->
title like concat('%', #{keyword}, '%')
</when>
<when test="type == 'c'.toString()">
content like concat('%', #{keyword}, '%')
</when>
<when test="type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</when>
</choose>
</trim>
</foreach>
</trim>
</if>
</sql>
search라는 id를 가지고 만약 검색 유형(type)이 null이 아니라면
동적SQL을 생성하기 위해 사용되는 trim구문을 시작한다.
<sql id="search">
<if test="type != null">
<trim>태그에서 prefix, suffix, prefixOverrides는 동적인 WHERE 조건을 구성할 때 주로 활용 된다.
prefix="where (" | WHERE 절을 시작하기 전에 삽입 되어야 하는 텍스트를 지정한다. 그러므로 "WHERE ("라는 문자열이 WHERE절을 시작하기 전에 추가된다. |
suffix=")" | WHERE 절이 끝난 후에 삽입되어야 하는 텍스트를 지정한다. 그러므로 ")"라는 문자열이 WHERE절이 끝난 후에 추가 된다. |
prefixOverrides="or" | WHERE 절 내부에 동적으로 추가되는 조건들의 첫부분이 "or"로 시작하면 이 부분을 제거한다. |
<trim prefix="where (" suffix=")" prefixOverrides="or">
<foreach> 태그는 컬렉션을 반복할 때 사용된다.
컬렉션은 여러 개의 요소를 담을 수 있는 자료 구조를 말하는데, array / list / set / map 등이 있다.
반복할 컬렉션을 typeToArray라는 변수로 지정 했고 컬렉션의 각 요소를 참조할 때 사용할 변수 이름으로
type라는 변수로 참조한다.
예를들어서 String[] typeToArray = { title, writer, comment}가 있다면 foreach를 진행하면서
type = title, type = writer 형식으로 가져온다는 말이다.
<foreach collection="typeToArray" item="type">
여기까지 OK.
collection="typeToArray"가 배열인지 리스트인지 그 외 기타등등인지 어떻게 알지?
궁금하다면 PagingVO를 확인해보면 된다.
우리는 이미 배열을 받겠다고 적어두었다.
// PaingVO class
public String[] getTypeToArray() {
// type 변수가 null 이라면 빈 문자열 배열을 반환
// 아니라면 type 변수의 각 문자를 포함한 문자열 배열을 반환
return this.type == null?
new String[] {} : this.type.split("");
}
<choose> 태그는 검색 유형에 따라 다른 검색 조건을 선택하기 위한 구문이고
<when> 태그는 각 검색 유형에 대한 검색 조건을 정의한다.
예를들어 t인 경우에는 title에서 키워드를 검삭하도록 설정하는 것이다.
<choose>
<when test="type == 't'.toString()"> <!-- .toString은 문자로 인지시켜주려고 붙임 -->
title like concat('%', #{keyword}, '%')
</when>
<when test="type == 'c'.toString()">
content like concat('%', #{keyword}, '%')
</when>
<when test="type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</when>
</choose>
그렇다면 <choose> 태그 바로 위에 사용된 <trim prefix="or"> 는 무엇일까?
말 그대로 <when>에 작성된 조건 코드의 앞에 or을 다 갖다 붙이겠다는 소리이다.
그래서 prefixOverrides="or"을 앞서 작성한 것이다.
<trim prefix="where (" suffix=")" prefixOverrides="or">부분부터 SQL 문으로 쓰자면
아래와 같이 표현된다.
prefixOverrides="or"을 썼을 때와 안썼을 때 코드가 어떻게 달라지는지 비교해 두었다.
# prefixOverrides="or"를 적용하지 않았을 때
WHERE (
OR title LIKE CONCAT('%', #{keyword}, '%')
OR content LIKE CONCAT('%', #{keyword}, '%')
OR writer LIKE CONCAT('%', #{keyword}, '%')
)
# prefixOverrides="or"를 적용했을 때
WHERE (
title LIKE CONCAT('%', #{keyword}, '%')
OR content LIKE CONCAT('%', #{keyword}, '%')
OR writer LIKE CONCAT('%', #{keyword}, '%')
)
<choose>에 따라서 WHERE안에 들어올 조건 구문이 달라지겠지만,
첫 번째 조건 구문에서는 OR이 적용되지 않는다는 사실에는 변함이 없다.
이렇게 작성한 예비구문을 사용하는 방법은 SQL문을 쓸 때 WHERE이 들어갈 위치에 집어넣으면 된다.
집어넣을 때는 아래와 같은 형태로 집어넣는다.
<include refid="search"></include>
구문 그대로 serach라는 id를 가진 SQL 조각을 가져와서 현재 SQL에 포함하라는 의미이다.
이렇게 작성된 예비구문은 검색할 때마다 변동이 생기는 게시판 리스트와 총 게시물 수를 구할 때
사용하도록 해야하고 적용된 boardMapper.xml은 아래와 같다.
boardMapper.xml
<?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="BoardMapper">
<select id="list" resultType="bvo">
select * from board
<include refid="search"></include>
order by bno desc limit #{pageStart}, #{qty}
</select>
<select id="total" resultType="int">
select count(bno) from board
<include refid="search"></include>
</select>
<!-- 예비구문 -->
<sql id="search">
<if test="type != null">
<trim prefix="where (" suffix=")" prefixOverrides="or">
<foreach collection="typeToArray" item="type">
<trim prefix="or">
<choose>
<!-- .toString은 문자로 인지시켜주려고 붙임 -->
<when test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</when>
<when test="type == 'c'.toString()">
content like concat('%', #{keyword}, '%')
</when>
<when test="type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</when>
</choose>
</trim>
</foreach>
</trim>
</if>
</sql>
</mapper>
이제 화면을 다듬기 위해 list.jsp로 넘어간다.
상단에는 검색을 위한 코드를, 하단에는 페이지네이션을 위한 코드를 작성한다.
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"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판</title>
</head>
<body>
<h1>정보 게시판</h1>
<!-- 검색 -->
<div>
<form action="/brd/list" method="get">
<c:set value="${ph.pgvo.type }" var="typed"></c:set>
<select name="type">
<option ${typed == null ? 'selected' : ''}>선택</option>
<option value="t" ${typed eq 't' ? 'selected' : ''}>제목</option>
<option value="w" ${typed eq 'w' ? 'selected' : ''}>작성자</option>
<option value="c" ${typed eq 'c' ? 'selected' : ''}>내용</option>
<option value="twc" ${typed eq 'twc' ? 'selected' : ''}>전체</option>
<option value="tc" ${typed eq 'tc' ? 'selected' : ''}>제목&내용</option>
<option value="tw" ${typed eq 'tw' ? 'selected' : ''}>제목&작성자</option>
<option value="wc" ${typed eq 'wc' ? 'selected' : ''}>작성자&내용</option>
</select>
<input type="text"name="keyword" placeholder="검색" value="${ph.pgvo.keyword }">
<input type="hidden" name="pageNo" value="1">
<input type="hidden" name="qty" value="${ph.pgvo.qty }">
<button type="submit">검색</button><span>${ph.totalCount}</span>
</form>
</div>
<table>
<tr>
<th>No</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
<!-- DB에서 가져온 리스트를 c:forEach를 통해 반복
var="bvo"는 BoardVO 객체를 참조한다. -->
<c:forEach items="${list}" var="bvo">
<tr>
<td>${bvo.bno }</td>
<td><a href="#">${bvo.title }</a></td>
<td>${bvo.writer }</td>
<td>${bvo.regdate }</td>
<td>${bvo.readcount }</td>
</tr>
</c:forEach>
</table>
<!-- 페이지네이션 -->
<div>
<!-- prev -->
<c:if test="${ph.prev }">
<a href="/brd/list?pageNo=${ph.startPage-1 }&qty=${ph.pgvo.qty}&type=${ph.pgvo.type}&keyword=${ph.pgvo.keyword}"> ◁ | </a>
</c:if>
<!-- paging -->
<c:forEach begin="${ph.startPage }" end="${ph.endPage }" var="i">
<a href="/brd/list?pageNo=${i}&qty=${ph.pgvo.qty}&type=${ph.pgvo.type}&keyword=${ph.pgvo.keyword}"> ${i} </a>
</c:forEach>
<!-- next -->
<c:if test="${ph.next }">
<a href="/brd/list?pageNo=${ph.endPage+1 }&qty=${ph.pgvo.qty}&type=${ph.pgvo.type}&keyword=${ph.pgvo.keyword}"> | ▷ </a>
</c:if>
</div>
<a href ="#"><button>글쓰기</button></a>
</body>
</html>
검색부분을 좀 더 상세히 설명하자면 c:set은 검색 유형을 select로 부터 받아온다.
select은 드롭다운 메뉴 형태를 만들어주고 option을 활용해서 아무 것도 선택하지 않았을 때,
즉 처음에는 선택에 있도록 고정해주었고 페이지가 바뀌더라도 선택한 type가 고정되어 있도록 해주었다.
<c:set value="${ph.pgvo.type }" var="typed"></c:set>
<select name="type">
<option ${typed == null ? 'selected' : ''}>선택</option>
<option value="t" ${typed eq 't' ? 'selected' : ''}>제목</option>
<option value="w" ${typed eq 'w' ? 'selected' : ''}>작성자</option>
<option value="c" ${typed eq 'c' ? 'selected' : ''}>내용</option>
<option value="twc" ${typed eq 'twc' ? 'selected' : ''}>전체</option>
<option value="tc" ${typed eq 'tc' ? 'selected' : ''}>제목&내용</option>
<option value="tw" ${typed eq 'tw' ? 'selected' : ''}>제목&작성자</option>
<option value="wc" ${typed eq 'wc' ? 'selected' : ''}>작성자&내용</option>
</select>
검색어를 입력하는 input 태그에는 value를 주어서 페이지가 바뀌어도 검색어가 계속 노출될 수 있도록 했고,
<input type="hidden"> 부분은 한 페이지에 보여질 게시물의 수를 서버로 전달하기 위한 용도로 만들었다.
<input type="text"name="keyword" placeholder="검색" value="${ph.pgvo.keyword }">
<input type="hidden" name="pageNo" value="1">
<input type="hidden" name="qty" value="${ph.pgvo.qty }">
마지막으로 검색 버튼 옆에 총 게시물 수를 나타냈다.
<button type="submit">검색</button><span>${ph.totalCount}</span>
검색 기능이 제대로 되는지 확인해보려고 DB에 게시물을 임의로 5개 정도 만들어서 넣어 두었다.
sql.sql
/* ... (기존 코드) */
-- 2023-12-10
CREATE TABLE board (
bno INT NOT NULL auto_increment,
title VARCHAR(200) NOT NULL,
writer VARCHAR(100) NOT NULL,
content text,
regdate datetime DEFAULT current_timestamp,
moddate datetime DEFAULT current_timestamp,
readcount INT DEFAULT 0,
PRIMARY KEY (bno));
# 게시판에 300개 채우기
INSERT INTO board (title, writer, content)
VALUES("titleTest","tester","contentTest");
# 게시물 검색할 때 쓰려고 5개 생성
INSERT INTO board (title, writer, content, readcount)
VALUES("안녕","1234","내용" , 0);
게시판 리스트 화면
게시판 리스트 중간 화면
게시판 리스트 마지막 화면
게시판 리스트 검색 했을 때
게시판 리스트 페이지 바뀌어도 type와 검색어 고정
어휴, 검색... 페이지네이션...
설명하기 너무 힘들어따...
코드 짜면서 동시에 글로 정리하는 내내 검색을 먼저 구현하고 페이지네이션을 후에 설명할까
고민했지만 글을 다 쓰고나니 역시 묶어두는 편이 좋은 것 같다.
숙련도가 높지 않은데 왔다갔다 하면서 설명하면 더 정신 없었을 것 같다.
[JSP/Servlet] 8. 게시판 리스트 - 검색과 페이지네이션
(다음 게시물 예고편)
[JSP/Servlet] 9. 게시글 쓰기
얼렁뚱땅 주니어 개발자
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!