코드 전략
1) 기존에 이미지가 있음
-> BOARD_IMG 테이블 UPDATE
2) 기존에 이미지가 없음
-> BOARD_IMG 테이블 INSERT
3) 기존에 이미지가 있는데 그대로 둠(수정 X)
-> file이 "선택된 파일없음" 제출
-> 기존 이미지가 유지되도록 설정
4) 기존에 이미지가 있는데 X 버튼 눌러서 삭제
-> file이 "선택된 파일없음" 제출
-> DB에서 해당 이미지 삭제(DELETE)
@PostMapping("{boardCode}/{boardNo}/updateView")
public String updateView(
@PathVariable("boardCode") int boardCode,
@PathVariable("boardNo") int boardNo,
@SessionAttribute("loginMember")Member loginMember,
RedirectAttributes ra,
Model model) {
// boardCode, boardNo가 일치하는 글 조회
Map<String, Integer>map =
Map.of("boardCode", boardCode, "boardNo", boardNo);
Board board = boardService.selectDetail(map);
// 게시글이 존재하지 않는 경우
if(board == null) {
ra.addFlashAttribute("message",
"해당 게시글이 존재하지 않습니다.");
return "redirect:/board/" + boardCode; // 게시글 목록
}
// 게시글 작성자가 로그인한 회원이 아닌경우
if(board.getMemberNo() != loginMember.getMemberNo()) {
ra.addFlashAttribute("message",
"글 작성자만 수정 가능합니다");
return String.format("redirect:/board/%d%d",
boardCode, boardNo); // 상세조회
}
// 게시글이 존재하고
// 로그인한 회원이 작성한 글이 맞을경우
// 수정화면으로 forward
model.addAttribute("board", board);
return "board/boardUpdate";
}
boardCode : 게시판 종류
boardNo : 수정할 게시글 번호
loginMember : 로그인한 회원정보(session)
ra : redirect 시 request scope로 데이터 전달
model : forward시 request scope로 데이터 전달
게시글 수정 Controller
/** 게시글 수정
* @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,
@RequestParam(value="deleteOrderList", required = false)String deleteOrderList,
RedirectAttributes ra
) {
// 1. 커맨드객체 inputBoard에 로그인한 회원번호 추가
inputBoard.setMemberNo(loginMember.getMemberNo());
// inputBoard에 세팅된 값
// : boardCode, boardNo, boardTitle, boardContent, memberNo
// 게시글 수정 서비스 호출 후 결과반환
int result = service.boardUpdate(inputBoard, images, deleteOrderList);
String message = null;
if(result > 0) {
message = "게시글이 수정되었습니다";
} else {
message = "수정실패";
}
ra.addFlashAttribute("message", message);
return String.format("redirect:/board/%d/%d",
boardCode, boardNo); // 상세조회
}
게시글 수정 ServiceImpl
// 게시글 수정
@Override
public int boardUpdate(Board inputBoard, List<MultipartFile> images, String deleteOrderList) {
// 1. 게시글 부분(제목/내용) 수정
int result = mapper.boardUpdate(inputBoard);
if(result == 0) return 0; // 수정 실패 시
// 2. 기존에 존재했던 이미지 중
// deleteOrderList에 존재하는 순서의 이미지 DELETE
// deleteOrderList에 작성된 값이 있다면
if(deleteOrderList != null &&
deleteOrderList.equals("") == false) {
result = mapper.deleteImage(deleteOrderList, inputBoard.getBoardNo());
// 삭제된 행이 없을 경우 -> SQL 실패
// -> 예외를 발생시켜 전체 rollback
if(result == 0) {
throw new RuntimeException("이미지 삭제 실패");
// 사용자 정의 예외로 바꾸면 더 좋다!!
}
}
// 3. 업로드된 이미지가 있을 경우
// UPDATE 또는 INSERT + transferTo()
// 실제 업로드된 이미지만 모아두는 리스트 생성
List<BoardImg> uploadList = new ArrayList<>();
for(int i=0 ; i<images.size() ; i++) {
// i번째 요소에 업로든된 파일이 없으면 다음으로~
if(images.get(i).isEmpty()) continue;
// 업로드된 파일이 있으면
String originalName = images.get(i).getOriginalFilename();
String rename = FileUtil.rename(originalName);
// 필요한 모든 값을 저장한 DTO 생성
BoardImg img
= BoardImg.builder()
.imgOriginalName(originalName)
.imgRename(rename)
.imgPath(webPath)
.boardNo(inputBoard.getBoardNo())
.imgOrder(i)
.uploadFile(images.get(i))
.build();
// 1행씩 update 수행
result = mapper.updateImage(img);
// 수정이 실패 == 기존에 이미지가 없었다
// == 새로운 이미지가 새 order번째 자리에 추가되었다
// --> INSERT
if(result == 0) {
result = mapper.insertImage(img);
}
// 수정, 삭제가 모두 실패한 경우 --> 말도 안되는 상황
if(result == 0) {
throw new RuntimeException("이미지 DB 추가 실패");
}
uploadList.add(img); // 업로드된 파일리스트에 img 추가
} // for end
// 새로운 이미지가 없는경우
if(uploadList.isEmpty()) return result;
// 임시 저장된 이미지 파일을 지정된 경로로 이동(transferTo())
try {
for(BoardImg img : uploadList) {
img.getUploadFile()
.transferTo(new File(folderPath + img.getImgRename()));
}
}catch(Exception e) {
e.printStackTrace();
throw new FileUploadFailException();
}
return result;
}
게시글 삭제, 수정 editBoard-mapper.xml
<!--
동적 SQL 중 <foreach>
- Mybatis에서 제공하는 향상된 for문
- 특정 SQL 구문을 반복할 때 사용
- 반복 사이에 구분자(separator)를 추가할 수 있음
[지원하는 속성]
collection : 반복할 객체의 타입 작성(list, set, map...)
item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..)
open : 반복 전에 출력할 sql
close : 반복 종료 후에 출력한 sql
separator : 반복 사이사이 구분자
-->
<!-- 여러 이미지 한 번에 INSERT -->
<insert id="insertUploadList">
INSERT INTO "BOARD_IMG"
<foreach collection="list" item="img"
open="(" close=")" separator=" UNION ALL ">
SELECT
NEXT_IMG_NO(),
#{img.imgPath},
#{img.imgOriginalName},
#{img.imgRename},
#{img.imgOrder},
#{img.boardNo}
FROM DUAL
</foreach>
</insert>
<insert id="boardDelete">
UPDATE "BOARD"
SET
BOARD_DEL_FL = DECODE(BOARD_DEL_FL, 'Y', 'N', 'Y')
WHERE
"BOARD_NO" = #{boardNo}
</insert>
<!-- 게시글부분만 수정(제목/내용) -->
<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>
<!--
[마이바티스 #, $의 의미]
# (PreparedStatement)
- {} 내 값을 SQL에 맞는 리터럴 표기법으로 변경해서 대입
ex) (int) 10 -> 10 (NUMBER(
(String) "apple" -> 'apple' (CHAR)
$ (Statement)
- {} 내 값을 있는 그대로 SQL에 대입 (SQL 코드로 인식)
ex) (int) 10 -> 10
(String) "apple" -> apple
* SQL Injection이 발생할 수 있기 때문에
권장하진 않음
-> 해당 SQL <forEach> 태그로 변경하면 좋음 -->
<!-- 기존에 존재하던 이미지 DB에서 삭제 -->
<delete id="deleteImage">
DELETE FROM "BOARD_IMG"
WHERE BOARD_NO = #{boardNo}
AND IMG_ORDER IN(${orders})
</delete>
<!-- 이미지 1행 수정 -->
<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(SEQ_IMG_NO.NEXTVAL,
#{imgPath},
#{imgOriginalName},
#{imgRename},
#{imgOrder},
#{boardNo} )
</insert>