본문 바로가기
Java/Servlet & JSP

JSP) 댓글, 대댓글 생성/나타내기

by 박채니 2022. 7. 6.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


댓글, 대댓글 생성하기

 

댓글 테이블

-- 댓글 테이블
create table board_comment (
    no number,
    comment_level number default 1, -- 댓글 1, 대댓글 2
    writer varchar2(15),
    content varchar2(2000),
    board_no number,
    comment_ref number, -- 대댓글인 경우, 댓글 참조. 댓글 null, 대댓글 댓글no(pk)
    reg_date date default sysdate,
    constraint pk_board_comment_no primary key(no),
    constraint fk_board_comment_writer foreign key(writer) references member(member_id) on delete set null,
    constraint fk_board_comment_board_no foreign key(board_no) references board(no) on delete cascade,
    constraint fk_board_comment_comment_ref foreign key(comment_ref) references board_comment(no) on delete cascade
);

create sequence seq_board_comment_no;

comment on column board_comment.no is '게시판댓글번호';
comment on column board_comment.comment_level is '게시판댓글 레벨';
comment on column board_comment.writer is '게시판댓글 작성자';
comment on column board_comment.content is '게시판댓글';
comment on column board_comment.board_no is '참조원글번호';
comment on column board_comment.comment_ref is '게시판댓글 참조번호';
comment on column board_comment.reg_date is '게시판댓글 작성일';

댓글을 작성한 경우, comment_level은 '1'이고 대댓글을 작성한 경우 comment_level을 '2'로 주어 댓글과 대댓글에 차이를 두었습니다.

또한 comment_ref는 대댓글인 경우 어떤 댓글을 참조하여 대댓글을 달았는 지 알아야하기 때문에 대댓글 시, 해당 컬럼에 댓글no를 주어 어떤 댓글을 참조하는 지 알 수 있도록 하였습니다.

만일 댓글인 경우, 해당 레코드는 null이 될 것입니다.

 

테스트 댓글/대댓글 작성

-- 287번 테스트댓글
insert into board_comment values(seq_board_comment_no.nextval, default, 'chany', '안녕하세요', 287, null, default);
insert into board_comment values(seq_board_comment_no.nextval, default, 'admin', '이달의 게시글로 선정!', 287, null, default);
insert into board_comment values(seq_board_comment_no.nextval, default, 'sinsa', '비가 주륵', 287, null, default);

-- 287번 테스트 대댓글
insert into board_comment values(seq_board_comment_no.nextval, 2, 'sinsa', '읽어주셔서 감사합니당', 287, 1, default);
insert into board_comment values(seq_board_comment_no.nextval, 2, 'qwerty', '흠..', 287, 1, default);
insert into board_comment values(seq_board_comment_no.nextval, 2, 'sinsa', '감사합니다. 어떤 혜택이 주어지죠?', 287, 2, default);

select * from board_comment order by no;

댓글/대댓글이 추가될 때마다 no가 생성되며, comment_level을 통해 댓글인지 대댓글인지 구분할 수 있습니다.

comment_level이 '1'인 no값 (1, 2, 3)은 댓글이고, '2'인 no값 (4, 5, 6)은 대댓글인 것을 파악할 수 있습니다.

또한 comment_ref를 통해 어떤 댓글의 대댓글인지 파악할 수 있는데, comment_ref가 null인 no값(1, 2, 3)은 댓글임을 알 수 있고, '1'인 no값(4, 5)는 댓글no값이 1에 대한 대댓글 / '2'인 no값(6)은 댓글no값이 2에 대한 대댓글인 점을 알 수 있습니다.

 

계층형 쿼리

- 부모레코드와 자식레코드를 연결해서 조회하는 쿼리

- 댓글트리, 메뉴, 조직도, 가게도 등

- start with : 최상위레코드 조건절(여러개일 수 있음)

- connect by : 부모레코드(prior키워드)와 자식레코드 관계설정

select
    *
from
    board_comment
where
    board_no = 287
start with
    comment_level = 1
connect by
    prior no = comment_ref;
--order siblings by
--    no desc

각 댓글에 따른 계층 구조를 파악할 수 있습니다.

order siblings by로 정렬도 가능!

 

※ 번외 - 조직도 나타내기

select
    level,  -- 계층형 쿼리에서만 사용가능한 가상쿼리
    emp_id,
    lpad(' ', (level - 1)*5) || emp_name 조직도,
    manager_id
from
    employee
start with
    emp_id = '200'
connect by
    prior emp_id = manager_id;


BoardComment 클래스 생성

public class BoardComment {
	private int no;
	private CommentLevel commentLevel;
	private String writer;
	private String content;
	private int boardNo;
	private int commentRef;
	private Timestamp regDate;
	
	public BoardComment() {
		super();
		// TODO Auto-generated constructor stub
	}

	public BoardComment(int no, CommentLevel commentLevel, String writer, String content, int boardNo, int commentRef,
			Timestamp regDate) {
		super();
		this.no = no;
		this.commentLevel = commentLevel;
		this.writer = writer;
		this.content = content;
		this.boardNo = boardNo;
		this.commentRef = commentRef;
		this.regDate = regDate;
	}
    
// getter, setter 생략

	@Override
	public String toString() {
		return "BoardComment [no=" + no + ", commentLevel=" + commentLevel + ", writer=" + writer + ", content="
				+ content + ", boardNo=" + boardNo + ", commentRef=" + commentRef + ", regDate=" + regDate + "]";
	}
}

 

CommentLevel - enum

/**
 * 댓글 COMMENT
 * 답글 REPLY
 */
public enum CommentLevel {
	COMMENT(1), REPLY(2);
	
	private int value;
	
	CommentLevel(int value) {
		this.value = value;
	}
	
	public int getValue() {
		return this.value;
	}
	
	public static CommentLevel valueOf(int value) {
		switch(value) {
		case 1: return COMMENT;
		case 2: return REPLY;
		default : throw new AssertionError("Unknown CommentLevel : " + value);
		}
	}
}

enum은 숫자가 올 수 없기 때문에 name과 value를 주어서 처리하였습니다.

COMMENT : name

(1) : value

enum으로 value를 가져오고, value로 enum을 가져오기 위해 getter와 valueOf() 메소드를 생성하였습니다.

 

boardView.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/views/common/header.jsp" %>
<%
	Board board = (Board)request.getAttribute("board");
	List<Attachment> attachments = ((BoardExt) board).getAttachments();
%>
<link rel="stylesheet" href="<%=request.getContextPath()%>/css/board.css" />
<section id="board-container">
	<h2>게시판</h2>
	<table id="tbl-board-view">
		<tr>
			<th>글번호</th>
			<td><%= board.getNo() %></td>
		</tr>
		<tr>
			<th>제 목</th>
			<td><%= board.getTitle() %></td>
		</tr>
		<tr>
			<th>작성자</th>
			<td><%= board.getWriter() %></td>
		</tr>
		<tr>
			<th>조회수</th>
			<td><%= board.getReadCount() %></td>
		</tr>
			<% if(attachments != null && !attachments.isEmpty()) {
				for(Attachment attach : attachments) { %>
		<tr>
			<th>첨부파일</th>
			<td>
				<%-- 첨부파일이 있을경우만, 이미지와 함께 original파일명 표시 --%>
				<img alt="첨부파일" src="<%=request.getContextPath() %>/images/file.png" width=16px>
				<a href="<%= request.getContextPath()%>/board/fileDownload?no=<%=attach.getNo()%>"><%= attach.getOriginalFilename() %></a>
			</td>
		</tr>
			<% 	}
			} %>
		<tr>
			<th>내 용</th>
			<td><%= board.getContent() %></td>
		</tr>
		<% 
		boolean canEdit = loginMember != null && (loginMember.getMemberId().equals(board.getWriter()) || loginMember.getMemberRole() == MemberRole.A);
		if(canEdit) { %>
		<tr>
			<%-- 작성자와 관리자만 마지막행 수정/삭제버튼이 보일수 있게 할 것 --%>
			<th colspan="2">
				<input type="button" value="수정하기" onclick="updateBoard()">
				<input type="button" value="삭제하기" onclick="deleteBoard()">
			</th>
		</tr>
		<% } %>
	</table>
	
	    <hr style="margin-top:30px;" />    
    
    <div class="comment-container">
    	<!-- 댓글 작성부 -->
        <div class="comment-editor">
            <form
            action="<%=request.getContextPath()%>/board/boardCommentEnroll" method="post" name="boardCommentFrm">
                <input type="hidden" name="boardNo" value="<%= board.getNo() %>" />
                <input type="hidden" name="writer" value="<%= loginMember != null ? loginMember.getMemberId() : "" %>" />
                <input type="hidden" name="commentLevel" value="1" />
                <input type="hidden" name="commentRef" value="0" />    
                <textarea name="content" cols="60" rows="3"></textarea>
                <button type="submit" id="btn-comment-enroll1">등록</button>
            </form>
        </div>
        <!--table#tbl-comment-->
    </div>
</section>
<script>
document.boardCommentFrm.content.addEventListener('focus', (e) => {
	if(<%= loginMember == null%>) {
		loginAlert();
	}
});

const loginAlert = () => {
	alert("로그인 후 이용가능합니다.");
	document.querySelector("#memberId").focus();
};

document.boardCommentFrm.addEventListener('submit', (e) => {
	if(<%= loginMember == null%>) {
		loginAlert();
		e.preventDefault();
		return;
	}
	
	// addEventListener에서는 return false가 안됨! e.preventDefault()와 return을 조합해서 사용
	if(!/^(.|\n)+$/.test(e.target.content.value)) {
		alert("내용을 작성해주세요.");
		e.preventDefault();
		return;
	}
});

필요한 정보들은 모두 input:hidden의 value값으로 설정하였으며, 유효성 검사까지 해줍니다.

 

Controller

BoardCommentEnrollServlet

@WebServlet("/board/boardCommentEnroll")
public class BoardCommentEnrollServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private BoardService boardService = new BoardService();
	
	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		try {
			CommentLevel commentLevel = CommentLevel.valueOf(Integer.parseInt(request.getParameter("commentLevel")));
			String writer = request.getParameter("writer");
			String content = request.getParameter("content");
			int boardNo = Integer.parseInt(request.getParameter("boardNo"));
			int commentRef = Integer.parseInt(request.getParameter("commentRef"));
			BoardComment boardComment = new BoardComment(0, commentLevel, writer, content, boardNo, commentRef, null);
			
			int result = boardService.insertBoardComment(boardComment);
			
			response.sendRedirect(request.getContextPath() + "/board/boardView?no=" + boardNo);
		} catch(Exception e) {
			e.printStackTrace();
			throw e;
		}
	}
}

 

Service

BoardService

public int insertBoardComment(BoardComment boardComment) {
    Connection conn = getConnection();
    int result = 0;
    try {
        result = boardDao.insertBoardComment(conn, boardComment);
        commit(conn);
    } catch(Exception e) {
        rollback(conn);
        throw e;
    } finally {
        close(conn);
    }
    return result;
}

 

Dao

BoardDao

// insertBoardComment  = insert into board_commnet values (seq_board_comment_no.nextval, ?, ?, ?, ?, ?, default)
public int insertBoardComment(Connection conn, BoardComment boardComment) {
    PreparedStatement pstmt = null;
    int result = 0;
    String sql = prop.getProperty("insertBoardComment");

    try {
        pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, boardComment.getCommentLevel().getValue());
        pstmt.setString(2, boardComment.getWriter());
        pstmt.setString(3, boardComment.getContent());
        pstmt.setInt(4, boardComment.getBoardNo());
        pstmt.setObject(5, boardComment.getCommentRef() == 0 ? null : boardComment.getCommentRef()); // java의 0은 null 의미 - DB는 0은 숫자로 취급!
        result = pstmt.executeUpdate();
    } catch (SQLException e) {
        throw new BoardException("댓글/답글 등록 오류", e);
    } finally {
        close(pstmt);
    }
    return result;
}

자바에서의 0은 int의 기본값 (값없음)을 의미하지만, DB 상에서는 number에 0, 음수, null 등 모두 올 수가 있습니다.

따라서 값없음을 의미하는 0을 넘겨주면 DB에서는 comment_no가 0인 행을 찾게 되고, 그에 따른 행이 없으니 오류가 발생할 것입니다.

따라서 Object로 받아 만일 0이라면 null을 대입, 그렇지 않다면 그대로 commentRef 값을 대입하도록 하였습니다.

 


댓글/대댓글 나타내기

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/views/common/header.jsp" %>
<%
	Board board = (Board)request.getAttribute("board");
	List<Attachment> attachments = ((BoardExt) board).getAttachments();
	List<BoardComment> commentList = (List<BoardComment>)request.getAttribute("commentList");
%>
<link rel="stylesheet" href="<%=request.getContextPath()%>/css/board.css" />
<section id="board-container">
	<h2>게시판</h2>
	<table id="tbl-board-view">
		<tr>
			<th>글번호</th>
			<td><%= board.getNo() %></td>
		</tr>
		<tr>
			<th>제 목</th>
			<td><%= board.getTitle() %></td>
		</tr>
		<tr>
			<th>작성자</th>
			<td><%= board.getWriter() %></td>
		</tr>
		<tr>
			<th>조회수</th>
			<td><%= board.getReadCount() %></td>
		</tr>
			<% if(attachments != null && !attachments.isEmpty()) {
				for(Attachment attach : attachments) { %>
		<tr>
			<th>첨부파일</th>
			<td>
				<%-- 첨부파일이 있을경우만, 이미지와 함께 original파일명 표시 --%>
				<img alt="첨부파일" src="<%=request.getContextPath() %>/images/file.png" width=16px>
				<a href="<%= request.getContextPath()%>/board/fileDownload?no=<%=attach.getNo()%>"><%= attach.getOriginalFilename() %></a>
			</td>
		</tr>
			<% 	}
			} %>
		<tr>
			<th>내 용</th>
			<td><%= board.getContent() %></td>
		</tr>
		<% 
		boolean canEdit = loginMember != null && (loginMember.getMemberId().equals(board.getWriter()) || loginMember.getMemberRole() == MemberRole.A);
		if(canEdit) { %>
		<tr>
			<%-- 작성자와 관리자만 마지막행 수정/삭제버튼이 보일수 있게 할 것 --%>
			<th colspan="2">
				<input type="button" value="수정하기" onclick="updateBoard()">
				<input type="button" value="삭제하기" onclick="deleteBoard()">
			</th>
		</tr>
		<% } %>
	</table>
	
	    <hr style="margin-top:30px;" />    
    
    <div class="comment-container">
    	<!-- 댓글 작성부 -->
        <div class="comment-editor">
            <form
            action="<%=request.getContextPath()%>/board/boardCommentEnroll" method="post" name="boardCommentFrm">
                <input type="hidden" name="boardNo" value="<%= board.getNo() %>" />
                <input type="hidden" name="writer" value="<%= loginMember != null ? loginMember.getMemberId() : "" %>" />
                <input type="hidden" name="commentLevel" value="1" />
                <input type="hidden" name="commentRef" value="0" />    
                <textarea name="content" cols="60" rows="3"></textarea>
                <button type="submit" id="btn-comment-enroll1">등록</button>
            </form>
        </div>
        <!--table#tbl-comment-->
        <table id="tbl-comment">
		<% if(commentList != null && !commentList.isEmpty()) {
			SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm");
			for(BoardComment comment : commentList) { %>        	
       		<tr class="<%= comment.getCommentLevel() == CommentLevel.COMMENT ? "level1" : "level2" %>">
       			<td>
       				<sub class="comment-writer"><%= comment.getWriter() %></sub>
       				<sub class="comment-date"><%= sdf.format(comment.getRegDate()) %></sub>
       				<div>
       					<%= comment.getContent() %>
       				</div>
       			</td>
       			<td>
       				<% if(comment.getCommentLevel() == CommentLevel.COMMENT) { %>
       				<button class="btn-reply" value="<%= comment.getNo() %>">답글</button>
       				<% } %>
       			</td>
        	</tr>
      			<% 	}
			} %>
        </table>
    </div>
</section>
<script>
document.boardCommentFrm.content.addEventListener('focus', (e) => {
	if(<%= loginMember == null%>) {
		loginAlert();
	}
});

const loginAlert = () => {
	alert("로그인 후 이용가능합니다.");
	document.querySelector("#memberId").focus();
};

document.boardCommentFrm.addEventListener('submit', (e) => {
	if(<%= loginMember == null%>) {
		loginAlert();
		e.preventDefault();
		return;
	}
	
	// addEventListener에서는 return false가 안됨! e.preventDefault()와 return을 조합해서 사용
	if(!/^(.|\n)+$/.test(e.target.content.value)) {
		alert("내용을 작성해주세요.");
		e.preventDefault();
		return;
	}
});

</script>


<% if(canEdit) { %>
	<form action="<%= request.getContextPath()%>/board/boardDelete" method="POST" name="boardDeleteFrm">
		<input type="hidden" name="no" value="<%= board.getNo() %>" />
	</form>
<script>
const updateBoard = () => {
	location.href = "<%= request.getContextPath()%>/board/boardUpdate?no=<%= board.getNo()%>";	
};

const deleteBoard = () => {
	if(confirm("정말 삭제하시겠습니까?")) {
		document.boardDeleteFrm.submit();
	}
};
<% } %>
</script>
<%@ include file="/WEB-INF/views/common/footer.jsp" %>

CommnetLevel이 2인 경우, 'level2'클래스를 적용 → 들여쓰기를 적용하여 대댓글처럼 보이도록 css효과를 주었고,

댓글인 경우에만 답글버튼이 활성화되도록 하였습니다.