네이버클라우드 AIaaS 개발자 양성과정 1기/DBMS, SQL, JDBC, Servlet

[비트캠프] 77일차(16주차4일) - Servlet(파일 업로드, 멀티파트), myapp-50(파일 업로드)

끈기JK 2023. 2. 23. 14:22

 

50 파일 업로드 처리 - 테이블 구조 정의

 

회원, 게시글, 첨부파일을 ER Diagram 으로 나타내면 다음과 같다.

app_member  I    OI<  app_board  I    OI<  app_board_file

 

app_member 의 member_id (PK) 가 app_board 의 writer (FK) 이다.

app_board 의 board_id (PK) 가 app_board_file 의 board_id (FK) 이다.

 

 

 

50 파일 업로드 처리 - 테이블과 자바 객체

 

Board 의 writer 필드에 Member 객체를 필수로 저장한다.

Board 의 attachedFiles 필드에 BoardFile 객체를 0개 이상 저장한다.

 

 

 

50 파일 업로드 처리 - Servlet 과 DAO

 

BoardInsertServlet 에서 insert() 실행을 BoardDao 에게 지시한다. 여기서 《table》app_board 에 insert 한다.

BoardInsertServle 에서 insert() 실행을 BoardFileDao 에게 지시한다. 여기서 BoardFileMapper.xml 이용해서 《table》app_board_file 에 insert 한다.

 

입력화면 생성: BoardFormServlet 에서 forwarding 을 form.jsp 로 보낸다.

 

 

 

50 파일 업로드 처리 - 조인 결과를 자바 객체로 받기

 

DBMS 에서 findByNo 로 board 데이터 찾을 때 Board, Member, BoardFile 3개 객체의 데이터가 사용된다.

Board 데이터의 board_id 는 각 행의 ID 컬럼 값이 같기 때문에 기존 객체 사용하여 객체 생성 및 컬럼 값 저장한다.

Member 데이터의 writer 도 각 행의 ID 컬럼 값이 같기 때문에 기존 객체 사용한다.

BoardFile 데이터의 boardfile_id 는 각 행의 ID 컬럼 값이 다르기 때문에 행마다 객체 생성 및 컬럼 값 저장한다.

 

 

 

50 파일 업로드 처리 - 멀티 파트 데이터 처리

 

요청 프로토콜

POST  /web/board/insert HTTP/1.1

...

Content-Type: multipart/form-data; boundary=~~~

Content-Length: ~~~

...

 

파라미터 정보를 객체로 변환할 때

Apache.org 의 fileupload 라이브러리를 멀티파트 형식의 요청 데이터를 분석해서 객체에 담아 준다.

 

 

 

Apache 파일 업로드 라이브러리 다운

 

central.sonatype.com 에서 'commons-fileupload' 검색 후 아래 코드 build.gradle 에 붙이고 $ gradle eclipse 실행한다.

commoms- 라이브러리 생성된다.

 

 

apache commons 사용법

 

apache.org  페이지 아래에서 Commons 클릭한다.

 

FileUpload 클릭한다.

 

필요 코드 참고한다.

 

 

 

Web Project 개발과 테스팅 흐름

 

(프로젝트 폴더) → (톰캣 테스트 환경의 배치 폴더)

src/main/java/ .java 컴파일 된 바이트 코드  →  tmp0/wtpwebapps/myapp-server/WEB-INF/classes/ .class 로 배치된다.

src/main/resources/ .xml, .properties  →  tmp0/wtpwebapps/myapp-server/WEB-INF/classes/ .xml, .properties 등으로 배치된다.

src/main/webapp/ .html, .js, .css, .gif 등  →  tmp0/wtpwebapps/myapp-server/ .html, .js, .css, .gif 등으로 배치된다.

 

프로젝트 폴더는 Eclipse IDE 가 관리한다.

배치 폴더는 Servlet Container 가 web browser 의 요청을 받으면 서블릿을 실행, HTML/CSS/JS 등 정적 파일을 다운로드 하여 응답한다.

 

 

### 50. 파일을 업로드하기: multipart/form-data MIME 타입 다루기 
- ddl4.sql 실행: app_board_file 테이블 생성 
- multipart/form-data 형식으로 파일을 업로드 하는 방법 
- apache commons-fileupload 라이브러리 사용법  
- Servlet에서 제공하는 API(3.0부터 추가됨)를 사용하여 파일 업로드를 처리하는 방법

 

util/DaoGenerator.java

SqlSession 객체 얻고나서 자동 close 하기 위해 try-with-resources 의 () 안에 넣는다.

public class DaoGenerator implements InvocationHandler {

  SqlSessionFactory sqlSessionFactory;

  public DaoGenerator(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
  }

  @SuppressWarnings("unchecked")
  public <T> T getObject(Class<T> classInfo) {
    String className = classInfo.getName();

    return (T) Proxy.newProxyInstance(
        getClass().getClassLoader(), // 현재 클래스의 로딩을 담당한 관리자: 즉 클래스 로딩 관리자
        new Class[] {classInfo}, // 클래스가 구현해야 할 인터페이스 정보 목록
        this // InvocationHandler 객체
        );
  }

  // 자동 생성된 프록시 객체에 대해 메서드를 호출하면
  // 실제 InvocationHandler의 invoke()가 호출된다.
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      String daoName = proxy.getClass().getInterfaces()[0].getSimpleName();
      String methodName = method.getName();
      String sqlStatementName = String.format("%s.%s", daoName, methodName);
      System.out.printf("%s.%s() 호출!\n", daoName, methodName);
      Class<?> returnType = method.getReturnType();

      if (returnType == int.class || returnType == void.class) {
        return args == null ? sqlSession.insert(sqlStatementName) :
          sqlSession.insert(sqlStatementName, args[0]);
      } else if (returnType == List.class) {
        return args == null ? sqlSession.selectList(sqlStatementName) :
          sqlSession.selectList(sqlStatementName, args[0]);
      } else {
        return args == null ? sqlSession.selectOne(sqlStatementName) :
          sqlSession.selectOne(sqlStatementName, args[0]);
      }
    }
  }


  public static void main(String[] args) throws Exception {

    BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
        new SqlSessionFactoryBuilder().build(
            Resources.getResourceAsStream("bitcamp/myapp/config/mybatis-config.xml")));

    DaoGenerator generator = new DaoGenerator(sqlSessionFactory);
    BoardDao dao = generator.getObject(BoardDao.class);

    //    Board b = new Board();
    //    b.setTitle("테스트1");
    //    b.setContent("테스트내용1");
    //    b.setPassword("1111");
    //    dao.insert(b);

    //    Board b = new Board();
    //    b.setNo(13);
    //    b.setTitle("테스트1xxx");
    //    b.setContent("테스트내용1xxxx");
    //    b.setPassword("1111");
    //    dao.update(b);

    //    dao.delete(13);

    //    List<Board> list = dao.findAll();
    //    for (Board b : list) {
    //      System.out.println(b);
    //    }

    //    System.out.println(dao.findByNo(13));
  }

}

 

 

doc/ddl4.sql

 

-- 게시글 첨부파일 정보를 저장하는 테이블 정의
create table app_board_file (
  boardfile_id int not null,
  filepath varchar(255) not null,
  origin_filename varchar(255) not null,
  mime_type varchar(30) not null,
  board_id int not null
);

alter table app_board_file
  add constraint primary key (boardfile_id),
  modify column boardfile_id int not null auto_increment;
  
alter table app_board_file
  add constraint app_board_file_fk foreign key (board_id) references app_board (board_id);

 

 

BoardFile.java

DBMS 에 맞춰 BoardFile 클래스 정의한다.

package bitcamp.myapp.vo;

import java.io.Serializable;
import java.util.Objects;

public class BoardFile implements Serializable {
  private static final long serialVersionUID = 1L;

  private int no;
  private String filepath;
  private String originalFilename;
  private String mimeType;
  private int boardNo;

/* 후략 */

 

BoardFileDao.java
package bitcamp.myapp.dao;

import java.util.List;
import bitcamp.myapp.vo.BoardFile;

public interface BoardFileDao {
  int insert(BoardFile boardFile);
  int insertList(List<BoardFile> boardFiles);
  List<BoardFile> findAllOfBoard(int boardNo);
  BoardFile findByNo(int boardFileNo);
  int delete(int boardFileNo);
  int deleteOfBoard(int boardNo);
}

 

ContextLoaderListener.java

웹 앱 시작시 BoardFileDao 객체 생성 및 Servlet Context 에 보관한다.

package bitcamp.myapp.listener;

import java.io.InputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.dao.MemberDao;
import bitcamp.myapp.dao.StudentDao;
import bitcamp.myapp.dao.TeacherDao;
import bitcamp.util.BitcampSqlSessionFactory;
import bitcamp.util.DaoGenerator;
import bitcamp.util.TransactionManager;

// 웹 애플리케이션이 시작/종료 될 때 실행되는 객체
@WebListener
// 서블릿 컨테이너에게 이 클래스가 리스너 구현체임을 알려줘야 한다.
// 그래야만 서블릿 컨테이너는 이 클래스의 인스턴스를 생성한다.
// 그리고 웹앱이 시작되거나 종료될 때 메서드를 호출해 준다.
public class ContextLoaderListener implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent sce) {
    // 웹 애플리케이션이 시작될 때 서블릿 컨테이너가 호출한다.
    System.out.println("ContextLoaderListener.contextInitialized() 호출됨!");

    try {
      InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
          "bitcamp/myapp/config/mybatis-config.xml");
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
          builder.build(mybatisConfigInputStream));

      TransactionManager txManager = new TransactionManager(sqlSessionFactory);

      BoardDao boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
      MemberDao memberDao = new DaoGenerator(sqlSessionFactory).getObject(MemberDao.class);
      StudentDao studentDao = new DaoGenerator(sqlSessionFactory).getObject(StudentDao.class);
      TeacherDao teacherDao = new DaoGenerator(sqlSessionFactory).getObject(TeacherDao.class);
      BoardFileDao boardFileDao = new DaoGenerator(sqlSessionFactory).getObject(BoardFileDao.class);

      // 서블릿 컨텍스트 보관소를 알아낸다.
      ServletContext ctx = sce.getServletContext();

      // 서블릿들이 공유할 객체를 이 보관소에 저장한다.
      ctx.setAttribute("txManager", txManager);

      ctx.setAttribute("boardDao", boardDao);
      ctx.setAttribute("memberDao", memberDao);
      ctx.setAttribute("studentDao", studentDao);
      ctx.setAttribute("teacherDao", teacherDao);
      ctx.setAttribute("boardFileDao", boardFileDao);

    } catch (Exception e) {
      System.out.println("웹 애플리케이션 자원을 준비하는 중에 오류 발생!");
      e.printStackTrace();
    }
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    // 웹 애플리케이션이 종료될 때 서블릿 컨테이너가 호출한다.
    System.out.println("ContextLoaderListener.contextDestroyed() 호출됨!");
  }
}

 

Board.java

Board 객체에 BoardFile 을 담을 List 필드를 attachedFiles 로 정의한다.

package bitcamp.myapp.vo;

import java.sql.Date;
import java.util.List;
import java.util.Objects;

public class Board implements java.io.Serializable {
  private static final long serialVersionUID = 1L;

  private int no;
  private String title;
  private String content;
  private String password;
  private Date createdDate;
  private int viewCount;
  //  private int writerNo;
  //  private String writerName;
  private Member writer;
  private List<BoardFile> attachedFiles;

 

 

webapp/board/view.jsp

<input> 속성에 multiple 로 파일 다중 선택 지정한다.

반복문에서 boardFile.no != 0 (파일이 있으)면 file download 링크 건다. file delete 버튼 생성한다.

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(JSP + MVC2 + EL + JSTL)</h1>

<c:if test="${empty board}">
  <p>해당 번호의 게시글 없습니다.</p>
  <div>
    <button id='btn-list' type='button'>목록</button>
  </div>
</c:if>

<c:if test="${not empty board}">
  <form id='board-form' action='update' method='post' enctype="multipart/form-data">
  <table border='1'>
  <tr>
    <th>번호</th>
    <td><input type='text' name='no' value='${board.no}' readonly></td>
  </tr>
  <tr>
    <th>제목</th>
    <td><input type='text' name='title' value='${board.title}'></td>
  </tr>
  <tr>
    <th>내용</th>
    <td><textarea name='content' rows='10' cols='60'>${board.content}</textarea></td>
  </tr>
  <tr>
    <th>작성자</th>
    <td>${board.writer.name}</td>
  </tr>
  <tr>
    <th>등록일</th>
    <td>${board.createdDate}</td>
  </tr>
  <tr>
    <th>조회수</th>
    <td>${board.viewCount}</td>
  </tr>
  <tr>
    <th>첨부파일</th>
    <td>
      <input type="file" name='files' multiple>
      <ul>
      <c:forEach items="${board.attachedFiles}" var="boardFile">
        <c:if test="${boardFile.no != 0}">
          <li>
            <a href="../download/boardfile?fileNo=${boardFile.no}">${boardFile.originalFilename}</a>
            [<a href="filedelete?boardNo=${board.no}&fileNo=${boardFile.no}">삭제</a>]
          </li>
        </c:if>
      </c:forEach>
      </ul>
    </td>
  </tr>
  </table>
	
	<div>
	  <button id='btn-list' type='button'>목록</button>
	  <button>변경</button>
	  <button id='btn-delete' type='button'>삭제</button>
	</div>
	</form>
</c:if>


<script>
document.querySelector('#btn-list').onclick = function() {
  location.href = 'list';
}

<c:if test="${not empty board}">
document.querySelector('#btn-delete').onclick = function() {
  var form = document.querySelector('#board-form');
  form.action = 'delete';
  form.submit();
}
</c:if>
</script>

</body>
</html>

 

 

DownloadServlet.java
package bitcamp.myapp.servlet;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.BoardFile;

@WebServlet("/download/boardfile")
public class DownloadServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  private BoardFileDao boardFileDao;

  @Override
  public void init() {
    ServletContext ctx = getServletContext();
    boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
  }

  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    try {
      // 다운로드 받을 파일 번호를 알아낸다.
      int fileNo = Integer.parseInt(request.getParameter("fileNo"));

      // 파일 번호를 이용하여 파일 정보를 가져온다.
      BoardFile boardFile = boardFileDao.findByNo(fileNo);
      if (boardFile == null) {
        throw new RuntimeException("파일 정보 없음!");
      }

      // 파일을 찾는다.
      File downloadFile = new File(
          this.getServletContext().getRealPath("/board/upload/" + boardFile.getFilepath()));
      if (!downloadFile.exists()) {
        throw new RuntimeException("파일이 존재하지 않음!");
      }

      // 파일을 보내기 전에 클라이언트에게 파일에 대한 정보를 알려주기 위해 응답헤더를 추가한다.
      // => 보내는 데이터의 MIME 타입을 알려준다.
      //    예) Content-Type: image/jpeg
      response.setContentType(boardFile.getMimeType());

      // => 보내는 데이터의 파일 이름을 알려준다.
      //    예) Content-Disposition: attachment; filename="test.gif"
      response.setHeader("Content-Disposition",
          String.format("attachment; filename=\"%s\"", boardFile.getOriginalFilename()));

      try (// 파일을 읽기 위해 준비한다.
          BufferedInputStream fileIn = new BufferedInputStream(new FileInputStream(downloadFile));
          BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());) {

        // 파일에서 데이터를 읽어 클라이언트로 보낸다.
        int b;
        while ((b = fileIn.read()) != -1) {
          out.write(b);
        }
        out.flush();
      }

    } catch (Exception e) {
      request.getRequestDispatcher("/downloadfail.jsp").forward(request, response);
    }
  }
}

 

webapp/downloadfail.jsp

다운로드 실패시 여기도 이동한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>다운로드 오류!</h1>
<p>파일 다운로드 실패!</p>
</body>
</html>

 

 

BoardFileDeleteServlet.java

파일 삭제 클릭시 이 서블릿 실행된다.

로그인 사용자의 세션 정보를 가져와서 게시글 작성자 번호와 비교 후 일치시 파일 삭제하고 현재 페이지 리다이렉트 한다.

이 서버 렌더링 방식과 다르게 클라이언트 렌더링 방식은 <li> 만 삭제하면 되므로 간편하다.

package bitcamp.myapp.servlet.board;

import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.Member;

@WebServlet("/board/filedelete")
public class BoardFileDeleteServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  private BoardDao boardDao;
  private BoardFileDao boardFileDao;

  @Override
  public void init() {
    ServletContext ctx = getServletContext();
    boardDao = (BoardDao) ctx.getAttribute("boardDao");
    boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    // 로그인 사용자의 정보를 가져온다.
    Member loginUser = (Member) request.getSession().getAttribute("loginUser");

    int boardNo = Integer.parseInt(request.getParameter("boardNo"));

    Board old = boardDao.findByNo(boardNo);

    if (old.getWriter().getNo() != loginUser.getNo()) {
      response.sendRedirect("../auth/fail");
      return;
    }

    boardFileDao.delete(Integer.parseInt(request.getParameter("fileNo")));

    response.sendRedirect("view?no=" + boardNo);
  }
}

 

mapper/BoardFileMapper.xml

BoardFileMapper.xml 정의한다.

insert 로 넘어오는 파라미터 타입이 List 면 <foreach> 속성 collection= "list" 나 "collection" 사용해야 한다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardFileDao">

  <resultMap type="boardfile" id="boardfileMap">
    <id column="boardfile_id" property="no"/>
    <result column="filepath" property="filepath"/>
    <result column="origin_filename" property="originalFilename"/>
    <result column="mime_type" property="mimeType"/>
    <result column="board_id" property="boardNo"/>
  </resultMap>

  <insert id="insert" parameterType="boardfile">
    insert into app_board_file(filepath, origin_filename, mime_type, board_id) 
    values(#{filepath}, #{originalFilename}, #{mimeType}, #{boardNo})
  </insert>
  
  <!-- SQL을 실행할 때 넘어오는 파라미터 타입이 List일 경우 
       foreach 태그의 collection 속성에 사용할 수 있는 파라미터 이름은 
       list 또는 collection 이 가능하다.  -->
  <insert id="insertList">
    insert into app_board_file(filepath, origin_filename, mime_type, board_id) 
    values
    <foreach collection="list" item="file" separator=",">
      (#{file.filepath}, #{file.originalFilename}, #{file.mimeType}, #{file.boardNo})
    </foreach>
  </insert>

  <select id="findAllOfBoard" resultMap="boardfileMap" parameterType="int">
    select 
      boardfile_id, 
      filepath, 
      origin_filename,
      mime_type,
      board_id
    from 
      app_board_file
    where 
      board_id = ${no}
    order by 
      origin_filename asc
  </select>

  <select id="findByNo" parameterType="int" resultMap="boardfileMap">
    select 
      boardfile_id, 
      filepath, 
      origin_filename,
      mime_type,
      board_id
    from 
      app_board_file
    where 
      boardfile_id = ${no}
  </select>
  
  <delete id="delete" parameterType="int">
    delete from app_board_file 
    where boardfile_id=#{no}
  </delete>
  
  <delete id="deleteOfBoard" parameterType="int">
    delete from app_board_file 
    where board_id=#{no}
  </delete>
</mapper>

 

config/mybatis-config.xml

여기서 BoardFileMapper.xml 등록한다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <properties resource="bitcamp/myapp/config/jdbc.properties"></properties>

  <typeAliases>
    <!-- 지정된 패키지의 모든 클래스에 대해 클래스 이름과 같은 이름으로 별명을 부여한다. -->
    <package name="bitcamp.myapp.vo"/>
  </typeAliases>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
      </dataSource>
    </environment>
  </environments>
  
  <mappers>
    <mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/MemberMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/StudentMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/TeacherMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/BoardFileMapper.xml"/>
  </mappers>
  
</configuration>

 

mapper/BoardMapper.xml

resultMap 설정한다.

<collection> 으로 property="attachedFiles" 설정한다. 콜렉션에 담는 타입은 ofType="boardfile" 로 설정한다.

findByNo SQL 에서 left outer join 으로 app_board_file 테이블 가져온다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardDao">

  <resultMap type="board" id="boardMap">
    <id column="board_id" property="no"/>
    <result column="title" property="title"/>
    <result column="content" property="content"/>
    <result column="pwd" property="password"/>
    <result column="created_date" property="createdDate"/>
    <result column="view_cnt" property="viewCount"/>
    
    <association property="writer" javaType="member">
      <id column="writer" property="no"/>
      <result column="name" property="name"/>
    </association>
    
    <collection property="attachedFiles" ofType="boardfile">
      <id column="boardfile_id" property="no"/>
      <result column="filepath" property="filepath"/>
      <result column="origin_filename" property="originalFilename"/>
      <result column="mime_type" property="mimeType"/>
      <result column="board_id" property="boardNo"/>
    </collection>
  </resultMap>

  <insert id="insert" parameterType="board" 
          useGeneratedKeys="true" keyColumn="board_id" keyProperty="no">
    insert into app_board(title, content, writer) 
    values(#{title}, #{content}, #{writer.no})
  </insert>

  <select id="findAll" resultMap="boardMap" parameterType="string">
    select 
      b.board_id, 
      b.title, 
      b.writer,
      b.created_date, 
      b.view_cnt,
      m.name 
    from 
      app_board b
      inner join app_member m on b.writer = m.member_id
    <if test="keyword != '' and keyword != null">
    where 
      b.title like(concat('%',#{keyword},'%'))
      or b.content like(concat('%',#{keyword},'%'))
    </if> 
    order by 
      b.board_id desc
  </select>

  <select id="findByNo" parameterType="int" resultMap="boardMap">
    select 
      b.board_id, 
      b.title, 
      b.content,
      b.writer, 
      (select name from app_member where member_id = b.writer) name, 
      b.created_date, 
      b.view_cnt,
      bf.boardfile_id,
      bf.filepath,
      bf.origin_filename,
      bf.mime_type
    from 
      app_board b
      left outer join app_board_file bf on b.board_id = bf.board_id
    where 
      b.board_id=#{no}
  </select>
  
  <update id="increaseViewCount" parameterType="int">
    update app_board set
      view_cnt = view_cnt + 1
    where board_id=#{maumdaerohaedodoi}
  </update>
  
  <update id="update" parameterType="board">
    update app_board set 
      title=#{title}, 
      content=#{content} 
    where board_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_board 
    where board_id=#{no}
  </delete>
</mapper>

 

 

webapp/board/form.jsp

첨부파일 <input> 추가한다. multiple 로 다수 파일 선택하게 한다.

<form> 속성에 enctype="multipart/form-data" 추가한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(JSP + MVC2)</h1>
<form action='insert' method='post' enctype="multipart/form-data">
<table border='1'>
<tr>
  <th>제목</th>
  <td><input type='text' name='title'></td>
</tr>
<tr>
  <th>내용</th>
  <td><textarea name='content' rows='10' cols='60'></textarea></td>
</tr>
<tr>
  <th>첨부파일</th>
  <td><input type="file" name='files' multiple></td>
</tr>
</table>
<div>
  <button>등록</button>
  <button id='btn-cancel' type='button'>취소</button>
</div>
</form>

<script>
document.querySelector('#btn-cancel').onclick = function() {
  location.href = 'list';
}
</script>

</body>
</html>

 

 

BoardInsertServlet.java

insert 시 첨부파일 추가한다.

DiskFileItemFactory 객체 생성 해서 ServletFileUpload 객체 생성 매개변수로 전달한다.

아래부터 전체를 try 로 묶는다.

upload.parseRequest(request) 로 각 파트를 담은 리스트 반환받는다.

일반 파라미터 맵과 첨부파일 보관 리스트로 분리 저장한다.

현재 웹 앱 실제 경로 알아내 파일 저장한다.

파일 1개마다 개별 insert 문 실행하지 않고, 파일 목록 담은 list 를 한번에 insert 한다.

package bitcamp.myapp.servlet.board;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
import bitcamp.util.TransactionManager;

@WebServlet("/board/insert")
public class BoardInsertServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  private TransactionManager txManager;
  private BoardDao boardDao;
  private BoardFileDao boardFileDao;

  @Override
  public void init() {
    ServletContext ctx = getServletContext();
    boardDao = (BoardDao) ctx.getAttribute("boardDao");
    boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
    txManager = (TransactionManager) ctx.getAttribute("txManager");
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    // 멀티파트 데이터를 디스크에 저장한 후 객체를 리턴해주는 일을 한다.
    DiskFileItemFactory factory = new DiskFileItemFactory();

    // 멀티파트로 전송된 요청 데이터를 읽을 준비를 한다.
    ServletFileUpload upload = new ServletFileUpload(factory);

    // 위에서 준비한 객체를 사용하여 멀티파트 요청 데이터를 처리한다.
    // => 리턴되는 값은 각 파트를 FileItem 객체에 담은 객체 목록이다.
    try {
      List<FileItem> items = upload.parseRequest(request);

      // 일반 파라미터 값을 저장할 맵
      Map<String,String> paramMap = new HashMap<>();

      // 첨부파일을 보관할 목록
      List<FileItem> files = new ArrayList<>();

      // 파트의 값을 일반 파라미터와 첨부파일 파라미터로 분리하여 저장한다.
      for (FileItem item : items) {
        if (item.isFormField()) {
          paramMap.put(item.getFieldName(), item.getString("UTF-8"));

        } else {
          files.add(item);
        }
      }

      Board board = new Board();
      board.setTitle(paramMap.get("title"));
      board.setContent(paramMap.get("content"));

      // 로그인 사용자의 정보를 가져온다.
      Member loginUser = (Member) request.getSession().getAttribute("loginUser");
      Member writer = new Member();
      writer.setNo(loginUser.getNo());
      board.setWriter(writer);

      txManager.startTransaction();
      boardDao.insert(board);

      List<BoardFile> boardFiles = new ArrayList<>();
      for (FileItem file : files) {
        if (file.getSize() == 0) {
          continue;
        }

        String filename = UUID.randomUUID().toString();

        // 임시 저장된 첨부파일을 특정 디렉토리로 옮긴다.
        // 이때 전체 경로 및 파일명을 File 객체에 담아 넘겨야 한다.
        // 1) 서블릿 컨테이너가 실행하는 현재 웹 애플리케이션의 실제 경로 알아내기
        String realPath = this.getServletContext().getRealPath("/board/upload/" + filename);
        System.out.println(realPath);
        file.write(new File(realPath));

        BoardFile boardFile = new BoardFile();
        boardFile.setOriginalFilename(file.getName());
        boardFile.setFilepath(filename);
        boardFile.setMimeType(file.getContentType());
        boardFile.setBoardNo(board.getNo());
        //boardFileDao.insert(boardFile);

        boardFiles.add(boardFile);
      }

      if (boardFiles.size() > 0) {
        boardFileDao.insertList(boardFiles);
      }

      txManager.commit();

    } catch (Exception e) {
      txManager.rollback();
      e.printStackTrace();
      request.setAttribute("error", "data");
    }

    request.getRequestDispatcher("/board/insert.jsp").forward(request, response);
  }

}

 

 

BoardUpdateServlet.java

여기서는 Servlet 3.0 방법을 쓴다.

@Multipartconfig 로 멀티파트 처리한다.

request.getParts(); 로 파트들을 모두 가져온다.

part 의 이름이 files 가 아닌걸로 List 에 담아야한다.

"file" 로 첨부파일 없어도 빈 파트가 넘어오므로 size 검사한다.

package bitcamp.myapp.servlet.board;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
import bitcamp.util.TransactionManager;

// multipart/form-data 를 처리할 때 Servlet 3.0 기본 라이브러리를 사용한다면
// 다음 애노테이션을 붙여야 한다.
@MultipartConfig(maxFileSize = 1024 * 1024 * 50)
@WebServlet("/board/update")
public class BoardUpdateServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  private TransactionManager txManager;
  private BoardDao boardDao;
  private BoardFileDao boardFileDao;

  @Override
  public void init() {
    ServletContext ctx = getServletContext();
    boardDao = (BoardDao) ctx.getAttribute("boardDao");
    boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
    txManager = (TransactionManager) ctx.getAttribute("txManager");
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    txManager.startTransaction();
    try {
      // 로그인 사용자의 정보를 가져온다.
      Member loginUser = (Member) request.getSession().getAttribute("loginUser");

      Board board = new Board();
      board.setNo(Integer.parseInt(request.getParameter("no")));
      board.setTitle(request.getParameter("title"));
      board.setContent(request.getParameter("content"));

      Board old = boardDao.findByNo(board.getNo());

      if (old.getWriter().getNo() != loginUser.getNo()) {
        response.sendRedirect("../auth/fail");
        return;
      }
      if (boardDao.update(board) == 0) {
        throw new RuntimeException("게시글이 존재하지 않습니다.");
      }

      // 게시글의 첨부파일 추가하기
      Collection<Part> parts = request.getParts();
      List<BoardFile> boardFiles = new ArrayList<>();
      for (Part part : parts) {
        if (!part.getName().equals("files") || part.getSize() == 0) {
          continue;
        }

        String filename = UUID.randomUUID().toString();

        // 임시 저장된 첨부파일을 특정 디렉토리로 옮긴다.
        // 이때 전체 경로 및 파일명을 File 객체에 담아 넘겨야 한다.
        // 1) 서블릿 컨테이너가 실행하는 현재 웹 애플리케이션의 실제 경로 알아내기
        String realPath = this.getServletContext().getRealPath("/board/upload/" + filename);
        System.out.println(realPath);
        part.write(realPath);

        BoardFile boardFile = new BoardFile();
        boardFile.setOriginalFilename(part.getSubmittedFileName());
        boardFile.setFilepath(filename);
        boardFile.setMimeType(part.getContentType());
        boardFile.setBoardNo(board.getNo());

        boardFiles.add(boardFile);
      }
      if (boardFiles.size() > 0) {
        boardFileDao.insertList(boardFiles);
      }
      txManager.commit();

    } catch (Exception e) {
      txManager.rollback();
      e.printStackTrace();
      request.setAttribute("error", "data");
    }

    request.getRequestDispatcher("/board/update.jsp").forward(request, response);
  }
}

 

 

BoardDeleteServlet.java
package bitcamp.myapp.servlet.board;

import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.Member;
import bitcamp.util.TransactionManager;

// 게시글 삭제는 게시글 수정 폼을 그대로 사용하기 때문에
// 요청 데이터가 multipart/form-data 형식으로 넘어온다.
@MultipartConfig(maxFileSize = 1024 * 1024 * 50)
@WebServlet("/board/delete")
public class BoardDeleteServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  private TransactionManager txManager;
  private BoardDao boardDao;
  private BoardFileDao boardFileDao;

  @Override
  public void init() {
    ServletContext ctx = getServletContext();
    boardDao = (BoardDao) ctx.getAttribute("boardDao");
    boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
    txManager = (TransactionManager) ctx.getAttribute("txManager");
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    txManager.startTransaction();
    try {
      // 로그인 사용자의 정보를 가져온다.
      Member loginUser = (Member) request.getSession().getAttribute("loginUser");

      int boardNo = Integer.parseInt(request.getParameter("no"));

      Board old = boardDao.findByNo(boardNo);

      if (old.getWriter().getNo() != loginUser.getNo()) {
        response.sendRedirect("../auth/fail");
        return;
      }

      boardFileDao.deleteOfBoard(boardNo);
      if (boardDao.delete(boardNo) == 0) {
        throw new RuntimeException("게시글이 존재하지 않습니다!");
      }

      txManager.commit();

    }  catch (Exception e) {
      txManager.rollback();
      e.printStackTrace();
      request.setAttribute("error", "data");
    }

    request.getRequestDispatcher("/board/delete.jsp").forward(request, response);

  }
}

 

 

 


 

 

조언

 

*

 

 


 

과제

 

/