form을 submit하여 업로드하는 방식도 있지만, 이번에는 Ajax 방식을 이용해 구현해 볼 것이다.
먼저, web.xml에서 첨부파일 설정값을 설정해주어야 한다.
web.xml 에서 servlet 태그 안에 내용을 추가한다.
<!-- 첨부파일 설정 값 -->
<multipart-config>
<location>c:\\upload\\temp</location>
<max-file-size>20971520</max-file-size>
<!-- byte 기본단위 1024 1024 1개 파일의 최대 업로드 용량은 20MB -->
<max-request-size>41943040</max-request-size>
<!-- 동시에 업로드하는 파일들의 총 크기 20971520 = 1024 * 1024 * 20(MB) -->
<file-size-threshold>20971520</file-size-threshold>
<!-- 스트림으로 처리할 수 있는 최대 크기를 초과하면 임시파일 저장 후 업로드 -->
</multipart-config>
location 위치대로, c드라이브에 upload 폴더 생성 > 그 안에 temp 폴더를 생성한다.
다음으로, 스프링의 업로드 처리는 MultipartResolver라는 타입의 객체를 빈으로 등록해야 한다.
웹과 관련된 설정이므로, servlet-context.xml 을 이용해서 설정한다.
src > main > webapp > WEB-INF > spring > appServlet 내에 있는
servlet-context.xml 을 열어 맨 아래에 있는 </beans:bean> 코드 바로 위에 내용을 추가한다.
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"></beans:bean>
<!-- 첨부파일 처리 -->
* Resolver : 어떤 요청이 왔을 때 요청을 받아 분배하여 처리하는 것
첨부파일 데이터 테이블 생성 및 매핑
이제 sql developer 에서 첨부파일용 테이블을 만들고 설정할 것이다.
admin 계정으로 접속해 쿼리문을 작성한다.
create table tbl_attach (
uuid varchar2(100) not null,
uploadpath varchar2(200) not null,
filename varchar2(100) not null,
filetype char(1) default 'I',
bno number(10,0)
);
alter table tbl_attach add constraint pk_attach
primary key (uuid); --기본키 설정
alter table tbl_attach add constraint fk_board_attach
foreign key (bno)
references tbl_board(bno); --외래키 설정
commit;
STS 로 돌아와서, 첨부파일의 정보들을 저장하는 클래스를 생성한다.
src/main/java > kr.icia.domain > new Class 생성 > name: BoardAttachVO
package kr.icia.domain;
import lombok.Data;
@Data
public class BoardAttachVO {
private String uuid; //중복방지용
private String uploadPath;
private String fileName;
private boolean fileType;
private Long bno;
}
BoardVO.java에도 변수를 추가한다.
private List<BoardAttachVO> attachList;
첨부파일용 새로운 인터페이스를 생성한다.
src/main/java > kr.icia.mapper > new Interface 생성 > name: BoardAttachMapper
package kr.icia.mapper;
import java.util.List;
import kr.icia.domain.BoardAttachVO;
public interface BoardAttachMapper {
public void insert(BoardAttachVO vo); //첨부파일 등록
public void delete(String uuid); //첨부파일 삭제
public List<BoardAttachVO> findByBno(Long bno); //첨부파일 목록
public void deleteAll(long bno); //첨부파일 여러개 한꺼번에 삭제
public List<BoardAttachVO> getOldFiles();
//111.zip커뮤니티를 사용하는 사용자들은 중복 파일명을 사용할 수도 있음
//시스템은 동일 파일명에 대해서 내부적으로 저장하는 다른 이름을 가짐
}
데이터베이스를 매핑할 XML을 생성한다.
src/main/resources > kr.icia.mapper > new XML file 생성 > name: BoardAttachMapper.xml
BoardMapper.xml에서 dtd를 복사한 후 코드를 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.icia.mapper.BoardAttachMapper">
<insert id = "insert">
insert into tbl_attach (uuid, uploadpath, filename, filetype, bno)
values (#{uuid},#{uploadPath},#{fileName},#{fileType},#{bno})
</insert>
<delete id="delete">
delete from tbl_attach where uuid = #{uuid}
</delete>
<select id="findByBno" resultType="kr.icia.domain.BoardAttachVO">
select * from tbl_attach where bno = #{bno}
</select>
</mapper>
첨부파일 화면처리
다음으로, 글을 쓸 때 첨부가 가능하게 만들어야 하므로
register.jsp 의 div 맨 밑에 코드를 추가한다.
<!-- 첨부파일 처리 -->
<br />
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<div class="form-group uploadDiv">
파일 첨부: <input type="file" name="uploadFile" multiple>
</div>
<div class="uploadResult">
<ul></ul>
</div>
</div>
</div>
</div>
</div>
서버를 구동해보면, 글쓰기 화면에서 파일첨부 버튼이 생성된 것을 볼 수 있다.
첨부파일 관련 클래스 생성
아직 눈에 보이는 버튼 부분만 구현한 것이므로, 이제 기능 부분도 구현해볼 것이다.
src/main/java > kr.icia.domain > new Class 생성 > name: AttachFileDTO
BoardAttachVO는 1대다 구조에서 어느 게시물의 첨부파일인지 처리하는 클래스였다면, AttachFileDTO는 첨부파일 1개에 대한 처리를 하는 클래스이다.
package kr.icia.domain;
import lombok.Data;
@Data
public class AttachFileDTO {
private String uuid;
private String uploadPath;
private String fileName;
private boolean image;
}
이제 새로운 첨부파일 컨트롤러를 만들 것이다.
src/main/java > kr.icia.controller > new Class 생성 > name: uploadController
package kr.icia.controller;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import kr.icia.domain.AttachFileDTO;
import lombok.extern.log4j.Log4j;
@Controller
@Log4j
public class uploadController {
String uploadFolder = "c:\\upload";
@PostMapping(value = "/uploadAjaxAction", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody //응답을 json형태로 리턴
public ResponseEntity<List<AttachFileDTO>> uploadAjaxPost(MultipartFile[] uploadFile) {
//rest방식으로 ajax처리. 파일을 받고 json 값을 리턴한다
List<AttachFileDTO> list = new ArrayList<>();
//여러개 파일 저장을 위한 객체 배열 타입 선언
String uploadFolderPath = getFolder();
File uploadPath = new File(uploadFolder, uploadFolderPath);
//예) c:\\upload\\2021\\04\\28에 파일 저장 예정
if(uploadPath.exists() == false) {
uploadPath.mkdirs();
} //경로에 폴더들이 생성되어있지 않다면 생성해라
//파일은 1개일수도, 여러개일 수도 있음
for (MultipartFile multipartFile : uploadFile) {
AttachFileDTO attachDTO = new AttachFileDTO();
String uploadFileName = multipartFile.getOriginalFilename();
//파일의 원래 이름 저장
//익스플로러용 처리
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
attachDTO.setFileName(uploadFileName); //파일이름저장
UUID uuid = UUID.randomUUID(); //universal unique identifier, 범용 고유 식별자
//파일의 중복을 회피
uploadFileName = uuid.toString() + "_" + uploadFileName;
//예) uuid_일일일.txt
try {
File saveFile = new File(uploadPath, uploadFileName);
multipartFile.transferTo(saveFile);
//서버에 파일 저장
attachDTO.setUuid(uuid.toString());
attachDTO.setUploadPath(uploadFolderPath);
list.add(attachDTO);
} catch (Exception e) {
e.printStackTrace();
}
}
return new ResponseEntity<>(list,HttpStatus.OK);
}
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator);
// 파일 검색 시간을 줄이기 위해서 폴더 1개에 모두 저장하는 것이 아니라 년월일로 구분하여 폴더를 생성하고 그 곳에 파일을 저장한다.
// File.separator : 폴더 구분자를 운영체제에 맞춰서 변경한다.
}
}
첨부파일을 저장할 때는 파일명이 중복되지 않도록 해야 하고, 한 폴더 내에 너무 많은 파일이 생성되지 않도록 고려해야 한다.
위의 문제를 방지하기 위해서 getFolder() 메소드를 이용해 년월일로 폴더를 생성하여 담을 수 있도록 만들고, UUID를 적용하여 파일 이름에 중복이 없도록 처리하고 있다.
첨부파일 버튼 스크립트
이제 페이지에서 파일을 첨부할 수 있도록 스크립트를 추가할 것이다. 파일 업로드에는 바이러스 감염 방지를 위해 일부 파일 확장자를 지정하여 파업로드를 제한하고, 파일 크기도 너무 큰 파일을 업로드하지 못하도록 걸러내는 작업을 포함해야 한다.
register.jsp 를 열고 하부에 <script>를 만든다.
<script>
$(document).ready(function(e){
var formObj=$("form[role='form']");
$("button[type='submit']").on("click",function(e){
e.preventDefault();
console.log("submit clicked");
});
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
//정규 표현식. 일부 파일의 업로드 제한. https://regexper.com/
var maxSize = 5242880; //5MB
function checkExtension(fileName, fileSize) {
if (fileSize >= maxSize){
alert("파일 크기 초과");
return false;
}
if (regex.test(fileName)) {
alert("해당 종류의 파일은 업로드 불가.");
return false;
}
return true;
}
$("input[type='file']").change(function(e) {
//첨부파일 정보가 변경됐다면,
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
//지정된 정보로 접근하여 배열 형태로 리턴
for (var i = 0; i < files.length; i++){
if (!checkExtension(files[i].name, files[i].size)){
//확장자가 걸러내야할 경우라면 더 이상 처리하지 않고 종료
return false;
}
formData.append("uploadFile", files[i]); //form에 관련 정보를 추가
}
$.ajax({
url:'/uploadAjaxAction',
processData:false,
contentType:false,
data:formData,
type:'POST',
dateType:'json',
success:function(result){
console.log(result);
}
});
});
});
</script>
여기서 첨부파일의 형식을 체크하기 위해 정규표현식(regExp)을 사용했다.
Reference ▶ 정규표현식 (Regex) 정리 (tistory.com)
서버 구동 후 글쓰기 화면에서 파일을 첨부할 수 있고, 파일이 첨부되면 아래처럼 콘솔창에 뜨게 된다.
지정된 경로에도 파일이 올라가고 있다.
첨부파일 목록 표시
현재는 첨부파일을 2개 이상 올렸을 때는 몇 개나 올렸는지 확인이 안 되고 있다.
이 부분을 보완하여 전체 목록이 표시되도록 수정할 것이다.
register.jsp 에서 첨부파일 업로드 함수인 function checkExtension() ajax의 success function 에 새로운 함수를 호출한다.
showUploadResult(result);
호출한 함수를 구현한다.
function showUploadResult(uploadResultArr) {
if (!uploadResultArr || uploadResultArr.length == 0) {
// json 처리 결과가 없다면 함수 종료.
return;
}
var uploadUL = $(".uploadResult ul");
var str = "";
// each 구문은 전달된 배열의 길이 만큼,
// each 이후의 함수를 반복 처리.
// https://api.jquery.com/jQuery.each/#jQuery-each-array-callback
$(uploadResultArr)
.each(
function(i, obj) {
var fileCallPath = encodeURIComponent(obj.uploadPath
+ "/"
+ obj.uuid
+ "_"
+ obj.fileName);
// encodeURIComponent :
// uri 로 전달되는 특수문자의 치환.
// & ?
var fileLink = fileCallPath
.replace(new RegExp(
/\\/g), "/");
// 전달되는 값들 중에서 역슬러시를 찾아서 슬러시로 변경.
str += "<li data-path='";
str += obj.uploadPath+"' data-uuid='";
str += obj.uuid+"' data-filename='";
str += obj.fileName+"' data-type='";
str += obj.image+"'><div>";
str += "<img src='/resources/img/attach.png' width='20' height='20'>";
str += "<span>" + obj.fileName
+ "</span> ";
str += "<b data-file='"+fileCallPath;
str += "' data-type='file'>[x]</b>";
str += "</div></li>";
});
uploadUL.append(str);
}// end_showUploadResult
Reference ▶ jQuery.each() | jQuery API Documentation
코드 작성 후, 첨부파일 아이콘을 attach.img를 명시한 resources/img 폴더 내에 넣는다.
서버를 구동하고 첨부파일을 추가하면 파일 첨부 아래에 첨부된 파일의 목록이 보여진다.
첨부파일 업로드 취소 기능 구현
첨부파일을 올렸을 때 올리기를 취소하고 싶을 수가 있다.
이 부분을 버튼[x]은 만들어 놓았지만 기능은 아직 구현하지 않았기 때문에 이제 구현해볼 것이다.
첨부파일이 삭제되면 첨부파일을 백업해두었던 c드라이브에서도 삭제되어야 하고, 웹페이지의 첨부파일 목록에서도 삭제돼야 한다.
먼저 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(); //파일삭제
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<String>("deleted", HttpStatus.OK);
//return null;
}
여기서 실제로 첨부파일을 삭제한 후에 콘솔에 fileName을 출력하도록 했는데, 실제로 표시되는 콘솔 로그를 보면 아래와 같을 것이다.
INFO : kr.icia.controller.uploadController - deleteFile: 2021%5C10%5C13%2F0064a823-8c6e-49f9-a08a-57796c378e53_%EC%8A%A4%ED%94%84%EB%A7%81%208%EC%9D%BC%EC%B0%A8.pdf
이 부분을 우리가 알아볼 수 있게 디코딩하는 작업을 거쳐야 한다.
실제로 디코딩하면 다음과 같이 변할 것이다.
INFO : kr.icia.controller.uploadController - deleteFile: 2021\10\13/0064a823-8c6e-49f9-a08a-57796c378e53_스프링 8일차.pdf
(이 부분은 임의로 아래 사이트에서 디코딩을 처리해본 결과이다.)
http://coderstoolbox.net/string/#!encoding=url&action=decode&charset=utf_8
다음으로, 버튼에 기능을 넣기 위해 register.jsp 에 스크립트를 추가한다.
//올리기 취소 버튼
$(".uploadResult").on("click", "b", function(e) {
console.log("delete file");
var targetFile = $(this).data("file"); //fileCallPath: 저장경로+uuid+파일명.확장자
var type = $(this).data("type"); //file
var targetLi = $(this).closest("li"); //첨부파일 목록 지우기 위해 지정
$.ajax({
url : '/deleteFile',
data : {
fileName : targetFile,
type : type
},
dataType : 'text',
type : 'POST',
success : function(result) {
alert(result);
targetLi.remove();
}
});
});
서버구동 후 글쓰기 화면으로 들어가 파일 첨부를 한 후에 [x]를 누르면 deleted 메세지와 함께 첨부파일 목록에서 삭제되고, 첨부파일이 저장되는 경로(c드라이브>upload>날짜폴더)에서도 파일이 정상적으로 삭제된 것을 확인할 수 있다.
글쓰기 완료시 첨부파일 정보를 DB에 저장하기
이후에 글을 읽었을 때도 첨부파일이 확인이 가능하도록 구현할 것인데, 그러려면 글쓰기에서 "전송"버튼을 클릭했을 때 첨부파일에 대한 정보도 같이 저장되어야 한다.
이 부분을 먼저 수정해볼 것이다.
register.jsp를 열고, 스크립트의 $("button[type='submit']").on("click", function(e) 함수 안에 내용을 추가한다.
$("button[type='submit']").on("click", function(e) {
e.preventDefault();
console.log("submit clicked");
//글 등록버튼을 누르면 첨부파일 정보도 함께 전송됨
var str="";
$(".uploadResult ul li").each(function(i,obj) {
var jobj = $(obj);
console.dir(jobj); //console.dir(): JavaScript객체의 모든 속성을 보는 방법
console.log("-----------------");
console.log(jobj.data("fileName"));
str += "<input type='hidden' name='attachList[";
str += i + "].fileName' value='" + jobj.data("filename");
str += "'>";
str += "<input type='hidden' name='attachList[";
str += i + "].uuid' value='" + jobj.data("uuid");
str += "'>";
str += "<input type='hidden' name='attachList[";
str += i + "].uploadPath' value='" + jobj.data("path");
str += "'>";
str += "<input type='hidden' name='attachList[";
str += i + "].fileType' value='" + jobj.data("type");
str += "'>";
});
formObj.append(str).submit();
});
이제 글쓰기 화면에서 submit 버튼을 누르면 해당 첨부파일의 정보를 formObj에 같이 넣고 정보가 넘어가게 된다.
BoardController.java 에서 "/register" 부분도 수정해야 한다. 기존 내부에 있던 소스는 주석처리한다.
@PostMapping("/register")
public String register(BoardVO board, RedirectAttributes rttr) {
if (board.getAttachList() != null) {
board.getAttachList().forEach(attach -> log.info(attach));
}
//log.info("register : " + board);
//service.register(board);
//rttr.addFlashAttribute("result", board.getBno());
return "redirect:/board/list";
}
기존에 log로 출력하던 board는 일단 중지하고, 첨부파일에 대한 요소를 로그로 출력하도록 하는 것이다.
서버를 구동하고 글쓰기 제출을 한 다음, 콘솔을 확인해보면 다음과 같이 콘솔창에 첨부파일 로그가 출력된다.
INFO : kr.icia.controller.BoardController - BoardAttachVO(uuid=1bb98eeb-97ce-4ff1-a647-06c20c42e58f, uploadPath=2021\10\13, fileName=스프링 10일차.pdf, fileType=false, bno=null)
INFO : kr.icia.controller.BoardController - BoardAttachVO(uuid=3d156ab1-c764-4390-a1a7-763b314f8913, uploadPath=2021\10\13, fileName=스프링 11일차.pdf, fileType=false, bno=null)
다음으로, BoardServiceImp.java 에 코드를 추가할 것이다.
먼저 기존 어노테이션을 사용하고 있던 @AllArgsConstructor 는 주석처리한다. (자동으로 만드는 것을 수동으로 바꿀 것이다.)
그리고 첨부파일 매퍼 클래스를 추가로 명시한다.
@Setter(onMethod_ = @Autowired)
private BoardAttachMapper attachMapper;
그리고 기존 register() 부분을 수정한다.
@Transactional 어노테이션을 추가하고, if구문과 forEach를 이용한 첨부파일을 삽입하는 코드를 추가했다.
@Transactional
@Override
public void register(BoardVO board) {
log.info("register......" + board);
mapper.insertSelectKey(board);
if(board.getAttachList() == null || board.getAttachList().size() <=0) {
return;
}
board.getAttachList().forEach(attach -> {attach.setBno(board.getBno());
attachMapper.insert(attach);
});
}
첨부파일이 없다면 게시물만 등록하고, 있다면 첨부파일도 같이 등록할 수 있도록 구성하는 것이다.
다시 BoardController.java 로 와서, 이전에 로그 출력하던 부분 주석을 다시 풀고 기존 거를 주석 처리한다.
잠시 로그 출력 방식을 바꿔서 확인해 보기 위한 작업이다.
@PostMapping("/register")
public String register(BoardVO board, RedirectAttributes rttr) {
/*if (board.getAttachList() != null) {
board.getAttachList().forEach(attach -> log.info(attach));
}*/
log.info("register : " + board);
service.register(board);
rttr.addFlashAttribute("result", board.getBno());
return "redirect:/board/list";
}
일단 주석만 바꾼 후 서버구동을 해보고, 글쓰기도 테스트해볼 것이다.
그러면 이제 기존과는 달리 글작성 시 첨부파일 정보도 DB에 저장되고, sql developer에서도 확인이 가능하다.
'개발입문 > SPRING 게시판 만들기' 카테고리의 다른 글
[SPRING] 게시글 삭제시 첨부파일과 댓글도 함께 삭제 (0) | 2022.10.14 |
---|---|
[SPRING] 첨부파일 다운로드 및 수정/삭제 (0) | 2022.10.14 |
[SPRING] 댓글 페이징 처리, 댓글 총 개수 보여주기 (0) | 2022.10.13 |
[SPRING] 댓글 화면 처리 및 CRUD (0) | 2022.10.13 |
[SPRING] 댓글 기능 구현 (0) | 2022.10.13 |
댓글