00. 화면 만들기
- write.jsp 준비
404 : 서블릿, 핸들러, 페이지가 없는 상태 스프링에서 처리할 매핑 처리된 메소드(핸들러) 자체가 없는 것
405 : 요청을 처리하는 애는 있지만, 해당 POST나 GET을 요청을 처리할 수 있는 핸들러가 없는 것
01. BoardController.java
- get 요청 처리할 수 있는 @GetMapping 만들기
- 지금은 로그인 하지 않아도 직접 URL에 /board/write 치면 글쓰기 화면으로 넘어가는 상태
- 따라서, Interceptor를 적용해야 함.
02. servlet-context.xml
- /board/write 추가
- 이 요청에 대해서만 Interceptor를 적용할 것이다.
<mapping path="/board/write"/>
- 로그인 안한 상태로 URL 입력하면 Interceptor에 지정한 대로 경고 메시지 출력되는지 확인
03. BoardController.java
- post 요청 할 수 있는 @PostMapping 만들기
- 매개변수에 ModelAndView와 @ModelAttribue 작성 (단, @ModelAttribue 사용할 때는, 해당 객체에 기본 생성자, setter가 꼭 있어야 하고 jsp에서 넘겨주는 name 속성과 해당 객체의 필드명이 동일해야 한다. 잘 확인할 것!)
@PostMapping("/write")
public ModelAndView wrtie(ModelAndView model, @ModelAttribute Board board){
model.setViewName("/board/write");
return model;
}
- multipart/form-data가 사용자가 POST 날리는 곳에서 없으면! => 입력한 값대로 나오지만, multipart/form-data가 있으면 JVM에 의해 기본값들만(0 아니면 null) 출력된다.
- 기존 Servlet 방식에서도 일반적인 request 방식으로는 값을 가져올 수 없어서 라이브러리를 추가했었다. (=> MultipartRequest)
04. pom.xml
- <form enctype="multipart/form-data"> 를 처리할 수 있는 라이브러리 추가하자.
https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
- 1.4 버전 Copy! -> pom.xml 에 추가
- 이 라이브러리에서 사용하는 또 다른 라이브러리 추가
https://mvnrepository.com/artifact/commons-io/commons-io
- 2.11.0 버전 Copy! -> pom.xml 에 추가
- 대용량 처리에 용이한 라이브러리이다.
- BUT 이렇게만 하는 것은 파일 업로드 관련한 라이브러리이고, 실제로 사용하려면 MultipartResolver를 빈으로 등록해야 한다.
05. Spring Bean Definication file - multipart-context.xml 파일 만들기
- src > main > webapp > WEB-INF > spring 밑에 생성(beans 최신버전 + c, p 네임스페이스 추가)
- 파일 업로드 시 사용할 MultipartResolver 등록
p:maxUploadSize : 최대 업로드 파일 크기를 지정(10MB)
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
p:maxUploadSize="10485760"
/>
- 스프링 프레임워크에서는 파일 업로드를 위해서 MultipartResolver 인터페이스가 정의되어있다.
- 파일 업로드 시에는 MultipartResolver 구현한 구현체를 통해서 하면 된다.
이 때, 구현체는 크게 2가지가 있다.
1) CommonsMultipartResolver (내부적으로 2개의 라이브러리가 꼭 필요하다. 보통 더 많이 사용된다.)
2) StandardServletMultipartResolver (서블릿 3.0 이상부터 제공되는 API)
06. root-context.xml
<import resource="multipart-context.xml"/>
추가
여기까지하면, Multipart form data로 요청하는 데이터도 MultipartReuqest를 직접 만드는 것이 아니라, MultipartResolver에 의해서 자동으로 주입된다.
이제부터 파일 업로드 부분
07. BoardController.java
- wirte() 의 매개변수에 추가 @RequestParam("upfile") MultipartFile upfile -> upfile은 jsp쪽의 form 데이터 안에 업로드하는 첨부파일을 가르키는 name 속성의 이름이다.
// 파일을 업로드하지 않으면 빈문자열 (""), 파일을 업로드하면 "파일명"
log.info("Upfile Name : {}", upfile.getOriginalFilename());
// 파일을 업로드하지 않으면 true, 파일을 업로드하면 false
log.info("Upfile is Empty : {}", upfile.isEmpty());
이렇게 출력해보면,
1) 첨부파일을 업로드하지 않고, 글작성 버튼을 누른 경우
(빈문자열)
true
2) 첨부파일을 업로드하고 글작성 버튼을 누른 경우
파일명
false
- 여기까지만 하는것은, 파일을 실제 저장한 상태가 아니라 메모리에 담아만 놓고 사용자가 보낸 파일의 정보만 보고 있는 상태이기 때문에 추가 로직이 필요하다.
1. 만약에 검색 필터를 적용한 Board를 구현하고 싶다면?
* BoardMapper.java
List<Board> findAll(Map<String, String> map, RowBounds rowBounds>
이렇게 해서 쿼리문에서 전달해주는 파라미터를 매개 값으로 받아주면 된다.
2. 첨부파일이 2개 이상일 경우?
스프링에서는 일단 파일을 MultipartFile[] 배열로 받는다.
public ModelAndView write(ModelAndView model, @ModelAttribute Board board, @RequestParam("upfile") MultipartFile[] upfile) { System.out.println(upfile[0].getOriginalFilename()); System.out.println(upfile[0].isEmpty()); System.out.println(upfile[1].getOriginalFilename()); System.out.println(upfile[1].isEmpty()); }
2개 받아오면 2개만큼 반복하면서 실제 파일에 저장하는 로직 수행시키고 DB 테이블에 INSERT하는 로직 써주기
한개를 저장하는 로직은 반복문 돌려서 저장하면 된다.
08. BoardController.java
▼ 최종 코드
@PostMapping("/write")
public ModelAndView write(ModelAndView model, HttpServletRequest request,
@SessionAttribute(name = "loginMember") Member loginMember,
@ModelAttribute Board board, @RequestParam("upfile") MultipartFile upfile) {
int result = 0;
// 1. 파일을 업로드했는지 확인 후, 파일을 저장하는 로직
if(upfile != null && !upfile.isEmpty()) {
// 실제 파일을 저장하는 로직 작성
String renamedFileName = null;
String location = request.getSession().getServletContext().getRealPath("resources/upload/board");
renamedFileName = FileProcess.save(upfile, location);
if(renamedFileName != null) {
board.setOriginalFileName(upfile.getOriginalFilename());
board.setRenamedFileName(renamedFileName);
}
}
// 2. 작성한 게시글 데이터를 데이터베이스에 저장하는 로직
board.setWriterNo(loginMember.getNo());
result = service.save(board);
if(result > 0) {
model.addObject("msg", "게시글이 정상적으로 등록되었습니다.");
model.addObject("location", "/board/list");
} else {
model.addObject("msg", "게시글 등록이 실패하였습니다.");
model.addObject("location", "/board/write");
}
model.setViewName("common/msg");
return model;
}
09. FileProcess.java
- com > kh > mvc > common > util 밑에 생성
@Slf4j
public class FileProcess {
// static으로 만들면 객체를 만들지 않고 FileProcess.~~ 이렇게 사용할 수 있다는 장점이 있다.
public static String save(MultipartFile upfile, String location) {
String renamedFileName = null;
String originalFileName = upfile.getOriginalFilename();
log.info("Upfile Name : {}", originalFileName);
log.info("location : {}", location);
// location이 실제로 존재하지 않으면 폴더를 생성하는 로직
// 아직, 실제로 물리적인것이 아니라 메모리에 location을 가지고 파일 객체의 정보만 가지고 있다.
File folder = new File(location);
// folder가 없으면, mkdirs() 해라(있으면 안 만듦)
if(!folder.exists()) {
folder.mkdirs();
}
// 사용자가 업로드한 파일명을 renamedFileName으로 이렇게 바꾸겠다.
renamedFileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmssSSS")) +
originalFileName.substring(originalFileName.lastIndexOf("."));
try {
// 사용자가 업로드한 파일 데이터를 지정한 파일에(새로운 File 객체에, 메모리상에 올려져있는 객체(아직 저장x)) 새로 저장한다.
upfile.transferTo(new File(location + "/" + renamedFileName));
} catch (IllegalStateException | IOException e) {
log.error("파일 전송 에러");
e.printStackTrace();
}
return renamedFileName;
}
}
10. BoardController.java
// 1. 파일을 업로드했는지 확인 후, 파일을 저장하는 로직
- if문으로 renamedFileNmae이 null이 아니면, board 객체에 다음 작업 수행
board.setOriginalFileName(upfile.getOriginalFilename()); -> oiginalFileName은 upfile에서 get
board.setRenamedFileName(renamedFileName); -> renamedFileName은 FileProcess에서 return하는 값이다.
- 이렇게 해야 DB의 테이블에 2개 값이 저장되는 것임
// 2. 작성한 게시글 데이터를 데이터베이스에 저장하는 로직
- BoardService를 통해서 Board 객체를 저장할 것이다.
- 반환값은 정수형(영향 받은 행의 개수)이기 때문에, 결과 값에 따라서 어떻게 처리할 것인지
11. BoardService.java
- 인터페이스에 save() 추상 메소드 생성
12. BoardServiceImpl.java
- 인터페이스 구현체에도 save() 메소드 생성
@Override
@Transactional
public int save(Board board) {
int result = 0;
if(board.getNo() != 0) {
// update
} else {
// insert
result = mapper.insertBoard(board);
}
return result;
}
13. board-mapper.xml
- INSERT 할 쿼리문이 있는지 확인
- 12번의 로직대로라면 쿼리문의 id는 insertBoard 여야 한다.
14. BoardMapper.java
- insertBoard() 추상 메소드 생성
- 이 때, 추상 메소드의 이름은 반드시 호출하려는 쿼리문의 id와 같은 insertBoard 여야 한다.
- 별도의 트랜잭션 처리는 @Transactional 로 한다. (에러가 없다면 commit, 에러가 발생하면 rollback)
- 이것이 가능한 이유는 @어노테이션을 붙여서 AOP를 사용한 것이기 때문이다.
15. BoardController.java
- @SessionAttribute(name = "loginMember") Member loginMember를 매개변수로 가져오고
- board.setWriterNo(loginMember.getNo()); 보드 객체에 set 한 번 해줘야 한다.
(쿼리문 짜기 나름인데 외래키 위배 조건 확인해볼 것)
(board객체에 작성자No값이 없어서 담아야한다. (보드 테이블 -> 멤버 테이블 참조하고 있는데, 해당하는 컬럼이 insert가 안되서 생기는 상황))
- (FK_BOARD_WRITER) violated - parent key not found
'Programming > Framework' 카테고리의 다른 글
DAY 163. Spring MVC 첨부파일 다운로드 (0) | 2022.01.30 |
---|---|
DAY 162. Spring MVC 게시글 상세조회 (0) | 2022.01.29 |
DAY 160. Spring MVC 게시판 페이징 (0) | 2022.01.26 |
DAY 159. Spring 회원 탈퇴 (0) | 2022.01.25 |
DAY 158. Spring 회원 정보 수정 (0) | 2022.01.24 |