08_Spring_240426(금)_72일차(0) - ★BoardProject★ - 15. 게시판 - 게시글 수정

soowagger·2024년 4월 30일

8_Spring

목록 보기
30/38

15. 게시글 수정

✅ 수정 버튼 클릭 시

boardDetail.js

// ------------- 게시글 수정 버튼 -------------------------

const updateBtn = document.querySelector("#updateBtn");

if(updateBtn != null) { // 수정 버튼 존재 시

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

        // GET 방식
        // 현재 : /board/1/2001?cp=1
        // 목표 : /editBoard/1/2001/update?cp=1
        // location.href = `/editBoard/${boardCode}/${boardNo}/update?cp=${boardCode}`;
        location.href = location.pathname.replace('board', 'editBoard')
                        + "/update"
                        + location.search;
    });

}

📌 상세 조회

✅ 기존에 있던 BoardServiceImpl 쪽에 있는 selectOne 메서드 사용

EditBoardController

private final BoardService boardService; // 필드 추가



/** 게시글 수정 화면 전환
 * @param boardCode : 게시판 종류
 * @param boardNo : 게시글 번호
 * @param loginMember : 로그인한 회원이 작성한 글이 맞는지 검사하는 용도
 * @param model : 포워드 시 request scope로 값 전달하는 용도 
 * @param ra : 리다이렉트 시 request scope로 값 전달하는 용도
 * @return
 */
@GetMapping("{boardCode:[0-9]+}/{boardNo:[0-9]+}/update") // 쿼리스트링 앞 주소까지만, 쿼리스트링은 파라미터 쪽에서 얻어오는 것 
public String boardUpdate(
				@PathVariable("boardCode") int boardCode,
				@PathVariable("boardNo") int boardNo,
				@SessionAttribute("loginMember") Member loginMember,
				Model model, 
				RedirectAttributes ra
				) {
	
	// 수정 화면에 출력할 기존의 제목/내용/이미지 조회
	// -> 게시글 상세 조회
	Map<String, Integer> map = new HashMap<>();
	map.put("boardCode", boardCode);
	map.put("boardNo", boardNo);
	
	// BoardService.selectOne(map) 호출
	Board board = boardService.selectOne(map);

	String message = null;
	String path = null;
	 
	if(board == null) {
		message = "해당 게시글이 존재하지 않습니다.";
		path = "redirect:/"; // 메인 페이지
		
		ra.addFlashAttribute("message", message);
		
	} else if(board.getMemberNo() != loginMember.getMemberNo()) {
		message = "본인이 작성한 글만 수정할 수 있습니다.";
		
		// 해당 글 상세조회 리다이렉트
		path = String.format("redirect:/board/%d/%d", boardCode, boardNo);
		
		ra.addFlashAttribute("message", message);
		
	} else {
		
		path = "board/boardUpdate"; // templates/board/boardUpdate.html로 forward
		model.addAttribute("board", board);
	}
	
	return path;
	
}

✋ 작성하지 않은 회원으로 localhost/editBoard/1/2007/update?cp=1 접근 시

📌 boardUpdate.html 하단에 구문 추가

<!-- 이미지 미리보기 설정 -->
<script th:inline="javascript">
    const imageList = /*[[${board.imageList}]]*/ [];
    const previewList = document.querySelectorAll('img.preview');

    const orderList = []; // 기존에 존재하던 이미지 순서를 기록

    for(let img of imageList) {
        previewList[img.imgOrder].src = img.imgPath + img.imgRename;
        
        orderList.push(img.imgOrder);
        // 다섯개 이미지가 전부다 있었다.
        // -> [0,1,2,3,4]
    }
</script>

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

boardUpdate.js


/* 선택된 이미지 미리보기 */
// const previewList = document.getElementsByClassName("preview"); // img 태그 5개
const inputImageList = document.getElementsByClassName("inputImage"); // input 태그 5개
const deleteImageList = document.getElementsByClassName("delete-image"); // x버튼 5개

// x버튼이 눌러져 삭제된 이미지의 순서를 저장
// * Set : 중복 저장 X, 순서 유지 X
const deleteOrder = new Set();


// 이미지 선택 이후 취소를 누를 경우를 대비한 백업 이미지
// (백업 원리 -> 복제품으로 기존 요소를 대체함)
const backupInputList = new Array(inputImageList.length);


/* ***** input 태그 값 변경 시(파일 선택 시) 실행할 함수 ***** */
/**
 * @param inputImage : 파일이 선택된 input 태그
 * @param order : 이미지 순서
 */
const changeImageFn = (inputImage, order) => {

  // byte단위로 10MB 지정
  const maxSzie = 1024 * 1024 * 10;

  // 업로드된 파일 정보가 담긴 객체를 얻어와 변수에 저장
  const file = inputImage.files[0];


  // ------------- 파일 선택 -> 취소 해서 파일이 없는 경우 ----------------
  if(file == undefined){
    console.log("파일 선택 취소됨");

    // 같은 순서(order)번째 backupInputList 요소를 얻어와 대체하기

    /* 한 번 화면에 추가된 요소는 재사용(다른 곳에 또 추가) 불가능 */

    // 백업본을 한 번 더 복제
    const temp = backupInputList[order].cloneNode(true);

    inputImage.after(temp); // 백업본을 다음 요소로 추가
    inputImage.remove();    // 원본을 삭제
    inputImage = temp;      // 원본 변수에 백업본을 참조할 수 있게 대입

    // 백업본에 없는 이벤트 리스너를 다시 추가
    inputImage.addEventListener("change", e => {
      changeImageFn(e.target, order);
    })

    return;
  }


  // ---------- 선택된 파일의 크기가 최대 크기(maxSize) 초과 ---------

  if(file.size > maxSzie){
    alert("10MB 이하의 이미지를 선택해주세요");

    // 해당 순서의 backup 요소가 없거나, 
    // 요소는 있는데 값이 없는 경우 == 아무 파일도 선택된적 없을 때
    if(backupInputList[order] == undefined
        || backupInputList[order].value == ''){

      inputImage.value = ""; // 잘못 업로드된 파일 값 삭제
      return;
    }

    // 이전에 정상 선택 -> 다음 선택에서 이미지 크기 초과한 경우
    // 백업본을 한 번 더 복제
    const temp = backupInputList[order].cloneNode(true);

    inputImage.after(temp); // 백업본을 다음 요소로 추가
    inputImage.remove();    // 원본을 삭제
    inputImage = temp;      // 원본 변수에 백업본을 참조할 수 있게 대입
 
    // 백업본에 없는 이벤트 리스너를 다시 추가
    inputImage.addEventListener("change", e => {
     changeImageFn(e.target, order);
    })

    return;
  }


  // ------------ 선택된 이미지 미리보기 --------------

  const reader = new FileReader(); // JS에서 파일을 읽고 저장하는 객체

  // 선택된 파일을 JS로 읽어오기 -> reader.result 변수에 저장됨
  reader.readAsDataURL(file);

  reader.addEventListener("load", e => {
    const url = e.target.result;

    // img 태그(.preview)에 src 속성으로 url 값을 대입
    previewList[order].src = url;

    // 같은 순서 backupInputList에 input태그를 복제해서 대입
    backupInputList[order] = inputImage.cloneNode(true);

    // 이미지가 성공적으로 읽어진 경우
    // deleteOrder에서 해당 순서를 삭제
    deleteOrder.delete(order);
  });
}


for(let i=0 ; i<inputImageList.length ; i++){

  // **** input태그에 이미지가 선택된 경우(값이 변경된 경우) ****
  inputImageList[i].addEventListener("change", e => {
    changeImageFn(e.target, i);
  })


  // **** x 버튼 클릭 시 ****
  deleteImageList[i].addEventListener("click", () => {

    // img, input, backup의 인덱스가 모두 일치한다는 특징을 이용

    // 삭제된 이미지 순서를 deleteOrder에 기록

    // 미리보기 이미지가 있을 때에만
    if(previewList[i].getAttribute("src") != null 
        &&  previewList[i].getAttribute("src") != ""  ){

      // 기존에 이미지가 존재하고 있을 경우에만
      if( orderList.includes(i) ){
        deleteOrder.add(i);
      }
    }

    previewList[i].src       = ""; // 미리보기 이미지 제거
    inputImageList[i].value  = ""; // input에 선택된 파일 제거
    backupInputList[i]       = undefined; // 백업본 제거

  });
}


// -------------------------------------------

// 제출 시 유효성 검사
const boardUpdateFrm = document.querySelector("#boardUpdateFrm");

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

  const boardTitle = document.querySelector("[name='boardTitle']");
  const boardContent = document.querySelector("[name='boardContent']");

  if(boardTitle.value.trim().length == 0){
    alert("제목을 작성해 주세요");
    boardTitle.focus();
    e.preventDefault();
    return;
  }

  if(boardContent.value.trim().length == 0){
    alert("내용을 작성해 주세요");
    boardContent.focus();
    e.preventDefault();
    return;
  }

  // input 태그에 삭제할 이미지 순서(Set)를 배열로 만든 후 대입
  // -> value(문자열) 저장 시 배열은 toString()호출되서 양쪽 []가 사라짐
  document.querySelector("[name='deleteOrder']").value
    = Array.from( deleteOrder );

	console.log(document.querySelector("[name='deleteOrder']"));
	// deleteOrder에 {2, 3} 이 있다면
	// <input type="hidden" name="deleteOrder" value="2,3">
  
  // 현재 페이지에서 얻어온 querystring을 input 태그 hidden 타입에 value 값으로 대입하기
  document.querySelector("[name='querystring']").value = location.search;
});

15-1) 게시글 수정 작업 진행

EditBoardController

/** 게시글 수정
 * @param boardCode : 게시판 종류
 * @param boardNo : 수정할 게시글 번호
 * @param inputBoard : 커맨드 객체(제목, 내용)
 * @param loginMember : 로그인한 회우너 번호 이용(로그인 == 작성자)
 * @param images : 제출된 input type="file" 모든 요소
 * @param ra : redirect 시 request scope 값 전달
 * @param deleteOrder : 삭제된 이미지 순서가 기록된 문자열(1,2,3)
 * @param querystring : 수정 성공 시 이전 파라미터 유지 (cp)
 * @return
 */
@PostMapping("{boardCode:[0-9]+}/{boardNo:[0-9]+}/update")
public String boardUpdate(
			@PathVariable("boardCode") int boardCode,
			@PathVariable("boardNo") int boardNo,
			@ModelAttribute Board inputBoard,
			@SessionAttribute("loginMember") Member loginMember,
			@RequestParam("images") List<MultipartFile> images,
			RedirectAttributes ra,
			@RequestParam(value="deleteOrder", required = false) String deleteOrder,
			@RequestParam(value="querystring", required = false, defaultValue = "") String querystring
			
			) throws IllegalStateException, IOException {
	
	// 1. 커맨드 객체(inputBoard)에 boardCode, boardNo, memberNo 세팅
	inputBoard.setBoardCode(boardCode);
	inputBoard.setBoardNo(boardNo);
	inputBoard.setMemberNo(loginMember.getMemberNo());
	// -> inputBoard (제목, 내용, boardCode, boardNo, memberNo)
	
	// 2. 게시글 수정 서비스 호출 후 결과 반환 받기
	int result = service.boardUpdate(inputBoard, images, deleteOrder);
	
	
	// 3. 서비스 결과에 따라 응답 제어
	String message = null;
	String path = null;
	
	if(result > 0) {
		message = "게시글이 수정되었습니다.";
		path = String.format("/board/%d/%d%s", boardCode, boardNo, querystring); // /board/1/2000?cp=3
	} else {
		message = "수정 실패..";
		path = "update"; // 수정 화면 전환 리다이렉트하는 상대 경로
		
	}
	
	ra.addFlashAttribute("message", message);
	
	return "redirect:" + path;
}

EditBoardServiceImpl

// * 게시글 수정
@Override
public int boardUpdate(Board inputBoard, List<MultipartFile> images, String deleteOrder) throws IllegalStateException, IOException {
	
	// 1. 게시글 (제목/내용) 부분 수정
	int result = mapper.boardUpdate(inputBoard);
	
	// 수정 실패 시 바로 리턴
	if(result == 0) return 0; 
	
	
	// ----------------------------------------------
	
	// 2. 기존 0 -> 삭제된 이미지(deleteOrder)가 있는 경우
	if(deleteOrder != null && !deleteOrder.equals("")) {
		
		Map<String, Object> map = new HashMap<>();
		map.put("deleteOrder", deleteOrder);
		map.put("boardNo", inputBoard.getBoardNo());
		
		result = mapper.deleteImage(map);
		
		// 삭제 실패한 경우(부분 실패 포함) -> 롤백
		if(result == 0) {
			throw new ImageDeleteException();
		}
		
	}
	
	// 3. 선택한 파일이 존재할 경우
	List<BoardImg> uploadList = new ArrayList<>();
	
	// images 리스트에서 하나씩 꺼내어 선택된 파일이 있는지 검사
	for(int i=0; i < images.size(); i++) {
		
		// 실제 선택된 파일이 존재하는 경우
		if( !images.get(i).isEmpty() ) {
			
			// 원본명
			String originalName = images.get(i).getOriginalFilename();
			
			// 변경명
			String rename = Utility.fileRename(originalName);
			
			// IMG_ORDER == i (인덱스 == 순서)
			
			// 모든 값을 저장할 DTO 생성 (BoardImg - Builder 패턴 사용)
			BoardImg img = BoardImg.builder()
							.imgOriginalName(originalName)
							.imgRename(rename)
							.imgPath(webPath)
							.boardNo(inputBoard.getBoardNo())
							.imgOrder(i)
							.uploadFile(images.get(i))
							.build();
			
			uploadList.add(img);
			
			// 4. 업로드하려는 이미지 정보(img)를 이용해서
			//    수정 또는 삽입 수행
			
			// 1) 기존 0 -> 새 이미지로 변경 -> 수정
			result = mapper.updateImage(img);
			
			if(result == 0) {
				// 수정 실패 == 기존 해당 순서(IMG_ORDER)에 이미지가 없었음
				// -> 삽입 수행
				
				// 2) 기존 X -> 새 이미지 추가
				result = mapper.insertImage(img);
				
				
				
			} 
		}
		
		// 수정 또는 삭제가 실패한 경우
		if(result == 0) {
			throw new ImageUpdateException(); // 예외 발생 -> 롤백
			
		}
	
		
	}
	
	if(uploadList.isEmpty()) {
		return result;
	}
	
	
	// 수정, 새 이미지 파일을 서버에 저장
	for(BoardImg img : uploadList) {
		img.getUploadFile().transferTo(new File(folderPath + img.getImgRename()));
	}
	
	return result;
}

✅ 서비스 Throw new 클래스 이미지

edit-board-mapper.xml

<!-- 게시글 부분 수정 (제목 내용) -->
<update id="boardUpdate">
	UPDATE "BOARD" SET
	BOARD_TITLE = #{boardTitle},
	BOARD_CONTENT = #{boardContent}
	WHERE BOARD_CODE = #{boardCode}
	AND BOARD_NO = #{boardNo}
	AND MEMBER_NO = #{memberNo}
</update>

<!-- 
 	#{} : 해당 컬럼 자료형에 맞는 리터럴로 변환
 	#{} : SQL에 값 그대로 추가 ('' 존재 X)
 -->

<!-- 게시글 이미지 삭제  -->
<delete id="deleteImage">
	DELETE FROM "BOARD_IMG"
	WHERE IMG_ORDER IN (${deleteOrder})
	AND BOARD_NO = #{boardNo}
</delete>



<!-- 게시글 이미지 수정 -->
<update id="updateImage">
	UPDATE "BOARD_IMG" SET
	IMG_ORIGINAL_NAME = #{imgOriginalName}, 
	IMG_RENAME = #{imgRename}
	WHERE BOARD_NO = #{boardNo}
	AND IMG_ORDER = #{imgOrder}
</update>



<!-- 게시글 이미지 삽입(1행)  -->
<insert id="insertImage">
	INSERT INTO "BOARD_IMG"
	VALUES(NEXT_IMG_NO(), 
			#{imgPath},
			#{imgOriginalName},
			#{imgRename},
			#{imgOrder},
			#{boardNo})
</insert>

✅ 수정 결과 Test

profile

0개의 댓글