국비교육(22-23)

57일차(1)/Spring(20) : 게시판 댓글 기능 코드 리뷰

서리/Seori 2022. 12. 27. 23:27

57일차(1)/Spring(20) : 게시판 댓글 기능 코드 리뷰

 

 

detail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>/views/cafe/detail.jsp</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<style>
   .content{
      border: 1px dotted gray;
   }
   
   /* 댓글 프로필 이미지를 작은 원형으로 만든다. */
   .profile-image{
      width: 50px;
      height: 50px;
      border: 1px solid #cecece;
      border-radius: 50%;
   }
   /* ul 요소의 기본 스타일 제거 */
   .comments ul{
      padding: 0;
      margin: 0;
      list-style-type: none;
   }
   .comments dt{
      margin-top: 5px;
   }
   .comments dd{
      margin-left: 50px;
   }
   .comment-form textarea, .comment-form button{
      float: left;
   }
   .comments li{
      clear: left;
   }
   .comments ul li{
      border-top: 1px solid #888;
   }
   .comment-form textarea{
      width: 84%;
      height: 100px;
   }
   .comment-form button{
      width: 14%;
      height: 100px;
   }
   /* 댓글에 댓글을 다는 폼과 수정폼은 일단 숨긴다. */
   .comments .comment-form{
      display: none;
   }
   /* .reply_icon 을 li 요소를 기준으로 배치 하기 */
   .comments li{
      position: relative;
   }
   .comments .reply-icon{
      position: absolute;
      top: 1em;
      left: 1em;
      color: red;
   }
   pre {
     display: block;
     padding: 9.5px;
     margin: 0 0 10px;
     font-size: 13px;
     line-height: 1.42857143;
     color: #333333;
     word-break: break-all;
     word-wrap: break-word;
     background-color: #f5f5f5;
     border: 1px solid #ccc;
     border-radius: 4px;
   }   
   
   .loader{
      /* 로딩 이미지를 가운데 정렬하기 위해 */
      text-align: center;
      /* 일단 숨겨 놓기 */
      display: none;
   }   
   
   .loader svg{
      animation: rotateAni 1s ease-out infinite;
   }
   
   @keyframes rotateAni{
      0%{
         transform: rotate(0deg);
      }
      100%{
         transform: rotate(360deg);
      }
   }
</style>
</head>
<body>
   <div class="container">
      
      <%-- 만일 이전글(더 옛날글)의 글번호가 0 가 아니라면(이전글이 존재 한다면) --%>
      <c:if test="${dto.prevNum ne 0}">
         <a href="detail?num=${dto.prevNum }&condition=${condition}&keyword=${encodedK}">이전글</a>
      </c:if>
      
      <%-- 만일 다음글(더 최신글)의 글번호가 0 가 아니라면(다음글이 존재 한다면) --%>
      <c:if test="${dto.nextNum ne 0 }">
         <a href="detail?num=${dto.nextNum }&condition=${condition}&keyword=${encodedK}">다음글</a>
      </c:if>
      
      <%-- 만일 검색 키워드가 있다면 --%>
      <c:if test="${not empty keyword }">
         <p>
            <strong>${condition }</strong> 조건 
            <strong>${keyword }</strong> 검색어로 검색된 내용 자세히 보기
         </p>
      </c:if>
      <h3>글 상세 보기</h3>
      <table>
         <tr>
            <th>글번호</th>
            <td>${dto.num }</td>
         </tr>
         <tr>
            <th>작성자</th>
            <td>${dto.writer }</td>
         </tr>
         <tr>
            <th>제목</th>
            <td>${dto.title }</td>
         </tr>
         <tr>
            <th>조회수</th>
            <td>${dto.viewCount }</td>   
         </tr>
         <tr>
            <th>작성일</th>
            <td>${dto.regdate }</td>
         </tr>
         <tr>
            <td colspan="2">
               <div>${dto.content }</div>
            </td>
         </tr>   
      </table>
      <c:if test="${sessionScope.id eq dto.writer }">
         <a href="updateform?num=${dto.num }">수정</a>
         <a href="javascript:" onclick="deleteConfirm()">삭제</a>
         <script>
            function deleteConfirm(){
               const isDelete=confirm("이 글을 삭제 하겠습니까?");
               if(isDelete){
                  location.href="delete?num=${dto.num}";
               }
            }
         </script>
      </c:if>
      <h4>댓글을 입력해주세요</h4>
      <!-- 원글에 댓글을 작성할 폼 -->
      <form class="comment-form insert-form" action="comment_insert" method="post">
         <!-- 원글의 글번호가 댓글의 ref_group 번호가 된다. -->
         <input type="hidden" name="ref_group" value="${dto.num }"/>
         <!-- 원글의 작성자가 댓글의 대상자가 된다. -->
         <input type="hidden" name="target_id" value="${dto.writer }"/>
   
         <textarea name="content">${empty id ? '댓글 작성을 위해 로그인이 필요 합니다.' : '' }</textarea>
         <button type="submit">등록</button>
      </form>
      
      <!-- 댓글 목록 -->
      <div class="comments">      
      
         <ul>
            <c:forEach var="tmp" items="${commentList }">
               <c:choose>
                  <c:when test="${tmp.deleted eq 'yes' }">
                     <li>삭제된 댓글 입니다.</li>
                  </c:when>
                  <c:otherwise>
                     <c:if test="${tmp.num eq tmp.comment_group }">
                        <li id="reli${tmp.num }">
                     </c:if>
                     <c:if test="${tmp.num ne tmp.comment_group }">
                        <li id="reli${tmp.num }" style="padding-left:50px;">
                           <svg class="reply-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-return-right" viewBox="0 0 16 16">
                                <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
                           </svg>
                     </c:if>
                           <dl>
                              <dt>
                                 <c:if test="${ empty tmp.profile }">
                                    <svg class="profile-image" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
                                      <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
                                      <path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
                                    </svg>
                                 </c:if>
                                 <c:if test="${not empty tmp.profile }">
                                    <img class="profile-image" src="${pageContext.request.contextPath}${tmp.profile }"/>
                                 </c:if>
                                 <span>${tmp.writer }</span>
                                 <c:if test="${tmp.num ne tmp.comment_group }">
                                    @<i>${tmp.target_id }</i>
                                 </c:if>
                                 <span>${tmp.regdate }</span>
                                 <a data-num="${tmp.num }" href="javascript:" class="reply-link">답글</a>
                                 <c:if test="${ (id ne null) and (tmp.writer eq id) }">
                                    <a data-num="${tmp.num }" class="update-link" href="javascript:">수정</a>
                                    <a data-num="${tmp.num }" class="delete-link" href="javascript:">삭제</a>
                                 </c:if>
                              </dt>
                              <dd>
                                 <pre id="pre${tmp.num }">${tmp.content }</pre>                  
                              </dd>
                           </dl>
                           <form id="reForm${tmp.num }" class="animate__animated comment-form re-insert-form" action="comment_insert" 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"></textarea>
                              <button type="submit">등록</button>
                           </form>
                        <c:if test="${tmp.writer eq id }">
                           <form id="updateForm${tmp.num }" class="comment-form update-form" action="comment_update" method="post">
                              <input type="hidden" name="num" value="${tmp.num }" />
                              <textarea name="content">${tmp.content }</textarea>
                              <button type="submit">수정</button>
                           </form>
                        </c:if>
                        </li>      
                  </c:otherwise>
               </c:choose>
            </c:forEach>
         </ul>
      </div>      
      <div class="loader">
         <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
              <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
              <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
         </svg>
      </div>
      
   </div>
   
   <script src="${pageContext.request.contextPath}/resources/js/sy_util.js"></script>
   <script>
      
      //클라이언트가 로그인 했는지 여부
      let isLogin=${ not empty id };
      
      document.querySelector(".insert-form")
         .addEventListener("submit", function(e){
            //만일 로그인 하지 않았으면 
            if(!isLogin){
               //폼 전송을 막고 
               e.preventDefault();
               //로그인 폼으로 이동 시킨다.
               location.href=
                  "${pageContext.request.contextPath}/users/loginform?url=${pageContext.request.contextPath}/cafe/detail?num=${dto.num}";
            }
         });
      
      /*
         detail
          페이지 로딩 시점에 만들어진 1 페이지에 해당하는 
         댓글에 이벤트 리스너 등록 하기 
      */
      addUpdateFormListener(".update-form");
      addUpdateListener(".update-link");
      addDeleteListener(".delete-link");
      addReplyListener(".reply-link");
      
      
      //댓글의 현재 페이지 번호를 관리할 변수를 만들고 초기값 1 대입하기
      let currentPage=1;
      //마지막 페이지는 totalPageCount 이다.  
      let lastPage=${totalPageCount};
      
      //추가로 댓글을 요청하고 그 작업이 끝났는지 여부를 관리할 변수 
      let isLoading=false; //현재 로딩중인지 여부 
      
      /*
         window.scrollY => 위쪽으로 스크롤된 길이
         window.innerHeight => 웹브라우저의 창의 높이
         document.body.offsetHeight => body 의 높이 (문서객체가 차지하는 높이)
      */
      window.addEventListener("scroll", function(){
         //바닥 까지 스크롤 했는지 여부 
         const isBottom = 
            window.innerHeight + window.scrollY  >= document.body.offsetHeight;
         //현재 페이지가 마지막 페이지인지 여부 알아내기
         let isLast = currentPage == lastPage;   
         //현재 바닥까지 스크롤 했고 로딩중이 아니고 현재 페이지가 마지막이 아니라면
         if(isBottom && !isLoading && !isLast){
            //로딩바 띄우기
            document.querySelector(".loader").style.display="block";
            
            //로딩 작업중이라고 표시
            isLoading=true;
            
            //현재 댓글 페이지를 1 증가 시키고 
            currentPage++;
            
            /*
               해당 페이지의 내용을 ajax 요청을 통해서 받아온다.
               "pageNum=xxx&num=xxx" 형식으로 GET 방식 파라미터를 전달한다. 
            */
            ajaxPromise("ajax_comment_list","get",
                  "pageNum="+currentPage+"&num=${dto.num}")
            .then(function(response){
               //json 이 아닌 html 문자열을 응답받았기 때문에  return response.text() 해준다.
               return response.text();
            })
            .then(function(data){
               //data 는 html 형식의 문자열이다. 
               console.log(data);
               // beforebegin | afterbegin | beforeend | afterend
               document.querySelector(".comments ul")
                  .insertAdjacentHTML("beforeend", data);
               //로딩이 끝났다고 표시한다.
               isLoading=false;
               //새로 추가된 댓글 li 요소 안에 있는 a 요소를 찾아서 이벤트 리스너 등록 하기 
               addUpdateListener(".page-"+currentPage+" .update-link");
               addDeleteListener(".page-"+currentPage+" .delete-link");
               addReplyListener(".page-"+currentPage+" .reply-link");
               //새로 추가된 댓글 li 요소 안에 있는 댓글 수정폼에 이벤트 리스너 등록하기
               addUpdateFormListener(".page-"+currentPage+" .update-form");
               
               //로딩바 숨기기
               document.querySelector(".loader").style.display="none";
            });
         }
      });
      
      //인자로 전달되는 선택자를 이용해서 이벤트 리스너를 등록하는 함수 
      function addUpdateListener(sel){
         //댓글 수정 링크의 참조값을 배열에 담아오기 
         // sel 은  ".page-xxx  .update-link" 형식의 내용이다 
         let updateLinks=document.querySelectorAll(sel);
         for(let i=0; i<updateLinks.length; i++){
            updateLinks[i].addEventListener("click", function(){
               //click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다. 
               const num=this.getAttribute("data-num"); //댓글의 글번호
               document.querySelector("#updateForm"+num).style.display="block";
               
            });
         }
      }
      function addDeleteListener(sel){
         //댓글 삭제 링크의 참조값을 배열에 담아오기 
         let deleteLinks=document.querySelectorAll(sel);
         for(let i=0; i<deleteLinks.length; i++){
            deleteLinks[i].addEventListener("click", function(){
               //click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다. 
               const num=this.getAttribute("data-num"); //댓글의 글번호
               const isDelete=confirm("댓글을 삭제 하시겠습니까?");
               if(isDelete){
                  // sy_util.js 에 있는 함수들 이용해서 ajax 요청
                  ajaxPromise("comment_delete.do", "post", "num="+num)
                  .then(function(response){
                     return response.json();
                  })
                  .then(function(data){
                     //만일 삭제 성공이면 
                     if(data.isSuccess){
                        //댓글이 있는 곳에 삭제된 댓글입니다를 출력해 준다. 
                        document.querySelector("#reli"+num).innerText="삭제된 댓글입니다.";
                     }
                  });
               }
            });
         }
      }
      function addReplyListener(sel){
         //댓글 링크의 참조값을 배열에 담아오기 
         let replyLinks=document.querySelectorAll(sel);
         //반복문 돌면서 모든 링크에 이벤트 리스너 함수 등록하기
         for(let i=0; i<replyLinks.length; i++){
            replyLinks[i].addEventListener("click", function(){
               
               if(!isLogin){
                  const isMove=confirm("로그인이 필요 합니다. 로그인 페이지로 이동 하시겠습니까?");
                  if(isMove){
                     location.href=
                        "${pageContext.request.contextPath}/users/loginform?url=${pageContext.request.contextPath}/cafe/detail?num=${dto.num}";
                  }
                  return;
               }
               
               //click 이벤트가 일어난 바로 그 요소의 data-num 속성의 value 값을 읽어온다. 
               const num=this.getAttribute("data-num"); //댓글의 글번호
               
               const form=document.querySelector("#reForm"+num);
               
               //현재 문자열을 읽어온다 ( "답글" or "취소" )
               let current = this.innerText;
               
               if(current == "답글"){
                  //번호를 이용해서 댓글의 댓글폼을 선택해서 보이게 한다. 
                  form.style.display="block";
                  form.classList.add("animate__fadeInLeft");
                  this.innerText="취소";   
                  form.addEventListener("animationend", function(){
                     form.classList.remove("animate__fadeInLeft");
                  }, {once:true});
               }else if(current == "취소"){
                  form.classList.add("animate__fadeOut");
                  this.innerText="답글";
                  form.addEventListener("animationend", function(){
                     form.classList.remove("animate__fadeOut");
                     form.style.display="none";
                  },{once:true});
               }
            });
         }
      }
      
      function addUpdateFormListener(sel){
         //댓글 수정 폼의 참조값을 배열에 담아오기
         let updateForms=document.querySelectorAll(sel);
         for(let i=0; i<updateForms.length; i++){
            //폼에 submit 이벤트가 일어 났을때 호출되는 함수 등록 
            updateForms[i].addEventListener("submit", function(e){
               //submit 이벤트가 일어난 form 의 참조값을 form 이라는 변수에 담기 
               const form=this;
               //폼 제출을 막은 다음 
               e.preventDefault();
               //이벤트가 일어난 폼을 ajax 전송하도록 한다.
               ajaxFormPromise(form)
               .then(function(response){
                  return response.json();
               })
               .then(function(data){
                  if(data.isSuccess){
                     /*
                        document.querySelector() 는 html 문서 전체에서 특정 요소의 
                        참조값을 찾는 기능
                        
                        특정문서의 참조값.querySelector() 는 해당 문서 객체의 자손 요소 중에서
                        특정 요소의 참조값을 찾는 기능
                     */
                     const num=form.querySelector("input[name=num]").value;
                     const content=form.querySelector("textarea[name=content]").value;
                     //수정폼에 입력한 value 값을 pre 요소에도 출력하기 
                     document.querySelector("#pre"+num).innerText=content;
                     form.style.display="none";
                  }
               });
            });
         }
      }
   </script>
</body>
</html>

 

ajax_commentList

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<c:forEach var="tmp" items="${commentList }">
   <c:choose>
      <c:when test="${tmp.deleted eq 'yes' }">
         <li>삭제된 댓글 입니다.</li>
      </c:when>
      <c:otherwise>
         <c:if test="${tmp.num eq tmp.comment_group }">
            <li id="reli${tmp.num }" class="page-${pageNum }">
         </c:if>
         <c:if test="${tmp.num ne tmp.comment_group }">
            <li id="reli${tmp.num }" class="page-${pageNum }" style="padding-left:50px;" >
               <svg class="reply-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-return-right" viewBox="0 0 16 16">
                      <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
               </svg>
         </c:if>
               <dl>
                  <dt>
                     <c:if test="${ empty tmp.profile }">
                        <svg class="profile-image" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
                          <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
                          <path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
                        </svg>
                     </c:if>
                     <c:if test="${not empty tmp.profile }">
                        <img class="profile-image" src="${pageContext.request.contextPath}${tmp.profile }"/>
                     </c:if>
                     <span>${tmp.writer }</span>
                     <c:if test="${tmp.num ne tmp.comment_group }">
                        @<i>${tmp.target_id }</i>
                     </c:if>
                     <span>${tmp.regdate }</span>
                     <a data-num="${tmp.num }" href="javascript:" class="reply-link">답글</a>
                     <c:if test="${ (id ne null) and (tmp.writer eq id) }">
                        <a data-num="${tmp.num }" class="update-link" href="javascript:">수정</a>
                        <a data-num="${tmp.num }" class="delete-link" href="javascript:">삭제</a>
                     </c:if>
                  </dt>
                  <dd>
                     <pre id="pre${tmp.num }">${tmp.content }</pre>                  
                  </dd>
               </dl>
               <form id="reForm${tmp.num }" class="animate__animated comment-form re-insert-form" action="comment_insert" method="post">
                  <input type="hidden" name="ref_group" value="${num }"/>
                  <input type="hidden" name="target_id" value="${tmp.writer }"/>
                  <input type="hidden" name="comment_group" value="${tmp.comment_group }"/>
                  <textarea name="content"></textarea>
                  <button type="submit">등록</button>
               </form>
            <c:if test="${tmp.writer eq id }">
               <form id="updateForm${tmp.num }" class="comment-form update-form" action="comment_update" method="post">
                  <input type="hidden" name="num" value="${tmp.num }" />
                  <textarea name="content">${tmp.content }</textarea>
                  <button type="submit">수정</button>
               </form>
            </c:if>
            </li>      
      </c:otherwise>
   </c:choose>
</c:forEach>

 


 

- 댓글을 저장하는 board_cafe 테이블

 

num: 번호는 시퀀스

writer: 작성자는 users_id를 참조하게 되어 있다

content : 댓글의 내용

target_id 댓글의 대상자

ref_group 원글의 글번호 저장. 원본 글의 primary key! 댓글 목록을 가져오는 기준.

 ex) 3번 글에 달린 댓글은 ref_group이 모두 3이다. 나중에 select를 편하게 하기 위해 부여한 값

comment_group : 댓글 내의 그룹

deleted : 삭제 처리

 

ref_group

- 첫번째 빨간 박스는 댓글의 대상자가 확실하지만, 아래에 또 댓글이 달리면 구분이 어렵다.

- 대댓글이 여러개 달리다보면 대상자가 모호하기 때문에 헷갈릴 수 있다.

 

- 위와 같이 들여쓰기를 여러단계로 한다면 대상자를 굳이 표시하지 않아도 별로 상관없지만

  레이아웃이 망가질 수도 있다... 그래서 한 단계만 들여쓰도록 했다.

 

comment_group 의 구조

- 1은 단일그룹, 2는 3개의 댓글이 하나의 그룹, 3은 2개의 댓글이 하나의 그룹

- 같은 댓글 그룹끼리는 몰려있어야 한다.

- 몰려있게 하기 위해서 comment_group 이라는 번호를 부여한 것이다.

 

- 위의 이미지에서는 댓글 그룹이 3개인 것. 번호를 이런 방식으로 부여한다.

- 원글의 댓글은 그룹번호와 댓글 본인의 번호가 같지만, 댓글의 댓글은 얼마든지 다를 수 있다.

- 원글의 댓글의 글 번호를 comment group 번호로 지정한다.

- 정렬할 때 comment group 번호를 사용해 정렬하면 같은 그룹끼리 몰려있을 수 있다!

 

deptno ASC

- emp 테이블 부서번호를 기준으로 정렬하면 같은 부서번호끼리 몰려있기 가능!

 

deptno ASC, ename ASC

- 2번 정렬하면 부서번호 그룹 내에서도 오름차순 정렬도 된다.

 

- mapper sql문을 확인해보면 2번 정렬한 것을 볼 수 있다.

- 일단 그룹별로 몰려있게 하고, 그 안에서 대댓글끼리도 시간 순서대로 정렬되게 한 것.

- 정렬만 되면 들여쓰기만 해서 순서대로 출력하면 된다.

 

- 원글의 댓글 or 대댓글을 판정하는 기준은? → 댓글번호와 그룹번호가 다르면!

- 이것을 기준으로 해서 들여쓰기를 하느냐 마느냐를 결정한다.(뷰 페이지에서)

 

- 1은 그룹과 댓글자신의 번호가 같은 경우

- 2는 그룹과 댓글자신의 번호가 다른 경우(패딩, 꺾은 화살표 출력)

 

- 이것을 검증하면서 댓글을 반복문 돌며 쭉 출력해주는 것이다.

- 들여쓰기를 안 할 것이냐 할 것이냐의 차이!

 

 

- 댓글 <li> 안에 dl, dt 정의형 목록을 출력했다.

- 이런 li 하나하나가 댓글 하나하나이다.

 

 

- 페이지소스에서 확인해보면, 댓글에 아이디가 부여되어 있다.

- 자기 자신의 글 번호를 활용한 id를 함께 출력해 놓았다.

- #reli7 댓글 자신의 글번호를 지정해서 뭔가를 하고싶다면 이렇게 지정해서 사용가능!

 

 

- 이것은 2페이지에 해당하는 댓글

 

 

- 페이지 로딩시점에 가져온 댓글(빨강)

- ajax요청으로 추가로 가져온 댓글(초록). ul 안에 append한 것이다.

 

 

- 댓글마다 폼 두개가 미리 준비되어 있다. 대댓글 폼/수정 폼

- 버튼을 클릭하면 숨겨놓았던 것을 보이게 한다!

 

- 글 번호를 활용한 id가 부여되어 있다. 자바스크립트로 필요할 때 컨트롤하기 위해서!

- 자기 자신의 글번호만 알고 있다면 <li> 안의 원하는 요소를 찍어낼 수 있다.

 

 

 

- 답글을 누르면 data-num="6" 이라는 번호를 가지고 아래의 폼을 불러올 수 있다.(보이게 한다)

- 6이라는 숫자를 사용해서 아래 있는 폼을 선택할 수 있다.

 

- javascript 코드. 클릭시에 display 속성을 바꾸어준다.

- display 속성 중 block : 보이게하기 / none: 안보이게 하기

 

 

- 애니메이션(animate css)을 빼면 그냥 이런 간단한 코드이다.

- 보이고 숨기는 것을 제어하는 것뿐!

 

- 애니메이션 클래스를 추가하고 제거하는것!!

 

- flash : 깜박이는 효과

- fadeout : 서서히 사라지는 효과

 

- 구글에 animate css 검색 : 링크

 

 

- 이렇게 들어가는 클래스명만 수정하면 다른 애니메이션 효과가 적용될 수 있다.(bounceInDown 등)

- 사용 후에는 반드시 제거해주어야 한다! 그러지 않으면 애니메이션 효과가 한 번밖에 적용이 안 된다.

- 애니메이션이 끝나면 animationend 이벤트가 발생하도록 해서 클래스를 제거해주면 된다.

 


 

- 특정 링크를 누르면 data-num 속성을 읽어와서 javascript로 작업한다.

- 하단 javascript에서 모두 그 값을 사용하고 있다.

 

- data-num을 읽어와서 선택자로 활용한다.

- ajax 요청으로 번호를 보내버린다.

 

- 성공여부를 응답받아서 삭제한다. 이 이벤트에는 번호 값이 핵심이다.

- 링크가 여러 개이므로 여러개의 버튼에 다 이벤트가 걸리게 하려면 반복문으로 돌아야 한다.

 

 

- a 링크에 이벤트 리스너를 등록해야 하는데

  a 링크가 여러 개이기 때문에 반복문을 돌면서 이벤트 리스너를 등록해야 한다.

- 또한, ajax요청을 통해서 받아온 문자열을 이용해서 새로운 댓글을 화면에 추가하게 되면 거기에도 a 링크가 있다.

 이 새로 추가한 요소에도 반복문을 돌면서 이벤트 리스너를 등록해야 한다.

 그렇기 때문에 반복문으로 이벤트를 걸어주는 것이다.

- 그런 이유 때문에 이벤트 리스너를 등록하는 작업을 함수 안에서 하고,

 필요할 때마다 해당 함수를 호출해서 이벤트 리스너를 일괄 등록하는 코드가 존재하는 것이다.

 

- javascript에 함수가 3개 있다.

  addUpdate / addDelete / addReplyListener

- 새로운 댓글을 호출할때마다 이 3종류의 이벤트를 걸어주어야 하기 때문에 있는 것

 

 

- 여기에 문자열로 선택자만 넣어주면

 document.querySelectorAll 을 사용해 이 선택자에 부합하는 모든 요소를 배열에 담아와서 

 모든 방 [ i ] 을 돌면서 이벤트 리스너를 걸어주는 것이다.

 

- 클라이언트가 어떤 작업을 할지(어떤 버튼을 클릭할지) 알 수 없으므로,

 클라이언트가 누르기 전에 이미 버튼에 모든 이벤트가 등록되어 있어야 한다.

ex) 가게에서 일하는 알바가 10명이라면, 10명의 알바에게 다 일을 가르쳐두어야 한다.

그래야 손님이 들어왔을 때 모두 다 대응을 할 수 있기 때문에!

 

- 이 코드를 jQuery로 작성하게 된다면? 이렇게만 작성하면 알아서 반복문 돌면서 작성해준다.

선택된 요소의 모든 동작을 다 해준다고 보면 된다!

 


 

- 스크롤을 바닥까지 하면 화살표가 돌면서 ajax 요청이 들어가고, 추가댓글을 가져온다.

- 이미 페이징 처리가 되어있다. 최초 페이지 로딩시에는 1페이지만 가져온 것!

 

service - getDetail메소드

- dto에 startRowNum / EndRowNum을 담아서 사용

 

- totalPageCount를 request 영역에 담는 이유는? totalPageCount도 필요하기 때문에.

 왜? 1페이지만 있으면 ajax요청을 할 필요가없다.

 따라서 전체 페이지 수도 알아야 한다.

 

- detail 페이지 javascript에서 EL을 활용해서 totalPageCount를 읽어온다.

 

- 이 부분은 페이지가(웹브라우저가) 읽어오면서 true를 찍어낸 것이다.

 

- 바닥까지 스크롤했는지 여부(isBottom)

- 마지막 페이지인지 여부(isLast). 마지막 페이지인지는 여기서 알아내서 사용한다.

 

- 스크롤이 바닥이고 + 로딩중이 아니고(화살표 돌아가기) + 마지막 페이지도 아니면 또 추가 요청을 하도록 한다.

- 3가지 조건을 바탕으로 또 ajax 요청을 할 것인지 말 것인지를 결정하는 것이다.

- 로딩해오면 로딩아이콘 도는 작업을 하고 페이지 번호를 증가시킨다.

 

- json이 아닌 문자열을 응답했다.

- 댓글은 json으로 응답하기 불편해서. 할 수는 있다...

 

 

- 브라우저가 html형식으로 출력한다. 응답된 내용을 ul에 인접한 html로 해석

- insertAdjacentHTML : 인접한 HTML로 해석해달라고 하는 것

- ul안에 li가 여러개 있는 상태인데 어디에 추가되는 댓글을 넣을 것인가를 정하는 것이다.

 

- 3번 위치를 가리키는것이 beforeend이다

- 원하는 위치에 집어넣을 수 있는 옵션이 있는 것

 

- html 규칙에 맞게끔 해석하면서 집어넣으라는 의미

- 응답된 문자열이 html로 해석되면서, 이것을 ul 밑에 추가하라는 의미이다.

 

- 댓글이 새로 만들어지면 새로운 a 요소들이 추가되는데, 이벤트가 걸려있지 않은 상태로 생성된다.

- 그래서 그 아래에서 함수를 추가하면서 이벤트리스너를 등록해 주는 것(addUpdate, addDelete, addReplyListener)

 

 

- 기존에 있던 1페이지의 댓글에는 저 이벤트리스너가 또 적용되면 안된다.

- 새로 추가된 댓글은 이런 클래스를 넣어주었다.

- 그래서 이것을 선택자로 사용해서, 이 클래스가 있는 댓글에만 이벤트리스너를 적용하도록 하면 된다.

 

- 2페이지 하위에 있는 업데이트 링크에 이 리스너를 적용시키도록 코드 작성

 

 

 

- ajax commentList의 내용이 뷰페이지와 거의 같다. pageNum만 빼고...

 

- 여기서 응답한 문자열이 data 안으로 들어오는 것이다.

 


 

- 댓글목록 위에 댓글 창이 나오도록 수정

 

- comments 부분은 숨겨지므로 comment위에 넣어야한다.

 

 

- 댓글 더보기 기능이 별로면 페이지 처리를 해도 무방하다.

- 댓글 더보기 처리가 되는 방식은?

- window.innerHeight 값 + scrollY 값이 body.offsetHeight 보다 더 크면!! 조건이 해당된다.

- scrollY : 바닥까지 스크롤했는지 여부

 

 

- 빨간색이 윈도우(브라우저) 창

- 파란색이 창을 통해서 바라보는 문서(document)

- 창을 통해서 문서를 바라보는데, 일부만 보이는 것이다.

- window.innerHeight가 윈도우의 세로 높이

 

- offset, innerHeight 는 똑같은데 scrollY 값만 점점 커진다.

- 정확하게 두 값이 똑같은지 비교하는게 아니고, 합한 값 이상이면 로딩하도록 한 것(유사한 값이면 됨)

- 스크롤될 때마다 javascript 가 계속 계산해보도록 하는 것이다.