기능을 구현하기 전에 수정을 위해서는 어떤 것들이 필요할지 생각해 보자. 💭
수정
- 수정 가능한 화면
- 기존 제목, 내용, 이미지, 댓글 (파라미터)
-> 이를 얻어오고자 하는 게시글 번호
목록, 이전/상세 조회
- type (게시판 종류 -> BOARD_CD)
- cp (현재 페이지)
BoardWriteController
, BoardWriteForm.jsp
재활용
- mode (update 모드)
...
<!-- 버튼 영역 -->
<div class="board-btn-area">
<c:if test="${loginMember.memberNo == detail.memberNo}">
<!-- detail?no=1621&type=1 -->
<%-- cp가 없을 경우에 대한 처리 --%>
<c:if test="${empty param.cp}">
<!-- 파라미터에 cp가 없을 경우 -->
<c:set var="cp" value="1"/>
</c:if>
<c:if test="${!empty param.cp}">
<!-- 파라미터에 cp가 있을 경우 param.cp -->
<c:set var="cp" value="${param.cp}"/>
</c:if>
<button id="updateBtn" onclick="location.href='write?mode=update&type=${param.type}&cp=${cp}&no=${detail.boardNo}'">수정</button>
...
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시글 등록</title>
<link rel="stylesheet" href="${contextPath}/resources/css/boardWriteForm-style.css">
<link rel="stylesheet" href="${contextPath}/resources/css/main-style.css">
<script src="https://kit.fontawesome.com/4dca1921b4.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<form action="write" enctype="multipart/form-data" method="POST" class="board-write"
onsubmit="return writeValidate()">
<!-- 제목 -->
<h1 class="board-title">
<input type="text" name="boardTitle" placeholder="제목을 입력해 주세요." value="${detail.boardTitle}">
</h1>
<%-- imageList에 존재하는 이미지 레벨을 이용하여 변수 생성 --%>
<c:forEach items="${detail.imageList}" var="boardImage">
<c:choose>
<c:when test="${boardImage.imageLevel == 0}">
<%-- c:set 변수는 pageScope가 기본값 (조건문이 끝나도 사용 가능) --%>
<c:set var="img0" value="${contextPath}${boardImage.imageReName}"/>
</c:when>
<c:when test="${boardImage.imageLevel == 1}">
<c:set var="img1" value="${contextPath}${boardImage.imageReName}"/>
</c:when>
<c:when test="${boardImage.imageLevel == 2}">
<c:set var="img2" value="${contextPath}${boardImage.imageReName}"/>
</c:when>
<c:when test="${boardImage.imageLevel == 3}">
<c:set var="img3" value="${contextPath}${boardImage.imageReName}"/>
</c:when>
<c:when test="${boardImage.imageLevel == 4}">
<c:set var="img4" value="${contextPath}${boardImage.imageReName}"/>
</c:when>
</c:choose>
</c:forEach>
<!-- 썸네일 -->
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<label for="img0">
<img class="preview" src="${img0}">
</label>
<input type="file" class="inputImage" id="img0" name="0" accept="image/*">
<span class="delete-image">×</span>
<!-- × : x 모양의 문자 -->
</div>
</div>
<!-- 업로드 이미지 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<div class="boardImg">
<label for="img1">
<img class="preview" src="${img1}">
</label>
<input type="file" class="inputImage" id="img1" name="1" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img2">
<img class="preview" src="${img2}">
</label>
<input type="file" class="inputImage" id="img2" name="2" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img3">
<img class="preview" src="${img3}">
</label>
<input type="file" class="inputImage" id="img3" name="3" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img4">
<img class="preview" src="${img4}">
</label>
<input type="file" class="inputImage" id="img4" name="4" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 내용 -->
<div class="board-content">
<!--
XSS 처리로 인해서 <와 같은 형태로 변한 문자들은
HTML 문서에 출력될 때 <가 아닌 해석된 형태 "<"로 출력이 된다.
-> 이 특징을 이용하면 별도로 XSS 처리를 해제하는 코드를 작성할 필요가 없다.
하지만 개행문자 <br> -> \n으로 변경하는 코드는 필요하다!
-->
<textarea name="boardContent">${detail.boardContent}</textarea>
</div>
<!-- 버튼 영역 -->
<div class="board-btn-area">
<button type="submit" id="writeBtn">등록</button>
<!-- insert 모드 -->
<c:if test="${param.mode == 'insert'}">
<button type="button" id="goToListBtn">목록으로</button>
</c:if>
<!-- update 모드 -->
<c:if test="${param.mode == 'update'}">
<button type="button" onclick="location.href='${header.referer}'">이전으로</button>
</c:if>
</div>
<!-- 숨겨진 값(hidden) -->
<!-- 동작 구분 -->
<input type="hidden" name="mode" value="${param.mode}">
<!-- 게시판 구분 -->
<input type="hidden" name="type" value="${param.type}">
<!-- 게시글 번호 -->
<input type="hidden" name="no" value="${param.no}">
<!-- 현재 페이지 -->
<input type="hidden" name="cp" value="${param.cp}">
<!-- 존재하던 이미지가 제거되었음을 기록하여 전달하는 input -->
<!-- value에 제거된 이미지의 레벨을 기록 -->
<!-- DELETE FROM BOARD_IMG
WHERE BOARD_NO = 1000
AND IMG_LEVEL IN (1,3) -->
<input type="hidden" name="deleteList" id="deleteList" value="">
</form>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<script src="${contextPath}/resources/js/board/board.js"></script>
<script src="${contextPath}/resources/js/board/boardWriteForm.js"></script>
</body>
</html>
// 미리보기 관련 요소 모두 얻어오기
const inputImage = document.getElementsByClassName("inputImage");
const preview = document.getElementsByClassName("preview");
const deleteImage = document.getElementsByClassName("delete-image");
// 게시글 수정 시 삭제된 이미지의 레벨(위치)를 저장할 input 요소
const deleteList = document.getElementById("deleteList");
// 게시글 수정 시 삭제된 이미지의 레벨(위치)를 기록해 둘 Set 생성
const deleteSet = new Set(); // 순서 X, 중복 허용 X (여러 번 클릭 시 중복 값 저장 방지)
for(let i=0; i<inputImage.length; i++){
// 파일이 선택되었을 때
inputImage[i].addEventListener("change", function(){
if(this.files[0] != undefined){ // 파일이 선택되었을 때
const reader = new FileReader(); // 선택된 파일을 읽을 객체 생성
reader.readAsDataURL(this.files[0]);
// 지정된 파일을 읽음 -> result에 저장(URL 포함) -> URL을 이용해서 이미지 볼 수 있음
reader.onload = function(e){ // reader가 파일을 다 읽어온 경우
// e.target == reader
// e.target.result == 읽어들인 이미지의 URL
// preview[i] == 파일이 선택된 input태그와 인접한 preview 이미지 태그
preview[i].setAttribute("src", e.target.result);
// 이미지가 성공적으로 불러와졌을 때
// deleteSet에서 해당 레벨을 제거(삭제 목록에서 제외)
deleteSet.delete(i);
}
} else { // 파일이 선택되지 않았을 때 (취소)
preview[i].removeAttribute("src"); // src 속성 제거
}
});
// 미리보기 삭제 버튼(x)이 클릭되었을 때의 동작
deleteImage[i].addEventListener("click", function(){
// 미리보기가 존재하는 경우에만 (이전에 추가된 이미지가 있을 때만) x버튼 동작
if(preview[i].getAttribute("src") != ""){
// 미리보기 삭제
preview[i].removeAttribute("src");
// input의 값을 "" 만들기
inputImage[i].value = "";
// deleteSet에 삭제된 이미지 레벨(i) 추가
deleteSet.add(i);
}
})
}
// 게시글 작성 유효성 검사
function writeValidate(){
const boardTitle = document.getElementsByName("boardTitle")[0];
const boardContent = document.querySelector("[name='boardContent']");
if(boardTitle.value.trim().length == 0){
alert("제목을 입력해 주세요!!!");
boardTitle.value = "";
boardTitle.focus();
return false;
}
if(boardContent.value.trim().length == 0){
alert("내용을 입력해 주세요!!!");
boardContent.value = "";
boardContent.focus();
return false;
}
// 제목, 내용이 유효한 경우
// deleteList(input 태그)에 deleteSet(삭제된 이미지 레벨)을 추가
// -> JS 배열 특징 사용
// --> JS 배열을 HTML 요소 또는 console에 출력하게 되는 경우 1,2,3 같은 문자열로 출력됨
// (배열 기호가 벗겨짐)
// *Set -> Array로 변경 -> deleteList.value에 대입
// Array.from(유사배열 | 컬렉션) : 유사배열 | 컬렉션을 배열로 변환해서 반환
deleteList.value = Array.from(deleteSet);
return true;
}
package edu.kh.community.board.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.oreilly.servlet.MultipartRequest;
import edu.kh.community.board.model.service.BoardService;
import edu.kh.community.board.model.vo.BoardDetail;
import edu.kh.community.board.model.vo.BoardImage;
import edu.kh.community.common.MyRenamePolicy;
import edu.kh.community.member.model.vo.Member;
// 컨트롤러 : 요청에 따라 알맞은 Service를 호출하고 결과에 따라 응답을 제어
@WebServlet("/board/write")
public class BoardWriteController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String mode = req.getParameter("mode"); // insert / update 구분
// insert는 별도 처리 없이 jsp로 위임
// update는 기존 게시글 내용을 조회하는 처리가 필요함
if(mode.equals("update")) {
int boardNo = Integer.parseInt( req.getParameter("no") );
// 게시글 상세조회 서비스를 이용해서 기존 내용 조회
// ( new BoardService() : 객체를 생성해서 변수에 저장 X -> 1회성 사용 )
BoardDetail detail = new BoardService().selectBoardDetail(boardNo);
// 개행문자 처리 해제( <br> -> \n )
detail.setBoardContent(detail.getBoardContent().replaceAll("<br>", "\n"));
req.setAttribute("detail", detail); // jsp에서 사용할 수 있도록 req에 값 세팅
}
String path = "/WEB-INF/views/board/boardWriteForm.jsp";
req.getRequestDispatcher(path).forward(req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// insert/update 구분 없이 전달받은 파라미터 모두 꺼내서 정리하기
// *** enctype="multipart/form-data" 인코딩 미지정 형식의 요청
// -> HttpServletRequest로 파라미터 얻어오기 불가능
// --> MultipartRequest를 이용(cos.jar 라이브러리 제공)
// ---> 업로드 최대 용량, 저장 실제 경로, 파일명 변경 정책, 문자 파라미터 인코딩 설정 필요
int maxSize = 1024 * 1024 * 100; // 업로드 최대 용량 (100MB)
HttpSession session = req.getSession(); // session 얻어오는 것은 지장 없음(사용 가능)
String root = session.getServletContext().getRealPath("/"); // webapp 폴더까지 경로
String folderPath = "/resources/images/board/"; // 파일 저장 폴더 경로
String filePath = root + folderPath;
String encoding = "UTF-8"; // 파라미터 중 파일 제외 파라미터(문자열)의 인코딩 지정
// ** MultipartRequest 객체 생성 **
// -> 객체가 생성됨과 동시에 파라미터로 전달된 파일이 지정된 경로에 저장(업로드)된다.
MultipartRequest mpReq = new MultipartRequest(req, filePath, maxSize, encoding, new MyRenamePolicy());
// MultipartRequest.getFileNames()
// - 요청 파라미터 중 모든 file 타입 태그의 name 속성 값을 얻어와
// Enumeration 형태로 반환 (Iterator의 과거 버전)
// --> 해당 객체에 여러 값이 담겨 있고 이를 순서대로 얻어오는 방법을 제공
// (보통 순서가 없는 모음(Set과 같은 경우)에서 하나씩 꺼낼 때 사용)
Enumeration<String> files = mpReq.getFileNames();
// file 타입 태그의 name 속성 0,1,2,3,4가 순서가 섞인 상태로 얻어와짐
// * 업로드된 이미지의 정보를 모아둘 List 생성
List<BoardImage> imageList = new ArrayList<BoardImage>();
while(files.hasMoreElements()) { // 다음 요소가 있으면 true
String name = files.nextElement(); // 다음 요소(name 속성 값)를 얻어옴
// System.out.println("name : " + name);
// file 타입 태그의 name 속성 값이 얻어와짐
// + 업로드가 안 된 file 타입 태그의 name도 얻어와짐
String rename = mpReq.getFilesystemName(name); // 변경된 파일명
String original = mpReq.getOriginalFileName(name); // 원본 파일명
// System.out.println("rename : " + rename);
// System.out.println("original : " + original);
if(rename != null) { // 업로드된 파일이 있을 경우 ==
// 현재 files에서 얻어온 name속성을 이용해
// 원본 또는 변경을 얻어왔을 때 그 값이 null이 아닌 경우
// 이미지 정보를 담은 객체(BoardImage)를 생성
BoardImage image = new BoardImage();
image.setImageOriginal(original); // 원본명 (다운로드 시 사용)
image.setImageReName(folderPath + rename); // 폴더 경로 + 변경명
image.setImageLevel( Integer.parseInt(name) ); // 이미지 위치(0은 썸네일)
imageList.add(image); // 리스트에 추가
} // if 끝
} // while 끝
// * 이미지를 제외한 게시글 관련 정보 *
String boardTitle = mpReq.getParameter("boardTitle");
String boardContent = mpReq.getParameter("boardContent");
int boardCode = Integer.parseInt(mpReq.getParameter("type")); // hidden (게시판 구분을 위해 만들어 놓음)
Member loginMember = (Member)session.getAttribute("loginMember");
int memberNo = loginMember.getMemberNo(); // 회원 번호
// 게시글 관련 정보를 하나의 객체(BoardDetail)에 담기
BoardDetail detail = new BoardDetail();
detail.setBoardTitle(boardTitle);
detail.setBoardContent(boardContent);
detail.setMemberNo(memberNo);
// boardCode는 별도 매개변수로 전달 예정
// ----------------- 게시글 작성에 필요한 기본 파라미터 얻어오기 끝 ----------------- //
BoardService service = new BoardService();
// 모드 (insert/update)에 따라 추가 파라미터 얻어오기 및 서비스 호출
String mode = mpReq.getParameter("mode"); // hidden
if(mode.equals("insert")) { // 삽입
// 게시글 삽입 서비스 호출 후 결과 반환 받기
// -> 반환된 게시글 번호를 이용해서 상세조회로 리다이렉트 예정
int boardNo = service.insertBoard(detail, imageList, boardCode);
String path = null;
if(boardNo > 0) { // 성공
session.setAttribute("message", "게시글이 등록되었습니다.");
path = "detail?no=" + boardNo + "&type=" + boardCode;
} else { // 실패
session.setAttribute("message", "게시글 등록 실패");
path = "write?mode=" + mode + "&type=" + boardCode;
}
resp.sendRedirect(path); // 리다이렉트
}
if(mode.equals("update")) { // 수정
// 앞선 코드는 동일(업로드된 이미지 저장, imageList 생성, 제목/내용 파라미터 동일)
// + update일 때 추가된 내용
// 어떤 게시글 수정? -> 파라미터 no
// 나중에 목록으로 버튼 만들 때 사용할 현재 페이지 -> 파라미터 cp
// 이미지 중 x버튼을 눌러서 삭제할 이미지 레벨 목록 -> 파라미터 deleteList
int boardNo = Integer.parseInt( mpReq.getParameter("no") );
int cp = Integer.parseInt( mpReq.getParameter("cp") );
String deleteList = mpReq.getParameter("deleteList"); // 1,2,3
// 게시글 수정 서비스 호출 후 결과 반환 받기
detail.setBoardNo(boardNo);
// detail, imageList, deleteList
int result = service.updateBoard(detail, imageList, deleteList);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/** 게시글 수정 Service
* @param detail
* @param imageList
* @param deleteList
* @return result
* @throws Exception
*/
public int updateBoard(BoardDetail detail, List<BoardImage> imageList, String deleteList) throws Exception {
Connection conn = getConnection();
// 1. 게시글 부분(제목, 내용, 마지막 수정일) 수정
// 1) XSS 방지 처리 (제목, 내용)
detail.setBoardTitle(Util.XSSHandling(detail.getBoardTitle()));
detail.setBoardContent(Util.XSSHandling(detail.getBoardContent()));
// 2) 개행문자 처리 (내용)
detail.setBoardContent(Util.newLineHandling(detail.getBoardContent()));
// 3) DAO 호출
int result = dao.updateBoard(conn, detail);
if(result > 0) { // 게시글 수정 성공 시
// 2. 이미지 부분(기존 -> 변경, 없다가 -> 추가) 수정
for(BoardImage img : imageList) {
img.setBoardNo(detail.getBoardNo()); // 게시글 번호 세팅
// img(변경명: a.jpg / 원본명: 1.jpg / 게시글번호: 1526 / 이미지레벨: 4)
/*
* UPDATE BOARD_IMG SET
IMG_RENAME = 'a.jpg',
IMG_ORIGINAL = '1.jpg'
WHERE BOARD_NO = 1526
AND IMG_LEVEL = 4;
* */
// 이미지 1개씩 수정
result = dao.updateBoardImage(conn, img);
// result == 1 : 수정 성공
// result == 0 : 수정 실패 -> 기존에 없다가 새로 추가된 이미지
// -> insert 진행
if(result == 0) {
result = dao.insertBoardImage(conn, img);
}
} // 향상된 for문 끝
// 3. 이미지 삭제
// deleteList ( 값이 있으면 "1,2,3" 이런 모양, 없으면 ""(빈 문자열) )
if(!deleteList.equals("")) { // 삭제된 이미지 레벨이 기록되어 있을 때만 삭제
result = dao.deleteBoardImage(conn, deleteList, detail.getBoardNo());
}
} // 게시글 수정 성공 시 if 끝
if(result > 0) commit(conn);
else rollback(conn);
close(conn);
return result;
}
/** 게시글 이미지 수정 DAO
* @param conn
* @param img
* @return result
* @throws Exception
*/
public int updateBoardImage(Connection conn, BoardImage img) throws Exception {
int result = 0;
try {
String sql = prop.getProperty("updateBoardImage");
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, img.getImageReName());
pstmt.setString(2, img.getImageOriginal());
pstmt.setInt(3, img.getBoardNo());
pstmt.setInt(4, img.getImageLevel());
result = pstmt.executeUpdate();
} finally {
close(pstmt);
}
return result;
}
/** 게시글 이미지 삭제 DAO
* @param conn
* @param deleteList
* @param boardNo
* @return result
* @throws Exception
*/
public int deleteBoardImage(Connection conn, String deleteList, int boardNo) throws Exception {
int result = 0;
try {
// 완성되지 않은 SQL
String sql = prop.getProperty("deleteBoardImage") + deleteList + ")";
// "DELETE FROM BOARD_IMG WHERE BOARD_NO = ? AND IMG_LEVEL IN( 1,0 )
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, boardNo);
result = pstmt.executeUpdate();
} finally {
close(pstmt);
}
return result;
}
<!-- 게시글 수정 -->
<entry key="updateBoard">
UPDATE BOARD SET
BOARD_TITLE = ?,
BOARD_CONTENT = ?,
UPDATE_DT = SYSDATE
WHERE BOARD_NO = ?
</entry>
<!-- 게시글 이미지 수정 -->
<entry key="updateBoardImage">
UPDATE BOARD_IMG SET
IMG_RENAME = ?,
IMG_ORIGINAL = ?
WHERE BOARD_NO = ?
AND IMG_LEVEL = ?
</entry>
<!-- 게시글 이미지 삭제 -->
<entry key="deleteBoardImage">
DELETE FROM BOARD_IMG
WHERE BOARD_NO = ?
AND IMG_LEVEL IN (
</entry>
정상적으로 게시글의 이미지와 내용이 수정된다.
또한 마지막 수정일에도 수정한 날짜가 반영되어 출력된다. 👀👌