백엔드/Spring Framework

게시판 구현 / 8-2. '상세 글' 파일 업로드(미리보기, 수정하기, 삭제)

maverick11471 2024. 7. 23. 17:55

2024.07.23 - [스프링 프레임워크] - 게시판 구현 / 8-1. '글 등록' 파일 업로드(미리보기, 수정하기, 삭제)

 

게시판 구현 / 8-1. '글 등록' 파일 업로드(미리보기, 수정하기, 삭제)

프론트 수정 글 등록 시 파일첨부를 미리보기 추가, 수정, 삭제하는 방법에 대해 알아보자.글 등록 시 미리보기 추가[post.jsp 수정] - 1. 선택된 파일을 배열로 변환 - 2. imageLoader 메소드 호출 - 3.

maverick11471.tistory.com

 

이전에는 글 등록시 파일업로드를 살펴봤다면, 이번에는 상세 글의 파일 업로드에 관해 알아보겠다.

작성한 작성자가 해당 글 상세페이지에 들어가 첨부파일을 수정하는 방법에 대해 알아보자.

 


붙임파일 추가하기(클릭이벤트)

[free-detail 수정]

 - post.jsp 에 있는 배열담는 script 가져오기

<script>

// 기존에 업로도되어 있는 파일들을 담아줄 배열
// 게시글번호, 파일번호, 파일명을 객체 형태로 담아준다.

$(() => {
    // 업로드되어 있던 파일들을 originFiles 배열에 담기
    for(let i = 0; i < $("#filecnt").val(); i++) {
        const originFileObj = {
            board_id: $("input[name='id']").val(),
            id: $("#fileId" + i).val(),
            filename: $("#filename" + i).val(),
            filestatus: "N" // 초기상태 변경없음: N, 변경되면: U, 삭제되면: D
        };

        originFiles.push(originFileObj);
    }

    $("#uploadFiles").on("change", (e) => {
        // input에 추가된 파일들 변수로 받기
        const files = e.target.files;

        // 변수로 받아온 파일들 배열로 변환
        const fileArr = Array.prototype.slice.call(files);

        for(file of fileArr) {
            // 미리보기 메소드 호출
            imageLoader(file);
        }
    });
});

 

 - script에 미리보기 처리하는 메소드, div 생성하는 메소드를 추가한다.

    // 미리보기 처리하는 메소드
        // 미리보기될 파일은 서버나 데이터베이스에 저장된 상태가 아니기 때문에
        // 파일 자체를 Base64 인코딩 방식으로 문자열로 변환해서 이미지로 호출해야 된다.
        // 이미지가 들어갈 태그 생성과 파일을 Base64 인코딩
        const imageLoader = (file) => {
            // 추가된 파일 uploadFiles 배열에 담기
            uploadFiles.push(file);

            let reader = new FileReader();

            // reader가 호출되면 실행될 이벤트 등록
            reader.onload = (e) => {
                // 이미지를 표출할 img 태그 생성
                let img = document.createElement("img");

                img.classList.add("upload-file");

                // 이미지인지 아닌지 판단
                if(file.name.toLowerCase().match(/(.*?)\.(jpg|jpeg|png|gif|svg|bmp)$/)) {
                    img.src = e.target.result;
                } else {
                    img.src = "/static/images/defaultFileImg.png";
                }

                // 미리보기 영역에 추가
                // makeDiv 메소드를 호출해서 만들어진 div 자체를 preview 영역에 추가
                $("#preview").append(makeDiv(img, file));
            }

            // 파일을 Base64인코딩된 문자열로 로드
            // 이 메소드가 실행되면서 위에서 등록한 onload 이벤트가 함께 동작한다.
            reader.readAsDataURL(file);
        }

        // 미리보기 영역에 추가될 div를 생성하는 메소드
        const makeDiv = (img, file) => {
            let div = document.createElement("div");

            div.classList.add("upload-file-div");

            // 삭제 버튼 추가
            let btn = document.createElement("input");

            btn.classList.add("upload-file-delete-btn");

            btn.setAttribute("type", "button");
            btn.setAttribute("value", "x");
            // 사용자 정의 속성 추가
            btn.setAttribute("deleteFile", file.name);

            // x 버튼에 클릭했을 때 삭제하는 기능 추가
            btn.onclick = (e) => {
                // 클릭된 버튼 변수로 받기
                const element = e.target;

                const deleteFileName = element.getAttribute("deleteFile");

                // 배열에서 파일 삭제
                for(let i = 0; i < uploadFiles.length; i++) {
                    if(deleteFileName === uploadFiles[i].name) {
                        uploadFiles.splice(i, 1);
                    }
                }
                // uploadFiles.filter(((file, index) => file.name != deleteFileName || uploadFiles.indexOf(file) != index));

                // input에서도 파일 삭제
                // input type="file"은 첨부된 파일들을 fileList 형태로 관리
                // fileList는 File 객체에 바로 담을수 없기 때문에
                // DataTransfer라는 클래스를 사용해서 변환 후에 담아줘야한다.
                let dataTransfer = new DataTransfer();

                for(i in uploadFiles) {
                    // uploadFiles 배열에 있는 File 객체를 하나씩 DataTransfer 객체에 담아준다.
                    const file = uploadFiles[i];
                    dataTransfer.items.add(file);
                }

                // input type="file"에 fileList 형태로 밀어넣기
                $("#uploadFiles")[0].files = dataTransfer.files;

                // 클릭된 btn 태그를 소유하고 있는 부모 div 태그 삭제
                const parentDiv = element.parentNode;
                $(parentDiv).remove();
            }

            // 파일 이름을 표출할 p 태그 생성
            let p = document.createElement("p");

            p.classList.add("upload-file-name");

            p.textContent = file.name;

            // div태그에 img, btn, p 태그 자식으로 추가
            div.appendChild(img);
            div.appendChild(btn);
            div.appendChild(p);

            return div;
        }

    </script>

붙임파일 수정하기

[free-detail.jsp 수정]

<div id="preview" class="mt-3 text-center"
     data-placeholder="파일을 첨부하려면 파일선택 버튼을 누르세요.">
    <!-- 파일 목록을 반복하여 각 파일의 미리보기를 생성 -->
    // varStatus는 JSTL의 <c:forEach> 태그에서 사용되는 속성으로, 
    // 반복문의 상태 정보를 담고는 변수입니다. 
    // 이 변수는 현재 반복 중인 아이템의 인덱스, 첫 번째 또는 마지막 아이템인지 여부 등
    // 여러 정보를 제공
    <c:forEach items="${fileList}" var="file" varStatus="status">
        <div class="upload-file-div">
            // 파일 ID와 파일명을 저장하기 위한 숨겨진 input
            // status.index: 현재 반복의 인덱스 (0부터 시작)
            <input type="hidden" id="fileId${status.index}" value="${file.id}">
            <input type="hidden" id="filename${status.index}" value="${file.filename}">
            // 파일 선택을 위한 숨겨진 input[type="file"] 생성
            <input type="file" id="changeFile${file.id}" name="changeFile${file.id}" style="display: none;">
            // 마지막 파일일 경우 파일 개수를 저장
            // status.last: 현재 아이템이 마지막 아이템인지 여부 (boolean 값)
            <c:if test="${status.last}">
            // status.count: 현재 반복이 몇 번째 아이템인지 (1부터 시작)
                <input type="hidden" id="filecnt" name="filecnt" value="${status.count}">
            </c:if>
            // 파일의 타입에 따라 이미지 표시
            <c:choose>
                <c:when test="${file.filetype eq 'image'}">
                    <img id="img${file.id}"
                         src="/upload/${file.filename}"
                         class="upload-file"
                         alt="${file.fileoriginname}"
                         onclick="fileClick(${file.id})"> // 클릭 시 파일 선택
                </c:when>
                <c:otherwise>
                    <img id="img${file.id}"
                         src="/static/images/defaultFileImg.png"
                         class="upload-file"
                         alt="${file.fileoriginname}"
                         onclick="fileClick(${file.id})"> // 클릭 시 파일 선택
                </c:otherwise>
            </c:choose>
        </div>
    </c:forEach>
</div>
<script>
    // 추가된 파일들을 담아줄 배열
    const uploadFiles = [];

    // 기존에 업로드되어 있는 파일들을 담아줄 배열
    // 게시글번호, 파일번호, 파일명을 객체 형태로 담아준다.
    const originFiles = [];

    // 변경된 파일들을 담아줄 배열
    const changeFiles = [];

    // 업로드되어 있는 파일을 클릭했을 때 실행될 메소드
    const fileClick = (fileId) => {
        // 해당 파일 ID에 매핑된 숨겨진 파일 입력 요소를 클릭하여 파일 선택 대화상자를 엽니다.
        $("#changeFile" + fileId).click();
    }
    
    const changFile = (fileId, e) => {
    // 이벤트에서 파일 목록을 가져옴
    const files = e.target.files;

    // FileList 객체를 배열로 변환
    const fileArr = Array.prototype.slice.call(files);

    // 변경된 파일을 changeFiles 배열에 추가
    changeFiles.push(fileArr[0]);

    // FileReader 객체 생성
    const reader = new FileReader();

    // 파일 읽기가 완료되면 실행되는 이벤트 핸들러
    reader.onload = (event) => {
        // 이미지와 파일명을 표시할 요소 가져오기
        const img = document.getElementById("img" + fileId);
        const p = document.getElementById("filename" + fileId);

        // 파일 형식이 이미지인지 확인
        if(fileArr[0].name.match(/(.*?)\.(jpg|jpeg|png|gif|bmp)$/)) {
            // 이미지가 맞으면 읽어온 결과로 이미지 소스를 설정
            img.src = event.target.result;
        } else {
            // 이미지가 아닐 경우 기본 이미지 설정
            img.src = "/static/images/defaultFileImg.png";
        }

        // 파일명 텍스트를 업데이트
        p.textContent = fileArr[0].name;
    }

    // 첫 번째 파일을 데이터 URL로 읽기 시작
    reader.readAsDataURL(fileArr[0]);

    // 기존에 originFiles 배열에 담겨있던 내용 변경
    for(let i = 0; i < originFiles.length; i++) {
        // 파일 ID가 originFiles의 ID와 일치하는 경우
        if(fileId == originFiles[i].id) {
            // 파일 상태를 'U'로 변경 (업데이트됨)
            originFiles[i].filestatus = "U";
            // 새로운 파일명을 업데이트
            originFiles[i].newfilename = fileArr[0].name;
        }
    }
}

</script>

 - onchage 추가

<!--밑에 표시된 파일을 클릭했을 때 파일 선택창이 뜨도록 input type="file" 하나 생성-->
<input type="file" id="changeFile${file.id}" name="changeFile${file.id}" style="display: none;"
		// onchange 추가
       onchange="changFile(${file.id}, event)">

 


붙임파일 삭제하기(클릭이벤트)

[free-detail.jsp 수정]

<div id="preview" class="mt-3 text-center"
     data-placeholder="파일을 첨부하려면 파일선택 버튼을 누르세요.">
    <c:forEach items="${fileList}" var="file" varStatus="status">
        <div class="upload-file-div">
            <input type="hidden" id="fileId${status.index}" value="${file.id}">
            <input type="hidden" id="filename${status.index}" value="${file.filename}">
            <!--밑에 표시된 파일을 클릭했을 때 파일 선택창이 뜨도록 input type="file" 하나 생성-->
            <input type="file" id="changeFile${file.id}" name="changeFile${file.id}" style="display: none;"
            	   // onchange 추가
                   onchange="changFile(${file.id}, event)">

 

<input type="button" value="x"
       deleteFile="${file.id}"
       class="upload-file-delete-btn"
       onclick="deleteFile(event)">
<script>
    const deleteFile = (e) => {
            // 클릭된 x 버튼 변수로 담기
            const element = e.target;

            const deleteFileId = element.getAttribute("deleteFile");

            // originFiles 배열에서 deleteFileId와 id가 같은 객체의 filestatus를 D로 변경
            for(let i = 0; i < originFiles.length; i++) {
                if(deleteFileId == originFiles[i].id) {
                    originFiles[i].filestatus = "D";
                }
            }

            // 부모 div 삭제
            const parentDiv = element.parentNode;
            $(parentDiv).remove();
</script>

백엔드로 전달

[free-detail.jsp] 수정

 


(참고) id=modify-form 형태 form

 - 객체 배열 형태의 originFiles 배열을 문자열로 변환하여 input에 담아서 전송

 - id = chageFiles인 input형태

<form id="modify-form" action="/board/modify.do" method="post" enctype="multipart/form-data">
    <input type="hidden" name="id" value="${freeBoard.id}">
    <input type="hidden" name="type" value="free">
    <!--객체 배열 형태의 originFiles 배열을 문자열로 변환하여 input에 담아서 전송-->
    <input type="hidden" name="originFiles" id="originFiles">
    <div class="form-group">
        <label for="title">제목</label>
        <input type="text" class="form-control" id="title" name="title" value="${freeBoard.title}" required>
    </div>
    <div class="form-group mt-3">
        <label for="nickname">작성자</label>
        <input type="text" class="form-control" id="nickname" name="nickname" value="${freeBoard.nickname}" readonly>
    </div>
    <div class="form-group mt-3">
        <label for="content">내용</label>
        <textarea class="form-control" id="content" name="content" rows="10" required>${freeBoard.content}</textarea>
    </div>
    <div class="form-group mt-3">
        <label for="regdate">등록일</label>
        <input type="text" class="form-control" id="regdate" name="regdate"
               value="<javatime:format value="${freeBoard.regdate}" pattern="yyyy-MM-dd"/>" readonly required>
    </div>
    <div class="form-group mt-3">
        <label for="moddate">수정일</label>
        <input type="text" class="form-control" id="moddate" name="moddate"
               value="<javatime:format value="${freeBoard.moddate}" pattern="yyyy-MM-dd"/>" readonly required>
    </div>
    <div class="form-group mt-3">
        <label for="cnt">조회수</label>
        <input type="text" class="form-control" id="cnt" name="cnt" value="${freeBoard.cnt}" readonly required>
    </div>
    <div class="form-group mt-3">
        <label for="uploadFiles">파일첨부</label>
        <input class="form-control" type="file" name="uploadFiles" id="uploadFiles" multiple>
        <div id="image-preview">
        	// id = chagneFiles input 형태
            <input type="file" id="changeFiles" name="changeFiles" style="display: none;"
                   multiple>
            <p style="color: red; font-size:0.9rem;">
                파일을 변경하려면 이미지를 클릭하세요. 파일을 다운로드하려면 파일이름을 클릭하세요. 파일을 추가하려면 파일 선택 버튼을 클릭하세요.
            </p>
            <div id="preview" class="mt-3 text-center"
                 data-placeholder="파일을 첨부하려면 파일선택 버튼을 누르세요.">
                <c:forEach items="${fileList}" var="file" varStatus="status">
                    <div class="upload-file-div">
                        <input type="hidden" id="fileId${status.index}" value="${file.id}">
                        <input type="hidden" id="filename${status.index}" value="${file.filename}">
                        <!--밑에 표시된 파일을 클릭했을 때 파일 선택창이 뜨도록 input type="file" 하나 생성-->
                        <input type="file" id="changeFile${file.id}" name="changeFile${file.id}" style="display: none;"
                               onchange="changFile(${file.id}, event)">
                        <c:if test="${status.last}">
                            <input type="hidden" id="filecnt" name="filecnt" value="${status.count}">
                        </c:if>
                        <c:choose>
                            <c:when test="${file.filetype eq 'image'}">
                                <img id="img${file.id}"
                                     src="/upload/${file.filename}"
                                     class="upload-file"
                                     alt="${file.fileoriginname}"
                                     onclick="fileClick(${file.id})">
                            </c:when>
                            <c:otherwise>
                                <img id="img${file.id}"
                                     src="/static/images/defaultFileImg.png"
                                     class="upload-file"
                                     alt="${file.fileoriginname}"
                                     onclick="fileClick(${file.id})">
                            </c:otherwise>
                        </c:choose>
                        <input type="button" value="x"
                               deleteFile="${file.id}"
                               class="upload-file-delete-btn"
                               onclick="deleteFile(event)">
                        <p id="filename${file.id}"
                           class="upload-file-name">
                            ${file.fileoriginname}
                        </p>
                    </div>
                </c:forEach>
            </div>
        </div>
    </div>
    <c:if test="${loginMember ne null and loginMember.id eq freeBoard.WRITER_ID}">
        <div class="container mt-3 mb-5 w-50 text-center">
            <button type="submit" id="btn-update" class="btn btn-outline-secondary">수정</button>
            <button type="button" id="btn-delete" class="btn btn-outline-secondary ml-2" onclick="location.href='/board/delete.do?id=${freeBoard.id}&type=free'">삭제</button>
        </div>
    </c:if>
</form>

 


 

[ script 수정]

<script>
     $(() => {
        // modify-form이 서브밋될 때
        // uploadFiles 배열에 담겨있는 파일들을 input name="uploadFiles"에 담기
        // changeFiles 배열에 담겨있는 파일들을 input name="changeFiles"에 담기
        // originFiles 배열에 담겨있는 객체들을 문자열로 변환하여 input name="originFiles"에 담기
        $("#modify-form").on("submit", (e) => {
            let dataTransfer1 = new DataTransfer();
            let dataTransfer2 = new DataTransfer();

            for(i in uploadFiles) {
                const file = uploadFiles[i];
                dataTransfer1.items.add(file);
            }

            $("#uploadFiles")[0].files = dataTransfer1.files;

            for(i in changeFiles) {
                const file = changeFiles[i];
                dataTransfer2.items.add(file);
            }

            $("#changeFiles")[0].files = dataTransfer2.files;

            $("#originFiles").val(JSON.stringify(originFiles));
        });
    });

 

[controller 수정]

@PostMapping("/modify.do")
// 매개변수 MultipartFile[] uploadFiles, MultipartFile[] changeFiles,
// @RequestParam(name = "originFiles", required = false) String originFiles 추가
public String modify(BoardDto boardDto, MultipartFile[] uploadFiles, MultipartFile[] changeFiles,
                     @RequestParam(name = "originFiles", required = false) String originFiles) {
    if(boardDto.getType().equals("free")) {
        boardService = applicationContext.getBean("freeBoardServiceImpl", BoardService.class);
    } else {
        boardService = applicationContext.getBean("noticeServiceImpl", BoardService.class);
    }
    
    // 매개변수 uploadFiles, changeFiles, originFiles 추가
    boardService.modify(boardDto, uploadFiles, changeFiles, originFiles);

    if(boardDto.getType().equals("free"))
        return "redirect:/board/free-detail.do?id=" + boardDto.getId();
    else
        return "redirect:/board/notice-detail.do?id=" + boardDto.getId();
}

 

[service 수정]

public interface BoardService {
	// 매개변수 MultipartFile[] uploadFiles, MultipartFile[] changeFiles, String originFiles 추가
    void modify(BoardDto boardDto, MultipartFile[] uploadFiles, MultipartFile[] changeFiles, String originFiles);
}

 

 

[impl 수정]

@Override
// 매개변수 MultipartFile[] uploadFiles, MultipartFile[] changeFiles, String originFiles 추가
public void modify(BoardDto boardDto, MultipartFile[] uploadFiles, 
MultipartFile[] changeFiles, String originFiles) {
    // JSON String 형태의 originFiles를 List<BoardFileDto> 형태로 변환
    List<BoardFileDto> originFileList = new ArrayList<>();

    try {
    // string인 originFileList를 List 형태로 변경
        originFileList = new ObjectMapper().readValue(originFiles, new TypeReference<List<BoardFileDto>>() {});
    } catch(IOException ie) {
        System.out.println(ie.getMessage());
    }

    String attachPath = "C:/tmp/upload/";

    File directory = new File(attachPath);

    if(!directory.exists()) {
        directory.mkdirs();
    }

    // 추가, 수정, 삭제 되는 파일들의 목록을 담아줄 리스트
    List<BoardFileDto> uFileList = new ArrayList<>();

    // 수정, 삭제되는 파일들을 uFileList에 담기
    if(originFileList.size() > 0) {
        originFileList.forEach(boardFileDto -> {
            if (boardFileDto.getFilestatus().equals("U") && changeFiles != null) {
                Arrays.stream(changeFiles).forEach(file -> {
                    if (boardFileDto.getNewfilename().equals(file.getOriginalFilename())) {
                        BoardFileDto updateBoardFileDto = FileUtils.parserFileInfo(file, attachPath);

                        updateBoardFileDto.setBoard_id(boardFileDto.getBoard_id());
                        updateBoardFileDto.setId(boardFileDto.getId());
                        updateBoardFileDto.setFilestatus("U");

                        uFileList.add(updateBoardFileDto);
                    }
                });
            } else if (boardFileDto.getFilestatus().equals("D")) {
                BoardFileDto deletBoardFileDto = new BoardFileDto();

                deletBoardFileDto.setBoard_id(boardFileDto.getBoard_id());
                deletBoardFileDto.setId(boardFileDto.getId());
                deletBoardFileDto.setFilestatus("D");

                uFileList.add(deletBoardFileDto);

                // 실제 서버에서 파일 삭제
                File deleteFile = new File(attachPath + boardFileDto.getFilename());

                deleteFile.delete();
            }
        });
    }
    
            
        // 추가된 파일들 uFileList에 담기
        if(uploadFiles != null && uploadFiles.length > 0) {
            Arrays.stream(uploadFiles).forEach(file -> {
                if(!file.getOriginalFilename().equals("") && file.getOriginalFilename() != null) {
                    BoardFileDto postBoardFileDto = FileUtils.parserFileInfo(file, attachPath);

                    postBoardFileDto.setBoard_id(boardDto.getId());
                    postBoardFileDto.setFilestatus("I");

                    uFileList.add(postBoardFileDto);
                }
            });
        }

        boardDto.setModdate(LocalDateTime.now());
        freeBoardDao.modify(boardDto, uFileList);
    }

 

 

[Dto 추가]

public class BoardFileDto {
    private int id;
    private int board_id;
    private String filename;
    private String fileoriginname;
    private String filepath;
    private String filetype;
    // filestatus, newfilename 추가 (impl에 쓰임)
    private String filestatus;
    private String newfilename;

 

[dao 수정]

 - impl에서 추가, 수정, 삭제한 내용을 uFileList에 넣어 Dao로 전송

public void modify(BoardDto boardDto, List<BoardFileDto> uFileList) {
    System.out.println("FreeBoardDao의 modify 메소드 실행");

    mybatis.update("FreeBoardDao.modify", boardDto);

    if(uFileList.size() > 0) {
        uFileList.forEach(boardFileDto -> {
        	// 추가
            if(boardFileDto.getFilestatus().equals("I")) {
                mybatis.insert("FreeBoardDao.postBoardFileOne", boardFileDto);
            // 수정
            } else if(boardFileDto.getFilestatus().equals("U")) {
                mybatis.update("FreeBoardDao.modifyBoardFileOne", boardFileDto);
            // 삭제
            } else if(boardFileDto.getFilestatus().equals("D")) {
                mybatis.delete("FreeBoardDao.deleteBoardFileOne", boardFileDto);
            }
        });
    }

    System.out.println("FreeBoardDao의 modify 메소드 실행 종료");
}

 

[mapper 수정]

<insert id="postBoardFileOne" parameterType="boardFile">
    INSERT INTO FREEBOARD_FILE(
        BOARD_ID,
        FILENAME,
        FILEORIGINNAME,
        FILEPATH,
        FILETYPE
    ) VALUES (
        #{board_id},
        #{filename},
        #{fileoriginname},
        #{filepath},
        #{filetype}
    )
</insert>

<update id="modifyBoardFileOne" parameterType="boardFile">
    UPDATE FREEBOARD_FILE
        SET
            FILENAME = #{filename},
            FILEORIGINNAME = #{fileoriginname},
            FILEPATH = #{filepath},
            FILETYPE = #{filetype}
        WHERE ID = #{id}
          AND BOARD_ID = #{board_id}
</update>

<delete id="deleteBoardFileOne" parameterType="boardFile">
    DELETE FROM FREEBOARD_FILE
        WHERE ID = #{id}
          AND BOARD_ID = #{board_id}
</delete>

 


아래는 free-detail.jsp 전문이다

<%--
  Created by IntelliJ IDEA.
  User: bitcamp
  Date: 24. 7. 15.
  Time: 오전 9:40
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="javatime" uri="http://sargue.net/jsptags/time" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<head>

</head>
<body>
    <div>
        <jsp:include page="${pageContext.request.contextPath}/header.jsp"></jsp:include>

        <main>
            <div class="container w-50 mt-5 mb-5">
                <h4>자유게시글 상세</h4>
            </div>
            <div class="container mt-3 w-50">
                <form id="modify-form" action="/board/modify.do" method="post" enctype="multipart/form-data">
                    <input type="hidden" name="id" value="${freeBoard.id}">
                    <input type="hidden" name="type" value="free">
                    <!--객체 배열 형태의 originFiles 배열을 문자열로 변환하여 input에 담아서 전송-->
                    <input type="hidden" name="originFiles" id="originFiles">
                    <div class="form-group">
                        <label for="title">제목</label>
                        <input type="text" class="form-control" id="title" name="title" value="${freeBoard.title}" required>
                    </div>
                    <div class="form-group mt-3">
                        <label for="nickname">작성자</label>
                        <input type="text" class="form-control" id="nickname" name="nickname" value="${freeBoard.nickname}" readonly>
                    </div>
                    <div class="form-group mt-3">
                        <label for="content">내용</label>
                        <textarea class="form-control" id="content" name="content" rows="10" required>${freeBoard.content}</textarea>
                    </div>
                    <div class="form-group mt-3">
                        <label for="regdate">등록일</label>
                        <input type="text" class="form-control" id="regdate" name="regdate"
                               value="<javatime:format value="${freeBoard.regdate}" pattern="yyyy-MM-dd"/>" readonly required>
                    </div>
                    <div class="form-group mt-3">
                        <label for="moddate">수정일</label>
                        <input type="text" class="form-control" id="moddate" name="moddate"
                               value="<javatime:format value="${freeBoard.moddate}" pattern="yyyy-MM-dd"/>" readonly required>
                    </div>
                    <div class="form-group mt-3">
                        <label for="cnt">조회수</label>
                        <input type="text" class="form-control" id="cnt" name="cnt" value="${freeBoard.cnt}" readonly required>
                    </div>
                    <div class="form-group mt-3">
                        <label for="uploadFiles">파일첨부</label>
                        <input class="form-control" type="file" name="uploadFiles" id="uploadFiles" multiple>
                        <div id="image-preview">
                            <input type="file" id="changeFiles" name="changeFiles" style="display: none;"
                                   multiple>
                            <p style="color: red; font-size:0.9rem;">
                                파일을 변경하려면 이미지를 클릭하세요. 파일을 다운로드하려면 파일이름을 클릭하세요. 파일을 추가하려면 파일 선택 버튼을 클릭하세요.
                            </p>
                            <div id="preview" class="mt-3 text-center"
                                 data-placeholder="파일을 첨부하려면 파일선택 버튼을 누르세요.">
                                <c:forEach items="${fileList}" var="file" varStatus="status">
                                    <div class="upload-file-div">
                                        <input type="hidden" id="fileId${status.index}" value="${file.id}">
                                        <input type="hidden" id="filename${status.index}" value="${file.filename}">
                                        <!--밑에 표시된 파일을 클릭했을 때 파일 선택창이 뜨도록 input type="file" 하나 생성-->
                                        <input type="file" id="changeFile${file.id}" name="changeFile${file.id}" style="display: none;"
                                               onchange="changFile(${file.id}, event)">
                                        <c:if test="${status.last}">
                                            <input type="hidden" id="filecnt" name="filecnt" value="${status.count}">
                                        </c:if>
                                        <c:choose>
                                            <c:when test="${file.filetype eq 'image'}">
                                                <img id="img${file.id}"
                                                     src="/upload/${file.filename}"
                                                     class="upload-file"
                                                     alt="${file.fileoriginname}"
                                                     onclick="fileClick(${file.id})">
                                            </c:when>
                                            <c:otherwise>
                                                <img id="img${file.id}"
                                                     src="/static/images/defaultFileImg.png"
                                                     class="upload-file"
                                                     alt="${file.fileoriginname}"
                                                     onclick="fileClick(${file.id})">
                                            </c:otherwise>
                                        </c:choose>
                                        <input type="button" value="x"
                                               deleteFile="${file.id}"
                                               class="upload-file-delete-btn"
                                               onclick="deleteFile(event)">
                                        <p id="filename${file.id}"
                                           class="upload-file-name">
                                            ${file.fileoriginname}
                                        </p>
                                    </div>
                                </c:forEach>
                            </div>
                        </div>
                    </div>
                    <c:if test="${loginMember ne null and loginMember.id eq freeBoard.WRITER_ID}">
                        <div class="container mt-3 mb-5 w-50 text-center">
                            <button type="submit" id="btn-update" class="btn btn-outline-secondary">수정</button>
                            <button type="button" id="btn-delete" class="btn btn-outline-secondary ml-2" onclick="location.href='/board/delete.do?id=${freeBoard.id}&type=free'">삭제</button>
                        </div>
                    </c:if>
                </form>
            </div>
        </main>

        <jsp:include page="${pageContext.request.contextPath}/footer.jsp"></jsp:include>
    </div>
    <script>
        // 추가된 파일들을 담아줄 배열
        const uploadFiles = [];

        // 기존에 업로도되어 있는 파일들을 담아줄 배열
        // 게시글번호, 파일번호, 파일명을 객체 형태로 담아준다.
        const originFiles = [];

        // 변경된 파일들을 담아줄 배열
        const changeFiles = [];

        $(() => {
            $("#modify-form").on("submit", (e) => {
                $("#regdate").val(`\${\$("#regdate").val()}T00:00:00`);
                $("#moddate").val(`\${\$("#moddate").val()}T00:00:00`);
            });

            // 업로드되어 있던 파일들을 originFiles 배열에 담기
            for(let i = 0; i < $("#filecnt").val(); i++) {
                const originFileObj = {
                    board_id: $("input[name='id']").val(),
                    id: $("#fileId" + i).val(),
                    filename: $("#filename" + i).val(),
                    filestatus: "N" // 초기상태 변경없음: N, 변경되면: U, 삭제되면: D
                };

                originFiles.push(originFileObj);
            }

            $("#uploadFiles").on("change", (e) => {
                // input에 추가된 파일들 변수로 받기
                const files = e.target.files;

                // 변수로 받아온 파일들 배열로 변환
                const fileArr = Array.prototype.slice.call(files);

                for(file of fileArr) {
                    // 미리보기 메소드 호출
                    imageLoader(file);
                }
            });

            // modify-form이 서브밋될 때
            // uploadFiles 배열에 담겨있는 파일들을 input name="uploadFiles"에 담기
            // changeFiles 배열에 담겨있는 파일들을 input name="changeFiles"에 담기
            // originFiles 배열에 담겨있는 객체들을 문자열로 변환하여 input name="originFiles"에 담기
            $("#modify-form").on("submit", (e) => {
                let dataTransfer1 = new DataTransfer();
                let dataTransfer2 = new DataTransfer();

                for(i in uploadFiles) {
                    const file = uploadFiles[i];
                    dataTransfer1.items.add(file);
                }

                $("#uploadFiles")[0].files = dataTransfer1.files;

                for(i in changeFiles) {
                    const file = changeFiles[i];
                    dataTransfer2.items.add(file);
                }

                $("#changeFiles")[0].files = dataTransfer2.files;

                $("#originFiles").val(JSON.stringify(originFiles));
            });
        });

        // 업로도되어 있어서 표출되어 있는 파일들을 클릭했을 때 실행될 메소드
        const fileClick = (fileId) => {
            $("#changeFile" + fileId).click();
        }

        const changFile = (fileId, e) => {
            const files = e.target.files;

            const fileArr = Array.prototype.slice.call(files);

            changeFiles.push(fileArr[0]);

            const reader = new FileReader();

            reader.onload = (event) => {
                const img = document.getElementById("img" + fileId);
                const p = document.getElementById("filename" + fileId);

                if(fileArr[0].name.match(/(.*?)\.(jpg|jpeg|png|gif|bmp)$/)) {
                    img.src = event.target.result;
                } else {
                    img.src = "/static/images/defaultFileImg.png";
                }

                p.textContent = fileArr[0].name;
            }

            reader.readAsDataURL(fileArr[0]);

            // 기존에 originFiles 배열에 담겨있던 내용 변경
            for(let i = 0; i < originFiles.length; i++) {
                if(fileId == originFiles[i].id) {
                    originFiles[i].filestatus = "U";
                    originFiles[i].newfilename = fileArr[0].name;
                }
            }
        }

        const deleteFile = (e) => {
            // 클릭된 x 버튼 변수로 담기
            const element = e.target;

            const deleteFileId = element.getAttribute("deleteFile");

            // originFiles 배열에서 deleteFileId와 id가 같은 객체의 filestatus를 D로 변경
            for(let i = 0; i < originFiles.length; i++) {
                if(deleteFileId == originFiles[i].id) {
                    originFiles[i].filestatus = "D";
                }
            }

            // 부모 div 삭제
            const parentDiv = element.parentNode;
            $(parentDiv).remove();
        }

        // 미리보기 처리하는 메소드
        // 미리보기될 파일은 서버나 데이터베이스에 저장된 상태가 아니기 때문에
        // 파일 자체를 Base64 인코딩 방식으로 문자열로 변환해서 이미지로 호출해야 된다.
        // 이미지가 들어갈 태그 생성과 파일을 Base64 인코딩
        const imageLoader = (file) => {
            // 추가된 파일 uploadFiles 배열에 담기
            uploadFiles.push(file);

            let reader = new FileReader();

            // reader가 호출되면 실행될 이벤트 등록
            reader.onload = (e) => {
                // 이미지를 표출할 img 태그 생성
                let img = document.createElement("img");

                img.classList.add("upload-file");

                // 이미지인지 아닌지 판단
                if(file.name.toLowerCase().match(/(.*?)\.(jpg|jpeg|png|gif|svg|bmp)$/)) {
                    img.src = e.target.result;
                } else {
                    img.src = "/static/images/defaultFileImg.png";
                }

                // 미리보기 영역에 추가
                // makeDiv 메소드를 호출해서 만들어진 div 자체를 preview 영역에 추가
                $("#preview").append(makeDiv(img, file));
            }

            // 파일을 Base64인코딩된 문자열로 로드
            // 이 메소드가 실행되면서 위에서 등록한 onload 이벤트가 함께 동작한다.
            reader.readAsDataURL(file);
        }

        // 미리보기 영역에 추가될 div를 생성하는 메소드
        const makeDiv = (img, file) => {
            let div = document.createElement("div");

            div.classList.add("upload-file-div");

            // 삭제 버튼 추가
            let btn = document.createElement("input");

            btn.classList.add("upload-file-delete-btn");

            btn.setAttribute("type", "button");
            btn.setAttribute("value", "x");
            // 사용자 정의 속성 추가
            btn.setAttribute("deleteFile", file.name);

            // x 버튼에 클릭했을 때 삭제하는 기능 추가
            btn.onclick = (e) => {
                // 클릭된 버튼 변수로 받기
                const element = e.target;

                const deleteFileName = element.getAttribute("deleteFile");

                // 배열에서 파일 삭제
                for(let i = 0; i < uploadFiles.length; i++) {
                    if(deleteFileName === uploadFiles[i].name) {
                        uploadFiles.splice(i, 1);
                    }
                }
                // uploadFiles.filter(((file, index) => file.name != deleteFileName || uploadFiles.indexOf(file) != index));

                // input에서도 파일 삭제
                // input type="file"은 첨부된 파일들을 fileList 형태로 관리
                // fileList는 File 객체에 바로 담을수 없기 때문에
                // DataTransfer라는 클래스를 사용해서 변환 후에 담아줘야한다.
                let dataTransfer = new DataTransfer();

                for(i in uploadFiles) {
                    // uploadFiles 배열에 있는 File 객체를 하나씩 DataTransfer 객체에 담아준다.
                    const file = uploadFiles[i];
                    dataTransfer.items.add(file);
                }

                // input type="file"에 fileList 형태로 밀어넣기
                $("#uploadFiles")[0].files = dataTransfer.files;

                // 클릭된 btn 태그를 소유하고 있는 부모 div 태그 삭제
                const parentDiv = element.parentNode;
                $(parentDiv).remove();
            }

            // 파일 이름을 표출할 p 태그 생성
            let p = document.createElement("p");

            p.classList.add("upload-file-name");

            p.textContent = file.name;

            // div태그에 img, btn, p 태그 자식으로 추가
            div.appendChild(img);
            div.appendChild(btn);
            div.appendChild(p);

            return div;
        }










    </script>
</body>
</html>