상품이미지 엔티티 구현
- ItemImg 클래스 생성
@Entity
@Table(name = "item_img")
@Data
public class ItemImg extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "item_img_id")
private Long id;
@Column(name = "image_name", nullable = false)
private String imgName; // 이미지 파일명
@Column(name = "original_name")
private String oriImgName; // 원본 이미지 파일명
@Column(name = "image_url")
private String imgUrl; // 이미지 조회 경로
@Column(name = "represent_img")
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;
}
}
✔️ updateItemImg: 이미지의 원본 파일명, 이미지 파일명, 이미지 URL을 업데이트. ItemImg 객체의 해당 필드들을 수정하는 데 사용됨.
DTO 구현
- 상품 등록시에는 화면에서 전달받은 DTO 객체를 엔티티 객체로 변환하는 작업을 해야 함
- modelmapper 라이브러리 사용하여 DTO(Data Transfer Object)와 엔티티(Entity) 간의 변환을 효율적으로 처리
1. 상품 저장 후 상품 이미지에 대한 데이터를 전달할 DTO 클래스
@Data
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);
}
}
✔️ 멤버 변수로 ModelMapper 객체 추가 ➡️ ItemImg 객체를 파라미터로 받아 ItemImgDTO 로 값을 복사해서 반환
✔️ of 메소드는 주로 컨트롤러에서 데이터 전송을 위해 ItemImgDTO 객체를 생성할 때 사용됨.
✔️ static으로 선언된 of 메소드는 ItemImgDTO 클래스의 인스턴스를 생성하지 않고도 호출할 수 있다
➡️ static으로 선언된 이유는 DTO 변환 로직이 인스턴스의 상태와 관계없이 동작하므로, static 메소드로 정의하여 클래스 수준에서 직접 호출할 수 있도록함. 이로 인해 매핑 로직을 재사용할 수 있고, 코드가 간결해짐.
2. 상품 데이터 정보를 전달하는 DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ItemFormDTO {
private Long id;
@NotBlank(message = "☑️ Item Name is required. ")
private String itemNm;
@NotNull(message = "☑️ Price is required. ")
@Positive(message = "Price must be a positive number.")
private Integer price;
@NotBlank(message = "☑️ Item Detail is required. ")
private String itemDetail;
@NotNull(message = "☑️ Stock Number is required. ")
@Min(value = 0, message = "☑️ Stock Number must be zero or positive.")
private Integer stockNum;
private ItemSellStatus itemSellStatus;
// 상품 저장 후 수정할 때 상품 이미지 정보를 저장하는 리스트
private List<ItemImgDTO> itemImgDTOList = new ArrayList<>();
// 수정 시에 아이디를 담아둘 용도
private List<Long> itemImgIds = new ArrayList<>();
private static ModelMapper modelMapper = new ModelMapper();
// ItemFormDTO의 필드를 Item 엔티티로 매핑
public Item createItem() {
return modelMapper.map(this, Item.class);
}
// Item 엔티티를 ItemFormDTO로 매핑하는 정적 메서드
public static ItemFormDTO of(Item item) {
return modelMapper.map(item, ItemFormDTO.class);
}
}
✔️ 문자열이아닌 Integer에서는 @Length가 아닌 @Min, @Max를 사용
✔️ createItem(): ItemFormDTO 객체의 데이터를 Item 엔티티 객체로 변환. 주로 상품을 데이터베이스에 저장하기 위해 사용
✔️ of(Item item): Item 엔티티 객체를 ItemFormDTO로 변환하는 정적 메소드. 주로 엔티티를 DTO로 변환하여 사용자에게 반환할 때 사용
기존 ItemController 페이지 수정
@Controller
@RequiredArgsConstructor
@Data
public class ItemController {
@GetMapping("/admin/item/new")
public String itemForm(Model model){
model.addAttribute("itemFormDTO", new ItemFormDTO());
return "item/itemForm";
}
}
- 상품 등록과 같은 관리자 페이지에서 중요한 것은 데이터의 무결성을 보장해야 한다는 것!
- 데이터가 의도와 다르게 저장되거나 잘못된 값이 저장되지 않도록 validation을 해야 함.
- 특히 데이터끼리 서로 연관이 있으면 어떤 데이터가 변함에 따라 다른 데이터도 함께 체크를 해야하는 경우 빈번.
상품 등록 페이지 구현
- ItemForm.html 생성 (코드가 길기 때문에 깃허브 소스 첨부)
https://github.com/SominY/Dora-Flower-Shop/blob/main/src/main/resources/templates/item/itemForm.html
* 앞에 memberForm 페이지 참고해서 부트스트랩 코드 작성함..! 아쉬운 점은 부트스트랩 파일 업로드 형식이 너무 제한적.. ㅠ 저런 디자인 말고 다른거 하고 싶은데 버전별로 다른듯..😣
- 폼 전송할 때 enctype(인코딩 타입) 값으로 'multipart/form-data' 입력 - 웹 폼에서 파일을 업로드할 때 사용되는 인코딩 방식. 이 방식은 데이터와 파일을 서버로 전송할 때 각각의 데이터를 구분하고, 파일을 포함한 복합적인 데이터 전송을 가능하게 함
상품 등록 시
- itemImgDTOList가 비어 있는 경우(새 상품 등록 시) 1~5개의 이미지 업로드 필드를 동적으로 생성.
- itemFormDTO.id가 비어 있을 경우, Register 버튼을 표시
상품 수정 시
- itemImgDTOList가 비어 있지 않은 경우(기존 상품 수정 시) 기존 이미지들을 수정할 수 있도록 이미지 업로드 필드를 생성.
- itemImgIds는 숨겨진 필드로 기존 이미지 ID를 전달.
- itemFormDTO.id가 존재할 경우, Update 버튼을 표시
#lists.isEmpty(...)
• #lists는 Thymeleaf의 내장 객체로, 리스트와 관련된 유틸리티 메소드를 제공
• isEmpty(...)는 #lists 객체의 메소드로, 리스트가 비어 있는지를 확인하는 기능
#strings.isEmpty(...)
• 특정 문자열이 빈 문자열("")인지, 또는 null인지 여부를 검사
#numbers.sequence(start, end)
• 지정된 시작 값과 끝 값 사이의 정수 시퀀스를 생성하여 반복할 수 있게 함
• Thymeleaf 템플릿에서 주로 반복문을 생성할 때 유용
th:formaction
• 폼의 제출 URL을 동적으로 설정
• 등록과 수정 작업에 따라 서로 다른 URL로 폼을 제출할 수 있도록 함
자바스크립트 코드
⚡️ 외부로 파일 빼고 싶으면 부트스트랩 jquery 코드 html에 넣어줘야함!!!!!
1. $(".custom-file-input").on("change", function () { ... });
• custom-file-input 클래스를 가진 HTML 요소에 change 이벤트 리스너를 추가. 사용자가 파일 선택 필드에서 파일을 선택할 때마다 이 함수가 실행.
2. var fileName = $(this).val().split("\\").pop();
• 사용자가 선택한 파일의 이름을 가져옴
• 작동 방식: $(this).val() - 현재 변경된 input 요소의 값을 가져옴. 이 값은 선택된 파일의 경로.
split("\\") - 경로를 역슬래시(\)를 기준으로 분리하여 파일 이름을 포함한 배열을 생성.
pop() - 배열의 마지막 요소, 즉 파일 이름을 가져옴.
3. var fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
• 파일의 확장자를 추출
• 작동 방식 : fileName.lastIndexOf(".") + 1 - 파일 이름에서 마지막 점(.)의 위치를 찾고, 이 위치 다음 문자부터 문자열의 끝까지를 가져옴.
substring(...) - 파일 확장자를 추출
4. fileExt = fileExt.toLowerCase();
• 파일 확장자를 소문자로 변환하여 대소문자 구분 없이 파일 형식을 검사할 수 있도록 함
5. if (fileExt != "jpg" && fileExt != "jpeg" && fileExt != "gif" && fileExt != "png" && fileExt != "bmp") { ... }
• 파일 확장자가 허용된 이미지 파일 형식인지 확인
• 허용되지 않는 파일 형식인 경우 사용자에게 알림을 띄우고 함수 실행을 종료 (return)
6. $(this).siblings(".custom-file-label").html(fileName);
• 선택한 파일의 이름을 .custom-file-label 클래스를 가진 형제 요소에 표시
• 작동 방식 : $(this).siblings(".custom-file-label") - 현재 파일 입력 요소의 형제 요소 중 .custom-file-label 클래스를 가진 요소를 선택.
.html(fileName) - 선택된 파일 이름을 해당 요소의 HTML로 설정
application.properties 설정 추가
# 파일 한 개당 최대 사이즈
spring.servlet.multipart.max-file-size=20MB
# 요청당 최대 파일 크기
spring.servlet.multipart.max-request-size=100MB
# 상품 이미지 업로드 경로
itemImgLocation=/Users/Desktop/e-commerce project/item
# 리소스 업로드 경로
uploadPath=file:/Users/Desktop/e-commerce project
WebMvcConfigurer 인터페이스 구현
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${uploadPath}")
String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations(uploadPath);
}
}
- addResourceHandlers(ResourceHandlerRegistry registry) : Spring MVC의 리소스 핸들러를 추가하는 메소드. 이 메소드를 통해 리소스(예: 이미지, CSS, JS 파일)의 요청을 처리하는 방법을 설정.
- registry.addResourceHandler("/images/**") : /images/** 패턴의 URL 요청을 리소스 핸들러로 등록. 즉, 클라이언트가 /images/로 시작하는 URL을 요청할 때, 이 핸들러가 요청을 처리함
- addResourceLocations(uploadPath) : uploadPath에 설정된 경로에서 리소스를 제공하도록 지정. 이 경로는 파일 시스템의 위치를 나타내며, 이 경로에서 요청된 파일을 제공하게 됨. 예를 들어, /images/example.jpg 요청이 들어오면 uploadPath로 지정된 경로에서 example.jpg 파일을 찾아서 반환.
https://jake-seo-dev.tistory.com/605
파일을 처리하는 Service 구현
public String uploadFile(String uploadPath, String originalFileName,
byte[] fileData) throws Exception {
// 서로 다른 개체들을 구별하기 위해서 이름을 부여할 때 사용
UUID uuid = UUID.randomUUID();
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
String savedFileName = uuid + extension;
String fileUploadFullUrl = uploadPath + "/" + savedFileName;
FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
fos.write(fileData);
fos.close();
return savedFileName;
}
UUID 생성 : UUID.randomUUID()를 사용하여 파일 이름 충돌을 방지. UUID는 파일 이름의 일부분으로 사용되며, 파일의 유일성을 보장.
파일 확장자 추출 : originalFileName에서 파일 확장자를 추출. 예를 들어, .jpg, .png와 같은 확장자를 추출.
파일 이름 생성 : UUID와 파일 확장자를 결합하여 저장할 파일 이름을 생성.
파일 경로 구성 : uploadPath와 savedFileName을 결합하여 파일의 전체 경로를 생성.
파일 저장
- FileOutputStream 객체를 생성하여 지정된 파일 경로 (fileUploadFullUrl)로 데이터를 쓸 수 있는 출력 스트림을 준비
- 바이트 배열 fileData를 파일에 기록. fileData는 파일의 실제 데이터이며, 이 데이터가 FileOutputStream을 통해 파일에 기록
- 스트림을 닫아 파일 출력을 완료하고 자원을 해제
예외 처리 : throws Exception을 선언하여 예외 발생 시 호출자에게 전달. 예를 들어, 파일 저장 중 IO 오류가 발생할 수 있음.
UUID는 'Universally Unique Identifier'의 약자로 128-bit의 고유 식별자. 다른 고유 ID 생성 방법과 다르게 UUID는 중앙 시스템에 등록하고 발급하는 과정이 없어서 상대적으로 더 빠르고 간단하게 만들 수 있다는 장점이 있다.
https://docs.tosspayments.com/resources/glossary/uuid
public void deleteFile(String filePath) throws Exception{
File deleteFile = new File(filePath);
if(deleteFile.exists()) {
deleteFile.delete();
log.info("파일을 삭제하였습니다.");
} else {
log.info("파일이 존재하지 않습니다.");
}
}
File 클래스 : java.io.File 클래스는 파일 및 디렉토리에 대한 경로를 나타내며, 파일 시스템에서 파일이나 디렉토리에 접근하고 조작하는 기능을 제공
ItemImgRepository 구현
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
}
ItemImgService, ItemImgServiceImpl 구현
public interface ItemImgService {
void saveItemImg(ItemImg itemImg, MultipartFile multipartFile)
throws Exception;
}
@Service
@Transactional
@RequiredArgsConstructor
public class ItemImgServiceImpl implements ItemImgService {
@Value("${itemImgLocation}")
private String itemImgLocation;
private final ItemImgRepository itemImgRepository;
private final FileService fileService;
@Override
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);
}
}
MultipartFile
Spring Framework에서 파일 업로드를 처리하기 위해 사용되는 인터페이스. 주로 웹 애플리케이션에서 클라이언트가 서버로 파일을 전송할 때 사용. 이 인터페이스는 파일 업로드와 관련된 다양한 기능을 제공하며, 파일의 메타데이터와 내용을 쉽게 처리할 수 있게 도와줌.
주요 기능
1. 파일 정보 접근
• getName() : 파일 필드의 이름을 반환
• getOriginalFilename() : 클라이언트가 업로드한 원본 파일 이름을 반환
• getContentType() : 파일의 MIME 타입을 반환 (예: image/png, text/plain 등)
2. 파일 내용 처리
• getBytes() : 파일의 내용을 바이트 배열로 반환
• getInputStream() : 파일 내용을 InputStream 형태로 반환. 이를 통해 파일 내용을 직접 스트리밍하거나 처리할 수 있다.
3. 파일 저장
• transferTo(File dest): 파일을 지정된 경로에 저장. 이 메소드는 파일을 실제 파일 시스템에 저장하는 데 사용.
• 조건 확인 : oriImgName이 비어있지 않은 경우에만 파일 업로드를 수행
• 파일 업로드 : fileService.uploadFile() 메소드를 호출하여 파일을 서버에 업로드. 이 메소드의 결과로 저장된 파일 이름(imgName)을 반환받는다.
• 이미지 URL 생성 : 파일의 접근 경로를 기반으로 URL을 생성. 이 URL은 클라이언트가 이미지에 접근하는 데 사용.
• 상품 이미지 정보 업데이트 : ItemImg 객체의 updateItemImg 메소드를 호출하여 이미지의 원본 이름, 저장된 파일 이름, URL을 업데이트.
• 데이터베이스 저장 : itemImgRepository.save(itemImg)를 호출하여 ItemImg 객체를 데이터베이스에 저장.
ItemService, ItemServiceImpl 구현
public interface ItemService {
Long saveItem(ItemFormDTO itemFormDTO, List<MultipartFile> itemImgFileList)
throws Exception;
}
@Service
@Transactional
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {
private final ItemRepository itemRepository;
private final ItemImgService itemImgService;
private final ItemImgRepository itemImgRepository;
@Override
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();
}
}
- itemRepository.save(item)를 호출하여 Item 객체를 데이터베이스에 저장
- itemImgFileList에 있는 모든 이미지 파일에 대해 반복.
- ItemImg객체 생성 후itemImg.setItem(item)을 호출하여 현재 Item 객체와 ItemImg 객체를 연결
- 첫 번째 이미지 파일 (i == 0)은 대표 이미지로 설정 (itemImg.setRepImgYn("Y"))
- itemImgService.saveItemImg(itemImg, itemImgFileList.get(i))를 호출하여 ItemImg 객체와 이미지 파일을 저장
- 저장된 Item 객체의 ID를 반환
List<MultipartFile> itemImgFileList는 클라이언트에서 전송된 파일들로 구성된 리스트로, MultipartFile 객체는 Spring MVC가 파일 업로드 요청을 처리할 때 자동으로 생성. 이 리스트는 컨트롤러 메소드에서 @RequestParam을 사용하여 수신되며, 이후 서비스 계층에서 파일을 저장하거나 처리하는 데 사용.
ItemController 추가 코드
private final ItemService itemService;
@PostMapping("/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 인터페이스에 findByItemOrderByIdAsc 메소드 추가
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemServiceImplTest {
@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 = "/Users/Desktop/e-commerce project/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 = ItemFormDTO.builder()
.itemNm("테스트상품")
.itemSellStatus(ItemSellStatus.SELL)
.itemDetail("테스트 상품입니다.")
.price(1000)
.stockNum(100)
.build();
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.getStockNum(), item.getStockNum());
assertEquals(multipartFileList.get(0).getOriginalFilename(),
itemImgList.get(0).getOriImgName());
}
}
- MockMultipartFile : Spring의 MockMultipartFile 클래스는 테스트 중 파일 업로드를 시뮬레이션하는 데 사용된다. 실제 파일이 아닌 가상의 파일을 생성할 수 있음. path, imageName, contentType, content를 매개변수로 받아 파일의 메타데이터와 내용을 설정함.
내용 참고 : 책 "스프링 부트 쇼핑몰 프로젝트 with JPA"
'Spring & Spring Boot' 카테고리의 다른 글
Spring Boot 쇼핑몰 프로젝트 | 상품 관리 (0) | 2024.08.17 |
---|---|
Spring Boot 쇼핑몰 프로젝트 | 상품 수정 (0) | 2024.08.13 |
Spring Boot 쇼핑몰 프로젝트 | 엔티티 연관 관계 매핑, 엔티티 공통 속성 공통화 (0) | 2024.08.02 |
Spring Boot 쇼핑몰 프로젝트 | 로그인/로그아웃 화면 연동, 페이지 권한 설정 (0) | 2024.08.01 |
Spring Boot 쇼핑몰 프로젝트 | 로그인 기능 구현 (0) | 2024.08.01 |