Part 24. 첨부파일의 다운로드 혹은 원본 보여주기
- 첨부파일의 업로드가 처리되는 과정도 복잡하지만, 이를 사용자가 사용하는 과정 역시 신경 써야 하는 일이 많다.
- 브라우저에서 보이는 첨부파일은 크게 1) 이미지 종류와 2) 일반 파일로 구분되므로 사용자의 첨부파일과 관련된 행위도 종류에 따라 다르게 처리되어야 한다.
- 만일 첨부파일이 이미지인 경우에는 섬네일 이미지를 클릭했을 때 화면에 크게 원본 파일을 보여주는 형태로 처리되어야 한다.
- 이 경우는 브라우저에서 새로운 < div > 등을 생성해서 처리하는 방식을 이용하는데 흔히 'light-box'라고 한다.
- 'light-box'는 jQuery를 이용하는 많은 플러그인들이 있으므로, 이를 이용하거나 직접 구현할 수 있다.
- 예제는 직접 구현하는 방식으로 한다.
- 첨부파일이 이미지가 아닌 경우에는 기본은 다운로드다.
- 사용자가 파일을 선택하면 다운로드가 실행되면서 해당 파일의 이름으로 다운로드가 가능해야 한다(한글 이름 처리 등이 이슈가 될 수 있다.).
24.1 첨부파일의 다운로드
- 이미지를 처리하기 전에 우선 좀 더 간단한 첨부파일의 다운로드부터 처리하도록 한다.
- 첨부파일의 다운로드는 서버에서 MIME 타입을 다운로드 타입으로 지정하고, 적절한 헤더 메시지를 통해 다운로드 이름을 지정하게 처리한다.
- 이미지와 달리 다운로드는 MIME 타입이 고정되기 때문에 메서드는 아래와 같이 시작하게 된다.
< UploadController >
@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public ResponseEntity<Resource> downloadFile(String fileName) {
log.info("download file: " + fileName);
FileSystemResource resource = new FileSystemResource("D:\\upload\\" + fileName);
log.info("resource: " + resource);
return null;
}
- ResponseEntity<>의 타입은 byte[] 등을 사용할 수 있으나, 이번 예제에서는 org.springframework.core.io.Resource 타입을 이용해 좀 더 간단하게 처리하도록 한다.
- 테스트를 위해 D:₩upload 폴더에 영문 파일을 하나 두고, '/download?fileName=파일이름'의 형태로 호출해 본다.
- 브라우저에는 아무런 반응이 없지만, 서버에는 로그가 기록되는 것을 먼저 확인한다.
브라우저
서버
- 서버에서 파일이 정상적으로 인식되었다는 것이 확인되면 ResponseEntity<>를 처리한다.
- 이때 HttpHeaders 객체를 이용해 다운로드 시 파일의 이름을 처리하도록 한다.
< UploadController >
@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public ResponseEntity<Resource> downloadFile(String fileName) {
log.info("download file: " + fileName);
Resource resource = new FileSystemResource("D:\\upload\\" + fileName);
log.info("resource: " + resource);
String resourceName = resource.getFilename();
HttpHeaders headers = new HttpHeaders();
try {
headers.add("Content-Disposition", "attachment; filename=" + new String(resourceName.getBytes("UTF-8"),"ISO-8859-1"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
}
- MIME 타입은 다운로드를 할 수 있는 'application/octet-stream'으로 지정하고, 다운로드 시 저장되는 이름은 'Content-Disposition'을 이용해 지정한다.
- 파일 이름에 대한 문자열 처리는 파일 이름이 한글인 경우 저장할 때 깨지는 문제를 막기 위해서이다.
- 크롬 브라우저에서 D:₩upload 폴더에 있는 파일의 이름과 확장자로 '/download?fileName=xxxx'와 같이 호출하면 브라우저는 자동으로 해당 파일을 다운로드하는 것을 볼 수 있다.
- IE 계열에서는 파일 다운로드가 호출이 안되는 문제가 발생한다.
- 이에 대한 처리는 조금 뒤에 살펴보도록 한다.
< 자동으로 해당 파일을 다운로드 >
< 한글이름 파일 다운로드 >
24.1.1 IE/Edge 브라우저의 문제
- 첨부파일의 다운로드 시 Chrome 브라우저와 달리 IE에서는 한글 이름이 제대로 다운로드 되지 않는다.
- 이것은 'Content-Disposition'의 값을 처리하는 방식이 IE의 경우 인코딩 방식이 다르기 때문이다.
- IE를 같이 서비스해야 한다면 HttpServletRequest에 포함된 헤더 정보들을 이용해 요청이 발생한 브라우저가 IE 계열인지 확인해서 다르게 처리하는 방식으로 처리한다.
- HTTP 헤더 메시지 중에서 디바이스의 정보를 알 수 있는 헤더는 'User-Agent' 값을 이용한다(이를 이용해 브라우저의 종류나 모바일인지 데스크톱인지 혹은 브라우저 프로그램의 종류를 구분할 수 있다.).
- 기존의 downloadFile()은 'User-Agent' 정보를 파라미터로 수집하고, IE에 대한 처리를 추가한다.
- Edge 브라우저는 IE와 또 다르게 처리되므로 주의한다.
< UploadController >
@GetMapping(value="/download" ,
produces=MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public ResponseEntity<Resource>
downloadFile(@RequestHeader("User-Agent")String userAgent, String fileName){
Resource resource = new FileSystemResource("c:\\upload\\" + fileName);
if(resource.exists() == false) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
String resourceName = resource.getFilename();
//remove UUID
String resourceOriginalName =
resourceName.substring(resourceName.indexOf("_")+1);
HttpHeaders headers = new HttpHeaders();
try {
boolean checkIE = (userAgent.indexOf("MSIE") > -1 ||
userAgent.indexOf("Trident") > -1);
String downloadName = null;
if(checkIE) {
downloadName = URLEncoder.encode(resourceOriginalName,
"UTF8").replaceAll("\\+", " ");
}else {
downloadName = new
String(resourceOriginalName.getBytes("UTF-8"),"ISO-8859-1");
}
headers.add("Content-Disposition", "attachment; filename="+downloadName);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new ResponseEntity<Resource>(resource, headers,HttpStatus.OK);
}
- downloadFile()은 @RequestHeader를 이용해 필요한 HTTP 헤더 메시지의 내용을 수집할 수 있다.
- 이를 이용해 'User-Agent'의 정보를 파악하고, 값이 'MSIE' 혹은 'Trident'(IE 브라우저의 엔진 이름-IE11처리)인 경우에는 다른 방식으로 처리하도록 한다.
- 위의 코드가 적용되면 우선은 Chrome에서 한글 파일의 다운로드를 먼저 시도한 후에 인터넷 등을 이용해 URL 주소로 인코딩하는 페이지를 이용해 파일 이름을 변환해 본다.
- IE에서 주소창에 한글을 적으면 400 에러가 발생한다.
- IE에서 테스트를 진행하고 싶다면 URL Encoding 작업을 해야 하는데 검색을 통해 웹페이지를 쉽게 찾을 수 있다.
- IE의 주소창에서는 한글이 직접 처리되지 않으므로, 변환된 문자열로 호출한다.
- 실행된 결과를 보면 IE와 Chrome 모두 정상적으로 파일 이름이 반영된다.
- IE와 비슷해 보이지만 Edge 브라우저에서는 다음과 같은 방법으로 처리해 주어야 한다.
- userAgent 내에 'Edge'라는 문자열이 있는지 확인한다.
- 다운로드하는 파일 이름에 'ISO-8859-1'인코딩을 적용하지 않는다.
24.1.2 업로드된 후 다운로드 처리
- 다운로드 자체에 대한 처리는 완료되었으므로, /uploadAjax 화면에서 업로드된 후 파일 이미지를 클릭한 경우에 다운로드가 될 수 있도록 처리한다.
- 이미지 파일이 아닌 경우는 아래와 같이 첨부파일 아이콘(이미지)이 보이게 된다.
- 위의 화면이 나오도록 처리되는 JavaScript 부분은 현재 아래와 같이 작성되었다.
< uploadAjax.jsp >
function showUploadedFile(uploadResultArr) {
var str = "";
$(uploadResultArr).each(function(i, obj) {
if(!obj.image) {
str += "<li><img src='/resources/img/attach.png'>" + obj.fileName + "</li>";
} else {
str += "<li>" + obj.fileName + "</li>";
var fileCallPath = encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid+"_"+obj.fileName);
str += "<li><img src='/display?fileName="+fileCallPath+"'><li>";
}
});
uploadResult.append(str);
}
- 수정되어야 하는 부분은 'attach.png' 파일을 클릭하면 다운로드에 필요한 경로와 UUID가 붙은 이름을 이용해 다운로드가 가능하도록 < a > 태그를 이용해 '/download?fileName=xxxx' 부분을 추가한다.
< uploadAjax.jsp >
function showUploadedFile(uploadResultArr) {
var str = "";
$(uploadResultArr).each(function(i, obj) {
if(!obj.image) {
var ileCallPath = encodeURIComponent( obj.uploadPath+"/"+obj.uuid+"_"+obj.fileName);
str += "<li><a herf='/download?fileName="+fileCallPath+"'>"
+ "<li><img src='/resources/img/attach.png'>" + obj.fileName + "</li>";
} else {
var fileCallPath = encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid +"_"+obj.fileName);
str += "<li><img src='/display?filename="+fileCallPath"'><li>";
}
});
uploadResult.append(str);
}
- 브라우저에서는 < img > 태그를 클릭하게 되면 자동으로 다운로드가 되는 것을 확인할 수 있다.
- 다운로드가 정상적으로 이루어지는 것을 확인하였다면 마지막으로 서버에서 파일 이름에 UUID가 붙은 부분을 제거하고 순수하게 다운로드 되는 파일의 이름으로 저장될 수 있도록 한다.
< UploadController >
@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName) {
Resource resource = new FileSystemResource("D:\\upload\\" + fileName);
if (resource.exists() == false) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
String resourceName = resource.getFilename();
// remove UUID
String resourceOriginalName = resourceName.substring(resourceName.indexOf("_") + 1);
HttpHeaders headers = new HttpHeaders();
try {
String downloadName = null;
if ( userAgent.contains("Trident")) {
log.info("IE browser");
downloadName = URLEncoder.encode(resourceOriginalName, "UTF-8").replaceAll("\\+", " ");
}else if(userAgent.contains("Edge")) {
log.info("Edge browser");
downloadName = URLEncoder.encode(resourceOriginalName, "UTF-8");
}else {
log.info("Chrome browser");
downloadName = new String(resourceOriginalName.getBytes("UTF-8"), "ISO-8859-1");
}
log.info("downloadName: " + downloadName);
headers.add("Content-Disposition", "attachment; filename=" + downloadName);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
}
- 수정된 부분은 resourceOriginalName을 생성해서 UUID 부분을 잘라낸 상태의 파일 이름으로 저장하도록 하는 것이다.
- 브라우저에서는 순수한 파일 이름으로 다운로드 되는 것을 확인할 수 있다.