검색 > 페이징 > Ajax > 파일 업/다운로드
1. 파일 업로드를 위해 DB 설계 (이미지 파일)
create table board_img(
file_seq number not null primary key, -- PK명
real_name varchar(500) not null, -- 원래 파일명
save_name varchar(500) not null, -- 서버에 저장될 파일명
reg_date date not null, -- 업로드 날짜
save_path varchar(500) not null, -- 서버에 저장될 경로
board_seq number not null, -- FK명
constraint board_file foreign key(board_seq) references board (board_seq)
);
2. 사전 설정
<!-- pom.xml -->
<!-- 서블릿 3.0이상 사용 가능한 파일 업로드 api-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<!-- commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<!-- Servlet-context.xml -->
<!-- 파일 업로드 -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="maxUploadSize" value="10485760" />
<beans:property name="defaultEncoding" value="utf-8" />
</beans:bean>
3. ImgMapper 생성 및 작성
<?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="ImgMapper">
<resultMap id='imgMap' type='java.util.HashMap'>
<result column='FILE_SEQ' property='fileSeq' />
<result column='REAL_NAME' property='realName' />
<result column='SAVE_NAME' property='saveName' />
<result column='REG_DATE' property='regDate' />
<result column='SAVE_PATH' property='savePath' />
<result column='BOARD_SEQ' property='boardSeq' />
</resultMap>
<insert id='uploadImg'>
INSERT INTO BOARD_IMG(
FILE_SEQ,
REAL_NAME,
SAVE_NAME,
REG_DATE,
SAVE_PATH,
BOARD_SEQ
) VALUES (
(SELECT NVL(MAX(FILE_SEQ),0)+1 FROM BOARD_IMG),
#{realName},
#{saveName},
sysdate,
#{savePath},
#{boardSeq}
)
</insert>
</mapper>
3. BoardMapper 작성
<select id='getMaxSeq' resultType='int'>
select
nvl(max(board_seq), 0) + 1
from board
</select>
이미지 정보를 넣어줄 때 게시글 번호를 같이 업로드를 해줘야 하니 게시글 번호를 반환해주는 쿼리를 작성해 줍니다.
4. Img Dao, DaoImpl 작성
// Img Dao
void uploadImg(List<Map<String, Object>> imgList);
// Img DaoImpl
@Autowired
private SqlSessionTemplate sqlSession;
@Override
public void uploadImg(List<Map<String, Object>> imgList) {
for(Map<String, Object> imgMap : imgList) {
sqlSession.insert("ImgMapper.uploadImg", imgMap);
}
}
5. Img Service, ServiceImpl 작성
// Img Service
void uploadImg(List<MultipartFile> imgs, int boardSeq);
// Img ServiceImpl
@Service
public class ImgServiceImpl implements ImgService{
@Autowired
private ImgDao imgDao;
@Override
public void uploadImg(List<MultipartFile> imgs, int boardSeq) {
List<Map<String, Object>> imgList = new ArrayList<Map<String,Object>>();
for(MultipartFile file : imgs) {
String ariginalFileName = file.getOriginalFilename();
String extension = ariginalFileName.substring(ariginalFileName.lastIndexOf("."));
String savedFileName = UUID.randomUUID().toString()+extension;
Map<String, Object> imgMap = new HashMap<String, Object>();
imgMap.put("saveName", savedFileName);
imgMap.put("realName", ariginalFileName);
imgMap.put("boardSeq", boardSeq);
FileUpload fileUpload = new FileUpload();
fileUpload.fileUpload(file, savedFileName);
imgMap.put("savePath", fileUpload.getUploadPath());
imgList.add(imgMap);
}
imgDao.uploadImg(imgList);
}
}
이미지 정보들을 서버에 저장할 이름으로 가공해서 DB에 insert 해줍니다.
6. Board Dao, DaoImpl 작성
// Board Dao
// 이전 코드
int getMaxSeq();
// Board DaoImpl
// 이전 코드
@Override
public int getMaxSeq() {
return sqlsession.selectOne("BoardMapper.getMaxSeq");
}
7. Board ServiceImpl 수정
/*
이전 코드
*/
@Autowired
private ImgService imgSer;
/*
중간 코드
*/
@Override
public void createBoard(Map<String, Object> param) {
Map<String, Object> boardMap = new HashMap<String, Object>();
boardMap.put("writer", param.get("writer"));
boardMap.put("title", param.get("title"));
boardMap.put("content", param.get("content"));
int boardSeq = boardDao.getMaxSeq();
boardDao.createBoard(param);
@SuppressWarnings("unchecked")
List<MultipartFile> imgFiles = (List<MultipartFile>) param.get("imgs");
imgSer.uploadImg(imgFiles, boardSeq);
}
게시글을 업로드 하기 전에 올라갈 게시글 번호를 저장한 후에 업로드를 해줍니다. 그러고나서 param에 담긴 이미지를 가공 후 업로드 할 수 있게 해줍니다.
8. Controller 수정
@RequestMapping(value="/create", method = RequestMethod.POST)
public String createPost(@RequestParam Map<String, Object> param, @RequestPart("imgs") List<MultipartFile> imgs) {
param.put("imgs", imgs);
boardSer.createBoard(param);
return "redirect:/list";
}
View로부터 전달받은 이미지 목록들(imgs)을 같이 받은 게시글 정보들(param)에 담아서 7번의 과정을 수행할 수 있도록 해줍니다.
9. create.jsp 수정
<form enctype="multipart/form-data" id='createForm'>
<table>
<tr>
<th>작성자</th>
<td><input type='text' id='writer' name='writer'></td>
</tr>
<tr>
<th>글 제목</th>
<td><input type='text' id='title' name='title'></td>
</tr>
<tr>
<th rowspan="2">글 내용</th>
<td rowspan="2"><textarea id='content' name='content'></textarea></td>
</tr>
</table>
<br>
<button id='plsImg'>이미지 등록</button>
<div id='imgPlace'></div>
<br>
<button id='createBtn'>글쓰기</button>
<button onclick='goToPreviousPage(event)'>이전</button>
</form>
<form> 태그 안에 이미지를 업로드 할 수 있도록 enctype="multipart/form-data"를 추가해주도록 합니다. 그리고 이미지를 한 개 씩 여러 개 올릴 수 있도록 버튼과 영역을 생성해 줍니다.
10. JQeury 작성
$(function(){
$('#createBtn').on('click',function(event){
event.preventDefault();
var chkWriter = $('#writer').val();
var chkTitle = $('#title').val();
if(chkTitle == '' || chkWriter == ''){
alert('작성하지 않은 내용이 있습니다.');
} else {
var files = $("[name=imgs]")[0].files;
var checks = [];
for (var i = 0; i < files.length ; i++){
checks.push(checkImg(files[i]));
}
Promise.all(checks).then(function(results) {
var exChk = results.reduce((sum, curr) => sum + curr, 0);
if (exChk === 0) {
$('#createForm').attr({
'action' : '/create',
'method' : 'post'
}).submit();
} else {
event.preventDefault();
alert("이미지의 크기가 너무 큽니다. 가로세로 500px을 넘기지 마세요.");
}
});
}
});
$("#plsImg").on("click",function(event){
event.preventDefault();
var putImg = '<input type="file" id="imgBtn" name="imgs" accept="image/*" value="파일 찾기"> <button id="delImgPut">-</button><br>';
$("#imgPlace").append(putImg);
});
$("#imgPlace").on("click", "#delImgPut", function(event){
event.preventDefault();
$(this).prev().remove(); // remove the input[type=file]
$(this).next().remove(); // remove the <br>
$(this).remove(); // remove the input[type=button]
});
})
function goToPreviousPage(event) {
event.preventDefault();
window.history.back();
}
function checkImg(file) {
return new Promise(function(resolve, reject) {
var _URL = window.URL || window.webkitURL;
var img = new Image();
img.onload = function() {
if (this.width > 500 || this.height > 500) {
resolve(1);
} else {
resolve(0);
}
};
img.onerror = function() {
reject(new Error("Image Load Error"));
};
img.src = _URL.createObjectURL(file);
});
}
일단 이미지 등록 버튼을 눌렀을 때, 이미지 업로드 구역 + 영역 지우기 버튼이 생기도록 해줍니다. ($("#plsImg").on("click",function(event){ ... });)
영역 지우기 버튼을 누르면 그 앞의 태그, 본인, 다음 태그까지 지우도록 해줍니다. ($("#imgPlace").on("click", "#delImgPut", function(event){ ... });)
업로드를 할 때, 이미지의 크기를 체크하는 부분을 추가해주도록 합니다. (성공 시 업로드, 실패 시 alert) 이 부분이 이번 프로젝트에서 제일 어려웠습니다..
11. FileUpload 작성
package com.JoAri.CRUD.img;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import org.springframework.web.multipart.MultipartFile;
public class FileUpload {
private static final String UPLOAD_PATH = "C:\\Work\\temporary\\File_Practice\\"; // 업로드할 파일 경로
public void fileUpload(MultipartFile file, String savedFileName) {
if (!file.isEmpty()) {
try {
Path uploadPath = new File(UPLOAD_PATH + savedFileName).toPath();
Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
// 예외 처리
}
}
}
public String getUploadPath() {
return UPLOAD_PATH;
}
}
실질적으로 이미지가 저장될 공간과 어떤 형태(위에선 경로+저장될 파일명)로 저장될 지 예외 시엔 어떻게 할 지 지정해 주는 클래스를 만들어 줍니다. (Img Service 에서 사용)
12. 결과
후기
휴... 이 번엔 글에서 보기와는 다르게 여기저기 왔다갔다 하며 코딩하고 고민도 하느라 시간이 조금 오래 걸렸습니다.
이번엔 개인적으로 의문점이 생기는 시간이었습니다. 게시글을 올리고 이미지를 올렸는데 이미지 업로드가 실패하면? 게시글을 보는 사람은 이미지를 제대로 볼 수가 없지 않나? 싶었습니다. 그러면 이미지부터 올리면 되는데 그러면 DataBase에서의 FK 때문에 위배되기 때문에 그 또한 안 되지 않나 싶었습니다.
보통은 어떻게 해결하나요??
다음 시간에 뵙겠습니다. 빠잇~!
'Spring > CRUD Project' 카테고리의 다른 글
[CRUD] 파일 다운로드 (1) | 2023.12.06 |
---|---|
[CRUD] 파일 업로드 및 조회 2 : 수정 및 출력 (1) | 2023.12.05 |
[CRUD] Ajax : 비동기 통신 (1) | 2023.11.30 |
[CRUD] 페이징 (0) | 2023.11.29 |
[CRUD] 검색 (1) | 2023.11.28 |