React, Spring Boot 이미지 base64 변환, 이미지 수정, 파일 압축, 파일 다운로드 구현방법 (ResponseEntity, Axios, API Request, React)
이번에는 React + Spring Boot를 조합하여 Spring의 API 통신 처리 및 react 화면 기능 구현으로 이미지를 수정하고, 압축하고 다운로드 하는 방법에 대해 포스팅 하겠습니다. 사용된 프로젝트 버전은 다음과 같습니다.
※ 프로젝트 버전 ※
Java : 17
Spring boot : 3.2.5
React : 18.3.1
React Router : v6
axios : 1.7.2
화면 구성
먼저 화면 구성에 대해 말씀드리겠습니다. image파일을 업로드하면 image data base64로 변환합니다.
해당 이미지를 업로드하면 아래 코드와 같이 image 데이터를 react hook에 저장합니다. 그리고 해당 부분을 리렌더링 하여 이미지 미리보기칸에 해당 이미지를 확인 할 수 있습니다.
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA0AAAATACAYAAADZdCcUAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb
이미지 업로드 후 이미지 미리보기 결과는 다음과 같습니다.
UI form의 경우 MUI와 react-drag-drop-files을 이용하여 구현하였습니다. 코드는 다음과 같습니다.
const [fileView, setFileView] = React.useState([]);
const [fileName, setFileName] = React.useState([]);
/* 함수 */
const handleChange = (file) => {
setCnt(cnt+1);
setFileName([...fileName, file[0].name]);
let reader = new FileReader();
reader.readAsDataURL(file[0]);
reader.onloadend = () => {
const base = reader.result;
console.log(base);
setFileView([...fileView, [base]])
}
};
{/* 이미지 업로드 */}
<Container maxWidth="sm" sx={{padding: 3}}>
<FileUploader
handleChange={handleChange}
name="file"
types={["PNG", "JPG", "JPEG"]}
label={"이미지 파일을 끌어와 업로드 하세요!"}
multiple={true}
/>
</Container>
체인지 이벤트 발생시, 매개변수를 파일로 받은 후 파일의 데이터를 읽어 리렌더링 합니다. hook에 실제 파일 저장용과 뷰어용 2개를 선언하였습니다. 그리고 간단하게 이미지 크기를 변경하는 프로세스를 추가하여 해당 이미지 파일을 API 통신을 통해 수정된 이미지 파일을 다운받을 수 있습니다.
수정 버튼 클릭시 호출되는 함수는 다음과 같습니다.
<Button variant="contained" size="large" onClick={ () => {
let fileList = new Array();
{fileView.map( (file, index) => {
fileList.push({fileName: fileName[index], file: file.toString()});
})};
let data = {
keyValue: "ImageSize",
width: width,
height: height,
fileList: fileList
};
if(width == null || width == "" || height == "" || height == null){
handleSubmit("정확한 수치를 입력 해 주세요.");
return;
}
if(parseInt(width) <= 0 || parseInt(height) <= 0){
handleSubmit("0은 입력할 수 없습니다.");
return;
}
axios.post(API_URL + location.pathname, JSON.stringify(data), {
headers: {"Content-Type": "application/json"
}
}).then((res) => {
console.log("UUID " + res.data.result.userUUID);
console.log("ORI NAME " + res.data.result.oriFileName);
//setUserUUID(res.data.result.userUUID);
fileDownload(res.data.result.userUUID, res.data.result.oriFileName);
}).catch((err) => {
console.log("err: " + err);
});
}}>
수정
</Button>
hook fileView의 index 만큼 fileList를 설정 해 주고, 통신에 필요한 데이터를 만들어 줍니다. 정상적으로 통신에 성공했다면 fileDownload() 함수를 호출하여 처리하였습니다. (만약 동일한 프로세스라면, 1번의 통신으로 처리하는 것을 권장합니다.)
API 호출
먼저 요청별 UUID를 이용하여 폴더를 생성합니다. 그 후 화면에 입력했던 파일 사이즈에 대해 참조하여 이미지를 재생성 합니다. 만약 재생성 해야 할 이미지 파일이 2개 이상이라면 모든 이미지파일을 zip파일로 압축합니다. 그렇지 않으면 압축 프로세스를 진행하지 않습니다. imagesConverterPath 변수는 이미지가 저장되어있는 폴더 경로입니다.
// Controller
@RequestMapping(value="/imageSizeModify")
@ResponseBody
public ParamsDTO imageSizeModify(Model model, HttpServletRequest req, HttpServletResponse res
, @RequestBody ParamsDTO paramsDTO) throws Exception{
return solutionService.imageModifyService(paramsDTO);
}
// Service
public ParamsDTO imageModifyService(ParamsDTO paramsDTO) throws Exception{
ParamsDTO result = new ParamsDTO();
if(paramsDTO.getFileList() != null && paramsDTO.getFileList().size() > 0){
Map<String, Object> map = imageUtils.imagesBase64Parse(paramsDTO.getFileList(), paramsDTO.getWidth(), paramsDTO.getHeight());
result.setResult(map);
}
return result;
}
// Utils
public Map<String, Object> imagesBase64Parse(List<Map<String, Object>> imagesStrList, int width, int height) throws Exception {
// result Map
Map<String, Object> rMap = new HashMap<String, Object>();
// UUID path 생성
UUID uuid = UUID.randomUUID();
String userRootPath = imagesConverterPath + uuid;
String userUUID = uuid.toString();
String fileExtType = "";
rMap.put("userUUID", userUUID);
File folder = new File(userRootPath);
if(!folder.exists()){
folder.mkdir();
}
// 파일 사이즈 변환
for(Map<String, Object> imgData : imagesStrList){
String img = imgData.get("file").toString();
String oriFileName = imgData.get("fileName").toString();
String[] strings = img.split(",");
fileExtType = oriFileName;
String extension;
switch (strings[0]) {
case "data:image/jpeg;base64":
extension = "jpeg";
break;
case "data:image/png;base64":
extension = "png";
break;
default:
extension = "jpg";
break;
}
byte[] data = DatatypeConverter.parseBase64Binary(strings[1]);
String fullPath = userRootPath + File.separator + oriFileName;
File file = new File(fullPath);
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file));
outputStream.write(data);
outputStream.close();
Image imageRead = ImageIO.read(file);
Image resizeImage = imageRead.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB );
Graphics g = newImage.getGraphics();
g.drawImage(resizeImage, 0, 0, null);
g.dispose();
ImageIO.write(newImage, extension, new File(fullPath));
}
if(imagesStrList.size() > 1){
String folderPath = imagesConverterPath+userUUID;
String zipFilePath = imagesConverterPath+userUUID+File.separator+userUUID+".zip";
FileOutputStream fileOutputStream = new FileOutputStream(zipFilePath);
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
File file = new File(folderPath);
File[] files = file.listFiles();
for(File f : files){
FileInputStream fileInputStream = new FileInputStream(f);
String zipPath = f.getName();
int extIndex = zipPath.lastIndexOf(".");
String extCHK = zipPath.substring(extIndex + 1, zipPath.length());
// zip 파일이 아닌 것들만 압축한다.
if(!"ZIP".equals(extCHK.toUpperCase())){
System.out.println("zipPath " + zipPath);
ZipEntry zipEntry = new ZipEntry(zipPath);
zipOutputStream.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while((length = fileInputStream.read(bytes)) >= 0){
zipOutputStream.write(bytes, 0, length);
}
zipOutputStream.closeEntry();
fileInputStream.close();
}
}
// 2장 이상이면 압축파일로.
fileExtType = userUUID+".zip";
}
rMap.put("oriFileName", fileExtType);
return rMap;
}
API 통신이 완료되면 해당 UUID 데이터를 response 합니다. (단 파일이 1개면 UUID와 oriFileName 파일 이름의 데이터를 가지고 있습니다.)
{
"result":
{
"oriFileName": "c3218489-4ced-4d69-baab-3f0dd4620e50.zip",
"userUUID": "c3218489-4ced-4d69-baab-3f0dd4620e50"
}
}
이미지 생성 -> 변환 후 다운로드의 함수는 다음과 같습니다.
function fileDownload(userUUID, oriFileName) {
axios({
url: API_URL + "/download/imageSizeModifyDownload",
method: 'POST',
responseType: 'blob', // Important
params: {
userUUID: userUUID,
oriFileName: oriFileName
}
}).then((response) => {
console.log("oriFileName " + oriFileName);
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', oriFileName); // Set the file name
document.body.appendChild(link);
link.click();
link.remove();
}).catch((error) => {
console.error('Error downloading the file', error);
});
}
여기서 중요한 부분은 reponseType이 'blob' 이며, 호출 성공시 a 태그를 이용합니다.
다운로드 API 처리는 다음과 같습니다.
@RequestMapping(value="/download/imageSizeModifyDownload")
public ResponseEntity<InputStreamResource> imageSizeModifyDownload(ParamsDTO paramsDTO)
throws Exception{
LOG.info("File Download params ", paramsDTO.getUserUUID(), paramsDTO.getOriFileName());
Path filePath = Paths.get(dirExternalResource+paramsDTO.getUserUUID()+File.separator+paramsDTO.getOriFileName());
byte[] data = Files.readAllBytes(filePath);
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+paramsDTO.getOriFileName());
int ext = paramsDTO.getOriFileName().lastIndexOf(".");
String oriFileNameExt = paramsDTO.getOriFileName().substring(ext + 1, paramsDTO.getOriFileName().length());
if("PNG,JPG,JPEG".contains(oriFileNameExt.toUpperCase())){ // 이미지 파일이면
String mediaTypeImg = "image/"+oriFileNameExt;
MediaType mediaType = null;
if("PNG".equals(oriFileNameExt.toUpperCase())){
mediaType = MediaType.IMAGE_PNG;
} else {
mediaType = MediaType.IMAGE_JPEG;
}
headers.add(HttpHeaders.CONTENT_TYPE, mediaTypeImg);
return ResponseEntity.ok()
.headers(headers)
.contentLength(data.length)
.contentType(mediaType)
.body(resource);
} else { // 압축 파일이면
headers.add(HttpHeaders.CONTENT_TYPE, "application/zip");
return ResponseEntity.ok()
.headers(headers)
.contentLength(data.length)
.contentType(MediaType.valueOf("application/zip"))
.body(resource);
}
}
해당 파일이 PNG, JPG, JPEG, ZIP 일 때 해당 mediaType를 맞춰 response합니다. image/png,jpg,jpeg 그리고 application/zip 차이가 있습니다. return하는 부분의 코드가 중복되는 것이 있어 드러워 보입니다, 하나로 처리하는 것으로 하는 것을 권장합니다.
테스트 결과는 다음과 같습니다.
테스트
화면
파일 가공
파일 다운로드