51일차 내용 정리

채공부·2025년 8월 4일

JavaScript

사진 게시글

Masonry
벽돌을 쌓아 올린 듯한 모양을 연상시키는 디자인

  • list.jsp 코드 추가
<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});
});
  • view.jsp 생성
<%
	// 자세히 보여줄 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 -->
  • GalleryDto 필드 추가
// 프로필 이미지를 출력하기 위한 필드
private String profileImage;

public void setProfileImage(String profileImage) {
	this.profileImage = profileImage;
}
	
public String getProfileImage() {
	return profileImage;
}
  • GalleryDao 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;
}

댓글 목록 출력

  • view.jsp 코드 추가
<%
	// 댓글 목록 얻어내기
	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 %>`;
        }
    });
});
  • save-comment.jsp 생성
<%
	// 폼 전송되는 댓글 정보를 얻어낸다
	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);	
%>
  • comment-upate.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}/gallery/view.jsp?num=<%=parentNum%>";
<% } else { %>
	alert("수정 실패");
	location.href = "${pageContext.request.contextPath}/gallery/view.jsp?num=<%=parentNum%>";
<% } %>
  • 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+"/gallery/view.jsp?num="+parentNum);
%>
  • new-form.jsp 코드 추가
<form id="saveForm"> // 폼에 아이디 부여
// 폼에 "submit" 이벤트가 일어났을 때 실행할 함수 등록
document.querySelector("#saveForm").addEventListener("submit", (e) => {
    // 여기서 폼 입력란에 대한 검증을 하고 폼 제출을 막고 싶으면 e.preventDefault() 를 실행하게 하면 된다
    // 만일 선택한 파일이 없다면 폼 전송 막기
    if (selectedFiles.length < 1) {
        alert("업로드할 이미지를 1개 이상 선택해 주세요");
        e.preventDefault();
    }
});
profile
학원 공부 내용 정리

0개의 댓글