관리자 글쓰기

본 내용은 혼자 공부하기 위하여 다른 포스트를 보면서 저에게 필요한 부분과 궁금했던 부분을 추가하여 게시하는 곳입니다.

궁금한 것에 대한 것은 모르는 것이 많겠지만 함께 알아보도록 노력하겠습니다.


참조 게시 포스트 : http://addio3305.tistory.com/


------------------------------------------------------------------------------------------------------------------------------------------

1. 파일 다중 업로드

1.1 JSP

파일 다중 업로드를 위해 jsp 파일을 수정하겠다.

boardWrite.jsp

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 
<html>
<head>
<title>글쓰기</title>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
    <form id="frm" enctype="multipart/form-data" >
        <table class="board_view">
            <colgroup>
                <col width="15%" >
                <col width="*" >
            </colgroup>
            <caption>게시글 작성</caption>
            <tbody>
                <tr>
                    <th scope="row">제목</th>
                    <td><input type="text"  name="TITLE" class="wdp_90" /></td>
                </tr>
                <tr>
                    <th scope="row">내용</th>
                    <td><textarea cols="100" rows="20" id="CONTENTS" name="CONTENTS" title="내용"></textarea></td>
                </tr>
            </tbody>
        </table>
        <div id="fileDiv">
            <p>
                <input type="file" name="file_0"/>
                <a href="#this" name="delete" class="btn">삭제하기</a>
            </p>
        </div>
         
 
 
        <a href="#this" id="add" class="btn">파일 추가하기</a>
        <a href="#this" id="list" class="btn">목록으로</a>
        <a href="#this" id="write" class="btn">글쓰기</a>
    </form>
    <%@ include file="/WEB-INF/include/include-body.jspf" %> 
     
    <script type="text/javascript">
        var g_count =1;
        $(document).ready(function(){
            $("#list").on("click",function(e){
                e.preventDefault();
                fn_openBoardList();
            })
            $("#write").on("click",function(e){
                e.preventDefault();
                fn_writeBoard();
            })
            $("a[name='delete']").on("click",function(e){
                e.preventDefault();
                fn_fileDelete($(this));
            })
            $("#add").on("click",function(e){
                e.preventDefault();
                fn_fileAdd();
            })
        });
         
         
        function fn_openBoardList(){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/openBoardList.do'/>");
            comSubmit.submit();
        }
         
        function fn_writeBoard(){
            var comSubmit = new ComSubmit("frm");
            comSubmit.setUrl("<c:url value='/sample/writeBoard.do'/>");
            comSubmit.submit();
        }
        function fn_fileDelete(obj){
            obj.parent().remove();
        }
        function fn_fileAdd(){
            var str = "<p><input type='file' name='file_"+(g_count++)+"'/><a href='#this' name='delete' class='btn'>삭제하기</a></p> ";
            $("#fileDiv").append(str);
             
            $("a[name='delete']").on("click",function(e){
                e.preventDefault();
                fn_fileDelete($(this));        
            })
        }
    </script>
</body>
</html>

- 31 줄

여기서 name이 file_0로 한 이유가 설명이 된다. 다른 파일들과 name에서 뒤의 숫자로 각각을 판단하여 추가하거나 삭제할 수 있도록 하기 위하여 name을 설정해주었다.


- 32,36 줄

삭제하기와 파일 추가하기 버튼을 생성해주었다. 이 버튼들로 더 등록할 파일을 추가하거나 삭제할 수 있다.


-43 줄

g_count 라는 것이 파일들을 더 추가할 때 서로 구분하기 위하여 name 뒤에 숫자를 붙일 때 쓰는 변수이다.


- 53~60 줄

삭제하기와 파일 추가하기 버튼에 대한 이벤트 처리를 주었다.


- 75~77 줄

삭제하기 버튼의 처리 중 해당 버튼의 부모 클래스를 지움으로써 파일 부분을 지우는데 여기서 부모 클래스는 fileDiv 안에 포함되어 있는 <p> 태그이다. 따라서 <p>태그를 지우면서 <input type="file"> 도 함께 지워진다.


- 78~85 줄

파일 추가하기 버튼의 처리로 <p>태그로 묶은 부분을 전체적으로 다 변수에 넣고 <div>태그에 추가하여 준다. 또한 해당 삭제하기 버튼에도 따로 이벤트 처리를 줌으로써 각각 고유의 삭제하기 버튼의 처리 기능이 완성 된다.


여기까지하면 전에 파일 업로드를 할 때 리스트와 반복물을 통해 파일들에 대한 처리를 서비스단이나 FileUtils에서 구성해주었기 때문에 문제없이 될 것이다. 상세보기에서도 첨부파일 부분은 반복문을 써놓았기 때문에 따로 수정할 필요는 없다.


1.2 테스트

다음과 같이 작성하고 파일을 2개 추가하였다.


- 다음 화면은 그 결과로 상세보기 화면에서도 2개의 파일을 볼 수 있다.



- 다음은 로그 화면으로 2개의 insertFile의 DAO 로그와 SQL 로그를 볼 수 있다.




2. 게시판 수정하기(파일 업로드)

앞서 게시판 수정하기에서는 글 제목과 글 내용만 수정하였다. 이제는 파일 업로드라는 새로운 것이 추가 되었기에 새로 코드를 수정하여야 한다. 하지만 수정하기에 앞서 어떤 식으로 만들 지에 대해 고민해 보아야 한다.


2.1 게시판 수정하기 처리

- 게시글의 내용만 수정하고 파일은 그대로한다.

- 기존의 파일을 변경한다.

- 기존의 파일을 삭제한다.

- 기존의 파일을 두고 새로운 파일을 추가한다.

- 기존의 파일을 삭제하고 파일을 추가한다.

- 기존의 파일을 수정하고 파일을 추가한다.

... 등등 여러가지 처리에 대한 방법이 필요할 것이다.


모두 같은 방법을 쓰는 것은 아니고 여기에서는 해당 게시물에 포함되는 모든 파일들을 삭제처리(DEL_GB="Y")를 하고 기존의 파일이 변경되면 새롭게 추가하고 파일이 추가되면 또 새롭게 추가하고 삭제했을 경우 그대로 두고 그대로 두었을 땐 삭제처리만 번복(DEL_GB="N")을 해주는 방식을 사용할 것이다. 그렇게 하기 위하여 먼저 Controller 부분을 작성하자.


2.2 Controller

SampleController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RequestMapping(value="/sample/openBoardModify.do")
public ModelAndView openBoardModify(CommandMap commandMap) throws Exception{
    ModelAndView mv = new ModelAndView("/sample/boardModify");
    Map<String,Object> map = sampleService.selectBoard(commandMap.getMap());
    mv.addObject("map",map.get("map"));
    mv.addObject("list",map.get("list"));
    return mv;
}
 
@RequestMapping(value="/sample/modifyBoard.do")
public ModelAndView modifyBoard(CommandMap commandMap, HttpServletRequest req) throws Exception{
    ModelAndView mv = new ModelAndView("redirect:/sample/openBoardDetail.do");
    sampleService.modifyBoard(commandMap.getMap(),req);
     
    mv.addObject("IDX", commandMap.get("IDX"));
    return mv;
}

- openBoardModify를 수정하자. Detail과 동일하게 파일을 넘겨줄 수 있게 list를 따로 할당해주었다. 크게 변경한 것은 없다. 또한 modifyBoard에도 파라미터로 HttpServletRequest 를 추가하였고 Service 단에도 같이 넘겨주었다.


2.3 Service

SampleServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void modifyBoard(Map<String, Object> map, HttpServletRequest req) throws Exception {
    sampleDAO.modifyBoard(map);
     
    sampleDAO.deleteFile(map,req);
     
    List<Map<String, Object>> list = fileUtils.parseUpdateFileInfo(map, req);
    Map<String,Object> tempMap = null;
    for(int i = 0; i<list.size();i++) {
        tempMap = list.get(i);
         
        if(tempMap.get("IS_NEW").equals("Y")) {
            sampleDAO.insertFile(tempMap);
        }else {
            sampleDAO.updateFile(tempMap);
        }
    }
}

- modifyBoard를 수정하자. HttpServletRequest 파라미터를 추가하였다.


- 5 줄

deleteFile 이라는 것은 아까 위에서 말했듯이 해당 게시물에 포함되는 해당 파일들을 삭제하는 부분이다. (DEL_GB="Y")


- 7 줄

우리가 만든 fileUtils 클래스의 메서드를 쓰는 것으로 parseInserFileInfo와 비슷한 기능을 하는 것으로 파일 정보를 list에 반환한다.


- 9~17 줄

반환 받은 리스트들, 즉 파일들에 IS_NEW라는 키가 있다. 해당 키는 새롭게 추가된 파일인지 기존의 파일인지를 구분하기 위해 만든 키로 Y는 새로운 파일로 insert 처리를 하고, N은 기존의 파일로 하여 update 처리를 할 수 있게 해주었다.


2.4 DAO

SampleDAO.java

1
2
3
4
5
6
7
public void deleteFile(Map<String, Object> map, HttpServletRequest req) {
    update("sample.deleteFile",map);
}
 
public void updateFile(Map<String, Object> map) {
    update("sample.updateFile",map);
}

- deleteFile과 updateFile 에 대한 sql 처리를 담당하는 메서드들을 추가하였다. Service 단에서 사용했던 것들로 insertFile은 기존에 이미 있다.


2.5 SQL

sample_SQL.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<update id="sample.deleteFile" parameterType="hashmap">
   <![CDATA[
       UPDATE TB_FILE1
       SET DEL_GB = 'Y'
       WHERE
           BOARD_IDX=#{IDX}
   ]]>
</update>
 
<update id="sample.updateFile" parameterType="hashmap">
   <![CDATA[
       UPDATE TB_FILE1
       SET DEL_GB = 'N'
       WHERE
           IDX=#{FILE_IDX}
   ]]>
</update>

- deleteFile에서 IDX는 게시글의 번호로 게시글에 해당 되는 모든 파일을 지우기 위하기 위함이고, updateFile에서 IDX는 파일 번호로 기존에 있던 파일 중 변경되지 않은 파일을 다시 복구하는 것이다. 물론 삭제는 DELETE 하는 것이 아니라 DEL_GB를 사용하여 기존에 보이지 않도록만 하는 것이다. 


2.6 FileUtils

FileUtils.java

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
public List<Map<String, Object>> parseUpdateFileInfo(Map<String, Object> map, HttpServletRequest req) throws Exception {
        MultipartHttpServletRequest mulReq = (MultipartHttpServletRequest)req;
        MultipartFile mulFile=null;
         
        String original_File_Name = null;
        String stored_File_Name = null;
        String original_Extension = null;
         
        Iterator<String> iterator = mulReq.getFileNames();
        String board_IDX = map.get("IDX").toString();
        String IDX=null;
        List<Map<String,Object>> fileList = new ArrayList<Map<String,Object>>();
        Map<String,Object> fileMap = null;
        while(iterator.hasNext()) {
            mulFile = mulReq.getFile(iterator.next());
             
            if(mulFile.isEmpty()==false) {
                original_File_Name = mulFile.getOriginalFilename();
                original_Extension = original_File_Name.substring(original_File_Name.lastIndexOf("."));
                stored_File_Name = CommonUtils.getRandomString() + original_Extension;
                 
                mulFile.transferTo(new File(filePath+stored_File_Name));
                 
                fileMap = new HashMap<String,Object>();
                fileMap.put("BOARD_IDX", board_IDX);
                fileMap.put("ORIGINAL_FILE_NAME", original_File_Name);
                fileMap.put("STORED_FILE_NAME", stored_File_Name);
                fileMap.put("FILE_SIZE", mulFile.getSize());
                fileMap.put("IS_NEW","Y");
                fileList.add(fileMap);
                 
            }else {
                String requestName = mulFile.getName();
                IDX = "IDX_"+requestName.substring(requestName.indexOf("_")+1);
                if(map.containsKey(IDX) == true &&map.get(IDX)!=null) {
                    fileMap = new HashMap<String,Object>();
                    fileMap.put("IS_NEW", "N");
                    fileMap.put("FILE_IDX",map.get(IDX));
                    fileList.add(fileMap);
                }
            }
        }
        return fileList;
    }

- 전에 작성했던 parseInsertFileInfo와 매우 흡사한 구조이다. 특별히 다른 것이라고는 파일 번호의 IDX가 필요하여 변수로 하여 받아온다는 것과 14~42줄 부분이다.


- 14~42줄

기존과 다르게 fileMap에 IS_NEW 키를 넣는 것을 알 수 있다. 위에서 말했듯이 새로운 파일이냐 아니냐를 구분하는 키이다. 기존의 파일인지 아닌지는 17줄의 if(mulFile.isEmpty()==false)를 통해서 알 수 있는데 수정하기 페이지에서 기존의 파일을 그대로 둘 시에는 Request에 포함되지 않는데 이는 <input type="file"> 태그로 넘겨져 오는데 이 부분이 없기 때문이다. 따라서 기존의 파일과 새 파일들은 17줄로 구분을 할 수 있다. 그러면 어떤 식으로 기존 파일 중에서도 삭제된 파일이나 수정된 파일 그리고 그대로 둔 파일을 구분 짓는 지에 대해서는 33~35줄을 보면 된다. mulFile로 부터 Name을 받아오는데 이는 <input type="file" > 태그의 name 속성에 해당하는 부분을 가져온다. 

예시로 작성을 해보자면 <input type="file" name="file_0"> 이라면 file_0을 가져오는 것이다. requestName에는 file_0이 있는 상태에서 34줄이 IDX = "IDX_" + "0" 처럼 해석이 되는 것이다. IDX = IDX_0이 되어 map.containsKey(IDX)를 통해 수정하기 페이지 중에 name이 IDX_0이 있는 것과 값을 갖고 있는 지 확인 뒤 그 값을 fileMap에 추가하는 것을 볼 수 있다. 물론 name이 IDX_0인 태그의 값은 기존 파일의 IDX 값을 갖고 있다.


2.7 JSP

boardModify.jsp

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>글 수정하기</title>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
    <form id="frm" enctype="multipart/form-data">
        <table class="board_view">
            <caption>글 수정하기</caption>
            <colgroup>
                <col width="15%">
                <col width="35%">
                <col width="15%">
                <col width="35%">
            </colgroup>
            <tbody>
                <tr>
                    <th scope="row">글 번호</th>
                    <td>
                        ${map.IDX }
                        <input type="hidden" id="IDX" name="IDX" value="${map.IDX }">
                    </td>
                    <th scope="row">조회수</th>
                    <td>${map.HIT_CNT }</td>
                </tr>
                <tr>
                    <th scope="row">작성자</th>
                    <td>${map.CREA_ID }</td>
                    <th scope="row">작성시간</th>
                    <td>${map.CREA_DTM }</td>
                </tr>
                <tr>
                    <th scope="row">제목</th>
                    <td colspan="3">
                        <input type="text" id="TITLE" name="TITLE" class="wdp_90" value="${map.TITLE }"/>
                    </td>
                </tr>
                <tr>
                    <th scope="row">내용</th>
                    <td colspan="3" class="view_text">
                        <textarea rows="20" cols="100" title="내용" id="CONTENTS" name="CONTENTS">${map.CONTENTS }</textarea>
                    </td>
                </tr>
                <tr>
                    <th>첨부파일</th>
                    <td colspan="3">
                        <div id="fileDiv">
                            <c:forEach items="${list }" varStatus="row" var="var" >
                                <p>
                                    <input name="IDX_${row.index }" id="IDX" type="hidden" value="${var.IDX }"/>
                                    <a href="#this" name="name_${row.index }" id="name_${row.index }">${var.ORIGINAL_FILE_NAME }</a>
                                    <input type="file" name="file_${row.index }">
                                    <a href="#this" id="delete_${row.index }" name="delete_${row.index }" class="btn">삭제하기</a>
                                </p>
                            </c:forEach>
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>
    </form>
    <a href="#this" id="list" class="btn">목록으로</a>
    <a href="#this" id="add" class="btn">파일 추가하기</a>
    <a href="#this" id="modify" class="btn">수정하기</a>
    <a href="#this" id="delete" class="btn">삭제하기</a>
     
    <%@ include file="/WEB-INF/include/include-body.jspf" %>
    <script type="text/javascript">
        var gfv_count = '${fn:length(list)+1}';
        $(document).ready(function(){
            $("#modify").on("click",function(e){
                e.preventDefault();
                fn_modifyBoard();
            })
            $("#list").on("click",function(e){
                e.preventDefault();
                fn_openBoardList();
            })
            $("#delete").on("click",function(e){
                e.preventDefault();
                fn_deleteBoard();
            })
            $("#add").on("click",function(e){
                e.preventDefault();
                fn_fileAdd();  
            })
            $("a[name^='delete']").on("click",function(e){
                e.preventDefault();
                fn_fileDelete($(this));
            })
        })
        function fn_fileAdd(){
            var str = "<p>" +
                    "<input type='file' id='file_"+(gfv_count)+"' name='file_"+(gfv_count)+"'>"+
                    "<a href='#this' class='btn' id='delete_"+(gfv_count)+"' name='delete_"+(gfv_count)+"'>삭제</a>" +
                "</p>";
            $("#fileDiv").append(str);
            $("#delete_"+(gfv_count++)).on("click", function(e){ //삭제 버튼
                e.preventDefault();
                fn_fileDelete($(this));
            });
        }
        function fn_fileDelete(obj){
            obj.parent().remove();
        }
         
        function fn_modifyBoard(){
            var comSubmit = new ComSubmit("frm");
            comSubmit.setUrl("<c:url value='/sample/modifyBoard.do'/>");
            comSubmit.submit();
        }
        function fn_openBoardList(){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/openBoardList.do'/>");
            comSubmit.submit();
        }
        function fn_deleteBoard(){
            var comSubmit = new ComSubmit("frm");
            comSubmit.setUrl("<c:url value='/sample/deleteBoard.do'/>");
            comSubmit.submit();
        }
    </script>
</body>
</html>

- 52~57 줄

해당 부분이 기존 파일이 있고 파일을 추가하거나 변경하고 삭제하고 할 수 있는 부분이다. 위에서 말했던 기존 파일에서 IDX 값을 가져오는 부분으로 55줄을 보면 <input type="file" name="file_${row.index}"> 라고 되있는 부분이 있다. 이 부분의 name값이 위에서 말했던 것처럼 돌아가는 것이다. row.index라는 것은 forEach 구문에서 속성 varStatus로 지정된 변수의 index값으로 0부터 증가하여 고유의 번호들을 지정해준다.


- 72 줄

var gfv_count 가 위에서 index값과 동일한 기능을 한다. 파일들의 개수를 계산하여 +1하여 중복을 피하였다.


- 95~105 줄

글쓰기 부분에서 추가하는 부분과 비슷한 방식이지만 고유 번호를 위한 gfv_count의 빈도 수가 많다보니 조금은 복잡해 보인다.


후에 다른 것은 크게 주목할 것들은 없다. 이후 테스트를 해보자.


3. 테스트

3.1 파일을 수정



- 위에 파일 하나만 수정하고 수정하여 보았다.



- 정상적으로 잘 처리 된다.



- 로그의 경우 modifyBoard로 게시물의 제목과 내용을 수정하고 deleteFile로 해당 파일들을 전부 삭제 한 뒤, insertFile로 수정한 파일을 새로 추가하고 기존에 그대로 남아 있는 파일을 복구 시키는 updateFile을 볼수 있다.


3.2 파일을 모두 삭제

- 파일을 모두 삭제하고 수정해보았다.



- 모두 삭제된 결과를 볼 수 있다.



- modifyBoard와 deleteFile 처리만 일어난 것을 볼 수 있다.


3.3 파일이 없는 게시물에 파일 추가

- 파일이 비어있던 게시물에 파일을 2개 추가하여 수정하여 보았다.




- 정상적으로 완료된 것을 볼 수 있다.



- modifyBoard가 실행되고 아무것도 없지만 deleteFile도 실행된 뒤 insertFile이 2개가 실행됨을 볼 수 있다. deleteFile에 대한 것은 스스로 조건을 달아 파일이 없는 경우에는 실행되지 않도록 만들어보도록 하고 여기까지 따라왔으면 잘 따라왔다는 것이다.