mvcPage
WEB-INF
폴더의 board.properties
하단에 다음의 내용을 추가/board/delete.do=kr.board.action.DeleteAction
# 댓글
/board/writeReply.do=kr.board.action.WriteReplyAction
sql
폴더의 table.sql
하단에 다음의 내용을 추가/* 댓글 */
CREATE TABLE zboard_reply(
re_num NUMBER NOT NULL,
re_content VARCHAR2(900) NOT NULL,
re_date DATE DEFAULT SYSDATE NOT NULL,
re_modifydate DATE,
re_ip VARCHAR2(40) NOT NULL,
board_num NUMBER NOT NULL,
mem_num NUMBER NOT NULL,
CONSTRAINT zreply_pk PRIMARY KEY (re_num),
CONSTRAINT zreply_fk1 FOREIGN KEY (board_num) REFERENCES zboard (board_num),
CONSTRAINT zreply_fk2 FOREIGN KEY (mem_num) REFERENCES zmember (mem_num)
);
CREATE SEQUENCE zreply_seq;
css
폴더의 layout.css
하단에 다음의 내용을 추가/* 글 상세 */
div#reply_div {
padding: 5px 10px 40px 10px;
margin-top: 10px;
background-color: #eee;
}
form#re_form {
width: 650px;
border: none;
}
span.re-title {
color: #000;
font-size: 12pt;
line-height: 200%;
}
span.letter-count {
font-size: 10pt;
color: #999;
}
textarea.rep-content {
width: 90%;
height: 50px;
margin: 10px 10px;
}
div#re_first, div#mre_first {
float: left;
width: 70%;
padding-left: 15px;
margin-bottom: 10px;
}
div#re_second, div#mre_second {
float: left;
width: 19%;
margin-bottom: 10px;
}
div#loading {
width: 100px;
height: 50px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
}
div.paging-button {
text-align: right;
}
div#output {
clear: both;
}
form#mre_form {
border: none;
margin: 5px;
}
DeleteAction
생성package kr.board.action;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import kr.board.dao.BoardDAO;
import kr.board.vo.BoardVO;
import kr.controller.Action;
import kr.util.FileUtil;
public class DeleteAction implements Action {
@Override
public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpSession session = request.getSession();
Integer user_num = (Integer)session.getAttribute("user_num");
if(user_num==null) { // 로그인되어 있지 않은 경우
return "redirect:/member/loginForm.do";
}
// 로그인되어 있는 경우
int board_num = Integer.parseInt(request.getParameter("board_num"));
BoardDAO dao = BoardDAO.getInstance();
BoardVO db_board = dao.getBoard(board_num);
if(user_num!=db_board.getMem_num()) { // 로그인한 회원 번호와 작성자 회원 번호가 일치하지 않는 경우
return "/WEB-INF/views/common/notice.jsp";
}
// 로그인한 회원 번호와 작성자 회원 번호가 일치하는 경우
// 글 삭제
dao.deleteBoard(board_num);
// 파일 삭제
FileUtil.removeFile(request, db_board.getFilename());
// JSP 경로 반환
return "redirect:/board/list.do";
}
}
ListAction
클래스에서 다음의 내용을 삭제 if(keyfield==null) keyfield = ""; // GET 방식으로 null 전송시 PagingUtil에서 문제가 발생할 수 있으므로 빈 문자열로 처리
if(keyword==null) keyword = "";
WriteReplyAction
생성package kr.board.action;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.codehaus.jackson.map.ObjectMapper;
import kr.board.dao.BoardDAO;
import kr.board.vo.BoardReplyVO;
import kr.controller.Action;
public class WriteReplyAction implements Action {
@Override
public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, String> mapAjax = new HashMap<String, String>();
HttpSession session = request.getSession();
Integer user_num = (Integer)session.getAttribute("user_num");
if(user_num==null) { // 로그인되어 있지 않은 경우
mapAjax.put("result", "logout");
}
else { // 로그인되어 있는 경우
// 전송된 데이터 인코딩 처리
request.setCharacterEncoding("UTF-8");
// 전송된 데이터를 자바빈에 저장
BoardReplyVO reply = new BoardReplyVO();
reply.setMem_num(user_num);
reply.setRe_content(request.getParameter("re_content"));
reply.setRe_ip(request.getRemoteAddr());
reply.setBoard_num(Integer.parseInt(request.getParameter("board_num")));
BoardDAO dao = BoardDAO.getInstance();
dao.insertReplyBoard(reply);
mapAjax.put("result", "success");
}
// JSON 데이터 생성
ObjectMapper mapper = new ObjectMapper();
String ajaxData = mapper.writeValueAsString(mapAjax);
request.setAttribute("ajaxData", ajaxData);
// JSP 경로 반환
return "/WEB-INF/views/common/ajax_view.jsp";
}
}
js
폴더에 새 JavaScript 파일 board-reply.js
생성$(function() {
let currentPage;
let count;
let rowCount;
// 댓글 목록
function selectData(pageNum) {
currentPage = pageNum;
// 로딩 이미지 노출
$('#loading').show();
$.ajax({
type:'post',
data:{pageNum:pageNum,board_num:$('#board_num').val()},
url:'listReply.do',
dataType:'json',
cache:false,
timeout:3000,
success:function(param) {
// 로딩 이미지 감추기
$('#loading').hide();
count = param.count;
rowCount = param.rowCount;
if(pageNum==1) {
// 처음 호출시는 해당 ID의 div 내부 내용물을 제거
$('#output').empty();
}
$(param.list).each(function(index, item) {
let output = '<div class="item">';
output += '<h4>' + item.id + '</h4>';
output += '<div class="sub-item">'
output += '<p>' + item.re_content + '</p>';
output += '<span class="modify-date">';
if(item.re_modifydate) {
output += '최근 수정일 : ' + item.re_modifydate;
}
else {
output += '등록일 : ' + item.re_date;
}
output += '</span>';
if(param.user_num == item.mem_num) { // 로그인한 회원 번호와 작성자 회원 번호 일치
output += ' <input type="button" data-renum="' + item.re_num + '" value="수정" class="modify-btn">'; // 수정, 삭제시 re_num 값이 필요하므로 버튼에 보관
output += ' <input type="button" data-renum="' + item.re_num + '" value="삭제" class="delete-btn">';
}
output += '<hr size="1" noshade width="100%">';
output += '</div>';
output += '</div>';
// 문서 객체에 추가
$('#output').append(output);
}); // end of each
// 다음 댓글 보기 버튼 처리
if(currentPage>=Math.ceil(count/rowCount)) {
// 다음 페이지가 없음
$('.paging-button').hide();
}
else {
// 다음 페이지가 존재
$('.paging-button').show();
}
},
error:function() { // JSON 형식에 맞지 않는 데이터가 전송된 경우, 서버 에러
alert('네트워크 오류 발생!');
}
}); // end of ajax
}
// 페이지 처리 이벤트 연결(다음 댓글 보기 버튼 클릭시 데이터 추가)
$('.paging-button input').click(function() {
selectData(currentPage + 1);
});
// 댓글 등록
$('#re_form').submit(function(event) {
if($('#re_content').val().trim()=='') {
alert('내용을 입력하세요!');
$('#re_content').val('').focus();
return false;
}
// <form> 태그 내 필드에 입력한 데이터를 모두 읽어옴
let form_data = $(this).serialize();
// 데이터 전송
$.ajax({
url:'writeReply.do',
type:'post',
data:form_data,
dataType:'json',
cache:false,
timeout:30000,
success:function(param) {
if(param.result=='logout') {
alert('로그인해야 작성할 수 있습니다.');
}
else if(param.result=='success') {
// 폼 초기화
initForm();
// 댓글 작성이 성공하면 새로 입력한 댓글을 포함해서 첫 번째 페이지의 댓글들을 다시 호출
selectData(1);
}
else {
alert('등록시 오류 발생!');
}
},
error:function() {
alert('네트워크 오류 발생!');
}
}); // end of ajax
// 기본 이벤트 제거
event.preventDefault();
});
// 댓글 작성 폼 초기화
function initForm() {
$('textarea').val('');
$('#re_first .letter-count').text('300/300')
}
// <textarea>에 내용 입력시 글자 수 체크
$(document).on('keyup', 'textarea', function() { // 동적으로 생성되는 미래 태그에도 이벤트 연결하기 위해 $(document).on() 사용
// 입력한 글자 수 구하기
let inputLength = $(this).val().length;
if(inputLength>300) { // 300자 초과
$(this).val($(this).val().substring(0, 300));
}
else { // 300자 이하
let remain = 300 - inputLength;
remain += '/300';
if($(this).attr('id')=='re_content') { // 등록 폼 글자 수
$('#re_first .letter-count').text(remain);
}
else { // 수정 폼 글자 수
$('#mre_first .letter-count').text(remain);
}
}
});
// 댓글 수정 버튼 클릭시 수정 폼 노출
$(document).on('click', '.modify-btn', function() {
// 댓글 번호
let re_num = $(this).attr('data-renum');
// 댓글 내용
let content = $(this).parent().find('p').html().replace(/<br>/gi, '\n'); // g: 지정 문자열 모두; i: 대소문자 무시
// 댓글 수정 폼 UI
let modifyUI = '<form id="mre_form">';
modifyUI += ' <input type="hidden" name="re_num" id="mre_num" value="' + re_num + '">';
modifyUI += ' <textarea rows="3" cols="50" name="re_content" id="mre_content" class="rep-content">' + content + '</textarea>';
modifyUI += ' <div id="mre_first"><span class="letter-count">300/300</span></div>';
modifyUI += ' <div id="mre-second" class="align-right">';
modifyUI += ' <input type="submit" value="수정">';
modifyUI += ' <input type="button" value="취소" class="re-reset">';
modifyUI += ' </div>';
modifyUI += ' <hr size="1" noshade width="96%">';
modifyUI += '</form>';
// 수정 버튼을 클릭하면 (이전에 수정 중이던) 다른 댓글의 수정 폼은 숨김; sub-item을 환원시키고 수정 폼 초기화
initModifyForm();
// 지금 클릭해서 수정하고자 하는 데이터(=수정 버튼을 감싸고 있는 <div> 태그) 감추기
$(this).parent().hide();
// 수정 폼을 수정하고자 하는 데이터가 있는 <div> 태그(=수정 버튼을 감싸고 있는 태그들 중 클래스가 item인 태그)에 노출
$(this).parents('.item').append(modifyUI);
// 입력한 글자 수 세팅
let inputLength = $('#mre_content').val().length;
let remain = 300 - inputLength;
remain += '/300';
// 문서 객체에 반영
$('#mre_first .letter-count').text(remain);
});
// 수정 폼에서 취소 버튼 클릭시 수정 폼 초기화
$(document).on('click', '.re-reset', function() {
initModifyForm();
});
// 댓글 수정 폼 초기화
function initModifyForm() {
$('.sub-item').show();
$('#mre_form').remove(); // <form> 태그는 id가 부여되어 있으므로 삭제하지 않고 단순히 숨기기만 하면 중복 문제가 발생
}
// 댓글 수정
$(document).on('submit', '#mre_form', function(event) {
if($('#mre_content').val().trim()=='') {
alert('내용을 입력하세요!');
$('#re_content').val('').focus();
return false;
}
// 폼에 입력한 데이터 반환
let form_data = $(this).serialize();
// 서버와 통신
$.ajax({
url:'updateReply.do',
type:'post',
data:form_data,
dataType:'json',
cache:false,
tiemout:30000,
success:function(param) {
if(param.result=='logout') {
alert('로그인해야 수정할 수 있습니다.');
}
else if(param.result=='success') {
$('#mre_form').parent().find('p').html($('#mre_content').val().replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>'));
$('#mre_form').parent().find('.modify-date').text('최근 수정일 : 5초 미만');
// 수정 폼 삭제 및 초기화
initModifyForm();
}
else if(param.result=='wrongAccess') {
alert('타인의 댓글을 수정할 수 없습니다.');
}
else {
alert('수정시 오류 발생!');
}
},
error:function() {
alert('네트워크 오류 발생!');
}
}); // end of ajax
// 기본 이벤트 제거
event.preventDefault();
});
// 댓글 삭제
$(document).on('click', '.delete-btn', function() {
// 댓글 번호
let re_num = $(this).attr('data-renum');
$.ajax({
url:'deleteReply.do',
type:'post',
data:{re_num:re_num},
dataType:'json',
cache:false,
timeout:30000,
success:function(param) {
if(param.result=='logout') {
alert('로그인해야 삭제할 수 있습니다.');
}
else if(param.result=='success') {
alert('삭제 완료!');
selectData(1);
}
else if(param.result=='wrongAccess') {
alert('타인의 댓글을 삭제할 수 없습니다.');
}
else {
alert('삭제시 오류 발생!');
}
},
error:function() {
alert('네트워크 오류 발생!');
}
});
});
// 초기 데이터 호출
selectData(1);
});
kr.util
패키지의 PagingUtil
클래스의 내용을 다음처럼 교체package kr.util;
public class PagingUtil {
private int startCount; // 한 페이지에서 보여줄 게시글의 시작 번호
private int endCount; // 한 페이지에서 보여줄 게시글의 끝 번호
private StringBuffer pagingHtml;// 페이지 표시 문자열
/**
* currentPage : 현재페이지
* totalCount : 전체 게시물 수
* rowCount : 한 페이지의 게시물의 수
* pageCount : 한 화면에 보여줄 페이지 수
* pageUrl : 호출 페이지 url
* addKey : 부가적인 key 없을 때는 null 처리 (&num=23형식으로 전달할 것)
* */
public PagingUtil(int currentPage, int totalCount, int rowCount,
int pageCount, String pageUrl) {
this(null,null,currentPage,totalCount,rowCount,pageCount,pageUrl,null);
}
public PagingUtil(int currentPage, int totalCount, int rowCount,
int pageCount, String pageUrl, String addKey) {
this(null,null,currentPage,totalCount,rowCount,pageCount,pageUrl,addKey);
}
public PagingUtil(String keyfield, String keyword, int currentPage, int totalCount, int rowCount,
int pageCount,String pageUrl) {
this(keyfield,keyword,currentPage,totalCount,rowCount,pageCount,pageUrl,null);
}
public PagingUtil(String keyfield, String keyword, int currentPage, int totalCount, int rowCount,
int pageCount,String pageUrl,String addKey) {
String sub_url = "";
if(keyword != null) sub_url = "&keyfield="+keyfield+"&keyword="+keyword;
if(addKey != null) sub_url += addKey;
// 전체 페이지 수
int totalPage = (int) Math.ceil((double) totalCount / rowCount);
if (totalPage == 0) {
totalPage = 1;
}
// 현재 페이지가 전체 페이지 수보다 크면 전체 페이지 수로 설정
if (currentPage > totalPage) {
currentPage = totalPage;
}
// 현재 페이지의 처음과 마지막 글의 번호 가져오기.
startCount = (currentPage - 1) * rowCount + 1;
endCount = currentPage * rowCount;
// 시작 페이지와 마지막 페이지 값 구하기.
int startPage = (int) ((currentPage - 1) / pageCount) * pageCount + 1;
int endPage = startPage + pageCount - 1;
// 마지막 페이지가 전체 페이지 수보다 크면 전체 페이지 수로 설정
if (endPage > totalPage) {
endPage = totalPage;
}
// 이전 block 페이지
pagingHtml = new StringBuffer();
if (currentPage > pageCount) {
pagingHtml.append("<a href="+pageUrl+"?pageNum="+ (startPage - 1) + sub_url +">");
pagingHtml.append("[이전]");
pagingHtml.append("</a>");
}
//페이지 번호.현재 페이지는 빨간색으로 강조하고 링크를 제거.
for (int i = startPage; i <= endPage; i++) {
if (i > totalPage) {
break;
}
if (i == currentPage) {
pagingHtml.append(" <b><span style='color:red;'>");
pagingHtml.append(i);
pagingHtml.append("</span></b>");
} else {
pagingHtml.append(" <a href='"+pageUrl+"?pageNum=");
pagingHtml.append(i);
pagingHtml.append(sub_url+"'>");
pagingHtml.append(i);
pagingHtml.append("</a>");
}
pagingHtml.append(" ");
}
// 다음 block 페이지
if (totalPage - startPage >= pageCount) {
pagingHtml.append("<a href="+pageUrl+"?pageNum="+ (endPage + 1) + sub_url +">");
pagingHtml.append("[다음]");
pagingHtml.append("</a>");
}
}
public StringBuffer getPagingHtml() {
return pagingHtml;
}
public int getStartCount() {
return startCount;
}
public int getEndCount() {
return endCount;
}
}
kr.board.vo
패키지에 새 클래스 BoardReplyVO
생성package kr.board.vo;
public class BoardReplyVO {
private int re_num;
private String re_content;
private String re_date; // 시간을 시분초 단위까지 활용하기 위해 Date가 아닌 String으로 처리
private String re_modifydate;
private String ip;
private int board_num;
private int mem_num;
private String id; // 작성자 아이디; 테이블에는 없지만 회원 번호 대신 화면에 표시할 것이므로 프로퍼티에 포함
public int getRe_num() {
return re_num;
}
public void setRe_num(int re_num) {
this.re_num = re_num;
}
public String getRe_content() {
return re_content;
}
public void setRe_content(String re_content) {
this.re_content = re_content;
}
public String getRe_date() {
return re_date;
}
public void setRe_date(String re_date) {
this.re_date = re_date;
}
public String getRe_modifydate() {
return re_modifydate;
}
public void setRe_modifydate(String re_modifydate) {
this.re_modifydate = re_modifydate;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public int getBoard_num() {
return board_num;
}
public void setBoard_num(int board_num) {
this.board_num = board_num;
}
public int getMem_num() {
return mem_num;
}
public void setMem_num(int mem_num) {
this.mem_num = mem_num;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}