Spring Framework
업데이트:
Attachment Model
패키지, 파일 생성
게시글 등록(+ 파일 업로드)
- 글쓰기 클릭 시 요청 주소
- boardList.jsp
<a class="btn btn-success float-right" href="../${pInfo.boardType}/insert">글쓰기</a>
- BoardController
// 게시글 등록 화면 전환용 Controller
@RequestMapping("{type}/insert")
public String insertView(@PathVariable("type") int type) {
return "board/boardInsert" + type;
// boardInsert1.jsp == 기존 방식
// boardInsert2.jsp == summernote 적용 방식
}
- 등록버튼 클릭 시 요청 주소
- boardInsert.jsp
<form action="insertAction" method="post" enctype="multipart/form-data" role="form" onsubmit="return validate();">
- Spring에서는 파일 업로드를 위한 MultipartResolver라는 인터페이스를 제공하고 있음.
- 실제 파일 업로드를 하기 위해서는 MultipartResolver의 구현체가 필요함.
MultipartResolver 구현체를 생성하는 방법 두 가지
- Servlet 3.0 이상 버전에서 MultipartRequest를 사용해 StandardServletMultipartResolver를 생성
- Apache Commons FileUpload 라이브러리를 이용해 CommonsMultipartResolver를 구현
스프링에서 라이브러리 추가하는 방법
- Maven을 이용하여 commons-fileupload 라이브러리 추가 -> mavenrepository에서 검색 후 pom.xml에 추가
- root-context.xml 파일에 CommonsMultipartResolver를 bean으로 등록하는 코드 추가
- pom.xml
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
- root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- 스프링에서 사용하는 proxy를 이용한 트랜잭션 제어가 안될 경우 추가적인 트랜잭션 매니저를 추가해서 문제 해결 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven proxy-target-class="true"/>
<!-- 파일 업로드를 위한 CommonsMultipartResolver 객체 bean 등록 -->
<!-- MultipartResolver를 bean으로 등록하면 multipart/form-data 형식의 요청을 받게될 경우
스프링 컨테이너가 해당 bean을 제어하여 input type="file" 태그를 별도로 얻어와서 처리하여
MultipartFile 객체로 반환해줌.
- input type="file"에 파일 업로드 여부와 상관없이 무조건 얻어옴
- 태그 하나 또는 업로드된 파일 하나 당 MultipartFile 객체 하나가 반환됨.
- MultipartResolver가 파일 관련된 내용(multipart/form-data)를 다 처리하므로
나머지 일반 텍스트 형태의 데이터를 별도의 처리 없이 사용 가능함.
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="104857600"/>
<property name="maxUploadSizePerFile" value="20971520"/>
<property name="maxInMemorySize" value="104857600"/>
</bean>
<!--
maxUploadSize : 한 요청당 업로드가 허용되는 최대 용량(바이트 단위)
maxUploadSizePerFile : 한 파일당 업로드가 허용되는 최대 용량(바이트 단위)
maxInMemorySize : 파일을 디스크에 저장하지 않고 메모리에 유지하도록 허용하는 최대 용량(바이트 단위)
지정된 용량보다 업로드 파일이 더 큰 경우 파일로 저장함.
(기본값 10240byte)
8bit == 1byte
1024byte == 1KB
1024KB == 1MB
104857600 byte == 100MB
20971520 byte == 20MB
-->
</beans>
- BoardController
// 게시글 등록(+ 파일 업로드) Controller
@RequestMapping("{type}/insertAction")
public String insertAction(@PathVariable("type") int type,
@ModelAttribute Board board,
@ModelAttribute("loginMember") Member loginMember,
@RequestParam(value="images", required = false) List<MultipartFile> images,
HttpServletRequest request,
RedirectAttributes ra) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("memberNo", loginMember.getMemberNo());
map.put("boardTitle", board.getBoardTitle());
map.put("boardContent", board.getBoardContent());
map.put("categoryCode", board.getCategoryNm());
map.put("boardType", type);
String savePath = request.getSession().getServletContext().getRealPath("resources/uploadImages");
int result = service.insertBoard(map, images, savePath);
String url = null;
if(result > 0){
swalIcon = "success";
swalTitle = "게시글 등록 성공";
url = "redirect:" + result;
}else {
swalIcon = "error";
swalTitle = "";
url = "redirect:insert";
}
ra.addFlashAttribute("swalIcon", swalIcon);
ra.addFlashAttribute("swalTitle", swalTitle);
return url;
}
- boardService
/** 게시글 등록(+ 파일 업로드) Service
* @param map
* @param savePath
* @param images
* @return
*/
public abstract int insertBoard(Map<String, Object> map, List<MultipartFile> images, String savePath);
- boardServiceImpl
// 게시글 삽입 Service 구현
@Transactional(rollbackFor = Exception.class)
@Override
public int insertBoard(Map<String, Object> map, List<MultipartFile> images, String savePath) {
int result = 0; // 최종 결과 저장 변수 선언
// 1) 게시글 번호 얻어오기 -> SEQ_BNO.NEXTVAL를 통해 얻어옴
int boardNo = dao.selectNextNo();
// 2) 게시글 삽입
if(boardNo > 0) { // 다음 게시글 번호를 얻어온 경우
map.put("boardNo", boardNo); // map에 boardNo 추가
// 크로스 사이트 스크립팅 방지 처리
// 추후 summernote api 사용을 염두하여
// 게시판 타입별로 크로스 사이트 스크립팅 방지 처리를 선택적으로 진행
if( (int)map.get("boardType") == 1) { // 자유게시판인 경우
String boardTitle = (String)map.get("boardTitle");
String boardContent = (String)map.get("boardContent");
// 크로스 사이트 스크립팅 방지 처리 적용
boardTitle = replaceParameter(boardTitle);
boardContent = replaceParameter(boardContent);
// 처리된 문자열을 다시 map에 세팅
map.put("boardTitle", boardTitle);
map.put("boardContent", boardContent);
// 개행문자 처리 -> 화면에서 JSTL을 이용해서 처리할 예정
}
// 게시글 삽입 DAO 메소드 호출
result = dao.insertBoard(map);
// 3) 게시글 삽입 성공 시 이미지 정보 삽입
if(result > 0) {
// 이미지 정보를 Attachment객체에 저장 후 List에 추가
List<Attachment> uploadImages = new ArrayList<Attachment>();
// DB에 저장할 웹상 접근 주소(filePath)
String filePath = "/resources/uploadImages";
// for문을 이용하여 파일 정보가 담긴 images를 반복접근
// -> 업로드된 파일이 있을 경우에만 uploadImages 리스트에 추가
for(int i=0; i<images.size(); i++) {
// i == 인덱스 == fileLevel과 같은 값
// 현재 접근한 images의 요소(MultipartFile)에 업로드된 파일이 있는지 확인
if( !images.get(i).getOriginalFilename().equals("") ) {
// 파일이 업로드 된 경우 == 업로드 된 원본 파일명이 있는 경우
// 원본 파일명 변경
String fileName = rename(images.get(i).getOriginalFilename());
// Attachment 객체 생성
Attachment at = new Attachment(filePath, fileName, i, boardNo);
uploadImages.add(at); // 리스트에 추가
}
}
if(!uploadImages.isEmpty()) { // 업로드 된 이미지가 있을 경우
// 파일 정보 삽입 DAO 호출
result = dao.insertAttachmentList(uploadImages);
// result == 삽입된 행의 개수
// 모든 데이터가 정상 삽입 되었을 경우 --> 서버에 파일 저장
if(result == uploadImages.size()) {
result = boardNo; // result에 boardNo 저장
// MultipartFile.transferTo
// -> MultipartFile 객체에 저장된 파일을
// 지정된 경로에 실제 파일의 형태로 변환하여 저장하는 메소드
for(int i=0; i<uploadImages.size(); i++) {
try {
images.get(uploadImages.get(i).getFileLevel())
.transferTo(new File(savePath + "/" + uploadImages.get(i).getFileName()));
}catch (Exception e) {
e.printStackTrace();
throw new InsertAttachmentFailException("파일 서버 저장 실패");
}
}
}else { // 파일 정보를 DB에 삽입하는데 실패했을 때
throw new InsertAttachmentFailException("파일 정보 DB 삽입 실패");
}
}
}
}
return result;
}
// 크로스 사이트 스크립트 방지 처리 메소드
private String replaceParameter(String param) {
String result = param;
if(param != null) {
result = result.replaceAll("&", "&");
result = result.replaceAll("<", "<");
result = result.replaceAll(">", ">");
result = result.replaceAll("\"", """);
}
return result;
}
// 파일명 변경 메소드
public String rename(String originFileName) {
SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss");
String date = sdf.format(new java.util.Date(System.currentTimeMillis()));
int ranNum = (int)(Math.random()*100000); // 5자리 랜덤 숫자 생성
String str = "_" + String.format("%05d", ranNum);
String ext = originFileName.substring(originFileName.lastIndexOf("."));
return date + str + ext;
}
- BoardDAO
/** 게시글 삽입 DAO
* @param map
* @return result
*/
public int insertBoard(Map<String, Object> map) {
return sqlSession.insert("boardMapper.insertBoard", map);
}
/** 파일 정보 삽입 DAO
* @param uploadImages
* @return result(성공한 행의 개수)
*/
public int insertAttachmentList(List<Attachment> uploadImages) {
return sqlSession.insert("boardMapper.insertAttachmentList", uploadImages);
}
- board-mapper.xml
<!-- 파일 정보 삽입(List 이용) -->
<insert id="insertAttachmentList" parameterType="list">
INSERT INTO ATTACHMENT
SELECT SEQ_FNO.NEXTVAL, A.* FROM (
<foreach collection="list" item="item" separator="UNION ALL">
SELECT #{item.filePath} FILE_PATH,
#{item.fileName} FILE_NAME,
#{item.fileLevel} FILE_LEVEL,
#{item.parentBoardNo} PARENT_BOARD_NO
FROM DUAL
</foreach>
)A
</insert>
댓글남기기