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