[포트폴리오 페이지]_19단계_게시판 심화_(파일 다운로드 2편)

2022. 4. 29. 12:47[Spring]_/[Spring]_포트폴리오 페이지 만들기

728x90
반응형

해당 게시글은 업로드 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

 

[포트폴리오 페이지]_15단계_CRUD 게시판 구현_(목록 Read 기능)

[환경] 개발툴 : IntelliJ DB : oracle 프레임워크 : spring , mybatis 사용 언어 : ES6, Java , Html5 , CSS [완성화면] 목록에서 번호를 클릭시 해당 게시글의 내용이 나오도록 한다. 개발 목표 : 기존에 만든..

yn971106.tistory.com

 

위의 포스팅에서 변경점은 다음과 같습니다.

//테이블 목록 클릭시 함수
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 를 통해 다운로드하는 방법을 공부해보겠습니다.

 

감사합니다.

728x90
반응형