JavaScript
Masonry
벽돌을 쌓아 올린 듯한 모양을 연상시키는 디자인
<script src="http://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
<script src="http://unpkg.com/imagesloaded@4/imagesloaded.pkgd.min.js"></script>
<div class="row" id="masonry-grid"></div>
// masonry 레이아웃을 초기화 하는 javascript
const grid = document.querySelector("#masonry-grid");
imagesLoaded(grid, ()=>{
new Masonry(grid, { percentPosition:true});
});
<%
// 자세히 보여줄 gallery의 PK 얻어내기
int num = Integer.parseInt(request.getParameter("num"));
// galley 정보 얻어오기
GalleryDto dto = test.dao.GalleryDao.getInstance().getData(num);
// gallery 에 업로드된 이미지 목록 얻어오기
List<GalleryImageDto> images = GalleryDao.getInstance().getImageList(num);
// 로그인된 userName (null 일 가능성 있다)
String userName = (String)session.getAttribute("userName");
%>
<jsp:include page="/WEB-INF/include/resource.jsp"/>
<div class="container">
<div class="card shadow">
<!-- 작성자 정보 -->
<div class="card-header d-flex align-items-center">
<%if(dto.getProfileImage() == null){ %>
<i style="font-size:100px;" class="bi bi-person-circle"></i>
<%}else{ %>
<img src="${pageContext.request.contextPath }/upload/<%=dto.getProfileImage() %>"
style="width:100px;height:100px;border-radius:50%;"/>
<%} %>
<strong class="ms-3"><%=dto.getWriter()%></strong>
<span class="text-muted ms-auto"><%=dto.getCreatedAt()%></span>
</div>
<!-- 본문 -->
<div class="card-body">
<h5 class="card-title"><%=dto.getTitle()%></h5>
<p class="card-text"><%=dto.getContent().replaceAll("\n", "<br>")%></p>
<div class="row">
<%for (GalleryImageDto tmp: images) {%>
<div class="col-md-6 mb-4">
<img src="<%=request.getContextPath()%>/upload/<%=tmp.getSaveFileName()%>"
class="img-fluid rounded" alt="photo">
</div>
<%}%>
</div>
</div>
</div> <!-- card shadow -->
</div> <!-- container -->
// 프로필 이미지를 출력하기 위한 필드
private String profileImage;
public void setProfileImage(String profileImage) {
this.profileImage = profileImage;
}
public String getProfileImage() {
return profileImage;
}
getData 메소드 수정public GalleryDto getData(int num) {
GalleryDto dto = null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = """
SELECT g.num, title, writer, content, TO_CHAR(g.createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt,
profileImage
FROM gallery g
JOIN users u ON writer = userName
WHERE g.num = ?
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, num);
rs = pstmt.executeQuery();
if (rs.next()) {
dto = new GalleryDto();
dto.setNum(rs.getInt("num"));
dto.setTitle(rs.getString("title"));
dto.setWriter(rs.getString("writer"));
dto.setContent(rs.getString("content"));
dto.setCreatedAt(rs.getString("createdAt")); // string 처리
dto.setProfileImage(rs.getString("profileImage"));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return dto;
}
<%
// 댓글 목록 얻어내기
List<CommentDto> commentList = CommentDao.getInstance().selectList(num);
// 로그인 했는지 여부를 알아내기
boolean isLogin = userName == null ? false:true;
%>
<div class="card my-3">
<div class="card-header bg-primary-subtle text-white">
댓글을 입력해 주세요
</div>
<div class="card-body">
<!-- 원글의 댓글을 작성할 폼 -->
<form action="${pageContext.request.contextPath}/gallery/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 <%=tmp.getNum() == tmp.getGroupNum() ? "":"ms-5"%>">
<%if(tmp.getDeleted().equals("yes")) {%>
<!-- 삭제된 댓글 -->
<div class="card-body bg-light text-muted rounded">삭제된 댓글입니다</div>
<%}else {%>
<!-- 삭제되지 않은 댓글 -->
<div class="card-body d-flex flex-column flex-sm-row position-relative">
<%if(tmp.getNum() != tmp.getGroupNum()){ %>
<i class="bi bi-arrow-return-right position-absolute" style="top:0;left:-30px"></i>
<%} %>
<!-- 앳글 작성자가 로그인된 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="save-comment.jsp" method="post">
<!-- 원글의 글번호, 댓글 대상자의 userName, 댓글의 그룹번호도 같이 전송해야 한다 -->
<input type="hidden" name="parentNum" value="<%=dto.getNum()%>"/>
<input type="hidden" name="targetWriter" value="<%= tmp.getWriter()%>"/>
<input type="hidden" name="groupNum" value="<%=tmp.getGroupNum()%>"/>
<textarea name="content" 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> <!-- card body -->
<%} %>
</div> <!-- card -->
<%} %>
</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");
});
});
// 취소 버튼을 눌렀을 때 이벤트 리스너 등록
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 + "번 댓글을 삭제하시겠습니까?");
// confirm 은 확인:true 취소:false
if (isDelete) {
// "delete.jsp?num = 삭제할 댓글 번호&parentNum = 원글의 번호" 형식의 요청이 되도록 한다
// \${} 는 jsp 가 해석하지 않도록 \${} 역슬래시를 붙여서 작성한다
location.href = `comment-delete.jsp?num=\${num}&parentNum=<%=num %>`;
}
});
});
<%
// 폼 전송되는 댓글 정보를 얻어낸다
int parentNum = Integer.parseInt(request.getParameter("parentNum"));
String targetWriter = request.getParameter("targetWriter");
String content = request.getParameter("content");
// 댓글의 그룹번호가 넘어오는지 읽어와 본다 (null 이면 원글의 댓글 저장 요청이다)
String strGroupNum = request.getParameter("groupNum");
// 댓글의 그룹번호를 담을 변수를 미리 만들고
int groupNum = 0;
// 만일 댓글의 그룹번호가 넘어 온다면 (댓글의 댓글이라면)
if(strGroupNum != null){
groupNum = Integer.parseInt(strGroupNum);
}
// 댓글 작성자 정보는 session 으로부터 얻어낸다
String writer = (String) session.getAttribute("userName");
// 저장할 댓글의 글 번호를 미리 얻어낸다
int num = CommentDao.getInstance().getSequence();
// 저장할 댓글 정보를 dto 에 담고
CommentDto dto = new CommentDto();
dto.setNum(num);
dto.setWriter(writer);
dto.setTargetWriter(targetWriter);
dto.setContent(content);
dto.setParentNum(parentNum);
// 만일 원글의 댓글이면
if(groupNum == 0){
dto.setGroupNum(num); // 원글의 댓글은 자신의 글번호가 댓글의 글번호이다
} else{
dto.setGroupNum(groupNum); // 대댓글은 전송된 그룹번호가 댓글의 그룹 번호이다
}
// DB 에 저장하고
boolean isSuccess = CommentDao.getInstance().insert(dto);
// 응답한다 (redirect: 새로운 경로로 요청을 다시 하라고 응답)
String cPath = request.getContextPath();
// 원래 글 자세히 보기 페이지로 이동하라는 redirect 응답
response.sendRedirect(cPath+"/gallery/view.jsp?num="+parentNum);
%>
<%
// 폼 전송되는 수정할 댓글의 정보를 얻어온다
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}/gallery/view.jsp?num=<%=parentNum%>";
<% } else { %>
alert("수정 실패");
location.href = "${pageContext.request.contextPath}/gallery/view.jsp?num=<%=parentNum%>";
<% } %>
<%
// 삭제할 댓글 번호
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+"/gallery/view.jsp?num="+parentNum);
%>
<form id="saveForm"> // 폼에 아이디 부여
// 폼에 "submit" 이벤트가 일어났을 때 실행할 함수 등록
document.querySelector("#saveForm").addEventListener("submit", (e) => {
// 여기서 폼 입력란에 대한 검증을 하고 폼 제출을 막고 싶으면 e.preventDefault() 를 실행하게 하면 된다
// 만일 선택한 파일이 없다면 폼 전송 막기
if (selectedFiles.length < 1) {
alert("업로드할 이미지를 1개 이상 선택해 주세요");
e.preventDefault();
}
});