본문 바로가기
주제 없음

240616(책 마무리)

by Coarti 2024. 6. 16.

Spring 파일 업로드, 다운로드 구현

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>restSpring</groupId>
	<artifactId>restSpring</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<properties>
		<java-version>11</java-version>
		<org.springframework-version>5.3.36</org.springframework-version>
	</properties>

	<dependencies>
		<!-- Necessary -->

		<!-- servlet for tomcat9 -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>4.0.1</version>
			<scope>provided</scope>
		</dependency>
		<!-- spring framework 5 -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>

		<!-- lombok -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.32</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.4.14</version>
		</dependency>

		<dependency>
			<groupId>net.coobird</groupId>
			<artifactId>thumbnailator</artifactId>
			<version>0.4.20</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.15.4</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<release>11</release>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.2.3</version>
			</plugin>
		</plugins>
	</build>
</project>

web.xml

 

	<servlet>
		<servlet-name>dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>


		<!-- web.xml -->
		<multipart-config>
			<location>C:\\업로드 폴더 경로</location>
			<max-file-size>20971520</max-file-size><!-- 20MB --> <!-- 파일 최대 크기 -->
			<max-request-size>41943040</max-request-size><!-- 40MB --> <!-- 2개이상 업로드시 크기 합의 최대 -->
			<file-size-threshold>20971520</file-size-threshold><!-- 20MB --> <!-- 메모리사용 -->
		</multipart-config>
	</servlet>

 

servlet-context.xml

  • jsp위치는 prefix 확인

 

<beans 

// 생략...

	<context:component-scan
		base-package="컨트롤러 위치" />
	<mvc:annotation-driven />
    
    <!-- webapp/WEB-INF/ -->
	<mvc:resources location="/resources/"  mapping="/resources/**" />
	<bean
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 		<property name="viewClass"
			value="org.springframework.web.servlet.view.JstlView" /> -->
		<property name="prefix" value="/WEB-INF/views/" />
		<property name="suffix" value=".jsp" />
	</bean>
	
	<bean id="multipartResolver"
		class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
	</bean>
	
</beans>

root-context.xml

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

</beans>

domin

package com.cg.domain;

import lombok.Data;

@Data
public class AttachFileDTO {
	private String fileName;
	private String uploadPath;
	private String uuid;
	private Boolean isImage;
}

 

Controller

package com.cg.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.cg.domain.AttachFileDTO;

import net.coobird.thumbnailator.Thumbnailator;

@Controller
@RequestMapping("/")
public class UploadContoller {
	private Logger log = LoggerFactory.getLogger(getClass());

	@GetMapping("uploadForm")
	public void uploadForm() {
		log.info("{}", "uploadForm..........");
	}

	@PostMapping("uploadFormAction")
	public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
		String uploadFolder = "C:\\project\\kb\\spring\\tmp";

		for (MultipartFile m : uploadFile) {
			log.info("--------------");
			log.info("{}", m.getOriginalFilename());
			log.info("{}", m.getSize());

			File saveFile = new File(uploadFolder, m.getOriginalFilename());

			try {
				m.transferTo(saveFile);
			} catch (Exception e) {
				log.error(e.getMessage());
			}
		}

	}

	@GetMapping("uploadAjax")
	public void uploadAjax() {
		log.info("upload ajax");
	}

	@PostMapping(value = "uploadAjaxAction", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<List<AttachFileDTO>> uploadAjaxPost(MultipartFile[] uploadFile, Model model) {
		// 화면에 보여줄 정보
		// 1.업로드된 파일의 이름과 원본파일의 이름
		// 2.파일이 저장된 경로
		// 3.업로드된 파일이 이미지인지 아닌지에 대한 정보

		List<AttachFileDTO> list = new ArrayList<>();
		String uploadFolder = "C:\\project\\kb\\spring\\tmp";

		//
		// 1. 중복방지: 폴더 만들기, 날짜별로 구분 yyyy/MM/dd
		String uploadFolderPath = getFolder();
		File uploadPath = new File(uploadFolder, uploadFolderPath);
		log.info("upload path: {}", uploadPath);

		if (!uploadPath.exists()) {
			uploadPath.mkdirs();
		}

		// upload
		for (MultipartFile m : uploadFile) {
			AttachFileDTO dto = new AttachFileDTO();
			log.info("--------------");
			log.info("{}", m.getOriginalFilename());
			log.info("{}", m.getSize());
			String uploadFileName = m.getOriginalFilename();
			dto.setFileName(uploadFileName);//

			// 2. 중복방지: uuid
			UUID uuid = UUID.randomUUID();

			uploadFileName = uuid + "_" + uploadFileName;
			log.info("uploadFileName: {}", uploadFileName);
//			File saveFile = new File(uploadFolder, uploadFileName);
			File saveFile = new File(uploadPath, uploadFileName);

			dto.setUploadPath(uploadFolderPath);//
			dto.setUuid(uuid.toString());//
			try {
				m.transferTo(saveFile);

				if (checkImageType(saveFile)) {
					dto.setIsImage(true);//
					FileOutputStream thumnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));
					// 이미지 썸네일 처리, 요청 환경에 맞는 이미지 크기 조절
					Thumbnailator.createThumbnail(m.getInputStream(), thumnail, 100, 100);
					thumnail.close();
				}
				list.add(dto);
			} catch (Exception e) {
				log.error(e.getMessage());
			}
		}
		return new ResponseEntity<List<AttachFileDTO>>(list, HttpStatus.OK);

	}

	@GetMapping("display")
	@ResponseBody
	public ResponseEntity<byte[]> getFile(String fileName) {
		log.info("fileName: {}", fileName);
		String uploadFolder = "C:\\project\\kb\\spring\\tmp\\";

		File file = new File(uploadFolder + fileName);
		log.info("file: {}", file);

		ResponseEntity<byte[]> result = null;

		try {
			HttpHeaders header = new HttpHeaders();
			header.add("Content-type", Files.probeContentType(file.toPath()));
			result = new ResponseEntity<byte[]>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
		} catch (Exception e) {
			e.printStackTrace();
		}

		return result;
	}

//	@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
//	@ResponseBody
//	public ResponseEntity<Resource> downloadFile(String fileName) {
//		String uploadFolder = "C:\\project\\kb\\spring\\tmp\\";
//		FileSystemResource resource = new FileSystemResource(uploadFolder + fileName);
//		log.info("resource: {}", resource);
//
//		String resourceName = resource.getFilename();
//
//		HttpHeaders headers = new HttpHeaders();
//		try {
//			headers.add("Content-Disposition",
//					"attachment; filename=" + new String(resourceName.getBytes("UTF-8"), "ISO-8859-1"));
//		} catch (Exception e) {
//			e.printStackTrace();
//		}
//		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
//	}
	@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
	@ResponseBody
	public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName) {
		String uploadFolder = "C:\\project\\kb\\spring\\tmp\\";
		FileSystemResource resource = new FileSystemResource(uploadFolder + fileName);
		log.info("resource: {}", resource);

		if (!resource.exists()) {
			return new ResponseEntity<Resource>(HttpStatus.NOT_FOUND);
		}

		String resourceName = resource.getFilename();
		// remove uuid
		String resourceOriginalName = resourceName.substring(resourceName.indexOf("_") + 1);

		HttpHeaders headers = new HttpHeaders();
		try {
			String downloadName = null;

			if (userAgent.contains("Trident")) {
				log.info("IE browser");
				downloadName = URLEncoder.encode(resourceOriginalName, "UTF-8").replaceAll("\\+", " ");
			} else if (userAgent.contains("Edge")) {
				log.info("Edge browser");
				downloadName = URLEncoder.encode(resourceOriginalName, "UTF-8");
			} else {
				log.info("Chrome browser");
				downloadName = new String(resourceOriginalName.getBytes("UTF-8"), "ISO-8859-1");
			}
			log.info("downloadName: {}", downloadName);

			headers.add("Content-Disposition", "attachment; filename=" + downloadName);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}

	@PostMapping("deleteFile")
	@ResponseBody
	public ResponseEntity<String> deleteFile(String fileName, String type) {
		log.info("deleteFile, fileName: {}, type: {}", fileName, type);
		String uploadFolder = "C:\\project\\kb\\spring\\tmp\\";

		File file;
		try {
			file = new File(uploadFolder + URLDecoder.decode(fileName, "UTF-8"));

			file.delete();
			if (type.equals("image")) {
				String largeFileName = file.getAbsolutePath().replace("s_", "");
				log.info("largeFileName: {}", largeFileName);

				file = new File(largeFileName);
				file.delete();
			}
		} catch (Exception e) {
			e.printStackTrace();
			return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
		}
		return new ResponseEntity<String>("deleted", HttpStatus.OK);	
	}

	private String getFolder() {
		SimpleDateFormat sdt = new SimpleDateFormat("yyyy-MM-dd");

		Date date = new Date();
		String str = sdt.format(date);

		// 년 / 월 / 일 순으로 폴더를 만들기 위함
		return str.replace("-", File.separator);
	}

	private boolean checkImageType(File file) {
		try {
			String contentType = Files.probeContentType(file.toPath());
			return contentType.startsWith("image");

		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

}

uploadAjax.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.uploadResult {
	width: 100%;
	background-color: gray;
}

.uploadResult ul {
	display: flex;
	flex-flow: row;
	justify-content: center;
	align-items: center;
}

.uploadResult ul li {
	list-style: none;
	padding: 10px;
	align-content: center;
	text-align: center;
}

.uploadResult ul li img {
	width: 100px;
}

.uploadResult ul li span {
	color: white;
}

.bigPictureWrapper {
	position: absolute;
	display: none;
	justify-content: center;
	align-items: center;
	top: 0;
	width: 100%;
	height: 100%;
	z-index: 100;
	background: rgba(255, 255, 255, 0.5);
}

.bigPicture {
	position: relative;
	display: flex;
	justify-content: center;
	align-items: center;
}

.bigPicture img {
	width: 600px;
}
</style>
</head>
<body>
	<div class='upload'>
		<input type="file" name='uploadFile' multiple="multiple">
	</div>
	<div class='uploadResult'>
		<ul>

		</ul>
	</div>

	<div class="bigPictureWrapper">
		<div class="bigPicture"></div>

	</div>
	<button id="uploadBtn">Submit</button>
	<script type="text/javascript">

    // 동일한 이름의 파일 업로드 시 덮어쓰기 됨: controller
    // 이미지 용량이 크면 섬네일 이미지를 생성함: controller
    // 이미지파일, 일반 파일 구분: controller

    // 확장자 제한, 파일 크기 제한
    const regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
    const maxSize = 5242880; //5MB
    function checkExtension(fileName, fileSize) {
        if (fileSize >= maxSize) {
            console.log('사이즈 초과');
            return false;
        }
        if (regex.test(fileName)) {
            console.log("업로드 불가 확장자");
            return false;
        }
        return true;
    }

    document.addEventListener("DOMContentLoaded", () => {
        const uploadBtn = document.querySelector("#uploadBtn");
        uploadBtn.addEventListener("click", () => {
            const formData = new FormData();
            const inputFile = document.querySelectorAll("input[name='uploadFile']")[0];

            const files = inputFile.files; // FileList는 list 타입이 아님
            console.log(files);
            for (let i = 0; i < files.length; i++) {
                console.log(files[i])
                if (!checkExtension(files[i].name, files[i].size)) {
                    return false;
                }
                formData.append("uploadFile", files[i]);
            }

            fetch("/uploadAjaxAction", {
                method: "post",
                body: formData
            })
                .then(response => {
                    if (response.ok) {
                        console.log("UPLOADED");
                        return response.json()
                    } else {
                        throw new Error('Network response was not ok.');
                    }
                })
                .then(result => {
                    // 업로드 후 페이지 초기화
                    // 결과 데이터 출력
                    console.log(result);
                    inputFile.value = '';
                    showUploadedFile(result);
                })
                .catch(error => console.log(error))


        })
        document.querySelector('.bigPictureWrapper').addEventListener('click', function() {
            const bigPicture = document.querySelector('.bigPicture');

            // bigPicture 요소를 작아지게 하는 애니메이션
            bigPicture.animate([
                { width: '100%', height: '100%' },
                { width: '0%', height: '0%' }
            ], {
                duration: 1000,
                fill: 'forwards'
            });

            // 1초 후 bigPictureWrapper 요소를 숨김
            setTimeout(() => {
                this.style.display = 'none';
            }, 1000);
        });
        
        document.querySelector('.uploadResult').addEventListener('click', function(e) {
            if (e.target && e.target.tagName === 'SPAN') {
                const targetFile = e.target.getAttribute('data-file');
                const type = e.target.getAttribute('data-type');

                const formData = new FormData();
                formData.append('fileName', targetFile)
                formData.append('type', type)
                
                
                console.log(formData);
                
                fetch('/deleteFile', {
                    method: 'POST',
                    body: formData
                })
                .then(response => response.text())
                .then(result => {
                    console.log(result);
                })
                .catch(error => {
                    console.error('Error:', error);
                });
            }
        });

    })
    const uploadResult = document.querySelector('.uploadResult ul');

    function showUploadedFile(uploadResultArr) {
        let str = '';

        uploadResultArr.forEach(v => {
            if (!v.isImage) {
                const fileCallPath = encodeURIComponent(v.uploadPath + "/" + v.uuid + "_" + v.fileName);
                str += '<li><div><a href="/download?fileName=' + fileCallPath + '"><img src="/resources/img/undraw_posting_photo.svg">' + v.fileName + '</a><span data-file="' + fileCallPath + '" data-type="file"> x </span></div></li>'
            } else {
                //str += '<li>' + v.fileName + '</li>'

                const fileCallPath = encodeURIComponent(v.uploadPath + "/s_" + v.uuid + "_" + v.fileName);

                let originPath = v.uploadPath + "\\" + v.uuid + "_" + v.fileName;
                originPath = originPath.replace(new RegExp(/\\/g), "/");

                str += "<li><div><a href=\"javascript:showImage('" + originPath + "')\"><img src = 'display?fileName=" + fileCallPath + "'></a><span data-file='" + fileCallPath + "' data-type='image'> x </span></div></li>"
            }
        })
        uploadResult.innerHTML = str;
    }

    function showImage(fileCallPath) {
        console.log(fileCallPath);

        document.querySelector('.bigPictureWrapper').style.display = 'flex';
        document.querySelector('.bigPicture').innerHTML =
            "<img src='/display?fileName=" + encodeURI(fileCallPath) + "' >";
        document.querySelector('.bigPicture').animate([
            {width: '0%', height: '0%'},
            {width: '100%', height: '100%'}
        ], {
            duration: 1000,
            fill: 'forwards'
        });
    }
    
</script>
</body>
</html>

 

 

사람마다 파일 처리 방식이 다르다

공통적인 부분을 정리해보자

업로드

  1. Ajax || form 선택
  2. multipart 설정(context || config || MultipartResolver)
  3. multipart를 받아주는 객체(Part, MultipartFile, ...)
  4. 중복 데이터 허용
  5. 업로드 시 유효처리(확장자, 크기)
  6. 한글 인코딩
  7. 스트림을 사용하여 업로드
  8. 결과 확인

다운로드

  1. 기준이 되는 저장위치 + 저장될 곳 + 파일이름.확장자
  2. MediaType.APPLICATION_OCTET_STREAM_VALUE
  3. 한글 디코딩
  4. uuid 제거
  5. Content-Disposition:
    attachment; filename=다운로드 파일 이름.확장자

삭제

  1. File 클래스에 delete() 메소드 사용
  2. 위치 + 이름.확장자 

 


700페이지 분량의 책 2주에 걸쳐 마무리 했다. 주말도 없이 평균 10시간이 넘도록 파봤다. 

얻게 된것은 Mybatis(mapper), 동적 SQL(trim, where, ...), 파일처리, 점진적으로 기능을 고도화하는 방법이 가장 도움이 되었다. 물론 Rest  API, fetch API를 통해 http 통신,헤더에 대해 좀더 알게 되고 스프링의 어노테이션, 테스트 코드 작성 등 여러가지가 있었지만 대표적으로 그렇다

 

Mybatis : 개발자가 직접 SQL을 작성하고 결과에 따른 데이터를 매핑을 지원한다. JDBC에서 반복적으로 등장한 코드를 자동화 해주어 코드가 간결해진다.

동적 SQL : Mybatis에서 제공하는 특별한 태그들로 사용자 요청에 따라 SQL을 상황에 맞춰 조작이 가능하다 여러 SQL을 만들어 호출하지 않고 공통된 부분안에 변화하는 부분을 조건을 통하여 변화를 줄수 있다

파일처리 : Multipart여러종류의 데이터를 한번에 서버측으로 전송하기 위함JDK에서 제공하는 라이브러리로 충분히 이해 가능하고 적당한 기능을 구현할 수 있다는게 신기했다.

728x90

'주제 없음' 카테고리의 다른 글

2026 스타트업 박람회 후기  (0) 2026.01.14
크라우드스트라이크(CrowdStrike) 보안 업데이트 충돌  (0) 2024.07.20
240613(구현 방법을 알게됨)  (0) 2024.06.13
240612  (1) 2024.06.12
MySQL 실행 계획, Index Hint  (0) 2024.06.11