http://www.yes24.com/Product/Goods/103453774
책 기준으로 작성하는 것을 알립니다
상품 등록 정보를 가지고 있는 상품(Item) 엔티티 클래스를 다루었고, 상품 이미지를 저장하는 상품 이미지 엔티티를 만들 겁니다. 상품 이미지 엔티티는 이미지 파일명, 원본 이미지 파일명, 이미지 조회 경로, 대표 이미지 여부를 갖도록 설계하겠습니다. 대표 이미지 여부가 "Y"인 경우 메인 페이지에서 상품을 보여줄 때 사용합니다.
ItemImg
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@Table(name = "item_img")
public class ItemImg extends BaseEntity{
@Id
@Column(name = "item_img_id")
@GeneratedValue(strategy =GenerationType.AUTO)
private Long id;
private String imgNme; //이미지 파일명
private String oriImgName;//원본 이미지 파일명
private String imgUrl;// 이미지 조회 경로
private String repimgYn;//대표 이미지 여부
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
public void updateItemImg(String oriImgname, String imgNme, String imgUrl){
this.oriImgName = oriImgname;
this.imgNme = imgNme;
this. imgUrl = imgUrl;
}
}
- 상품 엔티티와 다대일 단방향 관계로 매핑합니다. 지연 로딩을 설정하여 매핑된 상품 엔티티 정보가 필요할 경우 데이터를 조회하도록 합니다.
- 원본 이미지 파일명, 업데이트할 이미지 파일명, 이미지 경로를 파라미터로 입력 받아서 이미지 정보를 업데이트 하는 메소드 입니다.
다음으로 상품 등록 및 수정에 사용할 데이터 전달용 DTO클래스를 만들 겁니다. 엔티티 자체를 화면으로 반환할 수도 있지만 그럴 때 엔티티 클래스에 화면에서만 사용하는 값이 축가 됩니다. 특히 실제 쇼핑몰에서 상품 등록 페이지는 정말 많은 데이터를 입력해야 상품을 등록할 수 있습니다.
상품을 등록할 때는 화면으로부터 전달받은 DTO 객체를 엔티티 객체로 반환하는 작업을 해야 하고, 상품을 조회할 때는 엔티티 객체를 DTO 객체로 바꿔주는 작업을 해야 합니다. 이 작업은 반복적인 작업입니다. 멤버 변수가 몇 개 없다면 금방 할 수도 있지만 멤버 변수가 많아진다면 상당한 시간을 소모합니다.
이를 도와주는 라이브러리로 modelmapper 라이브러리가 있습니다. 이 라이브러리는 서로 다른 클래스의 값을 필드의 이름과 자료형이 같으면 getter, setter를 통해 값을 복사해서 객체를 반환해 줍니다.
의존성을 추가하겠습니다.
메이븐
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.0</version>
</dependency>
그래들
implementation group: 'org.modelmapper', name: 'modelmapper', version: '3.1.0'
먼저 상품 저장 후 이미지에 대한 데이터를 전달할 DTO 클래스를 만들 겁니다.
ItemImgDto
package com.shop.dto;
import com.shop.entity.ItemImg;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;
@Getter
@Setter
public class ItemImgDto {
private Long id;
private String imgName;
private String oriImgName;
private String imgUrl;
private String repImgYnl;
private static ModelMapper modelMapper = new ModelMapper();
public static ItemImgDto of(ItemImg itemImg){
return modelMapper.map(itemImg,ItemImgDto.class);
}
}
- 멤버 변수로 ModelMapper객체를 추가합니다.
- ItemImg엔티티 객체를 파라미터로 받아서 ItemImg 객체의 자료형과 멤버변수의 이름이 같을 때 ItemDto로 값을 복사해서 반환합니다. static 메소드로 선언해 ItemImgDto 객체를 생성하지 않아도 호출할 수 있도록 하겠습니다.
ItemFormDto
package com.shop.dto;
import com.shop.constant.ItemSellStatus;
import com.shop.entity.Item;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class ItemFormDto {
private Long id;
@NotBlank(message ="상품명은 필수 입력 값입니다.")
private String itemNm;
@NotNull(message = "가격은 필수 입력 값입니다.")
private Integer price;
@NotBlank(message = "이름은 필수 입력 값입니다.")
private String itemDetail;
@NotNull(message = "재고는 필수 입력 값입니다.")
private Integer stockNumber;
private ItemSellStatus itemSellStatus;
private List<ItemImgDto> itemImgDtoList = new ArrayList<>();
private List<Long> itemImgIds = new ArrayList<>();
private static ModelMapper modelMapper = new ModelMapper();
public Item createItem(){
return modelMapper.map(this, Item.class);
}
public static ItemFormDto of(Item item){
return modelMapper.map(item,ItemFormDto.class);
}
}
- 상품 저장 후 수정할 때 상품 이미지 정보를 저장하는 리스트입니다.
- 상품의 이미지 아이디를 저장하는 리스트입니다. 상품 등록 시에는 아직 상품의 이미지를 저장하지 않았기 때문에 아무 값도 들어가 있지 않고 수정 시에 이미지 아이디를 담아둘 용도로 사용합니다.
- ,4 modeMapper를 이용하여 엔티티 객체와 DTO 객체 간의 데이터를 복사하여 복사한 객체를 반환해 주는 메소드입니다.
상품 등록 페이지로 접근할 수 있도록 기존에 만들어 두었던 ItemController 클래스도 수정하겠습니다. ItemFormDto를 model객체에 담아서 뷰로 전달하도록 합니다.
ItemController
package com.shop.controller;
import com.shop.dto.ItemFormDto;
import com.shop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping(value = "/admin/item/new")
public String itemForm(Model model) {
model.addAttribute("itemFormDto", new ItemFormDto());
return "item/itemForm";
}
}
상품 등록 페이지 또한 기존에 미리 만들어 두었던 itemForm.html 파일을 수정하겠습니다, 상품 등록 페이지는 굉장히 복잡한 페이지입ㄴ;다. 현재는 상품 데이터로 몇 개 밖에 사용을 안 하지만 실제쇼핑몰은 엄청난 양의 데이터를 입력해야 합니다.
상품 등록 같은 관리자 페이지에서 중요한 것은 데이터의 무결성을 보장해야 한다는 것입니다. 데이터가 의도와 다르게 저장된다거나, 잘못된 값이 저장되지 않도록 밸리데이션(validation)을 해야 합니다. 특히 데이터끼리 서로 연관이 있으면 어떤 데이터가 변함에 따라서 다른 데이터도 함께 체크를 헤야 하는 경우가 많습니다.
이제 상품 등록 페이지 소스를 작성하겠습니다. 너무 길어서 복붙을 추천합니다.
itemForm
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<meta charset="UTF-8">
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function(){
var errorMessage = [[${errorMessage}]];
if(errorMessage != null){
alert(errorMessage);
}
bindDomEvent();
});
function bindDomEvent(){
$(".custom-file-input").on("change", function() {
var fileName = $(this).val().split("\\").pop(); //이미지 파일명
var fileExt = fileName.substring(fileName.lastIndexOf(".")+1); // 확장자 추출
fileExt = fileExt.toLowerCase(); //소문자 변환
if(fileExt != "jpg" && fileExt != "jpeg" && fileExt != "gif" && fileExt != "png" && fileExt != "bmp"){
alert("이미지 파일만 등록이 가능합니다.");
return;
}
$(this).siblings(".custom-file-label").html(fileName);
});
}
</script>
</th:block>
<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
<style>
.input-group {
margin-bottom : 15px
}
.img-div {
margin-bottom : 10px
}
.fieldError {
color: #bd2130;
}
</style>
</th:block>
<div layout:fragment="content">
<form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
<p class="h2">
상품 등록
</p>
<input type="hidden" th:field="*{id}">
<div class="form-group">
<select th:field="*{itemSellStatus}" class="custom-select">
<option value="SELL">판매중</option>
<option value="SOLD_OUT">품절</option>
</select>
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">상품명</span>
</div>
<input type="text" th:field="*{itemNm}" class="form-control" placeholder="상품명을 입력해주세요">
</div>
<p th:if="${#fields.hasErrors('itemNm')}" th:errors="*{itemNm}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">가격</span>
</div>
<input type="number" th:field="*{price}" class="form-control" placeholder="상품의 가격을 입력해주세요">
</div>
<p th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">재고</span>
</div>
<input type="number" th:field="*{stockNumber}" class="form-control" placeholder="상품의 재고를 입력해주세요">
</div>
<p th:if="${#fields.hasErrors('stockNumber')}" th:errors="*{stockNumber}" class="fieldError">Incorrect data</p>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">상품 상세 내용</span>
</div>
<textarea class="form-control" aria-label="With textarea" th:field="*{itemDetail}"></textarea>
</div>
<p th:if="${#fields.hasErrors('itemDetail')}" th:errors="*{itemDetail}" class="fieldError">Incorrect data</p>
<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
<div class="form-group" th:each="num: ${#numbers.sequence(1,5)}">
<div class="custom-file img-div">
<input type="file" class="custom-file-input" name="itemImgFile">
<label class="custom-file-label" th:text="상품이미지 + ${num}"></label>
</div>
</div>
</div>
<div th:if = "${not #lists.isEmpty(itemFormDto.itemImgDtoList)}">
<div class="form-group" th:each="itemImgDto, status: ${itemFormDto.itemImgDtoList}">
<div class="custom-file img-div">
<input type="file" class="custom-file-input" name="itemImgFile">
<input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
<label class="custom-file-label" th:text="${not #strings.isEmpty(itemImgDto.oriImgName)} ? ${itemImgDto.oriImgName} : '상품이미지' + ${status.index+1}"></label>
</div>
</div>
</div>
<div th:if="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
<button th:formaction="@{/admin/item/new}" type="submit" class="btn btn-primary">저장</button>
</div>
<div th:unless="${#strings.isEmpty(itemFormDto.id)}" style="text-align: center">
<button th:formaction="@{'/admin/item/' + ${itemFormDto.id} }" type="submit" class="btn btn-primary">수정</button>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</div>
</html>
- 상품 등록 시 실패 메시지를 받아서 상품 등록 페이지에 재진입 시 alert를 통해서 실패 사유를 보여줍니다.
- 파일 첨부 시 이미지 파일인지 검사를 합니다. 보통 데이터를 검증할 때는 스크립트에서 밸리데이션을 한 번 하고, 스크립트는 사용자가 변경이 가능하므로 서버에서 한 번 더 밸리데이션을 합니다. 스크립트에서 밸리데이션을 하는 이유는 서버 쪽으로 요청을 하면 네트워크를 통해 서버에 요청이 도착하고 다시 그 결과를 클라이언트에 반환하는 등 리소스를 소모하기 때문입니다.
- label 태그 안의 내용을 jquery의. html()을 이용하여 파일명을 입력해 줍니다.
- 상품 이미지 정보를 담고 있는 리스트가 비어 있다면 상품을 등록하는 경우입니다.
- 타임리프의 유틸리티 객체 #numbers. sequence(start, end)를 이용하면 start부터 end까지 반복 처리를 할 수 있습니다. 상품 등록 시 이미지의 개수를 최대 5개로 하겠습니다. num에는 1부터 5까지 숫자가 할당됩니다.
- label 태그에는 몇 번째 상품 이미지인지 표시를 합니다.
- 상품 이미지 정보를 담고 있는 리스트가 비어 있지 않다면 상품을 수정하는 경우입니다.
- 상품 수정 시 어떤 이미지가 수정됐는지 알기 위해서 상품 이미지의 아이디를 hidden 값으로 숨겨둡니다.
- 타임리프의 유틸리티 객체인 #string.isEmpty(string)을 이용하여 저장된 이미지 정보가 있다면 파일의 이름을 보여주고 , 없다면'상품 이미지+번호'를 출력합니다.
- 상품 아이디어가 없는 경우(상품을 처음 등록할 경우) 저장 로직을 호출하는 버튼을 보여줍니다.
- 상품의 아이디가 있는 경우 수정 로직을 호출하는 버튼을 보여줍니다.
application properties 설정 추가하기
#파일 한 개당 최대 사이즈
spring.servlet.multipart.maxFileSize=20MB
#요청당 최대 파일 크기
spring.servlet.multipart.maxRequestSize=100MB
#상품 이미지 업로드 경로
itemImgLocation=C:/shop.item
#리소스 업로드 경로
uploadPath=file:///C:/shop/
업로드한 파일을 읽어올 경로를 설정하겠습니다. WebMvcConfigurer 인터페이스를 구현하는 WebMvcConfig.java 파일을 작성합니다. addResourceHandlers메소드를 통해서 자신의 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정합니다.
WebMvcConfig
package com.shop.confing;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${uploadPath}")//스프링 벨류 써야함
String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations(uploadPath);
}
}
- app;ication.properties에 설정한 "uploadPath" 프로퍼티 값을 읽어옵니다.
- 웹 브라우저에 입력하는 url에 /images로 시작하는 경우 uploadPath에 설정한 폴더를 기준으로 파일을 읽어오도록 설정합니다.
- 로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정합니다.
다음으로 파일을 처리하는 FileService 클래스를 만들겠습니다. 파일을 업로드하는 메소드와 삭제하는 메소드를 작성하겠습니다.
FileService
package com.shop.service;
import lombok.extern.java.Log;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.util.UUID;
@Service
@Log
public class FileService {
public String uploadFile(String uploadPath, String originalFileName, byte[] fileData) throws Exception{
UUID uuid = UUID.randomUUID();
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
String savedFileName = uuid.toString() + extension;
String fileUploadFullUrl = uploadPath + "/" + savedFileName;
FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
fos.write(fileData);
fos.close();
return savedFileName;
}
public void deleteFile(String filePath) throws Exception{
File deleteFile = new File(filePath);
if(deleteFile.exists()) {
deleteFile.delete();
log.info("파일을 삭제하였습니다.");
} else {
log.info("파일이 존재하지 않습니다.");
}
}
}
- UUID(Universally Unique Identifier)는 서로 다른 개체들은 구별하기 위해서 이름을 부여할 때 사용합니다. 실제 사용 시 중복될 가능성이 거의 없기 때문에 파일의 이름으로 사용하면 파일명 중복 문제를 해결할 수 있습니다.
- UUID로 받는 값과 원래 파일의 이름의 확정자를 조합해서 저장될 파일 이름을 만듭니다.
- FileOutputStream 클래스는 바이트 단위의 출력을 내보내는 클래스입니다. 생성자로 파일이 저장될 위치와 파일의 이름을 넘겨 파일에 쓸 파일 출력 스트림을 만듭니다.
- fileData를 파일 출력 스트림에 입력합니다.
- 업로드된 파일의 이름을 반환합니다.
- 파일이 저장된 경로를 이용하여 파일 객체를 생성합니다.
- 해당 파일이 존재하면 파일을 삭제합니다.
상품의 이미지 정보를 저장하기 위해서 repository 패키지 아래에 JpaRepository를 상속받는 ItemImgRepository 인터페이스를 만듭니다.
ItemImgRepository
package com.shop.repository;
import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}
다음으로 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스를 service 패키지 아래에 생성합니다.
ItemImgService
package com.shop.service;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.util.StringUtils;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {
@Value("${itemImgLocation}")
private String itemImgLocation;
private final ItemImgRepository itemImgRepository;
private final FileService fileService;
public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile) throws Exception{
String oriImgName = itemImgFile.getOriginalFilename();
String imgName = "";
String imgUrl = "";
//파일 업로드
if(!StringUtils.isEmpty(oriImgName)){
imgName = fileService.uploadFile(itemImgLocation, oriImgName,
itemImgFile.getBytes());
imgUrl = "/images/item/" + imgName;
}
//상품 이미지 정보 저장
itemImg.updateItemImg(oriImgName, imgName, imgUrl);
itemImgRepository.save(itemImg);
}
}
- @Value 어노테이션을 통해 application.properties 파일에 등록한 itemImgLocation 값을 불러와서 itemImgLocation변수에 넣어 줍니다
- 사용자가 상품의 이미지를 등록했다면 저장할 경로와 파일의 이름 파일을 파일의 바이트 배열을 파일 업로드 파라미터로 uploadFile 메소드를 호출합니다. 호출 결과 로컬에 저장된 파일의 이름을 imgName 변수에 저장합니다.
- 저장한 상품 이미지를 불러올 경로로 설정합니다. 외부 리소스를 불러오는 urlPatterns로 WebMvcConfing 클래스에서"/images**"를 설정해 주었습니다. 또한 application.properties에서 설정한 uploadPath 프로퍼터 경로인"C:/shop/"아래 item 폴더에 이미지를 저장하므로 상품 이미지를 불러오는 경로로 "/images/item/"를 붙여줍니다.
- 5 입력받은 상품 이미지 정보를 저장합니다.
imgName:실제 로컬에 저장된 상품 이미지 파일의 이름
orilmgName:업로드했던 상품 이미지 파일의 원래 이름
imgUrl:업로드 결과 로컬에 저장된 상품 이미지 파일을 불러오는 경로
상품을 하나 등록하기 위해서 정말 많은 클래스들을 작성하고 있습니다. 실제 서비스는 이것보다 훨씬 많은 클래스가 필요할 것입니다. 다음으로 상품을 등록하느 ItemService 클래스를 만들겠습니다.
ItemService
package com.shop.service;
import com.shop.dto.ItemFormDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.transaction.Transactional;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
private final ItemImgService itemImgService;
private final ItemImgRepository itemImgRepository;
public Long saveItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList)
throws Exception {
//상품 등록
Item item = itemFormDto.createItem();
itemRepository.save(item);
//이미지 등록
for(int i=0;i<itemImgFileList.size();i++){
ItemImg itemImg = new ItemImg();
itemImg.setItem(item);
if(i == 0)
itemImg.setRepimgYn("Y");
else
itemImg.setRepimgYn("N");
itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
}
return item.getId();
}
}
- 상품 등록 폼으로부터 입력받은 데이터를 이용하여 item 객체를 생성합니다.
- 상품 데이터를 저장합니다.
- 첫 번째 이미지일 경우 대표 상품 이미지 여부 값을 "Y"로 세팅합니다. 나머지 상품 이미지는 "N"으로 설정합니다.
- 상품의 이미지 정보를 저장합니다.
ItemController
package com.shop.controller;
import com.shop.dto.ItemFormDto;
import com.shop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping(value = "/admin/item/new")
public String itemForm(Model model){
model.addAttribute("itemFormDto", new ItemFormDto());
return "item/itemForm";
}
@PostMapping(value = "/admin/item/new")
public String itemNew(@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
Model model, @RequestParam("itemImgFile") List<MultipartFile> itemImgFileList){
if(bindingResult.hasErrors()){
return "item/itemForm";
}
if(itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null){
model.addAttribute("errorMessage", "첫번째 상품 이미지는 필수 입력 값 입니다.");
return "item/itemForm";
}
try {
itemService.saveItem(itemFormDto, itemImgFileList);//상품 저장 로직 호출
}catch (Exception e){
model.addAttribute("errorMessage","상품 등록 중 에러가 발생하였습니다.");
return "item/itemForm";
}
return "redirect:/";
}
}
- 상품등록 시 필수 값이 없다면 다시 상품 등록 페이지로 전환합니다.
- 상품 등록 시 첫 번째 이미지가 없다면 에러 메시지와 함께 상품 등록 페이지로 전환합니다. 상품의 첫 번째 이미지는 메인 페이지에서 보여줄 상품 이미지로 사용하기 위해서 필수 값으로 지정하겠습니다.
- 상품 저장 로직을 호출합니다. 매개 변수로 상품 정보와 상품 이미지 정보를 담고 있는 itemImgFileList를 넘겨줍니다.
- 상품이 정상적으로 등록되었다면 메인 페이지로 이동합니다.
이제 상품 저장 로직 테스트 코드를 작성해야 합니다. 비즈니스가 점점 커지면서 상품에 추가되는 데이터들이 많습니다 즉
소스를 수정할 일이 많기 때문에 저장 로직에 대한 테스트 코드와 테스트 케이스를 잘 만들어 두는 게 중요합니다.
이미지가 잘 저장됐는지 테스트 코드를 작성하기 위해서 ItemImgRepository 인터페이스에 findByItemIdOrderByIdAsc 메소드를 추가하겠습니다. 매개변수로 넘겨준 상품 아이디를 가지며, 상품 이미지 아이디의 오름차순으로 가져오는 쿼리 메소드입니다.
ItemImgRepository
package com.shop.repository;
import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}
테스트를 우해 ItemServiceTest 클래스를 만들겠습니다.
ItemServiceTest
package com.shop.service;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemFormDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
@Transactional
@TestPropertySource(locations="classpath:application-test.properties")
public class ItemServiceTest {
@Autowired
ItemService itemService;
@Autowired
ItemRepository itemRepository;
@Autowired
ItemImgRepository itemImgRepository;
List<MultipartFile> createMultipartFiles() throws Exception{
List<MultipartFile> multipartFileList = new ArrayList<>();
for(int i=0;i<5;i++){
String path = "C:/shop/item"; //경로 설정 확실히 잘합시다
String imageName = "image" + i + ".jpg";
MockMultipartFile multipartFile =
new MockMultipartFile(path, imageName, "image/jpg", new byte[]{1,2,3,4});
multipartFileList.add(multipartFile);
}
return multipartFileList;
}
@Test
@DisplayName("상품 등록 테스트")
@WithMockUser(username = "admin", roles = "ADMIN")
void saveItem() throws Exception {
ItemFormDto itemFormDto = new ItemFormDto();
itemFormDto.setItemNm("테스트상품");
itemFormDto.setItemSellStatus(ItemSellStatus.SELL);
itemFormDto.setItemDetail("테스트 상품 입니다.");
itemFormDto.setPrice(1000);
itemFormDto.setStockNumber(100);
List<MultipartFile> multipartFileList = createMultipartFiles();
Long itemId = itemService.saveItem(itemFormDto, multipartFileList);
List<ItemImg> itemImgList = itemImgRepository.findByItemIdOrderByIdAsc(itemId);
Item item = itemRepository.findById(itemId)
.orElseThrow(EntityNotFoundException::new);
assertEquals(itemFormDto.getItemNm(), item.getItemNm());
assertEquals(itemFormDto.getItemSellStatus(), item.getItemSellStatus());
assertEquals(itemFormDto.getItemDetail(), item.getItemDetail());
assertEquals(itemFormDto.getPrice(), item.getPrice());
assertEquals(itemFormDto.getStockNumber(), item.getStockNumber());
assertEquals(multipartFileList.get(0).getOriginalFilename(), itemImgList.get(0).getOriImgName());
}
}
- MockMultipartFile 클래스를 이용하여 가짜 MultipartFile 리스트를 만들어서 반환해 주는 메소드입니다.
- 상품 등록 화면에서 입력받는 상품 데이터를 세팅해 줍니다.
- 상품 데이터와 이미지 정보를 파라미터로 넘겨서 저장 후 저장된 상품의 아이디 값을 반환 값으로 리턴해줍니다.
- 입력한 상품 데이터와 실제로 저장된 상품 데이터가 같은지 확인합니다.
- 상품 이미지는 첫 번째 파일의 원본 이미지 파일 이름만 같은지 확인하겠습니다.
테스트 코드 실행 결과 테스트가 정상적으로 통과했음을 볼 수 있습니다. 참고로 테스트 실행을 하면 더미 데이터로 넣은 이미지 데이터가 상품 이미지를 저장하는 폴더에 생성됩니다.
상품 작성을 정상적으로 하고 저장을 하였으면 다음과 같이 메인 페이지로 이동합니다.
나머지 수정,장바구니,결제 는 저작권상 비공개로 설정했습니다.
'Spring > 쇼핑물 프로젝트 책' 카테고리의 다른 글
주문 기능 구현하기 (0) | 2023.03.11 |
---|---|
상품 상세 페이지 (0) | 2023.03.11 |
쇼핑물 메인페이지 (0) | 2023.03.10 |
상품 관리 (0) | 2023.03.08 |
홈쇼핑 상품 수정 (0) | 2023.03.08 |