21.03.05 Untact프로젝트(게시물 수정 페이지에서 기존에 업로드된 파일 노출, 수정, 삭제, 용량 표시 등 )
2021. 3. 5. 20:37ㆍJAVA/Spring & Vue APP 프로젝트(백엔드)
#NOTE
DAO.java 수준에서는 동일 함수명이 존재해도 문제는 없음
But, DAO.xml 수준에서는 동일 함수명이 존재하면 문제 발생
#주요 소스코드
<modify.jsp>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="com.sbs.untact.util.Util" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ include file="../part/mainLayoutHead.jspf"%>
<c:set var="fileInputMaxCount" value="10" />
<script>
ArticleModify__fileInputMaxCount = parseInt("${fileInputMaxCount}");
const articleId = parseInt("${article.id}");
</script>
<script>
ArticleModify__submited = false;
function ArticleModify__checkAndSubmit(form) {
if ( ArticleModify__submited ) {
alert('처리중입니다.');
return;
}
form.title.value = form.title.value.trim();
if ( form.title.value.length == 0 ) {
alert('제목을 입력해주세요.');
form.title.focus();
return false;
}
form.body.value = form.body.value.trim();
if ( form.body.value.length == 0 ) {
alert('내용을 입력해주세요.');
form.body.focus();
return false;
}
var maxSizeMb = 50;
var maxSize = maxSizeMb * 1024 * 1024;
for ( let inputNo = 1; inputNo <= ArticleModify__fileInputMaxCount; inputNo++ ) {
const input = form["file__article__" + articleId + "__common__attachment__" + inputNo];
if (input.value) {
if (input.files[0].size > maxSize) {
alert(maxSizeMb + "MB 이하의 파일을 업로드 해주세요.");
input.focus();
return;
}
}
}
const startUploadFiles = function(onSuccess) {
var needToUpload = false;
for ( let inputNo = 1; inputNo <= ArticleModify__fileInputMaxCount; inputNo++ ) {
const input = form["file__article__" + articleId + "__common__attachment__" + inputNo];
if ( input.value.length > 0 ) {
needToUpload = true;
break;
}
}
if ( needToUpload == false ) {
for ( let inputNo = 1; inputNo <= ArticleModify__fileInputMaxCount; inputNo++ ) {
const input = form["deleteFile__article__" + articleId + "__common__attachment__" + inputNo];
//만약, input이 있고 input이 체크되어있으면(input.checked) needToUpload = true;
if ( input && input.checked ) {
needToUpload = true;
break;
}
}
}
if (needToUpload == false) {
onSuccess();
return;
}
var fileUploadFormData = new FormData(form);
$.ajax({
url : '/common/genFile/doUpload',
data : fileUploadFormData,
processData : false,
contentType : false,
dataType : "json",
type : 'POST',
success : onSuccess
});
}
const startSubmitForm = function(data) {
if (data && data.body && data.body.genFileIdsStr) {
form.genFileIdsStr.value = data.body.genFileIdsStr;
}
for ( let inputNo = 1; inputNo <= ArticleModify__fileInputMaxCount; inputNo++ ) {
const input = form["file__article__" + articleId + "__common__attachment__" + inputNo];
input.value = '';
}
for ( let inputNo = 1; inputNo <= ArticleModify__fileInputMaxCount; inputNo++ ) {
const input = form["deleteFile__article__" + articleId + "__common__attachment__" + inputNo];
if ( input ) {
input.checked = false;
}
}
form.submit();
};
ArticleModify__submited = true;
startUploadFiles(startSubmitForm);
}
</script>
<section class="section-1">
<div class="bg-white shadow-md rounded container mx-auto p-8 mt-8">
<form onsubmit="ArticleModify__checkAndSubmit(this); return false;" action="doModify" method="POST" enctype="multipart/form-data">
<input type="hidden" name="genFileIdsStr" value="" />
<input type="hidden" name="id" value="${article.id}" />
<div class="form-row flex flex-col lg:flex-row">
<div class="lg:flex lg:items-center lg:w-28">
<span>제목</span>
</div>
<div class="lg:flex-grow">
<input value="${article.title}" type="text" name="title" autofocus="autofocus"
class="form-row-input w-full rounded-sm" placeholder="제목을 입력해주세요." />
</div>
</div>
<div class="form-row flex flex-col lg:flex-row">
<div class="lg:flex lg:items-center lg:w-28">
<span>내용</span>
</div>
<div class="lg:flex-grow">
<textarea name="body" class="form-row-input w-full rounded-sm" placeholder="내용을 입력해주세요.">${article.body}</textarea>
</div>
</div>
<c:forEach begin="1" end="${fileInputMaxCount}" var="inputNo">
<c:set var="fileNo" value="${String.valueOf(inputNo)}" />
<c:set var="file" value="${article.extra.file__common__attachment[fileNo]}" />
<div class="form-row flex flex-col lg:flex-row">
<div class="lg:flex lg:items-center lg:w-28">
<span>첨부파일 ${inputNo}</span>
</div>
<div class="lg:flex-grow input-file-wrap">
<input type="file" name="file__article__${article.id}__common__attachment__${inputNo}"
class="form-row-input w-full rounded-sm" />
<c:if test="${file != null}">
<div>
<a href="${file.forPrintUrl}" target="_blank" class="text-blue-500 hover:underline">
${file.fileName}
</a>
( ${Util.numberFormat(file.fileSize)} Byte )
</div>
<div>
<label>
<!-- 이 체크박스에 체크하면 가까운 .input-file-wrap의 자식 input들 중 type이 file인 것의 value값을 없앤다 -->
<input onclick="$(this).closest('.input-file-wrap').find(' > input[type=file]').val('')" type="checkbox" name="deleteFile__article__${article.id}__common__attachment__${fileNo}" value="Y" />
<span>삭제</span>
</label>
</div>
<c:if test="${file.fileExtTypeCode == 'img'}">
<div class="img-box img-box-auto">
<a class="inline-block" href="${file.forPrintUrl}" target="_blank" title="자세히 보기">
<img class="max-w-sm" src="${file.forPrintUrl}">
</a>
</div>
</c:if>
</c:if>
</div>
</div>
</c:forEach>
<div class="form-row flex flex-col lg:flex-row">
<div class="lg:flex lg:items-center lg:w-28">
<span>수정</span>
</div>
<div class="lg:flex-grow">
<div class="btns">
<input type="submit" class="btn-primary bg-blue-500 hover:bg-blue-dark text-white font-bold py-2 px-4 rounded" value="수정">
<input onclick="history.back();" type="button" class="btn-info bg-red-500 hover:bg-red-dark text-white font-bold py-2 px-4 rounded" value="취소">
</div>
</div>
</div>
</form>
</div>
</section>
<%@ include file="../part/mainLayoutFoot.jspf"%>
<ArticleService.java>
public ResultData addArticle(Map<String, Object> param) {
articleDao.addArticle(param);
int id = Util.getAsInt(param.get("id"), 0);
changeInputFileRelIds(param, id);
return new ResultData("S-1", "성공하였습니다.", "id", id);
}
public ResultData deleteArticle(int id) {
articleDao.deleteArticle(id);
//게시물에 달린 첨부파일도 같이 삭제
//1. DB에서 삭제
//2. 저장소에서 삭제
genFileService.deleteGenFiles("article", id);
return new ResultData("S-1", "삭제하였습니다.", "id", id);
}
public ResultData modifyArticle(Map<String, Object> param) {
articleDao.modifyArticle(param);
int id = Util.getAsInt(param.get("id"), 0);
return new ResultData("S-1", "게시물을 수정하였습니다.", "id", id);
}
private void changeInputFileRelIds(Map<String, Object> param, int id) {
String genFileIdsStr = Util.ifEmpty((String)param.get("genFileIdsStr"), null);
if ( genFileIdsStr != null ) {
List<Integer> genFileIds = Util.getListDividedBy(genFileIdsStr, ",");
// 파일이 먼저 생성된 후에, 관련 데이터가 생성되는 경우에는, file의 relId가 일단 0으로 저장된다.
// 순서: 파일업로드 => 글저장
// 즉, ajax로 파일업로드는 하였지만, 파일업로드가 글작성보다 먼저 진행됐기 때문에 업로드된 파일들에는 relId가 일단 0으로 저장된 상태이다.
// 글저장은 아직 진행이 안되었기 때문에 신규 글의 ID를 지금부터 가져와서 파일의 relId로 업데이트해주어야 한다
// 따라서, 이것을 뒤늦게라도 다음 로직을 통해 고처야 한다.
for (int genFileId : genFileIds) {
genFileService.changeRelId(genFileId, id);
}
}
}
<GenFileService.java>
@Service
public class GenFileService {
//파일이 저장될 폴더 경로를 가져오는 명령어
@Value("${custom.genFileDirPath}")
private String genFileDirPath;
@Autowired
private GenFileDao genFileDao;
..............................(생략)
public ResultData saveFiles(Map<String, Object> param, MultipartRequest multipartRequest) {
//업로드 시작
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
Map<String, ResultData> filesResultData = new HashMap<>();
List<Integer> genFileIds = new ArrayList<>();
//fileMap.keySet() : file__article__0__common__attachment__1
for (String fileInputName : fileMap.keySet()) {
//fileInputName : file__article__0__common__attachment__1
MultipartFile multipartFile = fileMap.get(fileInputName);
if (multipartFile.isEmpty() == false) {
ResultData fileResultData = save(multipartFile);
int genFileId = (int) fileResultData.getBody().get("id");
genFileIds.add(genFileId);
filesResultData.put(fileInputName, fileResultData);
}
}
//genFileIds 리스트를 ","를 기준으로 문장형으로 조인(결합)시켜주는 구아바
//ex) [1,2,3,4,5] => 1,2,3,4,5
String genFileIdsStr = Joiner.on(",").join(genFileIds);
/* 삭제 시작 */
int deleteCount = 0;
for (String inputName : param.keySet()) {
String[] inputNameBits = inputName.split("__");
if (inputNameBits[0].equals("deleteFile")) {
String relTypeCode = inputNameBits[1];
int relId = Integer.parseInt(inputNameBits[2]);
String typeCode = inputNameBits[3];
String type2Code = inputNameBits[4];
int fileNo = Integer.parseInt(inputNameBits[5]);
GenFile oldGenFile = getGenFile(relTypeCode, relId, typeCode, type2Code, fileNo);
if (oldGenFile != null) {
deleteGenFile(oldGenFile);
deleteCount++;
}
}
}
return new ResultData("S-1", "파일을 업로드하였습니다.", "filesResultData", filesResultData, "genFileIdsStr",
genFileIdsStr, "deleteCount", deleteCount);
}
public void deleteGenFiles(String relTypeCode, int relId) {
//게시물에 달려있는 genFile 리스트 불러오기
List<GenFile> genFiles = genFileDao.getGenFiles(relTypeCode, relId);
for ( GenFile genFile : genFiles ) {
deleteGenFile(genFile);
}
}
private void deleteGenFile(GenFile genFile) {
//1. genFile의 저장소 경로를 찾고 저장소에서 삭제(유틸 활용)
String filePath = genFile.getFilePath(genFileDirPath);
Util.delteFile(filePath);
//2. genFile정보를 DB상에서 삭제
genFileDao.deleteFile(genFile.getId());
}
public List<GenFile> getGenFiles(String relTypeCode, int relId, String typeCode, String type2Code) {
return genFileDao.getGenFiles(relTypeCode, relId, typeCode, type2Code);
}
}