티스토리 뷰
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. 실행화면
'Spring > Spring Boot' 카테고리의 다른 글
Spring Boot : Spring Boot 과 JSP 사용하는 프로젝트 템플릿 (0) | 2021.06.28 |
---|---|
Spring Boot : @ResponseStatus, @ExceptionHandler (0) | 2020.08.08 |
Spring Boot : Auto Configuration (0) | 2020.07.23 |
Spring Boot : CommandLineRunner (0) | 2020.07.10 |
Spring Boot : WebJars 사용하기 (0) | 2020.07.07 |
- Total
- Today
- Yesterday
- 도커 개발환경 참고
- AWS ARN 구조
- Immuability에 관한 설명
- 자바스크립트 멀티 비동기 함수 호출 참고
- WSDL 참고
- SOAP 컨슈머 참고
- MySql dump 사용법
- AWS Lambda with Addon
- NFC 드라이버 linux 설치
- electron IPC
- mifare classic 강의
- go module 관련 상세한 정보
- C 메모리 찍어보기
- C++ Addon 마이그레이션
- JAX WS Header 관련 stackoverflow
- SOAP Custom Header 설정 참고
- SOAP Custom Header
- SOAP BindingProvider
- dispatcher 사용하여 설정
- vagrant kvm으로 사용하기
- git fork, pull request to the …
- vagrant libvirt bridge network
- python, js의 async, await의 차이
- go JSON struct 생성
- Netflix Kinesis 활용 분석
- docker credential problem
- private subnet에서 outbound IP 확…
- 안드로이드 coroutine
- kotlin with, apply, also 등
- 안드로이드 초기로딩이 안되는 경우
- navigation 데이터 보내기
- 레이스 컨디션 navController
- raylib
- Spring
- spring boot
- 스프링부트
- Rest
- 스프링
- Validation
- 상속
- Security
- WebMvc
- crud
- 설정
- one-to-many
- MYSQL
- mapping
- one-to-one
- form
- 외부파일
- XML
- Angular
- 매핑
- jsp
- 하이버네이트
- 자바
- RestTemplate
- Spring Security
- Many-To-Many
- hibernate
- 로그인
- login
- 설정하기