티스토리 뷰

(구)게시판 프로젝트

댓글, 대댓글

_Bibidi 2021. 5. 11. 16:16

 

[ 아이디어 정리 ]

 - 임시 용어 정의. 댓글 : parent가 자기 자신인 댓글, 대댓글 : parent가 자기 자신이 아닌 댓글.

 - reference 1번에 나오는 방법으로 parent, depth, order 필드를 이용하여 댓글 계층을 보여주는 방법. 이 글을 보면서 아이디어를 꽤 많이 얻었다. 그런데 내가 원하는 형태는 댓글에 대댓글을 한 번만 허용하도록 하는 형태이다. 대댓글에 다시 댓글을 달면 박스가 오른쪽으로 계속 밀리는 형태가 되는데, 매우 보기 싫다.

 - 대댓글이 한 번만 허용되면 대댓글의 깊이가 모두 같으므로 depth 필드가 필요없다. 또 원 댓글의 parent를 null이 아니라 자기 자신으로 지정하면 order by parent option, number option을 이용해 간단하게 댓글을 정렬하여 가져올 수 있다. parent를 이용해 댓글과 대댓글을 묶어서 정렬하고 그 묶음 속에서 다시 댓글과 대댓글을 정렬하면 되기 때문이다. 댓글들의 번호가 댓글 < 대댓글1 < 대댓글2 < ... 이기 때문에 이 성질은 반드시 성립한다.

 - 대댓글이 달린 댓글의 경우 물리적으로 삭제하는 것이 아니라 논리적으로 삭제하여 대댓글을 유지할 수 있다. 그리고 이렇게 구현하는 쪽이 삭제된 댓글에 대한 궁금증을 유발하여 커뮤니티에 좀 더 참여하도록 유도할 수 있다고 생각한다. 실제로 나도 삭제된 댓글이 욕 먹고 있으면 어떤 내용이었는지 물어보곤 했다.

 

 

 * Reference

1. forest71.tistory.com/51

 

 

[ 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 

 

[ 화면 ]

댓글 화면1
댓글 화면과 댓글 등록창
댓글 수정
대댓글

 - 댓글도 게시글과 마찬가지로 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
링크
«   2024/05   »
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 31
글 보관함