💾이번엔 파일을 업로드해보자!
파일을 업로드나 다운로드하는 기능은 보안에 취약하기 때문에 조심해야합니다.
아래 SQL을 수행해서 file을 저장할 테이블을 생성해줍니다.
create table file(
id int not null primary key auto_increment,
name varchar(256),
uuid varchar(40),
upload_path vharchar(1000),
post_id int,
foreign key(post_id)
references post(id));
글을 작성할 때 form을 아래와 같이 수정해서 파일을 전송할 수 있도록 만듭니다. 그리고 스크립트를 수정합니다. 글을 DB에 저장하고 나서 글의 id를 반환받습니다. 그리고 만약 form에 파일이 입력되어 있다면 file을 업로드합니다. 없다면 /board로 이동합니다.
<form action="/post" method="post" class="user">
<div class="form-group">
<input type="text" class="form-control form-control-user" name="title" placeholder="Title">
</div>
<div class="form-group">
<textarea class="form-control form-control-user" name="content"></textarea>
</div>
<div class="form-group">
<input type="file" id="files" multiple="multiple">
</div>
<a id="register" class="btn btn-primary btn-user btn-block">
Register Post
</a>
</form>
$("#register").click(function (){
var postVO = new Object()
var formData = new FormData()
var files = $('#files')[0].files
var postId
postVO.title = $("[name='title']").val()
postVO.content = $("[name='content']").val()
$.ajax({
method:"POST",
url:"/post",
data: JSON.stringify(postVO),
contentType: "application/json; charset=UTF-8",
success: function (data){
postId = data
if(files.length != 0){
for(var i=0; i<files.length; i++)
formData.append('files',files[i])
$.ajax({
method:"POST",
enctype: 'multipart/form-data',
url:"/"+postId+"/file",
data: formData,
processData: false,
contentType: false,
success:function(){
location.href='/board'
},
error: function (e){console.log(e)}
})
}
else
location.href='/board'
}
})
})
파일 업로드를 하기 위해서는 설정을 해주어야합니다. 아래와 같이 WebConfig 클래스를 수정합니다. UPLOAD_PATH는 파일이 업로드 될 경로입니다. 후에 FileService에서 사용하기 위해 public과 static변수로 만들고, 수정할 수 없도록 final로 만들어줍니다.
중복되는 코드들은 생략합니다.
public class WebConfig implements WebApplicationInitializer {
public final static String UPLOAD_PATH = "/Users/youhojoon/Desktop/CS/upload";
private final long MAX_FILE_SIZE = 20971520; //파일 최대 크기 1024 * 1024 * 20 = 20MB
private final long MAX_REQUEST_SIZE = 41943040; // 요청에서 받을 수 있는 최대 크기 40MB
private final int FILE_SIZE_THRESHOLD = 20971520; // 파일이 디스크에 기록되는 크기 제한 20MB
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
/*
생략
*/
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(ServletConfig.class);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher",
new DispatcherServlet(applicationContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
MultipartConfigElement multipartConfigElement = new MultipartConfigElement(UPLOAD_PATH, MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
dispatcher.setMultipartConfig(multipartConfigElement);
/*
생략
*/
}
}
이제 우리는 파일 요청을 서블렛을 통해 설정한 것처럼 받게 됩니다. 이 파일을 직접적으로 다루는 클래스를 ServletConfig에 설정해주어야 합니다. 그리고 파일 업로드도 로그인을 한 유저만 할 수 있도록 interceptor 경로에 추가해줍니다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/board","/post/**","/*/file");
registry.addInterceptor(new PostInterceptor()).addPathPatterns("/post/**");
}
@Bean
public StandardServletMultipartResolver standardServletMultipartResolver(){
return new StandardServletMultipartResolver();
}
파일의 정보를 담을 FileVO 클래스를 생성합니다.
@Data
@Builder
public class FileVO {
public int id;
public String name; //파일의 이름
public String uuid; //이름의 중복을 피하기 위해 앞에 붙는 랜덤한 문자열 ex)aad6af27-d71d-4a17-a627-b258cfe8b2fe
public String uploadPath; //파일이 저장된 위치
public int postId
}
@Builder Annotation은 lombok의 Annotation으로 디자인 패턴의 Builder패턴을 만들어줍니다.
FileVO의 alias를 추가해줍니다.
<configuration>
<typeAliases>
<typeAlias type="ac.kr.smu.vo.PostVO" alias="PostVO"/>
<typeAlias type="ac.kr.smu.vo.UserVO" alias="UserVO"/>
<typeAlias type="ac.kr.smu.vo.FileVO" alias="FileVO"/>
</typeAliases>
</configuration>
파일의 내용을 저장할 FileMapper 인터페이스와 xml을 생성합니다.
public interface FileMapper {
public void save(FileVO fileVO);
}
<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>
</mapper>
파일의 처리를 할 FileService를 생성합니다.
public interface FileService {
public void saveAll(int postId,List<MultipartFile> files);
}
@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {
private final FileMapper fileMapper;
@Override
public void saveAll(int postId,List<MultipartFile> files) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String upload_date = sdf.format(date);
upload_date.replace("-", File.separator);
File uploadPath = new File(WebConfig.UPLOAD_PATH,upload_date);
if(!uploadPath.exists()) //현재 날짜의 폴더가 없다면 생성 ex) 2021/04/03
uploadPath.mkdir();
for(MultipartFile file : files){//파일을 하나씩 저장
String fileName = file.getOriginalFilename();
UUID uuid = UUID.randomUUID();
FileVO fileVO = FileVO.builder().postId(postId).name(fileName).uuid(uuid.toString())
.uploadPath(uploadPath.toPath().toString()).build();
fileName = uuid.toString() + "_" + fileName;
File uploadFile = new File(uploadPath,fileName);
try{
file.transferTo(uploadFile);
fileMapper.save(fileVO);
}catch (IOException e){e.printStackTrace();}
}
}
}
FileController는 view를 반환할 필요가 없으므로 @RestController로 작성합니다.
@RestController
@RequestMapping(value = "/{postId}/file")
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@PostMapping
public void postFile(@RequestParam("files") List<MultipartFile> files, @PathVariable("postId") int postId){
fileService.saveAll(postId,files);
}
}
INSERT를 하고나서 postVO에 id를 저장해줄 수 있도록 수정합니다. useGeneratedKeys속성은 DB에서 자동으로 설정한 키를 사용한다는 속성이고 기본값은 false입니다. keyProperty는 키가 저장될 필드입니다.
<insert id="save" useGeneratedKeys="true" keyProperty="id">
INSERT INTO post(title, content, created_date, user_id)
VALUES (#{title}, #{content}, #{created_date}, #{user.id});
</insert>
글을 저장 후, 글의 id를 반환할 수 있도록 수정합니다.
@PostMapping
public @ResponseBody int postPost(@RequestBody PostVO postVO, UserVO user) {
postVO.setUser(user);
postService.save(postVO);
return postVO.getId();
}
로그인을 하지않고 postman으로 요청을 보내면 아래처럼 로그인 페이지로 이동됩니다.