[1] 파일업로드를 위한 인코딩 방식 multipart / form-data

//reg.jsp
<form method="post" action="reg" 
enctype="application/x-www-form-urlencoded"> <!-- 기본 인코딩방식. 이렇게 하면 파일명만 전달되므로 파일 업로드 불가 -->
  <div class="margin-top first">
  <h3 class="hidden">공지사항 입력</h3>
  
  //인코딩 방식 변경
<form method="post" action="reg" enctype="multipart/form-data">
   <div class="margin-top first">
    <h3 class="hidden">공지사항 입력</h3>

변경 전
변경후. form data에 입력값 자체는 들어갔는데 302에러가 뜸. content-type을 보면 multipart로 전달방식이 달라진걸 볼 수 있다

👉 regController에 넘어오는 값을 콘솔에 찍어보면 title: null값이 뜨는걸 알 수 있다.(오류: ORA-01400: NULL을 ("NEWLEC"."NOTICE"."TITLE") 안에 삽입할 수 없습니다) 따라서 파일업로드를 위해 설정값을 변경해줘야 한다.

 

 

 

[2] 파일업로드를 위한 서블릿 설정 변경

2-1) web.xml에 설정

<multipart-config>
	<location>/tmp</location>
    <max-file-size>20848820</max-file-size>
    <max-request-size>418018841</max-request-size>
    <file-size-threshold>1048576</file-size-threshold>
</multipart-config>

 

2-2) annotation으로 설정

@MultipartConfig(
		//location = "/tmp", // 파일메모리가 일정양이 넘어서는 경우에는 디스크를 통해 저장할수 있도록 바이트단위 설정
		// 절대경로를 사용하라는데 그러기가 어려워서 설정 안하고, 자바가 지정된 임시디렉토리를 쓰는게 낫다.
		fileSizeThreshold = 1024*1024, //전송메모리가 1메가바이트 이상일때.
		maxFileSize = 1024*1024*5, // 5메가. 사용자가 보내는 하나의 파일 용량 제한
		maxRequestSize = 1024*1024*5*5 // 25메가. 파일을 5개까지. 전체 요청에 대한 용량 제한

 

 

[3] 파일저장을 위한 물리경로 얻기

: Part를 이용하여 파일을 받아온다.

: //"/upload/" - 자바 api는 이런 상대경로가 아닌 물리경로로 업로드 될 곳이 지정된다.

//RegController.java

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

	String title = request.getParameter("title"); //jsp에서 키값 (name)확인하여 넣기
	String content = request.getParameter("content");
	String isOpen =request.getParameter("open");
		
	Part filepart = request.getPart("file"); //getParameter는 문자열이고 part는 해당 파트 자료를 받아오는 것
	filepart.getInputStream(); //스트림을 통해 파일을 전달받음
		
	String realPath = request.getServletContext().getRealPath("/upload"); //물리경로로 전환해준다
	System.out.println(realPath); //경로 확인을 위해 콘솔에 출력해봄

콘솔창에 찍히는 물리경로

👉 이클립스에 업로드 폴더를 만들어 두어도 (여긴 개발용 work space의 디렉토리임) , 실제로 코드가 동작될때는 임시서비스를 위한 배포서버에 옮겨져서 작동된다. (위의 경로가 이클립스가 운영중인 workspace) 따라서 위의 경로에 저장된다.

 

 

 

 

[4] 단일 파일 업로드

: 실제 서비스 구현시에는 Part.write를 쓰지만, 서블릿 3.0에 등장한 것. 하위호환성을 가지지 않으므로 기본적인 스트림 객체를 사용.

: 모든 입출력(콘솔, 파일, 소켓 등)은 기본적으로 스트림 객체를 사용.

 

//RegController.java

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		/*이부분은 게시글 본문을 받아오는 내용
        String title = request.getParameter("title"); //jsp에서 키값 (name)확인하여 넣기
		String content = request.getParameter("content");
		String isOpen =request.getParameter("open");
        */ 
		
		// 파일 입력값 받아오기
		Part filePart = request.getPart("file"); //getParameter는 문자열이고 part는 해당 파트 자료를 받아오는 것
		String fileName = filePart.getSubmittedFileName(); // 파일명 반환
		InputStream fis = filePart.getInputStream(); //스트림을 통해 파일을 전달받음
		
		String realPath = request.getServletContext().getRealPath("/upload"); //물리경로로 전환해준다
		File path = new File(realPath); //물리적인 경로가 있는지 확인 가능
			if(path.exists()) //boolean
					path.mkdirs(); //경로에 없는 부모폴더까지 같이 만들어줌
        
        String filePath = realPath + File.separator + fileName; //  파일명을 경로에 붙이기. 저장할 파일 경로+경로구분자+파일명
		
		// 파일 입력값 내보내기
		FileOutputStream fos = new FileOutputStream(filePath);
		
		int size=0; //버퍼가 읽어온 개수
		byte[] buf = new byte[1024]; //한번에 1키로바이트 단위로 읽어서 전달.read()가 해당 단위대로 읽어들인다
		while((size = fis.read(buf)) != -1) // read는 다 읽으면 -1반환. 읽어서 담은 b의 값이 -1이 아닌경우는 값을 계속 읽는다
			fos.write(buf,0,size); // 0번쨰부터 길이 (인덱스 아님) 만큼 읽어들이도록 함.
		// buf만 읽어들이면 담긴값을 모두 출력하는거지만, 900개만 읽어온경우 0~900만큼만 출력되도록 설정하는 것.
		
		fos.close();
		fis.close(); // 스트림 순서대로 닫아주기

👉 int를 반환하는 read()

int java.io.InputStream.read(byte[] b) throws IOException
-Reads some number of bytes from the input stream and stores them intothe buffer array b. 

 

- The number of bytes actually read is returned as an integer. This method blocks until input data is available, end of file is detected, or an exception is thrown. 
-If the length of b is zero, then no bytes are read and 0 is returned; otherwise, there is an attempt to read at least one byte.

-If no byte is available because the stream is at the end of the file, the value -1 is returned; otherwise, at least one byte is read and stored into b. 


❣  오류정리. 자꾸 JSPPsj 폴더 안에 upload파일로 생성됨. upload폴더를 만들어두었음에도 불구하고!

//잘못쳤던 코드

String realPath = request.getServletContext().getRealPath("/upload"); //물리경로로 전환해준다
String filePath = realPath + File.separator + fileName; // 파일명을 경로에 붙이기. 저장할 파일 경로+경로구분자+파일명
		
// 입력값 내보내기
FileOutputStream fos = new FileOutputStream(realPath); //filePath를 대입해야할걸 realPath를 넣음

👉 내 물리경로값은 제대로 떴다.  E:\jsp\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\JSPPrj\upload

👉 값을 내보낼때 outStream에 realPath를 넣으면 \JSPPrj\upload로 경로가 잡히므로, 마지막 값인 upload가 파일명이 되어서 upload폴더로 들어가는게 아닌, JSPPrj폴더에 upload파일로 생성이 되었던 것

/upload/파일명 이렇게 들어가야 upload폴더 안에 파일명 이름으로 파일이 생성되는데!

 

👉 따라서 outStream에는 realPath+파일명인 filePath가 들어가야 한다. 제대로 됐을 때의 출력값은 아래와 같다.

realPath: E:\jsp\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\JSPPrj\upload
filePath: E:\jsp\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\JSPPrj\upload\ojdbc11.jar

 

 

❣ 만약 폴더를 미리 만들어놓지 않은 채로 물리경로를 \upload로 설정했다면?

경로 오류로 친절하게 알려준다. 없는 폴더를 만들어서 저장해주진 않는다.


 

[5] 다중 파일 업로드

: 첨부파일 (name="file")이 두개가 될 경우, 파일 1,2로 나누는것보단 한번에 컬렉션이나 배열로 받는게 낫다

// reg.jsp

<tr>
      <th>첨부파일</th>
      <td colspan="3" class="text-align-left text-indent">
      <input type="file" name="file" /> </td>
 </tr>
 <tr>
      <th>첨부파일</th>
      <td colspan="3" class="text-align-left text-indent">
      <input type="file" name="file" /> </td>
 </tr>
 //<td>단위로 복사하면 가로로 첨부파일 두개가 생겨서 row단위로 복사해야 한줄씩 깔끔하게 나온다

 

: 서버쪽에선 Part부분에 여러개가 오게 된다. 몇 개가 올지 모르므로 collection으로 받을 수 있는 준비가 되어야 한다

: 넘어오는 파일 검증하기 ① file이라는 키로 넘어오는가? ② 하나만 업로드를 할 경우에도 값을 넘길 수 있는가?

//RegController.java

// 파일 입력값 받아오기
    Collection<Part> parts = request.getParts(); //파일을 여러개 받을 수 있다.
    
	StringBuilder builder = new StringBuilder(); // db에 파일명을 저장하기 위해 만듦

    for(Part p : parts) { //여러개 받도록 반복문 돌리기
        if(!p.getName().equals("file")) //art에서 받아온 file의 이름이 jsp에 있는 name="file"인지 골라낸다. 같지않으면 리젝.
            continue;
        if(p.getSize()==0) continue;// 파일 업로드가 없어서 0으로 넘어오는 건도 쳐내도록 한다
            
    //같으면 내려간다.

        Part filePart = p; //if문 검증이 맞으면 parts에서 받아온 p값을 filePart에 넣는다
        String fileName = filePart.getSubmittedFileName();
        builder.append(fileName); // append로 파일명에 콤마더하기
		builder.append(",");
            
        InputStream fis = filePart.getInputStream();

        String realPath = request.getServletContext().getRealPath("/upload");
       File path = new File(realPath); //물리적인 경로가 있는지 확인 가능
			if(path.exists()) //boolean
					path.mkdirs(); //경로에 없는 부모폴더까지 같이 만들어줌

        String filePath = realPath + File.separator + fileName;

        // 파일 입력값 내보내기
        FileOutputStream fos = new FileOutputStream(filePath);

        int size=0;
        byte[] buf = new byte[1024]; 
        while((size = fis.read(buf)) != -1) 
            fos.write(buf,0,size);

        fos.close();
        fis.close();
    }
    
// 마지막 파일명은 콤마 삭제
builder.delete(builder.length()-1, builder.length()); //a.jpg,b.jpg, 이런형태에서 11번째 인덱스를 삭제(길이는 12)

//중략
notice.setFiles(builder.toString()); //notice객체에 파일명 전달

 

 

[6] 업로드 파일 다운로드하기

: 예전엔 서버에서 다운로드 처리를 했지만 현재는 html에서 가능해졌다.

 <a download href="/upload/${fileName}"</a>

//detail.jsp

<tr>
    <th>첨부파일</th>
    <td colspan="3" class="text-align-left text-indent">
    <c:forTokens var="fileName" items="${n.files}" delims="," varStatus="st">

        <c:set var="style" value=""/>

        <c:if test="${fn:endsWith(fileName,'.zip')}">
            <c:set var="style" value="font-weight:bold; color:blue;"/>
        </c:if>
        //하이퍼링크에 절대경로를 추가해주면 웹에서 사진이 열리고, download를 붙이면 다운로드가 된다
        <a download href="/upload/${fileName}" style="${style}">${fn:toUpperCase(fileName)}</a>

        <c:if test="${!st.last}">
        /
        </c:if>
    </c:forTokens>
    </td>
</tr>

 

 

[7] 공개 상태 표시하기

7-1) admin페이지에서 공개된 글은 체크박스 표시되어있도록 하기

/admin/board/list.jsp

<tbody>
<!-- list에 있는 내용을 하나씩 꺼내서 n에 담는다 -->
<c:forEach var="n" items="${list}">
    <c:set var="open" value="" /> <!-- 체크박스 기본값 -->
    <c:if test="${n.pub}"> <!-- pub반환형이 true. 조건에 맞을때만 체크박스 표시 -->
        <c:set var="open" value="checked" />
    </c:if>

    <tr>
        <td>${n.id}</td>
        <td class="title indent text-align-left"><a href="detail?id=${n.id}">${n.title}</a> <!-- 제목 끝에 댓글개수 붙이기 -->
            <span class="text-red"> [${n.cmtCount}]</span></td>
        <td>${n.writerId}</td>
        <td><fmt:formatDate pattern="yy-MM-dd hh:mm"
                value="${n.regdate}" /></td>
        <td><fmt:formatNumber value="${n.hit}" /></td>

        <!-- 추가된 공개,삭제용 체크박스 -->
        <td><input type="checkbox" name="open-id" ${open} value="${n.id}"></td> //open으로 표시값 꽂아넣기
        <td><input type="checkbox" name="del-id" value="${n.id}"></td>
    </tr>

</c:forEach>
</tbody>

 

7-2) 사용자에게 공개된 글만 보이도록 하기

: jsp에서 공개된 글만 조건문을 달게되면, for문을 돌면서 공개된 글이 한개도 없을 경우 한 페이지가 빈 페이지로 나오게 될 수도 있다.

: 따라서 컨트롤러에서 공개글을 걸러줘야한다

//ListController.java
//사용자에게 요청이 왔을때 뭘 원하는지 캐치하고 서비스에게 값을 받아냄. 컨트롤러 역할이 심플해짐
		NoticeService service = new NoticeService();
		List<NoticeView> list = service.getNoticePubList(field, query, page); // 공개리스트만 넘기도록 하는 메서드
		int count = service.getNoticeCount(field, query);
		
		request.setAttribute("list", list);
		request.setAttribute("count", count);
		
		request.getRequestDispatcher("/WEB-INF/view/notice/list.jsp").forward(request, response);
	}

//NoticeService.java
// 공개된 글만 반환하는 NoticePubList. getNoticeList와 전체적으로 동일하나 쿼리문만 다름
	public List<NoticeView> getNoticePubList(String field, String query, int page) {
		List<NoticeView> list = new ArrayList<>();

		String sql = "select * from (" + "select rownum NUM, N.* " + "from (select * from NOTICE_VIEW where " + field
				+ " like ? order by regdate desc)N" + ") where PUB=1 AND NUM between ? and ?";

		String url = "jdbc:oracle:thin:@localhost:1521/xepdb1";

		try {
			//생략
            
			while (rs.next()) {
				int id = rs.getInt("ID"); // get안의 값은 컬럼 대소문자와 동일하게
				String title = rs.getString("TITLE");
				Date regdate = rs.getDate("REGDATE");
				String writerId = rs.getString("WRITER_ID");
				String hit = rs.getString("HIT");
				String files = rs.getString("FILES");
				// String content = rs.getString("CONTENT");
				int cmtCount = rs.getInt("CMT_COUNT");
				boolean pub = rs.getBoolean("PUB"); //pub값 추가

				NoticeView notice = new NoticeView(id, title, regdate, writerId, hit, files, pub,
						// content,
						cmtCount);

				list.add(notice);
			}
//이하동일

 

7-3) 공지사항 일괄공개

: 체크박스 클릭후 일괄공개 선택 시 일반유저 화면에서 공개되기

: 화면 하단에 현재 리스트의 아이디값을 숨겨두고, 체크된 값이랑 비교해서 넘어갈 수 있도록 하기

//관리자용 list.jsp
<div class="text-align-right margin-top">						
    <c:set var="ids" value="" /> // 초기값
    
    <c:forEach var="n" items="${list}"> //list를 n에 담는 for문
        <c:set var="ids" value="${ids} ${n.id}" /> // id값을 누적으로 ids에 담는다
    </c:forEach> //for문이 돌수록 가로로 값이 누적됨
    
    <input type="hidden" name ="ids" value="${ids}"> //text로 하면 보이므로 hidden

    <input type="submit" class="btn-text btn-default" name="cmd" value="일괄공개"> 
    <input type="submit" class="btn-text btn-default" name="cmd" value="일괄삭제">
    <a class="btn-text btn-default" href="reg">글쓰기</a>
</div>

현재 페이지의 모든 아이디값이 나오게 하고 숨겨둔다

 

 

 

: 한번에 실행되는 단위 = 트랜잭션. (업무적인단위, 논리적인 단위)

👉 공개와 비공개는 동시에 이루어져야 한다. 하나가 안되면 둘다 원상복구되어야 한다. 한쪽이 출금이면 한쪽이 입금인것처럼!

👉 트랜잭션 처리는 컨트롤러가 담당

//관리자용 ListController.java

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String[] openIds = request.getParameterValues("open-id"); //오픈된 아이디
    //String[] delIds = request.getParameterValues("del-id");
    //String cmd = request.getParameter("cmd");
    String ids_ = request.getParameter("ids"); //목록 전체 아이디를 받는 키
    String[] ids = ids_.trim().split(" "); // 배열값으로 변경 (상태변경이 된 값까지 싹 업데이트하기위해)

    NoticeService service = new NoticeService();

    switch(cmd) {
    case "일괄공개":
        for(String openId: openIds)

        List<String> oids = Arrays.asList(openIds); // 오픈아이디 배열을 리스트 형태로 변경
        // 전체열의 id에서 공개 열의 id를 빼면 비공개만 남는다
        // 1,2,3,4,5 - 2,3,4 = 1,5를 얻고자 하는 것.
        List<String> cids = new ArrayList(Arrays.asList(ids)); //전체아이디를 새 리스트로 만듦
        cids.removeAll(oids); // 새 아이디에서 오픈아이디를 모두 지워서 비공개 아이디를 얻음

	//한번에 실행되는 단위 = 트랜잭션. (업무적인단위, 논리적인 단위) 공개와 비공개는 동시에 이루어져야 한다. 하나가 안되면 둘다 원상복구
		service.pubNoticeAll(oids,cids);
            
        break;

id값을 출력해서 확인

 

👉 service로 넘긴 pubNoticeAll함수 구현

 

//NoticeService.java

//------------------ 글 공개용 오버로드. 공개, 비공개값 & 배열과 리스트 모두 받을수 있게 만들어서 편의성을 높임
	public int pubNoticeAll(int[] oids, int[] cids) {// int배열을 리스트 컬렉션으로 변환하는 메서드
		
		List<String> oidsList = new ArrayList<>();
		for(int i=0; i<oids.length; i++)
			oidsList.add(String.valueOf(oids[i]));
		
		List<String> cidsList = new ArrayList<>();
		for(int i=0; i<cids.length; i++)
			cidsList.add(String.valueOf(cids[i]));
				
		return pubNoticeAll(oidsList,cidsList); //리턴값으로 메소드를 호출하며 리스트를 넣어줌
	}
	
	public int pubNoticeAll(List<String> oids, List<String> cids) {// 리스트 컬렉션을 하나의 문자열로 변환하는 메서드
		
		String oidsCSV= String.join(",", oids) ; //구분자를 사용하여 문자열을 하나로 합쳐주는 join. 가변데이터나 컬렉션이 매개로 들어가야한다
		String cidsCSV= String.join(",", cids);
		
		return pubNoticeAll(oidsCSV,cidsCSV);//csv문자열로 변환해서 반환해야 한다
	}
	
	// CSV는 콤마로 구분된 값(comma separated value) "20,30,40"
	public int pubNoticeAll(String oidsCSV, String cidsCSV) { 
		int result =0;

		//동시에 실행되어야하므로 문장을 두개씩 만든다
		//String을 포맷 문자열화 해서 출력해도 되고 값을 +로 꽂아넣어도 된다
		String sqlOpen = String.format("UPDATE NOTICE SET PUB=1 WHERE ID IN (%s)",oidsCSV); //문자열을 그냥 넣으면 '30,40' 이렇게 들어가서 원하는 sql문이 되지않음
		String sqlClose = "UPDATE NOTICE SET PUB=0 WHERE ID IN ("+cidsCSV+")";
		
		String url = "jdbc:oracle:thin:@localhost:1521/xepdb1";

		try {
			Class.forName("oracle.jdbc.driver.OracleDriver");
			Connection con = DriverManager.getConnection(url, "id", "pw"); // 서버,아이디,패스워드
		
        //물음표에 꽂아넣지 않아서 statement는 없어도된다
			Statement stOpen = con.createStatement();			
			result+= stOpen.executeUpdate(sqlOpen); //result+=에 누적하는 방식으로 result를 만들어도 됨

			Statement stClose = con.createStatement();			
			result+= stClose.executeUpdate(sqlClose);
			
			stOpen.close();
			stClose.close();
			con.close();

		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return result; // 누적된 값이 총합으로 나온다
	}

 

 

 

+ Recent posts