2020년 01월 07일
업데이트:
게시글
게시글 등록 화면
boardInser.jsp
<form action="${contextPath }/board/insert.do" method="post"
enctype="multipart/form-data" role="form" onsubmit="return boardValidate();">
요청을 받아들일 Controller 생성
- 파라미터 얻어오기
제출되는 form태그의 encType이 multipart/form-data 형식이면
기존에 사용하던 request 객체로 파라미터를 얻어올 수 없다.
request.getParameter(name) == text로 넘어온 파라미터만 얻어 올 수 있다.cos.jar에서 제공하는 MultipartRequest 객체를 사용하면 파라미터를 얻어올 수 있다.
- 파일 이미지 업로드
데이터베이스에 BLOB타입으로 파일을 넣으면 속도가 매우 느려진다.
파일은 서버에 저장을 하고(Tomcat), 파일에 대한 정보만 데이터에 저장을 하는 방식으로 사용한다.
파일명 변환을 위한 Class
파일명이 중복되면 에러가 발생할 수 있으니 파일명 변환을 위한 클래스를 작성해주어야 한다.
cos.jar에서 중복되는 파일이 업로드 되었을 때
파일명을 바꿔주는 DefaultFileRenamePolicy 클래스를 제공해 주지만
중복되는 파일명이 있을 경우 파일명 뒤에 (n)이 붙는 형식으로 바꿔준다.
이런 방법이 아닌 파일명에 업로드된 시간을 표기하는 형식으로 변경하는 별도의 Class를 작성한다.FileRenamePolicy를 상속받는다.
파일 업로드 과정
파일 업로드 후 uploadImages폴더
- 게시글 등록 Controller
boardController.java
// 게시글 등록 Controller (+파일 업로드) ***************************************
else if(command.contentEquals("/insert.do")) {
errorMsg = "게시글 삽입 과정에서 오류 발생";
// 제출되는 form태그의 encType이 multipart/form-data 형식이면
// 기존에 사용하던 request 객체로 파라미터를 얻어올 수 없다.
// request.getParameter(name) == text로 넘어온 파라미터만 얻어 올 수 있다.
// -> cos.jar에서 제공하는 MultipartRequest 객체를 사용하면
// 파라미터를 얻어올 수 있다.
// 1. MultipartRequest 객체 생성하기
// 1-1. 전송 파일 용량 지정(byte단위)
int maxSize = 20* 1024 * 1024; // 20MB == 20* 1024KB == 20* 1024 * 1024
// 1-2. 서버에 업로드된 파일을 저장할 경로 지정
String root = request.getSession().getServletContext().getRealPath("/");
// WebContent
String filePath = root + "resources/uploadImages/";
System.out.println("filePath : " + filePath);
// 1-3. 파일명 변환을 위한 클래스 작성하기
//파일명을 바꿔주는 DefaultFileRenamePolicy 클래스를 제공해 주지만
// 중복되는 파일명이 있을 경우 파일명 뒤에 (n)이 붙는 형식으로 바꿔준다.
// 이런 방법이 아닌 파일명에 업로드된 시간을 표기하는 형식으로 변경하는 별도의 Class를 작성한다.
// 1-4. MultipartRequest 객체 생성
// -> 객체 생성과 동시에 파라미터로 넘어온 내용 중 파일이 서버에 바로 저장됨.
MultipartRequest multiRequest = new MultipartRequest(request, filePath, maxSize, "UTF-8", new MyFileRenamePolicy());
// 2. 생성한 MultipartRequest 객체에서 파일 정보만을 얻어와
// 별도의 List에서 모두 저장하기.
// 2-1. 파일 정보를 모두 저장할 List 객체 생성
List<Attachment> fList = new ArrayList<Attachment>();
// 2-2. MultipartRequest에서 업로드된 파일의 name 모두 반환 받기
Enumeration<String> files = multiRequest.getFileNames();
// Iterator : 컬렉션 요소 반복 접근자
// Enumeration : Iterator의 과거 버전
// 2-3. 얻어온 Enumeration 객체에 요소를 하나씩 반복 접근하여
// 업로드된 파일 정보를 Attachment 객체에 저장한 후
// fList에 추가하기
while(files.hasMoreElements()) { // 다음 요소가 있다면
// 현재 접근한 요소 값 반환
String name = files.nextElement(); // img 0
// 제출받은 file태그 요소 중 업로드된 파일이 있을 경우
if(multiRequest.getFilesystemName(name) != null) {
// Attachment 객체에 파일 정보 저장
Attachment temp = new Attachment();
temp.setFileName(multiRequest.getFilesystemName(name));
temp.setFilePath(filePath);
// name 속성에 따라 fileLevel 지정
int fileLevel =0;
switch(name) {
case "img0" : fileLevel =0; break;
case "img1" : fileLevel =1; break;
case "img2" : fileLevel =2; break;
case "img3" : fileLevel =3; break;
}
temp.setFileLevel(fileLevel);
// fList에 추가
fList.add(temp);
}
} // end while
// 3. 파일정보를 제외한 게시글 정보를 얻어와 저장하기
String boardTitle = multiRequest.getParameter("boardTitle");
String boardContent = multiRequest.getParameter("boardContent");
int categoryCode = Integer.parseInt(multiRequest.getParameter("categoryCode"));
// 세션에서 로그인한 회원의 번호를 얻어옴
Member loginMember = (Member)request.getSession().getAttribute("loginMember");
int boardWriter = loginMember.getMemberNo();
// 현재까지 fList,boardTitle, boardContent, categoryCode, boardWriter를 얻어왔음
// 얻어온 변수들을 모두 저장할 수 있는 Map 객체 생성
Map<String,Object> map = new HashMap<String, Object>();
map.put("fList", fList);
map.put("boardTitle", boardTitle);
map.put("boardContent", boardContent);
map.put("categoryCode", categoryCode);
map.put("boardWriter", boardWriter);
// 4. 게시글 등록 비즈니스 로직 수행 후 결과 반환받기
int result = service.insertBoard(map);
if(result>0) { // DB 삽입 성공 시 result에 삽입한 글 번호가 저장되어 있다.
swalIcon = "success";
swalTitle ="게시글 등록 성공";
path ="view.do?cp=1&no="+result;
}else {
swalIcon = "error";
swalTitle ="게시글 등록 실패";
path = "list.do"; // 게시글 목록
}
request.getSession().setAttribute("swalIcon", swalIcon);
request.getSession().setAttribute("swalTitle", swalTitle);
response.sendRedirect(path);
}
- 파일명 변환을 위한 Class
MyFileRenamePolicy.java
package com.kh.wsp.common;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.oreilly.servlet.multipart.FileRenamePolicy;
public class MyFileRenamePolicy implements FileRenamePolicy {
@Override
public File rename(File originalFile) {
// 업로드된 시간을 파일명에 작성 + _5자리 랜덤 숫자 추가
long currentTime = System.currentTimeMillis();
SimpleDateFormat ft = new SimpleDateFormat("yyyyMMddHHmmss");
int random = (int)(Math.random() * 10000); // 5자리 난수
String str = "_" + String.format("%05d", random);
// %d : 정수
// %5d : 5칸 오른쪽 정렬된 정수
// %05d : 5칸 오른쪽 정렬된 정수, 빈칸에는 0
// 파일명을 변경해도 확장자는 유지되어야 하므로
// 업로드된 원본 파일의 확장자 부분만 얻어오기
int dot = originalFile.getName().lastIndexOf(".");
// 원본 파일명에서 제일 마지막 . 위치
String ext = ""; // 확장자를 저장할 변수
// 파일명에 .이 있으면 찾고, 없으면 찾지 않는다. (없으면 -1반환)
if(dot != -1) {
ext = originalFile.getName().substring(dot);
// hello.jsp --> dot == 5
// originalFile name에서 5번 인덱스 이전까지 잘라내라.(hello)
// ext == .jsp
}
String fileName = ft.format(new Date(currentTime)) + str + ext;
// 20210107102655_74562.png
return new File(originalFile.getParent(), fileName);
}
}
- 첨부파일 vo
Attachment.java
package com.kh.wsp.board.model.vo;
public class Attachment {
private int fileNo;
private String filePath;
private String fileName;
private int fileLevel;
private int parentBoardNo;
public Attachment() {}
public Attachment(int fileNo, String filePath, String fileName, int fileLevel, int parentBoardNo) {
super();
this.fileNo = fileNo;
this.filePath = filePath;
this.fileName = fileName;
this.fileLevel = fileLevel;
this.parentBoardNo = parentBoardNo;
}
public int getFileNo() {
return fileNo;
}
public void setFileNo(int fileNo) {
this.fileNo = fileNo;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public int getFileLevel() {
return fileLevel;
}
public void setFileLevel(int fileLevel) {
this.fileLevel = fileLevel;
}
public int getParentBoardNo() {
return parentBoardNo;
}
public void setParentBoardNo(int parentBoardNo) {
this.parentBoardNo = parentBoardNo;
}
@Override
public String toString() {
return "Attachment [fileNo=" + fileNo + ", filePath=" + filePath + ", fileName=" + fileName + ", fileLevel="
+ fileLevel + ", parentBoardNo=" + parentBoardNo + "]";
}
}
- 게시글 등록+파일업로드 Service
BoardService.java
/** 게시글 등록 Service
* @param map
* @return result
* @throws Exception
*/
public int insertBoard(Map<String, Object> map) throws Exception {
Connection conn = getConnection();
int result =0;
// 1. 게시글 번호 얻어오기
int boardNo = dao.selectNextNo(conn);
if(boardNo>0) {
// 1-1. 얻어 온 게시글 번호를 map에 추가(게시글, 파일정보 삽입 DAO에서 사용하기 위해)
map.put("boardNo", boardNo);
// 2. 글 제목/내용 크로스 사이트 스크립팅 방지 처리
String boardTitle = (String)map.get("boardTitle");
String boardContent = (String)map.get("boardContent");
boardTitle = replaceParameter(boardTitle);
boardContent = replaceParameter(boardContent);
// 3. 글 내용 개행문자 \r\n -> <br> 변경 처리
boardContent = boardContent.replaceAll("\r\n", "<br>");
// 처리된 내용을 다시 map에 추가
map.put("boardTitle", boardTitle);
map.put("boardContent", boardContent);
try {
// 4. 게시글 부분(제목,내용,카테고리)만 BOARD 테이블에 삽입하는 DAO 호출
result = dao.insertBoard(conn,map);
// 5. 파일 정보 부분만 ATTACHMENT 테이블에 삽입하는 DAO 호출
List<Attachment> fList = (List<Attachment>)map.get("fList");
if(result>0 && !fList.isEmpty()) {
// 게시글 부분 삽입 성공 그리고 파일 정보가 있다면
result =0; // result 재활용을 위해 0으로 초기화
// fList의 요소를 하나씩 반복 접근하여
// DAO 메소드를 반복 호출해 정보를 삽입함.
for(Attachment at : fList) {
// 파일 정보가 저장된 Attachment 객체에
// 해당 파일이 작성된 게시글 번호를 추가 세팅
at.setParentBoardNo(boardNo);
result = dao.insertAttachment(conn,at);
if(result==0) { // 삽입 안 됐을 때(실패) 이미지 정보가 DB에 안 들어감
//break; //보류
// 강제로 예외 발생 (사용자 정의 예외)
throw new FileInsertFailedException("파일 정보 삽입 실패");
// 바로 아래있는 catch 구문이 잡는다.
}
}
}
}catch(Exception e) {
// 4,5번에 대한 추가 작업
// 게시글 또는 파일 정보 삽입 중 에러 발생 시 서버에 저장된 파일을 삭제하는 작업이 필요함.
// (보드컨트롤러에서 MultipartRequest객체가 생성되면 바로 서버에 등록이 됨,
// 객체는 생성되었지만 비즈니스 로직 수행 실패 시, 서버에는 있지만 데이터베이스에는 없어짐.. 정보 불일치)
List<Attachment> fList = (List<Attachment>)map.get("fList");
if(!fList.isEmpty()) {
for(Attachment at : fList ) {
String filePath = at.getFilePath();
String fileName = at.getFileName();
File deleteFile = new File(filePath + fileName);
if(deleteFile.exists()) {
// 해당 경로에 해당 파일이 존재하면
deleteFile.delete(); // 해당 파일 삭제
}
}
}
// 에러페이지가 보여질 수 있도록 catch한 Exception을 Controller로 던져준다
throw e;
} // end catch
// 6. 트랜잭션 처리
if(result>0) {
commit(conn);
// 삽입 성공 시 상세 조회 화면으로 이동해야되기 때문에
// 글 번호를 받환할 수 있도록 result에 boardNo를 대입
result = boardNo;
}else {
rollback(conn);
}
}
// 7. 커넥션 반환
close (conn);
// 8. 결과 반환
return result;
}
// 크로스 사이트 스크립팅
// 웹 애플리케이션에서 많이 나타나는 보안 취약점 중 하나로
// 웹 사이트 관리자가 아닌 사용자가 웹 페이지에 악성 스크립트를 삽입할 수 있는 취약점
// 크로스 사이트 스크립팅 방지 메소드
private String replaceParameter(String param) {
String result = param;
if(result != null) {
result = result.replaceAll("&", "&");
result = result.replaceAll("<", "<");
result = result.replaceAll(">", ">");
result = result.replaceAll("\"", """);
}
return result;
}
BoardDAO.java
/** 다음 게시글 번호 조회 DAO
* @param conn
* @return boardNo
* @throws Exception
*/
public int selectNextNo(Connection conn) throws Exception {
int boardNo =0;
String query = prop.getProperty("selectNextNo");
try {
stmt = conn.createStatement();
rset = stmt.executeQuery(query);
if(rset.next()) {
boardNo = rset.getInt(1);
}
}finally {
close(rset);
close(stmt);
}
return boardNo;
}
/** 게시글 삽입 DAO
* @param conn
* @param map
* @return result
* @throws Exception
*/
public int insertBoard(Connection conn, Map<String, Object> map) throws Exception {
int result =0;
String query = prop.getProperty("insertBoard");
try {
pstmt = conn.prepareStatement(query);
pstmt.setInt(1, (int)map.get("boardNo"));
pstmt.setString(2, (String)map.get("boardTitle"));
pstmt.setString(3, (String)map.get("boardContent"));
pstmt.setInt(4, (int)map.get("boardWriter"));
pstmt.setInt(5, (int)map.get("categoryCode"));
result = pstmt.executeUpdate();
}finally {
close(pstmt);
}
return result;
}
/** 파일 정보 삽입 DAO
* @param conn
* @param at
* @return result
* @throws Exception
*/
public int insertAttachment(Connection conn, Attachment at) throws Exception {
int result =0;
String query = prop.getProperty("insertAttachment");
try {
pstmt = conn.prepareStatement(query);
pstmt.setString(1, at.getFilePath());
pstmt.setString(2, at.getFileName());
pstmt.setInt(3, at.getFileLevel());
pstmt.setInt(4, at.getParentBoardNo());
result = pstmt.executeUpdate();
}finally {
close(pstmt);
}
return result;
}
board-query.xml
<!-- 다음 게시글 번호 조회 -->
<entry key="selectNextNo">
SELECT SEQ_BNO.NEXTVAL FROM DUAL
</entry>
<!-- 게시글 삽입 -->
<entry key="insertBoard">
INSERT INTO BOARD(BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_WRITER, CATEGORY_CD)
VALUES(?,?,?,?,?)
</entry>
<!-- 파일 정보 삽입 -->
<entry key="insertAttachment">
INSERT INTO ATTACHMENT
VALUES(SEQ_FNO.NEXTVAL, ?,?,?,?)
</entry>
- 사용자 정의 예외
파일 정보 삽입 실패시 발생할 사용자 정의 예외
FileInsertFailedException
package com.kh.wsp.board.model.exception;
// 파일 정보 삽입 실패시 발생할 사용자 정의 예외
public class FileInsertFailedException extends Exception {
public FileInsertFailedException() {
super();
}
public FileInsertFailedException(String message) {
super(message);
}
}
게시글 상세조회 시 이미지도 조회 가능하게 하기.
- 게시글 상세조회 Controller에 이미지 서비스 호출 코드 추가
BoardController.java
// 게시글 상세조회 Controller *********************************************
else if(command.contentEquals("/view.do")) {
errorMsg = "게시글 상세 조회 과정에서 오류 발생";
int boardNo = Integer.parseInt(request.getParameter("no"));
// 상세조회 비즈니스 로직 수행 후 결과 반환 받기
Board board = service.selectBoard(boardNo);
if(board!=null) { // 상세조회 성공 시
// 해당 게시글에 포함된 이미지 파일 목록 조회 서비스 호출
List<Attachment> fList = service.selectBoardFiles(boardNo);
if(!fList.isEmpty()) { // 해당 게시글 이미지 정보가 DB에 있을 경우
request.setAttribute("fList", fList);
}
path = "/WEB-INF/views/board/boardView.jsp";
request.setAttribute("board", board);
view = request.getRequestDispatcher(path);
view.forward(request, response);
}else {
request.getSession().setAttribute("swalIcon", "error");
request.getSession().setAttribute("swalTitle", "게시글 상세 조회 실패");
response.sendRedirect("list.do");
}
}
BoardService.java
/** 게시글에 포함된 이미지 목록 조회 Service
* @param boardNo
* @return fList
* @throws Exception
*/
public List<Attachment> selectBoardFiles(int boardNo) throws Exception {
Connection conn = getConnection();
List<Attachment> fList = dao.selectBoardFiles(conn,boardNo);
close(conn);
return fList;
}
BoardDAO.java
/** 게시글에 포함된 이미지 목록 조회 DAO
* @param conn
* @param boardNo
* @return fList
* @throws Exception
*/
public List<Attachment> selectBoardFiles(Connection conn, int boardNo)throws Exception {
List<Attachment> fList = null;
String query = prop.getProperty("selectBoardFiles");
try {
pstmt = conn.prepareStatement(query);
pstmt.setInt(1,boardNo);
rset = pstmt.executeQuery();
fList = new ArrayList<Attachment>();
while(rset.next()) {
Attachment at = new Attachment(
rset.getInt("FILE_NO"),
rset.getNString("FILE_NAME"),
rset.getInt("FILE_LEVEL"));
fList.add(at);
}
}finally {
close(rset);
close(pstmt);
}
return fList;
}
board-query.xml
<!-- 게시글에 포함된 이미지 목록 조회 -->
<entry key="selectBoardFiles">
SELECT FILE_NO, FILE_NAME, FILE_LEVEL
FROM ATTACHMENT
WHERE PARENT_BOARD_NO = ?
ORDER BY FILE_LEVEL
</entry>
boardView.jsp
<c:forEach var="file" items="${fList }" varStatus="vs">
<img class="boardImg" id="${file.fileNo }"
src="${contextPath }/resources/uploadImages/${file.fileName}">
<br>
</c:forEach>
게시글 등록 TEST
부트스트랩 사용(이미지 출력 형식 변경하기)
출력 화면
boardView.jsp
<!-- 이미지 출력 -->
<div class="carousel slide boardImgArea" id="board-image">
<!-- 이미지 선택 버튼 -->
<ol class="carousel-indicators">
<c:forEach var="file" items="${fList}" varStatus="vs">
<li data-slide-to="${vs.index }" data-target="#board-image"
<c:if test="${vs.first}"> class="active" </c:if> >
</li>
</c:forEach>
</ol>
<!-- 출력되는 이미지 -->
<div class="carousel-inner">
<c:forEach var="file" items="${fList}" varStatus="vs">
<div class="carousel-item <c:if test="${vs.first}">active</c:if>">
<img class="d-block w-100 boardImg" id="${file.fileNo}"
src="${contextPath}/resources/uploadImages/${file.fileName}">
</div>
</c:forEach>
</div>
<!-- 좌우 화살표 -->
<a class="carousel-control-prev" href="#board-image" data-slide="prev">
<span class="carousel-control-prev-icon"></span> <span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#board-image" data-slide="next">
<span class="carousel-control-next-icon"></span> <span class="sr-only">Next</span>
</a>
</div>
댓글남기기