프로젝트

[Spring boot + React] 게시글 작성하기(다중 파일 업로드 포함)

째로스 2023. 8. 9. 13:44

목표

리액트로 작성된 프론트단에서 게시글을 작성한 뒤 등록 버튼을 누르면

작성한 데이터들을 서버인 스프링으로 전송한다.

 

기본설정

<!-- 파일 업로드에 필요한 라이브러리 -->
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

pom.xml에 추가한다.

 

React

1) 입력 전

2) 입력 후

 

위처럼 게시판 종류, 제목, 내용, 파일들을 입력하고 등록버튼을 통해 스프링에 데이터를 전송할 것이다.

 

React(프론트) 전체 코드

import FloatingLabel from 'react-bootstrap/FloatingLabel';
import Form from 'react-bootstrap/Form';
import React, {useState} from 'react';
import axios from 'axios';

function PostCard() {
    const [files, setFiles] = useState([]);	//파일 상태 저장을 위한 state

    // 이미지 파일 업로드에서 다이얼로그를 통해 특정 파일들을 선택한 경우
    const handleChangeFile = (event) => {
        if(event.target.files.length!=0){  // 파일 업로드시 이미지를 선택하지 않았을 경우 기존의 올리려한 이미지들이 유지됨
            setFiles(Array.from(event.target.files || []));
        }
    }

    // 선택한 이미지 파일들 중에서 특정 파일을 삭제하고 싶을 때
    const handleDelete = (index) =>{
        setFiles([...files.slice(0,index), ... files.slice(index+1)])
    };


    function Send(){
      const fd = new FormData(); // 전송할 데이터들을 저장해둘 폼
      Object.values(files).forEach((file) => fd.append("file", file)); // 입력한 파일들 추가
      fd.append("category",document.getElementById('category').value); // 입력한 게시판 종류 추가
      fd.append("subject",document.getElementById('subject').value);   // 입력한 제목 추가
      fd.append("content",document.getElementById('content').value);   // 입려한 내용 추가
     
      //axios를 통해 http://localhost:8080/user/AxiosFileTest로 fd 전송
      axios.post('http://localhost:8080/user/AxiosFileTest', fd, {
        headers: {
          "Content-Type": `multipart/form-data; `  // 파일 데이터 전송을 위해서 명시필요
        },
      })
      .then((response) => {
      })
      .catch((error) => {
        // 예외 처리
      })
    }

    return (
        <>
        <h1> 글쓰기 </h1>
          // 게시판 선택을 위한 셀렉트폼
          <Form.Select id="category" aria-label="Default select example">
            <option>---게시판 선택---</option>
            <option>---자유게시판1---</option>
            <option>---자유게시판2---</option>
          </Form.Select>
          
          // 제목 입력을위한 textarea
          <FloatingLabel controlId="subject" label="제목" className="mb-3">
            <Form.Control as="textarea" placeholder="Leave a comment here" />
          </FloatingLabel>
          
          // 내용 입력을 위한 textarea
          <FloatingLabel controlId="content" label="내용">
            <Form.Control as="textarea" placeholder="Leave a comment here" style={{ height: '100px' }}/>
          </FloatingLabel>

          // 파일 업로드를 위한 폼, jpg/jpeg,png만 입력할 수 있도록 하였음, multiple 옵션을 통해 다중 파일 업로드 가능
          <Form.Group controlId="formFileMultiple" className="mb-3">
            <Form.Label>여러개의 이미지를 삽입하려면 드래그 및 컨트롤 클릭으로 다중 선택해주세요!</Form.Label>
            <Form.Control type="file" accept=".jpg,.png,.jpeg" onChange={handleChangeFile} multiple />
          </Form.Group>
          
          // 선택한 파일들을 프론트 상에 출력시켜줌
          <ul>
            {files.map((file,index)=>(
              <li key={`${file.name}_${index}`}>
                {file.name}
                <button onClick={()=>handleDelete(index)}>삭제</button>
              </li>
            ))}
          </ul>

          // 등록 버튼
          <div>
             <button onClick={()=> Send()} class="btn write-button btn-primary">등록</button>
          </div>
        </>
      );
  }
  
  export default PostCard;

 

위 코드에서 주목해야할 부분은 2 곳이다.

 

첫 번째는 데이터를 전송하는 Send 함수, 두 번째는 파일 업로드와 관련된 핸들러인 handleChangeFile, handleDelete다.

 

1. Send 함수

function Send(){
  const fd = new FormData(); // 전송할 데이터들을 저장해둘 폼
  Object.values(files).forEach((file) => fd.append("file", file)); // 입력한 파일들 추가
  fd.append("category",document.getElementById('category').value); // 입력한 게시판 종류 추가
  fd.append("subject",document.getElementById('subject').value);   // 입력한 제목 추가
  fd.append("content",document.getElementById('content').value);   // 입력한 내용 추가

  //axios를 통해 http://localhost:8080/user/AxiosFileTest로 fd 전송
  axios.post('http://localhost:8080/user/AxiosFileTest', fd, {
    headers: {
      "Content-Type": `multipart/form-data; `  // 파일 데이터 전송을 위해서 명시필요
    },
  })
  .then((response) => {
  })
  .catch((error) => {
    // 예외 처리
  })
}

files에는 현재 선택된 파일들의 상태 정보가 담겨있다.

이를 forEach를 통해 files 내부의 파일들을 하나하나 탐색하면서, 각 파일을 fd에 file이라는 이름으로 저장한다.

그리고 각각의 입력 폼에 작성된 게시판 종류, 제목, 내용 또한 fd에 추가 저장한다.

 

이해를 돕기위한 예로 JSON 형태로 이 데이터를 표현하면

{

    file : {"img1과 관련된 파일정보"," img2과 관련된 파일정보", "img3과 관련된 파일정보"},

    category : "자유게시판1",

    subject : "제목임",

    content : "나도 고수가 되고싶어요"

}

와 같은 형태로 fd가 존재하게 된다.

 

그 후 axios를 통해 우리가 원하는 url로 작성한 fd를 전송한다.

주의할 점은 파일을 전송하기 때문에, Content-Type을 multipart/form-data로 명시해야 한다는 점이다.

이 부분이 빠지면 스프링에서는 해당 데이터 형식을 이해할 수 없어 읽지 못한다.

 

2. handleChangeFile, handleDelete

const [files, setFiles] = useState([]);	//파일

const handleChangeFile = (event) => {
  console.log(event.target.files);
  if(event.target.files.length!=0){  // 이 조건문으로 인해 파일 업로드시 이미지를 선택하지 않아도 기존의 업로드할 내역들이 유지됨
    setFiles(Array.from(event.target.files || []));
  }
}

const handleDelete = (index) =>{
  setFiles([...files.slice(0,index), ... files.slice(index+1)])
};

handleChangeFile은 setFiles라는 state 변경 함수를 통해 files state를

입력받은 Array형태의 파일들로 변경한다.

 

handleDelete는 slice 메소드를 사용해서, 0~index-1까지의 값과 index+1에서 끝까지의 값으로 변경한다.

따라서 files[index]의 내용이 사라지면서 해당 이미지가 files state에서 삭제된다.

 

Spring

@RestController
@CrossOrigin(origins="*") // 이거 넣어야 CRos 에러 안남
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
    @RequestMapping(value="/AxiosFileTest", method=RequestMethod.POST)
    public Map<String,Object> AxiosFileTest (HttpServletRequest request,
        @RequestParam(value="file", required=false) MultipartFile[] files) throws SQLException {
        System.out.println(request.getParameter("category"));
        System.out.println(request.getParameter("subject"));
        System.out.println(request.getParameter("content"));

        Map<String,Object> resultMap = new HashMap<String,Object>();
        String FileNames ="";
        System.out.println("paramMap =>"+files[0]);

        String filepath = "D:/saveFolder/";  // 해당 파일을 저장할 절대주소
        for (MultipartFile mf : files) {

            String originFileName = mf.getOriginalFilename(); // 원본 파일 명
            long fileSize = mf.getSize(); // 파일 사이즈
            System.out.println("originFileName : " + originFileName);
            System.out.println("fileSize : " + fileSize);

            String safeFile =System.currentTimeMillis() + originFileName;

            FileNames = FileNames+","+safeFile;
            try {
                File f1 = new File(filepath+safeFile);
                mf.transferTo(f1);
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        Map<String, Object> paramMap = new HashMap<String, Object>();
        FileNames = FileNames.substring(1);
        System.out.println("FileNames =>"+ FileNames);
        resultMap.put("JavaData", paramMap);
        return resultMap;
    }
}

파일은 file이라는 파라미터로 저장해두었으므로 위 어노테이션을 통해 files에 저장되고

나머지 데이터들은 request.getParameter("파라미터이름"); 을 통해 수신할 수 있다.

 

전송받은 파일들을 위에서는 특정 디렉터리에 저장해두었는데, 이는 필요에 따라 수정하면 되겠다.

 

아래는 프론트단에서 전송한 데이터를 스프링에서 문제없이 수신하여 출력한 모습이다.