Item 엔티티 클래스를 다루었고, 상품의 이미지를 저장하는 상품 이미지 엔티티를 만들겠습니다.
@Entity
@Table(name="item_img")
@Getter @Setter
public class ItemImg extends BaseEntity{
@Id
@Column(name="item_img_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String imgName; //이미지 파일명
private String oriImgName; //원본 이미지 파일명
private String imgUrl; //이미지 조회 경로
private String repimgUrl; //대표 이미지 여부
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
public void updateItemImg(String oriImgName, String imgName, String imgUrl) {
this.oriImgName = oriImgName;
this.imgName = imgName;
this.imgUrl = imgUrl;
}
}
updateItemImg 함수는 원본 이미지 파일명, 업데이트할 이미지 파일명, 이미지 경로를 파라미터로 입력 받아서 이미지 정보를 업데이트하는 메소드입니다.
이제 상품 등록과 수정에 사용할 데이터 전달용 DTO 클래스를 만들겠습니다.
상품을 등록할 때는 화면으로부터 전달받은 DTO 객체를 엔티티 객체로 변환하는 작업을 해야하고, 상품을 조회할 때는 엔티티 객체를 DTO 객체로 바꿔주는 작업을 해야합니다. 이 작업은 반복적인 작업이고, 멤버 변수가 많아진다면 상당한 시간을 소모합니다.
이를 도와주는 라이브러리로 modelmapper 라이브러리가 있습니다. 이 라이브러리는 서로 다른 클래스의 값을 필드의 이름과 자료형이 같으면 getter, setter를 통해 값을 복사해서 객체를 반환해줍니다.
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.4.4</version>
</dependency>
이제 상품 저장 후 상품 이미지에 대한 데이터를 전달할 DTO 클래스를 만들겠습니다.
@Getter @Setter
public class ItemImgDto {
private Long id;
private String imgName;
private String oriImgName;
private String imgUrl;
private String repImgYn;
private static ModelMapper modelMapper = new ModelMapper();
public static ItemImgDto of(ItemImg itemImg) {
return modelMapper.map(itemImg, ItemImgDto.class);
}
}
ItemImg 엔티티 객체를 파라미터로 받아서 ItemImg 객체의 자료형과 멤버변수의 이름이 같을 때 ItemImgDto로 값을 복사해서 반환합니다. static 메소드로 선언해 ItemImgDto객체를 생성하지 않아도 호출할 수 있도록 합니다.
다음은 상품 데이터 정보를 전달하는 DTO입니다
@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);
}
}
modelMapper를 이용하여 엔티티 객체와 DTO 객체 간의 데이터를 복사하여 복사한 객체를 반환해주는 메소드입니다.
기존의 ItemController 클래스도 수정하겠습니다.
@Controller
public class ItemController {
@GetMapping(value = "/admin/item/new")
public String itemForm(Model model){
model.addAttribute("itemFormDto", new ItemFormDto());
return "item/itemForm";
}
}
ItemFormDto를 model 객체에 담아서 뷰로 전달하도록 합니다.
itemForm.html 수정 후 member 엔티티에 role을 admin으로 수정하여 회원가입을 진행하면 다음 처럼 상품 등록 페이지가 뜹니다. 어드민 권한을 가진 사람만 상품 등록을 할 수 있습니다.
이미지 파일을 등록할 때 서버에서 각 파일의 최대 사이즈와 한번에 다운 요청할 수 있는 파일의 크기를 지정할 수 있습니다. 또한 컴퓨터에서 어떤 경로에 저장할지를 관리하기 위해서 프로퍼티에 itemImgLocation을 추가하겠습니다.
#파일 한 개당 최대 사이즈
spring.servlet.multipart.maxFileSize=20MB
#요청당 최대 파일 크기
spring.servlet.multipart.maxRequestSize=100MB
#상품 이미지 업로드 경로
itemImgLocation=C:/shop/item
#리소스 업로드 경로
uploadPath=file:///C:/shop/
업로드한 파일을 읽어올 경로를 설정하겠습니다. WebMvcConfigurer 인터페이스를 구현하는 WebMvcConfig.java 파일을 작성합니다. addResourceHandlers 메소드를 통해서 자신의 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정합니다.
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${uploadPath}")
String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations(uploadPath);
}
}
다음은 파일을 처리하는 FileService 클래스를 만들겠습니다.
@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로 받은 값과 원래의 파일 이름의 확장자를 조합해서 저장될 파일 이름을 만듭니다. FileOutputStream 클래스는 바이트 단위의 출력을 내보내는 클래스입니다. 생성자로 파일이 저장될 위치와 파일의 이름을 넘겨 파일에 쓸 파일 출력 스트림을 만듭니다.
상품의 이미지 정보를 저장하기 위해서 repository 패키지 아래에 ItemImgRepository 인터페이스를 만듭니다.
다음으로 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스를 service 패키지 아래에 생성합니다.
@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);
}
public void updateItemImg(Long itemImgId, MultipartFile itemImgFile) throws Exception{
if(!itemImgFile.isEmpty()){
ItemImg savedItemImg = itemImgRepository.findById(itemImgId)
.orElseThrow(EntityNotFoundException::new);
//기존 이미지 파일 삭제
if(!StringUtils.isEmpty(savedItemImg.getImgName())) {
fileService.deleteFile(itemImgLocation+"/"+
savedItemImg.getImgName());
}
String oriImgName = itemImgFile.getOriginalFilename();
String imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
String imgUrl = "/images/item/" + imgName;
savedItemImg.updateItemImg(oriImgName, imgName, imgUrl);
}
}
}
상품을 등록하는 ItemService클래스입니다.
@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();
}
}
첫번 째 이미지일 경우 대표 상품 이미지 여부 값을 "Y"로 세팅합니다. 나머지 상품 이미지는 "N"으로 설정합니다.
이제 상품을 등록하는 url을 ItemController 클래스에 추가하겠습니다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
...
@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:/";
}
}
상품 등록 시 첫 번째 이미지가 없다면 에러 메시지와 함께 상품 등록 페이지로 전환합니다. 상품의 첫 번째 이미지는 필수 값으로 지정하겠습니다.
@SpringBootTest
@Transactional
@TestPropertySource(locations="classpath:application-test.properties")
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());
}
}
createMultipartFiles()함수는 MockMultipartFile 클래스를 이용하여 가짜 MultipartFile 리스트를 만들어서 반환해주는 메소드입니다.
실제 어플리케이션을 실행했더니 상품 등록한대로 제가 지정한 경로에 업로드한 이미지 파일들이 들어와있습니다.
다음은 상품 수정하기를 공부해보겠습니다.