📥 이번에는 글과 연결된 파일을 다운로드해보자!
버튼을 추가해서 글에 연결된 파일이 있을 시 버튼을 만들어 다운로드 할 수 있도록 수정합니다.
<form id="modifyForm" action="/post" method="PUT" class="user">
<input type="number" name="id" value="${post.id}" hidden>
<div class="form-group">
<input type="text" class="form-control form-control-user" name="title" placeholder="Title" value="${post.title}" readonly>
</div>
<div class="form-group">
<textarea class="form-control form-control-user" name="content" readonly>${post.content}</textarea>
</div>
<div class="form-group">
<input type="text" class="form-control form-control-user" name="name" placeholder="name" value="${post.user.name}" readonly>
</div>
<c:if test="${post.fileList.size() != 0}">
<c:forEach var="file" items="${post.fileList}">
<a href="/${post.id}/file/${file.id}" class="btn btn-primary btn-user btn-block">
${file.name}
</a>
</c:forEach>
</c:if>
아래 생략
글과 연결된 파일들을 저장할 fileList 필드를 추가해줍니다.
@Data
public class PostVO {
private int id;
private String title;
private String content;
private Timestamp created_date = new Timestamp(new Date().getTime());
private UserVO user;
private List<FileVO> fileList = new ArrayList<>();
}
전과 마찬가지로 로그인한 유저만 다운로드를 할 수 있도록 수정합니다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/board","/post/**","/*/file","/*/file/**");
registry.addInterceptor(new PostInterceptor()).addPathPatterns("/post/**");
}
resultMap 태그안 collection 태그가 SQL을 수행하여 연관된 파일들을 조회하여 PostVO의 fileList에 저장합니다. select속성이 실행할 SQL문이고 ofType이 collection에 들어갈 각 원소의 타입입니다. column은 SQL을 실행할 때 WHERE문의 조건으로 들어갈 column을 뜻합니다.
<resultMap id="post" type="PostVO">
<id property="id" column="id"/>
<id property="title" column="title"/>
<id property="content" column="content"/>
<id property="created_date" column="created_date"/>
<association property="user" javaType="UserVO">
<result property="id" column="user_id"/>
<result property="email" column="email"/>
<result property="name" column="name"/>
</association>
<collection property="fileList" column="id" select="findFileListById" ofType="FileVO"/>
</resultMap>
<select id="findFileListById" resultType="FileVO">
SELECT * FROM file WHERE post_id = #{id}
</select>
id로 file을 조회할 수 있도록 수정합니다.
public interface FileMapper {
public void save(FileVO fileVO);
public FileVO findById(int id);
}
<mapper namespace="ac.kr.smu.mapper.FileMapper">
<insert id="save">
INSERT INTO file(name,uuid,upload_path,post_id)
VALUES(#{name},#{uuid},#{uploadPath},#{postId})
</insert>
<select id="findById" resultType="FileVO">
SELECT * FROM file WHERE id = #{id}
</select>
</mapper>
getPath 메소드를 추가해 파일의 경로를 반환할 수 있도록 합니다.
@Data
@Builder
public class FileVO {
public int id;
public String name;
public String uuid;
public String uploadPath;
public int postId;
public String getPath(){
return uploadPath + "/" + uuid + "_" + name;
}
}
파일 다운로드를 구현하는 방법은 여러가지 방법이 있습니다. 저희는 Spring에서 제공하는 클래스인 FileSystemResource를 사용해 구현해보도록 하겠습니다. FileVO의 getPath 메소드를 이용하여 파일의 경로를 주어서 FileSystemResource 객체를 생성합니다. 만약 경로에 주어진 파일이 없다면 null을 반환합니다.
public interface FileService {
public List<FileVO> saveAll(int postId,List<MultipartFile> files);
public FileSystemResource getFileSystemResource(int id);
}
@Override
public FileSystemResource getFileSystemResource(int id) {
FileSystemResource resource = new FileSystemResource(fileMapper.findById(id).getPath());
if(!resource.exists())
return null;
return resource;
}
파일을 다운로드 할 수 있도록 수정합니다. 만약에 resource가 null이라면 404에러를 반환합니다. 브라우저가 첨부파일의 이름을 처리하는 방식이 다르기 때문에 HttpHeader에서 브라우저의 정보를 받아와 서로 다르게 처리해줍니다. Header의 User-Agent가 브라우저의 정보를 담고있습니다. 파일을 다운로드 할때는 Content-Disposition을 Header에 추가해주어야합니다.
@RestController
@RequestMapping(value = "/{postId}/file")
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@GetMapping(value = "/{fileId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<FileSystemResource> getFile(@PathVariable("fileId") int fileId,
@RequestHeader("User-Agent") String userAgent){
FileSystemResource resource = fileService.getFileSystemResource(fileId);
if(resource==null)
return ResponseEntity.notFound().build();
String fileName = resource.getFilename();
/*
파일의 진짜 이름으로 만들기 위해
ex)4a08afec-1abf-4300-aea0-31fd082362ad_KakaoTalk_Photo_2021-03-27-10-33-45.jpeg에서
첫번째 _를 찾아 잘라내면 KakaoTalk_Photo_2021-03-27-10-33-45.jpeg로
파일의 실제 이름을 반환할 수 있다.
*/
fileName = fileName.substring(fileName.indexOf("_")+1);
HttpHeaders headers = new HttpHeaders();
try{
if(userAgent.contains("Chrome"))
fileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");
else
fileName = URLEncoder.encode(fileName,"UTF-8");
if(userAgent.contains("Safari"))
headers.add("Content-Disposition", "attachment; filename*=utf-8''"+fileName);
else
headers.add("Content-Disposition","attachment; filename="+fileName);
}catch (Exception e){e.printStackTrace();}
return new ResponseEntity(resource,headers, HttpStatus.OK);
}
@PostMapping
public void postFile(@RequestParam("files") List<MultipartFile> files, @PathVariable("postId") int postId){
fileService.saveAll(postId,files);
}
}
@GetMapping 의 produces 속성은 response의 Content-Type을 뜻합니다.