45일차 내용 정리

채공부·2025년 7월 25일

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

게시판

댓글

  • view.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");
	});
});
  • CommentDao 에 댓글 수정하는 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;
	}
}
  • comment-udpate.jsp 생성
<%
	// 폼 전송되는 수정할 댓글의 정보를 얻어온다
	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%>";
<% } %>
  • view.jsp 코드 추가
// 삭제 버튼을 눌렀을 때 
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%>;
		}
	});
});
  • CommentDao 에 댓글 삭제하는 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;
	}
}
  • comment-delete.jsp 생성
<%
	// 삭제할 댓글 번호
	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);
%>
  • view.jsp 코드 수정
<!-- 댓글 목록 출력 -->
<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>
profile
학원 공부 내용 정리

0개의 댓글