File Upload
MultipartFile
DB
create table memboard(num smallint auto_increment primary key,
content varchar(2000),
uploadfile varchar(500),
viewcount smallint default 0,
writeday datetime);
- uploadfile는 업로드할 파일의 명칭을 자의적으로 지정해서 저장하기 위한 컬럼
 
DTO_MultipartFile
@Data
@Alias("memboard")
public class MemBoardDto {
	private String num;
	
	private String content;
	private String uploadfile;
	private MultipartFile multi; 
	private int viewcount;
	private Timestamp writeday;
}
<form>으로 전송 시 <input type=”file”>은 MultipartFile 객체의 형태로 전송되므로 String uploadfile에는 저장 및 출력되지 않음 
- 따라서 
MultipartFile 객체 형태의 DTO 변수로 지정 (file 타입의 input name 속성 값과 일치) 
<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"); 
		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);
		
		
		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);   
			}
		}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> <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>
- 글쓰기 버튼 : 로그인한 경우에만 출력
 
- 수정, 삭제 버튼 : 로그인했으며, 로그인 아이디가 게시물 작성자 아이디와 동일한 경우만 출력
 
- 목록 버튼 : 항상 출력
 
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; 
		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,b의 a에 해당 
- 일반적으로 최신순(
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 }¤tPage=${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 효과를 통해 구별