본문 바로가기
스프링(Spring)/게시판 만들기

[Spring 게시판] 목록(검색, 페이징, 정렬, 게시글 수, 말줄임표, 마우스오버 시 전체 제목, 등록자 표시)

by una⭐ 2020. 3. 12.

 

1. Controller(흐름제어)

 

BoardController

 

@Controller
public class BoardController {

	@Qualifier("BoardService")
	@Autowired
	BoardService boardService;
	
	// 게시글 목록, 검색, 페이징
	@RequestMapping(value = "/list", method = RequestMethod.GET)
	public ModelAndView list(Criteria cri) throws Exception {
		
		ModelAndView mv = new ModelAndView("list");
		
		cri.setCategory(current);
		PageMaker pageMaker = new PageMaker();
		pageMaker.setCri(cri);  
		
		pageMaker.setTotalCount(boardService.countPostListSearch(cri));
		 		
		if(boardService.countPostListSearch(cri)>0) {
			List<BoardVO> postList = boardService.listAllSearch(cri);
			mv.addObject("postList", postList);
		}
		
		mv.addObject("pageMaker", pageMaker);

		return mv;
		
	}
}

 

2. Service(비즈니스 로직, DB 연동 이외의 작업)

 

BoardService

 

public interface BoardService {
	
	//게시글 목록, 검색, 페이징
	public List<BoardVO> listAllSearch(Criteria cri) throws Exception;
	
	//페이징 전체 목록 개수 
	public int countPostListSearch(Criteria cri) throws Exception;
    
}

 

BoardServiceImpl

 

@Service("BoardService")
public class BoardServiceImpl implements BoardService {

	@Autowired
	BoardDao boardDao;

	//게시글 목록, 검색, 페이징
	@Override
	public List<BoardVO> listAllSearch(Criteria cri) throws Exception {
          
	    return boardDao.listAllSearch(cri);
        
	}
	
	//페이징 전체 목록 개수 
	@Override
	public int countPostListSearch(Criteria cri) throws Exception {
		    
		return boardDao.countPostListSearch(cri);
        
	}
}

 

3. Model(비즈니스 로직, DB 연동 작업)

 

BoardVO(게시판 데이터 저장 클래스)

 

public class BoardVO {
	
	private int boardSeq;
	private String category;
	private String title;
	private String content;
	private String file;
	private Timestamp register_date;
	private int hit;
	private String comment_yn;
	private String delete_yn;
	private int userSeq;
	
	private String checkTitle;
	
	//DB 컬럼은 아니지만, 댓글수를 가져올 때 별칭을 comment로 붙여서 가져왔음
	private int comment;	
	//DB 컬럼은 아니지만, 사용자 이름(등록자 또는 작성자)을 가져올 때 별칭을 name으로 붙여서 가져왔음
	private String writer;	
	
	//DB 컬럼은 아니지만 write페이지에서 file 저장
	private MultipartFile p_file;
	//DB 컬럼은 아니지만 서버에 저장된 uuid 파일 이름의 별칭을 storedFileName로 붙여서 가져왔음. view페이지에서 이용. 
	private String storedFileName;
	
	/***********getter, setter***********/
    
	@Override
	public String toString() {
		return "BoardVO [boardSeq=" + boardSeq + ", category=" + category + ", title=" + title + ", content=" + content
				+ ", file=" + file + ", register_date=" + register_date + ", hit=" + hit + ", comment_yn=" + comment_yn
				+ ", delete_yn=" + delete_yn + ", userSeq=" + userSeq + ", checkTitle=" + checkTitle + ", comment="
				+ comment + ", writer=" + writer + ", p_file=" + p_file + "]";
	}
	
}

 

Criteria(페이징 데이터 저장 클래스)

 

package com.ncomz.sample.dto;

import org.springframework.web.util.UriComponentsBuilder;

public class Criteria {
    
	private int page;
	private int perPageNum = 10;	//원하는 페이징 끝 번호
	
	private String option;
	private String keyword;
	
	private String category;	//list 출력 시 category 지정

	public int getPageStart() {
		return (this.page-1)*perPageNum;
	}

	public Criteria() {

		this.page = 1;
		this.option = null;
		this.keyword = null;
		
	}
	
	public int getPage() {
		return page;
	}

	public void setPage(int page) {
		
		if(page <= 0) {
			
			this.page = 1;
			
		} else {
			
			this.page = page;
			
		}	
	}
	
	public int getPerPageNum() {
		
		return perPageNum;
		
	}
	
	public void setPerPageNum(int perPageNum) {
		
		this.perPageNum = perPageNum;
		
	}
	public String getKeyword() {
		return keyword;
	}

	public void setKeyword(String keyword) {
		this.keyword = keyword;
	}
	
	public String getOption() {
		return option;
	}

	public void setOption(String option) {
		this.option = option;
	}
	
	//
	public String getCategory() {
		return category;
	}

	public void setCategory(String category) {
		this.category = category;
	}
	//

	public String makeQuery() {
		UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance().queryParam("page", page).queryParam("perPageNum", this.perPageNum);
				
		if (option!=null) {
			
			uriComponentsBuilder.queryParam("option", this.option).queryParam("keyword", this.keyword);
			
		}
		
		return uriComponentsBuilder.build().encode().toString();
	}
	
	
	@Override
	public String toString() {
		return "Criteria [page=" + page + ", perPageNum=" + perPageNum + ", option=" + option + ", keyword="
				+ keyword + "]";
	}
}

 

PageMaker(페이징 데이터 저장 클래스)

 

package com.ncomz.sample.dto;

import org.springframework.web.util.UriComponentsBuilder;

public class PageMaker {

	private Criteria cri;
	private int totalCount;
	private int startPage = 1;
	private int endPage;
	private boolean prev;
	private boolean next;
	private int displayPageNum = 10;

	public Criteria getCri() {
		return cri;
	}

	public void setCri(Criteria cri) {
		this.cri = cri;
	}

	public int getTotalCount() {
		return totalCount;
	}

	public void setTotalCount(int totalCount) {
		this.totalCount = totalCount;
		calcData();
	}

	private void calcData() {

		endPage = (int) (Math.ceil(cri.getPage() / (double) displayPageNum) * displayPageNum);

		startPage = (endPage - displayPageNum) + 1;
		if (startPage <= 0)
			startPage = 1;

		int tempEndPage = (int) (Math.ceil(totalCount / (double) cri.getPerPageNum()));
		if (endPage > tempEndPage) {
			endPage = tempEndPage;
		}

		prev = startPage == 1 ? false : true;

		next = endPage * cri.getPerPageNum() < totalCount ? true : false;

	}

	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 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 getDisplayPageNum() {
		return displayPageNum;
	}

	public void setDisplayPageNum(int displayPageNum) {
		this.displayPageNum = displayPageNum;
	}
	

	public String makeQuery(int page) {
	 
		UriComponentsBuilder uriComponentsBuilder = 
				UriComponentsBuilder.newInstance().queryParam("page", page).queryParam("perPageNum", this.cri.getPerPageNum());
	 
		if (this.cri.getOption() != null) {
			uriComponentsBuilder.queryParam("option", this.cri.getOption()).queryParam("keyword", this.cri.getKeyword());
		}
	  
		return uriComponentsBuilder.build().encode().toString();
	 
	 }
     
}

 

BoardDao

 

public interface BoardDao {

	public List<BoardVO> listAllSearch(Criteria cri) throws Exception;
    
	public int countPostListSearch(Criteria cri) throws Exception;

}

 

BoardDaoImpl

 

@Repository
public class BoardDaoImpl implements BoardDao {

	SqlSession sqlSession;
	
	//게시글 목록, 검색, 페이징
	@Override
	public List<BoardVO> listAllSearch(Criteria cri) throws Exception {
		
		return sqlSession.selectList("com.ncomz.sample.dao.BoardDao.listAllSearch", cri); 
	
    }
	
	//검색 페이징
	@Override
	public int countPostListSearch(Criteria cri) throws Exception {

		return sqlSession.selectOne("com.ncomz.sample.dao.BoardDao.countPostListSearch", cri);
	
    }
}

 

BoardDao.xml(SQL문)

 

<?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="com.ncomz.sample.dao.BoardDao">
   
   <sql id="search">
      <if test="keyword != null">
         <choose>
            <when test="option == 'all'.toString()">
               AND
               (title like CONCAT('%', #{keyword}, '%')
               OR
                  content like CONCAT('%', #{keyword}, '%'))
            </when>
            <when test="option == 'p_content'.toString()">
               AND
               (title like CONCAT('%', #{keyword}, '%')
               OR
                  content like CONCAT('%', #{keyword}, '%'))
            </when>
            <otherwise>
               AND
                  ${option} like CONCAT('%', #{keyword}, '%')
            </otherwise>
         </choose>
      </if>
   </sql>
   
   <!-- 검색 목록 -->
   <select id="listAllSearch" parameterType="com.ncomz.sample.dto.Criteria" resultType="com.ncomz.sample.dto.BoardVO">
      SELECT
          boardSeq,
          title,
          register_date,
          u.name as writer,
          hit,
          delete_yn,
          (SELECT COUNT(*) FROM TB_JMJ_COMMENT WHERE boardSeq = b.boardSeq) AS comment
      FROM TB_JMJ_USER as u
      JOIN TB_JMJ_BOARD as b
      ON u.userSeq = b.userSeq
      WHERE
          delete_yn = 'n'
          AND category = #{category}
          <include refid = "search"></include>
      ORDER BY
          boardSeq desc
      LIMIT #{pageStart}, #{perPageNum}
   </select>
   
   <!-- 페이징 검색 게시글 개수 -->
   <select id="countPostListSearch" parameterType="com.ncomz.sample.dto.Criteria" resultType="int">
      SELECT
            count(*)
        FROM
            TB_JMJ_BOARD
        WHERE
           delete_yn = 'n'
           AND category = #{category}
            <include refid = "search"></include>   
   </select>
   
</mapper>

 

4. View(화면)

 

list.jsp

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ page session="false" %>
<%    request.setCharacterEncoding("utf-8");
   response.setContentType("text/html;charset=UTF-8"); %>
<html>
<head>
<title>게시판</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<%
     String pageNum = request.getParameter("page");
%>
<script type="text/javascript">

   var clicktitle = "unsorted";
   var clickwriter = "unsorted";
   var clickhit = "unsorted";
   var clickdate = "unsorted";
   
   function Sorted(id){
      
      var SortedResult = new Array();
      
      <c:forEach items="${postList}" var = "postListTitle">
      
         console.log("${postListTitle.boardSeq}");
         SortedResult.push({
            title: "${postListTitle.title}" , 
            comment: "${postListTitle.comment}" ,
               hit: "${postListTitle.hit}",
               writer: "${postListTitle.writer}",
               register_date: "<fmt:formatDate value='${postListTitle.register_date}' pattern='yyyy-MM-dd a hh:mm:ss'/>" ,
               boardSeq: "${postListTitle.boardSeq}"
            });
   
      </c:forEach>
      
      var name = id;

      if(name == "clicktitle"){
         objSort(SortedResult,"title", id);
         tagParsing(SortedResult);
      }else if(name == "clickwriter"){
         objSort(SortedResult,"writer", id);
         tagParsing(SortedResult);
      }else if(name == "clickhit"){
         objSortNum(SortedResult,"hit", id);
         tagParsing(SortedResult);
      }else if(name == "clickdate"){
         objSort(SortedResult,"register_date", id);
         tagParsing(SortedResult);
      }
          
   }
   
   //objSort 함수 
   function objSort(obj, sort, id){
      
      if(eval(id) == "unsorted"){
         obj.sort(function(a,b){
            if(a[sort] > b[sort]) return 1;
            else if(a[sort] < b[sort]) return -1;
         });//sort
         eval(id +'= "sorted"');
      } else{
         obj.sort(function(a,b){
            if(a[sort] > b[sort]) return -1;
               else if(a[sort] < b[sort]) return 1;
         });//sort
         
         eval(id +'= "unsorted"');
         
      } 
      
   }
   
   //objSortNum (조회수)
   function objSortNum(obj, sort, id){
      
      if(eval(id) == "unsorted"){
         obj.sort(function(a,b){
         return a[sort] - b[sort]
         });//sort
         eval(id +'= "sorted"');
      } else{
         obj.sort(function(a,b){
            return b[sort] - a[sort]
         });//sort
         
         eval(id +'= "unsorted"');
         
      }
           
   }
   
   //tagParsing 함수 function tagParsing(obj, init)
   function tagParsing(obj){
      var title=0;
      var comment=0;
      var writer=0;
      var hit=0;
      var register_date=0;
      var boardSeq=0;
      var pageNum= document.getElementById("pageNumber").value;

      if(pageNum == null || pageNum == "" || isNaN(pageNum)){
         pageNum = 1;
      } 
        
      var seq = (pageNum-1)*10;
      
      var len = obj.length;
      var tag =  "<colgroup><col width='50'><col width='200'><col width='80'><col width='50'><col width='80'></colgroup>";
      
      for(i=0; i<obj.length ; i ++){
         
         var seqNumber = seq + i + 1; 
         
         tag+= "<tr><td align='center'>" 
         + "<input type='hidden' id='pageNumber' name='pageNumber' value=<%=pageNum %> />"
          +  seqNumber +  "</td>"
         +"<td id='title'>" + "<a href='/view?boardSeq=" + obj[i].boardSeq + "'>"+ obj[i].title + "(" + obj[i].comment + ")" +"</a>" +"</td>"
         + "<td id='writer'>" + obj[i].writer  + "</td>"
         + "<td id='hit'>" + obj[i].hit + "</td>"
         + "<td id='date'>" + obj[i].register_date + "</td></tr>";
         
         title += obj[i].title;
          comment += obj[i].comment;
         writer += obj[i].writer;
         hit += obj[i].hit;
         register_date += obj[i].register_date;
         boardSeq += obj[i].boardSeq;
      
      }//for
      
      document.getElementById("data").innerHTML = tag; 
      
   }
   
   //제목 중복 체크
   window.onload = function() {      
      
      var form = document.form1;
               
      if('${writeResult}'=='1'){
         alert('등록되었습니다.');
         location.href="list?current=${current}";
      }       
      if('${postDelResult}'=='1'){
         alert('게시글이 삭제되었습니다.');
         location.href="list?current=${current}";
      } 
           
      //라디오 버튼 값
      var radioVal = ${perPageNum};
      
           if(radioVal == '5'){
              
              $('input:radio[name=perPageNum]:input[value="10"]').attr("checked", false);
              $('input:radio[name=perPageNum]:input[value="5"]').attr("checked", true);
              
           }else if(radioVal == '20'){
              
              $('input:radio[name=perPageNum]:input[value="10"]').attr("checked", false);
              $('input:radio[name=perPageNum]:input[value="20"]').attr("checked", true);
              
           } else{
              
              $('input:radio[name=perPageNum]:input[value="10"]').attr("checked", true);
              
           }
           
           //말줄임표 (제목)
           $(".ellipsisTitle").each(function(){

               var length = 14; //표시할 글자수 정하기   
               
               $(this).each(function(){

                   if( $(this).text().length > length ){
               
                      //지정할 글자수 이후 표시할 텍스트
                       $(this).text($(this).text().substr(0,length)+'...'); 

                   }

               });

           });
           
           //말줄임표 (작성자)
           $(".ellipsisWriter").each(function(){

               var length = 4; //표시할 글자수 정하기   
               
               $(this).each(function(){

                   if( $(this).text().length > length ){
               
                      //지정할 글자수 이후 표시할 텍스트
                       $(this).text($(this).text().substr(0,length)+'...'); 

                   }

               });

           });
           
           //말줄임 된 것만 찾아서 툴팁 표시
           /* $('.ellipsisTitle').each(function () {
              
               if (this.offsetWidth < this.scrollWidth){
                  
                  $(this).attr('title', $(this).text());
                  
               }
               
           }); */ 
           
           console.log('${writeResult}'+"*****************");

       //탭 메뉴 색 변경
       if('${current}' == 'dance'){
          
          $('#currentDashboard').removeClass('current');
          
         $('#currentDance').css({
            
            "background-color": "#2baae1",
            "color": "#222"
            
         });
          
       } else if('${current}' == 'movie'){
          
         $('#currentDashboard').removeClass('current');
          
         $('#currentMovie').css({
            
            "background-color": "#2baae1",
            "color": "#222"
            
         });
          
       }
           
   }
</script>
<style type="text/css">
   div.container{
      text-align: center;
      width: 100%;
   }
    table {
      font-family: "Lato","sans-serif";
      font-size: 12px;
      width: 60%;
      border-collapse: collapse;
      margin-top: 10px;
      table-layout: fixed;
      margin-right: auto;
      margin-left: auto;
   }    
   th {
      width: 80px;
      height: 25px;
      text-align: center;                 
      background-color: #2baae1;
      color: white;
   }
   td {
      height: 25px;
   }
   a {
      text-decoration: none;
      color: #000000;
   }
   .pageNumberLi {
      list-style: none; 
      float: left; 
      padding: 6px;
   }
   /* after 위치를 이 요소의 기준으로 상대적으로 잡아준다. */
   [data-tooltip-text]:hover {
      position: relative;
   }
   /* 가상 선택자를 사용하여 가상으로 자녀 요소를 생성한다. */
   [data-tooltip-text]:after {
      /* bottom 값과 opacity 값에 대해 애니메이션 효과를 준다.  */
      transition: bottom .3s ease-in-out, opacity .3s ease-in-out;
      background-color: rgba(28, 166, 188, 0.8);
      box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
      border-radius: 5px;
      color: #FFFFFF;
      font-size: 12px;
      margin-bottom: 10px;
      padding: 7px 12px;
      /* 부모 요소를 기준으로 설정하게 된다. */
      position: absolute;
      width: auto;
      min-width: 50px;
      max-width: 300px;
      /* 요소의 경계에서 break point가 아니어도 줄바꿈을 한다. */
      word-wrap: break-word;
   
      z-index: 9999;
   
      opacity: 0;
      left: -9999px;
      top: 90%;
      
      /* 툴팁의 내용을 넣어준다. */
      content: attr(data-tooltip-text);
   }  
   [data-tooltip-text]:hover:after {
      top: 130%;
      left: 0;
      opacity: 1;
   }
</style>
<script src="https://code.jquery.com/jquery-1.10.2.js"></script>
</head>
<body>

<jsp:include page="/WEB-INF/views/header.jsp" flush ="false"/>

<div class="container">
   
   <form method="get" action="/list" name="form1">
   
      <!-- 검색바 -->
      <table>
         <tr>
            <td>
               <select name="option">
                  <option value="all">전체검색</option>
                  <option value="title" <c:out value="${pageMaker.cri.option == 'title'?'selected':''}" />>제목</option>
                  <option value="content" <c:out value="${pageMaker.cri.option == 'content'?'selected':''}" />>제목+내용</option>
                  <option value="writer" <c:out value="${pageMaker.cri.option == 'writer'?'selected':''}" />>작성자</option>
               </select>
               <input type="text" name="keyword" value="${pageMaker.cri.keyword}" />
               <input type="hidden" name="current" value="${current}" />
               <input type="submit" value="검색"/>
             </td>
          </tr>
          <tr>
             <td>
                <div style="float:left; border: 0.3px solid #9F9D9D; width: 85px; height: 17px; vertical-align:middle; text-align:center;">게시글수</div>
                <div style="float:left;">
                   <input type="radio" name="perPageNum" value="5">5
                  <input type="radio" name="perPageNum" value="10" checked="checked">10
                  <input type="radio" name="perPageNum" value="20">20
                </div>
             </td>
          </tr>
       </table>
   </form>
   
   <!-- 게시판 글 목록 -->
   <table border="1" width="1200px">
      <colgroup>
         <col width="50">
            <col width="200">
            <col width="80">
            <col width="50">
            <col width="80">
      </colgroup>
      <tr>
         <th>번호</th>
         <th id="clicktitle" onclick="Sorted('clicktitle')">
                제목
         </th>
         <th id="clickwriter" onclick="Sorted('clickwriter')">
               등록자
         </th>
         <th id="clickhit" onclick="Sorted('clickhit')">
               조회수
         </th>
         <th id="clickdate" onclick="Sorted('clickdate')">
               등록일
         </th>
      </tr>
   </table>
   <table border="1" id="data" width="1200px">
      <colgroup>
         <col width="50">
            <col width="200">
            <col width="80">
            <col width="50">
            <col width="80">
      </colgroup>
      <c:choose>
         <c:when test="${fn:length(postList) == 0}">
            <tr>
               <td colspan="5" align="center">등록된 게시글이 없습니다.</td>
             </tr>
         </c:when>
         <c:otherwise>
            <c:forEach var="postList" items="${postList}" varStatus="status">
               <tr>
                  <td align="center">
                  <input type="hidden" id="pageNumber" name="pageNumber" value=<%= pageNum %> />
                     <!-- (현재 페이지 개수 - 1)*페이지당 보여줄 게시글 수 + 카운트 -->
                     ${pageMaker.cri.getPageStart() + status.count}
                  </td>
                  <td id="title">
                     <a href="/view?boardSeq=${postList.boardSeq}&current=${current}" data-tooltip-text="${postList.title}" class="ellipsisTitle">${postList.title}</a>
                     (${postList.comment})
                     
                  </td>
                  <td id="writer">
                     <div class="ellipsisWriter" data-tooltip-text="${postList.writer}" class="ellipsisWriter">${postList.writer}</div>
                  </td>
                  <td id="hit">${postList.hit}</td>
                  <td id="date">
                     <fmt:formatDate value="${postList.register_date}" pattern="yyyy-MM-dd a hh:mm:ss"/>
                  </td>
               </tr>
            </c:forEach>
         </c:otherwise> 
      </c:choose>
   </table>
   
   <!-- 글 작성 페이지로 넘어가는 버튼 -->
   <table width="1200px">
      <tr align="right">
         <td>
            <button type="button" id="btnwrite" onclick="location.href='write?current=${current}'">글 작성</button>
         </td>
      </tr>
   </table>
   
   <table>
      <tr>
         <td>
            <ul>
                <c:if test="${pageMaker.prev }">
                   <li class="pageNumberLi">
                       <a href='<c:url value="list${pageMaker.makeQuery(pageMaker.startPage-1)}&current=${current}"/>'><i>[이전]</i></a>
                   </li>
               </c:if>
               <c:forEach begin="${pageMaker.startPage }" end="${pageMaker.endPage }" var="pageNum">
                   <li class="pageNumberLi">
                      <a href='<c:url value="list${pageMaker.makeQuery(pageNum)}&current=${current}"/>'>&nbsp;&nbsp;&nbsp;${pageNum }&nbsp;&nbsp;&nbsp;</a>
                   </li>
               </c:forEach>
               <c:if test="${pageMaker.next && pageMaker.endPage >0 }">
                   <li class="pageNumberLi">
                      <a href='<c:url value="list${pageMaker.makeQuery(pageMaker.endPage+1) }&current=${current}"/>'><i>[다음]</i></a> 
                   </li>
                </c:if>
            </ul>
         </td>
      </tr>
   </table>
</div>
</body>
</html>

 

댓글