본문 바로가기
다양한 기술들/레거시 Web 실습

[09] 게시판심화1 : 글쓰기(+파일첨부)

by 예스p 2023. 1. 11.

게시판 글쓰기 추가기능

일반게시판을 만들면서,

글쓸때에 카테고리를 선택하고, 파일을 첨부하는 기능을 추가해보자


 

 

<< 이전포스팅에서 완료된 요청     |    이번포스팅에서 구현할 요청 >>

기능 url요청 요청시 전달값   응답페이지 또는 url재요청
  /web     index.jsp
로그인요청 /login.me userId=?&userPwd=? 실패시 views/common/errorPage.jsp
성공시 /web  url재요청=> index.jsp
로그아웃요청 /logout.me     /web  url재요청=> index.jsp
회원가입페이지 /enrollForm.me     views/member/memberEnrollForm.jsp
회원가입요청 /insert.me userId=? ... 등  실패시 views/common/errorPage.jsp
성공시 /web  url재요청 => index.jsp
마이페이지요청 /myPage.me   로그인전 /web   url재요청 => index.jsp
로그인후 views.member/myPage.jsp
 정보변경요청

/update.me 

userId=? ... 등 실패시 view/common/errorPage.jsp
성공시 /myPage.me   url재요청 
공지사항 목록 /list.no     views/notice/noticeListView.jsp
공지사항글쓰기 /enrollForm.no     views/noticeEnrollForm.jsp
공지사항등록 /insert.no title=?&content=? 실패시 views/common/errorPage.jsp
성공시 /list.no   url재요청
공지 - 상세페이지 /detail.no no=? (수기기재) 실패시 views/common/errorPage.jsp
성공시 views/notice/noticeDetailView.jsp
공지 - 수정페이지 /updateForm.no no=?   views/notice/noticeUpdateForm.jsp
공지 - 수정요청 /update.no no=?&title=?&content=? 실패시 views/common/errorPage.jsp
성공시 /detail.no?no=XX   url재요청
일반게시판 목록 /list.bo cpage=?   views/board/boardListView.jsp
일반 글쓰기 /enrollForm.bo     views/board/boardEnrollForm.jsp
일반글 등록 /insert.bo   실패시 views/common/errorPage.jsp
성공시 /list.bo   url 재요청

 

 


카테고리 설정 기능

글쓰기 버튼 클릭시 첫번째로 카테고리를 가져와서

뿌려주는 역할을 해야한다.

이때 select태그 안에 option으로 뿌려준다.

 


STEP1) boardEnrollForm.jsp

  • 생성위치 : webProject/src/main/webapp/views/board
  • 글작성을 누를 시 들어오게 되는 글작성 페이지를 가작성한다.
.....
<%@ include file="../common/menubar.jsp" %>
.....
<form action="" method="post">
    카테고리
    <select name="category">
    <!-- db로부터 category테이블에 존재하는 값들 조회해와서 뿌려야함 -->
        ex)
        <option value="10">공통</option>
        <option value="20">운동</option>
        <option value="30">등산</option>
        ...
    </select>
    제목 <input type="text" name="title" required>
    내용 <textarea name="content" rows="10" style="resize:none" required></textarea>
    첨부파일 <input type="file" name="upfile">
    <button type="submit">작성하기</button>
    <button type="reset">취소하기</button>
</form>
.....

 

 

STEP2) boardListView.jsp(수정)

  • 게시글 리스트 페이지에서, 로그인한 상태에서만 보이는 글작성 버튼을 만든다
  • 클릭시 서블릿 enrollForm.bo 로 이동
<% if(loginUser != null) { %>
    <a href="<%=contextPath%>/enrollForm.bo">글작성</a>
<% } %>

 

 

STEP3) BoardEnrollFormController.java

  • 생성위치 : com.br.board.controller
    서블릿/매핑값enrollForm.bo
  • 글작성 버튼 클릭시 글작성 상세페이지로 포워딩하는 서블릿
  • 글작성 시에는 db의 카테고리 데이터들이 필요하다!
    >>카테고리 정보를 가져오는  Service메소드 실행
 void doGet(HttpServletRequest request, HttpServletResponse response) {
    ArrayList<Category> list = new BoardService().selectCategoryList();
    request.setAttribute("list",list);
    request.getRequestDispatcher("views/board/boardEnrollForm.jsp").forward(request, response);
}

 

 

STEP4) BoardServie.java(수정)

  • 카테고리 데이터 조회하는 selectCategoryList() 메소드생성
public ArrayList<Category> selectCategoryList() {
    Connection conn = getConnection();
    ArrayList<Category> list = new BoardDao().selectCategoryList(conn);
    close(conn);
    return list;
}

 

 

STEP5) board-mapper.xml (수정)

  • 카테고리 데이터 조회기능 sql문 작성
<entry key="selectCategoryList">
    SELECT
           CATEGORY_NO
         , CATEGORY_NAME
      FROM CATEGORY	
</entry>

 

 

STEP6) BoardDao.java(수정)

  • 카테고리 데이터 조회기능 selectCategoryList(conn) 메소드생성
public ArrayList<Category> selectCategoryList(Connection conn) {
    ArrayList<Category> list = new ArrayList();
    PreparedStatement pstmt = null;
    ResultSet rset = null;
    String spl = prop.getProperty("selectCategoryList");
    try {
        pstmt = conn.prepareStatement(spl);
        rset = pstmt.executeQuery();
        while(rset.next()) {
            list.add(new Category(rset.getInt("category_no"), rset.getString("category_name")));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }finally {
        close(rset);
        close(pstmt);
    }
    return list;
}

 

 

STEP7) boardEnrollForm.jsp (수정)

  • 가지고온 카테고리 데이터를 option태그 content부에 뿌려준다
  • 이때, 사용자는 옵션들 중에 하나를 선택한다
    >> 요청처리시 넘길 키값(value속성)은 카테고리의 번호를 넣는다
<select name="category">
    <!-- db로부터 category테이블에 존재하는 값들 조회해와서 뿌려야함 -->
    <% for(Category c : list) { %>
    	<!--value에는 카테고리 명이 아닌 번호를 넘겨야한다!(db에 기록?_) -->
        <option value="<%=c.getCategoryNo()%>"><%= c.getCategoryName() %></option>
    <% } %>
</select>

 

 


파일업로드 하여 글작성

일반적인 글작성 과정을 복습해보면서
첨부파일 업로드 기능을 추가해보자


STEP1) Attachment.java

  • 생성경로 : com.br.board.model.vo
  • 첨부파일에 대한 정보를 보관할 vo 클래스를 만든다.
  • db에서도 Board테이블이 아닌 별도 테이블(Attachment)에 별도 보관
    (상황에 따라 결정할 수 있음)
  • 필요한 컬럼 >>
    FILE_NO : 파일번호(PK, 시퀀스 생성)
    REF_BNO : 참조하는 게시글 번호
    ORIGIN_NAME : 파일 원본명
    CHANGE_NAME : 서버에 저장할 이름(원본명이 중복될 수 있으므로 변경필수)
    FILE_PATH : 파일이 어떤 폴더에 저장되어있는지 경로 저장
    UPLOAD_DATE : 업로드시점
    FILE_LEVEL : 썸네일로 활용할 대표사진은 1, 아니면 2
    STATUS : 상태(삭제 등)

 

 

STEP2) boardEnrollForm.jsp (수정)

  • 글쓰기 페이지의 input태그를 수정해준다.
    1) form요소 클릭시, 처리할 서블릿은 insert.bo
    2) enctype="multipart/form-data"
    >> 파일을 첨부할때 필요한 form 속성 
    >> 이걸 안쓰면 파일명만 넘어감
    3) method="post"
    >>get으로 할 시 파일이 넘어가지 않는다
<form action="<%=contextPath %>/insert.bo" method="post" enctype="multipart/form-data">

 

 

STEP3) baord-mapper.xml (수정)

  • db에 기록할때는, Board테이블에 먼저 insert 후 Attachement테이블에 insert한다.
    (항상 부모데이터를 먼저 insert해야함)
  • sql작성한것을 보면, Attachment에서 Board의 번호를 가져올때 시퀀스.currval을 사용했다
    >>Service단계에서 하나의 트렌젝션 처리해야함
INSERT
  INTO BOARD
  (
    BOARD_NO
  , BOARD_TYPE
  , CATEGORY_NO
  , BOARD_TITLE
  , BOARD_CONTENT
  , BOARD_WRITER
  )
  VALUES
  (
    SEQ_BNO.NEXTVAL
  , 1
  , 사용자가 선택한 카테고리번호
  , 입력한 제목
  , 입력한 내용
  , 로그인한 회원의 번호(요청에 안넘어옴, 별도로 구해야한다!)
  )
  
INSERT
   INTO ATTACHMENT
   (
     FILE_NO
   , REF_BNO
   , ORIGIN_NAME
   , CHANGE_NAME
   , FILE_PATH
   )
   VALUES
   (
     SEQ_FNO.NEXTVAL
   , SEQ_BNO.CURRVAL /*부모테이블의 번호 가져오기, 이때 하나의 트랜젝션으로 처리해야한다!*/
   , 첨부파일의 원본명
   , 첨부파일의 실제 업로드된 파일명
   , 저장경로
   )

 

 

STEP4) Cos.jar

  • 파일업로드는 자바 자체 클래스로는 불가, 외부 라이브러리를 가져와야 쓸수 있음
    >> 다양한 라이브러리중 보편적인 Cos.jar 사용 (com.oreilly.sevlet의 약자)
  • http://www.servlets.com
    >> 왼쪽 Cos File Upload Library
    >> 하단 Download 부분 cos-22.05.zip 다운
    >> 압축해제 후 lib폴더에 있는 cos.jar 파일을 프로젝트 파일의 lib폴더에 붙여넣기
  • 깃허브에는 jar파일이 올라가지 않음(.gitignore문서에 올라가지 않도록 지정해둠)
    >> 별도 작업 필요!
  • 단점 : 1.6G까지만 업로드 할수 있음

 

 

STEP5) MyFileRenamePolicy.java

  • 생성위치 : com.br.common
  • 사용자가 등록한 파일의 명칭은 다른 파일과 겹칠수 있기 때문에 이름을 수정해야한다. 
  • 파일명을 수정하는 객체 : 
    >>DefaultFileRenamePolicy (cos.jar에서 제공)
    - 해당 클래스 내부에 rename()메소드가 실행되면서 파일명 수정후 업로드
     *     rename(원본파일){
     *      기존에 동일한 파일명이 존재할경우
     *      파일명 뒤에 카운팅된 숫자를 붙여줌
     *      동일한것이 없을경우 그대로 저장
     *      return 수정파일;
     *     }  
    >> rename() 메소드는 원본 파일을 전달받아서 파일명 수정작업 후 수정된 파일을 반환한다.
  • 디폴트의 rename 메소드는 한글/특수문자/띄어쓰기는 검사가 안되기 때문에 나만의 객체를 만들어야 한다.
    (서버에 따라 문제가 될수 있기 때문)
    >> FileRenamePolicy를 상속해서 rename메소드를 완성시킨다
    >> 추상메소드 구현방법 :  클래스 생성후 클래스명 뒤에 implements FileRenamePolicy 입력하면 이클립스가 오류만듦. 해당 오류 클릭해서 add unimplemented method 클릭
  • 명명계획 : 파입업로드시간(연월일시분초)+5개의 랜덤값+원본파일 확장자
@Override
File rename(File originFile) {
    //1) 파일업로드시간
    // SimpleDateFormat은 매개변수로 포멧 넣고 .format(Date객체)하면 문자열로 포멧 반환
    // 시간은 HH
    //import는 java.util.Date로
    String currentTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
    //2) 5자리 랜덤값
    int ranNum = (int)(Math.random()*90000+10000);
    //3) 원본파일 확장자
    // 원본파일명.substring(.의 인덱스)로 구한다.
    // 단!! lastIndexOf로 뒤에서부터 인덱스를 찾아야한다(종종 이름에 .이 붙는 경우가 있음)
    String originName = originFile.getName();
    String ext = originName.substring(originName.lastIndexOf('.'));
    //바꿀이름저장
    String changeName = currentTime + ranNum + ext;
    //바꾼이름으로 파일객체 생성 후 리턴하기
    return new File(originFile.getParent(), changeName);
}

 

 

STEP6) BoardInsertController.java

  • 생성위치 : com.br.board.controller
    서블릿 / 매핑값 insert.bo
  • 작성하기 버튼을 누르면 호출되는 서블릿. 게시글 및 첨부파일을 저장한 후 목록페이지로 포워딩한다.
  • **처리과정**
  • 우선 post타입으므로 request를 인코딩해준다
  • enctype으로 지정한 multipart/form-data로 잘 전송된 상태에서 진행되도록 if문 내에서 구문을 작성한다.
     if(ServletFileUpload.isMultipartContent(request)) { ... }
  • 업로드된 파일을 처리하는데 필요한 변수들을 구한다.
    1. int maxSize :
      파일의 용량은 임의로 10mb로 지정한다.
      이때, byte단위로 입력을 받으므로 10*1024*1024로 입력해야 한다.
    2. String savePath :
      파일을 저장할 폴더 결정하기 : resources/board_upfiles
      >> 해당 폴더의 물리적인 경로를 알아내야한다!
      2-1) 세션객체.getServletContext() : jsp 내장객체인 application 반환
      2-2) app내장객체.getRealPath("/~~~") : 
      매개변수로 입력된 폴더의 물리정인 경로를 반환한다.
      매개변수의 경로 가장 앞에 붙은 '/'는 배포되는 폴더의 최상위 폴더인 webapp을 가르키므로 그 이후의 경로만 입력하면 된다. 
      >>getRealPath("/resources/board_upfiles/");
      **이때! 꼭 폴더명 위에 '/'를 붙여야 파일들이 그 안으로 저장된다.
  • 서버 업로드(폴더에 저장)는 한줄의 구문으로 완료된다. 
    MultipartRequest multiRequest 
    new MultipartRequest(requestsavePathmaxSize"UTF-8"new MyFileRenamePolicy());
    1. MultipartRequest 매개변수 생성자로 HttpServletReqest 객체인 request와 파일의 물리적인경로, 최대크기, 인코딩, 파일명명객체를 넘긴다.
    2. 이때 넘어간 MyFileRenamePolicy객체의 rename()메소드로 저장 이름이 만들어진다.
    3. request를 MultipartRequest타입인 multiRequest로 변환하는 작업이기도 하다.
      이렇게 생성된 multiRequest 객체는 기존의 서블릿에서 사용하던 request 객체를 대신하게 된다.
      >> multipart/form-data로 전송하는 경우 request로부터 바로 값을 뽑을 수 없기 때문(getParameter쓰면 null반환)
  • 이제, 기존에 하던 작업들을 진행하면 된다.
    이때, 데이터를 뽑을 때에는 multiRequest 객체를 사용해야하는데, 활용하는 메소드에 차이가 있다.
    1. multiRequest.getParameter("") :
      넘어온 데이터들 중 파일과 관련되지 않은 일반적인 데이터를 뽑을때는 request때처럼 getParameter를 사용한다.
    2. multiRequest.getOriginalFileName("") :
      넘어온 파일의 본래이름을 확인할때는 getOriginFileName을 사용한다.
      넘어온 파일이 없다면 null을 리턴하기때문에 파일이 넘어왔는지 여부를 검사할때도 활용된다.
    3. multiRequest.getFilesystemName("") 
      넘어온 파일의 저장명을 리턴하는 메소드이다.
    4. 파일의 저장경로는?
      메소드를 쓸 것 없이 직접입력한다.
      이때 주의할것은 폴더명 마지막에 '/'를 써야한다는 것이다.
      ex) "resources/board_upfiles/"  
  • db에 insert하는 과정이 실패되었다면, 이미 올라간 파일은 삭제해주어야한다.
    >> new File(파일경로).delete() 
    >> new File(savePath + at.getChangeName()).delete();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //post방식이고 한글을 썼으므로 인코딩작업한다.
    request.setCharacterEncoding("UTF-8");
	
    // enctype이 mutipart/form-data로 잘 전송되었을 경우 잘 수행되어야함(오류나는지 확인)
    if(ServletFileUpload.isMultipartContent(request)) {
        //1) 전달되는 파일들을 처리할 작업내용 (파일용량제한, 전달된 파일을 저장할 폴더 경로)
        //1-1) 파일의 용량제한 지정 >> int maxSize에 byte단위로 담기
        int maxSize = 10*1024*1024;
        //1-2) 전달된 파일을 저장시킬 폴더의 물리적인 경로 알아내기
        String savePath = request.getSession().getServletContext().getRealPath("/resources/board_upfiles/");
        //System.out.println(savePath); 어떻게 출력되나 확인
        //C:\workspaces\06_web-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\webProject\resources\board_upfiles\
 
        //2) 전달된 파일의 파일명 수정 및 서버에 업로드(폴더에 저장) 작업
        MultipartRequest multiRequest = new MultipartRequest(request, savePath, maxSize, "UTF-8", new MyFileRenamePolicy());
			
        //3) DB에 기록할 데이터를 뽑아서 vo에 주섬주섬 담기
        //    > 카테고리번호, 제목, 내용, 로그인한회원번호 => Board테이블 insert
        //    > ^넘어온 첨부파일이 있다면^ 원본명, 실제파일명, 저장경로 => Attachment 테이블 insert
        String category = multiRequest.getParameter("category");
        String boardTitle = multiRequest.getParameter("title");
        String boardContent = multiRequest.getParameter("content");
        //회원번호 알아오기
        HttpSession session = request.getSession();
        int userNo = ((Member)session.getAttribute("loginUser")).getUserNo();
        //Board담기
        Board b = new Board();
        b.setCategory(category);
        b.setBoardTitle(boardTitle);
        b.setBoardContent(boardContent);
        b.setBoardWriter(String.valueOf(userNo));
        //첨부파일 있을때만 Attachment담기
        Attachment at = null;
        //multiRequest.getOriginalFileName("키") : 
        //키값(input의 name)으로 넘어온 첨부파일의 원본명 문자열 리턴(없을시 null)
        if(multiRequest.getOriginalFileName("upfile") != null) {
            at = new Attachment();
            at.setOriginName(multiRequest.getOriginalFileName("upfile"));
            //실제 저장된 파일명을 알아내는 메소드
            at.setChangeName(multiRequest.getFilesystemName("upfile"));
            //절대경로 말고 상대경로로, 마지막은 /
            at.setFilePath("resources/board_upfiles/");
        }

        //4) 서비스요청 (insert하러가기)
        int result = new BoardService().insertBoard(b,at);

        //5) 응답뷰 지정
        if(result > 0) {
            //성공 => 목록페이지의 1번
            session.setAttribute("alertMsg", "일반게시글 작성 성공");
            response.sendRedirect(request.getContextPath()+"/list.bo?cpage=1");
        }else {
            //실패 => 에러페이지
            //첨부파일 업로드는 진행이 됐음. 단 db에 기록이 실패함
            //첨부파일이 있었다면 찾아서 삭제해야한다
            if(at != null) {
                //savePath는 폴더까지의 경로가 담겨으므로 이름까지 합쳐준다
                new File(savePath + at.getChangeName()).delete();
            }
            request.setAttribute("errorMsg", "일반게시글 작성 실패");
            request.getRequestDispatcher("views/common/errorPage.jsp").forward(request, response);
        }
    }
}

 

 

STEP7) BoardService.java (수정)

  • insertBoard(b,at)메소드 만들기
  • 이때 Dao는 메소드를 각각 만들어야한다
    >>단, commit은 한번에!
public int insertBoard(Board b,Attachment at) {
    Connection conn = getConnection();
    //부모테이블 먼저 insert해야 .currval이 잘 먹힌다.
    int result1 = new BoardDao().insertBoard(conn, b);
    //**첨부파일이 없는 게시글도 있기때문에 0이 아닌 1로 초기화해야한다! 
    int result2 = 1;
    if(at != null) {
        result2 = new BoardDao().insertAttachment(conn, at);
    }
    //둘중 하나라도 실패했다면 롤백해야한다
    if(result1>0 && result2>0) {
        commit(conn);
    }else {
        rollback(conn);
    }
    close(conn);
    return result1*result2;
}

 

 

STEP8) BoardDao.java (수정)

  • dao메소드는 따로따로 만든다
  • insertBoard(conn, b)
  • insertAttachment(conn, at)
public int insertBoard(Connection conn, Board b) {
    int result=0;
    PreparedStatement pstmt = null;
    String sql = prop.getProperty("insertBoard");
    try {
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, b.getCategory());
        pstmt.setString(2, b.getBoardTitle());
        pstmt.setString(3, b.getBoardContent());
        pstmt.setString(4, b.getBoardWriter());
        result = pstmt.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(pstmt);
    }
    return result;		
}

public int insertAttachment(Connection conn, Attachment at) {
    int result = 0;
    PreparedStatement pstmt = null;
    String sql = prop.getProperty("insertAttachment");
    try {
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, at.getOriginName());
        pstmt.setString(2, at.getChangeName());
        pstmt.setString(3, at.getFilePath());
        result = pstmt.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
    }finally {
        close(pstmt);
    }
    return result;
}

 

 

 

 

 

댓글