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 가 계속 계산해보도록 하는 것이다.
'국비교육(22-23)' 카테고리의 다른 글
58일차(1)/Spring(22) : 갤러리 게시판 구현 (1) | 2022.12.29 |
---|---|
57일차(2)/Spring(21) : Transaction, DataAccessException 활용 예제 (1) | 2022.12.28 |
56일차(3)/Spring(19) : 게시판 댓글 기능 구현 (0) | 2022.12.27 |
56일차(2)/Spring(18) : 예외 클래스, Exception Controller (0) | 2022.12.26 |
56일차(1)/Spring Boot(2) : Bean 생성하는 방법, AOP 활용 실습 (0) | 2022.12.26 |