관리 메뉴

거니의 velog

(11) 모델2 방식으로 효율적으로 개발하기 4_답변형 게시판 구현 8 본문

Java_JSP Model2

(11) 모델2 방식으로 효율적으로 개발하기 4_답변형 게시판 구현 8

Unlimited00 2023. 9. 26. 20:37

(7) 게시판 페이징 기능 구현

* 게시판 만들기 프로젝트의 마지막 단계이다.

* 어떤 게시판이든 목록의 글이 많아지면 한 페이지에 모든 글이 표시되는 것이 아니라 다음과 같이 [1], [2], [3] ... 이렇게 페이지별로 표시된다. 이렇게 하는 것이 보기에도 더 좋고 사용자가 이용하기에도 편리하기 때문이다.

* 이번에는 게시판의 페이징 기능을 구현해 보자. 먼저 글 목록에 페이징 기능이 어떻게 구현되는지 그 원리부터 살펴보자.

* 다음 그림은 게시판에 페이징 기능을 적용한 후 글 목록을 표시한 것이다.

* 여기서 하단에 보이는 숫자는 페이지 번호이다. 한 페이지마다 10개의 글이 표시되고, 이 페이지 10개가 모여 한 개의 섹션(section)이 된다. 첫 번째 섹션은 첫 번째 페이지부터 열 번째 페이지까지이다.

* 두 번째 섹션은 열한 번째 페이지부터 스무 번째 페이지까지이다. 따라서 사용자가 글 목록 페이지에서 [2]를 클릭하면 브라우저는 서버에 section 값으로는 1을, pageNum 값으로는 2를 전송하는 것이다. 그리고 글 목록에는 두 번째 페이지에 해당하는 글인 11에서 20번째 글을 테이블에서 조회한 후 표시한다.

* 다음 코드는 페이징 기능을 추가한 글 목록 조회 SQL문이다.

SELECT * FROM
    (
        SELECT ROWNUM as recNum
             , LVL
             , articleNO
             , parentNO
             , title
             , content
             , id
             , writedate
          FROM (
                    SELECT LEVEL as LVL
                         , articleNO
                         , parentNO
                         , title
                         , content
                         , id
                         , writedate
                      FROM t_board
                     START WITH parentNO = 0
                     CONNECT BY PRIOR articleNO = parentNO
                     ORDER SIBLINGS BY articleNO DESC
                )
    )
 WHERE recNum between (section-1)*100+(pageNum-1)*10+1 and (section-1)*100+pageNum*10; -- section과 pageNum 값으로 조건식의 recNum 범위를 정한 후 조회된 글 중에 해당하는 값이 있는 경우 최종적으로 조회한다.
 -- recNum between 1 and 10 -- section 값이 1이고 pageNum 값이 1인 경우이다.

* 페이징 기능을 구현하기 위해 서브 쿼리문과 오라클에서 제공하는 가상 컬럼인 ROWNUM을 이용한다. ROWNUM은 select 문으로 조회된 레코드 목록에 대해 오라클 자체에서 순서를 부여하여 레코드 번호를 순서대로 할당해 준다.

* 이 서브쿼리문의 실행 순서는 다음과 같다.

(1) 기존 계층형 구조로 글 목록을 일단 조회한다.

(2) 그 결과에 대해 다시 ROWNUM(recNum)이 표시되도록 서브 쿼리문을 이용해 다시 한번 조회한다.

(3) ROWNUM이 표시된 두 번째 결과에서 section과 pageNum으로 계산된 where 절의 between 연산자
    사이의 값에 해당하는 ROWNUM이 있는 레코드들만 최종적으로 조회한다.

* 다음은 SQL Developer에서 section 값이 1이고 pageNum이 1인 경우, 즉 첫 번째 페이지에 표시되는 8개의 글을 조회한 결과이다.

1. 본격적인 실습을 위해 페이지 기능을 구현한 자바 클래스와 JSP를 다음과 같이 추가한다.

2. BoardController 클래스를 다음과 같이 작성한다. /listArticle.do로 최초 요청 시 section과 pageNum의 기본값을 1로 초기화한다. 컨트롤러에서는 전달된 sectiom과 pageNum을 HashMap에 저장한 후 DAO로 전달한다.

package sec03.brd08;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
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 javax.servlet.http.HttpSession;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;

@WebServlet("/board/*")
public class BoardController extends HttpServlet {
	
	private static String ARTICLE_IMAGE_REPO = "C:\\board\\article_image";
	BoardService boardService;
	ArticleVO articleVO;

	public void init(ServletConfig config) throws ServletException {
		boardService = new BoardService();
		articleVO = new ArticleVO();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response)  throws ServletException, IOException {
		doHandle(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doHandle(request, response);
	}

	private void doHandle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String nextPage = "";
		request.setCharacterEncoding("utf-8");
		response.setContentType("text/html; charset=utf-8");
		HttpSession session;
		String action = request.getPathInfo();
		System.out.println("action:" + action);
		try {
			List<ArticleVO> articlesList = new ArrayList<ArticleVO>();
			if (action==null){	
				String _section=request.getParameter("section");
				String _pageNum=request.getParameter("pageNum"); // 최초 요청 시 또는 /listArticle.do로 요청 시 section 값과 pageNum 값을 구한다.
				
				int section = Integer.parseInt(((_section==null)? "1":_section));
				int pageNum = Integer.parseInt(((_pageNum==null)? "1":_pageNum)); // 최초 요청 시 section 값과 pageNum 값이 없으면 각각 1로 초기화한다. 
				
				Map<String, Integer> pagingMap = new HashMap<String, Integer>();
				pagingMap.put("section", section);
				pagingMap.put("pageNum", pageNum); // section 값과 pageNum 값을 HashMap에 저장한 후 메서드로 넘긴다.
				
				Map articlesMap=boardService.listArticles(pagingMap); // section 값과 pageNum 값으로 해당 섹션과 페이지에 해당되는 글 목록을 조회한다.
				articlesMap.put("section", section);
				articlesMap.put("pageNum", pageNum); // 브라우저에서 전송된 section과 pageNum값을 articlesMap에 저장한 후 listArticles.jsp로 넘긴다.
				request.setAttribute("articlesMap", articlesMap); // 조회된 글 목록을 articlesMap으로 바인딩하여 listArticles.jsp로 넘긴다.
				
				nextPage = "/board07/ listArticles.jsp";
				
				}else if(action.equals("/listArticles.do")){  			
					String _section=request.getParameter("section");
					String _pageNum=request.getParameter("pageNum");
					int section = Integer.parseInt(((_section==null)? "1":_section));
					int pageNum = Integer.parseInt(((_pageNum==null)? "1":_pageNum)); // 글 목록에서 명시적으로 페이지 번호를 눌러서 요청한 경우 section 값과 pageNum 값을 가져온다.
					
					Map pagingMap=new HashMap();
					pagingMap.put("section", section);
					pagingMap.put("pageNum", pageNum);
					Map articlesMap=boardService.listArticles(pagingMap);
					articlesMap.put("section", section);
					articlesMap.put("pageNum", pageNum);
					request.setAttribute("articlesMap", articlesMap);
					nextPage = "/board07/listArticles.jsp";
				
			} else if (action.equals("/articleForm.do")) {
				nextPage = "/board07/articleForm.jsp";
				
			} else if (action.equals("/addArticle.do")) {
				int articleNO = 0;
				Map<String, String> articleMap = upload(request, response);
				String title = articleMap.get("title");
				String content = articleMap.get("content");
				String imageFileName = articleMap.get("imageFileName");

				articleVO.setParentNO(0);
				articleVO.setId("hong");
				articleVO.setTitle(title);
				articleVO.setContent(content);
				articleVO.setImageFileName(imageFileName);
				articleNO = boardService.addArticle(articleVO);
				if (imageFileName != null && imageFileName.length() != 0) {
					File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
					File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
					destDir.mkdirs();
					FileUtils.moveFileToDirectory(srcFile, destDir, true);
				}
				PrintWriter pw = response.getWriter();
				pw.print("<script>" + "  alert('새글을 추가했습니다.');" + " location.href='" + request.getContextPath()
						+ "/board/listArticles.do';" + "</script>");

				return;
				
			} else if (action.equals("/viewArticle.do")) {
				String articleNO = request.getParameter("articleNO");
				articleVO = boardService.viewArticle(Integer.parseInt(articleNO));
				request.setAttribute("article", articleVO);
				nextPage = "/board07/viewArticle.jsp";
				
			} else if (action.equals("/modArticle.do")) {
				Map<String, String> articleMap = upload(request, response);
				int articleNO = Integer.parseInt(articleMap.get("articleNO"));
				articleVO.setArticleNO(articleNO);
				String title = articleMap.get("title");
				String content = articleMap.get("content");
				String imageFileName = articleMap.get("imageFileName");
				articleVO.setParentNO(0);
				articleVO.setId("hong");
				articleVO.setTitle(title);
				articleVO.setContent(content);
				articleVO.setImageFileName(imageFileName);
				boardService.modArticle(articleVO);
				if (imageFileName != null && imageFileName.length() != 0) {
					String originalFileName = articleMap.get("originalFileName");
					File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
					File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
					destDir.mkdirs();
					FileUtils.moveFileToDirectory(srcFile, destDir, true);
					File oldFile = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO + "\\" + originalFileName);
					oldFile.delete();
				}
				PrintWriter pw = response.getWriter();
				pw.print("<script>" + "  alert('글을 수정했습니다.');" + " location.href='" + request.getContextPath()
						+ "/board/viewArticle.do?articleNO=" + articleNO + "';" + "</script>");
				return;
				
			} else if (action.equals("/removeArticle.do")) {
				int articleNO = Integer.parseInt(request.getParameter("articleNO"));
				List<Integer> articleNOList = boardService.removeArticle(articleNO);
				for (int _articleNO : articleNOList) {
					File imgDir = new File(ARTICLE_IMAGE_REPO + "\\" + _articleNO);
					if (imgDir.exists()) {
						FileUtils.deleteDirectory(imgDir);
					}
				}

				PrintWriter pw = response.getWriter();
				pw.print("<script>" + "  alert('글을 삭제했습니다.');" + " location.href='" + request.getContextPath()
						+ "/board/listArticles.do';" + "</script>");
				return;

			} else if (action.equals("/replyForm.do")) {
				int parentNO = Integer.parseInt(request.getParameter("parentNO"));
				session = request.getSession();
				session.setAttribute("parentNO", parentNO);
				nextPage = "/board06/replyForm.jsp";
				
			} else if (action.equals("/addReply.do")) {
				session = request.getSession();
				int parentNO = (Integer) session.getAttribute("parentNO");
				session.removeAttribute("parentNO");
				Map<String, String> articleMap = upload(request, response);
				String title = articleMap.get("title");
				String content = articleMap.get("content");
				String imageFileName = articleMap.get("imageFileName");
				articleVO.setParentNO(parentNO);
				articleVO.setId("lee");
				articleVO.setTitle(title);
				articleVO.setContent(content);
				articleVO.setImageFileName(imageFileName);
				int articleNO = boardService.addReply(articleVO);
				if (imageFileName != null && imageFileName.length() != 0) {
					File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
					File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
					destDir.mkdirs();
					FileUtils.moveFileToDirectory(srcFile, destDir, true);
				}
				PrintWriter pw = response.getWriter();
				pw.print("<script>" + "  alert('답글을 추가했습니다.');" + " location.href='" + request.getContextPath()
						+ "/board/viewArticle.do?articleNO="+articleNO+"';" + "</script>");
				return;
			
			}else {
				nextPage = "/board06/listArticles.jsp";
			}

			RequestDispatcher dispatch = request.getRequestDispatcher(nextPage);
			dispatch.forward(request, response);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private Map<String, String> upload(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Map<String, String> articleMap = new HashMap<String, String>();
		String encoding = "utf-8";
		File currentDirPath = new File(ARTICLE_IMAGE_REPO);
		DiskFileItemFactory factory = new DiskFileItemFactory();
		factory.setRepository(currentDirPath);
		factory.setSizeThreshold(1024 * 1024);
		ServletFileUpload upload = new ServletFileUpload(factory);
		try {
			List items = upload.parseRequest(request);
			for (int i = 0; i < items.size(); i++) {
				FileItem fileItem = (FileItem) items.get(i);
				if (fileItem.isFormField()) {
					System.out.println(fileItem.getFieldName() + "=" + fileItem.getString(encoding));
					articleMap.put(fileItem.getFieldName(), fileItem.getString(encoding));
				} else {
					System.out.println("파라미터명:" + fileItem.getFieldName());
					//System.out.println("파일명:" + fileItem.getName());
					System.out.println("파일크기:" + fileItem.getSize() + "bytes");
					//articleMap.put(fileItem.getFieldName(), fileItem.getName());
					if (fileItem.getSize() > 0) {
						int idx = fileItem.getName().lastIndexOf("\\");
						if (idx == -1) {
							idx = fileItem.getName().lastIndexOf("/");
						}

						String fileName = fileItem.getName().substring(idx + 1);
						System.out.println("파일명:" + fileName);
								articleMap.put(fileItem.getFieldName(), fileName);  //익스플로러에서 업로드 파일의 경로 제거 후 map에 파일명 저장);
						File uploadFile = new File(currentDirPath + "\\temp\\" + fileName);
						fileItem.write(uploadFile);

					} // end if
				} // end if
			} // end for
		} catch (Exception e) {
			e.printStackTrace();
		}
		return articleMap;
	}

}

3. BoardService 클래스에서는 페이징 기능에 필요한 글 목록과 전체 글 수를 각각 조회할 수 있도록 다음과 같이 구현한다. HashMap을 생성한 후 조회한 두 정보를 각각 속성으로 저장한다.

* JSP로 넘겨줘야 할 정보가 많을 경우에는 각 request에 바인딩해서 넘겨도 되지만,
  HashMap을 사용해 같은 종류의 정보를 묶어서 넘기면 편리하다.
package sec03.brd08;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BoardService {
	
	BoardDAO boardDAO;

	public BoardService() {
		boardDAO = new BoardDAO();
	}

	public Map listArticles(Map<String, Integer> pagingMap) {
		Map articlesMap = new HashMap();
		List<ArticleVO> articlesList = boardDAO.selectAllArticles(pagingMap); // 전달된 pagingMap을 사용해 글 목록을 조회한다.
		int totArticles = boardDAO.selectTotArticles(); // 테이블에 존재하는 전체 글 수를 조회한다.
		articlesMap.put("articlesList", articlesList); // 조회된 글 목록을 ArrayList에 저장한 후 다시 articlesMap에 저장한다.
		articlesMap.put("totArticles", totArticles); // 전체 글 수를 articlesMap에 저장한다.
		//articlesMap.put("totArticles", 170);
		return articlesMap;
	}

	public List<ArticleVO> listArticles() {
		List<ArticleVO> articlesList = boardDAO.selectAllArticles();
		return articlesList;
	}

	public int addArticle(ArticleVO article) {
		return boardDAO.insertNewArticle(article);
	}

	public ArticleVO viewArticle(int articleNO) {
		ArticleVO article = null;
		article = boardDAO.selectArticle(articleNO);
		return article;
	}

	public void modArticle(ArticleVO article) {
		boardDAO.updateArticle(article);
	}

	public List<Integer> removeArticle(int articleNO) {
		List<Integer> articleNOList = boardDAO.selectRemovedArticles(articleNO);
		boardDAO.deleteArticle(articleNO);
		return articleNOList;
	}

	public int addReply(ArticleVO article) {
		return boardDAO.insertNewArticle(article);
	}

}

4. BoardDAO 클래스를 다음과 같이 작성한다. 전달받은 section과 pageNum 값을 이용해 SQL문으로 조회한다.

package sec03.brd08;

import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;


public class BoardDAO {
	
	private DataSource dataFactory;
	Connection conn;
	PreparedStatement pstmt;

	public BoardDAO() {
		try {
			Context ctx = new InitialContext();
			Context envContext = (Context) ctx.lookup("java:/comp/env");
			dataFactory = (DataSource) envContext.lookup("jdbc/oracle");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public List selectAllArticles(Map pagingMap){
		List articlesList = new ArrayList();
		int section = (Integer)pagingMap.get("section");
		int pageNum=(Integer)pagingMap.get("pageNum"); // 전송된 section과 pageNum 값을 가져온다.
		try{
		   conn = dataFactory.getConnection();
		   String query ="SELECT * FROM ( "
						+ "select ROWNUM  as recNum,"+"LVL,"
							+"articleNO,"
							+"parentNO,"
							+"title,"
							+"id,"
							+"writeDate"
				                  +" from (select LEVEL as LVL, "
								+"articleNO,"
								+"parentNO,"
								+"title,"
								+"id,"
								 +"writeDate"
							   +" from t_board" 
							   +" START WITH  parentNO=0"
							   +" CONNECT BY PRIOR articleNO = parentNO"
							  +"  ORDER SIBLINGS BY articleNO DESC)"
					+") "                        
					+" where recNum between(?-1)*100+(?-1)*10+1 and (?-1)*100+?*10"; // section과 pageNum 값으로 레코드 번호의 범위를 조건으로 정한다(이들 값이 각각 1로 전송되었으면 between 1 and 10이 된다).
		   System.out.println(query);
		   pstmt= conn.prepareStatement(query);
		   pstmt.setInt(1, section);
		   pstmt.setInt(2, pageNum);
		   pstmt.setInt(3, section);
		   pstmt.setInt(4, pageNum);
		   ResultSet rs =pstmt.executeQuery();
		   while(rs.next()){
		      int level = rs.getInt("lvl");
		      int articleNO = rs.getInt("articleNO");
		      int parentNO = rs.getInt("parentNO");
		      String title = rs.getString("title");
		      String id = rs.getString("id");
		      Date writeDate= rs.getDate("writeDate");
		      ArticleVO article = new ArticleVO();
		      article.setLevel(level);
		      article.setArticleNO(articleNO);
		      article.setParentNO(parentNO);
		      article.setTitle(title);
		      article.setId(id);
		      article.setWriteDate(writeDate);
		      articlesList.add(article);	
		   } //end while
		   rs.close();
		   pstmt.close();
		   conn.close();
	  }catch(Exception e){
	     e.printStackTrace();	
	  }
	  return articlesList;
    } 
	
	public List selectAllArticles() {
		List articlesList = new ArrayList();
		try {
			conn = dataFactory.getConnection();
			String query = "SELECT LEVEL,articleNO,parentNO,title,content,id,writeDate" + " from t_board"
					+ " START WITH  parentNO=0" + " CONNECT BY PRIOR articleNO=parentNO"
					+ " ORDER SIBLINGS BY articleNO DESC";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next()) {
				int level = rs.getInt("level");
				int articleNO = rs.getInt("articleNO");
				int parentNO = rs.getInt("parentNO");
				String title = rs.getString("title");
				String content = rs.getString("content");
				String id = rs.getString("id");
				Date writeDate = rs.getDate("writeDate");
				ArticleVO article = new ArticleVO();
				article.setLevel(level);
				article.setArticleNO(articleNO);
				article.setParentNO(parentNO);
				article.setTitle(title);
				article.setContent(content);
				article.setId(id);
				article.setWriteDate(writeDate);
				articlesList.add(article);
			}
			rs.close();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return articlesList;
	}


	private int getNewArticleNO() {
		try {
			conn = dataFactory.getConnection();
			String query = "SELECT  max(articleNO) from t_board ";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			ResultSet rs = pstmt.executeQuery(query);
			if (rs.next())
				return (rs.getInt(1) + 1);
			rs.close();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return 0;
	}

	public int insertNewArticle(ArticleVO article) {
		int articleNO = getNewArticleNO();
		try {
			conn = dataFactory.getConnection();
			int parentNO = article.getParentNO();
			String title = article.getTitle();
			String content = article.getContent();
			String id = article.getId();
			String imageFileName = article.getImageFileName();
			String query = "INSERT INTO t_board (articleNO, parentNO, title, content, imageFileName, id)"
					+ " VALUES (?, ? ,?, ?, ?, ?)";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			pstmt.setInt(1, articleNO);
			pstmt.setInt(2, parentNO);
			pstmt.setString(3, title);
			pstmt.setString(4, content);
			pstmt.setString(5, imageFileName);
			pstmt.setString(6, id);
			pstmt.executeUpdate();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}

		return articleNO;
	}

	public ArticleVO selectArticle(int articleNO) {
		ArticleVO article = new ArticleVO();
		try {
			conn = dataFactory.getConnection();
			String query = "select articleNO,parentNO,title,content, NVL(imageFileName, 'null') as imageFileName, id, writeDate" + " from t_board"
					+ " where articleNO=?";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			pstmt.setInt(1, articleNO);
			ResultSet rs = pstmt.executeQuery();
			rs.next();
			int _articleNO = rs.getInt("articleNO");
			int parentNO = rs.getInt("parentNO");
			String title = rs.getString("title");
			String content = rs.getString("content");
			String imageFileName = URLEncoder.encode(rs.getString("imageFileName"), "UTF-8"); //파일이름에 특수문자가 있을 경우 인코딩합니다.
			if(imageFileName.equals("null")) {
				imageFileName = null;
			}
			
			String id = rs.getString("id");
			Date writeDate = rs.getDate("writeDate");

			article.setArticleNO(_articleNO);
			article.setParentNO(parentNO);
			article.setTitle(title);
			article.setContent(content);
			article.setImageFileName(imageFileName);
			article.setId(id);
			article.setWriteDate(writeDate);
			rs.close();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return article;
	}

	public void updateArticle(ArticleVO article) {
		int articleNO = article.getArticleNO();
		String title = article.getTitle();
		String content = article.getContent();
		String imageFileName = article.getImageFileName();
		try {
			conn = dataFactory.getConnection();
			String query = "update t_board  set title=?,content=?";
			if (imageFileName != null && imageFileName.length() != 0) {
				query += ",imageFileName=?";
			}
			query += " where articleNO=?";

			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			pstmt.setString(1, title);
			pstmt.setString(2, content);
			if (imageFileName != null && imageFileName.length() != 0) {
				pstmt.setString(3, imageFileName);
				pstmt.setInt(4, articleNO);
			} else {
				pstmt.setInt(3, articleNO);
			}
			pstmt.executeUpdate();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void deleteArticle(int articleNO) {
		try {
			conn = dataFactory.getConnection();
			String query = "DELETE FROM t_board ";
			query += " WHERE articleNO in (";
			query += "  SELECT articleNO FROM  t_board ";
			query += " START WITH articleNO = ?";
			query += " CONNECT BY PRIOR  articleNO = parentNO )";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			pstmt.setInt(1, articleNO);
			pstmt.executeUpdate();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public List<Integer> selectRemovedArticles(int articleNO) {
		List<Integer> articleNOList = new ArrayList<Integer>();
		try {
			conn = dataFactory.getConnection();
			String query = "SELECT articleNO FROM  t_board  ";
			query += " START WITH articleNO = ?";
			query += " CONNECT BY PRIOR  articleNO = parentNO";
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			pstmt.setInt(1, articleNO);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next()) {
				articleNO = rs.getInt("articleNO");
				articleNOList.add(articleNO);
			}
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return articleNOList;
	}

	public int selectTotArticles() {
		try {
			conn = dataFactory.getConnection();
			String query = "select count(articleNO) from t_board "; // 전체 글 수를 조회한다.
			System.out.println(query);
			pstmt = conn.prepareStatement(query);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next())
				return (rs.getInt(1));
			rs.close();
			pstmt.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return 0;
	}

}

5. 이제 화면을 구현하는 JSP 페이지인 listArticles.jsp를 다음과 같이 작성한다. 전체 글 수(totArticles)가 100개를 넘는 경우, 100인 경우, 100개를 넘지 않는 경우로 나누어 페이지 번호를 표시하도록 구현한다.

전체 글 수가 100개가 넘지 않으면 전체 글 수를 10으로 나눈 몫에 1을 더한 값이 페이지 번호로 표시된다. 예를 들어 전체 글 수가 13개 이면 10으로 나누었을 때의 몫인 1에 1을 더해 2가 페이지 번호로 표시된다.

만약 전체 글 수가 100개일 때는 정확히 10개의 페이지가 표시되며, 100개를 넘을 때는 다음 section으로 이동할 수 있도록 마지막 페이지 번호 옆에 next를 표시한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    isELIgnored="false" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<%-- HashMap으로 저장해서 넘어온 값들은 이름이 길어 사용하기 불편하므로 <c:set> 태그를 이용해 각 값들을 짧은 변수 이름으로 저장한다. --%>
<c:set var="articlesList" value="${articlesMap.articlesList}" />
<c:set var="totArticles" value="${articlesMap.totArticles}" />
<c:set var="section" value="${articlesMap.section}" />
<c:set var="pageNum" value="${articlesMap.pageNum}" />

<%
	request.setCharacterEncoding("UTF-8");
%>
<!DOCTYPE html>
<html>

    <head>
        <style>
            .no-uline {
                text-decoration: none;
            }

            .sel-page {
                text-decoration: none;
                color: red;
            } /* 선택된 페이지 번호를 빨간색으로 표시한다. */

            .cls1 {
                text-decoration: none;
            }

            .cls2 {
                text-align: center;
                font-size: 30px;
            }
        </style>
        <meta charset="UTF-8">
        <title>글목록창</title>
    </head>

    <body>
        <table align="center" border="1" width="80%">
            <tr height="10" align="center" bgcolor="lightgreen">
                <td>글번호</td>
                <td>작성자</td>
                <td>제목</td>
                <td>작성일</td>
            </tr>
            <c:choose>
                <c:when test="${empty articlesList}">
                    <tr height="10">
                        <td colspan="4">
                            <p align="center">
                                <b><span style="font-size:9pt;">등록된 글이 없습니다.</span></b>
                            </p>
                        </td>
                    </tr>
                </c:when>
                <c:when test="${!empty articlesList}">
                    <c:forEach var="article" items="${articlesList }" varStatus="articleNum">
                        <tr align="center">
                            <td width="5%">${articleNum.count}</td>
                            <td width="10%">${article.id }</td>
                            <td align='left' width="35%">
                                <span style="padding-right:30px"></span>
                                <c:choose>
                                    <c:when test='${article.level > 1 }'>
                                        <c:forEach begin="1" end="${article.level }" step="1">
                                            <span style="padding-left:10px"></span>
                                        </c:forEach>
                                        <span style="font-size:12px;">[답변]</span>
                                        <a class='cls1' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title}</a>
                                    </c:when>
                                    <c:otherwise>
                                        <a class='cls1' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title }</a>
                                    </c:otherwise>
                                </c:choose>
                            </td>
                            <td width="10%">
                                <fmt:formatDate value="${article.writeDate}" />
                            </td>
                        </tr>
                    </c:forEach>
                </c:when>
            </c:choose>
        </table>

        <div class="cls2">
            <c:if test="${totArticles != null }"> <%-- 전체 글 수에 따라 페이징 표시를 다르게 한다. --%>
                <c:choose>
                    <c:when test="${totArticles >100 }">
                        <!-- 글 개수가 100 초과인경우 -->
                        <c:forEach var="page" begin="1" end="10" step="1">
                            <c:if test="${section >1 && page==1 }">
                                <a class="no-uline" href="${contextPath }/board/listArticles.do?section=${section-1}&pageNum=${(section-1)*10 +1 }">&nbsp; pre </a>
                            </c:if> <%-- 섹션 값 2부터는 앞 섹션으로 이동할 수 있는 pre를 표시한다. --%>
                            <a class="no-uline" href="${contextPath }/board/listArticles.do?section=${section}&pageNum=${page}">${(section-1)*10 +page } </a>
                            <c:if test="${page == 10 }">
                                <a class="no-uline" href="${contextPath }/board/listArticles.do?section=${section+1}&pageNum=${section*10+1}">&nbsp; next</a>
                            </c:if> <%-- 페이지 번호 10 오른쪽에는 다음 섹션으로 이동할 수 있는 next를 표시한다. --%>
                        </c:forEach>
                    </c:when>
                    <c:when test="${totArticles ==100 }"> <%-- 전체 글 수가 100개일 때는 첫 번째 섹션의 10개 페이지만 표시하면 된다. --%>
                        <!--등록된 글 개수가 100개인경우  -->
                        <c:forEach var="page" begin="1" end="10" step="1">
                            <a class="no-uline" href="#">${page } </a>
                        </c:forEach>
                    </c:when>

                    <c:when test="${totArticles< 100 }"> <%-- 전체 글 수가 100개보다 적을 때 페이징을 표시한다. --%>
                        <!--등록된 글 개수가 100개 미만인 경우  -->
                        <c:forEach var="page" begin="1" end="${totArticles/10 +1}" step="1"> <%-- 글 수가 100개가 되지 않으므로 표시되는 페이지는 10개가 되지 않고, 전체 글 수를 10으로 나누어 구한 몫에 1을 더한 페이지까지 표시된다. --%>
                            <c:choose>
                                <c:when test="${page==pageNum }">
                                    <a class="sel-page" href="${contextPath }/board/listArticles.do?section=${section}&pageNum=${page}">${page } </a>
                                </c:when> <%-- 페이지 번호와 컨트롤러에서 넘어온 pageNum이 같은 경우 페이지 번호를 빨간색으로 표시하여 현재 사용자가 보고 있는 페이지임을 알린다. --%>
                                <c:otherwise>
                                    <a class="no-uline" href="${contextPath }/board/listArticles.do?section=${section}&pageNum=${page}">${page } </a> <%-- 페이지 번호를 클릭하면 section 값과 pageNum 값을 컨트롤러로 전송한다. --%>
                                </c:otherwise>
                            </c:choose>
                        </c:forEach>
                    </c:when>
                </c:choose>
            </c:if>
        </div>
        <br><br>
        <a class="cls1" href="${contextPath}/board/articleForm.do">
            <p class="cls2">글쓰기</p>
        </a>
    </body>

</html>

6. 다음 주소로 요청하여 실행 결과를 확인한다.

- http://localhost:8090/pro17/board/listArticles.do

전체 글 수에 대해 총 두 페이지가 표시된다.

7. 여기서 두 번째 페이지인 [2]를 클릭하면 http://localhost:8090/pro17/board/listArticles.do?section=1&pageNum=2 로 요청한다.

8. 글 수를 좀 더 늘려보자. BoardService 클래스에서 totArticles를 170으로 설정한다.

9. 브라우저에서 글 목록창을 출력한다. 전체 글 수가 100개를 넘으므로 next가 표시된 것을 볼 수 있다.

전체 글 수가 100개를 넘으므로 next가 표시된다.

* 지금까지 답변형 게시판을 구현해 봤다. 지면의 한계로 소스 코드 중 중요한 부분 위주로 살펴보았다.

* 게시판 기능은 모든 웹 프로그래밍의 기본이므로 게시판 기능만 구현할 수 있다면 다른 기능도 쉽게 구현할 수 있을 것이다.