spring boot 이미지 저장 (feat.제발 상대경로 사용하지 마세요)

Hvvany·2023년 5월 18일
0

spring

목록 보기
1/1

이미지 저장 시 절대경로!

이미지 파일을 사용자로 부터 입력 받아 처리하기 까지 매우 복잡하여 정리를 해본다.

1. html 에서 input type=”file”로 입력 받기

ArticleBoardNewView

<input type="file" name="files" id="files" multiple="multiple" />

그러면 다음과 같이 input 요소가 생긴다

2. API로 전송 시 JSON 말고 formData로 보낸다

처음에 이미지를 JSON 형태로 고칠려고 했으나 불가능.

JSON을 formData객체에 함께 담아서 보내줘야 한다.

ArticleBoardNewView 의 method 내부 post요청 부분

methods: {
    sendArticle() {
      if ((this.content_text != "") & (this.content_title != "")) {
				// 폼 데이터로 보내줘야 하므로 객체 생성
        let formData = new FormData();
				// json형태로 바로 보내던 데이터를 일단 변수에 저장
        let data = {
          id: this.userInfo.userId,
          title: this.content_title,
          content: this.content_text,
        };
				// 그냥 files까지만 보내면 배열이 갈 줄 알았으나 배열 모양을 한 딕셔너리더라...{0:{이름:ㅇㅇ},1:...
	      // 얕은 복사(Array.from)을 통해 배열로 데이터를 바꾸어 저장해준다
				Array.from(document.querySelector("#files").files).forEach((file) => {
          formData.append("files", file);  // files로 같은 이름에 데이터를 append 하면 배열로 들어간다
          console.log(file);
        });

        formData.append(
          "key",
          new Blob([JSON.stringify(data)], { type: "application/json" })
        );

			//  => 위의 과정을 거치면 formData에는 'files'이름의 이미지 배열과 
			//                                 'key'이름의 기존json데이터(이메일,제목 등)담김

				// 아래는 POST 요청 부분
        http
          .post("/article/board/new", formData, {
            headers: {
              "Content-Type": "multipart/form-data",  //기존의 json 대신 formData 설정
            },
            transformRequest: [
              function () {
                return formData;
              },
            ],
          })
          .then(({ status }) => {
            if (status == 200) {
              this.$router.push({ name: "board" });
            }
          });
      } else {
        alert("정보를 입력해 주세요");
      }
    },
  },

Blob : Binary Large Object. → javascript에서 이미지, 음성 등 대용량 데이터 다루는 객체

3. 백엔드 Controller 에서 넘어온 formData처리

ArticleController의 boardnew post 부분

@PostMapping("/board/new")
	int postBoard(@RequestPart(value = "key") Article article, @RequestPart(value = "files", required = false) MultipartFile[] files) throws Exception {
// formData는 RequestPart 어노테이션을 통해 MultipartFile 클래스로 데이터를 받는다. 여기서 required = false 를 안해주니 에러가 발생하였다.

		System.out.println("article : " + article + ", files : " + files);
// 프린트 : article : Article [articleNo=0, title=asdf, id=ssafy, content=asdf, regTime=null], files : [Lorg.springframework.web.multipart.MultipartFile;@4049d099
		
		String realPath = "/Users/hvvany/Desktop/OISO_BE/last_pjt/trip/src/main/resources/static/imgs";  // 스프링 부트에서 파일 저장 시 상대경로로 하면 경로 못찾음
		String today = new SimpleDateFormat("yyMMdd").format(new Date());
		File folder = new File(realPath);
		if (!folder.exists()) {
			folder.mkdirs();
		}
		List<FileInfo> fileInfos = new ArrayList<FileInfo>();
		for (MultipartFile mfile : files) {
			FileInfo fileInfo = new FileInfo();
			String originalFileName = mfile.getOriginalFilename();
// 파일 경로 없으면 폴더 생성
			if (!originalFileName.isEmpty()) {
				String saveFileName = UUID.randomUUID().toString()  // UUID는 이미지 이름 중복 방지 위해 랜덤하게 생성된 고유값
						+ originalFileName.substring(originalFileName.lastIndexOf('.'));
				fileInfo.setSaveFolder(today);
				fileInfo.setOriginFile(originalFileName);
				fileInfo.setSaveFile(saveFileName);

				mfile.transferTo(new File(folder,saveFileName));
//			FileCopyUtils.copy(mfile.getInputStream(), new FileOutputStream(realPath + Paths.get(saveFileName).toFile()));
			}
			fileInfos.add(fileInfo);
		}
// article 객체에 이미지 파일 정보도 저장해준다 (배열 형태)
		article.setFileInfos(fileInfos);

// Service 단으로 데이터 전송하고 성공 여부는 int로 받는다.
		int cnt = service.postBoard(article);
		return cnt;
	}

이슈 정리

  1. ‘files’를 찾지 못한다

    ⇒ @RequestPart(value = "files", required = false) required = false 를 추가해주니 해결

  2. 상대 경로 문제

    java.io.IOException: java.io.FileNotFoundException: /private/var/folders/c3/hxgrbq710ndfsnm459mczt1c0000gn/T/tomcat.80.2385627806756500432/work/Tomcat/localhost/ROOT/src/main/resources/static/imgs/a14d2d56-c734-40f0-9c06-6d30cf39b48b.png (No such file or directory)

    스프링 부트 내부의 폴더에 저장하기 위해 상대 경로로 static 폴더에 접근해보려 했으나 경로를 찾지 못하고 에러가 발생했다.

    찾아보니 프로젝트 외부에 절대 경로를 사용하여 경로를 지정해주어야 한다고 한다…

  3. 위의 경로 문제로 알게 된 다른 사실

    mfile.transferTo(new File(folder,saveFileName));
    
    FileCopyUtils.copy(mfile.getInputStream(), new FileOutputStream(Paths.get(saveFileName).toFile()));

    둘 다 된다

Article (Dto) 참고하기 _ 수정함

public class Article {

	private int articleNo;
	private String title;
	private String id;
	private String content;
	private String regTime;
	private List<FileInfo> fileInfos;   // 파일 저장 위해서 기존 필드에 추가된 부분
	
	public Article() {
		super();
	}
	public Article(int articleNo, String title, String id, String content, String regTime, List<FileInfo> fileInfos) {
		super();
		this.articleNo = articleNo;
		this.title = title;
		this.id = id;
		this.content = content;
		this.regTime = regTime;
		this.fileInfos = fileInfos;      // 파일 저장 위해서 기존 필드에 추가된 부분
	}

4. Service 단 구현

ArticleService

...
int postBoard(Article article) throws Exception;
...

ArticleServiceImpl

...
@Override
	@Transactional
	public int postBoard(Article article) throws Exception {
		List<FileInfo> file = article.getFileInfos();
		if (file != null && !file.isEmpty()) {
			for (int i = 0; i < file.size(); i++) {
				String fileName = file.get(i).getOriginFile();
				System.out.println("Uploaded file name: " + fileName);
			// 프린트 : 스크린샷 2023-05-17 오후 10.00.26.png   b8c1a584-72fa-4bac-b0d7-be92b2194432.png
			}
			// 게시글 작성하는 매퍼 and 이미지 저장하는 매퍼 동시에 article을 보내주어 처리한다. 결과물은 and 연산으로 둘 다 0 아니면 성공 처리하게 구현했다.
			return articleMapper.postBoard(article) & articleMapper.fileRegister(article);
		}
		return 0;
	}
...

5. Mapper 단 구현

article.xml

...

	<insert id="postBoard">
		insert into board (id, title, content)
		values (#{id}, #{title}, #{content})
		<selectKey resultType="int" keyProperty="articleNo" order="AFTER">
		SELECT LAST_INSERT_ID()
		</selectKey>
	</insert>
	
	<insert id="fileRegister" parameterType="Article">
		insert into file_info (article_no, save_folder, original_file, save_file)
		values
		<foreach collection="fileInfos" item="fileinfo" separator=" , ">
			(#{articleNo}, #{fileinfo.saveFolder}, #{fileinfo.originFile}, #{fileinfo.saveFile})
		</foreach>
	</insert>

...

SELECT LAST_INSERT_ID() : 게시글 등록하면서 자동 생성된 auto increment 값을 받아와서 이미지 저장할 때 article_No에 값을 저장한다.

위의 게시글 명령 후 아직 커밋? 이 안된 상태이므로 정보를 가져와서 파일 업로드에 사용할 수 있다.

6. MySQL 단 구현

use enjoytrips;
drop table `file_info`;
CREATE TABLE `file_info` (
  `article_no` int NOT NULL,
  `save_folder` varchar(100) DEFAULT NULL,
  `original_file` varchar(100) DEFAULT NULL,
  `save_file` varchar(100) DEFAULT NULL
  );

이미지 저장을 위한 테이블 따로 생성

profile
Just Do It

0개의 댓글