Study Web Development

2월 7일

이전으로

Servlet & JSP

9. MVC

mvcPage

프로젝트 생성 및 설정
  1. WEB-INF 폴더의 board.properties 하단에 다음의 내용을 추가
/board/delete.do=kr.board.action.DeleteAction
# 댓글
/board/writeReply.do=kr.board.action.WriteReplyAction
  1. 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;
  1. 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;
}
Model
  1. 새 클래스 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";
	}

}
  1. ListAction 클래스에서 다음의 내용을 삭제
		if(keyfield==null) keyfield = ""; // GET 방식으로 null 전송시 PagingUtil에서 문제가 발생할 수 있으므로 빈 문자열로 처리
		if(keyword==null) keyword = "";
  1. 새 클래스 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";
	}

}
View
  1. 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, '&lt;').replace(/>/g, '&gt;').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);
});
DAO
  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("&nbsp;<b><span style='color:red;'>");
				pagingHtml.append(i);
				pagingHtml.append("</span></b>");
			} else {
				pagingHtml.append("&nbsp;<a href='"+pageUrl+"?pageNum=");
				pagingHtml.append(i);
				pagingHtml.append(sub_url+"'>");
				pagingHtml.append(i);
				pagingHtml.append("</a>");
			}
			pagingHtml.append("&nbsp;");
		}
		// 다음 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;
	}
}
자바빈
  1. 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;
	}
}

다음으로