[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>
👉 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;
👉 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; // 누적된 값이 총합으로 나온다
}