본문 바로가기
728x90
반응형

이번에 구현할 기능 목표

 

게시글 리스트 출력 및 페이징

 


 

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>&nbsp;
    	</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>&nbsp;
		</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>&nbsp;
    	</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>&nbsp;
    	</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>&nbsp;
		</c:forEach>
    	<c:if test="${nextPage ne 0}">
		    <a href="${pageContext.request.contextPath}?searchWord=${param.searchWord}&searchType=${param.searchType}&pageNum=${nextPage}">다음</a>&nbsp;
    	</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>&nbsp;
    	</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>&nbsp;
		</c:forEach>
    	<c:if test="${nextPage ne 0}">
		    <a href="${pageContext.request.contextPath}?startDate=${param.startDate}&endDate=${param.endDate}&pageNum=${nextPage}">다음</a>&nbsp;
    	</c:if>
    </c:when>
    
    <c:otherwise>
    	<c:if test="${prevPage ne 0}">
		    <a href="${pageContext.request.contextPath}?pageNum=${prevPage}">이전</a>&nbsp;
    	</c:if>
		<c:forEach var="i" begin="${beginPage}" end="${endPage}">
		    <a href="${pageContext.request.contextPath}?pageNum=${i}">${i}</a>&nbsp;
		</c:forEach>
    	<c:if test="${nextPage ne 0}">
		    <a href="${pageContext.request.contextPath}?pageNum=${nextPage}">다음</a>&nbsp;
    	</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 에 담은 여러 객체들을 ${} 형태로 꺼내어 사용하였다.

 

728x90
반응형

한걸음 한걸음

개인적인 기록