스프링 이미지 업로드(1)

2023. 10. 30. 20:07Springboot

1. Requirements

  • Spring boot version 2.4.5  

 2. Stacks

  Springboot

  • Loombook
  • Spring Data JPA 
  • Spring Boot DevTools
  • Spring Security
  • Spring Web
  • Spring boot version 2.4.5  

  Mybatis

 

3. Code

       1. host.jsp

           - localhost:8080/host/host으로 접속했을 때 나오는 첫 페이지(이는 임시이며 바뀔 예정)

           - 해당 밑에 있는 코드는 인증된 정보에 접근하는 방법이며 필자는 모든 jsp파일의 header.jsp에

             적어줌으로써 다른 jsp파일은 안 적혀있습니다.

          - ${principal}에 해당하는 변수를 사용할 시 꼭 필요합니다.

          - 해당 jsp는 form형태를 post방식으로 보냅니다.

          - 해당 페이지는 Springboot package com.pjh.company.config부분에 void configure(HttpSecurity http)에

            http.authorizeRequest().antMatchers("/host.**")를 통해 로그인이 되지 않으면 접근 불가능하게 설계되어있습니다.

          - 해당 페이지는 버튼을 누르면 모달이 켜지는 방식으로 설계되어있습니다.

          - 이는 추후에 업로드를 버튼을 누르면 db 저장되어있는 정보를 카드형식으로 출력되어질 예정입니다.

          - <style></style> 관련하여 추후 변경 예정입니다.

<!DOCTYPE html>
<html lang="en">

<head>
    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>pppp</title>
    <link rel="stylesheet" href="/css/style.css">
    <link rel="shortcut icon" sizes="76x76" type="image/x-icon"
        href="https://a0.muscache.com/airbnb/static/logotype_favicon-21cc8e6c6a2cca43f061d2dcabdf6e58.ico">
</head>
<style>
    .host-cards {
        width: 900px;
        margin: 20px auto 20px auto;
    }

    .card {
        border-radius: 15px;
    }

    .card>button {
        margin: 10px;
    }

    .card>img {
        border-top-left-radius: 10px;
        border-top-right-radius: 10px;
        height: 200px;
        padding: 5px;
    }

    .card-text {
        margin: 10px auto 20px auto;
    }

    .myTeamMember {
        background-color: white;
        width: 500px;
        margin: 20px auto 0px auto;
        border: 1px gray;
        padding: 20px;
        box-shadow: 2px 2px 2px 2px gray;
        border-radius: 5px;
    }
</style>

<body>
    <button id="openModal">파일 업로드 모달 열기</button>
    <div id="myModal" class="modal">
        <div class="modal-content">
            <span class="close-button" id="closeModal">X</span>
            <form class="upload-form" action="/image" method="post" enctype="multipart/form-data">  
                <div class="upload-form-detail">
                    <input type="text" placeholder="제목" name="title"/>
                    <input type="textarea" placeholder="설명" name="description"/>
                    <input type="file" name="file"  onchange="imageChoose(this)"/>
                <button class="cta blue">업로드</button>
                </div>
             </form>
        </div>
    </div>
</body>
</html>
<script src="/js/host.js" ></script>

 

       2. host.js

           - javascript에서 받은 form형태를 fetch의 비동기 통신을 통하여 post방법으로 controller로 보냅니다.

           - 해당 $. fetch({}) 안에 존재하는 url부분은 홑따옴표('')가 아닌 빽틱(``)입니다. 

document.addEventListener("DOMContentLoaded", function () {
    var button = document.querySelector("#openModal");
    var modal = document.querySelector("#myModal");
    var closeModal = document.querySelector("#closeModal");
    var signupLink = document.querySelector("#signupLink");

    button.addEventListener("click", function () {
        modal.style.display = "block";
    });

    closeModal.addEventListener("click", function () {
        modal.style.display = "none";
    });

    signupLink.addEventListener("click", function () {
        modal.style.display = "none";
    });
});

function imageChoose(input) {
    const file = input.files[0];
    if (file) {
        // You can perform actions with the selected file here
        console.log("Selected file: " + file.name);
    }
}

// Function to handle file upload
document.getElementById("uploadButton").addEventListener("click", function() {
    // Get the form and its data
    const form = document.getElementById("uploadForm");
    const formData = new FormData(form);

    // Send the form data to the Spring Controller
    fetch('/image', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        // Handle the response from the server
        console.log(data);
    })
    .catch(error => {
        // Handle any errors
        console.error(error);
    });
});

 

      3. application.yml

          - springboot를 실행할 때 기본적으로 필요한 요소를 적음

          - datasource는 mariadb를 직접 지정해야 사용할 수 있음

          - 변경점 없음

server:
  port: 8080(자신이 7070으로 설정했다면 localhost:7070으로 들어가야함)				
  servlet:
    context-path: /
    encoding:
      charset: utf-8
      enabled: true
    
spring:
  mvc:
    view:
      prefix: (src/main밑에 있는 폴더명에 저장)
      suffix: .jsp
      
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:(mybatis 연결포트)/(자신이 연결한 데이터베이스 이름)?serverTimezone=Asia/Seoul
    username: (자신이 만든 아이디)
    password: (자신이 만든 비밀번호)
    
  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
      
  servlet:
    multipart:
      enabled: true
      max-file-size: 2MB

  security:
    user:
      name: test
      password: 1234

   

  4. ImageController.java

          - @RequiredArgsConstrutor : private final키워드의 생성자를 사용할 수 있게 합니다.(의존성 주입)

          - 이미지를 전송하는 것이라 @RestController로 착각할 수 있으나, 이는 파일을 리턴할 것이기에 @Controller 사용을 합니다.

package com.pjh.web;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.pjh.config.auth.PrincipalDetails;
import com.pjh.handler.ex.CustomValidationException;
import com.pjh.service.ImageService;
import com.pjh.web.dto.image.ImageUploadDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class ImageController {
	
	private final ImageService imageService;
	
	@PostMapping("/image")
	public String imageUpload(ImageUploadDto imageUploadDto,
			@AuthenticationPrincipal PrincipalDetails principalDetails) {
		// 서비스 호출
		if(imageUploadDto.getFile().isEmpty()) {
			throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
		}
		
		imageService.사진업로드(imageUploadDto, principalDetails);
		return "redirect:/host/host";
		// 파일을 리턴하기 때문에 apiContrller메 사용하지 않음.
	}
}

  5. CustomValidationException.java

          - ImageController.java에서 !imageUploadDto.getFile().isEmpty()일때 

            오류상황을 표기하기 위해 exception처리를 합니다.

package com.pjh.handler.ex;

import java.util.Map;

public class CustomValidationException extends RuntimeException{
	
	// 객체 구분 할때
	private static final long serialVersionUID = 1L;
	
	private Map<String, String> errorMap;
	
	public CustomValidationException(String message, Map<String,String> errorMap){
		super(message);
		this.errorMap = errorMap;
	}
	
	
	public Map<String, String> getErrorMap(){
		return this.errorMap;
	}
}

 

  6. ControllerExceptionHandler.java

             - 모든 Exception을 처리해주는 핸들러입니다.

             - CustomValidationException을 새로 정의했기에 새롭게 만들어줍니다.

package com.pjh.handler;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

import com.pjh.handler.ex.CustomApiException;
import com.pjh.handler.ex.CustomValidationApiException;
import com.pjh.handler.ex.CustomValidationException;
import com.pjh.util.Script;
import com.pjh.web.dto.CMRespDto;

@RestController				// 데이터 리턴
@ControllerAdvice			// 모든 exception을 낚아챔
public class ControllerExceptionHandler {
	
	@ExceptionHandler(CustomValidationException.class)
	// RuntimeException이 발생하면 모든 페이지가 낚아챔
	public String validationException(CustomValidationException e) {
		return Script.back(e.getErrorMap().toString());
		
		//return new CMRespDto(-1, e.getMessage(), e.getErrorMap());
		// 제네릭 타입 리턴은 <?>가 편하다
		// 응답에 실패했고 에러메세지와 각각의 fieldError와 defaultMessage 리턴
	}
	
	@ExceptionHandler(CustomValidationApiException.class)
	public ResponseEntity<?> validationApiException(CustomValidationApiException e){
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), e.getErrorMap()),HttpStatus.BAD_REQUEST);
	}
	
	@ExceptionHandler(CustomApiException.class)
	public ResponseEntity<?> apiException(CustomApiException e){
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(),null), HttpStatus.BAD_REQUEST);
	}
}

 

       7. CMRespDto.java

             - 해당 파일은 공통적으로 발생하는 응답에 대한 dto입니다.

            - 변경점 없음

package com.pjh.comapny.web.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> {
	private int code;	// 1(성공), -1(실패)
	private String message;
	private T data;
}

        8. ImageUploadDto.java

             - host.jsp에서 form형식에 담긴 title, description, multipartfile을 처리하기 위해 만들어진 dto입니다.

package com.pjh.web.dto.image;

import org.springframework.web.multipart.MultipartFile;

import com.pjh.domain.image.Image;
import com.pjh.domain.user.User;

import lombok.Data;

@Data
public class ImageUploadDto {
	
	private MultipartFile file;
	
	private String title;
	private String description;
	private String postImageUrl;
	
	
	public Image toEntity(String postImageUrl, User user) {
		return Image.builder()
				.title(title)
				.description(description)
				.postImageUrl(postImageUrl)		// uuid_getFile().getOriginalFileName()
				.user(user)
				.build();
	}
}

 

        9. ImageService.java

             - @Service 비즈니스 로직을 구현을 위해 생성하는 service 어노테이션입니다.
             - @Transactional DB에 트랜잭션(일의 최소 단위) 처리를 springboot에서 구현한 형태로 수행할 수 있게 합니다.
             - UUID를 지정하여 같은 이미지 파일의 업로드 중복을 엄청나게 높은 확률로 제거합니다.

               ex) 1.jpg가 업로드가 중복될 수 있기에 UUID.getRandom()를 활용하여

                   e5ab7114-de57-4ae5-9963-93351602bb72_1.jpg로 저장하게 만듦.

 

package com.pjh.service;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.pjh.config.auth.PrincipalDetails;
import com.pjh.domain.image.Image;
import com.pjh.domain.image.ImageRepository;
import com.pjh.web.dto.image.ImageUploadDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor			// DI
@Service
public class ImageService {
	
	private final ImageRepository imageRepository;
	
	@Value("${file.path}")
	private String uploadFolder;
	// application.yml에 해당하는 file : path :로 들어옴
	
	public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails) {
		UUID uuid = UUID.randomUUID();
		String imageFileName = uuid+"_"+imageUploadDto.getFile().getOriginalFilename();
		// 업로드하려는 파일 네임이 String으로 들어옴
		
		System.out.println("이미지 파일이름 : "+imageFileName);
		Path imageFilePath = Paths.get(uploadFolder+imageFileName);
		// 실제 저장 장소
		
		try {
			Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
		}catch(Exception e) {
			e.printStackTrace();
		}
		// 통신이 일어나거나 I/O이 일어날때 -> 예외가 발생할 수 있음.
		
		Image image = imageUploadDto.toEntity(imageFileName, principalDetails.getUser());
		// toEntity() -> db에 해당 toEntity화한 imageFileName, princiaplDetails.getUser()를 저장하겠다.
		// db에 업로드를 진행한 유저아이디 + 업로드한 파일을 저장
		imageRepository.save(image);
		
		// System.out.println(imageEntity.toString());
		// 무한 참조 조심, User객체에 Image -> Image객체에 User 
		
		
	}
}

        10. ImageRepository

            - DB와 직접 맞닿아 있는 인터페이스입니다.

            - DB에 처리할 SQL문을 직접 사용할 수 있고 extends 하여 JpaRepository의 crud형태의 기본적인 연산이 지원됩니다.

package com.pjh.domain.image;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ImageRepository extends JpaRepository<Image, Integer>{

}

 

     

'Springboot' 카테고리의 다른 글

스프링 이미지 업로드(2)  (0) 2023.11.07
스프링 메모장  (1) 2023.11.01
스프링 회원 수정  (1) 2023.10.23
스프링 회원가입(2)  (1) 2023.10.23
스프링 회원가입(1)  (2) 2023.10.20