본문 바로가기

Programming/Framework

DAY 161. Spring MVC 게시판 게시글 작성

 

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