@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
로 변환 작업 필요- 상품 조회 시,
Entity
를DTO
로 변환 작업 필요
이를 해결하기 위해 modelmapper
를 이용한다.
modelmapper
서로 다른 클래스의 값을 (필드의 이름과 자료형이 같으면) getter, setter를 통해 값을 복사해서 그 객체를 반환함.
상품 저장 후 상품 이미지에 대한 데이터를 전달하는 DTO
_ItemImgDto_@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(); // 멤버변수로 ModelMapper 객체를 추가
public static ItemImgDto of(ItemImg itemImg){ // itemImg와 멤버변수 이름이 같으면 ItemImgDto로 값을 복사해서 반환
return modelMapper.map(itemImg, ItemImgDto.class);
}
}
of()#map()
: 파라미터인 ItemImg의 자료형과 멤버변수 이름이 같으면, ItemImgDto로 값을 복사하고 반환.상품 데이터 정보를 전달하는 DTO
_ItemFormDto_@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();
// 엔티티 <--> DTO 데이터를 복사하여 반환
public Item createItem(){
return modelMapper.map(this, Item.class);
}
public static ItemFormDto of(Item item){
return modelMapper.map(item, ItemFormDto.class);
}
}
상품 등록같은 관리자 페이지는
데이터의 무결성
을 보장해야한다.
한 상품의 내용이 수정되면 연관된 다른 데이터도 수정되어야 한다.
상품 등록은 ADMIN만 가능하기 때문에 서버 재실행 시, 회원 정보가 사라지는 것을 막기위해 프로퍼티 파일을 수정한다.
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.hibernate.ddl-auto=create
validate
: 서버 재실행 해도 테이블이 재생성되지 않음
이미지 파일 등록 시, 파일의 크기 및 다운 요청할 수 있는 파일의 크기 등이 설정 가능하다.
# File Max Size
spring.servlet.multipart.maxFileSize=20MB
# Request File Max Size
spring.servlet.multipart.maxRequestSize=100MB
# Upload Image Location
itemImgLocation=C:/shop/item
# Resource Upload Location
uploadPath=file:///C:/shop/
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${uploadPath}") // 프로퍼티에 설정한 uploadPath 프로퍼티 값 불러옴
String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**") // url에 /images로 시작하는 경우, uploadPath에 설정한 폴더를 기준으로 파일 읽어옴
.addResourceLocations(uploadPath); // 로컬 컴퓨터에 저장된 파일을 읽어올 root 경로 설정
}
}
@Service
@Log
public class FileService {
public String uploadFile(String uploadPath, String originalFileName,
byte[] fileData) throws Exception {
UUID uuid = UUID.randomUUID(); // 1. UUID : 객체 구별을 위해 유일한 이름 부여
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
String savedFileName = uuid.toString() + extension; // 2. UUID와 조합해서 저장할 파일 이름 생성
String fileUploadFullUrl = uploadPath + "/" +savedFileName;
FileOutputStream fos = new FileOutputStream(fileUploadFullUrl); // 3. FileOutputStream : 바이트 단위의 출력을 내보냄. 생성자에 저장될 위치와 파일 이름 넘겨서 출력 스트림 만듬
fos.write(fileData); // 4. fuleData를 파일 출력 스트림에 입력
fos.close();
return savedFileName; // 5. 업로드된 파일의 이름을 반환
}
public void deleteFile(String filePath) throws Exception{
File deleteFile = new File(filePath); // 6. 파일이 저장된 경로를 이용해서 파일 객체 생성
if(deleteFile.exists()){ // 7. 해당 파일이 존재하면 파일을 삭제
deleteFile.delete();
log.info("파일을 삭제하였습니다.");
}
else{
log.info("파일이 존재하지 않습니다.");
}
}
}
상품 이미지 정보를 저장하는 ItemImgRepository 인터페이스를 생성.
상품이미지 업로드 및 정보를 저장하는 ItemImgService
생성
@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {
@Value("${itemImgLocation}") // 1. 프로퍼티에 등록한 itemImgLocation을 String 객체에 저장
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)){
// 2. 상품의 이미지를 등록했다면, uploadFile을 통해 로컬에 저장된 파일의 이름을 받아옴
imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
// 3. 저장한 상품 이미지를 불러올 경로 / 프로퍼티의 uploadPath(C:/shop) 아래의 /images/item/imgName을 불러옴
imgUrl = "/images/item/"+imgName;
}
// 상품 이미지 정보 저장
// 4.5.
// imgName : 실제 로컬에 저자오딘 이미지 파일 이름
// oriImgName : 업로드했던 이미지 파일의 원래 이름
// imgUrl : 업로드 결과 로컬에 저장된 이미지 파일을 불러오는 경로
itemImg.updateItemImg(oriImgName, imgName, imgUrl);
itemImgRepository.save(itemImg);
}
}
@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(); // 1. 상품 등록 폼의 데이터로 item 생성
itemRepository.save(item); // 2. 상품 데이터 저장
// 이미지 등록
for(int i=0;i<itemImgFileList.size()> ; i++) {
ItemImg itemImg = new ItemImg();
itemImg.setItem(item);
if(i==0){ // 3. 첫번째 이미지면 대표 이미지 설정
itemImg.setRepImgYn("Y");
}else{
itemImg.setRepImgYn("N");
}
itemImgService.saveItemImg(itemImg, itemImgFileList.get(i)); // 4. 상품의 이미지 정보 저장
}
return item.getId();
}
}
이제 테스트 코드를 작성해서 정상적으로 이미지가 등록되는지 확인한다.
등록한 상품의 내용을 수정하는 페이지를 만들어보자.
페이지는 기존 등록 페이지를 재활용해서 사용한다.
@Transactional(readOnly = true) // 읽기전용으로 하면 JPA가 더티체킹(변경 감지) 안해서 성능향상
public ItemFormDto getItemDtl(Long itemId){
List<ItemImg> itemImgList =
itemImgRepository.findByItemIdOrderByIdAsc(itemId); // 해당 상품 이미지 조회 / 등록순으로 가져옴
List<ItemImgDto> itemImgDtoList = new ArrayList<>();
for(ItemImg itemImg : itemImgList){ // 조회한 ItemImg 엔티티를 ItemImgDto객체로 만들어서 List에 추가
ItemImgDto itemImgDto = ItemImgDto.of(itemImg);
itemImgDtoList.add(itemImgDto);
}
Item item = itemRepository.findById(itemId) // 상품 아이디로 상품 엔티티 조회 / 없으면 익센션
.orElseThrow(EntityNotFoundException::new);
ItemFormDto itemFormDto = ItemFormDto.of(item);
itemFormDto.setItemImgDtoList(itemImgDtoList);
return itemFormDto;
}
수정 페이지로 이동하도록 컨트롤러에 매핑을 한다.
public Long updateItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList) throws Exception {
// 상품 수정
Item item = itemRepository.findById(itemFormDto.getId()) // 멤버 폼으로 상품 불러옴
.orElseThrow(EntityNotFoundException::new);
item.updateItem(itemFormDto); // 상품을 상품 폼 데이터로 변경
List<Long> itemImgIds = itemFormDto.getItemImgIds(); // 폼에서 이미지정보들을 받아옴
// 이미지 등록
for(int i=0;i<itemImgFileList.size()>;i++){
itemImgService.updateItemImg(itemImgIds.get(i), itemImgFileList.get(i)); // 이미지를 업데이트하기 위해 (이미지 id, 이미지 파일 정보)를 파라미터로 전달
}
return item.getId();
}
Item 엔티티에 상품 정보를 업데이트하는 로직을 만든다.
그 이유는 비즈니스 로직에 추가를 해야 좀 더 객체지향적으로 코딩 가능하고, 재활용도 가능하기 때문
public void updateItem(ItemFormDto itemFormDto){
this.itemNm = itemFormDto.getItemNm();
this.price = itemFormDto.getPrice();
this.stockNumber = itemFormDto.getStockNumber();
this.itemDetail = itemFormDto.getItemDetail();
this.itemSellStatus = itemFormDto.getItemSellStatus();
}
ItemService
public Long updateItem(ItemFormDto itemFormDto, List<MultipartFile> itemImgFileList) throws Exception {
// 상품 수정
Item item = itemRepository.findById(itemFormDto.getId()) // 멤버 폼으로 상품 불러옴
.orElseThrow(EntityNotFoundException::new);
item.updateItem(itemFormDto); // 상품을 상품 폼 데이터로 변경
List<Long> itemImgIds = itemFormDto.getItemImgIds(); // 폼에서 이미지정보들을 받아옴
// 이미지 등록
for(int i=0;i<itemImgFileList.size()>;i++){
itemImgService.updateItemImg(itemImgIds.get(i), itemImgFileList.get(i)); // 이미지를 업데이트하기 위해 (이미지 id, 이미지 파일 정보)를 파라미터로 전달
}
return item.getId();
}
상품 수정하는 페이지를 컨트롤러에 매핑하자