예제)
테이블, 시퀀스 생성
테이블 생성 (파일 저장용)
create table fileboard(idx number primary key, writer varchar2(20), subject varchar2(100)
, origin varchar2(100), upload varchar(100));
시퀀스 생성 (idx에 제공)
create sequence fileboard_seq;
이름 널? 유형
------- -------- -------------
IDX NOT NULL NUMBER - 글번호
WRITER VARCHAR2(20) - 작성자
SUBJECT VARCHAR2(100) - 제목
ORIGIN VARCHAR2(100) - 사용자로부터 입력받은 파일명
UPLOAD VARCHAR2(100) - 서버에 저장된 파일명
DTO
FileBoard
//DAO 클래스의 메소드에서 사용하기 위한 객체를 표현하기 위한 클래스 - DTO 클래스
// -> 전달값이 저장된 Command 객체를 표현하기 위한 클래스의 기능
@Data
public class FileBoard {
private int idx;
private String writer;
private String subject;
private String origin;
private String upload;
//사용자로부터 입력되어 전달된 파일정보를 저장하기 위한 필드
private MultipartFile multipartFile;
}
Mapper
xml매퍼
interface 매퍼
public interface FileBoardMapper {
int insertFileBoard(FileBoard fileBoard);
int deleteFileBoard(int idx);
FileBoard selectFileBoard(int idx);
List<FileBoard> selectFileBoardList();
}
DAO
//DAO 클래스가 상속받을 인터페이스
public interface FileBoardDAO {
int insertFileBoard(FileBoard fileBoard);
int deleteFileBoard(int idx);
FileBoard selectFileBoard(int idx);
List<FileBoard> selectFileBoardList();
}
@Repository
@RequiredArgsConstructor
public class FileBoardDAOImpl implements FileBoardDAO {
private final SqlSession sqlSession;
@Override
public int insertFileBoard(FileBoard fileBoard) {
return sqlSession.getMapper(FileBoardMapper.class).insertFileBoard(fileBoard);
}
@Override
public int deleteFileBoard(int idx) {
return sqlSession.getMapper(FileBoardMapper.class).deleteFileBoard(idx);
}
@Override
public FileBoard selectFileBoard(int idx) {
return sqlSession.getMapper(FileBoardMapper.class).selectFileBoard(idx);
}
@Override
public List<FileBoard> selectFileBoardList() {
return sqlSession.getMapper(FileBoardMapper.class).selectFileBoardList();
}
}
Service
//서비스 클래스가 상속받을 인터페이스
public interface FileBoardService {
void addFileBoard(FileBoard fileBoard);
void removeFileBoard(int idx);
FileBoard getFileBoard(int idx);
List<FileBoard> getFileBoardList();
}
@Service
@RequiredArgsConstructor
public class FileBoardServiceImpl implements FileBoardService {
private final FileBoardDAO fileBoardDAO;
@Transactional(rollbackFor = Exception.class)
@Override
public void addFileBoard(FileBoard fileBoard) {
fileBoardDAO.insertFileBoard(fileBoard);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void removeFileBoard(int idx) {
/*
if(fileBoardDAO.selectFileBoard(idx) == null) {
throw new Exception();
}
*/
fileBoardDAO.deleteFileBoard(idx);
}
@Override
public FileBoard getFileBoard(int idx) {
return fileBoardDAO.selectFileBoard(idx);
}
@Override
public List<FileBoard> getFileBoardList() {
return fileBoardDAO.selectFileBoardList();
}
}
Controller
FileController
@RequestMapping(value = "/write", method = RequestMethod.GET)
public String fileBoardWrite() {
return "file/board_write";
}
파일 저장 디렉토리 위치
클라이언트가 접근할 수 없도록 하기 위해 업로드 파일을 숨긴 것이다.
클라이언트가 직접 접근하도록 하고 싶으면 resources 폴더에 만들면 된다.
FileController
public class FileController {
//WebApplicationContext 객체(스프링 컨테이너)를 제공받아 필드에 의존성 주입
private final WebApplicationContext context;
//FileBoardService 객체를 제공받아 필드에 의존성 주입
private final FileBoardService fileBoardService;
...
...
@RequestMapping(value = "/write", method = RequestMethod.GET)
public String fileBoardWrite() {
return "file/board_write";
}
@RequestMapping(value = "/write", method = RequestMethod.POST)
public String fileBoardWrite(@ModelAttribute FileBoard fileBoard) throws IllegalStateException, IOException {
if(fileBoard.getMultipartFile().isEmpty()) {//파일이 없는 경우
return "file/board_write";
}
//전달파일을 저장하기 위한 서버 디렉토리의 시스템 경로를 반환받아 저장
// -> 다운로드 프로그램에서만 파일에 접근 가능하도록 /WEB-INF 폴더에 업로드 폴더를 작성 (클라이언트 직접 접근 불가)
String uploadDirectory=context.getServletContext().getRealPath("/WEB-INF/upload");
//사용자로부터 전달받은 파일의 이름을 반환받아 Command 객체의 필드값을 변경
String origin=fileBoard.getMultipartFile().getOriginalFilename();
fileBoard.setOrigin(origin);
//서버 디렉토리에 업로드 처리되어 저장된 파일의 이름을 반환받아 Command 객체의 필드값으로 저장
// => 서버 디렉토리에 저장된 파일 이름은 중복되지 않도록 고유값을 사용
// => 중복되지 않는 고유값을 시스템의 현재 날짜와 시간에 대한 정수값(TimeStamp)으로 사용
String upload=System.currentTimeMillis()+"";//대신 UUID 사용해도 됨
fileBoard.setUpload(upload);
//파일 업로드 처리
fileBoard.getMultipartFile().transferTo(new File(uploadDirectory, upload));
//FILEBOARD 테이블에 행 삽입
fileBoardService.addFileBoard(fileBoard);
return "redirect:/file/list";
}
@RequestMapping(value = "/list")
public String fileBoardList(Model model) {
model.addAttribute("fileBoardList", fileBoardService.getFileBoardList());
return "file/board_list";
}
}
board_write.jsp
<body>
<h1>자료실(입력페이지)</h1>
<hr>
<form action="<c:url value="/file/write"/>" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>작성자</td>
<td><input type="text" name="writer" value="${fileBoard.write }"></td>
</tr>
<tr>
<td>제목</td>
<td><input type="text" name="subject" value="${fileBoard.subject }"></td>
</tr>
<tr>
<td>파일</td>
<td><input type="file" name="multipartFile"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">파일전송</button></td>
</tr>
</table>
</form>
</body>
board_list.jsp
<body>
<h1>자료실(출력페이지)</h1>
<hr>
<table>
<tr>
<th width="50">번호</th>
<th width="100">작성자</th>
<th width="300">제목</th>
<th width="350">파일명</th>
<th width="100">다운로드</th>
<th width="100">삭제</th>
</tr>
<c:forEach var="fileBoard" items="${fileBoardList }">
<tr>
<td align="center">${fileBoard.idx }</td>
<td align="center">${fileBoard.writer }</td>
<td>${fileBoard.subject }</td>
<td>${fileBoard.origin }</td>
<td align="center">
<button type="button" onclick="fileDownload(${fileBoard.idx });">다운로드</button>
</td>
<td align="center">
<button type="button" onclick="fileDelete(${fileBoard.idx });">삭제</button>
</td>
</tr>
</c:forEach>
</table>
<p>
<button type="button" onclick="location.href='<c:url value="/file/write"/>';">업로드</button>
</p>
<script type="text/javascript">
function fileDownload(idx) {
//URL 주소를 이용하여 자료실 번호 전달
location.href="<c:url value="/file/download"/>?idx="+idx;
}
function fileDelete(idx) {
if(confirm("자료를 정말로 삭제 하시겠습니까?")) {
location.href="<c:url value="/file/delete"/>?idx="+idx;
}
}
</script>
</body>
삭제 기능 구현하기
FileController
@RequestMapping("/delete")
public String fileBoardDelete(@RequestParam int idx) {
FileBoard fileBoard=fileBoardService.getFileBoard(idx);
String uploadDirectory=context.getServletContext().getRealPath("/WEB-INF/upload");
//서버 디렉토리에 저장된 업로드 파일을 삭제 처리
new File(uploadDirectory, fileBoard.getUpload()).delete();
fileBoardService.removeFileBoard(idx);
return "redirect:/file/list";
}
다운로드 기능 구현하기
다운로드(Download)
: 서버 디렉토리에 존재하는 파일을 클라이언트에게 전달하여 저장하는 기능
요청 처리 메소드에 의해 반환되는 문자열(VeiwName)으로 다운로드 프로그램을 실행하여 서버 디렉토리에 저장된 파일을 클라이언트에게 전달되도록 응답 처리할 것이다.
- BeanNameViewResolver 객체를 사용하여 반환되는 문자열(VeiwName)로 특정 프로그램을 실행하여 응답 처리할 것임
- Spring Bean Configuration File(servlet-context.xml)에 BeanNameViewResolver 클래스를 Srping Bean으로 등록해야 함
- 현재 사용중인 VeiwResolver 객체(JSP 문서 응답 처리)보다 먼저 실행될 수 있도록 우선권을 설정해야 함
BeanNameViewResolver 객체는 요청 처리 메소드에서 반환되는 문자열(ViewName)을 제공받아 같은 Spring Bean(객체) 중 같은 이름의 식별자(beanName)의 Spring Bean(객체)로 실행 메소드를 호출하여 클라이언트에게 응답 처리할 수 있도록 해준다.
- JSP 문서를 이용하여 응답 처리하지 않고 메소드의 명령을 실행하여 응답 처리할 수 있도록 한다.
- JSP 문서로 응답 처리하는 ViewResolver 객체보다 반드시 우선순위를 높게 설정해야 한다.
servlet-context.xml
BeanNameViewResolver 클래스를 Spring Bean으로 등록
<beans:bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<beans:property name="order" value="1"/>
</beans:bean>
파일 다운로드 기능을 제공하기 위한 클래스 FileDownload를 작성해보자
- FileDownload 클래스는 BeanNameResolver 객체에 의해 실행되는 클래스이며 spring Bean Configuration File(servlet-context.xml)에 Spring Bean으로 등록해주어야 한다.
- BeanNameViewResolver 객체에 의해 실행될 클래스는 반드시 AbstractView 클래스를 상속받아 작성해야 한다.
- renderMergedOutputModel() 메소드를 오버라이드 선언하여 응답 처리에 필요한 명령을 작성해주면 된다.
메소드 정리
- AbstractView.setContentType(String contentType) : AbstractView 객체에 저장될 클라이언트의 응답 파일형식(MimeType)을 변경하는 메소드
- AbstractView.getContentType() : AbstractView 객체에 저장될 클라이언트의 응답 파일형식(MimeType)을 반환하는 메소드
- response.setContentLengthLong(long len) : 클라이언트에게 파일의 크기를 전달하는 메소드
- FileCopyUtils.copy(InputStream in, OutputStream out) : 입력스트림으로 원시데이터를 읽어 출력 스트림으로 전달하여 저장하는 메소드 - 복사
FileDowndoad
public class FileDownload extends AbstractView {
public FileDownload() {
//클라이언트에게 응답될 파일형식(Mimetype)을 변경
setContentType("application/download; utf-8");
}
//BeanNameViewResolver 객체에 의해 자동 호출되는 메소드(실행 메소드)
// => model 매개변수에는 요청 처리 메소드(fileBoardDownload())에서 제공된 속성값이 엔트리로 저장된 Map 객체를 제공받아 사용
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
//요청 처리 메소드에서 제공된 속성값(다운로드 관련 파일 정보)을 객체로 반환받아 저장
String uploadDirectroy=(String)model.get("uploadDirectroy");
String originalFilename=(String)model.get("originalFilename");
String uploadFilename=(String)model.get("uploadFilename");
//서버 디렉토리에 저장된 업로드 파일에 대한 File 객체 생성
File file=new File(uploadDirectroy, uploadFilename);
//클라이언트에게 파일을 전달하여 저장하기 위한 파일형식(MimeType)을 클라이언트에게 전달
response.setContentType(getContentType());//AbstractView 객체에 저장될 클라이언트의 응답 파일형식을 반환받아 변경
response.setContentLengthLong((int)file.length());//클라이언트에게 파일의 크기를 전달
//클라이언트에게 저장될 파일명을 클라이언트에게 전달
// => 파일명에 대한 한글이 존재할 경우 부호화 처리하여 전달
originalFilename=URLEncoder.encode(originalFilename, "utf-8");
response.setHeader("Content-Disposition", "attachement;filename=\""+originalFilename+"\";");
//파일을 클라이언트에게 전달하기 위한 파일 출력스트림을 반환받아 저장
OutputStream out=response.getOutputStream();
//서버 디렉토리에 저장된 업로드 파일을 읽기 위한 입력스트림을 생성하여 저장
InputStream in=new FileInputStream(file);
//입력스트림으로 원시데이터를 읽어 출력 스트림으로 전달하여 저장 (복사)
FileCopyUtils.copy(in, out);//다운로드 처리
in.close();
}
}
servlet-context.xml
파일 다운로드 기능을 제공하는 클래스를 Spring Bean으로 등록
-> Spring Bean의 식별자(beanName)를 반드시 요청 처리 메소드의 반환값과 같도록 작성해야함
<beans:bean class="xyz.itwill10.util.FileDownload" id="fileDownload"/>
FileController
//BeanNameViewResolver 객체를 사용하여 반환되는 문자열(VeiwName)로 특정 프로그램을 실행하여 응답 처리
@RequestMapping("/download")
public String fileBoardDownload(@RequestParam int idx, Model model) {
FileBoard fileBoard=fileBoardService.getFileBoard(idx);
//Model 객체를 이용하여 실행될 프로그램(Spring Bean)에서 사용될 객체를 속성값으로 저장하여 제공
model.addAttribute("uploadDirectory", context.getServletContext().getRealPath("/WEB-INF/upload"));
model.addAttribute("originalFilename", fileBoard.getOrigin());
model.addAttribute("uploadFilename", fileBoard.getUpload());
//실행될 프로그램(Spring Bean)의 식별자(beanName) 반환
// => 실행될 프로그램에 대한 클래스를 작성하여 Spring Bean Configuration File
//(servlet-context.xml)에 Spring Bean으로 등록 - 어노테이션 사용 가능
return "fileDownload";
}
페이징 처리하기 (fileBoardList 메소드)
매퍼 수정
FileBoardMapper.xml (XML)
<!--
<select id="selectFileBoardList" resultType="FileBoard">
select idx, writer, subject, origin, upload from fileboard order by idx desc
</select>
-->
<select id="selectFileBoardList" resultType="FileBoard">
select * from (select rownum rn, board.* from (select idx, writer, subject, origin, upload
from fileboard order by idx desc) board) where rn between #{startRow} and #{endRow}
</select>
<select id="selectFileBoardCount" resultType="int">
select count(*) from fileboard
</select>
FileBoardMapper.java (인터페이스)
public interface FileBoardMapper {
int insertFileBoard(FileBoard fileBoard);
int deleteFileBoard(int idx);
FileBoard selectFileBoard(int idx);
//List<FileBoard> selectFileBoardList();
List<FileBoard> selectFileBoardList(Map<String,Object> map);
int selectFileBoardCount();
}
DAO 변경
DAO (DAO 클래스가 상속받을 인터페이스)
public interface FileBoardDAO {
int insertFileBoard(FileBoard fileBoard);
int deleteFileBoard(int idx);
FileBoard selectFileBoard(int idx);
//List<FileBoard> selectFileBoardList();
List<FileBoard> selectFileBoardList(Map<String,Object> map);
int selectFileBoardCount();
}
DAOImpl (DAO 클래스)
...
@Override
public List<FileBoard> selectFileBoardList(Map<String, Object> map) {
return sqlSession.getMapper(FileBoardMapper.class).selectFileBoardList(map);
}
@Override
public int selectFileBoardCount() {
return sqlSession.getMapper(FileBoardMapper.class).selectFileBoardCount();
}
...
Service 변경
인터페이스
//List<FileBoard> getFileBoardList();
Map<String, Object> getFileBoardList(int pageNum);
서비스 클래스
FileBoardServiceImpl
변경 전
@Override
public List<FileBoard> getFileBoardList() {
return fileBoardDAO.selectFileBoardList();
}
변경 후
//매개변수로 요청 페이지 번호를 전달받아 게시글 목록이 저장된 객체와 페이지 번호 관련 객체를
//Map 객체의 엔트리로 추가하여 반환하는 메소드
@Override
public Map<String, Object> getFileBoardList(int pageNum) {
//FILEBOARD 테이블에 저장된 모든 게시글의 갯수를 검색하여 반환하는 DAO 클래스의 메소드 호출
int totalBoard=fileBoardDAO.selectFileBoardCount();
int pageSize=5;//하나의 페이지에 출력될 게시글의 갯수 저장
int blockSize=5;//하나의 블럭에 출력될 페이지의 갯수 저장
//Pager 클래스로 객체를 생성하여 저장 - 생성자 매개변수에 페이징 처리 관련 값을 전달
// => Pager 객체 : 페이징 처리 관련 값이 저장된 객체
Pager pager=new Pager(pageNum, totalBoard, pageSize, blockSize);
//FileBoardDAO 클래스의 selectFileBoardList() 메소드를 호출하기 위해 매개변수에 전달하여
//저장될 Map 객체(시작 행번호, 종료 행번호) 생성
Map<String, Object> pageMap=new HashMap<String, Object>();
pageMap.put("startRow", pager.getStartRow());
pageMap.put("endRow", pager.getEndRow());
List<FileBoard> fileBoardList=fileBoardDAO.selectFileBoardList(pageMap);
//Controller 클래스에 반환되는 결과값을 제공하기 위한 Map 객체(시작 행번호, 종료 행번호) 생성
Map<String, Object> resultMap=new HashMap<String, Object>();
resultMap.put("pager", pager);
resultMap.put("fileBoardList", fileBoardList);
return resultMap;
}
Pager 클래스 선언
//페이징 처리 관련 값을 필드에 저장하기 위한 클래스
@Data
public class Pager {
//생성자를 이용하여 초기값을 전달받아 필드에 저장
private int pageNum;//요청 페이지의 번호
private int totalBoard;//전체 게시글의 갯수
private int pageSize;//하나의 페이지에 출력될 게시글의 갯수
private int blockSize;//하나의 블럭에 출력될 페이지 번호의 갯수
//생성자로 초기화된 필드값을 계산하여 결과값을 필드에 저장
private int totalPage;//전체 페이지의 갯수
private int startRow;//요청 페이지에 출력될 게시글의 시작 행번호
private int endRow;//요청 페이지에 출력될 게시글의 종료 행번호
private int startPage;//현재 블럭에 출력될 시작 페이지 번호
private int endPage;//현재 블럭에 출력될 종료 페이지 번호
private int prevPage;//이전 블럭에 출력될 시작 페이지 번호
private int nextPage;//다음 블럭에 출력될 시작 페이지 번호
public Pager(int pageNum, int totalBoard, int pageSize, int blockSize) {
super();
this.pageNum = pageNum;
this.totalBoard = totalBoard;
this.pageSize = pageSize;
this.blockSize = blockSize;
calcPage();
}
//계산된 결과값을 필드에 저장하는 메소드 - 생성자에서 호출하여 사용
private void calcPage() {
totalPage=(int)Math.ceil((double)totalBoard/pageSize);
if(pageNum<=0 || pageNum>totalPage) {
pageNum=1;
}
startRow=(pageNum-1)*pageSize+1;
endRow=pageNum*pageSize;
if(endRow>totalBoard) {
endRow=totalBoard;
}
startPage=(pageNum-1)/blockSize*blockSize+1;
endPage=startPage+blockSize-1;
if(endPage>totalPage) {
endPage=totalPage;
}
prevPage=startPage-blockSize;
nextPage=startPage+blockSize;
}
}
FileController 변경
변경전
@RequestMapping("/list")
public String fileBoardList(Model model) {
model.addAttribute("fileBoardList", fileBoardService.getFileBoardList());
return "file/board_list";
}
변경 후
@RequestMapping("/list")
public String fileBoardList(@RequestParam(defaultValue = "1") int pageNum,Model model) {
//System.out.println("pageNum = "+pageNum);
Map<String, Object> map=fileBoardService.getFileBoardList(pageNum);
model.addAttribute("pager", map.get("pager"));
model.addAttribute("fileBoardList", map.get("fileBoardList"));
return "file/board_list";
}
board_list.jsp 에서 번호 출력되도록 수정
<%@ 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>SPRING</title>
<style type="text/css">
table {
border: 1px solid black;
border-collapse: collapse;
}
th, td {
border: 1px solid black;
padding: 2px;
}
</style>
</head>
<body>
<h1>자료실(출력페이지)</h1>
<hr>
<p>
<button type="button" onclick="location.href='<c:url value="/file/write"/>';">업로드</button>
</p>
<table>
<tr>
<th width="50">번호</th>
<th width="100">작성자</th>
<th width="300">제목</th>
<th width="350">파일명</th>
<th width="100">다운로드</th>
<th width="100">삭제</th>
</tr>
<%-- 게시글 목록 출력 --%>
<c:forEach var="fileBoard" items="${fileBoardList }">
<tr>
<td align="center">${fileBoard.idx }</td>
<td align="center">${fileBoard.writer }</td>
<td>${fileBoard.subject }</td>
<td>${fileBoard.origin }</td>
<td align="center">
<button type="button" onclick="fileDownload(${fileBoard.idx });">다운로드</button>
</td>
<td align="center">
<button type="button" onclick="fileDelete(${fileBoard.idx });">삭제</button>
</td>
</tr>
</c:forEach>
</table>
<%-- 페이지 번호 출력 --%>
<c:choose>
<c:when test="${pager.startPage > pager.blockSize }">
<a href="<c:url value="/file/list"/>?pageNum=${pager.prevPage}">[이전]</a>
</c:when>
<c:otherwise>
[이전]
</c:otherwise>
</c:choose>
<c:forEach var="i" begin="${pager.startPage }" end="${pager.endPage }" step="1">
<c:choose>
<c:when test="${pager.pageNum != i }">
<a href="<c:url value="/file/list"/>?pageNum=${i}">[${i }]</a>
</c:when>
<c:otherwise>
[${i }]
</c:otherwise>
</c:choose>
</c:forEach>
<c:choose>
<c:when test="${pager.endPage != pager.totalPage }">
<a href="<c:url value="/file/list"/>?pageNum=${pager.nextPage}">[다음]</a>
</c:when>
<c:otherwise>
[다음]
</c:otherwise>
</c:choose>
<script type="text/javascript">
function fileDownload(idx) {
//질의문자열를 이용하여 자료실 번호 전달
location.href="<c:url value="/file/download"/>?idx="+idx;
}
function fileDelete(idx) {
if(confirm("자료를 정말로 삭제 하시겠습니까?")) {
location.href="<c:url value="/file/delete"/>?idx="+idx;
}
}
</script>
</body>
</html>
'학원 > 복기' 카테고리의 다른 글
[Spring] @RestController - 수정 (0) | 2023.08.19 |
---|---|
[Spring] RESTful API (0) | 2023.08.19 |
[Srping] commons fileupload 라이브러리 이용한 파일 처리 (0) | 2023.08.15 |
[Spring] 인터셉터(Interceptor) (0) | 2023.08.14 |
[Spring] 예외 클래스 / ExceptionHandler / jbcrypt 라이브러리 (0) | 2023.08.11 |