관리 메뉴

거니의 velog

(14) 스프링으로 답변형 게시판 만들기 3 본문

Java_Spring Framework part2

(14) 스프링으로 답변형 게시판 만들기 3

Unlimited00 2023. 11. 22. 13:47

6. 글상세창 구현하기

* 이번에는 글상세창으로 표시하는 기능을 구현해 보자.

1. 매퍼 파일 board.xml에 전달된 글 번호에 대해 글 정보를 조회하는 SQL문을 추가한다.

	<select id="selectArticle" resultType="articleVO" parameterType="int">
	    <![CDATA[
	      SELECT * from t_board
	      where articleNO = #{articleNO}		
	    ]]>
	</select>

2. 글상세창(viewArticle.jsp)을 나타낼 타일즈 기능을 설정한다.

[tiles_board.xml]

	<definition name="/board/viewArticle" extends="baseLayout">
		<put-attribute name="title" value="글상세창" />
		<put-attribute name="body" value="/WEB-INF/views/board/viewArticle.jsp" />
	</definition>

3. 첨부 파일을 표시할 파일 다운로드 컨트롤러인 FileDownloadController를 common/file 패키지에 구현한다.

4. 이전의 FileDownloadController 클래스를 복사해 붙여 넣은 후 다음과 같이 수정한다.

package com.myspring.pro30.common.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class FileDownloadController {
	private static final String ARTICLE_IMAGE_REPO = "C:\\board\\article_image";

	@RequestMapping("/download.do")
	protected void download(@RequestParam("imageFileName") String imageFileName, // 이미지 파일 이름을 바로 설정한다.
			@RequestParam("articleNO") String articleNO, 
			HttpServletResponse response) throws Exception {
		OutputStream out = response.getOutputStream();
		String downFile = ARTICLE_IMAGE_REPO + "\\" + articleNO + "\\" + imageFileName; // 글 번호와 파일 이름으로 다운로드 할 파일 경로를 설정한다.
		File file = new File(downFile);

		response.setHeader("Cache-Control", "no-cache");
		response.addHeader("Content-disposition", "attachment; fileName=" + imageFileName);
		FileInputStream in = new FileInputStream(file);
		byte[] buffer = new byte[1024 * 8];
		while (true) {
			int count = in.read(buffer);
			if (count == -1)
				break;
			out.write(buffer, 0, count);
		}
		in.close();
		out.close();
	}

}

5. 글목록창에서 전달된 글 번호를 이용하여 해당 글 정보를 조회한다.

[BoardControllerImpl.java]

	@RequestMapping(value = "/board/viewArticle.do", method = RequestMethod.GET)
	public ModelAndView viewArticle(@RequestParam("articleNO") int articleNO, // 조회할 글 번호를 가져온다.
			HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		
		String viewName = (String) request.getAttribute("viewName");
		articleVO = boardService.viewArticle(articleNO); // 조회한 글 정보를 articleVO에 설정한다.
		ModelAndView mav = new ModelAndView();
		mav.setViewName(viewName);
		mav.addObject("article", articleVO);
		return mav;
		
	}

6. Service 클래스와 DAO 클래스의 메서드는 다음과 같다.

	@Override
	public ArticleVO viewArticle(int articleNO) throws Exception {
		ArticleVO articleVO = boardDAO.selectArticle(articleNO);
		return articleVO;
	}
	@Override
	public ArticleVO selectArticle(int articleNO) throws DataAccessException {
		return sqlSession.selectOne("mapper.board.selectArticle", articleNO);
	}

7. 마지막으로 글 상세 정보를 표시할 JSP 파일을 다음과 같이 작성한다. 단, 글 수정과 삭제는 자신이 작성한 글일 경우에만 할 수 있도록 설정한다.

[viewArticle.jsp]

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
    isELIgnored="false" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<%-- 
<c:set var="article"  value="${articleMap.article}"  />
<c:set var="imageFileList"  value="${articleMap.imageFileList}"  />

 --%>
<%
  request.setCharacterEncoding("UTF-8");
%>

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <title>글보기</title>
    <style>
        #tr_file_upload {
            display: none;
        }

        #tr_btn_modify {
            display: none;
        }
    </style>
    <script src="http://code.jquery.com/jquery-latest.min.js"></script>
    <script type="text/javascript">
        function backToList(obj) {
            obj.action = "${contextPath}/board/listArticles.do";
            obj.submit();
        }

        function fn_enable(obj) {
            document.getElementById("i_title").disabled = false;
            document.getElementById("i_content").disabled = false;
            document.getElementById("i_imageFileName").disabled = false;
            document.getElementById("tr_btn_modify").style.display = "block";
            document.getElementById("tr_file_upload").style.display = "block";
            document.getElementById("tr_btn").style.display = "none";
        }

        function fn_modify_article(obj) {
            obj.action = "${contextPath}/board/modArticle.do";
            obj.submit();
        }

        function fn_remove_article(url, articleNO) {
            var form = document.createElement("form");
            form.setAttribute("method", "post");
            form.setAttribute("action", url);
            var articleNOInput = document.createElement("input");
            articleNOInput.setAttribute("type", "hidden");
            articleNOInput.setAttribute("name", "articleNO");
            articleNOInput.setAttribute("value", articleNO);

            form.appendChild(articleNOInput);
            document.body.appendChild(form);
            form.submit();

        }

        function fn_reply_form(url, parentNO) {
            var form = document.createElement("form");
            form.setAttribute("method", "post");
            form.setAttribute("action", url);
            var parentNOInput = document.createElement("input");
            parentNOInput.setAttribute("type", "hidden");
            parentNOInput.setAttribute("name", "parentNO");
            parentNOInput.setAttribute("value", parentNO);

            form.appendChild(parentNOInput);
            document.body.appendChild(form);
            form.submit();
        }

        function readURL(input) {
            if (input.files && input.files[0]) {
                var reader = new FileReader();
                reader.onload = function(e) {
                    $('#preview').attr('src', e.target.result);
                }
                reader.readAsDataURL(input.files[0]);
            }
        }
    </script>
</head>

<body>
    <form name="frmArticle" method="post" action="${contextPath}" enctype="multipart/form-data">
        <table border="0" align="center">
            <tr>
                <td width="150" align="center" bgcolor="#FF9933">
                    글번호
                </td>
                <td>
                    <input type="text" value="${article.articleNO }" disabled />
                    <input type="hidden" name="articleNO" value="${article.articleNO}" />
                </td>
            </tr>
            <tr>
                <td width="150" align="center" bgcolor="#FF9933">
                    작성자 아이디
                </td>
                <td>
                    <input type="text" value="${article.id }" name="writer" disabled />
                </td>
            </tr>
            <tr>
                <td width="150" align="center" bgcolor="#FF9933">
                    제목
                </td>
                <td>
                    <input type="text" value="${article.title }" name="title" id="i_title" disabled />
                </td>
            </tr>
            <tr>
                <td width="150" align="center" bgcolor="#FF9933">
                    내용
                </td>
                <td>
                    <textarea rows="20" cols="60" name="content" id="i_content" disabled />${article.content }</textarea>
                </td>
            </tr>
            <%-- 
 <c:if test="${not empty imageFileList && imageFileList!='null' }">
	  <c:forEach var="item" items="${imageFileList}" varStatus="status" >
		    <tr>
			    <td width="150" align="center" bgcolor="#FF9933"  rowspan="2">
			      이미지${status.count }
			   </td>
			   <td>
			     <input  type= "hidden"   name="originalFileName" value="${item.imageFileName }" />
			    <img src="${contextPath}/download.do?articleNO=${article.articleNO}&imageFileName=${item.imageFileName}" id="preview"  /><br>
			   </td>   
			  </tr>  
			  <tr>
			    <td>
			       <input  type="file"  name="imageFileName " id="i_imageFileName"   disabled   onchange="readURL(this);"   />
			    </td>
			 </tr>
		</c:forEach>
 </c:if>
 	 --%>

            <c:choose>
                <c:when test="${not empty article.imageFileName && article.imageFileName!='null' }">
                    <tr>
                        <td width="150" align="center" bgcolor="#FF9933" rowspan="2">
                            이미지
                        </td>
                        <td>
                            <input type="hidden" name="originalFileName" value="${article.imageFileName }" />
                            <img src="${contextPath}/download.do?articleNO=${article.articleNO}&imageFileName=${article.imageFileName}" id="preview" /><br>
                        </td>
                    </tr>
                    <tr>
                        <td></td>
                        <td>
                            <input type="file" name="imageFileName " id="i_imageFileName" disabled onchange="readURL(this);" />
                        </td>
                    </tr>
                </c:when>
                <c:otherwise>
                    <tr id="tr_file_upload">
                        <td width="150" align="center" bgcolor="#FF9933" rowspan="2">
                            이미지
                        </td>
                        <td>
                            <input type="hidden" name="originalFileName" value="${article.imageFileName }" />
                        </td>
                    </tr>
                    <tr>
                        <td></td>
                        <td>
                            <img id="preview" /><br>
                            <input type="file" name="imageFileName " id="i_imageFileName" disabled onchange="readURL(this);" />
                        </td>
                    </tr>
                </c:otherwise>
            </c:choose>
            <tr>
                <td width="150" align="center" bgcolor="#FF9933">
                    등록일자
                </td>
                <td>
                    <input type=text value="<fmt:formatDate value=" ${article.writeDate}" />" disabled />
                </td>
            </tr>
            <tr id="tr_btn_modify" align="center">
                <td colspan="2">
                    <input type=button value="수정반영하기" onClick="fn_modify_article(frmArticle)">
                    <input type=button value="취소" onClick="backToList(frmArticle)">
                </td>
            </tr>

            <tr id="tr_btn">
                <td colspan="2" align="center">
                	<%-- 로그인 ID가 작성자 ID와 같은 경우에만 수정하기, 삭제하기 버튼이 표시된다. --%>
                    <c:if test="${member.id == article.id }">
                        <input type=button value="수정하기" onClick="fn_enable(this.form)">
                        <input type=button value="삭제하기" onClick="fn_remove_article('${contextPath}/board/removeArticle.do', ${article.articleNO})">
                    </c:if>
                    <input type=button value="리스트로 돌아가기" onClick="backToList(this.form)">
                    <input type=button value="답글쓰기" onClick="fn_reply_form('${contextPath}/board/replyForm.do', ${article.articleNO})">
                </td>
            </tr>
        </table>
    </form>
</body>

</html>

8. 다음은 실행 결과이다. 로그인하지 않았을 때와 로그인 했을 때의 결과가 어떻게 다른지 비교해 보자.

- http://localhost/pro30/main.do

(1) 로그인 하지 않았을 때

(2) 로그인 했을 때 : 수정하기와 삭제하기 버튼이 활성화되어 보임


7. 글 수정하기

* 이번에는 글을 수정할 수 있는 기능을 스프링으로 구현해 보자. 글을 추가하는 기능과 유사하므로 쉽게 이해할 수 있을 것이다.

1. 매퍼 파일에 update문을 추가한다. if문을 사용하여 글 수정 시 이미지를 수정한 경우에만 이미지 파일 이름을 업데이트 하도록 지정한다.

[board.xml]

	<!-- Map으로 글 정보를 가져온다. -->
	<!-- 이미지를 수정한 경우에만 이미지 파일 이름을 수정한다. -->
	<update id="updateArticle" parameterType="java.util.Map">
		update t_board
		set title=#{title},
		content=#{content}
		<if test="imageFileName!='' and imageFileName!=null">
			, imageFileName=#{imageFileName}
		</if>
		where articleNO=#{articleNO}
	</update>

2. 자바 파일을 작성할 차례이다. 글 수정 시 이미지 파일도 수정해서 업로드해야 하므로 MultipartHttpServletRequest를 사용해 업로드한 후 글 정보를 articleMap에 key/value로 담아 테이블에 추가한다. 그리고 수정된 새 이미지를 글 번호 폴더에 업로드한 후에는 반드시 기존 이미지를 삭제해야 한다. 글 수정을 마친 후에는 다시 글상세창을 나타낸다.

[BoardControllerImpl.java]

	@RequestMapping(value = "/board/modArticle.do", method = RequestMethod.POST)
	@ResponseBody
	public ResponseEntity modArticle(MultipartHttpServletRequest multipartRequest, HttpServletResponse response)
			throws Exception {
		multipartRequest.setCharacterEncoding("utf-8");
		Map<String, Object> articleMap = new HashMap<String, Object>();
		Enumeration enu = multipartRequest.getParameterNames();
		while (enu.hasMoreElements()) {
			String name = (String) enu.nextElement();
			String value = multipartRequest.getParameter(name);
			articleMap.put(name, value);
		}

		String imageFileName = upload(multipartRequest);
		articleMap.put("imageFileName", imageFileName);

		String articleNO = (String) articleMap.get("articleNO");
		String message;
		ResponseEntity resEnt = null;
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.add("Content-Type", "text/html; charset=utf-8");
		try {
			boardService.modArticle(articleMap);
			if (imageFileName != null && imageFileName.length() != 0) {
				File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
				File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
				FileUtils.moveFileToDirectory(srcFile, destDir, true); // 새로 첨부한 파일을 폴더로 이동한다.

				String originalFileName = (String) articleMap.get("originalFileName");
				File oldFile = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO + "\\" + originalFileName);
				oldFile.delete(); // 기존 파일을 삭제한다.
			}
			message = "<script>";
			message += " alert('글을 수정했습니다.');";
			message += " location.href='" + multipartRequest.getContextPath() + "/board/viewArticle.do?articleNO="
					+ articleNO + "';";
			message += " </script>";
			resEnt = new ResponseEntity(message, responseHeaders, HttpStatus.CREATED);
		} catch (Exception e) {
			File srcFile = new File(ARTICLE_IMAGE_REPO + "\\" + "temp" + "\\" + imageFileName);
			srcFile.delete();
			message = "<script>";
			message += " alert('오류가 발생했습니다.다시 수정해주세요');";
			message += " location.href='" + multipartRequest.getContextPath() + "/board/viewArticle.do?articleNO="
					+ articleNO + "';";
			message += " </script>";
			resEnt = new ResponseEntity(message, responseHeaders, HttpStatus.CREATED);
		}
		return resEnt;
	}

3. Service 클래스와 DAO 클래스는 다음과 같다.

	@Override
	public void modArticle(Map articleMap) throws Exception {
		boardDAO.updateArticle(articleMap);
	}
	@Override
	public void updateArticle(Map articleMap) throws DataAccessException {
		sqlSession.update("mapper.board.updateArticle", articleMap);
	}

4. 이전에 글상세창을 구현할 때 사용한 viewArticle.jsp를 복사해 붙여 넣는다. 그리고 첨부 파일이 없는 글을 수정할 때는 파일 업로드 기능이 표시되므로 수정해야 한다.

[viewArticle.jsp]

			<c:choose>
				<c:when
					test="${not empty article.imageFileName && article.imageFileName!='null' }">
					<tr>
						<td width="150" align="center" bgcolor="#FF9933" rowspan="2">
							이미지</td>
						<td><input type="hidden" name="originalFileName"
							value="${article.imageFileName }" /> <img
							src="${contextPath}/download.do?articleNO=${article.articleNO}&imageFileName=${article.imageFileName}"
							id="preview" /><br></td>
					</tr>
					<tr>
						<td></td>
						<td><input type="file" name="imageFileName "
							id="i_imageFileName" disabled onchange="readURL(this);" /></td>
					</tr>
				</c:when>
				<c:otherwise> <%-- 첨부 파일이 없는 글을 수정할 때는 파일 업로드가 표시되도록 한다. --%>
					<tr id="tr_file_upload">
						<td width="150" align="center" bgcolor="#FF9933" rowspan="2">
							이미지</td>
						<td><input type="hidden" name="originalFileName"
							value="${article.imageFileName }" /></td>
					</tr>
					<tr>
						<td></td>
						<td><img id="preview" /><br> <input type="file"
							name="imageFileName " id="i_imageFileName" disabled
							onchange="readURL(this);" /></td>
					</tr>
				</c:otherwise>
			</c:choose>

5. 다음은 실행 결과이다. 글상세창에서 수정하기를 클릭하면 글을 수정할 수 있는 상태로 바뀐다. 글 내용과 이미지를 수정한 후 수정반영하기를 클릭한다.

- http://localhost/pro30/main.do

6. 그러면 다음과 같이 수정된 글 정보를 테이블에 반영하고 새로운 이미지를 업로드한 후 다시 글상세창을 나타낸다.


8. 글 삭제하기

* 이번에는 게시판에서 글을 삭제하는 기능을 추가해 보자.

1. 매퍼 파일에서 글 번호를 가져와 관련된 자식 글까지 삭제하는 delete 문을 추가한다.

[board.xml]

	<!-- 글 번호를 가져온다. -->
	<delete id="deleteArticle" parameterType="int">
	    <![CDATA[
	      delete from t_board
	      where articleNO in (
	         SELECT articleNO FROM  t_board
	         START WITH articleNO = #{articleNO}
	         CONNECT BY PRIOR  articleNO = parentNO )
	    ]]>
	</delete>

2. 컨트롤러에서는 글 번호를 가져와 해당 글을 삭제한다. 그리고 해당되는 이미지가 저장된 폴더도 함께 삭제한다.

[BoardControllerImpl.java]

	@Override
	@RequestMapping(value = "/board/removeArticle.do", method = RequestMethod.POST)
	@ResponseBody
	public ResponseEntity removeArticle(@RequestParam("articleNO") int articleNO, // 삭제할 글 번호를 가져온다.
			HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		
		response.setContentType("text/html; charset=UTF-8");
		String message;
		ResponseEntity resEnt = null;
		HttpHeaders responseHeaders = new HttpHeaders();
		responseHeaders.add("Content-Type", "text/html; charset=utf-8");
		try {
			boardService.removeArticle(articleNO); // 글 번호를 전달해서 글을 삭제한다.
			File destDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
			FileUtils.deleteDirectory(destDir); // 글에 첨부된 이미지 파일이 저장된 폴더도 삭제한다.

			message = "<script>";
			message += " alert('글을 삭제했습니다.');";
			message += " location.href='" + request.getContextPath() + "/board/listArticles.do';";
			message += " </script>";
			resEnt = new ResponseEntity(message, responseHeaders, HttpStatus.CREATED);

		} catch (Exception e) {
			message = "<script>";
			message += " alert('작업중 오류가 발생했습니다.다시 시도해 주세요.');";
			message += " location.href='" + request.getContextPath() + "/board/listArticles.do';";
			message += " </script>";
			resEnt = new ResponseEntity(message, responseHeaders, HttpStatus.CREATED);
			e.printStackTrace();
		}
		
		return resEnt;
		
	}

3. Service 클래스와 DAO 클래스는 다음과 같다.

	@Override
	public void removeArticle(int articleNO) throws Exception {
		boardDAO.deleteArticle(articleNO);
	}
	@Override
	public void deleteArticle(int articleNO) throws DataAccessException {
		sqlSession.delete("mapper.board.deleteArticle", articleNO);
	}

4. 다음은 실행 결과이다. 삭제하기를 클릭한다.

- http://localhost/pro30/main.do

5. 해당 글을 삭제한 후 글목록창을 다시 표시한다. 글이 삭제된 모습을 볼 수 있다.