Spring

[Spring] 파일 업로드하기

아잠만_ 2024. 8. 22. 10:32
참고한 게시글

파일 업로드 시

이미 저장된 파일은 그대로 두고 새로 추가되는 형식으로 바꾸고 싶어 찾아본 결과 시도한 방법이다

이미 저장된 파일을 따로 저장하는 변수인

let selectedFiles = [];

를 먼저 선언 후 file이 들어올 때 onchage를 통해서 파일 배열이 기존의 selectedFiles에 합치도록 했다(concat)

file추가

// 파일이 선택될 때 호출되는 함수
function concatFile(files) {
    // FileList를 배열로 변환
    let fileArr = Array.from(files);

    // 기존의 selectedFiles 배열에 새로운 파일 추가
    selectedFiles = selectedFiles.concat(fileArr);

    // 파일 목록 표시 업데이트
    updateFileList(selectedFiles);
}

그리고 또한 파일 목록을 호출해서 하나씩 삭제하는 버튼을 만들고자 했는데

여기서 사진파일은 사진도 함께 뜨도록하며 일반 파일은 설정된 파일을 띄우고자 설정했다

file 목록 ui 추가

// 파일 목록 표시를 업데이트하는 함수
function updateFileList(files) {
    const fileList = document.getElementById('file-list');
    fileList.innerHTML = ''; // 기존 파일 목록을 초기화

    // 선택된 파일을 반복하면서 미리보기 생성
    files.forEach((file) => {
        const reader = new FileReader();

        reader.onload = function(e) {
            const item = document.createElement('div');
            item.className = 'file-item';

            // 이미지 미리보기 생성
            const img = document.createElement('img');
            img.src = file.type.match("image.*") ? e.target.result : '/resources/images/noimage.jpg';
            img.className = 'file-preview';

            // 파일 이름 표시
            const fileName = document.createTextNode(file.name);

            // 삭제 버튼 생성
            const deleteButton = document.createElement('button');
            deleteButton.innerText = "X";
            deleteButton.addEventListener('click', (event) => {
                item.remove(); // 미리보기 항목 제거
                event.preventDefault();
                deleteFile(file); // 파일 삭제 처리
            });

            item.appendChild(img);
            item.appendChild(fileName);
            item.appendChild(deleteButton);
            fileList.appendChild(item);
        };

        reader.readAsDataURL(file); // 파일을 읽어 미리보기 생성
    });

//     console.log(selectedFiles); // 현재 선택된 파일 확인
}

그리고 마지막으로 x를 눌러 delete한 파일을 seletedFiles의 변수에서 삭제하는 함수도 설정하였다

파일 삭제

function deleteFile(deleteFile) {
    const inputFile = $('#uploadFile');
    const dataTransfer = new DataTransfer();
    selectedFiles = selectedFiles.filter(file => file!==deleteFile);
    selectedFiles.forEach(file => {
        dataTransfer.items.add(file);
    })
    inputFile.files = dataTransfer.files;
//     console.log(selectedFiles);
}

이제 파일 업로드를 위해 파일을 전송해야되는데

저장된 파일이 seletedFiles 변수에 있기때문에 ajax로 비동기화 처리를 시도했는데

파일을 보내는 것이기 때문에 contentType, processData을 필수로 선언 하였으며

csrf설정을 위해 beforeSend처리도 하였다

파일 업로드 비동기화 처리

$('#submit').on('click', function(){
		let custNum = $('#custNum').val();
		let empNum = $('#empNum').val();
		let carNum = $('#carNum').val();
		let amt = $('#amt').val();
		let perTme = $('#perTme').val();
		let perDet = $('#perDet').val();
		
		console.log(selectedFiles); // selectedFiles 배열 확인
		
	    // FormData 객체 생성
	    let formData = new FormData();
	    formData.append("custNum", custNum);
	    formData.append("empNum", empNum);
	    formData.append("carNum", carNum);
	    formData.append("amt", amt);
	    formData.append("perTme", perTme);
	    formData.append("perDet", perDet);

	    // 선택한 파일들을 FormData에 추가
	    selectedFiles.forEach(file => {
	        formData.append("uploadFile", file); // uploadFile 필드에 파일 추가
	    });

		$.ajax({
			type : "post",
			url : "/perSer/registPost",
			// 파일업로드시 필수!!
			contentType:false, //보내는데이터타입 false->"multipart/form-data"로 선언됩니다.
			processData:false, //폼데이터가 name=값&name=값 형식으로 자동변경되는 것을 막아줍니다.
			data : formData,
			// csrf설정 secuity설정된 경우 필수!!
			beforeSend:function(xhr){ 
				xhr.setRequestHeader("${_csrf.headerName}","${_csrf.token}"); 
			},
			success : function(res){
				console.log(res);
// 				location.href="/perSer/detail?serNum="+res;
			}
		})
	})

하지만 이렇게된다면 정보를 RequestParam으로 하나씩 받아야하기 때문에 받아야될 변수가 많을 수록 힘들다고 판단하여

더보기

파일 비동기화 처리 시 java

/**
	 * 수리내역 등록실행
	 * 요쳥 URI : /perSer/registPost
	 * 요청 파라미터 : PerSerVO
	 * 요청 방식 : post
	 */
	@ResponseBody
	@PostMapping("/registPost")
	public int registPost(
	        @RequestParam("custNum") int custNum,
	        @RequestParam("empNum") int empNum,
	        @RequestParam("carNum") String carNum,
	        @RequestParam("amt") int amt,
	        @RequestParam("perTme") int perTme,
	        @RequestParam("perDet") String perDet,
	        @RequestParam(value = "uploadFile", required = false) MultipartFile[] uploadFile) {
	    
	    // uploadFile이 null인지 확인
	    if (uploadFile == null) {
	        System.out.println("uploadFile is null"); // null 확인 로그
	    } else {
	        System.out.println("Number of files uploaded: " + uploadFile.length); // 업로드된 파일 수 확인
	    }

	    // PerSerVO 객체 생성
	    PerSerVO perSerVo = new PerSerVO();
	    perSerVo.setCustNum(custNum);
	    perSerVo.setEmpNum(empNum);
	    perSerVo.setCarNum(carNum);
	    perSerVo.setAmt(amt);
	    perSerVo.setPerTme(perTme);
	    perSerVo.setPerDet(perDet);
	    perSerVo.setUploadFile(uploadFile); // MultipartFile 배열 설정
		log.info("registPost -> perSerVO : "+perSerVo);
		
		int result = this.perSerService.registPost(perSerVo);
		return perSerVo.getSerNum();
	}

새로운 방법으로 파일을 seletedFiles에 그대로 저장하고 있지않고 input file도 변경하여 submit 할 수 있도록 하는 함수를 다시 변경했다

input 태그에 반영

//선택된 파일을 input 태그에 반영하는 함수
function updateInputFile() {
    const inputFile = document.getElementById('uploadFile');
    const dataTransfer = new DataTransfer(); // 새로운 DataTransfer 객체 생성

    // selectedFiles를 inputFile.files에 추가
    selectedFiles.forEach(file => {
        dataTransfer.items.add(file);
    });

    // inputFile의 files 속성을 업데이트
    inputFile.files = dataTransfer.files;
}

그리고 해당함수를 updateFileList함수와 deleteFile  함수 밑에 넣어  input 파일을 변경하도록 설정해서 

업로드 설정을 완료하였다

전체 소스 파일

regist.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<link type="text/css" href="/resources/ckeditor5/sample/css/sample.css" rel="stylesheet" media="screen"/>
<script type="text/javascript" src="/resources/js/jquery.min.js"></script>
<script type="text/javascript" src="/resources/ckeditor5/ckeditor.js"></script>
    <style>
        #file-list {
            display: flex;
            flex-direction: column;
            margin-top: 20px;
        }
        .file-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }
        .file-preview {
            margin-right: 10px;
        }
        img {
            width: 100px;
            border: 1px solid #ced4da;
            border-radius: .25rem;
        }
    </style>
<script>
let selectedFiles = [];
$(function(){
	ClassicEditor.create(document.querySelector('#temp'),{ckfinder:{uploadUrl:'/image/upload?${_csrf.parameterName}=${_csrf.token}'}})
	 .then(editor=>{window.editor=editor;
	
	$(".ck-blurred").keydown(function(){
		console.log("str : " + window.editor.getData());
		$("#perDet").val(window.editor.getData());
	});

	$(".ck-blurred").on("focusout",function(){
		$("#perDet").val(window.editor.getData());
	});
	
	})
	.catch(err=>{console.error(err.stack);});
	

		
	let currentPage = 1;
	$('#cust').on('click', function(){
		$('#modal-lg').modal('show');
		currentPage=1;
		let data = {"currentPage" : currentPage}
		$.ajax({
			type : "get",
			url : "/car/custList",
			dataType : "json",
			data : data,
			success : function(res){
				console.log(res.content);
				let str = '';
				$.each(res.content, function(idx,vo){
					console.log("vo", vo);
					str += "<tr class='custSel' data-num='"+vo.custNum+"' data-nm='"+vo.custNm+"'>";
					str += "<td>"+vo.custNm+"</td>";
					str += "<td>"+vo.pnum+"</td>";
					str += "<td>"+vo.addr+"</td>";
					str += "<td>"+vo.detAddr+"</td>";
					str += "<td>"+vo.pne+"</td>";
					str += "</tr>";
				});
				$('#custT').html(str);
				$('#custP').html(res.pagingArea);
			}
		})
	})
	$('#emp').on('click', function(){
		$('#modal-emp').modal('show');
		currentPage=1;
		let data = {"currentPage" : currentPage}
		$.ajax({
			type : "get",
			url : "/emp/empList",
			dataType : "json",
			data : data,
			success : function(res){
				console.log(res.content);
				let str = '';
				$.each(res.content, function(idx,vo){
					console.log("vo", vo);
					str += "<tr class='empSel' data-num='"+vo.empNum+"' data-nm='"+vo.nm+"'>";
					str += "<td>"+vo.nm+"</td>";
					str += "<td>"+vo.pnum+"</td>";
					str += "<td>"+vo.addr+"</td>";
					str += "<td>"+vo.detAddr+"</td>";
					str += "<td>"+vo.pne+"</td>";
					str += "</tr>";
				});
				$('#empT').html(str);
				$('#empP').html(res.pagingArea);
			}
		})
	})
	
	$(document).on('click', '.custSel', function(){
		let num = $(this).data("num");
		let nm = $(this).data("nm");
		$('#custNum').val(num);
		let name = nm+"("+num+")";
		$('#custInfo').val(name);
		$('#modal-lg').modal('hide');
		
		$.ajax({
			type : "get",
			url : "/cust/carList",
			dataType : "json",
			data : {"custNum" : num},
			success : function(res){
				console.log(res);
				let str = '';
				$.each(res, function(idx,vo){
					console.log("vo", vo);
					str += "<option value='"+vo.carNum+"'>"+vo.carNum+" ("+vo.mfr+")"+"</option>";
				});
				$('#carNum').html(str);
			}
		})
	})
	
	$(document).on('click', '.empSel', function(){
		currentPage=1;
		let num = $(this).data("num");
		let nm = $(this).data("nm");
		$('#empNum').val(num);
		let name = nm+"("+num+")";
		$('#empInfo').val(name);
		
		$('#modal-emp').modal('hide');
	})
})

// 파일이 선택될 때 호출되는 함수
function test(files) {
    // FileList를 배열로 변환
    let fileArr = Array.from(files);

    // 기존의 selectedFiles 배열에 새로운 파일 추가
    selectedFiles = selectedFiles.concat(fileArr);

    // 파일 목록 표시 업데이트
    updateFileList(selectedFiles);
}

// 파일 목록 표시를 업데이트하는 함수
function updateFileList(files) {
    const fileList = document.getElementById('file-list');
    fileList.innerHTML = ''; // 기존 파일 목록을 초기화

    // 선택된 파일을 반복하면서 미리보기 생성
    files.forEach((file) => {
        const reader = new FileReader();

        reader.onload = function(e) {
            const item = document.createElement('div');
            item.className = 'file-item';

            // 이미지 미리보기 생성
            const img = document.createElement('img');
            img.src = file.type.match("image.*") ? e.target.result : '/resources/images/noimage.jpg';
            img.className = 'file-preview';

            // 파일 이름 표시
            const fileName = document.createTextNode(file.name);

            // 삭제 버튼 생성
            const deleteButton = document.createElement('button');
            deleteButton.innerText = "X";
            deleteButton.addEventListener('click', (event) => {
                item.remove(); // 미리보기 항목 제거
                event.preventDefault();
                deleteFile(file); // 파일 삭제 처리
            });

            item.appendChild(img);
            item.appendChild(fileName);
            item.appendChild(deleteButton);
            fileList.appendChild(item);
        };

        reader.readAsDataURL(file); // 파일을 읽어 미리보기 생성
    });
	 // 선택된 파일 목록을 input 파일 태그에 반영
	    updateInputFile();

//     console.log(selectedFiles); // 현재 선택된 파일 확인
}

//선택된 파일을 input 태그에 반영하는 함수
function updateInputFile() {
    const inputFile = document.getElementById('uploadFile');
    const dataTransfer = new DataTransfer(); // 새로운 DataTransfer 객체 생성

    // selectedFiles를 inputFile.files에 추가
    selectedFiles.forEach(file => {
        dataTransfer.items.add(file);
    });

    // inputFile의 files 속성을 업데이트
    inputFile.files = dataTransfer.files;
}

function deleteFile(deleteFile) {
    const inputFile = $('#uploadFile');
    const dataTransfer = new DataTransfer();
    selectedFiles = selectedFiles.filter(file => file!==deleteFile);
    selectedFiles.forEach(file => {
        dataTransfer.items.add(file);
    })
    inputFile.files = dataTransfer.files;
//     console.log(selectedFiles);
 // 선택된 파일 목록을 input 파일 태그에 반영
    updateInputFile();
}
</script>
<div class="card card-success">
	<div class="card-header">
		<h2>수리 서비스 등록</h2>
	</div>

	<form action="/perSer/registPost?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
		<div class="card-body">
			<div class="row">
				<div class="col-sm-12">
					<!-- text input -->
					<div class="form-group"  data-target="#modal-emp" id="emp">
						<label for="empNum">사원</label> 
						<input hidden required type="text" class="form-control" placeholder="사원번호" name="empNum" id="empNum">
						<input required type="text" class="form-control" placeholder="사원정보" name="empInfo" id="empInfo">
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group" data-target="#modal-lg" id="cust">
						<label for="custNum">고객</label>
						<input hidden required="required" type="text" class="form-control" placeholder="고객" name="custNum" id="custNum">
						<input required="required" type="text" class="form-control" placeholder="고객" name="custInfo" id="custInfo">
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label for="carNum">자동차</label>
						<select name="carNum" id="carNum" class="form-control">
							<option value="고객을 선택해주세요"></option>
						</select>
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label for="amt">수리비용</label>
						<input required="required" type="number" class="form-control" placeholder="수리비용" name="amt" id="amt">
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label for="perTme">수리시간</label>
						<input required="required" type="text" class="form-control" placeholder="수리시간" name="perTme" id="perTme">
					</div>
				</div>
				<div class="col-sm-12">
					<div class="form-group">
						<label for="perDet">수리 내역</label>
						<div id="temp"></div>
						<textarea hidden rows="3" id="perDet" class="form-control" name="perDet" cols=""></textarea>
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<label for="uploadFile">첨부 파일</label>
						<div class="custom-file">
							<input onchange="test(this.files)" id="uploadFile" name="uploadFile" type="file" multiple="multiple" class="custom-file-input">
							<label class="custom-file-label" id="fileLabel"
								for="uploadFile">파일을 선택해주세요</label>
						</div>
					</div>
				</div>
				<div class="col-sm-6">
					<div class="form-group">
						<div id="file-list"></div>
					</div>
				</div>
			</div>
		</div>
		<!-- /.card-body -->
		<div class="card-footer row" style="justify-content: space-between;">
			<sec:csrfInput/>
			<div style="float: left;">
				<a href="/perSer/list" class="btn btn-info">목록</a>
			</div>
			<div style="float: right;">
				<button type="submit" class="btn btn-warning">등록</button>
			</div>
		</div>
	</form>
</div>
<div class="modal fade" id="modal-lg">
        <div class="modal-dialog modal-lg">
          <div class="modal-content">
            <div class="modal-header">
              <h4 class="modal-title">고객 정보</h4>
              <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <table class="table table-hover text-nowrap">
				<thead>
					<tr>
						<th>고객명</th>
						<th>우편번호</th>
						<th>주소</th>
						<th>상세주소</th>
						<th>연락처</th>
					</tr>
				</thead>
				<tbody id="custT">

				</tbody>
			</table>
            </div>
            <div class="modal-footer justify-content-between">
              <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
              <button type="button" class="btn btn-primary">Save changes</button>
            </div>
            <div class="card-footer clearfix" style="width: 100%" id="custP"></div>
          </div>
          <!-- /.modal-content -->
        </div>
        <!-- /.modal-dialog -->
      </div>
      <!-- /.modal -->
      
      
<div class="modal fade" id="modal-emp">
        <div class="modal-dialog modal-emp">
          <div class="modal-content">
            <div class="modal-header">
              <h4 class="modal-title">사원 정보</h4>
              <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <table class="table table-hover text-nowrap">
				<thead>
					<tr>
						<th>사원명</th>
						<th>우편번호</th>
						<th>주소</th>
						<th>상세주소</th>
						<th>연락처</th>
					</tr>
				</thead>
				<tbody id="empT">

				</tbody>
			</table>
            </div>
            <div class="modal-footer justify-content-between">
              <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
              <button type="button" class="btn btn-primary">Save changes</button>
            </div>
            <div class="card-footer clearfix" style="width: 100%" id="empP"></div>
          </div>
          <!-- /.modal-content -->
        </div>
        <!-- /.modal-dialog -->
      </div>
      <!-- /.modal -->

PerSerController.java

package kr.or.ddit.controller;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import kr.or.ddit.service.PerSerService;
import kr.or.ddit.util.ArticlePage;
import kr.or.ddit.util.UploadController;
import kr.or.ddit.vo.PerSerVO;
import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping("/perSer")
@Slf4j
public class PerSerController {
	
	@Autowired
	PerSerService perSerService;
	
	/**
	 * 수리내역 등록폼
	 * 요쳥 URI : /perSer/regist
	 * 요청 파라미터 : 
	 * 요청 방식 : get
	 */
	@GetMapping("/regist")
	public String perSerRegist() {
		return "perSer/regist";
	}
	
	/**
	 * 수리내역 등록실행
	 * 요쳥 URI : /perSer/registPost
	 * 요청 파라미터 : PerSerVO
	 * 요청 방식 : post
	 */
	@PostMapping("/registPost")
	public String registPost(@ModelAttribute PerSerVO perSerVo) {

		log.info("registPost -> perSerVO : "+perSerVo);
		
		int result = this.perSerService.registPost(perSerVo);
		return "redirect: /perSer/detail?serNum="+perSerVo.getSerNum();
	}

}