Spring Boot - 쇼핑몰 프로젝트 06 (상품 등록 및 조회하기 - 1)
상품 등록 및 조회하기
상품 등록하기
- 상품 이미지 엔티티는 이미지 파일명, 원본 이미지 파일명, 이미지 조회 경로, 대표 이미지 여부를 갖도록 설계
- 대표 이미지 여부가 “Y”인 경우 메인페이지에서 상품을 보여줄 때 사용
상품 등록 구현하기
com.shop.entity.ItemImg.java
// ItemImg.java
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@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 repimgYn; // 대표 이미지 여부
@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;
}
}
- DTO 객체와 Entity객체의 변환을 도와주는 modelmapper라이브러리 추가
- 서로 다른 클래스의 값을 필드의 이름과 자료형이 같으면 getter, setter를 통해 값을 복사해서 객체를 반환
pom.xml
// pom.xml
... 생략 ...
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
... 생략 ...
com.shop.dto.ItemImgDto.java
// ItemImgDto.java
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 repImgYn;
private static ModelMapper modelMapper = new ModelMapper();
public static ItemImgDto of(ItemImg itemImg) {
return modelMapper.map(itemImg, ItemImgDto.class);
}
}
com.shop.dto.ItemFormDto.java
// ItemFormDto.java
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);
}
}
com.shop.ItemController.java
수정하기
// ItemController.java
package com.shop.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;
import com.shop.dto.ItemFormDto;
@Controller
public class ItemController {
@GetMapping(value ="/admin/item/new")
public String itemFrom(Model model) {
model.addAttribute("itemFormDto", new ItemFormDto());
return "item/itemForm";
}
}
- 상품 등록 같은 관리자 페이지에서 중요한 것은 데이터의 무결성을 보장하는 것이 중요
- 잘못된 값이 저장되지 않도록 밸리데이션(validation)
- 데이터끼리 서로 연관이 있으면 어떤 데이터가 변함에 따라서 다른 데이터도 함께 체크를 해야 하는 경우가 많음
- 소스 양이 많기 때문에 깃허브에서 복사해서 사용
- 깃허브 주소 : https://github.com/roadbook2/shop
resources/templates/item/itemForm.html
https://rogi221.tistory.com/178
ItemForm.html - github
https://rogi221.tistory.com/177 깃허브에서 자료 복사해서 입력 // ItemFrom.html 상품 등록 판매중 품절 상품명 Incorrect data 가격 Incorrect data 재고 Incorrect data 상품 상세 내용 Incorrect data 저장 수정
rogi221.tistory.com
- 현재는 application을 재실행하면 데이터가 삭제되므로 데이터베이스에 저장된 데이터가 삭제되지 않도록 ddl-auto 설정 변경
- application.prperties의 ddl-auto 속성을 validate로 변경하면 애플리케이션 실행 시점에 엔티티와 테이블이 매핑이 정상적으로 되어있는지만 확인
// application.properties
spring.jpa.hibernate.ddl-auto=validate << 수정
// application-test.properties
spring.jap.hibernate.ddl-auto=create << 내용 추가
- 회원 가입 후 다시 로그인을 하였다면 ADMIN ROLE로 가입을 진행하였기 때문에 상품 등록 메뉴가 보이는 것을 확인
- 상품 등록 메뉴를 클릭하면 방금 전에 만들었던 상품 등록 페이지가 보이는 것을 확인 가능
- 이미지 파일을 등록할 때 서버에서 각 파일의 최대 사이즈와 한번에 다운 요청할 수 있는 파일의 크기 지정
- 어떤 경로에 저장할지를 관리하기 위해서 프로퍼티에itemImgLocation을 추가
- 프로젝트 내부가 아닌 자신의 컴퓨터에서 파일을 찾는 경로로 uploadPath 프로퍼티를 추가
application.properties 설정 추가
// application.properties
#파일 한 개당 최대 사이즈
spring.servlet.multipart.maxFileSize=20MB
#요청당 최대 파일 크기
spring.servlet.multipart.maxRequestSize=100MB
#상품 이미지 업로드 경로
itemImgLocation=C:/shop/item
#리소스 업로드 경로
uploadPath=file:///C:/shop/
- addResourceHandlers 메소드를 통해서 자신의 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정
com.shop.config.WebMvcConfig.java
// WebMvcConfig.java
package com.shop.config;
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("/imges/**")
.addResourceLocations(uploadPath);
}
}
com.shop.service.FileService.java
// FileService.java
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("파일이 존재하지 않습니다.");
}
}
}
- 상품의 이미지 정보를 저장하는 ItemImgRepository 인터페이스 작성
com.shop.repository.ItemImgRepository.java
// ItemImgRepository.java
package com.shop.repository;
import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
}
- 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스 작성
com.shop.service.ItemImgService.java
// ItemImgService.java
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.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.thymeleaf.util.StringUtils;
@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);
}
}
- 상품을 등록하는 ItemService 클래스 작성
com.shop.service.ItemService.java
// ItemService.java
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.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
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();
}
}
com.shop.controller.ItemController.java
// ItemController.java
package com.shop.controller;
import com.shop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;
import com.shop.dto.ItemFormDto;
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 itemFrom(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:/";
}
}
- 상품 저장 로직 테스트 코드를 작성
- 테스트 코드를 작성하기 위해서 ItemImgRepository 인터페이스에findByItemIdOrderByIdAsc 메소드를 추가
- 상품 이미지 아이디의 오름차순으로 가져오는 쿼리 메소드
com.shop.repository.ItemImgRepository.java
// ItemImgRepository.java
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);
}
com.shop.service.ItemServiceTest.java
// ItemServiceTest.java
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.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@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());
}
}
- <저장> 버튼을 눌렀을 때 상품이 정상적으로 저장됐다면 다음과 같이 메인 페이지로 이동
- 파일 업로드 경로인 C:/shop/item 경로에 들어가보면 업로드한 청바진 사진과 스웨터 사진이 올라온것을 확인
https://rogi221.tistory.com/179
Spring Boot - 쇼핑몰 프로젝트 06 (상품 등록 및 조회하기 - 2)
상품 등록 및 조회하기 - 2 상품 수정하기 상품 등록 후 콘솔창을 보면 insert into item 쿼리문에서 item_id에 들어가는 binding parameter 값 확인 해당 상품 아이디를 이용해서 상품 수정 페이지에 진입 예
rogi221.tistory.com