티스토리 뷰
[ 아이디어 정리 ]
- 임시 용어 정의. 댓글 : parent가 자기 자신인 댓글, 대댓글 : parent가 자기 자신이 아닌 댓글.
- reference 1번에 나오는 방법으로 parent, depth, order 필드를 이용하여 댓글 계층을 보여주는 방법. 이 글을 보면서 아이디어를 꽤 많이 얻었다. 그런데 내가 원하는 형태는 댓글에 대댓글을 한 번만 허용하도록 하는 형태이다. 대댓글에 다시 댓글을 달면 박스가 오른쪽으로 계속 밀리는 형태가 되는데, 매우 보기 싫다.
- 대댓글이 한 번만 허용되면 대댓글의 깊이가 모두 같으므로 depth 필드가 필요없다. 또 원 댓글의 parent를 null이 아니라 자기 자신으로 지정하면 order by parent option, number option을 이용해 간단하게 댓글을 정렬하여 가져올 수 있다. parent를 이용해 댓글과 대댓글을 묶어서 정렬하고 그 묶음 속에서 다시 댓글과 대댓글을 정렬하면 되기 때문이다. 댓글들의 번호가 댓글 < 대댓글1 < 대댓글2 < ... 이기 때문에 이 성질은 반드시 성립한다.
- 대댓글이 달린 댓글의 경우 물리적으로 삭제하는 것이 아니라 논리적으로 삭제하여 대댓글을 유지할 수 있다. 그리고 이렇게 구현하는 쪽이 삭제된 댓글에 대한 궁금증을 유발하여 커뮤니티에 좀 더 참여하도록 유도할 수 있다고 생각한다. 실제로 나도 삭제된 댓글이 욕 먹고 있으면 어떤 내용이었는지 물어보곤 했다.
* Reference
[ URL ]
- patch, delete에선 postNumber가 의미가 없는데, 이걸 지워야 하나? 아님 2번째가 postNumber로 통일되게 하는 게 좋은가 post 작업할 때도 그렇고 이게 가장 고민이다. 통일하는 게 기억하기 더 편해서 일단 통일하기로 결정한다.
GET comments/{postNumber} : comment list 조회
POST comments/{postNumber} : comment 등록
PATCH comments/{postNumber}/{commentNumber} : comment 수정
DELETE comments/{postNumber}/{commentNumber} : comment 삭제
[ DB Table and Sequence ]
create table comments (
comment_number number(20) primary key,
post_number number(20) references posts(post_number) on delete cascade,
comment_parent_number number(20) references comments(comment_number) on delete cascade,
comment_content varchar2(1000) not null,
comment_writer number(20) references users(user_number) on delete cascade,
date_commented date default sysdate not null,
date_comment_modified date,
comment_is_deleted char(1) default '0' not null
);
create sequence seq_comment;
[ CommentVO ]
@Data
public class CommentVO {
private Long number;
private Long postNumber;
private Long parentNumber;
private String content;
private String writer;
private Date dateCommented;
private Date dateModified;
private Boolean isDeleted;
}
[ CommentMapper Interface and Impl ]
public interface CommentMapper {
public int insertComment(CommentVO comment);
public int insertReplyComment(CommentVO comment);
public List<CommentVO> readCommentsByPostNumber(@Param("postNumber") Long postNumber, @Param("from") Long from, @Param("to") Long to);
public int readCommentsCountByPostNumber(Long postNumber);
public int readCommentsCountByParentCommentNumber(Long parentCommentNumber);
public int updateComment(CommentVO comment);
public int updateCommentIsDeletedByCommentNumber(@Param("commentNumber") Long commentNumber);
public int deleteCommentByCommentNumber(Long commentNumber);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bibidi.mapper.CommentMapper">
<resultMap type="com.bibidi.domain.CommentVO" id="commentMap">
<id property="number" column="comment_number"/>
<result property="number" column="comment_number" />
<result property="postNumber" column="post_nubmer" />
<result property="parentNumber" column="comment_parent_number" />
<result property="content" column="comment_content" />
<result property="writer" column="user_nickname" />
<result property="dateCommented" column="date_commented" />
<result property="dateModified" column="date_comment_modified" />
<result property="isDeleted" column="comment_is_deleted" />
</resultMap>
<insert id="insertComment">
<selectKey keyProperty="number" order="BEFORE" resultType="Long">
SELECT seq_comment.nextval FROM dual
</selectKey>
INSERT INTO comments
(comment_number, post_number, comment_parent_number, comment_content, comment_writer)
VALUES
(#{number}, #{postNumber}, #{number}, #{content},
(SELECT user_number FROM users WHERE user_nickname = #{writer}))
</insert>
<insert id="insertReplyComment">
INSERT INTO comments
(comment_number, post_number, comment_parent_number, comment_content, comment_writer)
VALUES
(seq_comment.nextval, #{postNumber}, #{parentNumber}, #{content},
(SELECT user_number FROM users WHERE user_nickname = #{writer}))
</insert>
<select id="readCommentsByPostNumber" resultMap="commentMap">
<![CDATA[
SELECT comment_number, post_number, comment_parent_number, comment_content, user_nickname, date_commented, date_comment_modified, comment_is_deleted
FROM
(SELECT comment_number, post_number, comment_parent_number, comment_content, user_nickname, date_commented, date_comment_modified, comment_is_deleted
FROM
(SELECT ROWNUM rn, comment_number, post_number, comment_parent_number, comment_content, user_nickname, date_commented, date_comment_modified, comment_is_deleted
FROM
(SELECT comment_number, post_number, comment_parent_number, comment_content, user_nickname, date_commented, date_comment_modified, comment_is_deleted
FROM comments
INNER JOIN users ON comment_writer = user_number
WHERE post_number = #{postNumber}
ORDER BY comment_parent_number DESC, comment_number DESC
)
WHERE ROWNUM <= #{to}
)
WHERE rn >= #{from}
)
ORDER BY comment_parent_number ASC, comment_number ASC
]]>
</select>
<select id="readCommentsCountByPostNumber" resultType="int">
SELECT count(*)FROM comments
WHERE post_number = #{postNumber}
</select>
<select id="readCommentsCountByParentCommentNumber" resultType="int">
SELECT count(*) FROM comments
WHERE comment_parent_number = #{parentCommentNumber}
</select>
<update id="updateComment">
UPDATE comments SET
comment_content = #{content},
date_comment_modified = sysdate
WHERE comment_number = ${number}
</update>
<update id="updateCommentIsDeletedByCommentNumber">
UPDATE comments SET
comment_is_deleted = '1'
WHERE comment_number = #{commentNumber}
</update>
<delete id="deleteCommentByCommentNumber">
DELETE FROM comments
WHERE comment_number = #{commentNumber}
</delete>
</mapper>
[ CommentMapper Tests ]
RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class CommentMapperTests {
@Setter(onMethod_ = @Autowired)
private CommentMapper commentMapper;
@Test
public void testInsertComment() {
Long postNumber = 80L;
CommentVO comment = new CommentVO();
comment.setPostNumber(postNumber);
comment.setContent("test comment");
comment.setWriter("bibidi");
log.info("THE NUMBER OF INSERTED COMMENT : " + commentMapper.insertComment(comment));
}
@Test
public void testInsertReplyComment() {
Long postNumber = 80L;
Long parentCommentNumber = 1L;
CommentVO comment = new CommentVO();
comment.setPostNumber(postNumber);
comment.setParentNumber(parentCommentNumber);
comment.setContent("test reply comment");
comment.setWriter("bibidi");
log.info("THE NUMBER OF INSERTED REPLY COMMENT : " + commentMapper.insertReplyComment(comment));
}
@Test
public void testReadCommentsByPostNumber() {
Long postNumber = 80L;
Long from = 1L;
Long to = 10L;
commentMapper
.readCommentsByPostNumber(postNumber, from, to)
.forEach(comment -> log.info(comment));
}
@Test
public void testReadCommentsCountByPostNumber() {
Long postNumber = 1L;
log.info("THE NUMBER OF COMMENTS BY POST NUMBER : " + commentMapper.readCommentsCountByPostNumber(postNumber));
}
@Test
public void testReadCommentsCountByParentCommentNumber() {
Long parentCommentNumber = 1L;
log.info("THE NUMBER OF A COMMENT AND REPLY COMMENTS OF IT : " + commentMapper.readCommentsCountByParentCommentNumber(parentCommentNumber));
}
@Test
public void testUpdateComment() {
CommentVO comment = new CommentVO();
comment.setNumber(1L);
comment.setContent("updated content");
log.info("THE NUMBER OF UPDATED COMMENT : " + commentMapper.updateComment(comment));
}
@Test
public void testUpdateCommentIsDeletedByCommentNumber() {
Long commentNumber = 1L;
log.info("THE NUMBER OF VIRTUALLY DELETED COMMENT : " + commentMapper.updateCommentIsDeletedByCommentNumber(commentNumber));
}
@Test
public void testDeleteCommentByCommentNumber() {
Long commentNumber = 2L;
log.info("THE NUMBER OF DELETED COMMENT : " + commentMapper.deleteCommentByCommentNumber(commentNumber));
}
}
[ CommentService Interface and Impl ]
public interface CommentService {
public int registerComment(CommentVO comment);
public List<CommentVO> getCommentsByPostNumber(Long postNumber, SearchCriteria searchCriteria);
public int getCommentsCountByPostNumber(Long postNumber);
public int modifyComment(CommentVO comment);
public int deleteCommentByCommentNumber(Long commentNumber);
}
@Service
@Log4j
public class CommentServiceImpl implements CommentService {
@Setter(onMethod_ = @Autowired)
private CommentMapper commentMapper;
@Override
public int registerComment(CommentVO comment) {
log.info("register comments...............");
int result = 0;
boolean isRoot = comment.getParentNumber() == null;
if (isRoot) {
result = commentMapper.insertComment(comment);
}
else {
result = commentMapper.insertReplyComment(comment);
}
return result;
}
@Override
public List<CommentVO> getCommentsByPostNumber(Long postNumber, SearchCriteria searchCriteria) {
log.info("get comments by post number.............");
long to = searchCriteria.getPageNumber() * searchCriteria.getContentQuantity();
long from = to - searchCriteria.getContentQuantity() + 1;
return commentMapper.readCommentsByPostNumber(postNumber, from, to);
}
@Override
public int getCommentsCountByPostNumber(Long postNumber) {
log.info("get comments count by post number..............");
return commentMapper.readCommentsCountByPostNumber(postNumber);
}
@Override
public int modifyComment(CommentVO comment) {
log.info("modify comment..................");
return commentMapper.updateComment(comment);
}
@Override
public int deleteCommentByCommentNumber(Long commentNumber) {
log.info("delete comment by comment number.................");
// root comment이고 reply comment가 없다면 1 있다면 2이상의 값을 가짐. root comment가 아니면 0이다.
boolean hasReplyComment = commentMapper.readCommentsCountByParentCommentNumber(commentNumber) >= 2;
int result = 0;
if (hasReplyComment) {
result = commentMapper.updateCommentIsDeletedByCommentNumber(commentNumber);
}
else {
result = commentMapper.deleteCommentByCommentNumber(commentNumber);
}
return result;
}
}
[ CommentService Tests ]
@RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class CommentServiceTests {
@Setter(onMethod_ = @Autowired)
private CommentService commentService;
private static final Long POST_NUMBER = 80L;
private static final SearchCriteria SEARCH_CRITERIA = new SearchCriteria();
@Test
public void Exists() {
assertNotNull(commentService);
log.info(commentService);
}
@Test
public void testRegisterComment() {
CommentVO comment = new CommentVO();
comment.setPostNumber(POST_NUMBER);
comment.setContent("temp content");
comment.setWriter("bibidi");
log.info("THE NUMBER OF REGISTERED COMMENTS : " + commentService.registerComment(comment));
comment.setParentNumber(1L);
log.info("THE NUMBER OF REGISTERED REPLY COMMENTS : " + commentService.registerComment(comment));
}
@Test
public void testGetCommentsByPostNumber() {
commentService
.getCommentsByPostNumber(POST_NUMBER, SEARCH_CRITERIA)
.forEach(comment -> log.info(comment));
}
@Test
public void testGetCommentsCountByPostNumber() {
log.info("COMMNETS COUNT IN POST : " + commentService.getCommentsCountByPostNumber(POST_NUMBER));
}
@Test
public void testModifyComment() {
final Long COMMNET_NUMBER = 1L;
CommentVO comment = new CommentVO();
comment.setNumber(COMMNET_NUMBER);
comment.setContent("modified content");
log.info("THE NUMBER OF MODIFIED COMMENTS : " + commentService.modifyComment(comment));
}
@Test
public void testDeleteCommentByCommentNumber() {
final Long COMMENT_NUMBER = 2L;
log.info("THE NUMBER OF DELETED COMMENTS : " + commentService.deleteCommentByCommentNumber(COMMENT_NUMBER));
}
}
[ REST Controller consume, produces ]
- consume : 수신하고자 하는 데이터 포맷 정의. body에 담긴 데이터 타입이 consume에 정의된 타입일 경우에만 처리.
- produces : 출력하고자 하는 데이터 포맷 정의. produces에 정의된 타입의 형태로만 응답하겠다는 의미. 다른 형식으로 달라고 요청하면 거부함.
* Reference
1. https://2ham-s.tistory.com/292
[ 화면 ]




- 댓글도 게시글과 마찬가지로 pagination을 이용해 일부 댓글만 표시한다. 아래에 위치한 댓글이 더 최신 댓글이며, 앞페이지에 있는 댓글이 뒷페이지에 있는 댓글들보다 더 최신 댓글이다.
- 수정, 답글을 클릭하면 그에 맞는 폼이 나온다. 다시 재클릭을하면 해당 폼이 사라진다. 수정창의 경우 수정의 편의를 위해 원본 내용을 복사해 오도록 구현했다.
- 댓글 삭제의 경우 대댓글이 없을 땐 물리 삭제를 한다. 대댓글이 있을 경우엔 논리 삭제를 하고 댓글 리스트엔 삭제된 댓글이라고 표시하도록 구현했다.
[ 댓글 서비스 객체 구현 ]
const commentService = (function() {
const csrfHeaderName = document.querySelector("meta[name=csrfHeaderName]").getAttribute("content");
const csrfToken = document.querySelector("meta[name=csrfTokenValue]").getAttribute("content");
const fakePostNumber = 0;
function addComment(comment, callback, error) {
$.ajax({
type : 'POST',
url : '/comments/' + comment.postNumber,
beforeSend : function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfToken);
},
data : JSON.stringify(comment),
contentType : "application/json",
success : function(data, status, xhr) {
if (callback) {
callback(data);
}
},
error : function(xhr, status, errorThrown) {
if (error) {
error(errorThrown);
}
}
})
}
function getComments(param, callback, error) {
const postNumber = param.postNumber;
const pageNumber = param.pageNumber;
let paramString = "";
if (pageNumber) paramString += "?pageNumber=" + pageNumber;
$.ajax({
type : 'GET',
url : '/comments/' + postNumber + ".json" + paramString,
success : function(data, status, xhr) {
if (callback) {
callback(data);
}
},
error : function(xhr, status, errorThrown) {
if (error) {
error(errorThrown);
}
}
})
}
function updateComment(comment, callback, error) {
$.ajax({
type : 'PATCH',
url : '/comments/' + fakePostNumber + '/' + comment.number,
beforeSend : function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfToken);
},
data : JSON.stringify(comment),
contentType : "application/json",
success : function(data, status, xhr) {
if (callback) {
callback(data);
}
},
error : function(xhr, status, errorThrown) {
if (error) {
error(errorThrown);
}
}
})
}
function removeComment(commentNumber, callback, error) {
$.ajax({
type : 'DELETE',
url : '/comments/' + fakePostNumber + '/' + commentNumber,
beforeSend : function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfToken);
},
success : function(data, status, xhr) {
if (callback) {
callback(data);
}
},
error : function(xhr, status, errorThrown) {
if (error) {
error(errorThrown);
}
}
})
}
return {
addComment : addComment,
getComments : getComments,
updateComment : updateComment,
removeComment : removeComment
};
})();
[ 댓글 나열과 이벤트 등록 ]
$(document).ready(function() {
const urlTokens = location.pathname.split('/');
const forumSlug = urlTokens[2];
const postNumber = urlTokens[3];
const metaNickname = document.querySelector("meta[name='userNickname']");
const userNickname = !metaNickname ? "" : metaNickname.getAttribute("content");
let commentCurrentPage = 1;
registerBtnEvent();
listComments(commentCurrentPage);
function registerBtnEvent() {
const isLogin = userNickname == '' ? false : true;
if (isLogin) {
// 댓글 등록 버튼 이벤트 등록
const commentRegisterBtn = document.querySelector("#comment-register-btn");
if (commentRegisterBtn !== null) {
commentRegisterBtn.addEventListener('click', function() {
const textArea = document.querySelector('#comment-register-content');
const content = textArea.value;
if (content == '') {
alert("내용을 채워주세요.");
return;
}
comment = {
postNumber: postNumber,
content: content,
writer: userNickname
};
commentService.addComment(
comment,
function(msg) {
textArea.value = '';
listComments(1);
}
);
});
}
// 게시물 삭제 이벤트 등록
const postDeleteBtn = document.querySelector('.post-delete-link');
if (postDeleteBtn !== null) {
const result = location.pathname.split('/');
postDeleteBtn.addEventListener('click', function(event) {
event.preventDefault();
postService.deletePost(
postNumber,
function(msg) {
alert("msg");
location.href = '/posts/' + forumSlug;
}
);
});
}
// 이모티콘 창 띄우는 이벤트 등록
const emoticonRegisterBtn = document.querySelector('.emoticon-register-btn');
if (emoticonRegisterBtn !== null) {
const commentRegisterDiv = emoticonRegisterBtn.parentNode.parentNode;
let emoticonDiv = null;
emoticonRegisterBtn.addEventListener('click', function(event) {
if (emoticonDiv == null) {
emoticonDiv = document.createElement('div');
emoticonDiv.setAttribute('class', 'emoticons');
let str = "<div class='emoticons__tab'>tabs</div>"
+ "<div class='emoticons__imgs'>imgs</div>";
emoticonDiv.innerHTML = str;
commentRegisterDiv.appendChild(emoticonDiv);
}
else {
emoticonDiv.remove();
emoticonDiv = null;
}
});
}
}
}
function listComments(pageNumber) {
const param = {
postNumber: postNumber,
pageNumber: pageNumber
};
const commentUl = document.querySelector(".comments__list");
commentService.getComments(
param,
function(page) {
const totalQuantity = page.commentsCount;
const list = page.comments;
if (!list || list.length == 0) {
commentUl.innerHTML = "";
registerCommentBtnEvent();
return;
}
let str = "";
list.forEach(item => {
if (item.number === item.parentNumber) {
if (item.isDeleted) {
const msgDeletedItem = "해당 댓글은 삭제되었습니다.";
str += "<li>"
+ "<div class='comment-wrapper'>"
if (userNickname != '') {
str += "<a class='comment-reply-link pull-right' href='#' data-target='" + item.number + "'> 답글 </a>";
}
str += "</div></div><div class='comment-body'>";
str += "<p>" + msgDeletedItem + "</p>";
str += "</div></div></li>";
return;
}
str += "<li>"
+ "<div class='comment-wrapper'>"
+ "<div class='comment-header'>"
+ "<div class='header--left' style='display:inline'>"
+ "<strong class='primary-font'>" + item.writer + "</strong>"
+ "</div>"
+ "<div class='header--right pull-right' style='display:inline'>"
+ "<time>" + displayTime(item.dateCommented) + " </time>";
if (item.writer === userNickname) {
str += "<a class='comment-delete-link' href='#' data-target='" + item.number + "'> 삭제 </a>"
+ "<a class='comment-edit-link' href='#' data-target='" + item.number + "'> 수정 </a>";
}
if (userNickname != '') {
str += "<a class='comment-reply-link' href='#' data-target='" + item.number + "'> 답글 </a>";
}
str += "</div></div><div class='comment-body'>";
str += "<p>" + item.content + "</p>";
str += "</div></div></li>";
}
else {
str += "<li>"
+ "<div class='comment-header'>"
+ "<div class='header--left' style='display:inline; padding-left:18px'>"
+ "<strong class='primary-font'>" + item.writer + "</strong>"
+ "</div>"
+ "<div class='header--right pull-right' style='display:inline'>"
+ "<time>" + displayTime(item.dateCommented) + " </time>";
if (item.writer === userNickname) {
str += "<a class='comment-delete-link' href='#' data-target='" + item.number + "'> 삭제 </a>"
+ "<a class='comment-edit-link' href='#' data-target='" + item.number + "'> 수정 </a>";
}
str += "</div></div><div class='comment-body'>"
+ "<p><i class='fa fa-angle-right fa-fw'></i>" + item.content + "</p>"
+ "</div></li>";
}
});
commentUl.innerHTML = str;
registerCommentBtnEvent();
commentCurrentPage = pageNumber;
addCommentPagination(totalQuantity);
}
);
}
function addCommentPagination(totalQuantity) {
const contentQuantity = 20;
const pageSize = 5;
let endPage = Math.ceil(commentCurrentPage / pageSize) * pageSize;
const startPage = endPage - pageSize + 1;
let hasNextPage = true;
const hasPreviousPage = (startPage != 1);
if (endPage * contentQuantity >= totalQuantity) {
hasNextPage = false;
endPage = Math.ceil(totalQuantity / contentQuantity);
}
let str = '';
if (hasPreviousPage) {
str += '<li class="paginate_button previous"><a href="' + (startPage - 1) + '">이전</a></li>';
}
for (let i = startPage; i <= endPage; i++) {
str += '<li class="paginate_button"><a href="#">' + i + '</a></li>';
}
if (hasNextPage) {
str += '<li class="paginate_button next"><a href="' + (endPage + 1) + '">다음</a></li>';
}
const commentPaginationUl = document.querySelector('.comments__pagination').querySelector('.pagination');
commentPaginationUl.innerHTML = str;
const commentItems = commentPaginationUl.querySelectorAll('.paginate_button');
commentItems.forEach(item => {
item.addEventListener('click', function(event) {
event.preventDefault();
const targetNumber = this.querySelector('a').innerText;
listComments(targetNumber);
});
});
}
function registerCommentBtnEvent() {
const isLogin = userNickname == '' ? false : true;
if (isLogin) {
// 댓글 삭제 이벤트 등록
const commentDeleteBtns = document.querySelectorAll('.comment-delete-link');
commentDeleteBtns.forEach(btn => {
const targetNumber = btn.dataset.target;
btn.addEventListener("click", function(event) {
event.preventDefault();
commentService.removeComment(
targetNumber,
function(msg) {
alert(msg);
listComments(commentCurrentPage);
}
);
});
});
// 댓글 수정 이벤트 등록
const commentEditLinks = document.querySelectorAll('.comment-edit-link');
commentEditLinks.forEach(link => {
let commentEditDiv = null;
link.addEventListener("click", function(event) {
event.preventDefault();
const commentWrapper = this.parentNode.parentNode.parentNode;
const commentContent = commentWrapper.querySelector('.comment-body').innerText;
if (commentEditDiv === null) {
const editDiv = document.createElement('div');
editDiv.setAttribute('class', 'comment-edit');
let str = "<textarea class='comment-edit__content' rows='5'>" + commentContent + "</textarea>"
+ "<div class='btns-wrapper'>"
+ "<button class='comment-edit__submit-btn btn btn-primary'>수정</button>"
+ "</div>";
editDiv.innerHTML = str;
commentWrapper.appendChild(editDiv);
commentEditDiv = editDiv;
// 제출 버튼 이벤트 등록
const commentEditBtn = commentEditDiv.querySelector(".comment-edit__submit-btn");
const targetNumber = this.dataset.target;
commentEditBtn.addEventListener('click', function() {
comment = {
number: targetNumber,
content: commentEditDiv.querySelector(".comment-edit__content").value
};
commentService.updateComment(
comment,
function(msg) {
alert(msg);
listComments(commentCurrentPage);
}
);
});
}
else {
commentEditDiv.remove();
commentEditDiv = null;
}
});
});
// 대댓글 등록 이벤트
const commentReplyLinks = document.querySelectorAll('.comment-reply-link');
commentReplyLinks.forEach(link => {
let commentReplyDiv = null;
link.addEventListener("click", function(event) {
event.preventDefault();
const commentWrapper = this.parentNode.parentNode.parentNode;
const commentContent = commentWrapper.querySelector('.comment-body').innerText;
if (commentReplyDiv === null) {
const replyDiv = document.createElement('div');
replyDiv.setAttribute('class', 'comment-reply');
let str = "<textarea class='comment-reply__content' rows='5'></textarea>"
+ "<div class='btns-wrapper'>"
+ "<button class='emoticon-register-btn btn btn-default'>이모티콘</button>"
+ "<button class='comment-reply__submit-btn btn btn-primary'>등록</button>"
+ "</div>";
replyDiv.innerHTML = str;
commentWrapper.appendChild(replyDiv);
commentReplyDiv = replyDiv;
// 제출 버튼 이벤트 등록
const commentReplyBtn = commentReplyDiv.querySelector(".comment-reply__submit-btn");
const targetNumber = this.dataset.target;
commentReplyBtn.addEventListener('click', function() {
comment = {
postNumber: postNumber,
parentNumber: targetNumber,
content: commentReplyDiv.querySelector(".comment-reply__content").value,
writer: userNickname
};
commentService.addComment(
comment,
function(msg) {
alert(msg);
listComments(commentCurrentPage);
}
);
});
}
else {
commentReplyDiv.remove();
commentReplyDiv = null;
}
});
});
}
}
function displayTime(timeValue) {
const date = new Date(timeValue);
const YYYY = date.getFullYear();
const MM = date.getMonth() + 1;
const DD = date.getDate();
const HH = date.getHours();
const mm = date.getMinutes();
const ss = date.getSeconds();
return YYYY + '-' + MM + '-' + DD + ' ' + HH + ':' + mm + ':' + ss;
}
});
[ ]
'(구)게시판 프로젝트' 카테고리의 다른 글
게시글(updated 21.06.01) (0) | 2021.05.07 |
---|---|
권한 관련 작업 (0) | 2021.05.05 |
(멀티)게시판 (0) | 2021.05.04 |
이름 짓는 법 총 정리 (1) | 2021.04.28 |
메인 화면 (0) | 2021.04.27 |
- Total
- Today
- Yesterday
- 인간 대포
- boj 2336
- 백준 1280
- boj 10473
- Ugly Numbers
- 백준 2243
- 백준 12713
- boj 9345
- 백준 9345
- 제로베이스 스쿨
- 부트 캠프
- 백준 10775
- boj 10775
- 백준 10473
- boj 2243
- 백준 16562
- boj 1106
- 제로베이스 백엔드 스쿨
- boj 14868
- 백준 3006
- boj 12713
- 디지털 비디오 디스크
- 백준 2336
- 터보소트
- 백준 1106
- 백준 14868
- boj 3006
- boj 1280
- boj 16562
- 사탕상자
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |