안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
댓글, 대댓글 생성하기
댓글 테이블
-- 댓글 테이블
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효과를 주었고,
댓글인 경우에만 답글버튼이 활성화되도록 하였습니다.
'Java > Servlet & JSP' 카테고리의 다른 글
JSP) 댓글 삭제 기능 (0) | 2022.07.06 |
---|---|
JSP) 답글 폼 생성하기 / 이벤트 버블링을 이용한 유효성 검사 (0) | 2022.07.06 |
JSP) 첨부파일 있는 게시글 수정 (0) | 2022.07.05 |
JSP) 게시글 및 첨부파일 삭제 (0) | 2022.07.04 |
JSP) 파일 다운로드 (0) | 2022.07.04 |