본문 바로가기
개발

폼 데이터와 함께 파일 업로드 (다중 파일 선택창 & 다중 파일 선택)

by ^..^v 2023. 4. 12.
728x90
반응형

하나의 파일 선택창에서 여러 개의 파일을 선택해서 업로드하는 기능을 확장해서 여러 개의 파일 선택창에서 여러 개의 파일을 선택해서 업로드하는 기능을 구현합니다. 

 

단일 파일 선택창에서 다중 선택한 파일 업로드 기능은 아래 내용을 참고하세요. 

https://myanjini.tistory.com/entry/%ED%8F%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%99%80-%ED%95%A8%EA%BB%98-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C

 

폼 데이터와 함께 파일 업로드

사용자 아이디, 이름, 이메일과 첨부파일을 입력받아서 서버로 전달하면, 서버는 지정된 경로에 첨부파일을 저장하고, 전달받은 값과 첨부파일 정보를 이름 : 값 형식으로 반환하는 예제입니다.

myanjini.tistory.com

 

프론트엔드 (리액트)

화면 구성

파일 선택창을 추가합니다. 첫번째 파일 선택창은 하나의 파일만 선택할 수 있도록 multiple 속성을 제거하고, 나머지 파일 선택창은 여러 개의 파일을 선택할 수 있도록 multiple 속성을 추가합니다. 

  return (
    <>
      <div>
        ID: <input type="text" value={userId} onChange={handlerChangeUserId} />
      </div>
      <div>
        NAME: <input type="text" value={userName} onChange={handlerChangeUserName} />
      </div>
      <div>
        EMAIL: <input type="text" value={userEmail} onChange={handlerChangeUserEmail} />
      </div>
      <div>
        IMAGE1: <input type="file" name="img1" ref={inputFiles1} onChange={handleChangeFile} accept="image/*" /> * 1개만 업로드 가능
        <br/>
        IMAGE2: <input type="file" name="img2" ref={inputFiles2} onChange={handleChangeFile} multiple accept="image/*" />
        <br/>
        IMAGE3: <input type="file" name="img3" ref={inputFiles3} onChange={handleChangeFile} multiple accept="image/*" />
      </div>
      <div>
        <button type="button" onClick={handlerUploadDataWithFile}>업로드</button>
      </div>
    </>
  );

 

업로드할 파일 데이터를 저장할 상태 변수와 이벤트 핸들러

각 파일 선택창에서 선택한 파일을 저장할 상태 변수를 정의합니다. 상태 변수는 배열 형식으로 파일 선택창의 이름을 키(key)로 하고 선택된 파일 목록을 값(value)으로 하는 객체를 파일 선택창의 개수 만큼 포함하고 있습니다.

  // 업로드할 파일 데이터를 저장할 상태 변수와 이벤트 핸들러
  const [imageFiles, setImageFiles] = useState([]);

  const handleChangeFile = e => {
    const name = e.target.name;
    const files = e.target.files;
    
    if (files.length > MAX_FILE_COUNT) {
      invalidFile("이미지는 최대 3개 까지 업로드가 가능합니다.");
      return;
    } 
    for (let i = 0; i < files.length; i++) {
      if (!files[i].type.match("image/.*")) {
        invalidFile("이미지 파일만 업로드 가능합니다.");
        return;
      } else if (files[i].size > MAX_FILE_SIZE) {
        invalidFile("이미지 크기는 1MB를 초과할 수 없습니다.");
        return;
      } 
    }

    const unchangedImageFiles = imageFiles.filter(fio => fio.name !== name)
    setImageFiles([...unchangedImageFiles, { name, files }]);
  };

 

서버로 전달할 폼 데이터 설정

상태 변수에 저장된 파일 데이터를 파일 선택창의 이름과 해당 파일 선택창에서 선택한 파일 목록으로 구분해서 폼 데이터 객체에 추가해 줍니다.

  // 서버로 전달할 폼 데이터(입력창 내용과 첨부 파일)를 설정
  const formData = new FormData();
  formData.append(
    'data',
    new Blob([JSON.stringify(datas)], { type: 'application/json' })
  );
  Object.values(imageFiles).forEach(
    fio => Object.values(fio.files).forEach(f => formData.append(fio.name, f)));

 

백엔드 (스프링부트)

파일 저장 기능 분리

하나의 파일 선택창에서 선택한 파일들을 저장하고 파일 정보를 반환하는 메소드를 추가합니다.

	private final String UPLOAD_DIR = "C:/temp/upload/";
	
	private List<Map<String, String>> saveFiles(MultipartFile[] files) {
		List<Map<String, String>> resultList = new ArrayList<>();
		
		if (files != null) {
			for (MultipartFile mf : files) {
				String originFileName = mf.getOriginalFilename();
				String savedFileName = UUID.randomUUID().toString();
				
				try {
					File f = new File(UPLOAD_DIR + savedFileName);
					mf.transferTo(f);
					
					Map<String, String> result = new HashMap<>();
					result.put("originalFileName", originFileName);
					result.put("savedFileName", savedFileName);
					resultList.add(result);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}		

		return resultList;
	}

 

컨트롤러 메서드

여러 파일 선택창의 값을 받을 수 있도록 @RequestPart 어노테이션이 붙은 파라미터를 파일 선택창의 개수 만큼 추가하고, saveFiles 메서드로 전달합니다. 

	@PostMapping("/upload")
	public ResponseEntity<String> updateMember(
			@RequestPart(value = "img1", required = false) MultipartFile[] files1,
			@RequestPart(value = "img2", required = false) MultipartFile[] files2,
			@RequestPart(value = "img3", required = false) MultipartFile[] files3,
			@RequestPart(value = "data", required = false) UserDto data) throws Exception {
		

		String uploadedDatas = "";
		uploadedDatas += "userId: " + data.getUserId() + "\n";
		uploadedDatas += "userName: " + data.getUserName() + "\n";
		uploadedDatas += "userEmail: " + data.getUserEmail() + "\n";
		
		
		List<Map<String, String>> resultList = saveFiles(files1);
		uploadedDatas += "\nimg1 ************\n";
		for(Map<String, String> result : resultList) {
			uploadedDatas += "originalFileName: " + result.get("originalFileName") + "\n";
			uploadedDatas += "savedFileName: " + result.get("savedFileName") + "\n";
		}
		resultList = saveFiles(files2);
		uploadedDatas += "\nimg2 ************\n";
		for(Map<String, String> result : resultList) {
			uploadedDatas += "originalFileName: " + result.get("originalFileName") + "\n";
			uploadedDatas += "savedFileName: " + result.get("savedFileName") + "\n";
		}
		resultList = saveFiles(files3);
		uploadedDatas += "\nimg3 ************\n";
		for(Map<String, String> result : resultList) {
			uploadedDatas += "originalFileName: " + result.get("originalFileName") + "\n";
			uploadedDatas += "savedFileName: " + result.get("savedFileName") + "\n";
		}
		
		return ResponseEntity.ok(uploadedDatas);
	}

 

전체 코드

나머지 코드는 "단일 파일 선택창에서 다중 선택한 파일 업로드 기능"과 동일하므로, 아래 문서를 참고하세요. 

https://myanjini.tistory.com/entry/%ED%8F%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%99%80-%ED%95%A8%EA%BB%98-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C

 

폼 데이터와 함께 파일 업로드

사용자 아이디, 이름, 이메일과 첨부파일을 입력받아서 서버로 전달하면, 서버는 지정된 경로에 첨부파일을 저장하고, 전달받은 값과 첨부파일 정보를 이름 : 값 형식으로 반환하는 예제입니다.

myanjini.tistory.com

리액트

import { useRef, useState } from 'react';
import axios from 'axios';


function App() {
  // FORM DATA를 저장할 상태 변수 
  // - INPUT, TEXTAREA 등에서 입력한 내용을 저장
  // - 최종 상태의 값을 서버로 전달
  // - 서버에서는 DTO 객체로 받아서 처리
  const [userId, setUserId] = useState('');
  const [userName, setUserName] = useState('');
  const [userEmail, setUserEmail] = useState('');

  const handlerChangeUserId = e => setUserId(e.target.value);
  const handlerChangeUserName = e => setUserName(e.target.value);
  const handlerChangeUserEmail = e => setUserEmail(e.target.value);
  
  // 파일 선택창의 값을 직접 제어하기 위해 ref 객체 변수를 정의 
  const inputFiles1 = useRef();
  const inputFiles2 = useRef();
  const inputFiles3 = useRef();

  // 제한할 파일의 크기와 개수를 정의
  const MAX_FILE_SIZE = 1 * 1024 * 1024; //1MB
  const MAX_FILE_COUNT = 3;

  // 파일의 종류, 크기, 개수 제한을 만족하지 않는 경우, 메시지를 보여주고 파일 입력창의 값을 초기화하는 함수
  const invalidFile = msg => {
    alert(msg);
    inputFiles1.current.value = '';
    inputFiles2.current.value = '';
    inputFiles3.current.value = '';
    setImageFiles([]);
  };

  // 업로드할 파일 데이터를 저장할 상태 변수와 이벤트 핸들러
  const [imageFiles, setImageFiles] = useState([]);

  const handleChangeFile = e => {
    const name = e.target.name;
    const files = e.target.files;
    
    if (files.length > MAX_FILE_COUNT) {
      invalidFile("이미지는 최대 3개 까지 업로드가 가능합니다.");
      return;
    } 
    for (let i = 0; i < files.length; i++) {
      if (!files[i].type.match("image/.*")) {
        invalidFile("이미지 파일만 업로드 가능합니다.");
        return;
      } else if (files[i].size > MAX_FILE_SIZE) {
        invalidFile("이미지 크기는 1MB를 초과할 수 없습니다.");
        return;
      } 
    }

    const unchangedImageFiles = imageFiles.filter(fio => fio.name !== name)
    setImageFiles([...unchangedImageFiles, { name, files }]);
  };

  // 서버로 전달할 입력창 내용을 객체로 정의 (단축 속성명)
  let datas = {
    userId, 
    userName, 
    userEmail
  };

  // 서버로 전달할 폼 데이터(입력창 내용과 첨부 파일)를 설정
  const formData = new FormData();
  formData.append(
    'data',
    new Blob([JSON.stringify(datas)], { type: 'application/json' })
  );
  Object.values(imageFiles).forEach(
    fio => Object.values(fio.files).forEach(f => formData.append(fio.name, f)));
  
  // 설정한 폼 데이터를 multipart/form-data 형식으로 서버로 전달
  const handlerUploadDataWithFile = () => {
    axios({
      method: 'POST',
      url: `http://localhost:8080/upload`,
      headers: { 'Content-Type': 'multipart/form-data;' }, 
      data: formData      
    })
    .then(response => {
      response.data.split('\n').forEach(d => console.log(d));
      alert(`${response.data}\n정상적으로 업로드했습니다.`);
    })
    .catch(error => {
      console.log(error);
      alert(`업로드 중 오류가 발생했습니다.`);
    });
  };

  return (
    <>
      <div>
        ID: <input type="text" value={userId} onChange={handlerChangeUserId} />
      </div>
      <div>
        NAME: <input type="text" value={userName} onChange={handlerChangeUserName} />
      </div>
      <div>
        EMAIL: <input type="text" value={userEmail} onChange={handlerChangeUserEmail} />
      </div>
      <div>
        IMAGE1: <input type="file" name="img1" ref={inputFiles1} onChange={handleChangeFile} accept="image/*" /> * 1개만 업로드 가능
        <br/>
        IMAGE2: <input type="file" name="img2" ref={inputFiles2} onChange={handleChangeFile} multiple accept="image/*" />
        <br/>
        IMAGE3: <input type="file" name="img3" ref={inputFiles3} onChange={handleChangeFile} multiple accept="image/*" />
      </div>
      <div>
        <button type="button" onClick={handlerUploadDataWithFile}>업로드</button>
      </div>
    </>
  );
}

export default App;

 

스프링부트

package board.controller;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import board.dto.UserDto;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class RestBoardApiController {

	private final String UPLOAD_DIR = "C:/temp/upload/";
	
	private List<Map<String, String>> saveFiles(MultipartFile[] files) {
		List<Map<String, String>> resultList = new ArrayList<>();
		
		if (files != null) {
			for (MultipartFile mf : files) {
				String originFileName = mf.getOriginalFilename();
				String savedFileName = UUID.randomUUID().toString();
				
				try {
					File f = new File(UPLOAD_DIR + savedFileName);
					mf.transferTo(f);
					
					Map<String, String> result = new HashMap<>();
					result.put("originalFileName", originFileName);
					result.put("savedFileName", savedFileName);
					resultList.add(result);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}		

		return resultList;
	}
	
	
	@PostMapping("/upload")
	public ResponseEntity<String> updateMember(
			@RequestPart(value = "img1", required = false) MultipartFile[] files1,
			@RequestPart(value = "img2", required = false) MultipartFile[] files2,
			@RequestPart(value = "img3", required = false) MultipartFile[] files3,
			@RequestPart(value = "data", required = false) UserDto data) throws Exception {
		

		String uploadedDatas = "";
		uploadedDatas += "userId: " + data.getUserId() + "\n";
		uploadedDatas += "userName: " + data.getUserName() + "\n";
		uploadedDatas += "userEmail: " + data.getUserEmail() + "\n";
		
		
		List<Map<String, String>> resultList = saveFiles(files1);
		uploadedDatas += "\nimg1 ************\n";
		for(Map<String, String> result : resultList) {
			uploadedDatas += "originalFileName: " + result.get("originalFileName") + "\n";
			uploadedDatas += "savedFileName: " + result.get("savedFileName") + "\n";
		}
		resultList = saveFiles(files2);
		uploadedDatas += "\nimg2 ************\n";
		for(Map<String, String> result : resultList) {
			uploadedDatas += "originalFileName: " + result.get("originalFileName") + "\n";
			uploadedDatas += "savedFileName: " + result.get("savedFileName") + "\n";
		}
		resultList = saveFiles(files3);
		uploadedDatas += "\nimg3 ************\n";
		for(Map<String, String> result : resultList) {
			uploadedDatas += "originalFileName: " + result.get("originalFileName") + "\n";
			uploadedDatas += "savedFileName: " + result.get("savedFileName") + "\n";
		}
		
		return ResponseEntity.ok(uploadedDatas);
	}
}

 

실행결과

728x90
반응형

댓글