# Spring - 파일업로드 연습1 (일반적인 방식)
# Spring - 파일업로드 연습2 (파일명 중복제거)
# Spring - 파일업로드 연습3 (업로드 결과를 iframe에 출력)
# Spring - 파일업로드 연습4 AJAX방식(파일 저장 디렉토리, 썸네일 이미지 생성)
# Spring - 파일업로드 연습5 AJAX방식(업로드한 파일표시, 썸네일 출력, 다운로드, 삭제)
# AJAX(Asynchronous Javascript And XML)란?
지금까지 ajax방식으로 파일을 업로드, 다운로드, 삭제 처리를 구현해보았는데 이제 전에 만들어 놓은 게시판에 파일 업로드 기능을 적용시켜보자. 먼저 게시글 작성화면에 파일첨부 기능을 추가하고, 게시글 상세화면에는 첨부파일의 목록을 보여주고 다운로드하거나 삭제를 할 수 있도록 해보자. 마지막으로는 수정화면에서는 새로운 첨부파일을 올리거나 삭제할 수 있도록 처리를 구현해보자.
1. DB(Oracle)에 첨부파일 테이블 생성
게시판 파일 업로드 기능을 구현하기에 앞서 DB(Oracle)에 업로드한 파일의 정보를 저장할 테이블을 생성해보자.
01) 첨부파일 테이블 생성
CREATE TABLE tbl_attach (
fullname VARCHAR2(150) NOT NULL, -- 첨부파일 이름
bno NUMBER NOT NULL, -- 게시물 번호
regdate DATE DEFAULT SYSDATE, -- 업로드 날짜
PRIMARY KEY(fullname)
);
02) bno 칼럼에 외래키 설정
ALTER TABLE tbl_attach ADD CONSTRAINT fk_board_attch FOREIGN KEY(bno) REFERENCES tbl_board(bno);
tbl_board와 tbl_attach의 관계도
03) tbl_board 테이블의 bno컬럼을 위한 시퀀스 생성
CREATE SEQUENCE seq_board START WITH 1 INCREMENT BY 1;
게시판 입력처리를 할 때 tbl_board 테이블의 기본키인 bno컬럼에 서브쿼리
(SELECT NVL(MAX(bno)+1, 1)FROM tbl_board)
로 값을 생성해 입력했었다.
하지만 지금은 게시글 입력처리와 동시에 파일 업로드도 같이 처리해줘야하는데 이렇게 되면 bno컬럼을 참조하지 못하게 되므로 bno칼럼을 시퀀스로 값이 자동 증가하도록 처리해주었다.
2. 게시판에 파일첨부 기능 추가
DB에 업로드한 파일의 정보를 저장할 테이블을 생성했으니 이제 본격적으로 게시판 파일첨부 기능을 구현해보자.
01) View - write.jsp
먼저 게시글을 작성화면에 ajax방식으로 파일업로드를 할 수 있도록 뷰를 수정해보자.
html, style 영역
<style>
/* 첨부파일을 드래그할 영역의 스타일 */
.fileDrop {
width: 600px;
height: 70px;
border: 2px dotted gray;
background-color: gray;
}
</style>
<div>
첨부파일 등록
<!-- 첨부파일 등록영역 -->
<div class="fileDrop"></div>
<!-- 첨부파일의 목록 출력영역 -->
<div id="uploadedList"></div>
</div>
body에 첨부파일을 드래그해서 등록할 수 있는 영역과 첨부파일의 목록을 출력해주는 영역을
div
로 작성해준다.
javascript, jquery 영역
common.js
// 이미지 파일 여부 판단
function checkImageType(fileName){
var pattern = /jpg|gif|png|jpeg/i;
return fileName.match(pattern);
}
// 업로드 파일 정보
function getFileInfo(fullName){
var fileName, imgsrc, getLink, fileLink;
// 이미지 파일일 경우
if(checkImageType(fullName)){
// 이미지 파열 경로(썸네일)
imgsrc = "/spring02/upload/displayFile?fileName="+fullName;
console.log(imgsrc);
// 업로드 파일명
fileLink = fullName.substr(14);
console.log(fileLink);
// 날짜별 디렉토리 추출
var front = fullName.substr(0, 12);
console.log(front);
// s_를 제거한 업로드이미지파일명
var end = fullName.substr(14);
console.log(end);
// 원본이미지 파일 디렉토리
getLink = "/spring02/upload/displayFile?fileName="+front+end;
console.log(getLink);
// 이미지 파일이 아닐경우
} else {
// UUID를 제외한 원본파일명
fileLink = fullName.substr(12);
console.log(fileLink);
// 일반파일디렉토리
getLink = "/spring02/upload/displayFile?fileName="+fullName;
console.log(getLink);
}
// 목록에 출력할 원본파일명
fileName = fileLink.substr(fileLink.indexOf("_")+1);
console.log(fileName);
// { 변수:값 } json 객체 리턴
return {fileName:fileName, imgsrc:imgsrc, getLink:getLink, fullName:fullName};
}
파일업로드에 관련하여 공통으로 사용할 javascript함수들을 따로 모듈화해서 include폴더에 js파일로 작성해주었다. 파일 업로드를 구현하는 페이지마다 공통으로 들어가는 함수들이기때문에 중복코드를 최대한 줄이기 위함이다. 페이지 상단에
<script type="text/javascript" src="${path}/include/js/common.js"></script>
를 작성해주면 페이지가 로드될때 같이 들어가게된다.
$(document).ready(function(){
// 파일 업로드 영역에서 기본효과를 제한
$(".fileDrop").on("dragenter dragover", function(e){
e.preventDefault(); // 기본효과 제한
});
// 드래그해서 드롭한 파일들 ajax 업로드 요청
$(".fileDrop").on("drop", function(e){
e.preventDefault(); // 기본효과 제한
var files = e.originalEvent.dataTransfer.files; // 드래그한 파일들
var file = files[0]; // 첫번째 첨부파일
var formData = new FormData(); // 폼데이터 객체
formData.append("file", file); // 첨부파일 추가
$.ajax({
url: "${path}/upload/uploadAjax",
type: "post",
data: formData,
dataType: "text",
processData: false, // processType: false - header가 아닌 body로 전달
contentType: false,
// ajax 업로드 요청이 성공적으로 처리되면
success: function(data){
console.log(data);
// 첨부 파일의 정보
var fileInfo = getFileInfo(data);
// 하이퍼링크
var html = "<a href='"+fileInfo.getLink+"'>"+fileInfo.fileName+"</a><br>";
// hidden 태그 추가
html += "<input type='hidden' class='file' value='"+fileInfo.fullName+"'>";
// div에 추가
$("#uploadedList").append(html);
}
});
});
파일을 드래그해서 드롭하면 바로 파일이 실행되는 이벤트를 제한하기 위해
preventDefault()
를 작성해준다. ajax 업로드 요청이 성공적으로 처리되면 common.js의getFileInfo()
함수를 호출하여 첨부파일의 정보를 가져와<a>
태그에 파일의 디렉토리를 링크해주고 원본파일명을 출력해준다.
입력 처리과정에서 날짜별 디렉토리를 포함한 파일명을 DB에 저장하기 위해<input>
태그의 value속성에 파일의 날짜별 디렉토리를 포함한 파일명을 넣어준다. 그리고 화면에 보이지 않게 하기 위해 hidden으로 처리해준다.
마지막으로 모든과정을 처리하고 나서 업로드할 파일을 목록에 출력해준다.
02) Model(DB연동 작업처리)
BoardVO
private String[] files;
BoardVO에 게시글의 첨부파일의 이름을 추가한다.
getter, setter, toString도 함께 추가한다.
boardMapper.xml
파일 업로드처리 구현 전의 게시글 입력 쿼리
<insert id="insert">
<!-- 게시글 번호를 서브쿼리로 생성 -->
INSERT INTO tbl_board (
bno, title, content, writer, show
) VALUES (
(SELECT NVL(MAX(bno)+1, 1)FROM tbl_board), #{title}, #{content}, #{writer}, 'y'
)
</insert>
파일 업로드처리 구현 후의 게시글 입력 쿼리
<insert id="insert">
<!-- 게시글 번호를 시퀀스로 생성 -->
INSERT INTO tbl_board (
bno, title, content, writer, show
) VALUES (
seq_board.NEXTVAL, #{title}, #{content}, #{writer}, 'y'
)
</insert>
앞서 sql을 작성할 때 말했듣이 이전의 게시글 insert에서는 게시글 번호를 서브쿼리로 생성해주었지만 파일업로드를 같이 처리하는 게시글 insert는 시퀀스로 게시글 번호를 생성하도록 변경하였다.
그 이유는 게시글과 파일정보는 동시에 입력 되어야하는데 서브쿼리를 사용하게 되면 파일의 정보를 입력할 때 게시글의 번호를 참조할 수가 없어지기 때문이다.
파일의 정보 입력 쿼리
<insert id="addAttach">
INSERT INTO tbl_attach (
fullname, bno
) VALUES (
#{fullName}, seq_board.CURRVAL
)
</insert>
CURRVAL
은 현재 시퀀스의 값을 의미한다.
BoardDAOImpl(BoardDAO인터페이스를 구현 클래스)
@Override
public void addAttach(String fullName) {
sqlSession.insert("board.addAttach", fullName);
}
파일업로드 메서드 추가
03) service(비지니스 로직, 핵심업무를 처리)
BoardService(BoardService인터페이스를 구현한 클래스)
@Transactional // 트랜잭션 처리 메서드로 설정
@Override
public void create(BoardVO vo) throws Exception {
String title = vo.getTitle();
String content = vo.getContent();
String writer = vo.getWriter();
title = title.replace("<", "<");
title = title.replace("<", ">");
writer = writer.replace("<", "<");
writer = writer.replace("<", ">");
title = title.replace(" ", " ");
writer = writer.replace(" ", " ");
content = content.replace("\n", "<br>");
vo.setTitle(title);
vo.setContent(content);
vo.setWriter(writer);
boardDao.create(vo);
// 게시물의 첨부파일 정보 등록
String[] files = vo.getFiles(); // 첨부파일 배열
if(files == null) return; // 첨부파일이 없으면 메서드 종료
// 첨부파일들의 정보를 tbl_attach 테이블에 insert
for(String name : files){
boardDao.addAttach(name);
}
}
게시글 입력처리 메서드에 게시물의 첨부파일 정보를 등록하는 로직을 추가한다.
첨부파일이 없으면 메서드를 종료시키고, 첨부파일의 갯수만큼 반복문을 수행한다.
04) Controller(흐름제어) - BoardController(변경사항 없음)
05) 구현화면 확인
파일 업로드 및 파일 정보를 크롬개발자 도구에서 확인
파일 저장 디렉토리에서 업로드가 되었는지 확인
2. 게시글 상세 및 수정화면 - 첨부파일 목록, 다운로드, 삭제, 새로운 첨부파일 올리기
이제는 게시글의 상세화면과 수정화면에서 첨부파일을 목록을 보여주고 첨부파일을 다운로드하거나 삭제할 수 있도록 구현해보자. 그리고 수정화면에서는 새로운 첨부파일을 올릴 수 있게 처리해보자. 전에 구현했던 내용과 크게 다른 내용이 없으므로 자세한 설명보다는 코드에 주석을 달아 놓았다.
01) View(게시글 상세화면) - view.jsp
html, style영역
<style>
#fileDrop {
width: 600px;
height: 80px;
border: 1px solid gray;
background-color: gray;
}
</style>
</head>
<body>
<!-- 첨부파일 목록 -->
<div>
첨부파일
<div id="uploadedList"></div>
</div>
<!-- 첨부파일을 드래그할 영역 -->
<div>
<div id="fileDrop"></div>
</div>
</body>
</html>
게시글 입력페이지와 동일하게 코드를 작성해준다.
<form>
태그 id속성에 form1을 추가해준다.
javascript, jquery영역
jquery
$(document).ready(function(){
// 1. 첨부파일 목록 불러오기 함수 호출
listAttach();
// 2. 첨부파일 추가 ajax요청
$("#fileDrop").on("dragenter dragover", function(e){
e.preventDefault(); // 기본효과 제한
});
$("#fileDrop").on("drop", function(e){
e.preventDefault(); // 기본효과 제한
var files = e.originalEvent.dataTransfer.files; // 드래그한 파일들
//console.log(files);
var file = files[0]; // 첫번째 첨부파일
var formData = new FormData(); // 폼데이터 객체
formData.append("file", file); // 첨부파일 추가
$.ajax({
url: "${path}/upload/uploadAjax",
type: "post",
data: formData,
dataType: "text",
processData: false, // processType: false - header가 아닌 body로 전달
contentType: false,
success: function(data){
console.log(data);
// 첨부 파일의 정보
var fileInfo = getFileInfo(data);
// 하이퍼링크
var html = "<a href='"+fileInfo.getLink+"'>"+fileInfo.fileName+"</a><br>";
// hidden 태그 추가
html += "<input type='hidden' class='file' value='"+fileInfo.fullName+"'>";
// div에 추가
$("#uploadedList").append(html);
}
});
});
// 3. 첨부파일 삭제 ajax요청
// 태그.on("이벤트", "자손태그", 이벤트 핸들러)
$("#uploadedList").on("click", ".fileDel", function(e){
var that = $(this); // 클릭한 a태그
$.ajax({
type: "post",
url: "${path}/upload/deleteFile",
data: {fileName: $(this).attr("data-src")},
dataType: "text",
success: function(result){
if(result == "deleted"){
that.parent("div").remove();
}
}
});
});
// 4. 게시글 수정버튼 클릭 이벤트 처리
$("#btnUpdete").click(function(){
var title = $("#title").val();
var content = $("#content").val();
if(title == ""){
alert("제목을 입력하세요");
document.form1.title.focus();
return;
}
if(content == ""){
alert("내용을 입력하세요");
document.form1.content.focus();
return;
}
document.form1.action="${path}/board/update.do"
// 첨부파일 이름을 form에 추가
var that = $("#form1");
var str = "";
// 태그들.each(함수)
// id가 uploadedList인 태그 내부에 있는 hidden태그들
$("#uploadedList .file").each(function(i){
str += "<input type='hidden' name='files["+i+"]' value='"+$(this).val()+"'>";
});
// form에 hidden태그들을 추가
$("#form1").append(str);
// 폼에 입력한 데이터를 서버로 전송
document.form1.submit();
});
});
// 첨부파일 목록 ajax요청 처리
function listAttach(){
$.ajax({
type: "post",
url: "${path}/board/getAttach/${dto.bno}",
success: function(list){
$(list).each(function(){
// each문 내부의 this : 각 step에 해당되는 값을 의미
var fileInfo = getFileInfo(this);
// a태그안에는 파일의 링크를 걸어주고, 목록에는 파일의 이름 출력
var html = "<div><a href='"+fileInfo.getLink+"'>"+fileInfo.fileName+"</a> ";
// 삭제 버튼
html += "<a href='#' class='fileDel' data-src='"+this+"'>[삭제]</a></div>"
$("#uploadedList").append(html);
});
}
});
}
02) Model(DB연동 작업처리)
boardMapper.xml
<!-- 게시물 첨부파일 추가 -->
<insert id="addAttach">
<!-- CURRVAL : 현재의 값 -->
INSERT INTO tbl_attach (
fullname, bno
) VALUES (
#{fullName}, seq_board.CURRVAL
)
</insert>
<!-- 게시글의 첨부파일 업데이트처리(입력처리) -->
<insert id="updateAttach">
INSERT INTO tbl_attach (
fullname, bno
) VALUES (
#{fullName}, #{bno}
)
</insert>
<!-- 게시글의 첨부파일 삭제처리 -->
<delete id="deleteAttach">
DELETE FROM tbl_attach WHERE fullname = #{fullname}
</delete>
boardDAOImpl(boardDAO인터페이스를 구현한 클래스)
// 게시글 첨부파일 목록
@Override
public List<String> getAttach(int bno) {
return sqlSession.selectList("board.getAttach", bno);
}
// 게시글 첨부파일 수정처리
@Override
public void updateAttach(String fullName, int bno) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("fullName", fullName);
map.put("bno", bno);
sqlSession.insert("board.updateAttach", map);
}
// 게시글 첨부파일 삭제처리
@Override
public void deleteFile(String fullname) {
sqlSession.delete("board.deleteAttach", fullname);
}
03) Service(비지니스 로직, 핵심업무 처리) - BoardServiceImpl(BoardService인터페이스를 구현한 클래스)
// 게시글의 첨부파일 목록
@Override
public List<String> getAttach(int bno) {
return boardDao.getAttach(bno);
}
// 게시글 수정
@Transactional
@Override
public void update(BoardVO vo) throws Exception {
boardDao.update(vo);
// 첨부파일 정보 등록
String[] files = vo.getFiles(); // 첨부파일 배열
// 첨부파일이 없으면 종료
if(files == null) return;
// 첨부파일들의 정보를 tbl_attach 테이블에 insert
for(String name : files){
boardDao.updateAttach(name, vo.getBno());
}
}
// 게시글의 첨부파일 삭제 처리
@Override
public void deleteFile(String fullname) {
boardDao.deleteFile(fullname);
}
게시글 입력처리와 마찬가지로 수정처리시에도 첨부파일이 없으면 메서드를 종료하고, 있으면 갯수만큼 반복문을 수행하여 테이블에 insert해준다.
04) Controller(흐름제어)
BoardController(게시판 관련 컨트롤러)
// 게시글 첨부파일 목록 매핑
@RequestMapping("/getAttach/{bno}")
@ResponseBody // view가 아닌 data를 리턴
public List<String> getAttach(@PathVariable("bno") int bno){
return boardService.getAttach(bno);
}
UploadController(업로드 관련 컨트롤러)
파일 삭제 매핑
@ResponseBody // view가 아닌 데이터 리턴
@RequestMapping(value = "/upload/deleteFile", method = RequestMethod.POST)
public ResponseEntity<String> deleteFile(String fileName) {
// 파일의 확장자 추출
String formatName = fileName.substring(fileName.lastIndexOf(".") + 1);
// 이미지 파일 여부 검사
MediaType mType = MediaUtils.getMediaType(formatName);
// 이미지의 경우(썸네일 + 원본파일 삭제), 이미지가 아니면 원본파일만 삭제
// 이미지 파일이면
if (mType != null) {
// 썸네일 이미지 파일 추출
String front = fileName.substring(0, 12);
String end = fileName.substring(14);
// 썸네일 이미지 삭제
new File(uploadPath + (front + end).replace('/', File.separatorChar)).delete();
}
// 원본 파일 삭제
new File(uploadPath + fileName.replace('/', File.separatorChar)).delete();
// 레코드 삭제
boardService.deleteFile(fileName);
// 데이터와 http 상태 코드 전송
return new ResponseEntity<String>("deleted", HttpStatus.OK);
}
파일을 삭제처리하면서 동시에 DB의 파일레코드도 함께 삭제처리