📍 글 개행처리 관련 -> css - white-space: pre-wrap;
const updateBtn = document.getElementById("updateBtn");
if(updateBtn != null){
updateBtn.addEventListener("click", () => {
location.href = location.pathname.replace("board","board2")
+ "/update"
+ location.search; // /board2/1/1513/update?cp=1 // GET 방식
});
}
🔑 게시글 수정 중 이미지 삭제
-> Set 사용(중복X, 순서X)
<%-- 기존 이미지가 있다가 삭제된 이미지의 순서를 기록 --%>
<input type="hidden" name="deleteList" value="">
<%-- 수정 성공 시 주소(쿼리스트링) 유지 용도 --%>
<input type="hidden" name="cp" value="${param.cp}">
📍 Array.from(deleteSet)
Set -> Array 변경
// 미리보기 관련 요소 모두 얻어오기
// 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("#boardUpdateFrm > h1.board-title > input[type=text]"); // 해당 요소 우클릭 후 copy -> copy selector
const boardContent = document.getElementsByName("boardContent")[0];
boardUpdateFrm.addEventListener("submit", e => {
if(boardTitle.value.trim().length == 0){
alert("제목을 입력해주세요");
boardTitle.focus();
boardTitle.value = "";
e.preventDefault();
return;
}
if(boardContent.value.trim().length == 0){
alert("내용을 입력해주세요");
boardContent.focus();
boardContent.value = "";
e.preventDefault();
return;
}
// input type ="hidden" 태그에
// deleteSet에 저장된 값을 "1,2,3" 형태로 변경해서 저장
// JS 배열은 string에 대입되거나 출력될 때
// 요소, 요소, 요소 형태의 문자열을 반환한다.
document.querySelector("[name = 'deleteList']").value = Array.from(deleteSet);
// e.preventDefault();
})
🔑 파일 관련 작업할 때는 예외 throws 주의
// 게시글 수정
@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;
}
/** 게시글 수정
* @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;
🔑 list는 length가 아닌 getSize() 사용
// 게시글 수정
@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("")) { // 미친 String 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++) {
// 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에 이미지가 없었다 == INSERT
rowCount = dao.imageInsert(img); // insert 실패 시에는 예외 처리 안 해주나?
}
}
} // 분류 for문 종료
// 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;
}
/** 게시글 수정
* @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);
}
<!-- 게시글 수정 -->
<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_RENAME = #{imageReName},
IMG_ORIGINAL = #{imageOriginal}
WHERE BOARD_NO = #{boardNo}
AND IMG_ORDER = #{imageOrder}
</update>
<!-- 이미지 삽입(1개) -->
<insert id="imageInsert">
INSERT INTO BOARD_IMG
VALUES(SEQ_BOARD_NO.NEXTVAL,
#{imagePath}, #{imageReName},
#{imageOriginal}, #{imageOrder}, #{boardNo})
</insert>