Search
Duplicate

[스프링 게시판] 게시글 수정 시 첨부파일 수정

@3/22/2023

A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: 오류 발생

상황
글 수정 시 첨부파일을 업데이트 하는 로직에서 발생
@Transactional public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException { Post post = findPostById(postId); if (post.getUser().getId() == user.getId()) { List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles(); List<UploadFile> uploadFiles = fileManager.storeFiles(multipartFiles); post.setUploadFiles(uploadFiles); post.setTitle(postUpdateDTO.getTitle()); post.setContent(postUpdateDTO.getContent()); } else { throw new IllegalStateException("게시글을 수정할 권한이 없습니다."); } }
Java
복사
예상키로는 post의 setUploadFiles를 통해 새로운 컬렉션으로 통으로 갈아 끼운 것이 원인듯함. 따라서 기존 컬렉션을 통해 cascade와 orphan removal을 하려던 차에 주인으로부터 벗어나버리니까 오류가 발생하는 것 같음
아래처럼 바꾸니 해결됨
@Transactional public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException { Post post = findPostById(postId); if (post.getUser().getId() == user.getId()) { List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles(); List<UploadFile> uploadFiles = post.getUploadFiles(); for (UploadFile newUploadFile : fileManager.storeFiles(multipartFiles)) { uploadFiles.add(newUploadFile); } post.setUploadFiles(uploadFiles); post.setTitle(postUpdateDTO.getTitle()); post.setContent(postUpdateDTO.getContent()); } else { throw new IllegalStateException("게시글을 수정할 권한이 없습니다."); } }
Java
복사

연관관계 매핑이 안 되는 문제

위를 해결하니 또 문제가 발생함
영속성 전이만을 통해 파일을 등록했더니 uploadFile에는 post 참조가 걸리지 않아 FK가 빈 채로 들어가는 문제가 발생함
UploadFile에 만들어뒀던 addToPost를 통해 양방향으로 참조를 걸어줌
public void addToPost(Post post) { this.post = post; post.getUploadFiles().add(this); }
Java
복사
해결됨
@Transactional public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException { Post post = findPostById(postId); if (post.getUser().getId() == user.getId()) { List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles(); if (multipartFiles != null) { List<UploadFile> uploadFiles = post.getUploadFiles(); for (UploadFile newUploadFile : fileManager.storeFiles(multipartFiles)) { newUploadFile.addToPost(post); } post.setUploadFiles(uploadFiles); } post.setTitle(postUpdateDTO.getTitle()); post.setContent(postUpdateDTO.getContent()); } else { throw new IllegalStateException("게시글을 수정할 권한이 없습니다."); } }
Java
복사

글 수정 시 기존에 첨부한 파일 표시

<ul class="list-group me-auto col-sm-5 col-md-4 mb-1"> <li th:each="uploadFile : ${uploadFiles}" class="list-group-item d-flex justify-content-between"> <i class="bi bi-file-earmark me-2"></i> <span class="me-auto" th:text="${uploadFile.originalFileName}"></span> <button type="button" class="btn-close" th:id="${uploadFile.id}" onclick="uploadFileDeleteAjax(this)"></button> </li> </ul>
HTML
복사
컨트롤러
@GetMapping("/attachment/{uploadFileId}/delete") public ResponseEntity deleteAttachment( @PathVariable Long uploadFileId, @SessionAttribute User loginUser ) throws IOException { UploadFile uploadFile = uploadFileRepository.findOne(uploadFileId).orElseThrow(() -> { throw new IllegalStateException("권한이 없습니다"); }); Long userId = uploadFile.getPost().getUser().getId(); if (userId == loginUser.getId()) { uploadFileRepository.delete(uploadFileId); fileManager.deleteFile(uploadFile); return ResponseEntity.ok().build(); } else { return ResponseEntity.badRequest().build(); } }
Java
복사
JS
function uploadFileDeleteAjax(button) { let uploadFileId = button.id; let li = button.closest("li"); let request = new XMLHttpRequest(); request.onload = () => { li.remove(); }; request.open("GET", "/files/attachment/" + uploadFileId + "/delete"); request.send(); }
JavaScript
복사

결과 화면

→ 이렇게 하니 문제점이 파일을 삭제하고 취소를 눌러도 파일 삭제된 것은 복구가 안 됨

hidden input으로 삭제 처리

PostController
@PostMapping("/{postId}/edit") public String updatePost( @PathVariable Long postId, @Validated @ModelAttribute PostUpdateDTO postUpdateDTO, BindingResult bindingResult, @SessionAttribute User loginUser ) throws IOException { if (bindingResult.hasErrors()) { return "posts/postUpdateForm"; } postService.updatePost(postId, postUpdateDTO, loginUser); return "redirect:/posts/" + postId; }
Java
복사
PostService
@Transactional public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException { Post post = findPostById(postId); if (post.getUser().getId() != user.getId()) { throw new IllegalStateException("게시글을 수정할 권한이 없습니다."); } // 첨부파일 삭제 if (postUpdateDTO.getDeleteFileIds() != null) { for (Long deleteFileId : postUpdateDTO.getDeleteFileIds()) { UploadFile uploadFile = uploadFileRepository.findOne(deleteFileId).orElseThrow(() -> { throw new IllegalStateException("존재하지 않는 파일입니다."); }); Long userId = uploadFile.getPost().getUser().getId(); if (userId == user.getId()) { uploadFileRepository.delete(deleteFileId); fileManager.deleteFile(uploadFile); } } } // 첨부파일 추가 List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles(); if (multipartFiles != null) { List<UploadFile> uploadFiles = post.getUploadFiles(); for (UploadFile newUploadFile : fileManager.storeFiles(multipartFiles)) { newUploadFile.addToPost(post); } post.setUploadFiles(uploadFiles); } post.setTitle(postUpdateDTO.getTitle()); post.setContent(postUpdateDTO.getContent()); }
Java
복사
postUpdateDTO에 deleteFileIds 필드 추가
@Getter @Setter public class PostUpdateDTO { private Long postId; @NotBlank private String title; @NotBlank private String content; private List<MultipartFile> uploadFiles; private List<Long> deleteFileIds; }
Java
복사
HTML
<ul class="list-group me-auto col-sm-5 col-md-4 mb-1"> <li th:each="uploadFile : ${uploadFiles}" class="list-group-item d-flex justify-content-between"> <i class="bi bi-file-earmark me-2"></i> <span class="me-auto overflow-hidden" th:text="${uploadFile.originalFileName}"></span> <button type="button" class="btn-close" th:id="${uploadFile.id}" onclick="uploadFileDelete(this)"></button> </li> </ul>
HTML
복사
JS
function uploadFileDelete(button) { let uploadFileId = button.id; let form = button.closest("form"); let li = button.closest("li"); let input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("deleteFileIds", uploadFileId); form.appendChild(input); li.remove(); }
JavaScript
복사
핵심은 JS를 통해 hidden input으로 삭제할 파일들의 item을 모으는 것

관련 커밋

9bbba07452a57adf8cbbae286a414ea92a1187d7
commit