Java/Spring Framework

[ Spring ] 상세조회 게시글 수정

Mungwang 2023. 8. 29. 09:19

💎 Console 

📢 location.pathname => '/board/1/2001'

📢 location.pathname.replace ("board", "board2") => '/board2/1/2001' 

      - replace : board => board2 로 바꾸는 함수

📢 location.pathname.replace ("board", "board2")  + "/update" => '/board2/1/2001/update'

📢 location.search => '?cp=1'

 

🎈 최종으로 원하는 주소는 '/board2/1/2001/update?cp=1'  이기때문에

 

💎 Jsp

📢 enctype = "multipart/form-data" : 제출 데이터 인코딩 X

🔑 파일 제출은 가능

🔑 MultiPartResolver 가 문자열, 파일을 구분

🔑 문자열 -> String, int, DTO, Map ( HttpMessageConverter )

🔑 파일 -> MultiPartFile 객체 -> transferTo( ) ( 파일을 서버에 저장 )

 
 <form action="update" method="POST"
            class="board-write" id="boardUpdateFrm" enctype="multipart/form-data">

            <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>
 

💎 JS

👍Set 객체를 생성하는 이유 const deleteset = new Set() :  게시글 수정 시 삭제된 이미지의 순서를 기록하기 위해

✌️Set 특징 : 순서가 정해져있지않고 중복을 허용하지않는다 == x 버튼클릭시 순서를 딱 한번만 저장할때 용이함!!

 

😂 input type ="file"의 value 값은 오로지 "" (빈칸) 빈문자열만 대입가능!!

 
 // 미리보기 관련 요소 모두 얻어오기
 
 // img 5개
 const preview = document.getElementsByClassName("preview");

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

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

 
 const deleteSet = new Set();   < 중요!!
 

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

 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", e => {
       
        // 미리보기 이미지가 있을 경우
        if(preview[i].getAttribute("src") != "") {

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

            // 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() == "") {
        alert("제목을 입력해주세요.");
        boardTitle.value = "";
        boardTitle.focus();
        e.preventDefault(); // form 기본 이벤트 제거
        return;
    }

    if(boardContent.value.trim() == "") {
        alert("내용을 입력해주세요.");
        boardContent.value = "";
        boardContent.focus();
        e.preventDefault(); // form 기본 이벤트 제거
        return;
    }
 
    document.querySelector("[name='deleteList']").value = Array.from(deleteSet);
 

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

-> ↑ jsp에있는 <input type ="hidden" name="deleteList" value=""> 의 value값에 리스트형식으로 값대입

-> Array.from( ) : Set -> Array 형식의 변경 () 에는 위에선언해둔 deleteSet 을 넣어준다!

 

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

💎 Spring

- 스프링으로 요청주소를 보내줄때  js에서 페이지 이동을 위한 조치를 먼저해준다.

  현재페이지 주소에서 ↓ 주소로 get방식 요청을 보낸다.

🔈locatio.pathname : URL에서 쿼리스트링전 즉 /board/1/1999 까지 얻어온다. 그후 replace를 이용하여

     board -> board2 로 바꾸고  "/update" + location.search 를 해줘 URL 주소를 완성한다.

🔈locatio.search : URL 에서 쿼리스트링 부문을 얻어온다 ?cp=1 

 

 
 // 게시글 수정 버튼 클릭 시
 const updateBtn = document.getElementById("updateBtn");

 updateBtn.addEventListener("click",()=>{

   location.href = location.pathname.replace("board","board2") + "/update" + location.search

})
 

🔎 BoardController2

📣 Board board = boardService.selectBoard(map)  -> 이 링크를 참고하세요!!

:  BoardController에서 이미 조회한 상세조회를 이용 ->  수정시 기존에 작성된 제목,내용,사진등이 필요하기때문!!

// 게시글 수정 화면 전환
   @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 == 필드 경우 필드에 파라미터 세팅) @ModelAttribute 생략가능 
		  , @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:"; // redirect는 GET방식으로 요청처리
	   
	   if(rowCount>0) {
		   message ="게시글이 수정되었습니다.";
		   path += "/board/" + boardCode + "/" + boardNo + "?cp=" + cp; // 상세조회 페이지		   
		   
	   }else {
		   message = "게시글 수정 실패";
		   path += "update";
	   }
	   
	  
	   ra.addFlashAttribute("message",message);
	   
	   return path;
	   
	   
   }

🔎 BoardService2

🔎 BoardServiceImpl2

📣 XSS 방지처리 Util.XSSHandling  -> 이 링크를 참고하세요!!

📣 이미지 삭제 실패시 전체 롤백을 위한 사영자정의 예외 처리 클래스 -> 이 링크를 참고하세요!!

// 게시글 수정 서비스
	@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<String, Object>();
				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<BoardImage>();
			
			// images에 담겨있는 파일 중 실제 업로드된 파일만 분류
			for (int i = 0; i < images.size(); i++) {

				if (images.get(i).getSize() > 0) {

					BoardImage img = new BoardImage();

					// img에 파일 정보를 담아서 uploadList에 추가
					img.setImagePath(webPath); // 웹 접근 경로
					img.setBoardNo(board.getBoardNo()); // 게시글 번호
					img.setImageOrder(i); // 이미지 순서

					// 파일 원본명0
					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에 있는 이미지들만 서버에 저장(transferTo())
			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

/** 게시글 수정
	 * @param board
	 * @return
	 */
	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
	 */
	public int imageInsert(BoardImage img) {
		
		return sqlSession.insert("boardMapper.imageInsert",img);
	}

🔎 board-mapper

<!-- 게시글 수정 -->
	<update id="boardUpdate">
		UPDATE BOARD SET
		BOARD_TITLE = #{boardTitle},
		BOARD_CONTENT = #{boardContent},
		B_UPDATE_DATE = SYSDATE
		WHERE BOARD_NO = #{boardNo}
		AND BOARD_CODE = #{boardCode}
	
	</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>
	
	<!-- 이미지 삽입-->
	<insert id="imageInsert">	
		INSERT INTO BOARD_IMG
		VALUES(SEQ_IMG_NO.NEXTVAL, #{imagePath}, #{imageReName},
                     #{imageOriginal},#{imageOrder},#{boardNo})
	</insert>