CSS & BootStrap
flex-row : 가로 방향으로 자식 컨텐츠를 정렬
flex-column : 새로 방향으로 자식 컨텐츠를 정렬
align-self-center : main 축에 따라 가운데 배치
<div class="container">
<h1>댓글 출력 레이아웃 sample</h1>
<div class="card mb-3">
<div class="card-body d-flex">
<img class="rounded-circle me-3" 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-primary 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-success">등록</button>
<button type="reset" class="btn btn-sm btn-secondary cancel-reply-btn">취소</button>
</form>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-sm-row">
<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:05</small>
</div>
<pre>첫번째 댓글입니다</pre>
<button class="btn btn-sm btn-outline-primary 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-success">등록</button>
<button type="reset" class="btn btn-sm btn-secondary cancel-reply-btn">취소</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<h3>로그인 폼</h3>
<form action="login.jsp" method="post">
<div class="form-floating mb-3">
<input type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<button class="btn btn-primary" type="submit">로그인</button>
</form>
</div>
is-valid : 입력값이 유효성 검사를 통과했을 때 input 요소에 적용
is-invalid : 입력값이 유효성 검사를 실패했을 때 input 요소에 적용
<div class="container">
<h3>폼 유효성 검증 상태 표시하기</h3>
<form action="">
<div>
<label class="form-label" for="id">아이디</label>
<input class="form-control is-valid" type="text" id="id">
<div class="valid-feedback">형식에 맞게 입력했습니다</div>
<div class="invalid-feedback">영문자 소문자로 시작하세요</div>
</div>
<div>
<label class="form-label" for="pwd">비밀번호</label>
<input class="form-control is-invalid" type="text" id="pwd">
</div>
<button type="submit" class="btn btn-primary">가입</button>
</form>
</div>
JAVA & JSP
SelectList 메소드 수정public List<CommentDto> selectList(int parentNum) {
List<CommentDto> list = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = new DbcpBean().getConn();
String sql = """
SELECT comments.num, writer, targetWriter, content, deleted, groupNum,
comments.createdAt, profileImage
FROM comments
INNER JOIN users ON comments.writer = users.userName
WHERE parentNum = ?
ORDER BY groupNum ASC, num ASC
""";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, parentNum);
rs = pstmt.executeQuery();
while (rs.next()) {
CommentDto dto = new CommentDto();
dto.setNum(rs.getInt("num"));
dto.setWriter(rs.getString("writer"));
dto.setTargetWriter(rs.getString("targetWriter"));
dto.setContent(rs.getString("content"));
dto.setParentNum(parentNum);
dto.setGroupNum(rs.getInt("groupNum"));
dto.setDeleted(rs.getString("deleted"));
dto.setCreatedAt(rs.getString("createdAt"));
dto.setProfileImage(rs.getString("profileImage"));
list.add(dto);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {
}
}
return list;
}
➜ 최신 댓글은 아래로
댓글 출력
<%
// 댓글 목록 DB 에서 읽어오기
List<CommentDto> commentList = CommentDao.getInstance().selectList(num);
// 로그인 했는지 여부를 알아내기
boolean isLogin = userName == null ? false : true;
%>
<div class="comments">
<%for(CommentDto tmp : commentList) {%>
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-sm-row">
<%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>
<pre><%=tmp.getContent()%></pre>
<%if(tmp.getWriter().equals(userName)) {%>
<%} 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>
<%} %>
<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", () => {
// 클릭한 버튼의 다음 형제요소의 class 목록에서 d-none 을 제거
// 댓글 버튼을 눌렀을 때 만일 로그인 하지 않았다면
if(!isLogin){
alert("댓글 작성을 위해 로그인이 필요합니다!");
location.href =
"${pageContext.request.contextPath}/user/loginform.jsp?url=${pageContext.request.contextPath}/board/view.jsp?num=<%=num%>";
}
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");
});
});