본문 바로가기

프로그래밍 기초/SPRING

[Spring]게시판에 댓글 기능 구현

댓글 테이블,시퀀스 만들기

Quantum DB-댓글을 담을 테이블과 시퀀스 생성

-- 댓글 정보를 저장할 테이블
CREATE TABLE board_cafe_comment(
	num NUMBER PRIMARY KEY, -- 댓글의 글번호
	writer VARCHAR2(100), -- 댓글 작성자
	content VARCHAR2(500), -- 댓글 내용
	target_id VARCHAR2(100), -- 댓글의대상이되는아이디(글작성자)
	ref_group NUMBER, -- 댓글 그룹번호
	comment_group NUMBER, -- 원글에 달린 댓글 내에서의 그룹번호
	deleted CHAR(3) DEFAULT 'no', -- 댓글이 삭제 되었는지 여부 
	regdate DATE -- 댓글 등록일 
);

CREATE board_cafe_seq;

 


댓글 저장하기

detail.jsp -원글의 댓글 작성기능폼 및 css작성

div 컨테이너안에 댓글을 작성할 수 있는폼을작성한다.

ref_group, target_id, content가 파라미터로 넘어가게된다.

ref_group(댓글 그룹번호)는 원글의 글번호,

target_id(댓글 대상자)는 원글의 작성자,

content는 댓글내용을 담는다.  commet_group번호는 따로 전송하지 않는다. = null

만약 로그인이 되어있지 않다면  '로그인이 필요합니다'라고 출력되도록 한다.

	<div class="comments">
		<!-- 원글에 댓글을 작성할 수 있는 폼 -->
		<div class="comment_form">
			<form action="comment_insert.do" method="post"> 
			<!-- 댓글의 그룹번호는 원글의 글번호가 된다. -->
				<input type="hidden" name="ref_group" value="${dto.num }"/>
			<!-- 댓글의 대상자는 원글의 작성자가 된다. -->
				<input type="hidden" name="target_id" value="${dto.writer }"/>
				<textarea rows="content"><c:if test="${empty id }">로그인이 필요합니다.</c:if></textarea>
				<button type="submit">등록</button>
            </form>
		</div>
   </div>
<style>
	/* 글 내용의 경계선 표시 */
	.content{
		border: 1px dotted #cecece;
	}
	/* 글 내용안에 있는 이미지의 크기 제한 */
	.content img{
		max-width: 100%;
	}
	/* 댓글에 관련된 css */
	.comments ul{
		padding: 0;
		margin: 0;
		list-style-type: none;
	}
	.comments ul li{
		border-top: 1px solid #888; /* li 의 윗쪽 경계선 */
	}
	.comments dt{
		margin-top: 5px;
	}
	.comments dd{
		margin-left: 26px;
	}
	.comments form textarea, .comments form button{
		float: left;
	}
	.comments li{
		clear: left;
	}
	.comments form textarea{
		width: 85%;
		height: 100px;
	}
	.comments form button{
		width: 15%;
		height: 100px;
	}
	/* 댓글에 댓글을 다는 폼과 수정폼을 일단 숨긴다. */
	.comment form{
		display: none;
	}
	.comment{
		position: relative;
	}
	.comment .reply_icon{
		width: 8px;
		height: 8px;
		position: absolute;
		top: 10px;
		left: 30px;
	}
	.comments .user-img{
		width: 20px;
		height: 20px;
		border-radius: 50%;
	}
</style>

detail.jsp -댓글(a)의 댓글 작성기능폼 

댓글의 댓글창에 만약 로그인이 안되어있다면 '로그인이 필요합니다'가 출력하도록 한다.

ref_group에는 원글의 번호가

target_id에는 댓글(a)의 작성자가

comment_group에는 댓글(a)의 comment_group이 들어가게 한다.

<form class="comment-insert-form" action="comment_insert.do" method="post">
				<!-- 덧글 그룹 -->
				<input type="hidden" name="ref_group" value="${dto.num }" />
				<!-- 덧글 대상 -->
				<input type="hidden" name="target_id" value="${tmp.writer }" />
				<input type="hidden" name="comment_group" value="${tmp.comment_group }" />
				<textarea name="content"><c:if test="${empty id }">로그인이 필요합니다.</c:if></textarea>
				<button type="submit">등록</button>
</form>	

Controller-댓글 저장요청처리하기

service에서 클라이언트의 요청을 인자로담은 saveComment를 실행하기로 한다.  =>아래의 servie-댓글저장하기 참고

detail.jsp의 comment_form에서 전송된 파라미터 ref_group(글번호이자 댓글의 그룹번호)를 detail에 get방식 파라미터로 넣어서 리다이렉트 시킨다.

	//댓글 저장 요청 처리
	@RequestMapping(value="/cafe/comment_insert",
			method=RequestMethod.POST)
	public ModelAndView authCommentInsert(HttpServletRequest request,
			@RequestParam int ref_group)
	{
    	service.saveComment(request);
		return new ModelAndView("redirect:/cafe/detail.do?num="+ref_group);
	}

 

Service-댓글 저장하기

요청으로 부터 ref_group(원글번호), target_id(원글작성자), content(댓글내용)가 파라미터를 얻어낸다.

 

폼에서 전송한 것이 댓글인 경우와 대댓글인 경우가 있다.

작성된 것이 댓글인 경우 : commnet_group(그룹번호)은 null값이다.

                                댓글의 num(글번호)를 댓글의 commnet_group(그룹번호)로 집어넣는다.

대댓글인경우 : 원댓글의 comment_group(그룹번호)가 댓글의  comment_group(그룹번호)가 된다.

	//댓글 저장하는 메소드
	@Override
	public void saveComment(HttpServletRequest request) {
		//댓글 작성자
		String writer=(String)request.getSession()
				.getAttribute("id");
		//댓글의 그룹번호
		int ref_group=
			Integer.parseInt(request.getParameter("ref_group"));
		//댓글의 대상자 아이디
		String target_id=request.getParameter("target_id");
		//댓글의 내용
		String content=request.getParameter("content");
		//댓글 내에서의 그룹번호 (null 이면 원글의 댓글이다)
		String comment_group=
				request.getParameter("comment_group");		
		//저장할 댓글의 primary key 값이 필요하다
		int seq = cafeCommentDao.getSequence();
		//댓글 정보를 Dto 에 담기
		CafeCommentDto dto=new CafeCommentDto();
		dto.setNum(seq);
		dto.setWriter(writer);
		dto.setTarget_id(target_id);
		dto.setContent(content);
		dto.setRef_group(ref_group);
		
		if(comment_group==null) {//원글의 댓글인 경우
			//댓글의 글번호가 댓글의 그룹 번호가 된다.
			dto.setComment_group(seq);
		}else {//댓글의 댓글인 경우
			//comment_group 번호가 댓글의 그룹번호가 된다.
			dto.setComment_group
				(Integer.parseInt(comment_group));
		}
		//댓글 정보를 DB 에 저장한다.
		cafeCommentDao.insert(dto);	
		
	}

 

참고

1.원글의 글번호가 댓글의 ref_group번호가 된다.

2.원글의 댓글은 댓글의 번호와 comment_group번호가 같다. 

3.댓글의 댓글은 댓글의 번호와 commnet_group번호가 다르고 comment_group번호는 최초로 시작된

댓글의 글번호로 부여된다.

 


댓글 읽어오기

Service -글의 detail을 얻어올 때 댓글전체도 얻어오기

detail.jsp의 요청에 응답할 service메소드 getDetail에 댓글 목록도 얻어와서 request에 담아준다.

	@Override
	public void getDetail(HttpServletRequest request) {
		//파라미터로 전달되는 글번호
		int num=Integer.parseInt(request.getParameter("num"));
		
		//검색과 관련된 파라미터를 읽어와 본다.
		String keyword=request.getParameter("keyword");
		String condition=request.getParameter("condition");
		
		//CafeDto 객체 생성 (select 할때 필요한 정보를 담기 위해)
		CafeDto dto=new CafeDto();
		
		if(keyword != null) {//검색 키워드가 전달된 경우
			if(condition.equals("titlecontent")) {//제목+내용 검색
				dto.setTitle(keyword);
				dto.setContent(keyword);
			}else if(condition.equals("title")) {//제목 검색
				dto.setTitle(keyword);
			}else if(condition.equals("writer")) {//작성자 검색
				dto.setWriter(keyword);
			}
			//request 에 검색 조건과 키워드 담기
			request.setAttribute("condition", condition);
			/*
			 *  검색 키워드에는 한글이 포함될 가능성이 있기 때문에
			 *  링크에 그대로 출력가능하도록 하기 위해 미리 인코딩을 해서
			 *  request 에 담아준다.
			 */
			String encodedKeyword=null;
			try {
				encodedKeyword=URLEncoder.encode(keyword, "utf-8");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
			//인코딩된 키워드와 인코딩 안된 키워드를 모두 담는다.
			request.setAttribute("encodedKeyword", encodedKeyword);
			request.setAttribute("keyword", keyword);
		}		
		//CafeDto 에 글번호도 담기
		dto.setNum(num);
		//조회수 1 증가 시키기
		cafeDao.addViewCount(num);
		//글정보를 얻어와서
		CafeDto dto2=cafeDao.getData(dto);
		//request 에 글정보를 담고 
		request.setAttribute("dto", dto2);
		//댓글 목록을 얻어와서 request 에 담아준다.
		List<CafeCommentDto> commentList=cafeCommentDao.getList(num);
		request.setAttribute("commentList", commentList);
	}

 

Mapper -댓글전체 얻어오기

users테이블의 id와 cafecomment테이블의 writer가 겹치므로 이를 활용하여 users테이블의 profile을 얻어낸다.

comment_group번호에 대해서 먼저 오름차순한 이후에 num에 대해서 오름차순 해서 출력시 순서를 정리해준다.

	<select id="getList" resultType="cafeCommentDto" parameterType="int">
		SELECT num,writer,content,target_id,ref_group,
			comment_group,deleted, board_cafe_comment.regdate,profile
		FROM board_cafe_comment
		INNER JOIN users
		ON board_cafe_comment.writer=users.id
		WHERE ref_group=#{ref_group}
		ORDER BY comment_group ASC, num ASC
	</select>

 

detail.jsp- 댓글들(commnetList)를 반복문을 돌면서 읽어온다.

댓글의 deleted가 'yes'일 때는 '삭제된 댓글입니다'라고 출력되게 조건문을 사용한다.

 참고 : dl dt dd  : 설명 리스트 목록

        dl로 시작 , dto로 타이틀목록을 만들고 dd로 그에대한 설명을 해줌

       <dl>

          <dt></dt>

          <dd></dd>

       </dl>

 

<ul>
<c:forEach items="${commentList }" var="tmp">
	<c:choose>
		<c:when test="${tmp.deleted ne 'yes' }">
			<li class="comment" id="comment${tmp.num }" <c:if test="${tmp.num ne tmp.comment_group }">style="padding-left:50px;"</c:if> >
				<c:if test="${tmp.num ne tmp.comment_group }">
					<img class="reply_icon" src="${pageContext.request.contextPath}/resources/images/re.gif"/>
				</c:if>
				<dl>
					<dt>
						<c:choose>
							<c:when test="${empty tmp.profile }">
								<img class="user-img" src="${pageContext.request.contextPath}/resources/images/default_user.jpg"/>
							</c:when>
							<c:otherwise>
		   					 <img class="user-img" src="${pageContext.request.contextPath}${tmp.profile}"/>
							</c:otherwise>
						</c:choose>
						
						<span>${tmp.writer }</span>
						<c:if test="${tmp.num ne tmp.comment_group }">
							to <strong>${tmp.target_id }</strong>
						</c:if>
						<span>${tmp.regdate }</span>
						<a href="javascript:" class="reply_link">답글</a> |
						<c:choose>
							<%-- 로그인된 아이디와 댓글의 작성자가 같으면 --%>
							<c:when test="${id eq tmp.writer }">
								<a href="javascript:" class="comment-update-link">수정</a>&nbsp;&nbsp;
								<a href="javascript:deleteComment(${tmp.num })">삭제</a>
							</c:when>
							<c:otherwise>
								<a href="javascript:">신고</a>
							</c:otherwise>
						</c:choose>
					</dt>
                       <dd>
						<pre>${tmp.content }</pre>
					</dd>
				</dl>
				<form class="comment-insert-form" action="comment_insert.do" method="post">
					<!-- 덧글 그룹 -->
					<input type="hidden" name="ref_group" value="${dto.num }" />
                    <!-- 덧글 대상 -->
					<input type="hidden" name="target_id" value="${tmp.writer }" />
					<input type="hidden" name="comment_group" value="${tmp.comment_group }" />
					<textarea name="content"><c:if test="${empty id }">로그인이 필요합니다.</c:if></textarea>
					<button type="submit">등록</button>
				</form>	
                  <!-- 로그인한 아이디와 댓글의 작성자와 같으면 수정폼 출력 -->				
				<c:if test="${id eq tmp.writer }">
					<form class="comment-update-form" action="comment_update.do">
						<input type="hidden" name="num" value="${tmp.num }" />
						<textarea name="content">${tmp.content }</textarea>
						<button type="submit">수정</button>
					</form>
					</c:if>
			</li>				
		</c:when>
		<c:otherwise>
        <li <c:if test="${tmp.num ne tmp.comment_group }">style="padding-left:50px;"</c:if> >삭제된 댓글 입니다.</li>
		</c:otherwise>
	</c:choose>
</c:forEach>
</ul>

댓글 삭제하기

자신의 댓글은 수정,삭제가 가능하다. 

 

Mapper-댓글 삭제하기

삭제시 댓글이 테이블에서 사라지는 것이아니라 '댓글이 삭제되었습니다'라고 출력할 것이기때문에

delete가아닌 update문을 수행한다. 삭제를 원하는 테이블의 deleted가 yes가 되도록 바꾼다.

	<delete id="delete" parameterType="int">
		UPDATE board_cafe_comment
		SET deleted='yes'
		WHERE num=#{num}
	</delete>

 

datail.jsp-댓글 삭제를 눌렀을 때 호출되는 함수

	//댓글 삭제를 눌렀을때 호출되는 함수
	function deleteComment(num){
		var isDelete=confirm("확인을 누르면 댓글이 삭제 됩니다.");
		if(isDelete){
			//페이지 전환 없이 ajax요청을 통해서 삭제하기
			$.ajax({
				url:"comment_delete.do",//    <-상대경로로        절대경로로->"/cafe/comment_delete.do"요청
				method:"post",
				data:{"num":num}, // num이라는 파라미터명으로 삭제할 댓글의 번호 전송
				success:function(responseData){
					if(responseData.isSuccess){
						var sel="#comment"+num;
						$(sel).text("삭제된 댓글 입니다.");
					}
				}
			});
		}
	}

 

cafeController - 댓글삭제 요청처리 

@responsebody + return type : map 이면 ajax형태로 처리된다.

ajax실행 시 "isSuccess"로 true를 리턴하게 한다.

	//댓글 삭제 요청 처리\
	@ResponseBody
	@RequestMapping(value= "/cafe/comment_delete",
			method=RequestMethod.POST)
	public Map<String, Object> 
		authCommentDelete(HttpServletRequest request,
			@RequestParam int num){
		service.deleteComment(num);
		Map<String,Object> map=new HashMap<>();
		map.put("isSuccess",true);
		return map; //{"isSuccess":true}형식의 JSON문자열이 응답된다.
	}
}

 

LoginAspect

로그인하지않았다면 ajax를 아예 실행하지 않고 "isSuccess"로 false를 리턴하게한다.

	@Around("execution(java.util.Map auth*(..))")
	public Object loginCheckAjax(ProceedingJoinPoint joinPoint) throws Throwable {
		//aop 가 적용된 메소드에 전달된 값을 Object[] 로 얻어오기
		Object[] args=joinPoint.getArgs();
		//로그인 여부
		boolean isLogin=false;
		HttpServletRequest request=null;
		for(Object tmp:args) {
			//인자로 전달된 값중에 HttpServletRequest type 을 찾아서
			if(tmp instanceof HttpServletRequest) {
				//원래 type 으로 casting
				request=(HttpServletRequest)tmp;
				//HttpSession 객체 얻어내기 
				HttpSession session=request.getSession();
				//세션에 "id" 라는 키값으로 저장된게 있는지 확인(로그인 여부)
				if(session.getAttribute("id") != null) {
					isLogin=true;
				}
			}
		}
		//로그인 했는지 여부
		if(isLogin){
			// aop 가 적용된 메소드를 실행하고 
			Object obj=joinPoint.proceed();
			// 리턴되는 값을 리턴해 주기 
			return obj;
		}
		//로그인을 하지 않았으면
		Map<String, Object> map=new HashMap<>();
		map.put("isSuccess", false);
		return map;
		
	}

 


Mapper commnet-시퀀스 미리 생성하기

저장시점이 아닌 미리 글번호를 얻어와서 테이블에 삽입한다.

댓글의 글번호가 대댓글의 그룹번호로 쓰일 것이기 때문에 대댓글을 쓸 때는 

 

	<select id="getSequence" resultType="int">
		SELECT board_cafe_comment_seq.NEXTVAL
		FROM DUAL
	</select>

댓글 수정하기

 

로그인 되어있는 자신의 댓글에는 수정,삭제가 뜨고 수정을 누르면 아래의 수정폼이 뜬다.

 

detail.jsp-수정 링크 클릭 시 수정폼이 보여지게하는 이벤트 등록

	//댓글 수정 링크를 눌렀을때 호출되는 함수 등록
	$(".comment-update-link").click(function(){
		$(this)
		.parent().parent().parent()
		.find(".comment-update-form")
		.slideToggle(200);
	});

detail.jsp-수정폼 생성

<!-- 로그인한 아이디와 댓글의 작성자와 같으면 수정폼 출력 -->				
	<c:if test="${id eq tmp.writer }">
		<form class="comment-update-form" action="comment_update.do" method="post">
			<input type="hidden" name="num" value="${tmp.num }" />
			<textarea name="content">${tmp.content }</textarea>
			<button type="submit">수정</button>
		</form>
	</c:if>

datail.jsp-수정폼 제출 시 일어나는 이벤트 등록

폼을 제출은 하지 않을 것이기 때문에 return false;해준다.

	//댓글 수정 폼에 submit 이벤트가 일어났을때 호출되는 함수 등록
	$(".comment-update-form").on("submit", function(){
		// "private/comment_update.do"
		var url=$(this).attr("action");
		//폼에 작성된 내용을 query 문자열로 읽어온다.
		// num=댓글번호&content=댓글내용
		var data=$(this).serialize();
		//이벤트가 일어난 폼을 선택해서 변수에 담아 놓는다.
		var $this=$(this);
		$.ajax({
			url:url,
			method:"post",
			data:data,
			success:function(responseData){
				// responseData : {isSuccess:true}
				if(responseData.isSuccess){
					//폼을 안보이게 한다 
					$this.slideUp(200);
					//폼에 입력한 내용 읽어오기
					var content=$this.find("textarea").val();
					//pre 요소에 수정 반영하기 
					$this.parent().find("pre").text(content);
				}
			}
		});
		//폼 제출 막기 
		return false;
	});

Controller-댓글 수정요청 처리

ajax로 처리하기 위해 @Responsebody , return map 해준다. 

	//댓글 수정 요청 처리(ajax)
	@ResponseBody
	@RequestMapping("/cafe/comment_update")
	public Map<String,Object>
		authCommentUpdate(HttpServletRequest request,
				@ModelAttribute CafeCommentDto dto){
		service.updateComment(dto);
		Map<String,Object> map=new HashMap<>();
		map.put("isSuccess", true );
		return map;
	}

Mapper -댓글수정하기

	<update id="update" parameterType="cafeCommnetDto">
		UPDATE board_cafe_comment
		SET content =#{content}
		WHERE num=#{num}
	</update>

datail.jsp-댓글 textarea에 click이벤트가 일어났을 때 로그인 여부에 따라 반응하기

비로그인 상태에서 click시 팝업창이 뜬다.

	//폼에 focus 이벤트가 일어 났을때 실행할 함수 등록 
	$(".comments form textarea").on("click", function(){
		//로그인 여부
		var isLogin=${not empty id};
		if(isLogin==false){
			var isMove=confirm("로그인 페이지로 이동하시겠습니까?");
			if(isMove){
				location.href="${pageContext.request.contextPath}/users/loginform.do?url=${pageContext.request.contextPath}/cafe/detail.do?num=${dto.num}";
			}
		}
	});