일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- HTML
- 스프링 Ioc Container
- 오라클주별데이터
- 오라클통계
- 스프링 제어역전
- 제이쿼리
- maven
- 자바왕초보
- 스프링과 스프링부트 차이점
- java
- 자바왕기초
- 자바 왕기초
- 세션
- 오라클월별데이터
- 스프링 구글차트
- 오라클일별데이터
- 자바 기초
- Spring Boot가 해결하려고 했던 문제
- jsp
- 자바기초
- 스프링
- 스프링 에러
- 스프링 구글차트로 기간별 현황 조회하기
- 스프링 부트가 해결하려고 했던 문제
- 오라클클라우드에 젠킨스 설치하기
- 스프링 Ioc
- 썸머노트
- CSS
- 오라클
- 자바
- Today
- Total
Just Do it
Part 6) 파일 업로드 처리 (Ch 25) 본문
*이 카테고리의 글은 <코드로 배우는 스프링 웹 프로젝트> 에서 각 파트별로 진행하는 프로젝트의 흐름을 보기 위해 각 장의 내용을 간단히 요약한 것이다.
* 출처: 코드로 배우는 스프링 웹 프로젝트 개정판, 구멍가게 코딩단, 남가람북스
Ch 25. 프로젝트의 첨부파일 - 등록
- 첨부파일은 게시물의 등록/조회/수정/삭제 화면에서 처리할 필요가 있으므로 각 단계마다 나누어서 개발을 진행한다.
25.1 첨부파일 정보를 위한 준비
- 첨부파일이 게시물과 합쳐지면 가장 먼저 진행해야 하는 일은 게시물과 첨부파일의 관계를 저장하는 테이블의 설계가 우선이다.
- 게시물의 첨부파일은 각자 고유한 UUID를 가지고 있기 때문에 별도의 PK를 지정할 필요는 없지만, 게시물을 등록할 때 첨부파일 테이블 역시 같이 insert 작업이 되어야 하므로 트랜잭션 처리가 필요하다 .
- 첨부파일을 보관하는 테이블은 tbl_attach로 설계한다. tbl_board는 tbl_reply와 이미 외래키 관계를 가지고 있으므로 첨부파일이 추가되면 아래와 같은 구조가 된다.
CREATE TABLE TBL_ATTACH (
UUID VARCHAR2(100) NOT NULL, -- UUID 값
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);
- SQL을 처리하기 위해서는 파일 정보를 처리하기 위해 파라미터를 여러 개 사용해야 하는 불편함이 있으므로, domain 패키지에 아예 BoardAttachVO 클래스를 설계하는 것이 유용하다. (AttachFileDTO와 거의 유사하지만 게시물의 번호가 추가되었고, 혼란을 피하기 위해서 새로 클래스를 작성한다.)
- 기존의 BoardVO는 등록 시 한 번에 BoardAttachVO를 처리할 수 있도록 List<BoardAttachVO>를 추가한다.
25.1.1 첨부파일 처리를 위한 Mapper 처리
- 첨부파일 정보를 데이터베이스를 이용해서 보관하므로 이를 처리하는 SQL을 Mapper 인터페이스와 XML을 작성해서 처리한다.
- BoardAttachMapper의 경우는 첨부파일의 수정이라는 개념이 존재하지 않기 때문에, insert()와 delete() 작업만을 처리한다. 그리고 특정 게시물의 번호로 첨부파일을 찾는 작업이 필요하므로 findByBno() 메서드를 정의한다.
- Mapper 인터페이스의 SQL을 처리하는 BoardAttachMapper.xml을 추가한다.
25.2 등록을 위한 화면 처리
- 첨부파일 자체의 처리는 Ajax를 통해서 이루어지므로, 게시물의 등록 시점에는 현재 서버에 업로드된 파일들에 정보를 등록하려는 게시물의 정보와 같이 전송해서 처리한다.
- 이 작업은 게시물의 등록 버튼을 클릭했을 때 현재 서버에 업로드된 파일의 정보를 <input type='hidden'> 으로 만들어서 한 번에 전송하는 방식을 사용한다.
- 게시물의 등록을 담당하는 register.jsp 파일에서 첨부파일을 추가할 수 있도록 수정하는 작업부터 시작한다. 기존 게시물의 제목이나 내용을 입력하는 부분 아래쪽에 첨부파일 등록을 위한 새로운 <div>를 추가한다.
- 그리고 업로드를 위한 uploadAjax.jsp의 CSS 부분도 register.jsp 내에 추가한다.
25.2.1 JavaScript 처리
- 복잡한 부분은 파일을 선택하거나 'Submit Button'을 클릭했을 때의 자바스크립트 처리이다.
- 가장 먼저 'Submit Button'을 클릭했을 때 첨부파일 관련된 처리를 할 수 있도록 기본 동작을 막는 작업부터 시작한다.
- 파일의 업로드는 별도의 업로드 버튼을 두지 않고, <input type='file'>의 내용이 변경되는 것을 감지해서 처리하도록 한다.
- $(document).ready() 내에 파일 업로드 시 필요한 코드를 추가한다.
- 첨부된 파일 처리는 기존과 동일하지만 아직은 섬네일이나 파일 아이콘을 보여주는 부분은 처리하지 않는다.
- 브라우저의 콘솔창을 이용해서 업로드가 정상적으로 처리되는지만 확인한다.
- 업로드된 결과를 화면에 섬네일 등을 만들어서 처리하는 부분은 별도의 showUploadResult() 함수를 제작하고 결과를 반영한다.
<script type="text/javascript">
$(document).ready(function(e){
var formObj = $("form[role='form']");
$("button[type='submit']").on("click", function(e){
e.preventDefault();
console.log("submit clicked");
});
// 파일 확장자 exe.sh.zip.alz 업로드 금지
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
// 파일 당 최대 용량 5MB로 지정
var maxSize = 5242880;
// 파일 확장자와 용량 확인
function checkExtension(fileName, fileSize){
if (fileSize > maxSize) {
alert("파일 당 용량은 최대 5MB까지만 가능합니다.")
return false;
}
if (regex.test(fileName)) {
alert("해당 종류의 파일은 첨부할 수 없습니다.(.exe, .sh, .zip, .alz 첨부 불가)")
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]);
}
// 업로드된 결과를 화면에 섬네일 등을 만들어서 처리한다.
function showUploadResult(uploadResultArr){
if(!uploadResultArr || uploadResultArr.length == 0){ return; }
var uploadUL = $(".uploadResult ul");
var str = "";
$(uploadResultArr).each(function(i, obj){
// 이미지 파일인 경우
if(obj.image){
var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName );
str += "<li><div>";
str += "<span>" + obj.fileName + "</span>";
str += "<button type='button' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button>";
str += "<img src='/display?fileName=" + fileCallPath + "'>";
str += "</div></li>";
// 일반 파일인 경우
}else {
var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName );
var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
str += "<li><div>";
str += "<span>" + obj.fileName + "</span>";
str += "<button type='button' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/resources/img/attach.png'></a>";
str += "</div></li>";
}
});
uploadUL.append(str);
}
//파일 업로드 작업 비동기 ajax 처리
$.ajax({
url:'/uploadAjaxAction',
processData: false,
contentType: false,
type: 'POST',
data: formData, // 서버에 보내는 데이터
dataType: 'json', // 서버에서 받아올 데이터
success: function(result){
console.log(result);
showUploadResult(result); // 파일 업로드 결과를 처리해주는 함수
}
});
});
});
</script>
- 게시물 등록 화면에서 첨부파일이 업로드되면 아래와 같은 모습으로 보이게 된다.
25.2.2 첨부파일의 변경 처리
- 첨부파일의 변경은 사실상 업로드된 파일의 삭제이므로 'x'모양의 아이콘을 클릭할 때 이루어지도록 이벤트를 처리한다.
- 삭제를 위해서는 업로드된 파일의 경로와 UUID가 포함된 파일 이름이 필요하므로 앞서 작성된 showUploadResult 함수 부분을 수정해주면 된다. showUploadResult() 함수 내 <button> 태그에 'data-file'과 'data-type' 정보를 추가한다.
...
// 이미지 파일인 경우
if(obj.image){
var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName );
str += "<li><div>";
str += "<span>" + obj.fileName + "</span>";
str += "<button type='button' data-file=\'"+ fileCallPath+ "\' data-type='image' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/display?fileName=" + fileCallPath + "'>";
str += "</div></li>";
// 일반 파일인 경우
}else {
var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName );
var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
str += "<li><div>";
str += "<span>" + obj.fileName + "</span>";
str += "<button type='button' data-file=\'"+ fileCallPath+ "\' data-type='file' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/resources/img/attach.png'></a>";
str += "</div></li>";
}
...
- 'x' 아이콘을 클릭하면 서버에서 삭제하도록 이벤트를 처리한다.
// 원하지 않는 첨부파일 삭제
$(".uploadResult"). on("click", "button", function(e){
console.log("delete file");
var targetFile = $(this).data("file");
var type = $(this).data("type");
var targetLi = $(this).closest("li");
$.ajax({
url: '/deleteFile',
data: {fileName: targetFile, type: type},
dataType: 'text',
type: 'POST',
sucess: function(result){
targetLi.remove();
}
});
- 브라우저에서 첨부파일을 삭제하면 업로드된 파일도 같이 삭제되는 것을 확인할 수 있다.
25.2.3 게시물 등록과 첨부파일의 데이터베이스 처리
- 게시물의 등록 과정에서는 첨부파일의 상세 조회는 의미가 없고, 단순히 새로운 첨부파일을 추가하거나 삭제해서 자신이 원하는 파일을 게시물 등록할 때 같이 포함하도록 한다.
- Ajax를 이용하는 경우 이미 어떠한 파일을 첨부로 처리할 것인지는 이미 완료된 상태이므로 남은 작업은 게시물이 등록될 때 첨부파일과 관련된 자료를 같이 전송하고, 이를 데이터베이스에 등록 하는 것이다.
- 게시물의 등록은 <form> 태그를 통해서 이루어지므로, 이미 업로드된 첨부파일의 정보는 별도의 <input type='hidden'> 태그를 생성해서 처리한다.
- 이를 위해서는 첨부파일 정보를 태그로 생성할 때 첨부파일과 관련된 정보(data-uuid, data-filename, data-type)를 <li> 태그 내에 추가한다.
...
$(uploadResultArr).each(function(i, obj){
// 이미지 파일인 경우
if(obj.image){
var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName );
str += "<li data-path='"+ obj.uploadPath + "' data-uuid='" + obj.uuid + "' data-filename='" + obj.fileName+ "' data-type='"+ obj.image+ "'><div>";
str += "<span>" + obj.fileName + "</span>";
str += "<button type='button' data-file=\'"+ fileCallPath+ "\' data-type='image' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/display?fileName=" + fileCallPath + "'>";
str += "</div></li>";
// 일반 파일인 경우
}else {
var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName );
var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
str += "<li data-path='"+ obj.uploadPath + "' data-uuid='" + obj.uuid + "' data-filename='" + obj.fileName+ "' data-type='"+ obj.image+ "'><div>";
str += "<span>" + obj.fileName + "</span>";
str += "<button type='button' data-file=\'"+ fileCallPath+ "\' data-type='file' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/resources/img/attach.png'></a>";
str += "</div></li>";
}
});
...
- <input type='hidden'>으로 처리된 첨부파일의 정보는 BoardVO로 수집된다.
- BoardVO에는 attachList라는 이름의 변수로 첨부파일의 정보를 수집하기 때문에 <input type='hidden'>의 name은 'attachList[인덱스번호]'와 같은 이름을 사용하도록 한다.
- register.jsp 화면에서는 자바스크립트를 이용해서 기존 <form> 태그를 전송하는 부분을 아래와 같이 수정한다.
var formObj = $("form[role='form']");
$("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);
str += "<input type='hidden' name='attachList[" + i + "].fileName' value='" + jobj.data("filename")+ "'>";
str += "<input type='hidden' name='attachList[" + i + "].uuid' value='" + jobj.data("uuid")+ "'>";
str += "<input type='hidden' name='attachList[" + i + "].uploadPath' value='" + jobj.data("path")+ "'>";
str += "<input type='hidden' name='attachList[" + i + "].fileType' value='" + jobj.data("type")+ "'>";
});
formObj.append(str).submit();
});
25.3 BoardController, BoardService의 처리
- 파라미터를 수집하는 BoardController는 별도의 처리 없이 전송되는 데이터가 제대로 수집되었는지를 먼저 확인한다.
- BoardController의 register()는 BoardService를 호출하기 전에 log를 이용해서 확인하는 작업을 먼저 진행한다.
- 브라우저에서 첨부파일을 추가하고 게시물을 등록하면 서버에서는 아래와 같은 로그들이 출력되는 것을 볼 수 있다.
- 이때 첨부파일이 이미지인지 여부에 따라서 fileType 등이 제대로 처리되는지 확인한다.
25.3.1 BoardServiceImpl 처리
- BoardMapper와 BoardAttachMapper는 이미 작성해 두었기 때문에 남은 작업은 BoardServiceImpl에서 두 개의 Mapper 인터페이스 타입을 주입하고, 이를 호출하는 일이다.
- 2개의 Mapper를 주입받아야 하기 때문에 자동주입 대신에 Setter 메서드를 이용하도록 BoardServiceImpl을 수정한다.
*수정 전(@AllArgsConstructor 를 이용한 자동주입)
*수정 후 (@Setter, @Autowired 이용)
- 게시물의 등록 작업은 tbl_board 테이블과 tbl_attach 테이블 양쪽 모두 insert가 진행되어야 하기 때문에 트랜잭션 처리가 필요하다.
- 일반적인 경우라면 오라클의 시퀀스(sequence)를 이용해서 nextval과 currval을 이용해서 처리하겠지만, 예제는 이미 MyBatis의 selectkey를 이용했기 때문에 별도의 currval을 매번 호출할 필요는 없다.
- BoardServiceImply 내 register() 메서드에 @Transactional 어노테이션을 추가하고 아래와 같이 메서드를 수정해준다.
*수정 전)
*수정 후)
- BoardServiceImpl의 register()는 트랜잭션 하에서 tbl_board에 먼저 게시물을 등록하고, 각 첨부파일은 생성된 게시물 번호를 셋팅한 후 tbl_attach 테이블에 데이터를 추가한다.
- MyBatis 쪽에 문제가 없다면 데이터베이스의 tbl_attach 테이블에 첨부파일이 여러 개 등록되었을 때 아래와 같은 모습으로 출력되는 것을 볼 수 있다.
'신입 개발자가 되기 위해 공부했던 독학 자료들 > 코드로 배우는 스프링 웹프로젝트' 카테고리의 다른 글
Part 6) 파일 업로드 처리 (Ch 28 & Ch 29) (0) | 2022.03.11 |
---|---|
Part 6) 파일 업로드 처리 (Ch 26 & Ch 27) (0) | 2022.03.10 |
Part 6) 파일 업로드 처리 (Ch22) (0) | 2022.03.09 |
Part 6) 파일 업로드 처리 (Ch21) (0) | 2022.03.09 |
Part 5) AOP와 트랜잭션 (Ch19 & Ch20 ) (0) | 2022.03.07 |