2022. 4. 29. 12:47ㆍ[Spring]_/[Spring]_포트폴리오 페이지 만들기
해당 게시글은 업로드 1편 , 다운로드 2편으로 구성되어 있습니다.
[환경]
개발툴 : IntelliJ
DB : oracle
프레임워크 : spring , mybatis
사용 언어 : ES6, Java , Html5 , CSS
완성 화면]
input type="file" 생성
해당 버튼 클릭시 파일 선택하는 창이 뜸
한가지 or 다중 파일 선택시 하단에 썸네일, 이름 , 삭제버튼 생성
삭제버튼 클릭시 해당 row 삭제와 동시에 전달하는 데이터도 삭제
등록후 해당 게시물 조회시
정상적으로 파일 이름과 함께 다운로드가 되는 모습
개발 목표 : SPA 로 구현한 게시판에서 글 작성시 파일 업로드 기능 추가,
파일 업로드시 Mulitple 속성 부여하여 여러개의 파일 업로드 하도록 구현파일 다운로드시 글 작성자가 저장한 파일 명 그대로 다운로드하도록 구현, 한국어 인코딩 추가
1] 구성
유저가 파일을 업로드 할 때
- > Input type=file 태그를 활용하여 FormData 형태의 구조로 Controller에 전달
-> Controller에서는 전달받은 데이터를 핸들링하는 Service 호출
-> Service에서 해당 데이터를 매핑하여 반환
-> Controller에서 반환된 데이터를 가지고 DB 입력하는 Service 호출
-> 호출받은 Service가 해당 데이터를 DB에 입력
-> 호출 끝
유저가 파일을 다운로드 할 때
-> 목록에서 row 선택
-> 선택시 상세화면이 열리면서 해당 게시글의 filename과 filerealname을 DB에서 전달받음
-> 첨부파일의 a태그 선택
-> 매핑되어있는 url 로 Download Controller 호출과 동시에 HttpServletRequest 로 filename과 filerealname 값 전달
-> 전달받은 데이터로 서버에 저장된 filename을 찾고 filerealname의 이름으로 파일 다운로드 반환
-> 다운로드 끝
2] 상세 조회 페이지
2-1] 목록에서 글 선택시
이전 코드는 다음 포스팅 참고
https://yn971106.tistory.com/94
위의 포스팅에서 변경점은 다음과 같습니다.
//테이블 목록 클릭시 함수
function selectBoard(bno){
fetch('/board/selectboard.do',{
method:"POST",
headers: {
'content-type': 'application/json'
},
body :JSON.stringify({
bno: bno
})
}).then(res => res.json())
.then(data => {
console.log(data);
console.log(data[0].content);
let url = "/board/selectboarddetail.do"
let parentContent = document.querySelector('#contentwrap');
let id = _commons().util.random();
$common.sethistory(url,parentContent,id);
let jsondata = {
"title" : data[0].title,
"content" : data[0].content,
"writer" : data[0].writer,
"regdate" : data[0].regdate,
"bno" : data[0].bno,
"filename" : data[0].filename,
"filerealname" : data[0].filerealname
}
$('#content').load(url,jsondata,function(){
});
})
}
row 선택시 해당 게시물에 대한 정보는 BoardVO 객체의 형태의 JSON 으로 전달받습니다.
VO 객체를 업로드 1편에서 변경하였기 때문에 별도의 수정없이 원하는 데이터를 가져오게 됩니다.
받은 데이터 중 filename 과 filerealname 의 key를 가진 value 를 json으로 묶어서
상세조회 페이지 컨트롤러에 전달합니다
해당 컨트롤러는 ModelAndView 구조로 받은 데이터들을 바뀔 jsp 파일에 전달하게 됩니다.
받은 데이터 매핑은 다음과 같이 2줄을 추가하고
String filename = map.get("filename");
String filerealname = map.get("filerealname");
전달하는 데이터는 ModelAndView 객체에 2줄을 추가하는 방식으로 진행하고 이를 반환합니다.
mv.addObject("filename",filename);
mv.addObject("filerealname",filerealname);
2-2] 상세조회 jsp 파일
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div id="contentwrap2">
<h2> 제목 : <span class="title">${title}</span></h2>
<p> 작성자 : <span class="writer">${writer}</span> </p>
<p> 내용 : <span class="boardcontent">${content}</span> </p>
<p>작성일자 : <span class="date">${regdate}</span> </p>
<p class="attachfile" style="display: none"> 첨부파일 :</p>
<button class="backbtn">뒤로가기</button>
<button class="updatebtn" style="display: none">수정하기</button>
<button class="rmbtn" style="display: none">게시글 삭제</button>
</div>
</body>
</html>
<script src='/js/jquery-3.6.0.min.js?ver=1'></script>
<script>
$(document).ready(function(){
let $localstorage = $commons.storage.g_variable;
let writercheck = "${writer}";
let bno = "${bno}";
let rmbtn = document.querySelector('.rmbtn');
let backbtn = document.querySelector('.backbtn');
let $history = $commons.history;
let changecontent =document.querySelector('#content');
let $common = $commons.history
let upbtn = document.querySelector('.updatebtn');
let attach = document.querySelector('.attachfile');
//받아온 파일 선언 -> UUID 형식의 파일
let responsefilename = "${filename}";
console.log(responsefilename);
//파일없을때 숨기기.\
if(responsefilename !== "[]"){
attach.style.display = '';
createFileNode();
}
// authcheck
if( writercheck === $localstorage.getValue("loginUser")){
rmbtn.style.display = '';
upbtn.style.display = '';
}
function createFileNode(){
// 앞뒤 [] 제거
let replacefilename = responsefilename.slice(1,-1);
// , 공백 을 기준으로 내용 분할 -> 배열로 저장
const filenamearr = replacefilename.split(", ");
//사용자가 저장한 파일 이름
let realname = "${filerealname}"
// 앞뒤 [] 제거
let converrealfilename = realname.slice(1,-1);
// , 공백을 기준으로 내용 분할 -> 배열로 저장
const filerealnamearr = converrealfilename.split(", ");
for(let i = 0; i < filenamearr.length;i++){
console.log(filenamearr[i]);
let test = filenamearr[i];
console.log(test);
let node = document.createElement("a")
let template = `
<a href="fileDownload.do?fileName=\${filenamearr[i]}&&filerealName=\${filerealnamearr[i]}">\${filerealnamearr[i]}
</a>`
node.innerHTML = template;
attach.appendChild(node);
}
}
//수정
upbtn.onclick = function(){
//history set
let historycontent = document.querySelector('#contentwrap2');
let id = _commons().util.random();
$history.sethistory("/board/updateboard.do",historycontent,id);
let url = "/board/updateboard.do";
let date = "${regdate}";
let json ={
title : "${title}",
content : "${content}",
regdate : date.toString(),
bno : "${bno}"
}
$('#content').load(url,json,function(){
})
}
rmbtn.onclick = function(){
fetch('/board/remove.do',{
method : "POST",
headers : {
'content-type' : 'application/json'
},
body :JSON.stringify({
bno: bno
})
}).then(res => res.json());
$common.deletehistory("/board/selectboarddetail.do");
let url = 'menu/menu1.do'
$('#main_content').load(url,function(){
});
}
backbtn.onclick = function(){
let link = $history.gethistory("/board/selectboarddetail.do");
let oldElement = document.querySelector('div#contentwrap2');
changecontent.replaceChild(link.content,oldElement);
}
});
</script>
우선 받아온 값을 판별합니다.
//파일없을때 숨기기.\
if(responsefilename !== "[]"){
attach.style.display = '';
createFileNode();
}
공백 배열로 받을시 style.display = ''; 로 감추고
노드생성함수를 실행시킵니다.
function createFileNode(){
// 앞뒤 [] 제거
let replacefilename = responsefilename.slice(1,-1);
// , 공백 을 기준으로 내용 분할 -> 배열로 저장
const filenamearr = replacefilename.split(", ");
//사용자가 저장한 파일 이름
let realname = "${filerealname}"
// 앞뒤 [] 제거
let converrealfilename = realname.slice(1,-1);
// , 공백을 기준으로 내용 분할 -> 배열로 저장
const filerealnamearr = converrealfilename.split(", ");
for(let i = 0; i < filenamearr.length;i++){
console.log(filenamearr[i]);
let test = filenamearr[i];
console.log(test);
let node = document.createElement("a")
let template = `
<a href="fileDownload.do?fileName=\${filenamearr[i]}&&filerealName=\${filerealnamearr[i]}">\${filerealnamearr[i]}
</a>`
node.innerHTML = template;
attach.appendChild(node);
}
}
해당 함수는
배열의 slice 함수로 앞뒤의 [] 를 제거하고
split을 이용해서 확장자를 기준으로 분리시킵니다.
그리고 받은 파일의 갯 수 만큼 순회하며
a 태그를 생성하게 되는데,
이때 a 생성된 a 태그를 클릭시 fileDownload.do 라는 url 리퀘스트를 던지게 됩니다.
전달값은
fileName , filerealName 이렇게 2가지를 전달합니다.
3] fileDownloadController 생성
새로운 컨트롤러를 생성합니다.
소스코드]
package com.yoon.controller.download;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Controller
public class FileDownController {
@RequestMapping("fileDownload.do")
public void fileDownload4(HttpServletRequest request, HttpServletResponse response) throws Exception {
//String path = request.getSession().getServletContext().getRealPath("저장경로");
String filename =request.getParameter("fileName");
String realname = request.getParameter("filerealName");
System.out.println("hhhhhh");
System.out.println("realname = " + realname);
String realFilename="";
System.out.println(filename);
if(realname!=null && !"".equals(realname)) { // 파일 다운로드시 한국어로 인코딩해서 사용자에게 보여주는 코드
String conVertNm = URLEncoder.encode(realname, "EUC-KR");
String realnameKr = new String(realname.getBytes("iso8859-1"), "EUC-KR");
System.out.println("### realname: " + realname + ", conVertNm: " + conVertNm);
//한글 포함시 URLEncoder.encode 로 인코딩
if (realname.matches(".*[ㄱ-ㅎ ㅏ-ㅣ가-힣]+.*") || realnameKr.matches(".*[ㄱ-ㅎ ㅏ-ㅣ가-힣]+.*")) {
System.out.println("###(2)#### realnameKr: " + realnameKr + ", realnameKr: " + realname);
if (realnameKr.matches(".*[ㄱ-ㅎ ㅏ-ㅣ가-힣]+.*")) {
realname = new String(realname.getBytes("iso8859-1"), "EUC-KR");
}
} else {
realname = new String(realname.getBytes("iso8859-1"), "UTF-8");
System.out.println("#### (3) #### realname : " + realname);
}
realname = URLEncoder.encode(realname, "UTF-8");
}
try {
String browser = request.getHeader("User-Agent");
//파일 인코딩
if (browser.contains("MSIE") || browser.contains("Trident")
|| browser.contains("Chrome")) {
filename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+",
"%20");
} else {
filename = new String(filename.getBytes("UTF-8"), "ISO-8859-1");
}
} catch (UnsupportedEncodingException ex) {
System.out.println("UnsupportedEncodingException");
}
realFilename = "C:\\filetest\\" + filename;
System.out.println(realFilename);
File file1 = new File(realFilename);
if (!file1.exists()) {
return ;
}
Long fileLength = file1.length();
//파일명 한글시 인코딩.
response.setCharacterEncoding("utf-8");
// 파일명 지정
response.setContentType("application/octer-stream;charset=utf-8");
response.setHeader("Content-Transfer-Encoding", "binary;");
response.setHeader("Content-Disposition", "attachment; filename=\"" + realname + "\"");
response.setHeader("Content-Length",""+fileLength);
response.setHeader("Pragma", "no-cache;");
response.setHeader("Expires", "-1;");
try {
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(realFilename);
int ncount = 0;
byte[] bytes = new byte[512];
while ((ncount = fis.read(bytes)) != -1 ) {
os.write(bytes, 0, ncount);
}
fis.close();
os.close();
} catch (Exception e) {
System.out.println("FileNotFoundException : " + e);
}
}
}
해당 Mapping 요청을 받게 되면
HttpServletRequest request
로 전달받은 파라미터를 사용할 수 있고,
HttpServletResponse response
의 방식으로 응답합니다.
a 태그에서 전달받은 값을 할당합니다.
String filename =request.getParameter("fileName");
String realname = request.getParameter("filerealName");
여기에서 사용자가 파일을 다운로드 할 때. 인코딩 단계를 거치지 않게되면
전혀다른 파일명으로 저장이 됩니다. 이를 방지하기 위한 코드는 다음과 같습니다.
if(realname!=null && !"".equals(realname)) { // 파일 다운로드시 한국어로 인코딩해서 사용자에게 보여주는 코드
String conVertNm = URLEncoder.encode(realname, "EUC-KR");
String realnameKr = new String(realname.getBytes("iso8859-1"), "EUC-KR");
System.out.println("### realname: " + realname + ", conVertNm: " + conVertNm);
//한글 포함시 URLEncoder.encode 로 인코딩
if (realname.matches(".*[ㄱ-ㅎ ㅏ-ㅣ가-힣]+.*") || realnameKr.matches(".*[ㄱ-ㅎ ㅏ-ㅣ가-힣]+.*")) {
System.out.println("###(2)#### realnameKr: " + realnameKr + ", realnameKr: " + realname);
if (realnameKr.matches(".*[ㄱ-ㅎ ㅏ-ㅣ가-힣]+.*")) {
realname = new String(realname.getBytes("iso8859-1"), "EUC-KR");
}
} else {
realname = new String(realname.getBytes("iso8859-1"), "UTF-8");
System.out.println("#### (3) #### realname : " + realname);
}
realname = URLEncoder.encode(realname, "UTF-8");
}
전달받은 UUID 의 값을 인코딩 하는 부분은 다음과 같습니다.
try {
String browser = request.getHeader("User-Agent");
//파일 인코딩
if (browser.contains("MSIE") || browser.contains("Trident")
|| browser.contains("Chrome")) {
filename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+",
"%20");
} else {
filename = new String(filename.getBytes("UTF-8"), "ISO-8859-1");
}
} catch (UnsupportedEncodingException ex) {
System.out.println("UnsupportedEncodingException");
}
UUID 의 값을 인코딩하고 아래의 코드를 통해서
저장된 위치안에 있는 파일중 전달받은 UUID 와 동일한 이름을 가진 파일을 찾아 변수에 저장합니다.
만약 없으면 종료시킵니다.
realFilename = "C:\\filetest\\" + filename;
System.out.println(realFilename);
File file1 = new File(realFilename);
if (!file1.exists()) {
return ;
}
응답할 부분의 헤더 설정입니다.
Long fileLength = file1.length();
//파일명 한글시 인코딩.
response.setCharacterEncoding("utf-8");
// 파일명 지정
response.setContentType("application/octer-stream;charset=utf-8");
response.setHeader("Content-Transfer-Encoding", "binary;");
response.setHeader("Content-Disposition", "attachment; filename=\"" + realname + "\"");
response.setHeader("Content-Length",""+fileLength);
response.setHeader("Pragma", "no-cache;");
response.setHeader("Expires", "-1;");
그중 아래의 부분이 사용자가 다운로드시 보여질 파일명의 이름입니다.
response.setHeader("Content-Disposition", "attachment; filename=\"" + realname + "\"");
아래의 코드로 찾은 정보들로 하여 파일을 다운로드 하게 해줍니다.
try {
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(realFilename);
int ncount = 0;
byte[] bytes = new byte[512];
while ((ncount = fis.read(bytes)) != -1 ) {
os.write(bytes, 0, ncount);
}
fis.close();
os.close();
} catch (Exception e) {
System.out.println("FileNotFoundException : " + e);
}
자바에서 데이터는 Stream을 통해 입출력 됩니다.
데이터를 입력 받을 때 -> inputStream
데이터를 출력 할 때 -> outputStream
각각 생성자로 선언한뒤
fis 에 파일위치,이름의 정보를 입력하고, fis의 정보를 읽으면서
출력스트림에 해당 정보를 입력합니다.
출력이 끝나면 입력스트림과 출력스트림을 각각 종료함으로 파일다운로드가 끝나게됩니다.
4] 바꿔야 하는 점
이로써 파일 업로드 다운로드가 끝났습니다
controller 호출시 a 태그에 href로 데이터가 클라이언트에 노출되도록 전달이 됩니다.
이를 막기위해서는 ajax 통신 방법을 선택해야 합니다.
다음에는 ajax 를 통해 다운로드하는 방법을 공부해보겠습니다.
감사합니다.
'[Spring]_ > [Spring]_포트폴리오 페이지 만들기' 카테고리의 다른 글
[Modal_Callback]_모달 팝업 사용자 선택 Boolean 값으로 콜백 받기 (0) | 2022.11.04 |
---|---|
[포트폴리오 페이지]_18단계_게시판 심화_(파일 업로드 1편) (0) | 2022.04.29 |
[포트폴리오 페이지]_17단계_CRUD 게시판 구현_(Delete) (0) | 2022.04.26 |
[포트폴리오 페이지]_16단계_CRUD 게시판 구현_(Update 기능) (0) | 2022.04.26 |
[포트폴리오 페이지]_15단계_CRUD 게시판 구현_(목록 Read 기능) (0) | 2022.04.26 |