Spring/Spring MVC2

Board - 게시물 등록 및 수정 / 첨부 파일 인코딩 및 등록

제주니어 2022. 10. 24. 21:34

게시글 등록
write.jsp
<body>
<h2>게시판 작성</h2>
<div id='board-write-container'>
    <form action="${ path }/board/write" method="POST" enctype="multipart/form-data">
        <table id='tbl-board'>
            <tr>
                <th>제목</th>
                <td><input type="text" name="title" id="title"></td>
            </tr>
            <tr>
                <th>작성자</th>
                <td><input type="text" name="writerId" value="${ board.writerId }" readonly></td>
            </tr>
            <tr>
                <th>첨부파일</th>
                <td><input type="file" name="upfile"></td>
            </tr>
            <tr>
                <th>내용</th>
                <td><textarea name="content" cols="50" rows="15" ></textarea></td>
            </tr>
            <tr>
                <th colspan="2">
                    <input type="submit" value="등록">
                    <input type="reset" value="취소">
                </th>
            </tr>
        </table>
    </form>
</div>
</body>

 

servlet-context.xml

 

<exclude-mapping path="/board/write"/>
  • 로그인 하지 않은 사람은 url로접근하지 못하도록 인터셉터에 매핑한다. 
BoardController

 

@GetMapping("/write")
public String write() {
    log.info("게시글 작성 페이지 요청");

    return "board/write";
}
  • jsp 페이지가 연결되도록 Controller 에서 get 요청을 한다. 

  • 클래스 상단에 @RequestMapping("/board") 설정하면 mapping 할 때 자동으로 앞에 "/board"를 표시해준다. 
  • 추가로, post 요청을 처리하는 메소드를 만든다. (GetMapping만 하면 405 에러 발생)

 

pom.xml

 

<!-- 파일 업로드 -->
<dependency>
 <groupId>commons-io</groupId>
 <artifactId>commons-io</artifactId>
 <version>2.11.0</version>
</dependency>

<dependency>
 <groupId>commons-fileupload</groupId>
 <artifactId>commons-fileupload</artifactId>
 <version>1.4</version>
</dependency>
  • 파일 업로드를 위한 라이브러리 등록
  • maven-repository > 검색 키워드 : commons-io / commons-fileupload
multipart-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:c="http://www.springframework.org/schema/c"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" 
		p:maxUploadSize="10485760"
	/>
</beans>
  • 파일 업로드 시 사용할 MultipartResolver 빈 등록 → Spring Bean Definition file 등록
  • p:maxUploadSize: 최대 업로드 파일 크기를 지정한다. (10MB)
  • 해당 xml 사용하기 위해 root-context에 import 한다. 
MultipartFileUtil

 

@Slf4j
public class MultipartFileUtil {
	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이 실제로 존재하지 않으면 폴더를 생성하는 로직
		File folder = new File(location);
		
		if(!folder.exists()) {
			folder.mkdirs();
		}
		
		renamedFileName = 
				LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmssSSS")) + 
				originalFileName.substring(originalFileName.lastIndexOf("."));
		
		try {
			// 업로드한 파일 데이터를 지정한 파일에 저장한다.
			upfile.transferTo(new File(location + "/" + renamedFileName));
			
		} catch (IllegalStateException | IOException e) {
			log.error("파일 전송 에러");
			e.printStackTrace();
		}
		
		return renamedFileName;
	}

	public static void delete(String location) {
		log.info("location : {}", location);
		
		File file = new File(location);
		
		if(file.exists()) {
			file.delete();
		}
	}
}

 

BoardController

 

@PostMapping("/write")
public ModelAndView write(
        ModelAndView model,
        @ModelAttribute Board board, 
        @RequestParam("upfile") MultipartFile upfile,
        @SessionAttribute("loginMember") Member loginMember){

    int result = 0;

    log.info("Upfile is Empty : {}", upfile.isEmpty());
    log.info("Upfile Name : {}", upfile.getOriginalFilename());

    // 1. 파일을 업로드 했는지 확인 후 파일을 저장
    if(upfile != null && !upfile.isEmpty()) {
        // 파일을 저장하는 로직 작성
        String location = null;
        String renamedFileName = null;

        try {
            location = resourceLoader.getResource("resources/upload/board").getFile().getPath();
            renamedFileName = MultipartFileUtil.save(upfile, location);

        } catch (IOException e) {
            e.printStackTrace();
        }

        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/view?no=" + board.getNo());
    } else {
        model.addObject("msg", "게시글 등록 실패");
        model.addObject("location", "/board/write");
    }

    model.setViewName("common/msg");

    return model;
}

 

  • ModelAndVie:  jsp에서 넘어오는 값과 View Page를 같이 저장한다. 
  • @RequestParam : 사용자가 요청시 전달하는 값을 Handler(Controller)의 매개변수로 1:1 맵핑할 때 사용되며, upfile 정보를 가져온다.
  • @ModelAttribute : 사용자가 요청시 전달하는 값을 오브젝트 형태로 매핑해준다. 
  • @SessionAttribute : 컨트롤러 밖에서 만들어 준 세션 데이터에 접근할 때 사용하며, 로그인 세션에 저장된 "loginMember"을 가져와 매개변수 loginMember에 담아준다. 

log.info("Upfile is Empty : {}", upfile.isEmpty());

  • 파일을 업로드하지 않으면 true, 파일을 업로드하면 false

log.info("Upfile Name : {}", upfile.getOriginalFilename());

  • 파일을 업로드하지 않으면 빈문자열, 파일을 업로드하면 "파일명"

  • ResourceLoader를 사용하기 위해 추가한다. 
  • ResourceLoader : 리소스를 읽어오는 기능을 제공하는 인터페이스다.

1. 파일을 업로드 했는지 확인 후 파일을 저장

  • getResource: Resource 객체를 반환한다. 
  • MultipartFileUtil의 save 메서드를 통해 매개변수 upfile과 location을 넘겨주고 리턴값을 renamedFileName 에 저장한다. 

  • 파일명을 재설정하여 renamedFileName 매개 변수에 저장하고 이를 retrun한다. 

2. 작성한 게시글 데이터를 데이터 베이스에 저장

setViewName()

  • 뷰의 이름 설정

addObject() 

  • 데이터 전달
BoardService

 

public interface BoardService {

	int save(Board board);
}

 

BoardServiceImpl

 

@Service
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardMapper mapper;

	@Override
	@Transactional
	public int save(Board board) {
		int result = 0;

		if(board.getNo() != 0) {
			// update
		} else {
			// insert
			result = mapper.insertBoard(board);
		}
		 
		return result;
	}

}

 

board-mapper.xml

 

<insert id="insertBoard" parameterType="Board"
    useGeneratedKeys="true" keyProperty="no" keyColumn="NO">
    INSERT INTO BOARD (
        NO,
        WRITER_NO,
        TITLE,
        CONTENT,
        ORIGINAL_FILENAME,
        RENAMED_FILENAME,
        READCOUNT,
        STATUS,
        CREATE_DATE,
        MODIFY_DATE,
        TYPE
    )
    VALUES(
        SEQ_BOARD_NO.NEXTVAL,
        #{writerNo},
        #{title},
        #{content},
        #{originalFileName},
        #{renamedFileName},
        DEFAULT,
        DEFAULT,
        DEFAULT,
        DEFAULT,
        DEFAULT
    )
</insert>

 

BoardMapper

 

@Mapper
public interface BoardMapper {

	int insertBoard(Board board);
}

 

게시글에 등록된 파일 다운로드
view.jsp
<body>
<tr>
    <th>첨부파일</th>
    <td>
        <c:if test="${ empty board.originalFileName }">
            <span>-</span>
        </c:if>
        <c:if test="${ not empty board.originalFileName }">
            <img src="${ path }/resources/images/file.png" width="20px" height="20px">
            <a href="javascript:" id="fileDown">
                <span>${ board.originalFileName }</span>
            </a>
            <%-- 
            <br><a href="${ path }/resources/upload/board/${board.renamedFileName}"
                download="${ board.originalFileName }">파일 다운</a>
            --%>
        </c:if>
    </td>
</tr>
<script>
$(document).ready(() => {
$("#fileDown").on("click", () => {
    location.assign("${ path }/board/fileDown?oname=${ board.originalFileName }&rname=${ board.renamedFileName }");
});
});
</script>
</body>
  • view.jsp 일부분

 

BoardController
@GetMapping("/fileDown")
   public ResponseEntity<Resource> fileDown(
         @RequestHeader("user-agent") String userAgent,
         @RequestParam String oname, @RequestParam String rname) {

      Resource resource = null;
      String downFileName = null;

      log.info("oname : {}, rname : {}", oname, rname);
      log.info("{}", userAgent);

      try {
        // 클라이언트로 전송할 파일의 경로와 파일명을 가져온다.
         resource = resourceLoader.getResource("resources/upload/board/" + rname);

         // 브라우저별 인코딩 처리
         if(userAgent.indexOf("MSIE") != -1 || userAgent.indexOf("Trident") != -1) {
               downFileName = URLEncoder.encode(oname, "UTF-8").replaceAll("\\+", "%20");
         } else {
            downFileName = new String(oname.getBytes("UTF-8"), "ISO-8859-1");
         }

      // 응답 메시지 작성 (html X, file O)
         return ResponseEntity.ok()
               // application/octet-stream 모든 종류의 2진 데이터를 뜻한다.
               .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
               // 파일 링크를 클릭했을 때 다운로드 화면이 출력되게 처리하는 부분
               .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + downFileName)
               .body(resource);
      } catch (UnsupportedEncodingException e) {
         e.printStackTrace();
      }

      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
   }

 

게시글 수정
view.jsp
<%--글작성자/관리자인경우 수정삭제 가능 --%>
<tr>
    <th colspan="2">
        <c:if test="${ not empty loginMember && loginMember.id == board.writerId }">
            <button type="button" 
                onclick="location.href='${ path }/board/update?no=${ board.no }'">수정</button>
            <button type="button" id="btnDelete">삭제</button>
        </c:if>
        <button type="button" onclick="location.href='${ path }/board/list'">목록으로</button>
    </th>
</tr>
  • view.jsp 일부분

 

BoardController
@GetMapping("/update")
public ModelAndView update(
        ModelAndView model, 
        @RequestParam int no,
        @SessionAttribute("loginMember") Member loginMember) {
    Board board = null;

    board = service.findBoardByNo(no);

    if(board.getWriterId().equals(loginMember.getId())) {
        model.addObject("board", board);
        model.setViewName("board/update");
    } else {
        model.addObject("msg", "잘못된 접근입니다.");
        model.addObject("location", "/board/list");
        model.setViewName("common/msg");
    }

    return model;
}
  • update.jsp 로 연결되는 controller 설정한다. 
@RequestMapping(value = "/update", method = { RequestMethod.POST })
   public ModelAndView update(
         ModelAndView model,
         @ModelAttribute Board board,
         @RequestParam("upfile") MultipartFile upfile,
         @SessionAttribute("loginMember") Member loginMember) {
      int result = 0;
      String location = null;
      String renamedFileName = null;

      if (service.findBoardByNo(board.getNo()).getWriterId().equals(loginMember.getId())) {
         if (upfile != null && !upfile.isEmpty()) {

             try {

               location = resourceLoader.getResource("resources/upload/board").getFile().getPath();

               // 기존에 업로드 된 파일이 있음
               if (board.getRenamedFileName() != null) {

                  // 이전에 업로드 된 첨부 파일이 존재하면 삭제
                  MultipartFileUtil.delete(location + "/" + board.getRenamedFileName());
               }

               renamedFileName = MultipartFileUtil.save(upfile, location);

               if (renamedFileName != null) {
                  board.setOriginalFileName(upfile.getOriginalFilename());
                  board.setRenamedFileName(renamedFileName);
               }

             } catch (IOException e) {
               e.printStackTrace();
            }
         }

         result = service.save(board);

         if (result > 0) {

            model.addObject("msg", "수정 완");
            model.addObject("location", "/board/view?no=" + board.getNo());
         } else {

            model.addObject("msg", "다시 수정하쇼");
            model.addObject("location", "/board/update?no=" + board.getNo());
         }
      } else {
         model.addObject("msg", "잘못 접근");
         model.addObject("location", "/board/list");
      }

      model.setViewName("common/msg");

      return model;
   }

 

BoardService
public interface BoardService {
	int save(Board board);
}

 

BoardServiceImpl
@Override
@Transactional
public int save(Board board) {
    int result = 0;

    if(board.getNo() != 0) {
        // update
        result = mapper.updateBoard(board);
    } else {
        // insert
        result = mapper.insertBoard(board);
    }

    return result;
}

 

board-mapper.xml
<update id="updateBoard" parameterType="Board">
    UPDATE BOARD 
    <trim prefix="SET" suffixOverrides=",">
        <if test="title != null">
            TITLE = #{title},
        </if>
        <if test="content != null">
            CONTENT = #{content},
        </if>
        <if test="originalFileName != null">
            ORIGINAL_FILENAME = #{originalFileName},
        </if>
        <if test="renamedFileName != null">
            RENAMED_FILENAME = #{renamedFileName},
        </if>
        MODIFY_DATE = SYSDATE 
    </trim>		
    WHERE NO = #{no}
</update>

 

BoardMapper
@Mapper
public interface BoardMapper {

	int updateBoard(Board board);
	
	int updateStatus(@Param("no") int no, @Param("status") String status);

}