게시판 - 글 수정 기능 구현 (23.08.29)

·2023년 8월 29일
0

Spring

목록 보기
24/36
post-thumbnail

🌷 게시글 수정


이번 포스팅에서는 게시글 수정 기능을 구현해 보려고 한다.


👀 코드로 살펴보기

🌼 VS Code

🌱 boardUpdate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"  %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"  %>

<c:forEach items="${boardTypeList}" var="boardType">
    <c:if test="${boardType.BOARD_CODE == boardCode}" >
        <c:set var="boardName" value="${boardType.BOARD_NAME}"/>
    </c:if>
</c:forEach>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${boardName}</title>

    <link rel="stylesheet" href="/resources/css/board/boardWrite-style.css">
</head>
<body>
    <main>
        <jsp:include page="/WEB-INF/views/common/header.jsp"/>

        <form action="update" method="POST" 
            class="board-write" id="boardUpdateFrm" enctype="multipart/form-data">  
            <%-- enctype="multipart/form-data" : 제출 데이터 인코딩 X 
                    -> 파일 제출 가능
                    -> MultiPartResolver가 문자열, 파일을 구분
                    --> 문자열 -> String, int, DTO, Map (HttpMessageConverter)
                    --> 파일   -> MultiPartFile 객체 -> transferTo() (파일을 서버에 저장)
            --%>
            

            <h1 class="board-name">${boardName}</h1>

            <!-- 제목 -->
            <h1 class="board-title">
                <input type="text" name="boardTitle" placeholder="제목" value="${board.boardTitle}">
            </h1>

            <%-- 
                board.imageList에 존재하는 이미지 객체를 얻어와
                순서(imageOrder) 별로 변수 생성
            --%>

            <c:forEach items="${board.imageList}" var="img">
                <c:choose>
                    <c:when test="${img.imageOrder == 0}">
                        <c:set var="img0" value="${img.imagePath}${img.imageReName}"/>
                    </c:when>

                    <c:when test="${img.imageOrder == 1}">
                        <c:set var="img1" value="${img.imagePath}${img.imageReName}"/>
                    </c:when>

                    <c:when test="${img.imageOrder == 2}">
                        <c:set var="img2" value="${img.imagePath}${img.imageReName}"/>
                    </c:when>

                    <c:when test="${img.imageOrder == 3}">
                        <c:set var="img3" value="${img.imagePath}${img.imageReName}"/>
                    </c:when>

                    <c:when test="${img.imageOrder == 4}">
                        <c:set var="img4" value="${img.imagePath}${img.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" name="images" class="inputImage" id="img0" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>
            </div>

            <!-- 업로드 이미지 영역 -->
            <h5>업로드 이미지</h5>
            <div class="img-box">

                <div class="boardImg">
                    <label for="img1">
                        <img class="preview" src="${img1}">
                    </label>
                    <input type="file" name="images" class="inputImage" id="img1" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

                <div class="boardImg">
                    <label for="img2">
                        <img class="preview" src="${img2}">
                    </label>
                    <input type="file" name="images" class="inputImage" id="img2" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

                <div class="boardImg">
                    <label for="img3">
                        <img class="preview" src="${img3}">
                    </label>
                    <input type="file" name="images" class="inputImage" id="img3" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

                <div class="boardImg">
                    <label for="img4">
                        <img class="preview" src="${img4}">
                    </label>
                    <input type="file" name="images" class="inputImage" id="img4" accept="image/*">
                    <span class="delete-image">&times;</span>
                </div>

            </div>

            <!-- 내용 -->
            <div class="board-content">
                <textarea name="boardContent">${board.boardContent}</textarea>
            </div>


             <!-- 버튼 영역 -->
            <div class="board-btn-area">
                <button type="submit" id="writebtn">등록</button>
            </div>

            <%-- 기존 이미지가 있다가 삭제된 이미지의 순서를 기록 --%>
            <input type="hidden" name="deleteList" value="">

            <%-- 수정 성공 시 주소(쿼리스트링) 유지 용도 --%>
            <input type="hidden" name="cp" value="${param.cp}">
            
        </form>

    </main>

    <jsp:include page="/WEB-INF/views/common/footer.jsp"/>

    <script src="/resources/js/board/boardUpdate.js"></script>

</body>
</html>

🌱 boardUpdate.js

// 미리보기 관련 요소 모두 얻어오기

// img 5개
const preview = document.getElementsByClassName("preview");

// file 5개
const inputImage = document.getElementsByClassName("inputImage");

// x 버튼 5개
const deleteImage = document.getElementsByClassName("delete-image");

// 게시글 수정 시 삭제된 이미지의 순서를 기록할 Set 객체 생성
const deleteSet = new Set(); // 순서 X, 중복 X
// -> x 버튼 클릭 시 순서를 한 번만 저장하는 용도

// -> 위에 얻어온 요소들의 개수가 같음 == 인덱스가 일치함

for(let i = 0; i < inputImage.length; i++){
    
    // 파일이 선택되거나, 선택 후 취소되었을 때
    inputImage[i].addEventListener("change", e=>{
        
        const file = e.target.files[0]; // 선택된 파일의 데이터

        if(file != undefined){ // 파일이 선택되었을 때

            const reader = new FileReader(); // 파일을 읽는 객체

            reader.readAsDataURL(file);
            // 지정된 파일을 읽은 후 result 변수에 URL 형식으로 저장

            reader.onload = e=>{ // 파일을 다 읽은 후 수행
                preview[i].setAttribute("src", e.target.result);

                // 이미지가 성공적으로 읽어지면
                // deleteSet에서 삭제
                deleteSet.delete(i);
            }

        } else { // 선택 후 취소되었을 때
            // -> 선택된 파일 없음 -> 미리보기 삭제
            preview[i].removeAttribute("src");
        }

    });

    // 미리보기 삭제 버튼(x버튼)
    deleteImage[i].addEventListener("click", ()=>{

        // 미리보기 이미지가 있을 경우
        if(preview[i].getAttribute("src") != ""){

            // 미리보기 삭제
            preview[i].removeAttribute("src");

            // input type="file" 태그의 value를 삭제
            // ** input type="file"의 value는 ""(빈칸)만 대입 가능 **
            inputImage[i].value = "";

            // deleteSet에 삭제된 이미지 순서(i) 추가
            deleteSet.add(i);

        }
    })
}

// 게시글 수정 시 제목, 내용 작성 여부 검사
const boardUpdateFrm = document.getElementById("boardUpdateFrm");
const boardTitle = document.querySelector("[name='boardTitle']");
const boardContent = document.querySelector("[name='boardContent']");

boardUpdateFrm.addEventListener("submit", e=>{

    if(boardTitle.value.trim().length == ""){
        alert("제목을 입력해 주세요.");
        boardTitle.value = "";
        boardTitle.focus();
        e.preventDefault(); // form 기본 이벤트 제거
        return;
    }

    if(boardContent.value.trim().length == ""){
        alert("내용을 입력해 주세요.");
        boardContent.value = "";
        boardContent.focus();
        e.preventDefault();  // form 기본 이벤트 제거
        return;
    }

    // input type="hidden" 태그에
    // deleteSet에 저장된 값을 "1,2,3" 형태로 변경해서 저장

    // Array.from(deleteSet) : Set -> Array 변경

    // JS 배열은 String에 대입되거나 출력될 때
    // 요소,요소,요소 형태의 문자열을 반환한다!

    document.querySelector("[name='deleteList']").value
        = Array.from(deleteSet);

    // e.preventDefault(); // 확인만 하고 지울 예정
})

🌼 Spring

🌱 BoardController2.java

...
	// 게시글 수정 화면 전환
	@GetMapping("{boardCode}/{boardNo}/update")
	public String boardUpdate(
			@PathVariable("boardCode") int boardCode
			, @PathVariable("boardNo") int boardNo
			, Model model // 데이터 전달용 객체(기본 scope : request)
			) {
		
		Map<String, Object> map = new HashMap<String, Object>();
		
		map.put("boardCode", boardCode);
		map.put("boardNo", boardNo);
		
		Board board = boardService.selectBoard(map);
		
		model.addAttribute("board", board);

		// forward (요청 위임) -> request scope 유지
		
		return "board/boardUpdate";
	}
	
	@PostMapping("{boardCode}/{boardNo}/update")
	public String boardUpdate(
			Board board // 커맨드 객체(name == 필드 경우 필드에 파라미터 세팅)
			, @RequestParam(value = "cp", required = false, defaultValue = "1") int cp // 쿼리스트링 유지
			, @RequestParam(value = "deleteList", required = false) String deleteList // 삭제할 이미지 순서
			, @RequestParam(value = "images", required = false) List<MultipartFile> images // 업로드된 파일 리스트
			, @PathVariable("boardCode") int boardCode
			, @PathVariable("boardNo") int boardNo
			, HttpSession session // 서버 파일 저장 경로를 얻어올 용도
			, RedirectAttributes ra // 리다이렉트 시 값 전달용
			) throws IllegalStateException, IOException {
		
		// 1) boardCode, boardNo를 커맨드 객체(board)에 세팅
		board.setBoardCode(boardCode);
		board.setBoardNo(boardNo);
		
		// board(boardCode, boardNo, boardTitle, boardContent)
		
		// 2) 이미지 서버 저장 경로, 웹 접근 경로
		String webPath = "/resources/images/board";
		String filePath = session.getServletContext().getRealPath(webPath);
		
		// 3) 게시글 수정 서비스 호출
		int rowCount = service.boardUpdate(board, images, webPath, filePath, deleteList);
		
		// 4) 결과에 따라 message, path 설정
		String message = null;
		String path = "redirect:";
		
		if(rowCount > 0) {
			message = "게시글이 수정되었습니다.";
			path += "/board/" + boardCode + "/" + boardNo + "?cp=" + cp; // 상세 조회 페이지
			
		} else {
			message = "게시글 수정 실패";
			path += "update";
		}
		
		ra.addFlashAttribute("message", message);
		
		return path;
	}
	
}

🌱 BoardService2.java

...
	/** 게시글 수정
	 * @param board
	 * @param images
	 * @param webPath
	 * @param filePath
	 * @param deleteList
	 * @return rowCount
	 */
	int boardUpdate(Board board, List<MultipartFile> images, String webPath, String filePath, String deleteList) throws IllegalStateException, IOException;

🌱 BoardServiceImpl2.java

...
	// 게시글 수정 서비스
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int boardUpdate(Board board, List<MultipartFile> images, String webPath, String filePath,
			String deleteList) throws IllegalStateException, IOException {
		
		// 1. 게시글 제목/내용만 수정
		// 1) XSS 방지 처리
		board.setBoardTitle(Util.XSSHandling(board.getBoardTitle()));
		board.setBoardContent(Util.XSSHandling(board.getBoardContent()));
		
		// 2) DAO 호출
		int rowCount = dao.boardUpdate(board);
		
		// 2. 게시글 부분이 수정 성공했을 때
		if(rowCount > 0) {
			
			if(!deleteList.equals("")) { // 삭제할 이미지가 있다면

				// 3. deleteList에 작성된 이미지 모두 삭제
				Map<String, Object> deleteMap = new HashMap<>();
				deleteMap.put("boardNo", board.getBoardNo());
				deleteMap.put("deleteList", deleteList);
				
				rowCount = dao.imageDelete(deleteMap);
				
				if(rowCount == 0) { // 이미지 삭제 실패 시 전체 롤백
									// -> 예외 강제로 발생
					
					throw new ImageDeleteException();
				}
				
			} 
			
			// 4. 새로 업로드된 이미지 분류 작업
			
			// images : 실제 파일이 담긴 List
			//			-> input type="file" 개수만큼 요소가 존재
			//			-> 제출된 파일이 없어도 MultiPartFile 객체가 존재
			
			List<BoardImage> uploadList = new ArrayList<>();
			
			// images에 담겨 있는 파일 중 실제 업로드된 파일만 분류
			for(int i=0; i<images.size(); i++) {

				// i번째 요소에 업로드한 파일이 있다면
				if(images.get(i).getSize() > 0) {
					
					BoardImage img = new BoardImage();
					
					// img에 파일 정보를 담아서 uploadList에 추가
					img.setImagePath(webPath); // 웹 접근 경로
					img.setBoardNo(board.getBoardNo()); // 게시글 번호
					img.setImageOrder(i); // 이미지 순서
					
					// 파일 원본명
					String fileName = images.get(i).getOriginalFilename();
					
					img.setImageOriginal(fileName); // 원본명
					img.setImageReName( Util.fileRename(fileName) ); // 파일 변경명
					
					uploadList.add(img);
					
					// 오라클은 다중 UPDATE를 지원하지 않기 때문에
					// 하나씩 UPDATE 수행
					
					rowCount = dao.imageUpdate(img);
					
					if(rowCount == 0) {
						// 수정 실패 == DB에 이미지가 없었다.
						// -> 이미지 삽입
						
						rowCount = dao.imageInsert(img);
					}
				}
			}
			
			// 5. uploadList에 있는 이미지들만 서버에 저장
			if(!uploadList.isEmpty()) {
				
				for(int i=0; i<uploadList.size(); i++) {
					
					int index = uploadList.get(i).getImageOrder();
					
					// 파일로 변환
					String rename = uploadList.get(i).getImageReName();

					images.get(index).transferTo( new File(filePath + rename) );
				}
			}
			
		}
		
		return rowCount;
	}
}

🌱 BoardDAO2.java

...
	/** 게시글 수정
	 * @param board
	 * @return rowCount
	 */
	public int boardUpdate(Board board) {
		return sqlSession.update("boardMapper.boardUpdate", board);
	}

	/** 이미지 삭제
	 * @param deleteMap
	 * @return rowCount
	 */
	public int imageDelete(Map<String, Object> deleteMap) {
		return sqlSession.delete("boardMapper.imageDelete", deleteMap);
	}

	/** 이미지 수정
	 * @param img
	 * @return rowCount
	 */
	public int imageUpdate(BoardImage img) {
		return sqlSession.update("boardMapper.imageUpdate", img);
	}

	/** 이미지 삽입(1개)
	 * @param img
	 * @return rowCount
	 */
	public int imageInsert(BoardImage img) {
		return sqlSession.insert("boardMapper.imageInsert", img);
	}

}

🌱 boardMapper.xml

...
	<!-- 게시글 수정 -->
	<update id="boardUpdate">
		UPDATE BOARD SET
		BOARD_TITLE = #{boardTitle},
		BOARD_CONTENT = #{boardContent},
		B_UPDATE_DATE = SYSDATE
		WHERE BOARD_CODE = #{boardCode}
		AND BOARD_NO = #{boardNo}
	</update>
	
	<!-- 이미지 삭제 -->
	<delete id="imageDelete">
		DELETE FROM BOARD_IMG
		WHERE BOARD_NO = #{boardNo}
		AND IMG_ORDER IN ( ${deleteList} )
	</delete>
	
	<!-- 이미지 수정 -->
	<update id="imageUpdate">
		UPDATE BOARD_IMG SET
		IMG_PATH = #{imagePath},
		IMG_ORIGINAL = #{imageOriginal},
		IMG_RENAME = #{imageReName}
		WHERE BOARD_NO = #{boardNo}
		AND IMG_ORDER = #{imageOrder}
	</update>

	<!-- 이미지 삽입(1개) -->
	<insert id="imageInsert">
		INSERT INTO BOARD_IMG
		VALUES(SEQ_IMG_NO.NEXTVAL, #{imagePath}, #{imageReName},
			#{imageOriginal}, #{imageOrder}, #{boardNo}
		)
	</insert>

🌱 loginFilter.java


게시글 작성자가 아닌 유저가 url을 통해 게시글 삽입, 수정, 삭제하는 일을 방지하기 위해 ```loginFilter```에 '/board2/*'를 작성해 주어야 한다!
package edu.kh.project.common.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import edu.kh.project.member.model.dto.Member;

// Filter : 클라이언트의 요청/응답을 걸러내거나 첨가하는 클래스

// 클라이언트 <-> Filter <-> DispatcherServlet

// @WebFilter : 해당 클래스를 필터로 등록하고, 지정된 주소로 요청이 올 때마다 동작

@WebFilter(filterName="loginFilter",
			urlPatterns = {"/myPage/*", "/board2/*"})
public class LoginFilter implements Filter {

	public void init(FilterConfig fConfig) throws ServletException {
		// 서버가 켜질 때나 필터 코드가 변경되었을 때 필터가 생성됨
		// -> 생성 시 초기화 용도로 사용하는 메소드
		System.out.println("--- 로그인 필터 생성 ---");
	}

	public void destroy() {
		// 필터 코드가 변경되었을 때
		// 변경 이전 필터를 파괴하는 메소드
		System.out.println("--- 이전 로그인 필터 파괴 ---");
	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		// 필터링 코드를 작성하는 메소드

		// 1) ServletRequest, ServletResponse 다운캐스팅
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse resp = (HttpServletResponse)response;
		
		// 2) HttpServletRequest를 이용해서 HttpSession 얻어오기
		HttpSession session = req.getSession();
		
		// 3) session에서 "loginMember" key를 가진 속성을 얻어와
		//	  null인 경우 메인 페이지로 redirect 시키기
		
		/*
		Member loginMember = (Member)session.getAttribute("loginMember");
		
		if(loginMember.getAuthority() != 2) { // 관리자가 아니면 메인 페이지로
			resp.sendRedirect("/");
		}
		*/
		
		if(session.getAttribute("loginMember") == null) {
			resp.sendRedirect("/");
			
		} else {
			// 4) 로그인 상태인 경우 다음 필터 또는
			//	  DispatcherServlet으로 전달
			chain.doFilter(request, response);
		}
		
	}

}
profile
풀스택 개발자 기록집 📁

0개의 댓글