Spring - 게시판 만들기 연습 (댓글 페이징 처리, 비밀댓글 입력, 목록 처리)
1. 구현화면
01) 댓글 페이징 처리
게시판 페이징과 동일하게 처리(페이지당 게시물 수 : 10, 화면당 페이지 수 : 4)
02) 비밀댓글 입력폼
댓글 입력창은 로그인된 회원에게만 보이도록 처리
댓글 입력시 비밀댓글 선택 유무에 따라 DB에 'y'나 'n'가 입력되게 처리
03) 비밀 댓글 목록 처리
비회원, 비로그인시에는 비밀댓글은 안보이도록 처리
로그인, 해당게시물의 작성자는 모든 비밀댓글을 비밀댓글 작성자는 본인의 비밀댓글을 볼수 있도록 처리
2. 소스코드 리뷰
01) Controller(흐름제어)
ReplyController
로그인 여부 확인을 위해 session 추가, 페이징처리작업 추가, ModelAndView로 작업처리
package com.example.spring02.controller.board; import java.util.List; import javax.inject.Inject; import javax.servlet.http.HttpSession; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import com.example.spring02.model.board.dto.ReplyVO; import com.example.spring02.service.board.ReplyPager; import com.example.spring02.service.board.ReplyService; @RestController @RequestMapping("/reply/*") public class ReplyController { @Inject ReplyService replyService; // 댓글 입력 @RequestMapping("insert.do") public void insert(@ModelAttribute ReplyVO vo, HttpSession session){ String userId = (String) session.getAttribute("userId"); vo.setReplyer(userId); replyService.create(vo); } // 댓글 목록(@Controller방식 : veiw(화면)를 리턴) @RequestMapping("list.do") public ModelAndView list(@RequestParam int bno, @RequestParam(defaultValue="1") int curPage, ModelAndView mav, HttpSession session){ // **페이징 처리 int count = replyService.count(bno); // 댓글 갯수 ReplyPager replyPager = new ReplyPager(count, curPage); int start = replyPager.getPageBegin(); int end = replyPager.getPageEnd(); List<ReplyVO> list = replyService.list(bno, start, end, session); // 뷰이름 지정 mav.setViewName("board/replyList"); // 뷰에 전달할 데이터 지정 mav.addObject("list", list); mav.addObject("replyPager", replyPager); // replyList.jsp로 포워딩 return mav; } } | cs |
02) Service(비지니스 로직, DB연동 이외의 작업 처리)
ReplyService(인터페이스)
로그인여부 확인을 위해 session추가, 페이징 처리를 위한 파라미터 추가(int start, int end)
package com.example.spring02.service.board; import java.util.List; import javax.servlet.http.HttpSession; import com.example.spring02.model.board.dto.ReplyVO; public interface ReplyService { // 댓글 목록 public List<ReplyVO> list(Integer bno, int start, int end, HttpSession session); // 댓글 입력 public void create(ReplyVO vo); // 댓글 수정 public void update(ReplyVO vo); // 댓글 삭제 public void delete(Integer rno); // 댓글 갯수 public int count(int bno); } | cs |
ReplyServiceImpl(인터페이스 구현 클래스)
로그인, 비로그인 / 게시물작성자, 댓글작성자 조건에 따라 비밀댓글 목록 처리
package com.example.spring02.service.board; import java.util.List; import javax.inject.Inject; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Service; import com.example.spring02.model.board.dao.ReplyDAO; import com.example.spring02.model.board.dto.ReplyVO; @Service public class ReplyServiceImpl implements ReplyService { @Inject ReplyDAO replyDao; // 댓글 목록 @Override public List<ReplyVO> list(Integer bno, int start, int end, HttpSession session) { List<ReplyVO> items = replyDao.list(bno, start, end); // 세션에서 현재 사용자 id값 저장 String userId = (String) session.getAttribute("userId"); for(ReplyVO vo : items){ // 댓글 목록중에 중에 비밀 댓글이 있을 경우 if(vo.getSecretReply().equals("y")){ if(userId== null){ // 비로그인 상태면 비밀 댓글로 처리 vo.setReplytext("비밀 댓글입니다."); } else { // 로그인 상태일 경우 String writer = vo.getWriter(); // 게시물 작성자 저장 String replyer = vo.getReplyer(); // 댓글 작성자 저장 // 로그인한 사용자가 게시물의 작성자X 댓글 작성자도 X 비밀댓글로 처리 if(!userId.equals(writer) && !userId.equals(replyer)) { vo.setReplytext("비밀 댓글입니다."); } } } } return items; } // 댓글 작성 @Override public void create(ReplyVO vo) { replyDao.create(vo); } // 댓글 수정 @Override public void update(ReplyVO vo) { // TODO Auto-generated method stub } // 댓글 삭제 @Override public void delete(Integer rno) { // TODO Auto-generated method stub } // 댓글 갯수 @Override public int count(int bno) { return replyDao.count(bno); } } | cs |
ReplyPager(페이징 처리 클래스)
package com.example.spring02.service.board; public class ReplyPager { // 페이지당 게시물 수 public static final int PAGE_SCALE = 10; // 화면당 페이지 수 public static final int BLOCK_SCALE = 4; private int curPage; // 현재 페이수 private int prevPage; // 이전 페이지 private int nextPage; // 다음 페이지 private int totPage; // 전체 페이지 갯수 private int totBlock; // 전체 페이지 블록 갯수 private int curBlock; // 현재 페이지 블록 private int prevBlock; // 이전 페이지 블록 private int nextBlock; // 다음 페이지 블록 // WHERE rn BETWEEN #{start} AND #{end} private int pageBegin; // #{start} private int pageEnd; // #{end} // [이전] blockBegin -> 41 42 43 44 45 46 47 48 49 50 [다음] private int blockBegin; // 현재 페이지 블록의 시작번호 // [이전] 41 42 43 44 45 46 47 48 49 50 <- blockEnd [다음] private int blockEnd; // 현재 페이지 블록의 끝번호 // 생성자 // BoardPager(레코드 갯수, 현재 페이지 번호) public ReplyPager(int count, int curPage){ curBlock = 1; // 현재 페이지 블록 번호 this.curPage = curPage; // 현재 페이지 설정 setTotPage(count); // 전체 페이지 갯수 계산 setPageRange(); // setTotBlock(); // 전체 페이지 블록 갯수 계산 setBlockRange(); // 페이지 블록의 시작, 끝 번호 계산 } public void setBlockRange(){ // *현재 페이지가 몇번째 페이지 블록에 속하는지 계산 // (현재페이지-1)/페이지 블록단위+1 // 1페이지 => 1블록 (1-1)/10 + 1 => 1 // 9페이지 => 1블록 (9-1)/10 + 1 => 1 // 11페이지 => 2블록 (11-1)/10 + 1 => 2 // 57페이지 => 6블록 (57-1)/10 + 1 => 6 curBlock = (int)Math.ceil((curPage-1) / BLOCK_SCALE)+1; // *현재 페이지 블록의 시작, 끝 번호 계산 // 페이지 블록의 시작번호 // (현재블록-1)*블록단위+1 // 1블록 => (1-1)*10 + 1 => 1 // 2블록 => (2-1)*10 + 1 => 11 // 6블록 => (6-1)*10 + 1 => 51 blockBegin = (curBlock-1)*BLOCK_SCALE+1; // 페이지 블록의 끝번호 // 블록시작번호+블록단위-1; // 1블록 => 1+10-1 => 10 // 2블록 => 11+10-1 => 20 // 6블록 => 51+10-1 => 60 blockEnd = blockBegin+BLOCK_SCALE-1; // *마지막 블록이 범위를 초과하지 않도록 계산 // [이전] 61 62 => 이러한 경우 70번까지 나오지 않도록하기 위해서 if(blockEnd > totPage) blockEnd = totPage; // *이전을 눌렀을 때 이동할 페이지 번호 prevPage = (curPage == 1)? 1:(curBlock-1)*BLOCK_SCALE; // *다음을 눌렀을 때 이동할 페이지 번호 nextPage = curBlock > totBlock ? (curBlock*BLOCK_SCALE) : (curBlock*BLOCK_SCALE)+1; // 마지막 페이지가 범위를 초과하지 않도록 처리 if(nextPage >= totPage) nextPage = totPage; } public void setPageRange(){ // WHERE rn BETWEEN #{start} AND #{end} // 시작번호 = (현재페이지-1)*페이지당 게시물수 +1 pageBegin = (curPage-1)*PAGE_SCALE+1; // 끝번호 = 시작번호+페이지당 게시물수 -1 pageEnd = pageBegin+PAGE_SCALE-1; } // Getter/Setter public int getCurPage() { return curPage; } public void setCurPage(int curPage) { this.curPage = curPage; } public int getPrevPage() { return prevPage; } public void setPrevPage(int prevPage) { this.prevPage = prevPage; } public int getNextPage() { return nextPage; } public void setNextPage(int nextPage) { this.nextPage = nextPage; } public int getTotPage() { return totPage; } public void setTotPage(int count) { // 91개의 게시물을 10개씩 9페이지를 처리하고 남은 1개의 게시물도 페이지에 출력하기 위해서는 // 항상 올림으로 처리해야한다. // Math.ceil(실수) 올림 처리 // 모든 페이지는 올림처리 totPage = (int) Math.ceil(count*1.0 / PAGE_SCALE); } public int getTotBlock() { return totBlock; } // 페이지 블록의 갯수 계산(총 100페이지라면 10개의 블록) public void setTotBlock() { // 전체 페이지 갯수 / 10 // 91 / 10 => 9.1 => 10개 totBlock = (int)Math.ceil(totPage / BLOCK_SCALE); } public int getCurBlock() { return curBlock; } public void setCurBlock(int curBlock) { this.curBlock = curBlock; } public int getPrevBlock() { return prevBlock; } public void setPrevBlock(int prevBlock) { this.prevBlock = prevBlock; } public int getNextBlock() { return nextBlock; } public void setNextBlock(int nextBlock) { this.nextBlock = nextBlock; } public int getPageBegin() { return pageBegin; } public void setPageBegin(int pageBegin) { this.pageBegin = pageBegin; } public int getPageEnd() { return pageEnd; } public void setPageEnd(int pageEnd) { this.pageEnd = pageEnd; } public int getBlockBegin() { return blockBegin; } public void setBlockBegin(int blockBegin) { this.blockBegin = blockBegin; } public int getBlockEnd() { return blockEnd; } public void setBlockEnd(int blockEnd) { this.blockEnd = blockEnd; } } | cs |
03) Model(비지니스 로직, DB연동 작업처리)
ReplyVO - 비밀 댓글의 유무, 게시글의 작성자 추가
package com.example.spring02.model.board.dto; import java.util.Date; public class ReplyVO { private Integer rno; // 댓글 번호 private Integer bno; // 게시글 번호 private String replytext; // 댓글 내용 private String replyer; // 댓글 작성자 private String userName; // 댓글 작성자의 이름(회원의 이름) private Date regdate; // 댓글 작성일자 private Date updatedate; // 댓글 수정일자 private String secretReply; // **댓글 숨김 유무 추가 private String writer; // **게시글의 작성자 추가 // Getter/Setter public Integer getRno() { return rno; } public void setRno(Integer rno) { this.rno = rno; } public Integer getBno() { return bno; } public void setBno(Integer bno) { this.bno = bno; } public String getReplytext() { return replytext; } public void setReplytext(String replytext) { this.replytext = replytext; } public String getReplyer() { return replyer; } public void setReplyer(String replyer) { this.replyer = replyer; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Date getRegdate() { return regdate; } public void setRegdate(Date regdate) { this.regdate = regdate; } public Date getUpdatedate() { return updatedate; } public void setUpdatedate(Date updatedate) { this.updatedate = updatedate; } public String getSecretReply() { return secretReply; } public void setSecretReply(String secretReply) { this.secretReply = secretReply; } public String getWriter() { return writer; } public void setWriter(String writer) { this.writer = writer; } // toString() @Override public String toString() { return "ReplyVO [rno=" + rno + ", bno=" + bno + ", replytext=" + replytext + ", replyer=" + replyer + ", userName=" + userName + ", regdate=" + regdate + ", updatedate=" + updatedate + ", secretReply=" + secretReply + ", writer=" + writer + "]"; } } | cs |
ReplyDAO(인터페이스) - 목록 메서드 : 페이징 처리를 위한 파라미터 추가(int start, int end)
package com.example.spring02.model.board.dao; import java.util.List; import com.example.spring02.model.board.dto.ReplyVO; public interface ReplyDAO { // 댓글 목록 public List<ReplyVO> list(Integer bno, int start, int end); // 댓글 입력 public void create(ReplyVO vo); // 댓글 수정 public void update(ReplyVO vo); // 댓글 삭제 public void delete(Integer rno); // 댓글 갯수 public int count(int bno); } | cs |
ReplyDAOImpl(인터페이스 구현 클래스)
HashMap에 게시글의 번호, 페이지 시작번호, 페이지 끝번호를 저장
package com.example.spring02.model.board.dao; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import org.apache.ibatis.session.SqlSession; import org.springframework.stereotype.Repository; import com.example.spring02.model.board.dto.ReplyVO; @Repository public class ReplyDAOImpl implements ReplyDAO { @Inject SqlSession sqlSession; // 댓글 목록 @Override public List<ReplyVO> list(Integer bno, int start, int end) { Map<String, Object> map = new HashMap<String, Object>(); map.put("bno", bno); map.put("start", start); map.put("end", end); return sqlSession.selectList("reply.listReply", map); } // 댓글 작성 @Override public void create(ReplyVO vo) { sqlSession.insert("reply.insertReply", vo); } // 댓글 수정 @Override public void update(ReplyVO vo) { // TODO Auto-generated method stub } // 댓글 삭제 @Override public void delete(Integer rno) { // TODO Auto-generated method stub } // 댓글 갯수 @Override public int count(int bno) { return sqlSession.selectOne("reply.countReply",bno); } } | cs |
replyMapper.xml
댓글 목록 - 페이징 sql include 처리, 게시글작성자를 추가하기 위해서 select 서브쿼리 작성
<?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와 중복되지 않도록 네임스페이스 기재 --> <mapper namespace="reply"> <!-- 댓글 입력 --> <insert id="insertReply"> INSERT INTO tbl_reply (rno, bno, replytext, replyer, secret_reply) VALUES (reply_seq.NEXTVAL, #{bno}, #{replytext}, #{replyer}, #{secretReply}) </insert> <!-- 댓글 목록 --> <select id="listReply" resultType="com.example.spring02.model.board.dto.ReplyVO"> <include refid="pagingHeader"/> <!-- SELECT rno, bno, replytext, replyer, user_name AS userName, r.regdate, r.updatedate FROM tbl_reply r, tbl_member m WHERE r.replyer = m.user_id AND bno=#{bno} ORDER BY rno --> SELECT r.rno, bno, r.replytext, r.replyer, r.secret_reply AS secretReply, r.regdate, r.updatedate, m.user_name AS userName, (SELECT writer FROM tbl_board WHERE bno = r.bno) AS writer FROM tbl_reply r, tbl_member m WHERE r.replyer = m.user_id AND bno = #{bno} ORDER BY rno <include refid="pagingFooter"/> </select> <!-- 댓글 갯수(페이징처리) --> <select id="countReply" resultType="int"> SELECT COUNT(*) FROM tbl_reply WHERE bno=#{bno} </select> <!-- 페이징 sql --> <sql id="pagingHeader"> SELECT * FROM ( SELECT ROWNUM AS rn, A.* FROM ( </sql> <sql id="pagingFooter"> ) A ) WHERE rn BETWEEN #{start} AND #{end} </sql> </mapper> | cs |
04) View(화면)
view.jsp(게시물 상세보기 화면)
jQuery - 비밀댓글 체크여부 확인, 비밀댓글 파라미터 추가
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>게시글 작성</title> <%@ include file="../include/header.jsp" %> <script> $(document).ready(function(){ listReply("1"); // **댓글 목록 불러오기 //listReply2(); // ** json 리턴방식 // 댓글 쓰기 버튼 클릭 이벤트 (ajax로 처리) $("#btnReply").click(function(){ var replytext=$("#replytext").val(); var bno="${dto.bno}" // ** 비밀댓글 체크여부 var secretReply = "n"; // 태그.is(":속성") 체크여부 true/false if( $("#secretReply").is(":checked") ){ secretReply = "y"; } //alert(secretReply); // **비밀댓글 파라미터 추가 var param="replytext="+replytext+"&bno="+bno+"&secretReply="+secretReply; $.ajax({ type: "post", url: "${path}/reply/insert.do", data: param, success: function(){ alert("댓글이 등록되었습니다."); //listReply2(); listReply("1"); } }); }); // 게시글 목록 버튼 클릭 이벤트 : 버튼 클릭시 상세보기화면에 있던 페이지, 검색옵션, 키워드 값을 가지로 목록으로 이동 $("#btnList").click(function(){ location.href="${path}/board/list.do?curPage=${curPage}&searchOption=${searchOption}&keyword=${keyword}"; }); // 게시글 삭제 버튼 클릭이벤트 $("#btnDelete").click(function(){ if(confirm("삭제하시겠습니까?")){ document.form1.action = "${path}/board/delete.do"; document.form1.submit(); } }); // 게시글 수정 버튼 클릭이벤트 $("#btnUpdete").click(function(){ //var title = document.form1.title.value; ==> name속성으로 처리할 경우 //var content = document.form1.content.value; //var writer = document.form1.writer.value; var title = $("#title").val(); var content = $("#content").val(); //var writer = $("#writer").val(); if(title == ""){ alert("제목을 입력하세요"); document.form1.title.focus(); return; } if(content == ""){ alert("내용을 입력하세요"); document.form1.content.focus(); return; } /* if(writer == ""){ alert("이름을 입력하세요"); document.form1.writer.focus(); return; } */ document.form1.action="${path}/board/update.do" // 폼에 입력한 데이터를 서버로 전송 document.form1.submit(); }); }); // Controller방식 // **댓글 목록1 function listReply(num){ $.ajax({ type: "get", url: "${path}/reply/list.do?bno=${dto.bno}&curPage="+num, success: function(result){ // responseText가 result에 저장됨. $("#listReply").html(result); } }); } // RestController방식 (Json) // **댓글 목록2 (json) function listReply2(){ $.ajax({ type: "get", //contentType: "application/json", ==> 생략가능(RestController이기때문에 가능) url: "${path}/reply/listJson.do?bno=${dto.bno}", success: function(result){ console.log(result); var output = "<table>"; for(var i in result){ output += "<tr>"; output += "<td>"+result[i].userName; output += "("+changeDate(result[i].regdate)+")<br>"; output += result[i].replytext+"</td>"; output += "<tr>"; } output += "</table>"; $("#listReply").html(output); } }); } // **날짜 변환 함수 작성 function changeDate(date){ date = new Date(parseInt(date)); year = date.getFullYear(); month = date.getMonth(); day = date.getDate(); hour = date.getHours(); minute = date.getMinutes(); second = date.getSeconds(); strDate = year+"-"+month+"-"+day+" "+hour+":"+minute+":"+second; return strDate; } </script> </head> <body> <%@ include file="../include/menu.jsp" %> <h2>게시글 보기</h2> <form name="form1" method="post"> <div> <!-- 원하는 날짜형식으로 출력하기 위해 fmt태그 사용 --> 작성일자 : <fmt:formatDate value="${dto.regdate}" pattern="yyyy-MM-dd a HH:mm:ss"/> <!-- 날짜 형식 => yyyy 4자리연도, MM 월, dd 일, a 오전/오후, HH 24시간제, hh 12시간제, mm 분, ss 초 --> </div> <div> 조회수 : ${dto.viewcnt} </div> <div> 제목 <input name="title" id="title" size="80" value="${dto.title}" placeholder="제목을 입력해주세요"> </div> <div> 내용 <textarea name="content" id="content" rows="4" cols="80" placeholder="내용을 입력해주세요">${dto.content}</textarea> </div> <div> 이름 <%-- <input name="writer" id="writer" value="${dto.writer}" placeholder="이름을 입력해주세요"> --%> ${dto.userName} </div> <div style="width:650px; text-align: center;"> <!-- 게시물번호를 hidden으로 처리 --> <input type="hidden" name="bno" value="${dto.bno}"> <!-- 본인이 쓴 게시물만 수정, 삭제가 가능하도록 처리 --> <c:if test="${sessionScope.userId == dto.writer}"> <button type="button" id="btnUpdete">수정</button> <button type="button" id="btnDelete">삭제</button> </c:if> <!-- 상세보기 화면에서 게시글 목록화면으로 이동 --> <button type="button" id="btnList">목록</button> <hr> </div> </form> <div style="width:650px; text-align: center;"> <br> <!-- 로그인 한 회원에게만 댓글 작성폼이 보이게 처리 --> <c:if test="${sessionScope.userId != null}"> <textarea rows="5" cols="80" id="replytext" placeholder="댓글을 작성해주세요"></textarea> <br> <!-- **비밀댓글 체크박스 --> <input type="checkbox" id="secretReply">비밀 댓글 <button type="button" id="btnReply">댓글 작성</button> </c:if> <hr> </div> <!-- **댓글 목록 출력할 위치 --> <div id="listReply"></div> </body> </html> | cs |
replyList.jsp(댓글 목록 화면)
페이징 처리 작업 - 첫 페이지로 이동, 이전 페이지 블럭으로 이동, 다음 페이지 블럭으로 이동, 끝 페이지로 이동
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <%@ include file="../include/header.jsp" %> </head> <body> <table style="width:700px"> <!-- 댓글 목록 --> <c:forEach var="row" items="${list}"> <tr> <td> ${row.userName}(<fmt:formatDate value="${row.regdate}" pattern="yyyy-MM-dd HH:mm:ss"/>) <br> ${row.replytext} <hr> </td> </tr> </c:forEach> <!-- **페이징 처리 --> <tr> <td> <!-- 현재 페이지 블럭이 1보다 크면 처음으로 이동 --> <c:if test="${replyPager.curBlock > 1}"> <a href="javascript:listReply('1')">[처음]</a> </c:if> <!-- 현재 페이지 블럭이 1보다 크면 이전 페이지 블럭으로 이동 --> <c:if test="${replyPager.curBlock > 1}"> <a href="javascript:listReply('${replyPager.prevPage}')">[이전]</a> </c:if> <!-- 페이지 블럭 처음부터 마지막 블럭까지 1씩 증가하는 페이지 출력 --> <c:forEach var="num" begin="${replyPager.blockBegin}" end="${replyPager.blockEnd}"> <c:choose> <c:when test="${num == replyPager.curPage}"> ${num} </c:when> <c:otherwise> <a href="javascript:listReply('${num}')">${num}</a> </c:otherwise> </c:choose> </c:forEach> <!-- 현재 페이지 블럭이 전체 페이지 블럭보다 작거나 같으면 다음페이지로 이동 --> <c:if test="${replyPager.curBlock <= replyPager.totBlock}"> <a href="javascript:listReply('${replyPager.nextPage}')">[다음]</a> </c:if> <!-- 현재 페이지 블럭이 전체 페이지 블럭보다 작거나 같으면 끝으로 이동 --> <c:if test="${replyPager.curBlock <= replyPager.totBlock}"> <a href="javascript:listReply('${replyPager.totPage}')">[끝]</a> </c:if> </td> </tr> </table> </body> </html> | cs |