CSS & BootStrap
.card-body img{
width: 50px;
height: 50px;
}
.btn-close{
/* sclae : 확대 or 축소*/
transform: scale(0.7);
}
<div class="container">
<h1>댓글 수정, 삭제 기능이 포함된 card</h1>
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-sm-row position-relative">
<!-- 삭제 (X) 버튼 -->
<button data-num="1" class="btn-close position-absolute top-0 end-0 m-1"></button>
<img class="rounded-circle me-3 align-self-center" src="images/1.jpg" alt="프로필 이미지">
<div class="flex-grow-1">
<div class="d-flex justify-content-between">
<div>
<strong>작성자: 유재석</strong>
<span>수신자: 전체</span>
</div>
<small>25.07.25 12:00</small>
</div>
<pre>첫번째 댓글입니다</pre>
<!-- 수정 버튼 (본인에게만 보임) -->
<button class="btn btn-sm btn-outline-secondary edit-btn">수정</button>
<!-- 댓글 입력 폼 (처음에는 숨김) -->
<div class="d-none form-div">
<form action="comment-update.jsp" method="post">
<textarea class="form-control mb-2" rows="2">기존 댓글</textarea>
<button type="submit" class="btn btn-sm btn-success">수정 완료</button>
<button type="reset" class="btn btn-sm btn-secondary cancel-edit-btn">취소</button>
</form>
</div>
</div>
</div>
</div>
</div>
// 클래스명이 edit-btn 인 모든 버튼에 "click" 이벤트 리스너 등록
document.querySelectorAll(".edit-btn").forEach(item=>{
item.addEventListener("click", ()=>{
// 클릭한 버튼의 다음 형제 요소의 class 목록에서 d-none 을 제거
item.nextElementSibling.classList.remove("d-none");
// 클릭한 버튼의 class 목록에 d-none 을 추가
item.classList.add("d-none");
});
});
// 취소 버튼을 눌렀을 때 이벤트 리스너 등록
document.querySelectorAll(".cancel-edit-btn").forEach(item=>{
item.addEventListener("click", ()=>{
// 가장 가까운 부모 요소 중에 클래스 속성이 form-div 인 요소
const formDiv = item.closest(".form-div");
// formDiv 에 d-none 클래스르 추가해서 안 보이게 하고
formDiv.classList.add("d-none");
// formDiv 의 이전 형제 요소(댓글 버튼)에 d-none 클래스 제거해서 보이게 한다
formDiv.previousElementSibling.classList.remove("d-none");
});
});
// 삭제 버튼을 눌렀을 때
document.querySelectorAll(".btn-close").forEach(item=>{
item.addEventListener("click", ()=>{
// data-num 속성에 출력된 삭제할 댓글의 번호
const num = item.getAttribute("data-num");
const isDelete = confirm(num+"번 댓글을 삭제하시겠습니까?");
});
});
JAVA & JSP
<div class="card mt-4">
<div class="card-header bg-light">
<strong>본문 내용</strong>
</div>
<div class="card-body p-1">
<%=dto.getContent()%>
</div>
</div>
<% if(dto.getWriter().equals(userName)) { %>
<div class="text-end pt-2">
<a class="btn btn-warning btn-sm" href="edit.jsp?num=<%=dto.getNum()%>">Edit</a>
<a class="btn btn-danger btn-sm" href="delete.jsp?num=<%=dto.getNum()%>">Delete</a>
</div>
<% } %>
<div class="card my-3">
<div class="card-header bg-primary-subtle text-white">
댓글을 입력해 주세요
</div>
<div class="card-body">
<!-- 원글의 댓글을 작성할 폼 -->
<form action="${pageContext.request.contextPath}/board/save-comment.jsp" method="post">
<!-- 숨겨진 입력값 -->
<input type="hidden" name="parentNum" value="<%=dto.getNum() %>"/>
<input type="hidden" name="targetWriter" value="<%=dto.getWriter() %>" />
<div class="mb-3">
<label for="commentContent" class="form-label">댓글 내용</label>
<textarea id="commentContent" name="content" rows="5" class="form-control" placeholder="댓글을 입력하세요"></textarea>
</div>
<button type="submit" class="btn bg-primary-subtle text-white">등록</button>
</form>
</div>
</div>
<!-- 댓글 목록 출력 -->
<div class="comments">
<% for(CommentDto tmp : commentList) { %>
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-sm-row position-relative">
<!-- 댓글 작성자가 로그인된 userName과 같을 때만 삭제 버튼 출력 -->
<% if(tmp.getWriter().equals(userName)) { %>
<button data-num="<%=tmp.getNum()%>" class="btn-close position-absolute top-0 end-0 m-1"></button>
<% } %>
<% if(tmp.getProfileImage() == null) { %>
<i style="font-size:50px;" class="bi bi-person-circle me-3 align-self-center"></i>
<% } else { %>
<img class="rounded-circle me-3 align-self-center"
src="${pageContext.request.contextPath}/upload/<%=tmp.getProfileImage()%>"
alt="프로필 이미지"
style="width:50px; height:50px;">
<% } %>
<div class="flex-grow-1">
<div class="d-flex justify-content-between">
<div>
<strong><%=tmp.getWriter()%></strong>
<span>@<%=tmp.getTargetWriter()%></span>
</div>
<small><%=tmp.getCreatedAt()%></small>
</div>
<pre><%=tmp.getContent()%></pre>
<!-- 댓글 작성자와 로그인된 userName이 같으면 수정폼, 다르면 댓글폼 출력 -->
<% if(tmp.getWriter().equals(userName)) { %>
<!-- 수정 버튼 (본인에게만 보임) -->
<button class="btn btn-sm bg-primary-subtle btn-outline-secondary edit-btn">수정</button>
<!-- 댓글 수정 폼 (처음에는 숨김) -->
<div class="d-none form-div">
<form action="comment-update.jsp" method="post">
<!-- 댓글 번호 및 원래 페이지로 돌아가기 위한 parentNum 전송 -->
<input type="hidden" name="num" value="<%=tmp.getNum()%>"/>
<input type="hidden" name="parentNum" value="<%=num%>"/>
<textarea name="content" class="form-control mb-2" rows="2"><%=tmp.getContent()%></textarea>
<button type="submit" class="btn btn-sm btn-success">수정 완료</button>
<button type="reset" class="btn btn-sm btn-secondary cancel-edit-btn">취소</button>
</form>
</div>
<% } else { %>
<button class="btn btn-sm bg-primary-subtle btn-outline-secondary show-reply-btn">댓글</button>
<!-- 댓글 입력 폼 (처음에는 숨김) -->
<div class="d-none form-div">
<form action="comment-save.jsp" method="post">
<textarea class="form-control mb-2" rows="2" placeholder="댓글을 입력하세요"></textarea>
<button type="submit" class="btn btn-sm btn-outline-primary">등록</button>
<button type="reset" class="btn btn-sm btn-outline-secondary cancel-reply-btn">취소</button>
</form>
</div>
<% } %>
</div>
</div>
</div>
<% } %>
</div>
// 클라이언트가 로그인 했는지 여부
const isLogin = <%=isLogin%>;
document.querySelector("#commentContent").addEventListener("input", ()=>{
//원글의 댓글 입력란에 입력했을때 만일 로그인 하지 않았다면
if(!isLogin){
alert("댓글 작성을 위해 로그인이 필요합니다!");
location.href ="${pageContext.request.contextPath }/user/loginform.jsp?url=${pageContext.request.contextPath }/board/view.jsp?num=<%=num %>";
}
});
// 모든 댓글 버튼에 이벤트 등록
document.querySelectorAll(".show-reply-btn").forEach(item=>{
// 매개변수에 전달된 item 은 댓글 button 의 참조값이다
item.addEventListener("click", ()=>{
//댓글 버튼을 눌렀을때 만일 로그인 하지 않았다면
if(!isLogin){
alert("댓글 작성을 위해 로그인이 필요합니다!");
location.href = "${pageContext.request.contextPath }/user/loginform.jsp?url=${pageContext.request.contextPath }/board/view.jsp?num=<%=num %>";
return;
}
// 클릭한 버튼의 다음 형제요소의 class 목록에서 d-none 을 제거
item.nextElementSibling.classList.remove("d-none");
// 클릭한 버튼의 class 목록에 d-none 을 추가
item.classList.add("d-none");
});
});
document.querySelectorAll(".cancel-reply-btn").forEach(item=>{
item.addEventListener("click", ()=>{
// 가장 가까운 부모 요소중에 클래스 속성이 form-div 인요소
const formDiv=item.closest(".form-div");
// formDiv 에 d-none 클래스 추가해서 안보이게 하고
formDiv.classList.add("d-none");
// formDiv 의 이전 형제요소(댓글버튼)에 d-none 클래스 제거해서 보이게 한다
formDiv.previousElementSibling.classList.remove("d-none");
});
});
// 클래스명이 edit-btn 인 모든 버튼에 "click" 이벤트 리스너 등록
document.querySelectorAll(".edit-btn").forEach(item=>{
item.addEventListener("click", ()=>{
// 클릭한 버튼의 다음 형제 요소의 class 목록에서 d-none 을 제거
item.nextElementSibling.classList.remove("d-none");
// 클릭한 버튼의 class 목록에 d-none 을 추가
item.classList.add("d-none");
});
});
update 메소드 생성public boolean update(CommentDto dto) {
Connection conn = null;
PreparedStatement pstmt = null;
int rowCount = 0;
try {
conn = new DbcpBean().getConn();
String sql = """
UPDATE comments
SET content = ?
WHERE num = ?
""";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, dto.getContent());
pstmt.setInt(2, dto.getNum());
rowCount = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null)
pstmt.close();
if (conn != null)
conn.close();
} catch (Exception e) {
}
}
if (rowCount > 0) {
return true;
} else {
return false;
}
}
<%
// 폼 전송되는 수정할 댓글의 정보를 얻어온다
int num = Integer.parseInt(request.getParameter("num"));
String content = request.getParameter("content");
// 원글의 글번호
String parentNum = request.getParameter("parentNum");
CommentDto dto = new CommentDto();
dto.setNum(num);
dto.setContent(content);
boolean isSuccess = CommentDao.getInstance().update(dto);
%>
<% if (isSuccess) { %>
alert("수정 완료");
location.href = "${pageContext.request.contextPath}/board/view.jsp?num=<%=parentNum%>";
<% } else { %>
alert("수정 실패");
location.href = "${pageContext.request.contextPath}/board/view.jsp?num=<%=parentNum%>";
<% } %>
// 삭제 버튼을 눌렀을 때
document.querySelectorAll(".btn-close").forEach(item=>{
item.addEventListener("click", ()=>{
// data-num 속성에 출력된 삭제할 댓글의 번호
const num = item.getAttribute("data-num");
const isDelete = confirm(num+"번 댓글을 삭제하시겠습니까?");
// confirm 은 확인:true 취소:false
if(isDelete){
// "delete.jsp?num = 삭제할 댓글 번호&parentNum = 원글의 번호" 형식의 요청이 되도록 한다
// \${} 는 jsp 가 해석하지 않도록 \${} 역슬래시를 붙여서 작성한다
location.href = `comment-delete.jsp?\${num}&parentNum=<%=num%>;
}
});
});
delete 메소드 생성// 댓글을 삭제(deleted => yes) 하는 메소드
public boolean delete(int num) {
Connection conn = null;
PreparedStatement pstmt = null;
int rowCount = 0;
try {
conn = new DbcpBean().getConn();
String sql = """
UPDATE comments
SET deleted = 'yes'
WHERE num = ?
""";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, num);
rowCount = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null)
pstmt.close();
if (conn != null)
conn.close();
} catch (Exception e) {
}
}
if (rowCount > 0) {
return true;
} else {
return false;
}
}
<%
// 삭제할 댓글 번호
int num = Integer.parseInt(request.getParameter("num"));
// redirect 이동할 때 필요한 원글의 글 번호
String parentNum = request.getParameter("parentNum");
// dao 객체를 이용해서 삭제하고
CommentDao.getInstance().delete(num);
// redirect 이동
String cPath = request.getContextPath();
response.sendRedirect(cPath+"/board/view.jsp?num="+parentNum);
%>
<!-- 댓글 목록 출력 -->
<div class="comments">
<% for(CommentDto tmp : commentList) { %>
<div class="card mb-3">
<% if(tmp.getDeleted().equals("yes")) { %>
<!-- 삭제된 댓글 -->
<div>삭제된 댓글입니다</div>
<% } else { %>
<!-- 삭제되지 않은 댓글 -->
<div class="card-body d-flex flex-column flex-sm-row position-relative">
<!-- 앳글 작성자가 로그인된 userName 과 같을 때만 삭제 버튼 출력 -->
<% if(tmp.getWriter().equals(userName)) { %>
<button data-num="<%=tmp.getNum()%>" class="btn-close position-absolute top-0 end-0 m-1"></button>
<% } %>
<% if(tmp.getProfileImage() == null) { %>
<i style="font-size:50px;" class="bi bi-person-circle me-3 align-self-center"></i>
<% } else { %>
<img class="rounded-circle me-3 align-self-center"
src="${pageContext.request.contextPath}/upload/<%=tmp.getProfileImage() %>"
alt="프로필 이미지"
style="width:50px; height:50px;">
<% } %>
<div class="flex-grow-1">
<div class="d-flex justify-content-between">
<div>
<strong><%=tmp.getWriter()%></strong>
<span>@<%=tmp.getTargetWriter()%></span>
</div>
<small><%=tmp.getCreatedAt()%></small>
</div>
<pre><%=tmp.getContent()%></pre>
<!-- 댓글 작성작가 로그인된 userName 과 같으면 수정폼, 다르면 댓글폼을 출력한다 -->
<% if(tmp.getWriter().equals(userName)) { %>
<!-- 수정 버튼 (본인에게만 보임)-->
<button class="btn btn-sm bg-primary-subtle btn-outline-secondary edit-btn">수정</button>
<!-- 댓글 수정 폼 (처음에는 숨김) -->
<div class="d-none form-div">
<form action="comment-update.jsp" method="post">
<!-- 댓글을 수정하기 위한 댓글의 번호, 이 페이지로 다시 돌아오기 위한 parentNum 도 같이 전송되도록 -->
<input type="hidden" name="num" value="<%=tmp.getNum()%>"/>
<input type="hidden" name="parentNum" value="<%=num%>"/>
<textarea name="content" class="form-control mb-2" rows="2"><%=tmp.getContent()%></textarea>
<button type="submit" class="btn btn-sm btn-success">수정 완료</button>
<button type="reset" class="btn btn-sm btn-secondary cancel-edit-btn">취소</button>
</form>
</div>
<% } else { %>
<button class="btn btn-sm bg-primary-subtle btn-outline-secondary show-reply-btn">댓글</button>
<!-- 댓글 입력 폼 (처음에는 숨김) -->
<div class="d-none form-div">
<form action="comment-save.jsp" method="post">
<textarea class="form-control mb-2" rows="2"
placeholder="댓글을 입력하세요"></textarea>
<button type="submit" class="btn btn-sm btn-outline-primary">등록</button>
<button type="reset" class="btn btn-sm btn-outline-secondary cancel-reply-btn">취소</button>
</form>
</div>
<% } %>
</div>
</div>
<% } %>
</div>
<% } %>
</div>