안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
파일 다운로드
boardDetail.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<jsp:include page="/WEB-INF/views/common/header.jsp">
<jsp:param value="게시판 상세보기" name="title"/>
</jsp:include>
<style>
div#board-container{width:400px;}
input, button, textarea {margin-bottom:15px;}
button { overflow: hidden; }
/* 부트스트랩 : 파일라벨명 정렬*/
div#board-container label.custom-file-label{text-align:left;}
</style>
<div id="board-container" class="mx-auto text-center">
<input type="text" class="form-control"
placeholder="제목" name="boardTitle" id="title"
value="${board.title}" required>
<input type="text" class="form-control"
name="memberId"
value="${board.member.name}" readonly required>
<c:if test="${not empty board.attachments}">
<c:forEach items="${board.attachments}" var="attachment" varStatus="vs">
<button type="button"
class="btn btn-outline-success btn-block" onclick="location.href='${pageContext.request.contextPath}/board/fileDownload.do?no=${attachment.no}'">
첨부파일${vs.count} - ${attachment.originalFilename}
</button>
</c:forEach>
</c:if>
<textarea class="form-control" name="content"
placeholder="내용" required>${board.content}</textarea>
<input type="number" class="form-control" name="readCount" title="조회수"
value="${board.readCount}" readonly>
<input type="datetime-local" class="form-control" name="created_at"
value='${board.createdAt}'>
</div>
<jsp:include page="/WEB-INF/views/common/footer.jsp"></jsp:include>
Controller (Service - Dao 생략)
BoardController
Resource : 다음 구현체들의 추상화레이어를 제공 (인터페이스)
웹 상 자원 : UrlResponse
classpath 자원 : ClassPathResource
서버컴퓨터 자원 : FileSystemResource
ServletContext(web root) 자원 : ServletContextResource
입출력자원 : InputStreamResource
이진데이터 자원 : ByteArrayResource
@ResponseBody : 핸들러의 반환된 자바 객체를 응답메세지 바디에 직접 출력하는 경우 사용
@Autowired
ResourceLoader resourceLoader;
@GetMapping("/fileDownload.do")
// 이진데이터가 작성된 응답이 올 것을 기대하고 브라우저는 그것을 읽어서 본인 컴퓨터에 저장!!
@ResponseBody
public Resource fileDownload(@RequestParam int no, HttpServletResponse response) throws IOException {
Attachment attach = boardService.selectOneAttachment(no);
log.debug("attach = {}", attach);
String saveDirectory = application.getRealPath("/resources/upload/board");
File downFile = new File(saveDirectory, attach.getRenamedFilename());
// "file:" -> 프로토콜 접두사 (사용 시 FileSystemResource 출동!!)
String location = "file:" + downFile; // File#toString은 파일의 절대경로를 반환
log.debug("location = {}", location);
Resource resource = resourceLoader.getResource(location); // 해당 자원에 대한 실제 구현체를 만들어서 반환!
log.debug("resource = {}", resource);
log.debug("resource#file = {}", resource.getFile());
// 응답헤더 작성 -> 브라우저가 이를 알고 다운로드 받을 준비 + 다운로드 받을 파일이름 지정
response.setContentType("application/octet-stream; charset=utf-8"); // 이진데이터 임을 명시
String filename = new String(attach.getOriginalFilename().getBytes("utf-8"), "iso-8859-1");
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename +"\"");
return resource;
}
@콘솔출력값
DEBUG: com.ce.spring2.board.controller.BoardController - attach = Attachment(no=21, boardNo=62, originalFilename=2022-02-24.png, renamedFilename=20220830_150030667_393.png, downloadCount=0, createdAt=2022-08-30T15:00:30)
DEBUG: com.ce.spring2.board.controller.BoardController - location = file:C:\Workspaces\spring_workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\hello-spring2\resources\upload\board\20220830_150030667_393.png
DEBUG: com.ce.spring2.board.controller.BoardController - resource = URL [file:C:/Workspaces/spring_workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp1/wtpwebapps/hello-spring2/resources/upload/board/20220830_150030667_393.png]
DEBUG: com.ce.spring2.board.controller.BoardController - resource#file = C:\Workspaces\spring_workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\hello-spring2\resources\upload\board\20220830_150030667_393.png
해당 파일의 filename을 가져오기 위하여 DB에서 Attachment 객체를 가져왔습니다.
그 후 해당 위치의 File객체를 생성하여 "file:" 프로토콜 접두사를 이용해 FileSystemResource를 출동!!시킵니다.
ResourceLoader로부터 해당 파일의 절대 경로에 대해 실제 구현체를 만들어서 Resource 객체를 반환해줍니다.
* 응답헤더 작성
한글파일을 대비하여 filename 생성
→ getBytes()로 생성(utf-8 방식으로 쓰인)
→ "iso-8859-1" 방식으로 인코딩 (톰캣에 의해 전송되면서 깨지지 않음!)
생성된 filename을 Header에 추가해주면 다운로드 처리 완료!
※ CONTENT-DISPOSITION 관련
https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Disposition
다운로드는 잘 되지만 네트워크 상의 응답 헤더를 확인해보면 Content-Type이 설정한 타입이 아닌 'text/html'로 받아온 것을 확인할 수 있습니다.
그 이유는 작성 이후 spring이 content-type를 뒤집어 씌웠기 때문입니다. (오버라이딩 처리)
따라서 직접 명시를 해줘야할 필요가 있습니다.
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE 처리
// produces : 핸들러가 어떤 것을 생성하는 지 명시해줌
@GetMapping(path = "/fileDownload.do", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
이진데이터임을 파악하고 실제 타입 확장자별로 mediaType을 지정해준 것을 확인할 수 있습니다.
※ 파일이름이 한글로 된 파일 다운로드 시
한글을 "iso-8859-1"로 인코딩 처리했기 때문에 Content-Disposition에는 깨진 파일이름이 보여지지만, 크롬에서 역으로 디코딩할 때는 한글로 올바르게 출력됩니다.
'Java > Spring' 카테고리의 다른 글
Spring) Ajax - MessageConverter와 @ResponseBody를 통해 응답 (0) | 2022.09.06 |
---|---|
Spring) Ajax - jsonView빈을 통해 응답 (0) | 2022.09.05 |
Spring) 2개 이상 테이블 조회(JOIN) - association, collection (0) | 2022.09.02 |
Spring) 트랜잭션 처리 - @Transactional (0) | 2022.08.31 |
Spring) 파일 업로드 처리 (0) | 2022.08.31 |