Front-End/React, React Native

React, Spring Boot, 이미지 base64 변환, 이미지 수정, 파일 압축, 파일 다운로드 구현 방법 (ResponseEntity, Axios, API Request, React, image base64)

개발자 DalBy 2024. 6. 4. 15:56
반응형

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하는 부분의 코드가 중복되는 것이 있어 드러워 보입니다, 하나로 처리하는 것으로 하는 것을 권장합니다.

 

테스트 결과는 다음과 같습니다.

 

테스트

화면

파일 2개 업로드
파일 2개 업로드

 

파일 가공

파일 가공
파일 가공

 

파일 다운로드

 

 

 



반응형