댓글 관련 기능 구현 (2)

DeadWhale·2022년 5월 30일
0

Servlet/JSP

목록 보기
17/22
post-thumbnail

개요

게시글을 상세히 조회 후 댓글을 조회/작성/삭제/수정하는 서비스

페이지를 로딩하면서 모든 댓글을 조회하는 selectReplyList()함수를 호출하는 경우 모든 페이지를 호출한 다음 댓글 목록을 조회해서 추가하는 수행 서순이라 상대적으로 댓글이 느리게 보여진다

이런한 문제때문에 페이지를 불러오면서 JSP EL로 페이지를 따로 만들어
include해 페이지를 읽어오면서 동시에 불러온다.

<jsp:include page="/WEB-INF/views/board/reply.jsp"/>

boardDetail.jsp에서 어떻게 댓글목록을 조회해 오냐 하면
boardDetail Servlet에서 댓글 목록을 조회한 다음 setAttribute해와 가져온다.

// 게시글 상세조회가 되었을 경우
if(detail != null) { 
//rList한번만 조회하다 보니 댓글 조회 서비스 메서드를 호출하면서 바로 메서드를 호출해온다.
List<Reply> rList =new ReplyService().selectReplyList(boardNo);
//req객체에 setting
req.setAttribute("rList", rList); 
}
//jsp로 전달
req.setAttribute("detail", detail);

전체적인 형태는 reply.js에서 샘플로 만들어봤던게 있어서
비교적 많들기 쉬웠던거같다.

jsp button에 이벤트속성을 추가한 후 js함수를 호출한다.

## 로그인한 유저의 번호와 == 댓글 작성자의 번호가 같은 경우
<c:if test="${loginMember.memberNo==reply.memberNo}">
  <div class="reply-btn-area">
    <button onclick="showUpdateReply(${reply.replyNo},this)">수정</button>
    <button onclick="deleteReply(${reply.replyNo})">삭제</button>
  </div>
</c:if> 

댓글 작성

textarea영역에 작성된 내용을 reply테이블에 insert한다.
이 때 입력영역에 여러칸의 공백만 입력 혹은 아무것도 입력되어있지 않은 경우에는 alert를 전달하면서 바로 함수를 종료시킨다.

const addReply = document.getElementById("addReply");
const replyContent = document.getElementById("replyContent");

addReply.addEventListener("click",function(){ //버튼이 클릭되었을 때
    //전역변수 회원번호를 이용해 로그인 여부 확인
    if(loginMemberNo == "" ){
        alert("로그인 후 이용해주세요")
        return;
    }
    
    //댓글 내용이 작성되어 있나 확인
    if(replyContent.value.trim().length==0){ //미작성
        alert("댓글을 입력해주세요")
        replyContent.value="";
        replyContent.focus();
        return;
    }

그 후 ajax를 통해 비동기조회를 한다.


$.ajax({
        url:contextPath+"/reply/insert",
        data:{
            "replyContent":replyContent.value,
            "memberNo":loginMemberNo,
            "boardNo":boardNo},
        type:"post",
        success:function(result){
            if(result>0){//등록 성공
                alert("댓글이 등록되었습니다.")
                replyContent.value="";
                selectReplyList();
            }else{ //등록 실패
                alert("댓글 등록이 실패했습니다..")
            }
        },
        error:function(req,status,error){
            console.log("댓글 등록 실패");
            console.log(req.responseText);
        }

댓글이 정상되면 전역변수로 선언되어있는 textarea영역을 들어있는 값을 전부 지우고 댓글 목록을 조회하는 js함수를 호출한다.


댓글 삭제

deleteReply 함수를 호출해 댓글의 상태를 나타내는 컬럼인 REPLY_ST의 상태를 삭제를 나타내는 'Y'로 변경한다.

function deleteReply(replyNo){
    if(confirm("정말로 삭제 하시겠습니까?")){
        $.ajax({
            url:contextPath+"/reply/delete",
            data:{"replyNo":replyNo},
            type:"get",
            success:function(result){
                if(result>0){
                    alert("삭제되었습니다.")
                    selectReplyList();
                }else{
                    alert("삭제 실패했습니다..")
                }},
            error:function(req,status,error){
                console.log("댓글 삭제 실패");
                console.log(req.responseText);}
        })
    }
}

confirm을 이용해 사용자의 의사를 확인하 바로 ajax를 통해
비동기 통신을 요청한다.


댓글 수정

댓글 수정의 경우 생각할 부분이 생각 보다 많아서 놀랐다
그냥 원래 reply.content를 불러와 textarea에 넣으면 될 줄 알았는데 여러가지 고려할 사항이 많았다.


1) 단계 댓글 수정을 누르면 그 행이 선택
2) 단계 행 내용을 지우기 전 원래 상태를 저장(백업)
3) 단계 댓글에 작성되어있던 내용만 얻어오기
4번)댓글 행 내부 내용을 모두 삭제.
5번) textarea 요소 생성 + 클래스 추가 + 내용 추가
6번) replyRow에 생성된 textarea 추가
7번) 버튼 영역 + 수정 / 취소버튼 생성
아래 순서대로 수행된다.

let beforeReplyRow; //수정전 원래 행의 상태를 저장할 변수 (백업용 변수)
function showUpdateReply(replyNo,btn){
    //            댓글번호   ,   이벤트 발생요소(수정버튼)

    //**  댓글수정이 한개만 열릴 수 있도록 만들기 */
    const temp = document.querySelectorAll(".update-textarea");

  
  ## 이 부분이 느낌점이 많은 코드였는데 간단한데 현명한 로직같았다
  ## 취소버튼을 누르면 원래 값을 HTML형태로 집어넣어 복원하는 느낌인대
  ## 이와 같은 원리로 이미 열려있는 update-text-area가 있는 경우 복원하는 느낌이다.
    if(temp.length>0){
        if(confirm("다른 댓글이 수정 중입니다. 현재 댓글을 수정하시겠습니까?")){ 
            temp[0].parentElement.innerHTML = beforeReplyRow;
          }else{
            return;
        }
    } 

  
## 헷갈리는데 직독직해 하면 이해할만하다
## 눌린 버튼의 부모(버튼영역을 감싸는 div)부모(수정영역을 감싸는 div) 타겟팅한다
## 이 부모는 한행을 의미하게 된다.
     const replyRow = btn.parentElement.parentElement
     beforeReplyRow = replyRow.innerHTML;
  
## 한행의 자식중 1번째는 실제 내용의 위치인대 이 영역의 태그또한 문자열로 저장한다 이건 나중에 취소나 다른창을 닫아버리는데 사용한다.
    let beforeContent = replyRow.children[1].innerHTML;
## 백업용 문자열을(태그포함) 저장하고 한행을 전부 비워버린다.
     replyRow.innerHTML="";

    
      const textarea = document.createElement("textarea");
    textarea.classList.add("update-textarea");

    //*************************** */
  ##아래에서 다시 한번 더 설명적음
    // XSS 방지처리를 해제
    beforeContent = beforeContent.replaceAll("&amp;","&");
    beforeContent = beforeContent.replaceAll("&lt;","<");
    beforeContent = beforeContent.replaceAll("&gt;",">");
    beforeContent = beforeContent.replaceAll("&quot;","\""); //이스케이프 수행 시켜줘야됨.
    
    //&lt;script&gt;&lt;/script&gt;
    
    // 개행문자 처리도 해제
    beforeContent = beforeContent.replaceAll("<br>","\n");
     textarea.value = beforeContent;
    
    replyRow.append(textarea);


    const replyBtnArea = document.createElement("div");
    replyBtnArea.classList.add("reply-btn-area");
    //버튼생성
    const updateBtn = document.createElement("button");
    updateBtn.innerText="수정";
    updateBtn.setAttribute("onclick","updateReply("+replyNo+",this)");
   
    const cancelBtn =document.createElement("button");
    cancelBtn.innerText="취소";
    cancelBtn.setAttribute("onclick","updateCancel(this)")
    replyBtnArea.append(updateBtn,cancelBtn); 
    replyRow.append(replyBtnArea);
}

## 위는 요소를 다시 만드는 영역
## 다만 수정과 취소 버튼의 이벤트가 다르게 사용된다.

//-----------------------댓글 수정을 취소 ----------------------------
function updateCancel(btn){
    //매개변수 btn : 클릭된 취소 버튼
    //beforeReplyRow : 수정 전의 원래 행의 댓글을 저장한 변수.

if(confirm("댓글 수정을 취소하시겠습니까?")){
	btn.parentElement.parentElement.innerHTML = beforeReplyRow;
    }
}
## 위에서 미리 만들어둔 HTML태그까지 포함된 문자열을 집어넣는데
## 이게 문자열이지만 innerHTML으로 집어넣어 태그로 인식된다.

댓글 수정 ajax

내용을 수정하는 update문을 수행하는 ajax

function updateReply(replyNo,btn){
    //새로 작성된 댓글 내용 얻어오기
    const replyContent = btn.parentElement.previousElementSibling.value;
    $.ajax({
        url:contextPath+"/reply/update",
        data:{"replyNo":replyNo,"replyContent":replyContent},
        type:"post",
        success:function(result){
            if(result>0){//등록 성공
                alert("댓글이 수정되었습니다.")
                selectReplyList();
            }else{ //등록 실패
                alert("댓글 수정이 실패했습니다..")
            }
        },
        error:function(req,status,error){
            console.log("댓글 수정 실패");
            console.log(req.responseText);
        }
    })
}

데이터 조정 (Util)

개행문자 변경 처리

공부하면 할수록 신기한게 하나씩 나오는 것같다
textArea에 띄어쓰기가 있을 경우 \n 으로 적히게 되는데
이게 코드상에서 < br> 으로 적용하게 하기 위해서는 개행문자의 변경 처리가 따로 필요하다
아래는 이를 변경하기 위한 코드

//댓글 등록/수정
//게시글 등록 / 수정
//내소개 / 상품정보같이 띄어쓰기 있는 모든 분야에 사용.
//reply.setReplyContent(reply.getReplyContent().replaceAll("(\n|\r|\r\n|\n\r)", "<br>"));

//Cross site Script(ing) (크로스 사이트 스크립트(팅)) 공격 방지 처리 -> util에 작성
reply.setReplyContent(Util.XssHandling(reply.getReplyContent()));

XSS(Cross Site Scripting)

크로스사이트스크립팅 공격방지

각종 태그 형식 / 이스케이프문자 / 엔퍼센트등 스크립트 방식의 문자열을 삽입해 공격할 수 있는 웹 취약점중 하나이다
근대 뭐 간단하게 막을 수 있다.

// 여기서는 html < br > 으로 변경하기 때문에 개행문자 변환 메서드를 후순위로 호출해야한다.
//static으로 선언해둔 개행문자 변경후 반환하는 메서드 사용
reply.setReplyContent(Util.newLineHandling(reply.getReplyContent()));

위 두개의 기능은 공통적으로 사용되는 곳이 많아 Util이라는 class에
public static선언으로 많은 재사용이 가능하다.

public class Util {
	//개행문자-> <br>태그 변경 메서드 메서드
	public static String newLineHandling(String content) {
		return content.replaceAll("(\n|\r|\r\n|\n\r)", "<br>");
	}
	
	//XSS : 관리자가 아닌 이용자가 악성 스크립트를 입력칸에 삽입해 공격하는. 웹 취약점 중하나
	//Cross site Script(ing) (크로스 사이트 스크립트(팅)) 공격 방지 처리 -> util에 작성
	public static String XssHandling(String content) {
		// < , > (태그) , &nbsp(띄어쓰기) , ""(속성값)
		//HTML 코드가 아닌 문자그대로 보이게 변경.
		if(content != null) {
			content = content.replaceAll("&","&amp;"); //& 기호 변환이 최상단에 위치해야 된다.
			content = content.replaceAll("<","&lt;");
			content = content.replaceAll(">","&gt;");
			content = content.replaceAll("\"","&quot;");
		}
		return content;
	}
}

0개의 댓글