File Upload

MultipartFile

DB

create table memboard(num smallint auto_increment primary key,
--varchar 컬럼등 생략
content varchar(2000),
uploadfile varchar(500),
viewcount smallint default 0,
writeday datetime);
  • uploadfile는 업로드할 파일의 명칭을 자의적으로 지정해서 저장하기 위한 컬럼

DTO_MultipartFile

@Data
@Alias("memboard")
public class MemBoardDto {

	private String num;
	//String 타입 생략
	private String content;
	private String uploadfile;
	private MultipartFile multi; //여기서 form의 이름과 맞춰준다
	private int viewcount;
	private Timestamp writeday;
}
  • <form>으로 전송 시 <input type=”file”>MultipartFile 객체의 형태로 전송되므로 String uploadfile에는 저장 및 출력되지 않음
  • 따라서 MultipartFile 객체 형태의 DTO 변수로 지정 (file 타입의 input name 속성 값과 일치)

Insert Form

<form action="insert" method="post" enctype="multipart/form-data">
		<table>
			<tr>
				<th>제목</th>
				<td><input type="text" name="subject" class="form-control" required="required">
			</tr>
			<tr>
				<th>파일업로드</th>
				<td><input type="file" name="multi" class="form-control"></td>
			</tr>
			<tr>
				<td colspan="2">
					<textarea name="content" class="form-control" required="required"></textarea>
				</td>
			</tr>
			<tr>
				<td colspan="2" align="center">
					<button type="submit">등록</button>
				</td>
			</tr>
		</table>
	</form>
  • 파일 업로드를 위한 <form>이므로 반드시 enctype=”multipart/form-data” 속성 지정
  • <input type=”file”>name 속성은 DTO의 변수명과 일치 (DTO를 통해 교환되므로)

Mapper_SQL

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="boot.data.mapper.MemBoardMapperInter">
	<select id="getMaxNum" resultType="int">
		select ifnull(max(num),0) from memboard
	</select>
	<select id="getList" parameterType="HashMap" resultType="memboard">
		select * from memboard order by num desc limit #{start},#{perpage}
	</select>
</mapper>

Service

@Service
public class MemBoardService implements MemBoardServiceInter {
	
	@Autowired
	MemBoardMapperInter mapperInter;

	//메서드 생략
}

Controller_Insert Logic

@Controller
@RequestMapping("/memboard")
public class MemBoardController {
	
	@Autowired
	MemBoardService service;

	@PostMapping("/insert")
	public String insert(@ModelAttribute MemBoardDto dto,HttpSession session) {
		
		SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMddHHmmss");
		
		String path=session.getServletContext().getRealPath("/savefile");
		String uploadfile=sdf.format(new Date())+"_"+dto.getMulti().getOriginalFilename();
		
		if(dto.getMulti().getOriginalFilename().equals(""))
			dto.setUploadfile("no"); //업로드한 파일이 없으면 no라고 저장
		else {
			try {
				dto.getMulti().transferTo(new File(path+"\\"+uploadfile));
			} catch (IllegalStateException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			dto.setUploadfile(uploadfile);
		}
		dto.setMyid((String)session.getAttribute("myid"));
		dto.setName((String)session.getAttribute("loginname"));
		
		service.insertBoard(dto);
		
		return "redirect:content?num="+service.getMaxNum();
	}
}
  • 업로드한 파일은 DTO의 MultipartFile 객체에 저장되어있음 (이를 호출하여 처리 가능)
    - DB의 uploadfile 컬럼은 실제 파일이 아닌, 처리된 String(varchar) 형태의 자료가 저장됨 (DTO를 통해 DB에 저장하기 위해서는 set 해주어야함)
    - 실제 파일은 transferTo() 객체로 실제 저장 경로에 저장
  • Insert Form에서 입력하지 않은 기본 정보는 Controller에서 별도로 set 하여 DTO를 통해 저장
  • insert하는 동시에 상세페이지(content)에 이동하여 해당 데이터를 출력하기 위해 DB의 최종 시퀀스 값을 구하여 넘거주어야 함 (getMaxNum())

Controller_Detail Page

@Controller
@RequestMapping("/memboard")
public class MemBoardController {

	@GetMapping("/content")
	public ModelAndView content(@RequestParam String num,int currentPage) {
		
		ModelAndView model=new ModelAndView();
		model.setViewName("/memboard/content");

		service.updateviewcount(num);
		
		MemBoardDto dto=service.getData(num);
		
		model.addObject("dto", dto);
		
		//업로드파일의 확장자 얻기
		int dotloc=dto.getUploadfile().lastIndexOf("."); //마지막 점의 위치
		String ext=dto.getUploadfile().substring(dotloc+1);
		//System.out.println(dotloc+","+ext);
		
		if(ext.equalsIgnoreCase("jpg")||ext.equalsIgnoreCase("gif")||
			ext.equalsIgnoreCase("png")||ext.equalsIgnoreCase("jpeg"))
			model.addObject("bupload", true);

		model.addObject("currentPage", currentPage);
		
		return model;
	}
}
  • 업로드한 파일이 이미지 파일이 아닐 수 있으므로, 파일의 확장자를 구분하여 조건 지정
    • 파일의 확장자가 이미지 관련이면 특정 boolean 데이터를 View에 전달 (bupload, true)
    • 확장자명은 대소문자 구분을 무시하기 위해 equalsIgnoreCase() 사용
  • 전체 목록은 Pagination 처리를 할 것이므로 현재 페이지 데이터(currentPage)를 항상 넘겨주고 받아야 함

Download File

Controller

@Controller
public class DownloadController {

	//외부서버의 파일을 내 컴퓨터로 다운로드하는소스
	@GetMapping("/memboard/download")
	public void download(HttpServletRequest request,
			HttpServletResponse response,
			@RequestParam String clip)
	{
		String path=request.getSession().getServletContext().getRealPath("/savefile");
		File file=new File(path+"\\"+clip);
		System.out.println("파일 경로:"+file);
		setHeaderType(response, request, file);

		try {
			transport(new FileInputStream(file),
					response.getOutputStream(), file);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}  
	}
	private void setHeaderType(HttpServletResponse response,
			HttpServletRequest request,
			File file)
	{
		String mime = request.getSession().getServletContext().getMimeType(file.toString());
		if(mime != null)
			mime = "application/octet-stream";
		response.setContentType(mime);
		response.setHeader("Content-Disposition",
				"attachment;filename=" + toEng(file.getName()));
		response.setHeader("Content-Length", "" + file.length());

	}

	private void transport(InputStream in, OutputStream out, File file)
			throws IOException
	{
		BufferedInputStream bin = null;
		BufferedOutputStream bos = null;

		try{
			bin = new BufferedInputStream(in);
			bos = new BufferedOutputStream(out);

			byte[] buf=new byte[(int)file.length()];
			int read=0;
			while((read = bin.read(buf)) != -1)
			{
				bos.write(buf, 0, read);   //객체, 시작(offset), 길이
			}
		}catch(Exception e){
			System.out.println("transport error : " + e);
		}finally{
			bos.close();
			bin.close();
		}
	}
	//////////////////////////////////////////////////////////////
	public String toEng(String str)
	{
		String tmp=null;
		try{
			tmp = new String(str.getBytes("utf-8"), "8859_1");
		}catch(Exception e){}
		return tmp;
	}
}
  • 외부 서버의 파일을 다운로드 받기 위한 Controller
  • 다운로드 받을 파일명을 clip 변수로 받으므로 매핑 시 clip으로 전달 (임의 지정 가능)
    • clip 변수로 해당 클래스의 매핑 주소(/memboard/download)로 데이터 이동 시, 해당 데이터를 자동 다운로드

View_Detail Page

<c:if test="${dto.uploadfile!='no' }">
	<span style="float: right"><a href="download?clip=${dto.uploadfile }">
		<i class="bi bi-arrow-down-circle"></i>&nbsp;<b>${dto.uploadfile }</b></a>
	</span>
</c:if>
  • <c:if>조건으로 업로드한 파일이 있을 경우(DB에 저장된 파일 컬럼 데이터가 존재할 경우)만 다운로드 링크 출력
  • 파일명(링크) 클릭 시 <a>의 매핑 주소로 이동하여 해당 파일 자동 다운로드
    • 매핑 주소가 /memboard/download이므로 링크 주소는 download
    • 전달 데이터를 Controller에서 clip으로 받으므로 clip을 넘겨주며, 그 값은 실제 저장 경로에 저장된 파일명(그대로 DB에 저장해놓음)

  • 예시 이미지의 링크를 클릭 시, 저장 경로에 해당 이름으로 저장된 파일이 자동 다운로드 됨
<c:if test="${bupload}">
	<img src="../savefile/${dto.uploadfile }">
</c:if>
  • 업로드된 파일이 이미지가 아닐 경우, src 경로가 올바르게 지정되어도 View에서는 xbox 출력
  • 따라서 해당 파일이 이미지 파일인지 여부에 따른 조건문 생성
    • Controller에서 파일의 확장자를 구분하여 특정 boolean 데이터 전달
    • 전달한 데이터 조건(bupload)에 따라 파일을 <img> src 경로에 삽입한 정보를 출력할지 결정

  • 이미지가 아닌 파일을 업로드 했을 때

  • 이미지 파일을 업로드 했을 때
<c:if test="${sessionScope.loginok!=null }">
	<button type="button" onclick="loaction.href='form'">글쓰기</button>
</c:if>
<c:if test="${sessionScope.loginok!=null and sessionScope.myid==dto.myid }">
	<button type="button" onclick="loaction.href='updateform?num=${dto.num}'">수정</button>
	<button type="button" onclick="loaction.href='delete?num=${dto.num}'">삭제</button>
</c:if>
<button type="button" onclick="loaction.href='list'">목록</button>
  • 글쓰기 버튼 : 로그인한 경우에만 출력
  • 수정, 삭제 버튼 : 로그인했으며, 로그인 아이디가 게시물 작성자 아이디와 동일한 경우만 출력
  • 목록 버튼 : 항상 출력

Pagination

Controller

@Controller
@RequestMapping("/memboard")
public class MemBoardController {
	
	@Autowired
	MemBoardService service;

	@GetMapping("/list")
	public ModelAndView list(@RequestParam(value = "currentPage",defaultValue = "1") int currentPage) {
		
		ModelAndView model=new ModelAndView();
		model.setViewName("/memboard/memList");
		
		int totalcount=service.getTotalCount();
		
		model.addObject("totalcount", totalcount);
		
		//페이징처리에 필요한 변수선언
		int totalPage; //총 페이지수
		int startPage; //각블럭에서 보여질 시작페이지
		int endPage; //각블럭에서 보여질 끝페이지
		int startNum; //db에서 가져올 글의 시작번호(mysql은 첫글이 0,오라클은 1)
		int perPage=10; //한페이지당 보여질 글의 갯수
		int perBlock=5; //한블럭당 보여질 페이지 개수
		
		totalPage=totalcount/perPage+(totalcount%perPage==0?0:1);

		startPage=(currentPage-1)/perBlock*perBlock+1;
		     
		endPage=startPage+perBlock-1;
 
		 if(endPage>totalPage)
			 endPage=totalPage;

		startNum=(currentPage-1)*perPage;
		int printNum=totalcount-startNum;
		
		List<MemBoardDto> list=service.getList(startNum, perPage);
		
		model.addObject("list", list);
		model.addObject("currentPage", currentPage);
		model.addObject("totalpage", totalPage);
		model.addObject("startpage", startPage);
		model.addObject("endpage", endPage);
		model.addObject("startnum", startNum);
		model.addObject("perpage", perPage);
		model.addObject("perblock", perBlock);
		model.addObject("printnum", printNum);
		
		return model;
	}
}
  • 목록의 현재 페이지 데이터(currentPage)는 항상 연속적으로 전달 (초기 데이터는 null이므로 defaultValue=1로 지정 : 첫 시작 시 1페이지)
  • Pagination에 필요한 데이터 생성 및 전달
    • 전체 페이지 수 : Sql을 통해 전체 데이터의 count(*) 조회
    • 페이징 블럭(perPage) 당 시작 페이지 :
      • 페이징 블럭에 필요한 페이지 개수는 양의 정수를 perPage로 나누었을 때 도출 가능한 나머지 개수
      • / 연산자를 통해 나머지 제거 후 * 연산을 하여 나머지 제거 (동일한 몫을 가진 페이지는 동일한 페이징 블럭에 속함)
      • 현재 페이지 -1을 하지 않으면 perPage의 배수에 해당하는 페이지의 경우, 해당 페이지보다 +1인 페이지가 시작 페이지(startPage)가 되는 모순 발생
    • 페이징 블럭 당 끝 페이지 : 첫 페이지 + 블럭 당 페이지 개수
    • 페이지 별 시작 데이터의 순번 : 직전 페이지까지의 모든 데이터 +1
      • Sql문에서는 limit a,ba에 해당
      • 일반적으로 최신순(desc)으로 출력하므로 Sql 조회 결과의 순서와 View에서 출력하는 순서가 반대
      • View의 출력 순번 : 전체 페이지 - Sql 조회 결과의 순번
  • 생성한 모든 데이터를 View에 전달하여 사용

View_Full List

<c:if test="${totalcount==0 }">
	<tr>
		<td colspan="5" align="center"><h4>등록된 글이 없습니다</h4></td>
	</tr>
</c:if>
<c:if test="${totalcount!=0 }">
	<c:forEach var="dto" items="${list }">
		<tr>
			<td>${printnum }</td>
			<c:set var="printnum" value="${printnum-1 }"></c:set>
			<td><a href="content?num=${dto.num }&currentPage=${currentPage}">${dto.subject }</a></td>
			<td>${dto.name } (${dto.myid })</td>
			<td>${dto.viewcount }</td>
			<td><fmt:formatDate value="${dto.writeday }" pattern="yyyy-MM-dd"/>
		</tr>
	</c:forEach>
</c:if>
  • <c:forEach>로 데이터 개수만큼 반복
    • Controller에서 현재 페이지인 currentPage에 해당하는 데이터만 조회하므로(limit 조건) 조회 데이터 전체 출력해도 자동으로 Pagination 처리됨
    • JSTL은 증감 연산자가 없으므로 <c:set>으로 반복 시마다 출력 순번을 조작해주어야 함
<!-- 페이징 -->
<c:if test="${totalcount>0 }">
	<div style="width: 800px;text-align: center">
		<ul class="pagination justify-content-center">
			<!-- 이전 -->
			<c:if test="${startPage>1 }">
				<li class="page-item"><a href="list?currentPage=${startPage-1 }">이전</a></li>
			</c:if>
			
			<c:forEach var="pp" begin="${startPage }" end="${endPage }">
				<c:if test="${pp==currentPage }">
					<li class="page-item active">
						<a class="page-link" href="list?currentPage=${pp }">${pp }</a>
					</li>
				</c:if>
				<c:if test="${pp!=currentPage }">
					<li class="page-item">
						<a class="page-link" href="list?currentPage=${pp }">${pp }</a>
					</li>
				</c:if>
			</c:forEach>
			
			<!-- 다음 -->
			<c:if test="${endPage<totalPage }">
				<li class="page-item"><a href="list?currentPage=${endPage+1 }">다음</a></li>
			</c:if>
		</ul>
	</div>
</c:if>
  • Pagination 자체의 출력 조건 : 전체 데이터가 하나 이상 존재
  • ‘이전(←)’ 버튼
    • 출력 조건 : 페이징 블럭이 최초 값(첫번째 블럭)이 아닐 경우
    • 클릭 시 이벤트 : 현재 블럭의 시작 페이지 직전 페이지로 이동
  • ‘다음(→)’ 버튼
    • 출력 조건 : 페이징 블럭이 최후 값(마지막 블럭)이 아닐 경우
    • 클릭 시 이벤트 : 현재 블럭의 끝 페이지 직후 페이지로 이동
  • 중간 페이지 : 블럭 당 시작 페이지부터 끝 페이지까지 전부 출력
    • 출력되는 페이지 중 현재 페이지와 같은 페이지는 BootStrap 효과를 통해 구별
profile
초보개발자

0개의 댓글