Part 6) 파일 업로드 처리 (Ch 26 & Ch 27)
*이 카테고리의 글은 <코드로 배우는 스프링 웹 프로젝트> 에서 각 파트별로 진행하는 프로젝트의 흐름을 보기 위해 각 장의 내용을 간단히 요약한 것이다.
* 출처: 코드로 배우는 스프링 웹 프로젝트 개정판, 구멍가게 코딩단, 남가람북스
Ch26 게시물의 조회와 첨부파일
- 게시물의 조회에서는 첨부파일을 다운로드하거나 원본 이미지의 파일을 볼 수 있는 기능을 사용하게 된다.
- 게시물의 조회에서 고민해야 하는 내용은 첨부파일을 한 번에 볼 것인지, Ajax를 이용해서 별도로 처리할 것인지에 대한 결정이다.
- 게시물의 정보는 tbl_board에 기록되어 있고, 첨부파일의 정보는 tbl_attach에 기록되어 있기 때문에 화면엔서 두 테이블에 있는 정보를 사용하기 위해서는 다음과 같은 방식을 고려할 수 있다.
1) BoardVO 객체를 가져올 때 join을 처리해서 한꺼번에 게시물과 첨부파일의 정보를 같이 처리하는 방식이다. 데이터베이스를 한 번만 호출하게 되므로 효율적이지만 MyBatis 쪽에서 처리해야 하는 일이 많아진다.-> 전통적인 방식으로 쿼리를 이용해서 두 개의 테이블을 join 해서 처리하는 방식이다.-> 쿼리를 한 번만 실행하기 때문에 데이터베이스의 부하를 줄여줄 수 있다.
2) JSP에서 첨부파일의 정보를 Ajax를 이용해서 처리하는 방식이다. 다시 쿼리를 쳐리해야 하는 불편함이 있지만 난이도가 낮고, 화면에서 처리는 JavaScript 처리가 복잡하다.
- 예제는 Ajax를 이용하는 방식을 사용해서 구성한다.
- 가장 큰 이유는 기존에 개발해 둔 코드를 최소한으로 수정해서 사용하는 것이 가능하기 때문이기도 하고, 앞에서 작성한 예제 코드를 어느 정도 사용할 수 있기 때문이다.
- 또한 댓글의 처리 역시 Ajax를 이용했으므로 동작 방식에 일관성을 유지하는 의도도 있다.
* join을 이용해서 하나의 객체를 구성하는 방식은 조금 뒤쪽에서 알아보자.
26.1 BoardService와 BoardController 수정
- 게시물을 조회할 때 첨부파일을 Ajax로 처리하기로 했다면 우선적으로 서버 측에서 JSON 데이터를 만들어서 화면에 올바르게 전송하는 작업을 먼저 처리해야 한다.
- BoardAttachMapper에서는 이미 게시물의 번호를 이용해서 BoardAttachVO 타입으로 반환하는 메서드인 findByBno() 메서드가 완성된 상태이므로, BoardService와 BoardServceImpl 클래스를 수정한다.
public interface BoardService {
...
// 10. 해당 게시물의 첨부파일 목록을 가져온다.
public List<BoardAttachVO> getAttachList(Long bno);
}
// 10. 해당 게시물의 첨부파일 목록을 가져온다.
@Override
public List<BoardAttachVO> getAttachList(Long bno) {
log.info("get AttachList by Bno" + bno);
return attachMapper.findByBno(bno);
}
26.2 BoardController의 변경과 화면 처리
- BoardController는 특정한 게시물 번호를 이용해서 첨부파일과 관련된 데이터를 JSON으로 반환하도록 처리한다.
//6. 해당 게시물의 첨부파일 목록을 가져온다.
@GetMapping(value = "/getAttachList", produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ResponseEntity<List<BoardAttachVO>> getAttachList(Long bno) {
log.info("getAttachList" + bno);
return new ResponseEntity<>(service.getAttachList(bno), HttpStatus.OK);
}
26.2.1 게시물 조회 화면의 처리
- get.jsp 내에서 가장 먼저 해당 게시물의 첨부파일을 가져오는 부분이 자동으로 동작하게 처리하는 작업부터 시작한다.
- get.jsp 내에 <script> 태그를 새로 추가하고, $(document).ready()를 이용해서 첨부파일의 데이터를 가져오는 부분을 즉시 실행 함수를 이용해서 처리한다.
<script type="text/javascript">
$(document).ready(function(){
// 첨부파일 목록을 가져오는 작업.
(function(){
var bno = '<c:out value="${board.bno}"/>';
$.getJSON(
"/sjboard/getAttachList",
{bno: bno},
function(arr){console.log(arr);
});
})();
});
</script>
- 브라우저에서 첨부파일이 추가된 게시물을 선택하면 아래 그림과 같이 콘솔창에 해당 게시물의 첨부파일의 목록을 볼 수 있다.
- 첨부파일 데이터를 가져왔다면 파일들을 보여줄 수 있도록 <div>를 생성한다.
- 기존의 게시물이 보여지는 <div> 아래쪽에 별도의 <div>를 생성해서 처리하고 첨부파일 목록 관련 CSS도 추가한다.
- get.jsp 내에 추가되는 <div>는 크게 첨부파일의 목록을 보여주는 <div class='uploadResult'>와 원본 이미지를 보여주는 <div class='bigPictureWrapper'> 부분이다.
26.2.2 첨부파일 보여주기
- JSON으로 가져온 첨부파일 데이터는 작성된 <div>안에서 보이도록 처리해주어야 한다. 전달된 JSON 데이터는 BoardAttachVO 객체이다.
- 자바스크립트에서는 화면에 보여주는 부분을 작성한다.
- 게시물 등록 페이지인 register.jsp 에서 첨부 파일 목록을 출력하는 자바스크립트 코드와 거의 비슷하지만 게시물 등록과 달리 첨부파일의 삭제 표시 등의 필요 없는 부분은 조금 정리해서 출력하도록 한다.
<script type="text/javascript">
$(document).ready(function(){
// 첨부파일 목록을 가져오는 작업.
(function(){
var bno = '<c:out value="${board.bno}"/>';
$.getJSON(
"/sjboard/getAttachList",
{bno: bno},
function(arr){
console.log(arr);
var str="";
$(arr).each(function(i, attach){
// 이미지 파일인 경우
if(attach.image){
var fileCallPath = encodeURIComponent(attach.uploadPath + "/s_" + attach.uuid + "_" + attach.fileName );
str += "<li data-path='"+ attach.uploadPath + "' data-uuid='" + attach.uuid + "' data-filename='" + attach.fileName+ "' data-type='"+ attach.image+ "'><div>";
str += "<span>" + attach.fileName + "</span><br>";
str += "<img src='/display?fileName=" + fileCallPath + "'>";
str += "</div></li>";
// 일반 파일인 경우
}else {
var fileCallPath = encodeURIComponent(attach.uploadPath + "/" + attach.uuid + "_" + attach.fileName );
var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
str += "<li data-path='"+ attach.uploadPath + "' data-uuid='" + attach.uuid + "' data-filename='" + attach.fileName+ "' data-type='"+ attach.image+ "'><div>";
str += "<span>" + attach.fileName + "</span><br>";
str += "<img src='/resources/img/attach.png'></a>";
str += "</div></li>";
}
});
$(".uploadResult ul").html(str);
})
})();
});
</script>
26.2.3 첨부파일 클릭 시 이벤트 처리
- 첨부파일의 목록이 보인다면 이미지 파일의 경우 화면에서 원본 이미지, 일반 파일의 경우에는 다운로드 처리가 필요하다.
//첨부파일 클릭 시 이벤트 처리
$(".uploadResult").on("click", "li", function(e){
console.log("view image");
var liObj = $(this);
var path = encodeURIComponent(liObj.data("path")+"/"+liObj.data("uuid")+"_"+liObj.data("filename"));
//data-type='true' = 이미지파일 (원본이미지를 보여준다.)
if(liObj.data("type")){
showImage(path.replace(new RegExp(/\\/g),"/"));
}
//data-type='false' = 일반파일 (다운로드한다.)
else{
self.location = "/download?fileName=" + path;
}
});
// 이미지 파일의 원본 이미지를 보여주는 함수
function showImage(fileCallPath){
$(".bigPictureWrapper").css("display","flex").show();
$(".bigPicture")
.html("<img src='/display?fileName="+ fileCallPath+"'>")
.animate({width:'100%', height: '100%'});
}
- 첨부파일과 관련된 정보는 모두 <li> 태그 내에 존재하므로 이를 이용해서 이미지 파일과 일반 파일을 구분해서 처리한다.- 파일 경로의 경우 함수로 전달될 때 문제가 생기므로 replace()를 이용해서 변환한 뒤에 전달한다.
26.2.4 원본 이미지 창 닫기
- 게시물의 조회에서 마지막 처리는 원본 이미지가 보이는 창을 닫는 작업이다.
- 앞의 예제와 동일하게 <div>를 감추는 형태로 작성한다.
//원본 이미지가 띄워진 전체 화면 클릭 시 사라지는 이벤트
$(".bigPictureWrapper").on("click", function(e){
$(".bigPicture").animate({width:'0%', height: '0%'});
setTimeout(function() {
$(".bigPictureWrapper").hide();
});
});
Ch27 게시물의 삭제와 첨부파일
- 게시물을 삭제할 때는 게시물이 포함된 첨부파일 역시 같이 삭제할 필요가 있다. 단순히 데이터베이스 상에서 삭제만 이루어지는 것이 아니라 실제 폴더 내의 파일도 같이 삭제할 필요가 있기 때문에 작업의 순서 역시 신경 써야만 한다.
- 폴더에서의 파일 삭제는 위험한 작업이기 때문에 가능하면 뒤쪽으로 미루고 먼저 데이터베이스의 삭제 작업을 처리한 후 실제 파일을 삭제하도록 한다.
27.1 첨부파일 삭제 처리
- 첨부파일과 관련된 SQL은 BoardAttachMapper 인터페이스와 XML파일에 작성되어 있으므로 이를 먼저 처리한다.
27.1.1 BoardServiceImpl의 변경
- BoardServiceImpl은 첨부파일 삭제와 실제 게시물의 삭제가 같이 처리되도록 트랜잭션 하에서 BoardAttachMapper의 deleteAll()을 호출하도록 수정한다.
27.1.2 BoardController의 파일 삭제
- BoardController는 데이터베이스의 삭제를 먼저 호출하고, 이후 파일을 삭제 해야 한다. 다만 파일을 삭제하기 위해서는 해당 게시물의 첨부파일 목록이 필요하다.
- 문제는 첨부파일의 목록을 구한다고 해도 이미지 파일의 경우에는 섬네일 파일이 생성되어 있으므로 이에 대한 처리가 같이 필요하다는 점이다. 따라서 작업의 순서를 정리하면 다음과 같다.
1) 해당 게시물의 첨부파일 정보를 미리 준비
2) 데이터베이스 상에서 해당 게시물과 첨부파일 데이터 삭제
3) 첨부파일 목록을 이용해서 해당 폴더에서 섬네일 이미지(이미지 파일의 경우)와 일반 파일을 삭제
1) Criteria 수정
- 게시물의 삭제 후에 페이지 번호나 검색 조건을 유지하면서 이동하기 위해서는 'redirect'에 필요한 파라미터들을 매번 추가해야하는 불편함이 있다. 이를 Criteria에서 처리할 수 있도록 아래와 같이 수정한다.
- UriComponentsBuilder는 브라우저에서 GET 방식 등의 파라미터 전송에 사용되는 문자열(쿼리스트링)을 손쉽게 처리할 수 있는 클래스이다.
2) 파일 삭제 처리
- 파일 삭제는 BoarderController에 deleteFiles()라는 메서드를 추가해서 처리하도록 한다. deleteFiles()는 java.nio.file 패키지의 Path를 이용해서 처리한다.
- 실제 첨부파일을 삭제하는 작업은 원본 파일을 삭제한 후 이미지의 경우 섬네일 파일을 추가적으로 삭제한다.
- 특정 게시물을 삭제하는 메서드 remove() 안에 deleteFiles() 메서드를 넣어 게시물 삭제 시 첨부파일 목록도 함께 수정되도록 한다.
- BoardController의 remove()는 삭제 전에 먼저 해당 게시물의 첨부파일 목록을 확보한다. 이후에 데이터베이스에서 게시물과 첨부파일 데이터를 삭제한다. 만익 삭제에 성공했다면 실제 파일의 삭제를 시도한다.
- 삭제 화면은 별다르처리 없이 기존 게시물의 수정/삭제 화면에서 동일하게 테스트할 수 있다.
- 게시물의 수정/삭제 화면에서 게시물을 삭제하면 모든 내용들이 정리된다.