Study/Spring

[Spring] Spring + Jquery + Html 다중 파일 업로드

AC 2021. 3. 14. 00:41

 

 

 

원래는 Daum Editor 와 연동해 게시판 내 사진 프리뷰, 업로드 전체가 구현되어있지만

본 포스트는 프리뷰된 다중 파일이 어떻게 다시 form으로 재조합되고 결과를 받는지에 중점을 두었다.

 

이미지를 서버에 올리지 않고 미리보기하는 방법은 아래 포스트에 적혀있다.

 

[html 파일 올리기 전에 preview 하기]

 

위의 포스트를 보지 않아도 무방하다.

위의 리소스를 수정하여 포스트할 것이기 때문이다.

 

 

[Client-Side]

- 이미지첨부를 클릭했을 때, 당장 이미지가 올라가지 않고 먼저 미리보기 된다.

- 미리보기는 개별적으로 삭제가 가능하다.

- 삭제된 이미지는 실제로 서버에 업로드되지 않고, 최종적으로 미리보기중인 파일만 업로드한다.

 

 

초기 화면은 이렇다. 이미지를 thumbnail처럼 보이기 위해 width, height를 조정한 것 외에는 별다른 css가 없다.

사진첨부가 그냥 텍스트인것 같지만 클릭이 된다(...) css 적용하면 그럴듯해보인다.

 

 

 

 

 

이미지 첨부 시 아래와 같이 추가된다. 삭제를 누르면 box 단위로 삭제된다.

삭제 시 실제로 전송될 목록에서도 삭제된다.

등록 버튼을 누르면 서버로 multipart ajax 통신을 시작하며, 다시 서버에서 결과를 반환받는다.

 

코드는 아래와 같다. js 파일을 따로 만들지 않고 한번에 작성하였다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

<%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>이미지 첨부</title>

 

<style>

#preview img {

    width: 100px;

    height: 100px;

}

 

#preview p {

    text-overflow: ellipsis;

    overflow: hidden;

}

 

.preview-box {

    border: 1px solid;

    padding: 5px;

    border-radius: 2px;

    margin-bottom: 10px;

}

</style>

</head>

 

<body>

    <div class="wrapper">

        <div class="header">

            <h1>사진 첨부</h1>

        </div>

        <div class="body">

            <!-- 첨부 버튼 -->

            <div id="attach">

                <label class="waves-effect waves-teal btn-flat" for="uploadInputBox">사진첨부</label>

                <input id="uploadInputBox" style="display: none" type="file" name="filedata" multiple />

            </div>

            

            <!-- 미리보기 영역 -->

            <div id="preview" class="content"></div>

            

            <!-- multipart 업로드시 영역 -->

            <form id="uploadForm" style="display: none;" />

        </div>

        <div class="footer">

            <button class="submit"><a href="#" title="등록" class="btnlink">등록</a></button>

        </div>

    </div>

    

    

    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

    

    <script>

        //임의의 file object영역

        var files = {};

        var previewIndex = 0;

 

        // image preview 기능 구현

        // input = file object[]

        function addPreview(input) {

            if (input[0].files) {

                //파일 선택이 여러개였을 시의 대응

                for (var fileIndex = 0; fileIndex < input[0].files.length; fileIndex++) {

                    var file = input[0].files[fileIndex];

 

                    if (validation(file.name))

                        continue;

 

                    var reader = new FileReader();

                    reader.onload = function(img) {

                        //div id="preview" 내에 동적코드추가.

                        //이 부분을 수정해서 이미지 링크 외 파일명, 사이즈 등의 부가설명을 할 수 있을 것이다.

                        var imgNum = previewIndex++;

                        $("#preview")

                                .append(

                                        "<div class=\"preview-box\" value=\"" + imgNum +"\">"

                                                + "<img class=\"thumbnail\" src=\"" + img.target.result + "\"\/>"

                                                + "<p>"

                                                + file.name

                                                + "</p>"

                                                + "<a href=\"#\" value=\""

                                                + imgNum

                                                + "\" onclick=\"deletePreview(this)\">"

                                                + "삭제" + "</a>" + "</div>");

                        files[imgNum] = file;

                    };

                    reader.readAsDataURL(file);

                }

            } else

                alert('invalid file input'); // 첨부클릭 후 취소시의 대응책은 세우지 않았다.

        }

 

        //preview 영역에서 삭제 버튼 클릭시 해당 미리보기이미지 영역 삭제

        function deletePreview(obj) {

            var imgNum = obj.attributes['value'].value;

            delete files[imgNum];

            $("#preview .preview-box[value=" + imgNum + "]").remove();

            resizeHeight();

        }

 

        //client-side validation

        //always server-side validation required

        function validation(fileName) {

            fileName = fileName + "";

            var fileNameExtensionIndex = fileName.lastIndexOf('.'+ 1;

            var fileNameExtension = fileName.toLowerCase().substring(

                    fileNameExtensionIndex, fileName.length);

            if (!((fileNameExtension === 'jpg')

                    || (fileNameExtension === 'gif'|| (fileNameExtension === 'png'))) {

                alert('jpg, gif, png 확장자만 업로드 가능합니다.');

                return true;

            } else {

                return false;

            }

        }

 

        $(document).ready(function() {

            //submit 등록. 실제로 submit type은 아니다.

            $('.submit a').on('click',function() {                        

                var form = $('#uploadForm')[0];

                var formData = new FormData(form);

    

                for (var index = 0; index < Object.keys(files).length; index++) {

                    //formData 공간에 files라는 이름으로 파일을 추가한다.

                    //동일명으로 계속 추가할 수 있다.

                    formData.append('files',files[index]);

                }

 

                //ajax 통신으로 multipart form을 전송한다.

                $.ajax({

                    type : 'POST',

                    enctype : 'multipart/form-data',

                    processData : false,

                    contentType : false,

                    cache : false,

                    timeout : 600000,

                    url : '/imageupload',

                    dataType : 'JSON',

                    data : formData,

                    success : function(result) {

                        //이 부분을 수정해서 다양한 행동을 할 수 있으며,

                        //여기서는 데이터를 전송받았다면 순수하게 OK 만을 보내기로 하였다.

                        //-1 = 잘못된 확장자 업로드, -2 = 용량초과, 그외 = 성공(1)

                        if (result === -1) {

                            alert('jpg, gif, png, bmp 확장자만 업로드 가능합니다.');

                            // 이후 동작 ...

                        } else if (result === -2) {

                            alert('파일이 10MB를 초과하였습니다.');

                            // 이후 동작 ...

                        } else {

                            alert('이미지 업로드 성공');

                            // 이후 동작 ...

                        }

                    }

                    //전송실패에대한 핸들링은 고려하지 않음

                });

            });

            // <input type=file> 태그 기능 구현

            $('#attach input[type=file]').change(function() {

                addPreview($(this)); //preview form 추가하기

            });

        });

    </script>

</body>

</html>

Colored by Color Scripter

cs

 

 

 

 

[Server-Side]

 

Spring 서버를 기준으로, 아래와 같은 설정이 필요하다.

controller config 은 생략하였다. 알아서 등록하도록 하자.

 

pom.xml

 

application-config

 

 

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

package controller;

 

import java.util.List;

 

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.multipart.MultipartFile;

 

@Controller

public class MainController {

    private static final int RESULT_EXCEED_SIZE = -2;

    private static final int RESULT_UNACCEPTED_EXTENSION = -1;

    private static final int RESULT_SUCCESS = 1;

    private static final long LIMIT_SIZE = 10 * 1024 * 1024;

    

    @RequestMapping("/")

    public String main() {

        return "index";

    }

 

    //로직은 언제나 Service에서 짜도록 하자.

    //중간실패시 rollback은 고려하지 않았음.

    @ResponseBody

    @RequestMapping(value="/imageupload", method=RequestMethod.POST)

    public int multiImageUpload(@RequestParam("files")List<MultipartFile> images) {

        long sizeSum = 0;

        for(MultipartFile image : images) {

            String originalName = image.getOriginalFilename();

            //확장자 검사

            if(!isValidExtension(originalName)){

                return RESULT_UNACCEPTED_EXTENSION;

            }

            

            //용량 검사

            sizeSum += image.getSize();

            if(sizeSum >= LIMIT_SIZE) {

                return RESULT_EXCEED_SIZE;

            }

            

            //TODO 저장..

        }

        

        //실제로는 저장 후 이미지를 불러올 위치를 콜백반환하거나,

//특정 행위를 유도하는 값을 주는 것이 옳은 것 같다.

        return RESULT_SUCCESS;

    }

    

    //required above jdk 1.7 - switch(String)

    private boolean isValidExtension(String originalName) {

        String originalNameExtension = originalName.substring(originalName.lastIndexOf("."+ 1);

        switch(originalNameExtension) {

        case "jpg":

        case "png":

        case "gif":

            return true;

        }

        return false;

    }

}

 

Colored by Color Scripter

cs

 

컨트롤러는 크게 어려울 것 없이 읽는 그대로 이해할 수 있도록 작성하였다.

 

 

 

 

이 프로젝트에서 컨트롤러를 거치면 json 으로 데이터가 다시 클라이언트로 날아가고, 그에따라 alert이 발생한다.

 

 

 

 

 

 

 

다른 코드 분석하고 이거저거 찾아보면서 최대한 기초적인 코드를 작성하느라 하루를 날렸다....

어쩌다보니 Spring보다 Front-End쪽을 더 하는 것 같다.

 

 

 

 

 extracold_multipart_spring_example.zip

포스팅에 사용한 프로젝트 예제

 

 

 

 

++++++ 추가

Client-Side에 addPreview 에서, FileReader가 비동기로 동작하는데 file을 외부 블럭에서 써버려서 썸네일은 다른데 내부 파일이 모두 같은 파일로 동작하는 문제가 있었다.

 

addPreview 쪽을

 

대략 이런식

 

으로 나누면 해결된다.

 

출처 : http://extracold.tistory.com/40

LIST