개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 24강. 첨부파일의 다운로드 혹은 원본 보여주기

ee2ee2 2022. 9. 29. 17:30
728x90
반응형

해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


한동안 회사 프로젝트로 바빠 블로그 작성에 신경을 못썼다.. 마지막 글이 3달 전이라니.. 이제 다시 개인 공부도 열심히 해야지....! 


첨부파일은 크게 2가지로 분류된다.

1) 이미지 종류 (이 경우는 섬네일 이미지를 클릭하면 원본 파일을 보여주는 형태로 처리)

2) 일반 파일 (이 경우는 해당 파일의 이름으로 다운로드)

위 두가지의 다운로드를 구현해본다.

 

24.1 첨부파일의 다운로드

먼저, 첨부파일의 다운로드부터 구현해볼 것이다. 첨부파일의 다운로드는 서버에서 MIME 타입을 다운로드 타입으로 지정하고, 적절한 헤더 메시지를 통해서 다운로드 이름을 지정하게 처리할 것이다.

UploadController.java 일부

	@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;
	}

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

실행 결과

http://localhost:8080/download?fileName=aaa.txt

이와 같이 서버에서 파일이 정상적으로 인식된 것이 확인되면 ResponseEntity<>를 처리한다. HttpHeaders 객체를 이용해서 다운로드 시 파일의 이름을 처리하도록 한다.

UploadController.java 일부

	@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);
		
		//HttpHeaders 객체를 이용해서 다운로드 시 파일의 이름을 처리하도록 함
		String resourceName = resource.getFilename();
		
		HttpHeaders headers = new HttpHeaders();
		
		try {
			//Content-Disposition을 attachment로 주는 경우, 뒤에 오는 fileName과 함께 Body에 오는 값을 다운로드 받으라는 의미 
			headers.add("Content-Disposition","attachment; fileName=" + new String(resourceName.getBytes("UTF-8"), "ISO-8859-1"));
		} catch (UnsupportedEncodingException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}

 

실행 결과

파일이 다운로드 됨을 확인 할 수 있다.


24.1.1 IE/Edge 브라우저의 문제 <참고>

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);
		
		log.info("resource : " + resource);
		
		//다운로드 받을 파일이 없을 때
		if(resource.exists() == false) {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
		
		//HttpHeaders 객체를 이용해서 다운로드 시 파일의 이름을 처리하도록 함
		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("\\+", " ");
			} else if(userAgent.contains("Edge")) {
				log.info("Edge browser");
				downloadName = URLEncoder.encode(resourceName, "UTF-8");
				
				log.info("Edge name : " + downloadName);
			} else {
				log.info("Chrome browser");
				downloadName = new String(resourceName.getBytes("UTF-8"), "ISO-8859-1");
			}
			
			//Content-Disposition을 attachment로 주는 경우, 뒤에 오는 fileName과 함께 Body에 오는 값을 다운로드 받으라는 의미 
			headers.add("Content-Disposition","attachment; fileName=" + downloadName);
		} catch (UnsupportedEncodingException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}

24.1.2 업로드된 후 다운로드 처리

이제 다운로드 처리는 되었으니, /uploadAjax 화면에서 업로드된 후 파일 이미지를 클릭한 경우에 다운로드 될 수 있도록 구현해본다. 이미지 파일이 아닌 경우에는 아래와 같이 첨부파일 아이콘이 보이도록 한다.

이미지 파일이 아닌 경우에 첨부파일 아이콘을 띄우도록 함.

UploadAjax.jsp 일부 

		function showUploadedFile(uploadResultArr) {	//JSON 데이터를 받아서 해당 파일의 이름을 추가
			var str = "";
			
			$(uploadResultArr).each(function(i, obj) {
				
				if(!obj.image) {	//이미지가 아니면,
					// 파일을 클릭하면 다운로드에 필요한 경로와 UUID가 붙은 파일 이름을 이용해서 다운로드가 가능하도록 처리하는 부분
					var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName);
					
					console.log(fileCallPath);
					
					//섬네일 이미지
					str += "<li><a href='/download?fileName=" + fileCallPath + "'>" + "<img src='/resources/img/attach.png'>" + obj.fileName + "</a></li>";				
				} else {
					//섬네일 이미지
					var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName);					
					
					console.log("fileCallPath : " + fileCallPath);
					
					str += "<li><a href=\"javascript:showImage(\'"+originPath+"\')\"> <img src='/display?fileName=" + fileCallPath + "'></a></li>";
				}
			});
			uploadResult.append(str);
		}

 

 

실행 결과

 aaa.txt 클릭시 다운로드 진행됨 (단, 파일명이 uuid가 붙어 알아보기 어려움)

다운로드 내역

 

다운로드시, UUID가 붙은 부분을 제거하고 순수하게 파일의 이름으로 저장될 수 있도록 조치해보자.

 

UploadController.java 일부

public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName) 메소드 내용 중 일부 변경

		//HttpHeaders 객체를 이용해서 다운로드 시 파일의 이름을 처리하도록 함
		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");
				
				log.info("Edge name : " + downloadName);
			} else {
				log.info("Chrome browser");
				downloadName = new String(resourceOriginalName.getBytes("UTF-8"), "ISO-8859-1");
			}

실행 결과

파일 명이 정상적으로 다운 받아지는 것을 확인


24.2 원본 이미지 보여주기

일반 첨부파일과 달리 섬네일이 보여지는 이미지 파일의 경우, 섬네일을 클릭하면 원본 이미지를 볼 수 있게 처리해본다.

섬네일의 이미지'업로드된 경로' + /s_ + UUID_ + 파일이름' 이었다면, 원본 이미지의 이름은 중간에 '/s_'가 '/'로 변경되는 점이 다르다는 것을 알아두자!

또한, 원본 이미지를 화면에서 보기 위해서는 <div>를 생성하고, 해당 <div>에 이미지 태그를 작성해서 넣어주는 작업과 이를 화면상에서 고정 위치를 이용해 보여줄 필요가 있다.

UploadAjax.jsp 일부

showUploadedFile 메소드 일부 수정

    //$(document).ready()의 바깥쪽에 작성함. 추후, <a> 태그에서 직접 showImage()를 호출할 수 있는 방식으로 작성하기 위함.
	function showImage(fileCallPath) { //섬네일을 클릭하였을 때 원본 이미지 보여주는 메소드
		alert(fileCallPath);
	}

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

		function showUploadedFile(uploadResultArr) {	//JSON 데이터를 받아서 해당 파일의 이름을 추가
			var str = "";
			
			$(uploadResultArr).each(function(i, obj) {
				if(!obj.image) {	//이미지가 아니면,
					// 파일을 클릭하면 다운로드에 필요한 경로와 UUID가 붙은 파일 이름을 이용해서 다운로드가 가능하도록 처리하는 부분
					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></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></li>";
				}
			});
			
			uploadResult.append(str);
		}

실행 결과

브라우저에서 파일 업로드 이후에 섬네일 클릭시, 섬네일 명이 경고창에 출력되는 것을 확인

반응형

CSS와 HTML 처리

실제 이미지는 '.bicPicture'안에 <img> 태그를 생성해서 넣게된다. 이 때, CSS의 flex 기능을 이용하면 화면에 정중앙에 배치할 수 있다.

UploadAjax.jsp 일부

<body>

	<div class='bigPictureWrapper'>
		<div class='bigPicture'>
		
		</div>
	</div>
	
	<style> 
		<%@ include file="/WEB-INF/views/includes/uploadAjax.css" %>
	</style>

UploadAjax.css 

	.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;
		align-content: center;
		text-align: center;
	}
	
	.uploadResult ul li img{
		width: 100px;
	}
	
	.uploadResult ul li span{
		color: white;
	}
	
	.bigPictureWrapper {
		position: absolute;
		display: none;
		justify-content: center;
		align-items: center;
		top: 0%;
		width: 100%;
		height: 100%;
		backgroud-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;
	}

 

실행 결과

UploadAjax.jsp 일부

화면에 원본 이미지 출력하기

	//$(document).ready()의 바깥쪽에 작성함. 추후, <a> 태그에서 직접 showImage()를 호출할 수 있는 방식으로 작성하기 위함.
	function showImage(fileCallPath) { //섬네일을 클릭하였을 때 원본 이미지 보여주는 메소드
		//alert(fileCallPath);
	
		$(".bigPictureWrapper").css("display","flex").show();
		
		//내부적으로 화면 가운데 배치하는 작업 후 <img> 태그를 추가하고, JQuery의 animate()를 이룔해서 지정된 시간동안 화면에서 열리는 효과를 처리함. 
		$(".bigPicture")
		.html("<img src='display?fileName=" + encodeURI(fileCallPath) + "'>")
		.animate({width: '0%', height: '0%'}, 1000);
		
		//이미지를 다시 한 번 클릭하면 사라지도록 설정
 		$(".bigPictureWrapper").on("click", function(e) {
			$(".bigPicture").animate({width: '0%', height: '0%'}, 1000);
			setTimeout(() => {
				$(this).hide();
			}, 1000);
		}); 
	}

실행 결과


24.3 첨부파일의 삭제

첨부파일의 삭제에서는 아래와 같은 사항을 염두해 구현해야한다.

1) 이미지 파일의 경우에는 섬네일까지 같이 삭제되어야 하는 점

2) 파일을 삭제한 후에는 브라우저에서도 섬네일이나 파일 아이콘이 삭제되도록 처리하는 점

3) 비정상적으로 브라우저의 종료 시 업로드된 파일의 처리

 

24.3.1 일반 파일과 이미지 파일의 삭제

일반 파일, 이미지 파일 여부에 따라 삭제 방법이 다르다. 이에 따라, 서버 측에서는 파일의 확장자를 검사해서 일반 파일인지 이미지 파일인지를 파악하거나 파라미터로 파일의 종류를 파악하여 삭제 처리를 다르게할 필요가 있다.

UploadAjax.jsp의 showUploadFile()  - 화면에서 삭제 기능

첨부파일이 업로드 된 후에 생기는 이미지 파일 옆에 'x' 표시를 추가하도록 해보자.

		function showUploadedFile(uploadResultArr) {	//JSON 데이터를 받아서 해당 파일의 이름을 추ㅏ=가
			var str = "";
			
			$(uploadResultArr).each(function(i, obj) {
				
				if(!obj.image) {//이미지가 아니면,
					
					// 파일을 클릭하면 다운로드에 필요한 경로와 UUID가 붙은 파일 이름을 이용해서 다운로드가 가능하도록 처리하는 부분
					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);
		}

실행 결과

x 표시가 생김을 확인할 수 있다.

UploadAjax.jsp 일부

: 'x' 클릭시 파일 삭제하는 기능 추가

	// 파일 삭제 'x'표시에 대한 이벤트 처리
	$(".uploadResult").on("click", "span", function(e) {
		
		var targetFile = $(this).data("file");
		var type = $(this).data("type");
		
		console.log(targetFile);
		
		$.ajax({
			url: '/deleteFile',
			data: {fileName: targetFile, type:type},
			dataType:'text',
			type: 'POST',
				success: function(result){
					alert(result);
				}
		});	//$.ajax
	});

 

UploadController.java 추가 - 서버에서 첨부파일의 삭제

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

	@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(largeFileName);
				
				file = new File(largeFileName);
				file.delete();
			}
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
		
		return new ResponseEntity<>("deleted", HttpStatus.OK);
	}

 

파일 삭제 테스트 결과

1. 파일 업로드

2. 파일 업로드 확인

3. 문서 파일(이미지가 아닌) 삭제

4. 업로드 폴더에서 삭제됨

5. 이미지 파일 삭제

6. 섬네일 삭제 후, 원본 이미지도 삭제 됨을 확인

7. 정상 삭제 확인


24.3.2 첨부파일 삭제 고민

파일 업로드 후, 파일을 삭제하는 것에 대한 처리는 끝났다. 이제 사용자가 비정상적으로 브라우저를 종료했을 때에는 어떻게 처리해줄 것인가 고민해야 한다. 서버에는 Ajax를 이용해서 업로드했기 때문에 이미 저장이 된 상태지만, 사용자가 비정상 종료를 해버린다면 이를 감지할 수 있는 방법이 없다.

이에 대한 가장 좋은 해결책은 실제 최종적인 결과와 서버에 업로드된 파일의 목록을 비교해서 처리하는 것이다. 이는 주로 spring-batch나 Quartz 라이브러리를 이용해서 처리하는데, 이는 뒤에서 추후 구현해보도록 하자!