본문 바로가기

Programming/SpringBoot

[SpringBoot] 게시글 이미지(파일) 미리보기 & 업로드 (ver.팝업창)

 


게시글 이미지(파일) 미리보기 & 업로드 (팝업창 버전)


 

 

0. 과정


 

게시판 등록란에서 '이미지 선택' 버튼을 누른다.

 

 

파일 선택 팝업창에 있는 '파일 선택' 버튼으로 이미지를 가져온다.

 

 

선택한 이미지를 미리볼 수 있다.

이때 본래 이미지(파일)명과 새롭게 저장한 파일 이름이 함께 뜬다.

 

 

이렇게 저장된 이미지는 '작성완료' 버튼을 누르기 전

즉, 게시물 등록 전에 webapp 에 만든 upload 디렉토리 (path지정) 에 저장된다.

 

 

이 과정을 정리하자면...

  1. board/insertBoard.jsp 에 이미지 선택 버튼과 미리보기 버튼을 div로 분리한다.
    1. 이미지 선택 버튼 div : 누르면(onclick) selectImg() 로 이동
    2. 이미지 미리보기 div : 이미지 미리보기, 화면에 표시하고 폴더에 저장할 image, savefilename 히든태그
  2. 이미지를 고르는 팝업창을 selectImg() 로 게시판의 자바스크립트를 작성하는 board.js 에 작성한다.
  3. BoardController.java 에 selectImg()로 연 팝업창이 selectimg.jsp 로 이동하도록 매핑한다.
  4. board/selectimg.jsp 를 만들고 file을 선택해서 여는 form을 작성한다.
    1. 파일을 선택하면(type="file") selectedimage() 함수가 발동 (onchange="selectedimage();")
    2. 파일을 선택하면 발동하는 selectedimage() 함수는 submit 기능
  5. BoardController.java 에 아래 기능을 하고, completeUpload.jsp 로 이동하는 fileupload 매핑하기 
    1. upload 폴더에 사진 저장하도록 경로 설정
    2. 사용자가 선택한 파일이름과 서버에 저장되는 이름 따로 저장
    3. 파일 업로드
  6. board/completeUpload.jsp 생성하여 파일 이름 저장, 화면에 표시, 업로드, 이미지 미리보기 처리하기

정도가 되겠다. (뭔소리야?)

 

표현 방식에 따라 과정이야 달라질 수 있다. 가장 중요한 5번의 '전달 받은 이미지 파일을 처리하는 과정'을 집중적으로 알아보겠다.

 

 

 

1. 파일 업로드를 위한 태그 작성


insertBoard.jsp

<div id="main_container">
    <h2>게시글 등록</h2>
    <div class="board">
        <form class="insertBoard" method="post" name="insertBoard" action="insertBoard" enctype="multipart/form-data">
            <div class="field">
                <label>작성자</label>
                <input type="text" name="userid" value="${loginUser.userid}" readonly/>
            </div>

            <div class="field">
                <label>비밀번호</label>
                <input type="password" name="pass"/>
            </div>

            <div class="field">
                <label>이메일</label>
                <input type="text" name="email" value="${loginUser.email}" readonly/>
            </div>

            <div class="field">
                <label>제목</label>
                <input type="text" name="title"/>
            </div>

            <div class="field">
                <label>내용</label>
                <textarea name="content" rows="10" cols="100"></textarea>
            </div>

            <div class="field">
                <label>이미지</label>
                <input type="button" value="이미지 선택" onclick="selectImg()"/>
            </div>


            <div class="field">
                <label>이미지 미리보기</label>
                <div style="flex: 4">
                    <c:choose>
                        <c:when test="${empty dto.image}">
                            <img src="" id="previewimg" width="150" style="display: none"/>
                        </c:when>
                        <c:otherwise>
                            <img src="/upload/${dto.savefilename}" id="previewimg" width="150"/>
                        </c:otherwise>
                    </c:choose>
                    <input type="hidden" name="savefilename" value="${dto.savefilename}"> <!-- 이미지 이름 저장 -->
                    <input type="hidden" name="image" value="${dto.image}"> <!-- 저장될 파일 이름 저장 -->
                    <div id="image">${dto.image}</div> <!-- 이미지 이름 표시 -->
                    <div id="savefilename">${dto.savefilename}</div> <!-- 저장될 이미지 이름 표시 -->
                </div>
            </div>

            <div class="field">
                <input type="submit" value="작성완료" onclick="return boardCheck()"/>
                <input type="button" value="돌아가기" onclick="location.href='main'"/>
            </div>
            <div class="field">${msg}</div>
        </form>
    </div>
</div>

 

insertBoard.jsp 의 전체 코드다.

 

▶ form 태그 (enctype="multipart/form-data")

<form ... method="post" action="insertBoard" enctype="multipart/form-data">

 

form 에 적용할 속성 중 enctype 을 주목해보자.

 

enctype 은 form 데이터를 서버로 전송할 때 데이터를 어떻게 인코딩할 건지를 정의하는 속성이다.

그 중에서도  multipart/form-data 는 파일 업로드 시 필수적으로 사용해야하는 인코딩 방식이다.

 

 multipart/form-data 는 데이터를 여러 부분으로 나누어 전송하며, 각 부분이 다른 형식(텍스트, 파일 등)으로 인코딩 된다. form에서 텍스트와 파일이 함께 전송된다면, 각 항목을 구분할 수 있도록 해주는 것이다.

 

 

<div class="field">
    <label>이미지</label>
    <input type="button" value="이미지 선택" onclick="selectImg()"/>
</div>


<div class="field">
    <label>이미지 미리보기</label>
    <div style="flex: 4">
        <c:choose>
            <c:when test="${empty dto.image}">
                <img src="" id="previewimg" width="150" style="display: none"/>
            </c:when>
            <c:otherwise>
                <img src="/upload/${dto.savefilename}" id="previewimg" width="150"/>
            </c:otherwise>
        </c:choose>
        <input type="hidden" name="savefilename" value="${dto.savefilename}"> <!-- 이미지 이름 저장 -->
        <input type="hidden" name="image" value="${dto.image}"> <!-- 저장될 파일 이름 저장 -->
        <div id="image">${dto.image}</div> <!-- 이미지 이름 표시 -->
        <div id="savefilename">${dto.savefilename}</div> <!-- 저장될 이미지 이름 표시 -->
    </div>
</div>

 

두 div 가 나타내는 부분은 위의 그림과 같다.

 

▶ 이미지 라벨

"이미지 선택" button 을 누르면 파일을 선택하는 팝업창이 나오는 selectImg() 로 이동한다.

 

 

▶ 이미지 미리보기 라벨

1. 이미지 파일이 존재할 경우 미리보기로 보여질 <img> 태그

2. 파일 이름(image)와 저장되는 파일 이름(savefilename)을 받을 히든 태그

3. 파일 이름을 화면에 드러낼 <div id="image">, <div id="savefilename"> 태그

가 존재한다.

 

 

 

이미지(파일) 값이 없다면 미리보기가 표시되지 않지만

이미지(파일)이 선택되면 위와 같이 공간을 일부 차지한다.

 

 

▶ selectImg() 함수

function selectImg(){
    var opt = "toolbar=no, menubar=no, resizable=no, width=450, height=200";
    window.open('selectimg', 'selectimg', opt);
}

 

/script/board.js 에 selectImg() 함수를 선언한다.

selectImg() 는 'selectimg' 로 이동하는 'selectimg' 이름의 창을 opt 속성으로 팝업창을 띄울 것이다.

 

 

팝업창 명령어는 다음과 같다.

window.open(url, name, specs, replace)

 

url : 새 창에 보여질 주소

name : 새로 열릴 창의 속성 또는 창의 이름을 지정

specs : 창의 속성

replace : 히스토리 목록에 새 항목을 만들지 현재 항목을 대체할지 결정

 

 

 

2. 파일 선택창으로 이동


BoardController.java

@GetMapping("/selectimg")
public String selectimg() {
    return "board/selectimg";
}

 

앞서 팝업창이 selectimg 로 이동하도록 주소를 지정하였다.

이는 컨트롤러에서 매핑을 통해 board/selectimg.jsp 로 보낸다.

 

 

selectimg.jsp

<div id="wrap" align="center" style="width:100%">
    <form name="frm" action="fileupload" method="post" enctype="multipart/form-data">
    	<h1>파일 선택</h1>
    	<input type="file" name="image" onchange="selectedimage();">
    </form>
</div>

 

 

새로 열린 팝업창이 띄우는 selectimg 화면을 만든 것이다.

 

 

<input type="file" name="image" onchange="selectedimage();">

▶ <input type="file">

사용자에게 파일을 선택할 수 있는 파일창을 띄운다. 파일을 선택하면, 선택된 파일 정보가 input 에 설정된다.

 

 

▶ onchange

onchange 속성은 HTML 에서 사용자가 입력 필드 값을 변경했을 때 발생하는 이벤트를 처리하는데 사용한다.

사용자가 파일을 선택했을 때 파일 정보가 바뀌면서 selectedimage() 자바스크립트 함수가 호출된다.

 

 

▶ selectedimage()

<script type="text/javascript">
    function selectedimage(){
      document.frm.submit(); // 파일을 선택할 시 작동
    }
</script>

 

seletedimage() 함수는 form(frm 폼) 데이터를 submit 하는 코드를 담는다.

데이터는 multipart/form-data 인코딩 방식으로 fileupload 로 날아가게 된다.

 

 

 

🌟 3. 파일 업로드 처리 과정


BoardController.java

@Autowired
ServletContext context;
    
@PostMapping("/fileupload")
    public String fileupload(
            HttpServletRequest request,
            Model model,
            @RequestParam("image") MultipartFile file ) {

        // HttpSession session = request.getSession();
        // ServletContext context = session.getServletContext();
        String path = context.getRealPath("/upload");
        String filename = file.getOriginalFilename(); // MultipartFile file 에 전송된 선택 파일이름 추출
        String savefilename = makeSavefilename(filename);
        String uploadPath = path + "/" + savefilename; // 저장경로 + abc123456789.jap
        try {
            file.transferTo(new File(uploadPath)); // 파일 업로드
        } catch (IllegalStateException | IOException e) {
            e.printStackTrace();
        }
        model.addAttribute("image", filename); // 사용자가 선택한 파일이름
        model.addAttribute("savefilename", savefilename); // 서버에 저장되는 이름

        return "board/completeUpload";
    }

    private String makeSavefilename(String filename) {
        Calendar today = Calendar.getInstance();
        long t = today.getTimeInMillis();

        String fn1 = filename.substring(0, filename.lastIndexOf(".")); // abc.jsp -> abc
        String fn2 = filename.substring(filename.lastIndexOf(".") + 1); // abc.jsp -> jsp
        return fn1 + t + "." + fn2; // abc123456789.jsp
    }

 

이번 포스트의 핵심 내용이 되겠다.

굉장히 복잡해보이지만, 보통 이미지 업로드 할 때 쓰이는 기본 로직이다. 외워서 작성해도 된다는 말이다.

 

 

▶ ServletContext

@Autowired
ServletContext context;

@PostMapping("/fileupload")
public String fileupload(
            HttpServletRequest request,
            Model model,
            @RequestParam("image") MultipartFile file ) {
            
        // HttpSession session = request.getSession();
        // ServletContext context = session.getServletContext();
        String path = context.getRealPath("/upload");

 

fileupload 메소드의 매개변수는 HttpServletRequest, Model, @RequestParam 이 있다.

 

HttpServletRequest 는 ServletContext 객체를 session.getServletContext() 로 가져오기 위해 선언했지만

@Autowired 어노테이션으로 ServletContext 객체를 Spring이 자동으로 주입해주도록 설정했다.

( 정확히는 Spring의 IoC 컨테이너에 의해 자동으로 주입이라고 한다) 

따라서 굳이 세션을 거치지 않아도 ServletContext 객체를 context 이름으로 사용할 수 있게 되었다.

 

ServletContext는 서블릿(Web application)에서 application 레벨의 전역적인 정보를 제공하는 객체다. 주로 서블릿 컨테이너(ex Tomcat)에 application 초기화 및 환경 설정을 관리하는데 사용된다.

라고하니 이해가 안 돼서 조금 더 쉽게 풀자면..

application은 request, session 보다 큰 요청 범위인 '웹 어플리케이션이 시작할 때부터 종료할 때까지 존재'하는 개념이다.

ServletContext는 application 전역에서 공유되는 자원과 환경 설정을 관리하며

구체적으로 파일 경로 처리, 어플리케이션 초기화 파라미터 처리, 데이터 공유 등의 작업을 수행하는데 사용된다.

지금은 서버에서 application의 실제 파일 시스템 경로를 얻고자 할 때 사용했다.

 

String path = context.getRealPath("/upload");

 

현재 전체 폴더("/") 에서 "upload" 에 해당하는 경로를 찾아 path 라는 String 변수로 저장했다.

 

 

우리가 기대하는 경로는 webapp 밑에 upload 디렉토리 인 것이다. (없다면 만들어주자)

 

 

▶ 파일 이름 추출과 savefilename 지정

String filename = file.getOriginalFilename(); // MultipartFile file 에 전송된 선택 파일이름 추출
String savefilename = makeSavefilename(filename);
private String makeSavefilename(String filename) {
    Calendar today = Calendar.getInstance();
    long t = today.getTimeInMillis();

    String fn1 = filename.substring(0, filename.lastIndexOf(".")); // abc.jsp -> abc
    String fn2 = filename.substring(filename.lastIndexOf(".") + 1); // abc.jsp -> jsp
    return fn1 + t + "." + fn2; // abc123456789.jsp
}

 

로직이 조금 복잡해져서 savefilename 을 지정하는 부분을 메소드로 따로 분리했다.

해당 로직은 이름이 최대한 겹치지 않도록 savefilename을 지정하는 방법이다.

 

Calendar today = Calendar.getInstance();
long t = today.getTimeInMillis();

 

Calendar 객체에서 오늘 날짜를 받아 밀리초를 계산했다.

밀리초는 시간을 13자리 숫자로 표현한다.

보기만해도 복잡한 숫자로, 이 점을 이용해 기존 이미지 파일 이름과 조합하면 절대 겹칠 수 없는 savefilename을 만드는 것이다.

 

String fn1 = filename.substring(0, filename.lastIndexOf(".")); // abc.jsp -> abc
String fn2 = filename.substring(filename.lastIndexOf(".") + 1); // abc.jsp -> jsp

 

본격적인 조합을 위해 선택한 filename을 분리한다.

 

lastIndexOf()filename의 끝에서부터 "." 을 찾아 위치값을 반환한다.

substring() filename의 시작위치 맨 처음(0)부터 "."이 있는 위치(lastIndexOf이 반환)까지의 값을 가져온다.

파일 이름이 abc.jpeg 라면 abc를, w7.png라면 w7을 반환하는 것이다.

 

같은 원리로 확장자도 분리한다.

lastIndexOf() 이 반환하는 위치는 "." 의 위치이기 때문에, "." 뒤에 있는 확장자를 가져오기 위해서는 +1을 해주어야 한다.

파일 이름이 abc.jpeg 라면 jpeg를, w7.png라면 png를 반환하는 것이다.

 

 

▶ 저장 경로를 설정해 파일 업로드

String uploadPath = path + "/" + savefilename; // 저장경로 + abc123456789.jsp
try {
    file.transferTo(new File(uploadPath)); // 파일 업로드
} catch (IllegalStateException | IOException e) {
    e.printStackTrace();
}

 

저장경로 uploadPath 를 만들어준다.

앞서 지정한 path인 "/upload" 에 "/(savefilename)" 을 붙여 upload 디렉토리에 savefilename으로 파일을 저장할 것이다.

 

/upload/savefilename 의 결과

 

file.transferTo(new File(uploadPath)); // 파일 업로드

 

file : form 에서 선택한 MultipartFile 객체

uploadPath : 파일을 저장할 경로 (/uploads/savefilename)

new File() : File 객체 생성

transferTo() : file에 있는 데이터를 서버의 특정 위치로 저장

 

한 마디로, 클라이언트에서 업로드된 파일을 지정한 경로로 전송하는 코드다.

 

 

▶ model 객체에 담기

model.addAttribute("image", filename); // 사용자가 선택한 파일이름
model.addAttribute("savefilename", savefilename); // 서버에 저장되는 이름

return "board/completeUpload";

 

데이터를 completeUpload.jsp 로 보내기 위해

사용자가 선택한 파일의 이름(filename)과 새로 조합해 서버에 저장한 savefilename을 model 객체에 addAttribute로 담아준다.

 

 

 

4. 팝업창에서 선택한 파일의 정보 전달


completeUpload.jsp

<script type="text/javascript">
    <%-- 값을 opener 에 보내줌 --%>
    opener.document.insertBoard.savefilename.value = '${savefilename}';
    opener.document.insertBoard.image.value = '${image}';
    opener.document.getElementById("image").innerHTML = '${image}';
    opener.document.getElementById("savefilename").innerHTML = '${savefilename}';

    opener.document.getElementById("previewimg").setAttribute('src', '/upload/' + '${savefilename}');
    opener.document.getElementById("previewimg").style.display = 'inline'; // 이미지 미리보기

    self.close();
</script>

 

팝업창을 통해 전달한 파일의 정보는 자바스크립트를 통해 전달했다.

 

태그에 해당하는 정보는 위와 같다.

 

 

▶ name의 태그를 찾아 값을 부여

opener.document.insertBoard.savefilename.value = '${savefilename}';
opener.document.insertBoard.image.value = '${image}';

 

opener 객체는 현재 팝업 창의 부모 창을 참조하는 객체다. opener 객체를 통해 해당 팝업창에서 부모 창의 DOM에 접근할 수 있다.

 

DOM(Document Object Model)은 웹 페이지 구조를 트리 형태로 표현한 객체 모델로, Java Script를 통해 요소를 선택하고 조작할 수 있게 해주는 인터페이스 정도로 일단은 간단하게 이해하고 넘어가자.

 

<!-- insertBoard.jsp 에 name 이 savefilename, image 인 태그 -->
<input type="hidden" name="savefilename" value="${dto.savefilename}"> <%-- 이미지 이름 저장 --%>
<input type="hidden" name="image" value="${dto.image}"> <%-- 저장될 파일 이름 저장 --%>

 

팝업창의 부모 창은 insertBoard.jsp 다. 해당 문서에는 insertBoard 라는 name의 form 이 있고, savefile, image 라는 name을 가진 태그들이 존재한다. EL 문법을 이용해 해당 태그의 값을 앞서 model에 저장한 값들로 각각 변경했다.

 

 

▶ id를 찾아 HTML 내용 업데이트

opener.document.getElementById("image").innerHTML = '${image}';
opener.document.getElementById("savefilename").innerHTML = '${savefilename}';
opener.document.getElementById("previewimg").setAttribute('src', '/upload/' + '${savefilename}');
opener.document.getElementById("previewimg").style.display = 'inline'; // 태그 보임

 

getElementById()id 값에 따른 태그 속성을 불러올 수 있다.

 

<!-- inserBoard.jsp 에 id 가 image, savefilename 인 태그 -->
<div id="image">${dto.image}</div> <!-- 이미지 이름 표시 -->
<div id="savefilename">${dto.savefilename}</div> <!-- 저장될 이미지 이름 표시 -->

 

id가 image, savefilename 인 태그에는 HTML을 수정하는 innerHTML를 사용하여 표시될 이미지 데이터의 이름을 값을 추가해주었다.

 

<!-- insertBoard.jsp 에 id 가 previewimg 인 태그 -->
<c:choose>
    <c:when test="${empty dto.image}">
        <img src="" id="previewimg" width="150" style="display: none"/>
    </c:when>
    <c:otherwise>
        <img src="/upload/${dto.savefilename}" id="previewimg" width="150"/>
    </c:otherwise>
</c:choose>

 

id가 previewimg 인 태그는 사용자에게 '미리' 보여줄 이미지를 설정하고,

display:none 으로 되어 있던 previewimg 태그가 보이도록 스타일 설정을 해주었다.

 

 

▶ 팝업창 닫기

self.close();

 

앞서 데이터를 모두 전달하고 팝업창을 닫음으로써 팝업창이 할 일은 끝이 났다.

 

 


 

팝업창을 사용하는 건 웹페이지를 이용하는 사용자로부터 긍정적인 반응을 얻지 못한다.

그래서 요즘은 팝업창을 거의 사용하지 않다고 한다.

 

팝업창을 사용하지 않고 파일을 불러오기 위해서는

1. form에 enctype="multipart/form-data" 속성을 적용하고

2. input type="file" 을 선택하는 태그 만들어서

3. 서버로 이미지를 처리하도록 보내주어야 하는데

 

이런 식의 get/post 방법으로 데이터를 보내줄 때에는 새로고침이 불가피하다.

작성한 내용을 저장해두고 이미지를 불러올 때마다 서버에 전달하고 다시 돌아와도 가능은 하지만..

반복되는 작업으로 인한 과부화는 피할 수 없을 것이다.

 

이때 사용하는 방법이 Ajax 다.

'새로고침 없이 데이터 요청하는 기능'을 수행할 수 있는 JavaScript 코드로,

해당 내용도 짧게나마 다음포스트로기회가된다면(...) 다뤄보고자 한다.