# Upload파일 설정
# 파일 하나의 최대사이즈 10MB (-1 이면 무제한)
spring.servlet.multipart.file-size-threshold = 10MB
# 여러 파일 업로드 시 전체 합 30MB (-1 이면 무제한)
spring.servlet.multipart.max-request-size = 30MB
package com.myapp.shoppingmall.entities;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.Data;
//실제 Table과 매칭
@Entity
@Table(name = "products")
@Data // Get, Set, Construct, toSring 생성됨
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NotBlank(message = "상품명을 입력해주세요.")
@Size(min = 2, message = "최소 2글자 이상 입력해주세요.")
private String name;
private String slug;
@Size(min = 5, message = "최소 5자 이상 입력해주세요.")
private String description; // 상품 설명
private String image; // 상품 이미지 파일 이름
@Pattern(regexp = "^[1-9][0-9]*") // 맨앞자리 1~9만 사용가능, 나머지는 0~9 사용가능 숫자만 1 ~ 99999999 이런 식으로 표현 가능
private String price; // 문자열로 하고 int로 변환해서 사용
@Pattern(regexp = "^[1-9][0-9]*", message = "카테고리를 선택해주세요") // 생성 후 Update는 안됨
@Column(name = "category_id", updatable = false)
private String categoryId; // 상품의 카테고리 id
@Column(name = "created_at")
@CreationTimestamp // Insert 시 자동으로 날짜 입력
private LocalDateTime createdAt; // 상품 등록 날짜
@Column(name = "updated_at")
@UpdateTimestamp // Update 시 자동으로 날짜 입력
private LocalDateTime updatedAt; // 상품 수정 날짜
}
https://velog.io/@koo8624/Spring-CreationTimestamp-UpdateTimestamp
@GetMapping
public String index(Model model) {
List<Product> products = productRepo.findAll();
List<Category> categories = categoryRepo.findAll();
// Category id와 name을 map에 담아 index페이지에 전송
HashMap<Integer, String> cateIdAndName = new HashMap<>();
for (Category category : categories) {
cateIdAndName.put(category.getId(), category.getName());
}
model.addAttribute("products", products);
model.addAttribute("cateIdAndName", cateIdAndName);
return "/admin/products/index";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/fragments/head :: head-admin"></head>
<body>
<nav th:replace="/fragments/nav :: nav-admin"></nav>
<main role="main" class="container">
<div class="display-2">Products</div>
<a th:href="@{/admin/products/add}" class="btn btn-primary my-3">추가하기</a>
<div th:if="${message}" th:object="${message}" th:text="${message}" th:class="'alert ' + ${alertClass}"></div>
<div th:if="${!products.empty}">
<table class="table sorting" id="products">
<tr>
<th>상품명</th>
<th>이미지</th>
<th>카테고리</th>
<th>가 격</th>
<th>수 정</th>
<th>삭 제</th>
</tr>
<tr th:each="product : ${products}">
<td th:text="${product.name}"></td>
<td>
<!-- 이미지 파일이름이 한글이면 이미지 파일을 찾지를 못 함, 영어로 해야함-->
<img th:src="@{'/media/' + ${product.image}}" style="height: 2em" />
</td>
<!-- Thymeleaf문법에서 ${}안에 한번 더 ${} 사용하려면 중복사용하는 곳 앞,뒤에 __(언더바) 2번 앞에다가 입력해줘야함-->
<td th:text="${cateIdAndName[__${product.categoryId}__]}"></td>
<td th:text="${product.price} + '원'"></td>
<td><a th:href="@{'/admin/products/edit/' + ${product.id}}">수정</a></td>
<td><a th:href="@{'/admin/products/delete/' + ${product.id}}" class="deleteConfirm">삭제</a></td>
</tr>
</table>
</div>
<div th:if="${products.empty}">
<div class="display-4">현재 페이지가 없습니다.</div>
</div>
</main>
<footer th:replace="/fragments/footer :: footer"></footer>
</body>
</html>
@GetMapping("/add")
public String add(@ModelAttribute Product product, Model model) {
List<Category> categories = categoryRepo.findAll();
model.addAttribute("categories", categories);
// model.addAttribute("product", new Product()); // @ModelAttribute Product product 동일함
// 상품을 추가하는 add 페이지에 product 객체와 product의 category를 선택할 수 있게 리스트 전달
return "/admin/products/add";
}
@PostMapping("/add")
public String add(@Valid Product product, BindingResult bindingResult, MultipartFile file, RedirectAttributes attr,
Model model) throws IOException { // 유효성검사 실패시 bindingResult로 데이터 넘어옴, 파일은 따로 받아서 정리
if (bindingResult.hasErrors()) {
List<Category> categories = categoryRepo.findAll();
model.addAttribute("categories", categories);
return "/admin/products/add";
}
boolean fileOk = false;
byte[] bytes = file.getBytes(); // Upload된 이미지 파일의 데이터
String fileName = file.getOriginalFilename(); // Upload된 파일의 이름
Path path = Paths.get("src/main/resources/static/media/" + fileName); // 파일을 저장할 위치와 이름까지
if (fileName.endsWith("jpg") || fileName.endsWith("png")) {
fileOk = true; // 확장자가 .jpg, .png 만 등록 가능
}
// 성공적으로 추가됨
attr.addFlashAttribute("message", "상품이 성공적으로 등록되었습니다.");
attr.addFlashAttribute("alertClass", "alert-success");
// slug 만들기
String slug = product.getName().toLowerCase().replace(" ", "-");
// 똑같은 상품명이 있는지 확인
Product prductExists = productRepo.findByName(product.getName());
if (!fileOk) { // 파일 Upload가 안됬거나 확장자가 jpg, png가 아님
attr.addFlashAttribute("message", "이미지는 jpg나 png 확장자를 사용해주세요");
attr.addFlashAttribute("alertClass", "alert-danger");
attr.addFlashAttribute("product", product);
} else if (prductExists != null) {
attr.addFlashAttribute("message", "이미 등록된 상품입니다. 상품명을 변경해주세요.");
attr.addFlashAttribute("alertClass", "alert-danger");
attr.addFlashAttribute("product", product);
} else { // product와 image를 저장한다.
product.setSlug(slug);
product.setImage(fileName); // 이미지는 파일의 이름만 입력(주소는 /media/폴더 이므로 동일)
productRepo.save(product); // product 저장
Files.write(path, bytes); // (이미지파일이 저장될 이미지파일 이름이 포함된 주소, 파일데이터)
}
return "redirect:/admin/products/add";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/fragments/head :: head-admin"></head>
<body>
<nav th:replace="/fragments/nav :: nav-admin"></nav>
<main role="main" class="container">
<div class="display-2">상품 등록</div>
<a th:href="@{/admin/products}" class="btn btn-primary my-3">돌아가기</a>
<!-- enctype="multipart/form-data" 파일이나 이미지전송에 필요-->
<form method="post" enctype="multipart/form-data" th:object="${product}" th:action="@{/admin/products/add}">
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">에러 발생</div>
<div th:if="${message}" th:class="${'alert ' + alertClass}" th:text="${message}"></div>
<div class="form-group">
<label for="">상품명</label>
<input type="text" class="form-control" th:field="*{name}" placeholder="상품명" />
<span class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
</div>
<div class="form-group">
<label for="">상품설명</label>
<input type="text" class="form-control" th:field="*{description}" placeholder="상품설명" />
<span class="error" th:if="${#fields.hasErrors('description')}" th:errors="*{description}"></span>
</div>
<label for="">이미지</label>
<div class="form-group">
<img src="#" id="imgPreview" />
<input class="form-control" type="file" th:id="file" th:name="file" />
</div>
<div class="form-group">
<label for="">가 격</label>
<input type="text" class="form-control" th:field="*{price}" placeholder="가격(원)" />
<span class="error" th:if="${#fields.hasErrors('price')}" th:errors="*{price}"></span>
</div>
<div class="form-group">
<label for="">카테고리</label>
<select th:field="*{categoryId}" class="form-control">
<option value="0">카테고리 선택</option>
<option th:each="category : ${categories}" th:value="${category.id}" th:text="${category.name}"></option>
</select>
<span class="error" th:if="${#fields.hasErrors('categoryId')}" th:errors="*{categoryId}"></span>
</div>
<button type="submit" class="btn btn-danger">추 가</button>
</form>
</main>
<footer th:replace="/fragments/footer :: footer"></footer>
<script>
$(function () {
$('#imgPreview').hide(); // 처음엔 숨김
$('#file').change(function () {
// 파일이 변경되었을 때(새 등록 or 파일교체)
readURL(this); // readURL 함수 실행
});
});
function readURL(input) {
// 파일(이미지) 있을 경우에 실행
if (input.files && input.files[0]) {
let reader = new FileReader(); // 파일리더 객체 생성
reader.readAsDataURL(input.files[0]); // 파일리더로 첫번째 파일 경로 읽기
// reader가 주소를 다읽으면 onload 이벤트가 발생하고 이 때 화면에 사진을 표시한다.
reader.onload = function (e) {
$('#imgPreview').attr('src', e.target.result).width(50).height(50).show();
};
}
}
</script>
</body>
</html>
@GetMapping("/edit/{id}")
public String edit(@PathVariable("id") int id, Model model) {
Product product = productRepo.getById(id);
List<Category> categories = categoryRepo.findAll();
model.addAttribute("product",product);
model.addAttribute("categories",categories);
return "admin/products/edit";
}
@PostMapping("/edit")
public String edit(@Valid Product product, BindingResult bindingResult,
MultipartFile file, RedirectAttributes attr, Model model) throws IOException { // 유효성검사 실패시 bindingResult로 데이터 넘어옴, 파일은 따로 받아서 정리
// 미리 id로 수정하기전의 상품 정보를 불러옴
Product currentProduct = productRepo.getById(product.getId());
if (bindingResult.hasErrors()) {
List<Category> categories = categoryRepo.findAll();
model.addAttribute("categories", categories);
product.setImage(currentProduct.getImage());
return "/admin/products/edit";
}
boolean fileOk = false;
byte[] bytes = file.getBytes(); // Upload된 이미지 파일의 데이터
String fileName = file.getOriginalFilename(); // Upload된 파일의 이름
Path path = Paths.get("src/main/resources/static/media/" + fileName); // 파일을 저장할 위치와 이름까지
if(!file.isEmpty()) { // 이미지 파일이 존재하면
if (fileName.endsWith("jpg") || fileName.endsWith("png")) {
fileOk = true; // 확장자가 .jpg, .png 만 등록 가능
}
} else { // 이미지는 수정 안함
fileOk = true; // 기존 이미지 사용함
}
// 성공적으로 수정됨
attr.addFlashAttribute("message", "상품이 성공적으로 수정되었습니다.");
attr.addFlashAttribute("alertClass", "alert-success");
String slug = product.getName().toLowerCase().replace(" ", "-");
// 이름으로 찾고 현재 product의 id 값을 제외한 데이터만 검색
Product prductExists = productRepo.findByNameAndIdNot(product.getName(), product.getId());
if (!fileOk) {
attr.addFlashAttribute("message", "이미지는 jpg나 png 확장자를 사용해주세요");
attr.addFlashAttribute("alertClass", "alert-danger");
attr.addFlashAttribute("product", product);
} else if (prductExists != null) {
attr.addFlashAttribute("message", "이미 등록된 상품입니다. 상품명을 변경해주세요.");
attr.addFlashAttribute("alertClass", "alert-danger");
attr.addFlashAttribute("product", product);
} else { // product와 image를 저장한다.
product.setSlug(slug);
if(!file.isEmpty()) {
Path currentpath = Paths.get("src/main/resources/static/media/" + currentProduct.getImage());
Files.delete(currentpath); // 새로운 이미지파일이 있기 때문에 기존 파일을 삭제
product.setImage(fileName);
Files.write(path, bytes); // 새 이미지 파일 저장
} else {
product.setImage(currentProduct.getImage());
}
productRepo.save(product);
}
return "redirect:/admin/products/edit/" + product.getId();
}
@GetMapping("delete/{id}")
public String delete(@PathVariable("id") int id, RedirectAttributes attr) throws IOException {
// id로 상품을 삭제하기 전에 먼저 id로 제품객체를 불러와서 이미지 파일을 삭제한 후 제품 삭제
Product currentProduct = productRepo.getById(id);
Path currentPath = Paths.get("src/main/resources/static/media/" + currentProduct.getImage());
Files.delete(currentPath);
productRepo.deleteById(id);
attr.addAttribute("message", "성공적으로 삭제되었습니다.");
attr.addAttribute("alertClass", "alert-success");
return "redirect:/admin/products";
}