본문 바로가기
프로그래밍/Springboot-토이프로젝트

【게시판-12】프로필사진 등록('폼 태그'와 '드래그 앤 드롭'방식으로 파일 업로드)

by 코이킹 2022. 9. 29.
반응형

안녕하세요 코이킹입니다.
이번 포스트는 프로필 사진 등록('폼 태그'와 '드래그 앤 드롭'방식으로 파일 업로드) 구현에 대한 내용이 되겠습니다. 


1. 목표 

- 스프링 부트를 사용하여 복잡한 프로세스가 포함된 기능을 갖춘 웹 어플리케이션을 구현할 수 있다.  

- '폼 태그'와 '드래그 앤 드롭'방식(자바스크립트 함수)으로 파일 업로드 요청하는 기능을 구현할 수 있다.

- 스프링 부트를 사용하여 파일의 업로드 요청을 수신하는 기능을 구현할 수 있다.

- 스프링 부트를 사용하여 파일의 다운로드 기능을 구현할 수 있다.

- 업로드한 파일(정적 컨텐츠)을 화면에 표시할 수 있다.

2. 어떻게 구현할지에 대한 설명 

프로필 사진 등록기능의 핵심은 다음과 같습니다. 

 ① 프로필 사진으로 선택하고 싶은 사진의 임시 업로드

 ② 임시 업로드한 사진을 섬네일로 표시 

 ③ 섬네일로 표시한 사진이 문제가 없다면, 프로필 사진을 등록

 ④ 페이지 상단의 공통메뉴에 등록된 프로필 사진 표시

 

프로필 사진 등록 기능의 핵심을 바탕으로 해야 할 일을 정의해보면 다음과 같습니다. 

질문하기 답변추려내기
사진의 임시업로드 임시업로드는 어디에서 / 어떻게 이루어 질 것인가? 프로필 사진 등록 페이지를 추가하여, 임시업로드 시의 파일 데이터를 입력받는다.
  프로필 등록 페이지 이동경로  페이지 상단의 공통메뉴의 링크에서 이동.
  임시업로드 요청과 수신방법 요청은 POST방식으로 이루어지며, 폼 태그를 사용하는 방식과 드래그 앤 드롭동작에 반응하는 자바스크립트 함수를 사용하는 방식 두가지를 지원할 것.
요청시 요청헤더의 컨텐츠 타입을 멀티파트 폼으로 설정.
회원가입 컨트롤러에 섬네일 등록 메서드를 추가하여 요청을 수신한다.
  임시업로드된 파일은 언제 / 어디에서 / 어떻게 보존될 것인가? 업로드와 동시에 스프링 부트의 정적파일 디렉토리 하위의 tmp/디렉토리에 "회원번호_타임스탬프_원본파일명.원본확장자"포맷의 파일명으로 보존된다. 
섬네일로 표시 섬네일은 언제 / 어디에서 / 어떻게 표시될 것인가? 임시 업로드 요청의 응답이 리턴되면, 섬네일 표시요청을 보낸다. 요청은 GET방식으로 이루어지며, 프로필사진 등록페이지에 섬네일도 표시된다. 
표시되는 섬네일의 사이즈는 섬네일 란의 크기를 초과할 수 없다. 
프로필 사진 등록 프로필 사진등록은 어디에서 / 어떻게 이루어 질 것인가? 프로필 사진 등록 페이지에서 섬네일이 표시된 상태로 '등록 버튼을 클릭'시 프로필사진 등록 요청을 서버로 보낸다. 

  프로필 사진등록 요청과 수신방법 요청은 POST방식으로 이루어지며, 폼 태그를 사용하여 요청을 보낸다.
  등록된 프로필 사진파일은 언제 / 어디에서 / 어떻게 보존될 것인가? 프로필 사진등록 요청에 따라 섬네일 보존 디렉토리에서 프로필사진 디렉토리로 파일을 이동시킨다. 
등록된 프로필 사진표시 등록된 프로필 사진은 언제 / 어디에서 / 어떻게 표시될 것인가? 등록된 프로필 사진은 페이지 상단의 공통메뉴에서 표시될것.
공통사항 업로드될 프로필사진의 용량제한은? 용량제한은 10MB로 한다.
  업로드될 프로필사진의 확장자 형식은? 확장자 형식은 사진파일만으로 제한한다. (.png,.jpg,.jpeg,.gif)

 

3. 프로세스 흐름

 

4. 데이터 흐름 

① 임시파일 업로드 (섬네일 등록 요청)

1-1) 폼 태그 사용 시

 - 파일 선택 버튼 클릭하여 파일 탐색기를 열어, 업로드할 파일을 선택합니다. 

 - 폼 태그에 데이터가 들어오는 동작을 자바스크립트 함수가 인식하여, 섬네일 등록 요청을 보냅니다. 

1-2) 드래그 앤 드롭 사용 시

 - 업로드할 파일을 드래그하여 업로드 상자위에 올립니다. 

 - 자바스크립트 함수가 드래그 동작을 인식하여, 업로드 상자의 색상을 변경하여 임시 업로드가 가능함을 유저에게 알립니다. 

 - 파일을 드롭하면, 자바스크립트 함수가 파일을 드롭하는 동작을 인식하여, 섬네일 등록 요청을 서버로 보냅니다. 

2) 업로드한 임시파일은 정적 파일 디렉터리 하위의 tmp/디렉터리에 "회원번호_타임스탬프_원본 파일명. 원본 확장자"포맷의 파일명으로 보존합니다. 

3) 보존된 임시 파일명을 섬네일 등록 요청의 응답 시에 응답 바디에 넣어서 보냅니다. 

 

② 섬네일 표시

 1) 섬네일 등록 요청의 응답 결과로 받은 '임시 등록된 파일명'을 파라미터로 하여 섬네일 표시 요청을 보냅니다.

 2) 섬네일 표시 요청의 결과로 파일 데이터를 다운로드하여 섬네일 상자에 표시합니다. 

 

③ 프로필 사진 등록 요청 보내기 

 1) 임시파일을 프로필 사진 디렉터리로 이동시킵니다.

 2) DB에 프로필 사진을 등록합니다.

 3) 세션의 유저 인증정보에프로필 사진 정보를 추가합니다. 

 4) 프로필 사진 등록 요청 처리가 완료되면 프로필 사진 등록 페이지로 리다이렉트 합니다.

 

④ 프로필 사진 표시

 1) 세션에 저장된 프로필 사진의 정보를 가지고 스프링 부트의 정적 리소스에 접근하여 프로필 사진을 표시합니다.

 

5. 소스코드와 해석

1) 설정 파일 : src/main/resources/application.yml

      
  servlet:
    multipart:
      enabled: true 
      max-file-size: 10MB
      max-request-size: 10MB
      
  devtools:
    livereload:
      enabled: true
    remote:
      restart:
        enabled: true

property:
  app:
    static-path: src/main/resources/static/
    temp-upload-path: uploads/tmp
    upload-path-member-pic: uploads/member-profile
    allow-extentions: .png,.jpg,.jpeg,.gif

2~6 : 파일 업로드를 위한 설정, 업로드할 수 있는 파일의 용량을 10MB으로 제한합니다. 

8~13 : 등록한 프로필 사진을 바로 보일 수 있게 하는 설정입니다. ※ 이 설정이 없음 이클립스에서 임포트 한 프로젝트를 리프레쉬해주어야 합니다.  

15~20 : 임시 파일과 프로필 사진이 위치할 경로를 지정하고, 허용할 파일의 확장자를 지정합니다. 

 

2) 프로필 사진 등록 페이지 템플릿 : src/main/resources/templates/auth/memberInfo.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
  layout:decorate="~{common/layout}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, inital-scale=1.0"/>

</head>
<body>

<div layout:fragment="contents">
<style type="text/css">
	#upload-box {
		width : 300px;
		height: 300px;
		border : 1px solid gray;
		box-shadow: 2px 3px 9px hsl(0, 0% 50%);
		padding: 10px;
	}
	#upload-btn {
		width : 180px;
		height: 50px;
		border : 1px solid gray;
		box-shadow: 2px 3px 9px hsl(0, 0% 50%);
		padding: 10px;
	}
</style>
    <div class="nav justify-content-center col-12">

	    <form method="post" th:action=@{/auth/profile-pic}>
	    <div id="hidden-val">

		</div>
	    <div class="mb-3">

		    <div class="row">
			    <div class="col-6">Upload Profile Picture
				    <section id="upload-box" style="
					    width:200px;
					    height:200px;
					    border:1px solid gray;
					    box-shadow:2px 3px 9px hsl(0, 0% 50%);
					    padding: 10px;"
				    >
				    <div id="upload-element">
				        <input class="btn btn-info" id="upload-btn" type="file" value="Select File" />
				    </div>
				    </section>
			    </div>

			    <div class="col-6">ThumbNail
			    <section id="thumb-box" style="
					    width:200px;
					    height:200px;
					    border:1px solid gray;
					    box-shadow:2px 3px 9px hsl(0, 0% 50%);"
				    >


				    <img id="thumbNail" style=" width:180px; height:180px;">
				    </section>

			    </div>

		    </div>

		</div>
	    <div class="nav justify-content-center col-8">
	    	<input class="btn btn-success" type="submit" value="Apply Profile-Picture">
	    </div>
	    </form>
    </div>
    <hr/>

	<div>
		<button class="btn btn-secondary py-2 my-2" type="button" onclick="location.href='javascript:history.back();'">Return to Previous Page</button>
		<button class="btn btn-info py-2 my-2" type="button" onclick="location.href='/board/list'">Back to List</button>
	</div>

<script type="text/javascript" th:inline="javascript">

var baseApi  = "http://localhost:8080"
var authApi = baseApi+"/auth"

var uploadBox = $('#upload-box')[0];
var btnUpload = uploadBox.querySelector('.upload-btn');
var inputFile = uploadBox.querySelector('input[type="file"]');
var uploadEl = uploadBox.querySelector('#upload-element');


$(document).ready(function() {
	$('#upload-btn').change(function(){

		const formData = new FormData();
		formData.append('file', $('#upload-btn')[0].files[0]);

		uploadThumb({
			url: authApi+'/thumb',
			method: 'POST',
			data: formData,
			progress: () => {},
		    loadend: () => {}
		});

	});
});


$(document).ready(function() {

	/* When it occur as soon as event of Drag in upload-box. */
	uploadBox.addEventListener('dragenter', function(e) {
	    //console.log('dragenter');
	});

	/* When it is doing to Drag in upload-box. */
	uploadBox.addEventListener('dragover', function(e) {
	    e.preventDefault();
	    //console.log('dragover');

        var vaild = e.dataTransfer.types.indexOf('Files') >= 0;

        if(!vaild){
            this.style.backgroundColor = 'red';
        }
        else{
            this.style.backgroundColor = 'green';
        }
	});

	/* When event of Drag get out of upload-box. */
	uploadBox.addEventListener('dragleave', function(e) {
	    //console.log('dragleave');

	    this.style.backgroundColor = 'white';
	});

	/* When it occur event of Drop in upload-box. */
	uploadBox.addEventListener('drop', function(e) {
	    e.preventDefault();
	    console.dir(e.dataTransfer);
	    const data = e.dataTransfer;

	    $("input[type='file']")
	    .prop("files", data.files);

	    this.style.backgroundColor = 'white';


	    if (!isValidFile(data)) return;

	    const formData = new FormData();
	    formData.append('file', data.files[0]);

	    uploadThumb({
	    	url: authApi+'/thumb',
	    	method: 'POST',
	    	data: formData,
	    	progress: () => {},
	    	loadend: () => {}
	    });

	});

});

function getThumb(obj) {
	var method = obj.method || 'GET';
	var url = obj.url || '';

	const xhr = new XMLHttpRequest();

	/* 성공 */
	xhr.addEventListener('loadend', function() {

		const data = xhr.response;
		console.log("getThumb addEventListener loadend ");
		console.log(data);

		var img = document.getElementById('thumbNail');
		var url = window.URL || window.webkitURL;
		img.src = url.createObjectURL(data);

		//if(obj.loadend())
		//	obj.loadend(data);
	});

	if(obj.async === false)
		xhr.open(method, url, obj.async);
	else
		xhr.open(method, url, true);

	xhr.responseType = 'blob';
	xhr.send();
}

function uploadThumb(obj) {
	const xhr = new XMLHttpRequest();

	var method = obj.method || 'GET';
	var url = obj.url || '';
	var data = obj.data || null;

	/* 성공 or 에러 */
	xhr.addEventListener('load', function() {

		const data = xhr.responseText;

		if(obj.load)
			obj.load(data);
	});

	/* 성공 */
	xhr.addEventListener('loadend', function() {

		const data = xhr.responseText;
		console.log("xhr.addEventListener loadend ");
		console.log(data);

		getThumb({
	    	url: authApi+'/thumb?filename='+data,
	    	method: 'GET',
	    	loadend: () => {}
	    });

		var picName = "<input type=\"hidden\" name=\"filename\" value=\""+data+"\"/>"
		$('#hidden-val').append(picName);

		if(obj.loadend) {

		}
	});

	/* 실패 */
	xhr.addEventListener('error', function() {

		console.log('Ajax Error : ' + xhr.status + ' / ' + xhr.statusText);

		if(obj.error){
			obj.error(xhr, xhr.status, xhr.statusText);
		}
	});

	/* 중단 */
	xhr.addEventListener('abort', function() {

		if(obj.abort){
			obj.abort(xhr);
		}
	});

	/* 진행 */
	xhr.upload.addEventListener('progress', function() {

		if(obj.progress){
			obj.progress(xhr);
		}
	});

	/* 요청 시작 */
	xhr.addEventListener('loadstart', function() {

		if(obj.loadstart)
			obj.loadstart(xhr);
	});

	if(obj.async === false)
		xhr.open(method, url, obj.async);
	else
		xhr.open(method, url, true);

	if(obj.contentType)
		xhr.setRequestHeader('Content-Type', obj.contentType);

	xhr.send(data);
}


function isValidFile(data) {

	//if (data.types.indexOf('Files') < 0) return false;

	if (data.files[0].type.indexOf('image') < 0) {
		alert('It only can upload Image-File');
		return false;
	}

	if (data.files.length > 1) {
		alert('It only can upload one Image-File per trying to upload.');
		return false;
	}

	if (data.files[0].size >= (1024 * 1024 * 10)) {
		alert('It can\'t upload a Image-File which is over 10MB.');
		return false;
	}

	return true;
}

</script>

</div>
</body>
</html>

 

-  92~107 : 태그의 id 속성이 upload-btn인 요소의 변화를 감지하여, uploadThumb함수를 실행합니다. 

- 110~166 : 드래그 앤 드롭 이벤트를 감지하여, uploadThumb함수를 실행합니다. 

- 168~196 : 업로드한 임시파일(섬네일 사진)을 다운로드하는 함수입니다.

- 198~ 277 : 임시파일(섬네일 사진)을 업로드하는 함수입니다.

 

3) 페이지 상단의 고정 메뉴 템플릿 : src/main/resources/templates/common/common_header.html

	        <div class="col-6 mx-auto my-auto">
		        <div th:if="${#strings.isEmpty(session?.member?.profilePicture)}">
		        <a th:href="@{/auth/member/} + ${session.member.memberNo}">
		        <svg xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" fill="currentColor" class="bi bi-file-earmark-person" viewBox="0 0 16 16">
	                <path d="M11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
	                <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2v9.255S12 12 8 12s-5 1.755-5 1.755V2a1 1 0 0 1 1-1h5.5v2z"/>
	            </svg>
	            </a>
	            </div>

	            <div th:if="${not #strings.isEmpty(session?.member?.profilePicture)}">
		        <a th:href="@{/auth/member/} + ${session.member.memberNo}">
		          <img width="32px" height="32px" alt="profile-pic" th:src="@{/uploads/member-profile/} + ${session.member.profilePicture}">
	            </a>
	            </div>
            </div>

- 등록된 프로필 사진을 표시하거나, 등록된 프로필 사진이 없다면 프로필 아이콘을 표시합니다. 

 

4) 회원 기능 컨트롤러 : src/main/java/com/sb/template/controller/MembershipController.java

	@GetMapping("/member/{memberNo}")
	public String readMember(@PathVariable int memberNo,
			HttpServletRequest req, HttpServletResponse res, Model model) {

		log.info("memberNo : {}", memberNo);
		HttpSession session = req.getSession();
		Member member = (Member)session.getAttribute("member");

		try {
			if (memberNo != member.getMemberNo()) {
				log.error("Param memberNo : {}, Session memberNo", memberNo, member.getMemberNo());
				model.addAttribute("message", "Unmatch MemberNo");
				model.addAttribute("redirectUrl", "/auth/join");
				return "/common/message";
			}

			Optional<Member> result = memberService.getMemberInfoByMemberId(member.getMemberId());
			if (result.isEmpty()) {
				log.error("Param memberNo : {}, Session memberNo", memberNo, member.getMemberNo());
				model.addAttribute("message", "Unmatch MemberNo");
				model.addAttribute("redirectUrl", "/auth/join");
				return "/common/message";
			}

			model.addAttribute("memberInfo", result);

		} catch (NullPointerException e) {
			log.error(e.getMessage());
			model.addAttribute("message", "Session value is null");
			model.addAttribute("redirectUrl", "/auth/login");
			return "/common/message";

		} catch (Exception e) {
			log.error(e.getMessage());
			model.addAttribute("message", "Server error");
			model.addAttribute("redirectUrl", "/auth/join");
			return "/common/message";
		}

		return "/auth/memberInfo";
	}



	@PostMapping("/thumb")
	@ResponseBody
	public ResponseEntity<?> uploadThumb(
			@RequestParam(value = "file") MultipartFile [] file,
			HttpServletRequest req, HttpServletResponse res
			) throws IOException {

		    HttpSession session = req.getSession();
		    Member member = (Member)session.getAttribute("member");

			String uploadedFile = filesStorageService.saveOne(file[0], member.getMemberNo().toString());

			log.info("Response Data : ["+uploadedFile+"]");

			return ResponseEntity.ok(uploadedFile);
	}

	@GetMapping("/thumb")
	@ResponseBody
	public ResponseEntity<Resource> getThumb(@RequestParam(value = "filename") String filename,
			HttpServletRequest req, HttpServletResponse res) throws IOException {

	    HttpSession session = req.getSession();
	    Member member = (Member)session.getAttribute("member");

		Resource resource = filesStorageService.load(filename, member.getMemberNo().toString());

        HttpHeaders header = new HttpHeaders();
        header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+resource.getFilename());
        header.add("Cache-Control", "no-cache, no-store, must-revalidate");
        header.add("Pragma", "no-cache");
        header.add("Expires", "0");


        log.info("thumb response Data : "+resource);
        return ResponseEntity.ok()
                .headers(header)
                .contentLength(resource.getFile().length())
                .body(resource);
	}


	@PostMapping("/profile-pic")
	public String joinMemberProfilePic(@RequestParam(value = "filename") String filename,
			HttpServletRequest req, HttpServletResponse res, Model model) throws IOException {

	    HttpSession session = req.getSession();
	    Member member = (Member)session.getAttribute("member");

		// exist and data
		Optional<Member> memberOp = memberService.getMemberInfoByMemberId(member.getMemberId());
		if (memberOp.isEmpty()) {
			model.addAttribute("message", member.getMemberId()+" not exist!!");
			model.addAttribute("nextUrl", "/auth/join");
			return "/common/message";
		}

		String saveFileName = new File (filesStorageService.move(filename, filename)).getName();

		Member memberForSave = memberOp.get();
		memberForSave.setProfilePicture(saveFileName);

		log.info("setProfilePicture : {}", saveFileName);

		memberService.updateMember(memberForSave);

		member.setProfilePicture(saveFileName);
		session.setAttribute("member", member);

		return "redirect:/auth/member/" + member.getMemberNo();
	}

- readMember메서드 : 프로필 사진 등록 페이지를 리턴하는 메서드입니다.

- uploadThumb메서드 : 섬네일 업로드 요청을 수신하는 메서드입니다.

- getThumb메서드 : 섬네일 파일을 다운로드하는 메서드입니다. 

- joinMemberProfilePic메서드 : 프로필 사진을 등록하는 메서드 입니다.

 

5) 사진 파일 관련 기능 서비스 :  src/main/java/com/sb/template/service/storage/ImageStorageServiceImpl.java

package com.sb.template.service.storage;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class ImageStorageServiceImpl implements FilesStorageService {

	@Value("${property.app.static-path}")
	private String staticPath;

	@Value("${property.app.temp-upload-path}")
	private String tempUploadPath;

	@Value("${property.app.upload-path-member-pic}")
	private String uploadPathMemberPic;

	@Value("${property.app.allow-extentions}")
	private List<String> allowExtentions;

	@Override
	public void init(Path dir) {
		try {
			Files.createDirectory(dir);
		} catch (IOException e) {
			throw new RuntimeException("Could not initialize folder for upload!");
		}
	}

	@Override
	public List<String> save(MultipartFile[] files, String uniqId) {
		try {
			List<String> uploadedFiles = new ArrayList<String>();
			log.info("Resource Path : ["+ staticPath+tempUploadPath+"]");
			String uniqImgeFileDir = staticPath+tempUploadPath;
			Path root = Paths.get(uniqImgeFileDir);

			int cnt =0;
			for (MultipartFile file : files) {
				String fileName = uniqId+"_"+
						new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
					    + "_"+
					    file.getOriginalFilename();

				log.info("[ "+cnt+" ]"+ fileName);

				Files.copy(file.getInputStream(), root.resolve(fileName));
				uploadedFiles.add(fileName);

				cnt++;
			}

			return uploadedFiles;

		} catch (Exception e) {
			throw new RuntimeException("Could not store the file. Error: " + e.getMessage());
		}
	}


	@Override
	public String saveOne(MultipartFile file, String uniqId) {
		try {

			log.info("Resource Path : ["+ staticPath+tempUploadPath+"]");
			String uniqImgeFileDir = staticPath+tempUploadPath;
			Path root = Paths.get(uniqImgeFileDir);
			String fileName = uniqId+"_"+
					new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
					   + "_"+
					file.getOriginalFilename();

			Files.copy(file.getInputStream(), root.resolve(fileName));

			return fileName;

		} catch (Exception e) {
			throw new RuntimeException("Could not store the file. Error: " + e.getMessage());
		}
	}



	@Override
	public Resource load(String filename, String uniqId) {

		log.info("Load File :"+staticPath+tempUploadPath+"/"+filename);

		try {
			String uniqImgeFileDir = staticPath+tempUploadPath;
			Path root = Paths.get(uniqImgeFileDir);

			Path file = root.resolve(filename);
			Resource resource = new UrlResource(file.toUri());

			if (resource.exists() || resource.isReadable()) {
				return resource;
			} else {
				throw new RuntimeException("Could not read the file!");
			}
		} catch (MalformedURLException e) {
			throw new RuntimeException("Error: " + e.getMessage());
		}
	}

	@Override
	public String move(String tempFile, String moveFile) {
		log.info("Temp File : {} -> Move File : {} ", (staticPath+tempUploadPath+"/"+tempFile), (staticPath+uploadPathMemberPic+"/"+moveFile));

		try {
			String tempFileDir = staticPath+tempUploadPath;
			String moveFileDir = staticPath+uploadPathMemberPic;

			Path tempRoot = Paths.get(tempFileDir);
			Path moveRoot = Paths.get(moveFileDir);

			Path tempFilePath = tempRoot.resolve(tempFile);
			Path moveFilePath = moveRoot.resolve(moveFile);

			Resource tempFileResource = new UrlResource(tempFilePath.toUri());
			Resource moveFileResource = new UrlResource(moveFilePath.toUri());

			Files.move(tempFileResource.getFile().toPath(), moveFileResource.getFile().toPath());

			return moveFilePath.toString();

		} catch (IOException e) {
			e.printStackTrace();
		}

		return null;
	}

	@Override
	public void deleteAll(String path) {
		FileSystemUtils.deleteRecursively(new File(path));
	}

	@Override
	public Stream<Path> loadAll(String path) {
		try {
			Path root = new File(path).toPath();
			return Files.walk(root, 1).filter(filepath -> !filepath.equals(root)).map(root::relativize);
		} catch (IOException e) {
			throw new RuntimeException("Could not load the files!");
		}
	}
}

 

6. 동작확인 

 

7. 전체 소스코드

https://github.com/leeyoungseung/template-springboot/tree/feature/membership_board_fileupload

 

GitHub - leeyoungseung/template-springboot

Contribute to leeyoungseung/template-springboot development by creating an account on GitHub.

github.com

 

8. 참고자료 

※ 파일 업로드기능은 아래의 주소의 블로그의 내용을 참고하여 작성했습니다. 

설명이 매우 자세하니 방문해 보시는 것을 추천드립니다. 

https://dev-gorany.tistory.com/254


이것으로 프로필 사진 등록('폼 태그'와 '드래그 앤 드롭'방식으로 파일 업로드) 구현에 대한 포스트는 마치겠습니다.

다음 포스트는 REST API를 활용한 댓글 구현에 대한 내용이 되겠습니다. 

 

반응형

댓글