티스토리 뷰

728x90

1. 파일 업로드를 위해서 우선 파일을 선택할 template이 필요하다.

  1-1 아래는 thymeleaf로 작성된 form으로 중요한 부분은 form 테그 부분이다.

  1-2 input type이 file이고 전송될 파일의 이름은 name에 지정된 imageFile이 된다.

  1-3 파일이므로 post로 메소드를 설정하고 전송할 action에는 파일을 수신처리할 url을 지정한다.

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
    integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"
    th:href="@{/webjars/bootstrap/4.5.0/css/bootstrap.min.css}">

  <title>Recipe Homer</title>
</head>

<body>
  <!--/*@thymesVar id="recipe" type="pe.pilseong.recipe.domain.Recipe"*/-->
  <div class="container-fluid" style="margin-top: 20px">
    <div class="row">
      <div class="col-md-6 offset-md-3">
        <div class="card text-white bg-primary">
          <div class="card-body p-0">
            <div class="card-title">
              <p class="m-2 font-weight-bold">Upload a new recipe image</p>
            </div>
            <div class="card-text bg-white m-0 p-2">
              <form action="http:\\localhost" method="post" enctype="multipart/form-data"
                th:action="@{'/recipe/' + ${recipe.getId()} + '/image'}">
                <div class="form-group">
                  <label class="text-dark">Select File</label>
                  <input id="imageFile" name="imageFile" type="file" 
                    class="form-control border border-white p-0 m-0">
                </div>
                <div class="text-center">
                  <button type="submit" class="btn btn-primary">Submit</button>
                </div>         
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- Optional JavaScript -->
  <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
    integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"
    th:src="@{/webjars/jquery/3.5.1/jquery.min.js}">
  </script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
    integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
    th:src="@{/webjars/popper.js/1.16.0/popper.min.js}">
  </script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
    integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"
    th:src="@{/webjars/bootstrap/4.5.0/js/bootstrap.min.js}">
  </script>

</body>

</html>

 

  1-4 실행화면

 

 

2. 파일을 전송하였으면 수신할 controller가 필요하다.

  2-1 아래의 ImageController에는 3가지 메소드가 있다.

  2-2 showUploadImageForm 메소드는 이미지를 선택할 ui template을 보내준다. 위의 html 코드가 실행된 view이다.

  2-3 handleImagePost 메소드는 이미지를 선택하고 form을 submit했을 때 수신하는 메소드이다.

    2-3-1 이 메소드는 수신한 파일을 데이터베이스에 저장하고 redirect하는 기능을 가지고 있다.

  2-4 renderImageFromDB 메소드는 html에서 src로 요청한 image를 보내준다.

    2-4-1 DB에서 받아온 image를 response의 outputstream으로 보내주는 것이 핵심이다.

    2-4-2 image는 Byte[]이고 sream은 byte만 처리하므로 변환이 필요하다.

    2-4-3 IOUtils tomcat에 포함되어있는 stream 복사를 위한 편리한 기능을 지원한다.

 

package pe.pilseong.recipe.controller;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.RecipeCommand;
import pe.pilseong.recipe.service.ImageService;
import pe.pilseong.recipe.service.RecipeService;

@Controller
@RequiredArgsConstructor
@Slf4j
public class ImageController {

  private final RecipeService recipeService;
  private final ImageService imageService;

  @GetMapping("/recipe/{recipeId}/image")
  public String showUploadImageFormn(@PathVariable("recipeId") Long recipeId, Model model) {

    model.addAttribute("recipe", recipeService.findCommandById(recipeId));

    return "recipe/imageUploadForm";
  }

  @PostMapping("/recipe/{recipeId}/image")
  public String handleImagePost(@PathVariable("recipeId") Long recipeId,
      @RequestParam("imageFile") MultipartFile file) {

    log.debug("handleImagePost in ImageController with multipart size :: " + file.getSize());

    imageService.saveImageFile(recipeId, file);

    return "redirect:/recipe/" + recipeId + "/show";
  }

  @GetMapping("/recipe/{recipeId}/recipeimage")
  public void renderImageFromDB(@PathVariable("recipeId") Long recipeId, HttpServletResponse response)
      throws IOException {

    log.debug("renderImageFromDB in IamgeController");
    RecipeCommand recipeCommand = recipeService.findCommandById(recipeId);

    if (recipeCommand.getImage() != null) {
      byte[] byteArray = new byte[recipeCommand.getImage().length];

      int index = 0;
      for (byte b : recipeCommand.getImage()) {
        byteArray[index++] = b;
      }

      response.setContentType("image/jpeg");
      InputStream is = new ByteArrayInputStream(byteArray);
      IOUtils.copy(is, response.getOutputStream());
    }
  }

}

 

3. 이미지를 저장하기 위한 서비스

  3-1 아래 코드는 위의 controller에서 사용한 image service 이다.

  3-2 saveImageFile은 이미지를 저장하는 로직이다.

    3-2-1 중요한 부분은 Recipe의 image가 Byte[] 인데 MultipartFile은 byte[]이라 변환이 필요하다.

    3-2-2 JPA의 Entity의 타입은 Wrapper타입이 권장사항이라 이런 귀찮은 부분이 필요하다.

 

package pe.pilseong.recipe.service;

import org.springframework.web.multipart.MultipartFile;

public interface ImageService {
  void saveImageFile(Long recipeId, MultipartFile file);
}

 

package pe.pilseong.recipe.service;

import java.io.IOException;
import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.repository.RecipeRepository;

@Service
@Slf4j
@RequiredArgsConstructor
public class ImageServiceImpl implements ImageService {

  private final RecipeRepository recipeRepository;

  @Override
  public void saveImageFile(Long recipeId, MultipartFile file) {
    log.debug("saveImageFile in ImageService");

    Optional<Recipe> recipeOptioanl = recipeRepository.findById(recipeId);

    Recipe recipe = null;
    if (recipeOptioanl.isPresent()) {
      try {
        recipe = recipeOptioanl.get();
        Byte[] imageBytes = new Byte[file.getBytes().length];
        int index = 0;
        for (byte b: file.getBytes()) 
          imageBytes[index++]=b;
        
        recipe.setImage(imageBytes);
      } catch (IOException e) {
        e.printStackTrace();
      }
      recipeRepository.save(recipe);
    } else {
      throw new RuntimeException("Recipe not found with id :: " + recipeId);
    }
  }

}

 

4. 페이지를 표출하는 부분이다.

  4-1 역시 타임리프로 작성된 코드로 신경쓸 부분은 th:src로 지정하고 있는 이미지를 로딩하는 부분이다.

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
    integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"
    th:href="@{/webjars/bootstrap/4.5.0/css/bootstrap.min.css}">

  <title>Recipe Homer</title>
</head>

<body>
  <!--/*@thymesVar id="recipe" type="pe.pilseong.recipe.domain.Recipe"*/-->
  <div class="container-fluid" style="margin-top: 20px">
    <div class="row">
      <div class="col-md-8 offset-md-2">
        <div class="card text-white bg-primary">
          <div class="card-body p-0">
            <div class="card-title d-flex my-1">
              <p class="m-2 font-weight-bold" th:text="${recipe.description}">Recipe Description Here!</p>
              <a class="btn btn-secondary" href="#" th:href="@{'/recipe/' + ${recipe.id} + '/image'}">Change Image</a>
            </div>
            <div class="card-text bg-white text-dark m-0 p-2">
              <div class="row">
                <div class="col-md-3">
                  <h5>Categories:</h5>
                </div>
                <div class="col-md-3">
                  <ul>
                    <li th:remove="all">cat one</li>
                    <li th:remove="all">cat two</li>
                    <li th:remove="all">cat three</li>
                    <li th:each="category : ${recipe.categories}" th:text="${category.description}">cat four</li>
                  </ul>
                </div>
                <div class="col-md-6">
                  <img src="../../static/images/guacamole400x400WithX.jpg"
                    th:src="@{'/recipe/' + ${recipe.id} + '/recipeimage'}" width="200" height="200" >
                </div>
              </div>
              <div class="row">
                <div class="col-md-3">
                  <h5>Prep Time:</h5>
                </div>
                <div class="col-md-3">
                  <p th:text="${recipe.prepTime} + ' min'">30 min</p>
                </div>
                <div class="col-md-3">
                  <h5>Difficulty:</h5>
                </div>
                <div class="col-md-3">
                  <p th:text="${recipe.difficulty}">Easy</p>
                </div>
              </div>
              <div class="row">
                <div class="col-md-3">
                  <h5>Cooktime:</h5>
                </div>
                <div class="col-md-3">
                  <p th:text="${recipe.cookTime} + ' min'">30 min</p>
                </div>
                <div class="col-md-3">
                  <h5>Servings:</h5>
                </div>
                <div class="col-md-3">
                  <p th:text="${recipe.servings}">4</p>
                </div>
              </div>
              <div class="row">
                <div class="col-md-3">
                  <h5>Source:</h5>
                </div>
                <div class="col-md-3">
                  <p th:text="${recipe.source}">30 min</p>
                </div>
                <div class="col-md-3">
                  <h5>URL:</h5>
                </div>
                <div class="col-md-3">
                  <p th:text="${recipe.url}">http://www.example.com</p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="card text-white bg-primary mt-3">
          <div class="card-body p-0">
            <div class="card-title d-flex my-1 justify-content-between">
              <p class="m-2 font-weight-bold">Ingredients</p>
              <a class="btn btn-success" th:href="@{'/recipe/' + ${recipe.id} + '/ingredients'}">View</a>
            </div>
            <div class="card-text bg-white text-dark m-0 p-2">
              <div class="row">
                <div class="col-md-12">
                  <ul>
                    <li th:remove="all">1 Cup of milk</li>
                    <li th:remove="all">1 Teaspoon of chocolate</li>
                    <li th:remove="all">1 Teaspoon of Sugar</li>
                    <li th:each="ingredient : ${recipe.ingredients}"
                        th:text="${ingredient.amount + ' '
                        + ingredient.uom.description + ' of ' 
                        + ingredient.description}">1 Teaspoon of Doggy</li>
                  </ul>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="card text-white bg-primary mt-3">
          <div class="card-body p-0">
            <div class="card-title">
              <p class="m-2 font-weight-bold">Directions</p>
            </div>
            <div class="card-text bg-white text-dark m-0 p-2">
              <div class="row">
                <div class="col-md-12">
                  <p th:text="${recipe.directions}">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean
                    massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
                    quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
                    Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut,
                    imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt.
                    Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula,
                    porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis,
                    feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean
                    imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam
                    rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet
                    adipiscing sem neque sed ipsum.</p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="card text-white bg-primary mt-3">
          <div class="card-body p-0">
            <div class="card-title">
              <p class="m-2 font-weight-bold">Notes</p>
            </div>
            <div class="card-text bg-white text-dark m-0 p-2">
              <div class="row">
                <div class="col-md-12">
                  <p th:text="${recipe.note.recipeNote}">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean
                    massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
                    quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
                    Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut,
                    imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt.
                    Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula,
                    porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis,
                    feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean
                    imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam
                    rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet
                    adipiscing sem neque sed ipsum.</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- Optional JavaScript -->
  <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
    integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"
    th:src="@{/webjars/jquery/3.5.1/jquery.min.js}">
  </script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
    integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
    th:src="@{/webjars/popper.js/1.16.0/popper.min.js}">
  </script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
    integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"
    th:src="@{/webjars/bootstrap/4.5.0/js/bootstrap.min.js}">
  </script>

</body>

</html>

 

5. 실행화면

 

728x90
댓글