[33] 파일 업로드 처리 - 첨부파일의 다운로드 혹은 원본 보여주기

 

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);
		Resource resource = new FileSystemResource("c:\\upload\\" + fileName);
		
		log.info("resource :" + resource);
		return null;
	}

 

ResponseEntity<>의 타입은 byte[] 등을 사용할 수 있으나, 이번 예제에서는 org.springframework.core.io.Resource 타입을 이용해서 좀 더 간단히 처리하도록 한다.

 

테스트를 위해서 C:\upload 폴더에 영문 파일을 하나 두고, '/download?fileName=파일이름' 의 형태로 호출해 본다

 

브라우저에는 아무런 반응이 없지만, 서버에는 로그가 기록되는 것을 확인 할 수 있다!

 

서버에서 파일이 정상적으로 인식되었다는 것이 확인되면 ReponseEntity<>를 처리한다. 이때 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("c:\\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'을 이용해서 지정한다. 파일 이름에 대한 문자열 처리는 파일 이름이 한글인 경우 저장할 때 깨지는 문제를 막기 위해서이다. 크롬 브라우저에서 C:\upload 폴더에 있는 파일의 이름과 확장자로 '/download?fileName=xxxx'와 같이 호출하면 브라우저는 자동으로 해당 파일을 다운로드하는 것을 볼 수 있다. IE 계열에서는 파일 다운로드가 호출이 안되는 문제가 발생한다. 이에 대한 처리는 조금 뒤에 살펴보도록 한다. 

 

다운로드가 되는 화면

 

1-1. IE/Edge 브라우저의 문제

 

첨부파일의 다운로드 시 Chrome 브라우저와 달리 IE에서는 한글 이름이 제대로 다운로드 되지 않는다. 이것은 'Content-Disposition'의 값을 처리하는 방식이 IE의 경우 인코딩 방식이 다르기 때문이다.

 

IE를 같이 서비스해야 한다면 HttpServletRequest에 포함된 헤더 정보들을 이용해서 요청이 발생한 브라우저가 IE 계열인지 확인해서 다르게 처리하는 방식으로 처리한다. HTTP 헤더 메시지 중에서 디바이스의 정보를 알 수 있는 헤더는 'User-Agent'을 이용한다. 

 

기존의 downloadFile()은 'User-Agent' 정보를 파라미터로 수집하고 IE에 대한 처리를 추가한다. Edge 브라우저는 IE와 또 다르게 처리되므로 주의한다. 

 

- uploadController.java

@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
	@ResponseBody
	public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName){
		log.info("download file : " + fileName);
		Resource resource = new FileSystemResource("c:\\upload\\" + fileName);
		
		if(resource.exists() == false) {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
		
		String resourceName = resource.getFilename();
		
		HttpHeaders headers = new HttpHeaders();
		
		try {
			String downloadName = null;
			if(userAgent.contains("Trident")) {
				log.info("IE browser");
				
				downloadName = URLEncoder.encode(resourceName, "UTF-8").replaceAll("\\+", " ");
				log.info("Edge name: " + downloadName);
			}else {
				log.info("Chrome browser");
				downloadName = new String(resourceName.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 브라우저 엔진 이름) 인 경우에는 다른 방식으로 처리하도록 한다. 

 

위의 코드가 적용되면 우선은 Chrome에서 한글파일의 다운로드를 먼저 시도한 후에 인터넷 등을 이용해서 URL 주소로 인코딩하는 페이지를 이용해서 파일 이름을 변환해 본다. IE에서 주소창에 한글을 적으면 아래와 같이 에러가 발생한다. 

 

IE에서 테스트를 진행하고 싶다면 'URLEncode website'를 검색해서 인코딩 결과를 알아내고 변환된 문자열로 호출하면 된다! 

 

(여기서 Chrome으로 먼저 한글파일을 확인해보았는데 자꾸 다운로드가 되지않았는데 upload 폴더 바로 아래 이미지파일이 없어서였다 ㅠㅠ 다시 옮겨주니까 정상적으로 한글파일도 다운로드 되었음!)

에러화면...

 

1-2. 업로드된 후 다운로드 처리

 

다운로드 자체에 대한 처리는 완료되었으므로, /uploadAjax 화면에서 업로드된 후 파일 이미지를 클릭한 경우에 다운로드가 될 수 있도록 처리한다. 이미지 파일이 아닌 경우는 아래와 같이 첨부파일 아이콘이 보이게 된다. 

 

수정되어야 하는 부분은 'attach.png' 파일을 클릭하면 다운로드에 필요한 경로와 UUID가 붙은 파일 이름을 이용해서 다운로드가 가능하도록 <a> 태그를 이용해서 '/download?fileName=xxxx' 부분을 추가한다. 

 

- uploadAjax.jsp 일부

 

브라우저에서는 <img> 태그를 클릭하게 되면 자동으로 다운로드가 되는 것을 확인할 수 있다. 

 

 

다운로드가 정상적으로 이루어지는걸 확인했다면 마지막으로 서버에서 파일 이름에 UUID가 붙은 부분을 제거하고 순수하게 다운로드되는 파일의 이름으로 저장될 수 있도록 한다. 

 

 

수정된 부분은 resourceOriginalName을 생성해서 UUID 부분을 잘라낸 상태의 파일이름으로 저장하도록 하는 것이다. 브라우저에서는 순수한 파일 이름으로 다운로드 되는 것을 확인할 수 있다. 

 

2. 원본 이미지 보여주기

 

일반 첨부파일과 달리 섬네일이 보여지는 이미지 파일의 경우 섬네일을 클릭하면 원본 이미지를 볼 수 있게 처리한다. 섬네일의 이미지가 '업로드된 경로 + /s_ + UUID_ + 파일이름' 이었다면, 원본 이미지의 이름은 중간에 '/s_'가 '/' 로 변경되는 점이 다르다. 원본 이미지를 화면에서 보기 위해서는 <div>를 생성하고, 해당 <div>에 이미지 태그를 작성해서 넣어주는 작업과 이를 화면상에서 절대 위치를 이용해서 보여줄 필요가 있다.

 

2-1. 원본 이미지를 보여줄 <div> 처리

 

이미지의 경우 일반 파일과 달리 이미 이미지 파일 데이터는 섬네일과 동일한 방식으로 처리될 수 있기 때문에 사실상 핵심적인 부분은 이미지를 보여주는 <div>를 처리하는 부분이 핵심이다.

 

<div>를 처리하는 부분은 섬네일 파일을 클릭할 때 이루어 지도록 JavaScript 함수를 작성한다.

 

- uploadAjax.jsp

 

showImage() 함수는 jQuery의 $(document).ready()의 바깥쪽에 작성한다. 이렇게 하는 이유는 나중에 <a> 태그에서 직접 showImage()를 호출할 수 있는 방식으로 작성하기 위해서다.

 

섬네일 이미지를 보여주도록 처리하는 JavaScript 코드에서는 섬네일의 클릭 시 showImage()가 호출될 수 있는 코드를 추가한다. 

 

 

이미지 첨부파일의 경우 업로드된 경로와 UUID가 붙은 파일의 이름이 필요하기 때문에 originPath 라는 변수를 통해서 하나의 문자열로 생성한다. 생성된 문자열은 '\' 기호 때문에 일반 문자열과는 다르게 처리되므로, '/'로 변환한 뒤 showImage()에 파라미터로 전달한다.

 

브라우저에서는 파일 업로드 이후에 섬네일을 클릭하면 showImage()가 호출되는 것을 확인할 수 있다. 

 

 

[ CSS와 HTML 처리 ]

 

실제 원본 이미지를 보여주는 영역은 아래와 같이 작성한다.

 

<div calss = 'bigPictureWrapper'>
	<div class = 'bigPicture'>
	</div>
</div>

<style>
.uploadResult{
	width:100%;
	background-color:gray;
}

.uploadResult ul{
	display:flex;
	flex-flow:row;
	justify-content:center;
	align-items:center
}

.uploadResult ul li{
	list-style:none;
	padding:10px;
}

.uploadResult ul li img{
	width:20px;
}

.uploadResult ul li span{
	color:white
}

.bigPictureWrapper {
	position : absolute;
	display : none;
	justify-content : center;
	align-items : center;
	top : 0%;
	width : 100%;
	height : 100%;
	background-color : gray;
	z-index : 100;
	background : rgba(255,255,255,0.5);
}

.bigPicture {
	position : relative;
	display:flex;
	justify-content : center;
	align-items : center;
}

.bigPicture img{
	width : 600px;
}
</style>

 

실제 이미지는 '.bigPicture'안에 <img> 태그를 생성해서 넣게 된다. 이때 CSS의 flex 기능을 이용하면 화면의 정중앙에 배치하는 것은 손쉽게 처리된다. showImage() 함수에서는 약간의 코드를 이용해서 화면에 원본 이미지를 보여줄 수 있도록 수정한다. 

 

(책에서 encodeURI부분에 +가 없어서 에러가 난다 꼭 넣어주도록 하자!)

function showImage(fileCallPath){
	//alert(fileCallPath);
	
	$(".bigPictureWrapper").css("display", "flex").show();
	
	$(".bigPicture")
	.html("<img src = '/display?fileName=" +encodeURI(fileCallPath)+"'>")
	.animate({width:'100%', height:'100%'}, 1000);
}

 

showImage()는 내부적으로 화면 가운데 배치하는 작업 후 <img> 태그를 추가하고, jQuery의 animate()를 이용해서 지정된 시간 동안 화면에서 열리는 효과를 처리한다. 첨부파일의 섬네일을 클릭하면 다음 그림처럼 회색 화면의 배경이 깔리고, 원본 이미지가 출력되는 것을 볼 수 있다. 

 

왕크니까 왕귀엽다ㅜ

 

[ <div> 이벤트 처리 ]

 

원본 이미지가 보여지는 <div>는 전체 화면을 차지하기 때문에 다시 한번 클릭하면 사라지도록 이벤트를 처리한다. 

 

- uploadAjax.jsp의 일부 (document 함수 밖에 놔야함!!)

		
	$(".bigPictureWrapper").on("click", function(e){
		$(".bigPicture").animate({width : '0%', height : '0%'}, 1000);
		setTimeout(() => {
			$(this).hide();
		}, 1000);
	});

 

원본 이미지 혹은 주변 배경을 선택하면 우선은 이미지를 화면 중앙으로 작게 점차 줄여준다. (1초 동안) JQuery의 에니메이션이 끝난 후 이벤트를 감지하는 방식도 있지만, 예제는 1초 후에 자동으로 배경창을 안 보이도록 처리하는 방식을 이용한다.

 

setTimeout()에 적용된 '=> (ES6의 화살표 함수)'는 Chrome에서는 정상 작동하지만, IE 11에서는 제대로 동작하지 않으므로 필요하다면 다음의 코드로 내용을 변경해서 사용한다. 

 

	$(".bigPictureWrapper").on("click", function(e){
		$(".bigPicture").animate({width : '0%', height : '0%'}, 1000);
		setTimeout(function(){
			$(this).hide();
		}, 1000);
	});

 

으.. 여기서 좀 해맸는데 div 코드들이 style css 밑으로 있어야 작동하나보다! 

 

3. 첨부파일 삭제

 

첨부파일 삭제는 생각보다 많은 고민이 필요한 작업이다.. 다음과 같은 문제점들을 고민해야 한다.

  • 이미지 파일의 경우에는 섬네일까지 같이 삭제되어야 하는 점
  • 파일을 삭제한 후에는 브라우저에서도 섬네일이나 파일 아이콘이 삭제되도록 처리하는 점
  • 비정상적으로 브라우저의 종료 시 업로드된 파일의 처리

3-1. 일반 파일과 이미지 파일의 삭제

 

업로드된 첨부파일의 삭제는 Ajax를 이용하거나 <form> 태그를 이용하는 방식 모두를 적용할 수 있다. 이미 업로드된 첨부파일의 삭제는 일반 파일의 경우에는 업로드된 파일만을 삭제하면 되지만, 이미지의 경우에는 생성된 섬네일 파일과 원본 파일을 같이 삭제해야 한다. 

 

서버 측에서 삭제하려는 파일의 확장자를 검사해서 일반 파일인지 이미지 파일인지를 파악하거나 파라미터로 파일의 종류를 파악하고, 이를 이용해서 처리를 다르게 한다. 

 

[ 화면에서 삭제 가능 ]

 

첨부파일이 업로드된 후에 생기는 이미지 파일 옆에 'x' 표시를 추가하도록 아래와 같이 수정한다.

 

- uploadAjax.jsp 일부

function showUploadedFile(uploadResultArr){
		   var str = "";
		   
		   $(uploadResultArr).each(function(i, obj){
		     
			   if(!obj.image){
		       
		       var fileCallPath =  encodeURIComponent( obj.uploadPath+"/"+ obj.uuid +"_"+obj.fileName);
		       
		       var fileLink = fileCallPath.replace(new RegExp(/\\/g), "/");
		       
		       str += "<li><a href='/download?fileName="+fileCallPath+"'><img src='/resources/img/attach.png'>"
		    		   +obj.fileName+"</a>" + "<span data-file=\ '"+ fileCallPath+"\' data-type='file'> x </span>"
		    		   + "<div></li>"

		     }else{
		       
		       var fileCallPath =  encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid +"_"+obj.fileName);
		       var originPath = obj.uploadPath+ "\\"+obj.uuid +"_"+obj.fileName;
		       originPath = originPath.replace(new RegExp(/\\/g),"/");
		       
		       str += "<li><a href=\"javascript:showImage(\'"
		    		   +originPath+"\')\"><img src='/display?fileName="+fileCallPath+"'></a>"
		    				   + "<span data-file=\'" + fileCallPath + "\' data-type='image'> x </span><li>";
		     }
		   });
			
			uploadResult.append(str);
		}

 

변경된 부분은 <span> 태그를 이용해서 섬네일이나 파일 아이콘 옆에 'x' 표시를 추가한 점과 <span> 태그에 'data-file'과 'data-type'속성을 추가한 부분이다. 화면을 보면 디자인은 볼품없지만, 테스트할 수 있도록 만든 'x' 표시가 보인다. ㅋㅋ

 

 

'x' 표시에 대한 이벤트 처리는 아래와 같이 작성한다. 

 

	$(".uploadResult").on("click", "span", function(e){
		var targetFile = $(this).data("file");
		var type = $(this).data("type");
		console.log(targetFile);
		
		$.afax({
			url : '/deleteFile',
			data : {fileName : targetFile, type : type},
			dataType : 'text',
			type : 'POST',
				success : function(result){
					alert(result);
				}
		}); // $ajax
	});

 

첨부파일의 삭제는 <span> 태그를 이용해서 처리하지만, 첨부파일의 업로드 후에 생성되기 때문에 '이벤트 위임'방식으로 처리한다. 이벤트 처리에서는 Ajax를 이용해서 첨부파일의 경로와 이름, 파일의 종류를 전송한다. 

 

[ 서버에서의 첨부파일의 삭제 ]

 

서버 측에서 첨부파일은 전달되는 파라미터의 이름과 종류를 파악해서 처리한다.

 

- uploadController.java

	@PostMapping("/deleteFile")
	@ResponseBody
	public ResponseEntity<String> deleteFile(String fileName, String type) {
		log.info("deleteFile : " + fileName);
		
		File file;
		
		try {
			file = new File("c:\\upload\\" + URLDecoder.decode(fileName, "UTF-8"));
			
			file.delete();
			
			if(type.equals("image")) {
				String largeFileName = file.getAbsolutePath().replace("s_", "");
				log.info("largetFileName : " + largeFileName);
				file = new File(largeFileName);
				file.delete();
			}
		}catch(UnsupportedEncodingException e) {
			e.printStackTrace();
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
		
		return new ResponseEntity<String>("deleted", HttpStatus.OK);
	}

 

deleteFile()은 브라우저에서 전송하는 파일 이름과 종류를 파라미터로 받아서 파일의 종류에 따라 다르게 동작한다. 브라우저에서 전송되는 파일 이름은 '경로 + UUID + _ + 파일이름' 으로 구성되어 있으므로, 일반 파일의 경우에는 파일만을 삭제한다. 

 

이미지의 경우 섬네일이 존재하므로, 파일 이름의 중간에 's_'가 들어가 있다. 일반 이미지 파일의 경우 's_'가 없도록 되어 있으므로, 이 부분을 변경해서 원본 이미지 파일도 같이 삭제하도록 처리한다.

 

3-2. 첨부파일의 삭제 고민

 

첨부파일을 삭제하는 작업의 최대 고민은 사용자가 비정상적으로 브라우저를 종료하고 나가는거다... 서버에는 Ajax를 이용해서 업로드 했기 때문에 이미 저장이 된 상태지만, 사용자가 '작업관리자'나 전원 버튼을 누르는 등의 조치를 해서 브라우저 자체를 종료해 버린다면 이를 감지할 수 있는 적당한 방법은 없다... (브라우저의 창이 닫히는 이벤트는 가능하지만 비정상적인 종료는 문제가 된다)

 

이에 대한 가장 좋은 해결책은 실제 최종적인 결과와 서버에 업로드된 파일의 목록을 비교해서 처리하는 것이다. 보통 이런 작업은 spring-batch나 Quartz 라이브러리를 이용해서 처리한다. 뒤쪽에서 다루겠다!