이번에 구현할 기능 목표
게시글 리스트 출력 및 페이징
1. 테이블 생성
게시글을 저장하기 위한 테이블생성이 필요하다.
dbeaver, workbench 등 db에 접속해서 query 를 날릴 수 있는 상태를 만들어 둔다. 직접 접속해도되고
객체 테이블 생성은 GPT 도움받으니 편하네..
CREATE TABLE Posts ( num INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(255) NOT NULL, author NVARCHAR(100) NOT NULL, contents NVARCHAR(MAX) NOT NULL, date DATETIME DEFAULT GETDATE() );
IDENTITY 가 sequence 개념으로 알아서 1씩 증가시켜준다.
date는 저장시점시 시각이 기록되니 따로 넣지않아도
* Controller, Service, Dao, VO 생성
스프링에서 기본 구성인 위 4가지 class 를 통해 게시판 기능을 구현해본다.
1. Controller 생성
URL Mapping 을 할수있는 Controller를 생성한다.
com.example.controller 아래 PostController.java 를 생성한다.
//컨트롤러 명시 @Controller public class PostContoller { @Autowired PostListService postListService; ModelAndView mav = new ModelAndView(); @GetMapping("/") public ModelAndView postList(HttpServletRequest request) { //검색에서 받아올 파라미터들 저장 String searchWord = request.getParameter("searchWord"); String searchType = request.getParameter("searchType"); String startDate = request.getParameter("startDate"); String endDate = request.getParameter("endDate"); PostResult pResult = new PostResult(); SearchParameter sp = new SearchParameter(); Map params = new HashMap(); //페이징 위해서 int pageNum = 0; int totalPageNum = 0; if(request.getParameter("pageNum") !=null && !request.getParameter("pageNum").isEmpty()) { pageNum = Integer.parseInt(request.getParameter("pageNum"))-1; } //넘길 객체에 페이지 넘버 저장 params.put("pageNum", pageNum); //searchParameter if (searchWord != null || startDate != null || endDate != null) { sp.setEndDate(endDate); sp.setSearchType(searchType); sp.setSearchWord(searchWord); sp.setStartDate(startDate); pResult = postListService.getList(sp, pageNum*10); } else { pResult = postListService.getList(pageNum*10); } //글 목록 mav.addObject("list", pResult.getPostList()); //페이지넘버들 mav.addObject("curPageNum", pageNum+1); mav.addObject("totalPageNum", pResult.getTotalPostNum()); mav.addObject("param", sp); //List jsp 페이지 반환 mav.setViewName("List"); return mav; } }
2. Service 생성
package com.example.post.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.example.post.dao.PostListDao; import com.example.post.model.PostResult; import com.example.post.model.SearchParameter; @Service public class PostListService{ @Autowired PostListDao postListDao; public PostResult getList(int pageNum){ PostResult pResult = new PostResult(); pResult.setPostList(postListDao.selectTotalList(pageNum)); int totalPosts = postListDao.selectTotalPostCount(); pResult.setTotalPostNum((int) Math.ceil((double) totalPosts / 10)); return pResult; } public PostResult getList(SearchParameter sp, int pageNum){ PostResult pResult = new PostResult(); pResult.setPostList(postListDao.selectList(pageNum, sp)); //페이징을 위한 코드. 한 페이지당 게시글 10개씩 표시를 위해 10으로 나누었음. pResult.setTotalPostNum((int) Math.ceil((double) postListDao.selectPostCount(sp) / 10)); return pResult; } }
검색조건이 있을때, 없을때를 위해 두개의 메서드를 작성하였다.
3. Model (VO) 객체 생성
package com.example.post.model; import lombok.Data; @Data public class Post { private String num; private String title; private String author; private String contents; private String date; }
package com.example.post.model; import java.util.List; import lombok.Data; @Data public class PostResult { private List<Post> postList; private int totalPostNum; }
게시글 내용이 들어갈 Post.java 와 게시글 목록을 위한 PostResult.java 두가지
4. Dao 작성
package com.example.post.dao; import java.util.HashMap; import java.util.List; import java.util.Map; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import com.example.post.model.Post; import com.example.post.model.SearchParameter; import lombok.extern.slf4j.Slf4j; @Repository public class PostListDao { @Autowired private SqlSessionTemplate sqlSession; public List<Post> selectTotalList(int pageNum) { return sqlSession.selectList("PostMapper.selectTotalList", pageNum); } public List<Post> selectList(int pageNum, SearchParameter sp) { Map<String, Object> params = new HashMap<>(); params.put("pageNum", pageNum); params.put("sp", sp); return sqlSession.selectList("selectList", params); } public int selectTotalPostCount() { return sqlSession.selectOne("PostMapper.selectTotalPostCount"); } public int selectPostCount(SearchParameter sp) { Map<String, Object> params = new HashMap<>(); params.put("sp", sp); return sqlSession.selectOne("PostMapper.selectPostCount", params); } }
페이징을 위해 전체 Count 수가 필요하고, 전체카운트수 외에 Post 정보들을 가져와야한다.
한번에 할수있을거같긴한데 아직 실력이 부족함
5. Dao 에서 연결할 Mapper 추가
mapper 는 resources 아래 만든 mapper 폴더에 .xml 파일로 만들면 된다.
이름을 자유롭게 지어도 됨. (application.properties 에 classpath:mapper/*.xml 으로 설정해두었기 때문)
나는 mapper-post.xml 라고 작성하였음.
<?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="PostMapper"> <!-- TEST1 이 현재 내 게시판 테이블임.. --> <select id="selectTotalList" parameterType="int" resultType="com.example.post.model.Post"> SELECT * FROM TEST1 ORDER BY t.num DESC OFFSET #{pageNum} ROWS FETCH NEXT 10 ROWS ONLY </select> <select id="selectList" parameterType="map" resultType="com.example.post.model.Post"> SELECT * FROM TEST1 <where> <if test="sp.startDate != null and !sp.startDate.isEmpty()"> AND CONVERT(varchar, DATE, 23) BETWEEN #{sp.startDate} AND #{sp.endDate} </if> <if test="sp.searchWord != null and !sp.searchWord.isEmpty()"> AND ${sp.searchType} LIKE CONCAT('%', #{sp.searchWord}, '%') </if> </where> ORDER BY num DESC OFFSET #{pageNum} ROWS FETCH NEXT 10 ROWS ONLY </select> <select id="selectTotalPostCount" resultType="int"> SELECT COUNT(*) AS postCount FROM TEST1 </select> <select id="selectPostCount" parameterType="map" resultType="int"> SELECT COUNT(*) AS postCount FROM TEST1 <where> <if test="sp.startDate != null and !sp.startDate.isEmpty()"> AND CONVERT(varchar, DATE, 23) BETWEEN #{sp.startDate} AND #{sp.endDate} </if> <if test="sp.searchWord != null and !sp.searchWord.isEmpty()"> AND ${sp.searchType} LIKE CONCAT('%', #{sp.searchWord}, '%') </if> </where> </select>
mapper namespace 에 PostMapper 로 지정해 놓았기 때문에 dao 에서도 PostMapper.selectTotalList 등으로 호출이 가능하다.
6. 화면에 표시할 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> <h2><a href="${pageContext.request.contextPath}/">조회화면</a></h2><hr> 검색 <form action="${pageContext.request.contextPath}/" onsubmit="return checkDate()"> <select name="searchType"> <option value="title">제목</option> <option value="contents">내용</option> <option value="author">작성자</option> </select> 시작일자 <input type="date" id="startDate" name="startDate"> 종료일자 <input type="date" id="endDate" name="endDate"> <input type="text" name="searchWord"> <input type="submit" value="검색" /> </form> <a href="${pageContext.request.contextPath}/Write">글쓰기</a> <table> <tr> <td>게시글번호</td><td>제목</td><td>작성자</td><td>날짜</td> <!-- 5개 --> </tr> <c:forEach items="${list}" var="post"> <tr> <td>${post.num}</td><td><a href="${pageContext.request.contextPath}/Content?num=${post.num}">${post.title}</a></td><td>${post.author}</td><td>${post.date}</td> </tr> </c:forEach> </tr> </table> <hr> <c:set var="beginPage" value="${curPageNum - (curPageNum % 10)}" /> <c:set var="endPage" value="${beginPage + 10 <= totalPageNum ? beginPage + 9 : totalPageNum}" /> <c:set var="beginPage" value="${beginPage == 0 ? 1 : beginPage}" /> <c:set var="prevPage" value="${beginPage < 9 ? 0 : beginPage-1}" /> <c:set var="nextPage" value="${endPage < totalPageNum - (totalPageNum % 10) ? (beginPage == 1 ? beginPage + 9 : beginPage + 10) : 0}" /> <!-- beginPage 가 1인경우는 한자리수인데, 0부터 시작할수는 없어서 1부터 시작하므로 첫페이지 다음 버튼은 9를 더해야함. 그 뒤로는 10부터 시작이라 10씩 더해야 자릿수가 바뀜 --> <c:choose> <c:when test="${not empty param.searchWord and not empty param.startDate}"> <c:if test="${prevPage ne 0}"> <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${prevPage}">이전</a> </c:if> <c:forEach var="i" begin="${beginPage}" end="${endPage}"> <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${i}">${i}</a> </c:forEach> <c:if test="${nextPage ne 0}"> <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${nextPage}">다음</a> </c:if> </c:when> <c:when test="${not empty param.searchWord}"> <c:if test="${prevPage ne 0}"> <a href="${pageContext.request.contextPath}?searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${prevPage}">이전</a> </c:if> <c:forEach var="i" begin="${beginPage}" end="${endPage}"> <a href="${pageContext.request.contextPath}?searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${i}">${i}</a> </c:forEach> <c:if test="${nextPage ne 0}"> <a href="${pageContext.request.contextPath}?searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${nextPage}">다음</a> </c:if> </c:when> <c:when test="${not empty param.startDate}"> <c:if test="${prevPage ne 0}"> <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&pageNum=${prevPage}">이전</a> </c:if> <c:forEach var="i" begin="${beginPage}" end="${endPage}"> <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&pageNum=${i}">${i}</a> </c:forEach> <c:if test="${nextPage ne 0}"> <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&pageNum=${nextPage}">다음</a> </c:if> </c:when> <c:otherwise> <c:if test="${prevPage ne 0}"> <a href="${pageContext.request.contextPath}?pageNum=${prevPage}">이전</a> </c:if> <c:forEach var="i" begin="${beginPage}" end="${endPage}"> <a href="${pageContext.request.contextPath}?pageNum=${i}">${i}</a> </c:forEach> <c:if test="${nextPage ne 0}"> <a href="${pageContext.request.contextPath}?pageNum=${nextPage}">다음</a> </c:if> </c:otherwise> </c:choose> <script> function checkDate() { const startDate = document.getElementById("startDate").value; const endDate = document.getElementById("endDate").value; if (startDate && !endDate) { alert("종료일자를 입력해주세요."); return false; } if (!startDate && endDate) { alert("시작일자를 입력해주세요."); return false; } if (startDate && endDate && new Date(startDate) > new Date(endDate)) { alert("시작일자가 종료일자보다 늦을 수 없습니다."); return false; } return true; } </script> </body> </html>
jstl 을 이용했다. css 의 경우 이 코드들고가서 ai 에게 css 간단하게 짜달라고 하면 짜주니 바로 적용이 가능하다.
주 기능으로 검색기능과 페이징을 구현하였다.
controller 에서 mav 에 담은 여러 객체들을 ${} 형태로 꺼내어 사용하였다.