[쇼핑몰] 상품 등록하기 - 2
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function(){
//상품 등록 시 실패 메시지를 받아서
//상품 등록 페이지에 재진입 시 alert를 통해서 실패 사유를 보여줌
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;
}
//label 태그 안의 내용을 jquery의 .html()을 이용하여 파일명을 입력
$(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>
- 파일을 전송할 때는 form 태그에 enctype(인코딩 타입) 값으로 "multipart/form-data"를 입력
- 모든 문자를 인코딩하지 않음을 명시, 속성은 method 속성값이 "post"인 경우 사용
<form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
- 상품 판매 상태의 경우 판매 중과 품절 상태가 있다.
- 상품주문이 많이 들어와서 재고가 없을 경우 주문 시 품절 상태로 바꿔줄 수 있다.
- 상품을 등록만 먼저 해놓고 팔지 않을 경우에도 이용
<div class="form-group">
<select th:field="*{itemSellStatus}" class="custom-select">
<option value="SELL">판매중</option>
<option value="SOLD_OUT">품절</option>
</select>
</div>
//상품 이미지 정보를 담고 있는 리스트가 비어 있다면 상품을 등록
<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
//타임리프의 유틸리티 객체 #numbers.sequence(start,end)를 이용하면
//start부터 end까지 반복 처리를 할 수 있다.
//상품 등록 시 이미지 개수를 최대 5개
<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 태그에는 몇 번째 상품 이미지인지 표시
<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">
//상품수정 시 어떤 이미지가 수정됐는지 알기 위해서
//상품 이미지의 아이디를 hidden값으로 숨겨둠
<input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
//타임리프의 유틸리티 객체인 #string.isEmpty(string)을 이용하여
//저장된 이미지 정보가 있다면 파일의 이름을 보여주고
//없다면 '상품 이미지 + 번호'를 출력
<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>
application.properties 설정 변경
//애플리케이션 실행 시점에 테이블을 삭제한 수 재생성하지 않는다.
//엔티티와 테이블이 매핑이 정상적으로 되어 있는지만 확인
//엔티티를 추가가 필요할 경우 create와 validate를 번갈아 가면서 사용
spring.jpa.hibernate.ddl-auto=validate
application-test.properties 설정 추가하기
//application.properties의 설정이 validate이면 테스트 코드 실행 시 테이블이 자동으로 생성되지 않음
//테스트 환경에서는 ddl-auto를 create로 설정
spring.jpa.hibernate.ddl-auto=create
- 이미지 파일을 등록할 때 서버에서 각 파일의 최대 사이즈와 한번에 다운 요청할 수 있는 파일의 크기를 지정
- 컴퓨터에서 어떤 경로에 저장할지를 관리하기 위해서 프로퍼티에 itemImgLocation을 추가
application.properties 설정 추가
#파일 한개당 최대 사이즈
spring.servlet.multipart.max-file-size=20MB
#요청당 최대 파일 크기
spring.servlet.multipart.max-request-size=100MB
#상품이미지 업로드 경로
itemImgLocation=C:/shop/item
#리소스 업로드 경로
uploadPath=file:///C:/shop/
- 업로드한 파일을 읽어올 경로를 설정
- WebMvcConfigurer 인터페이스를 구현하는 WebMvcConfig.java 파일을 작성
- addResourceHandlers 메소드를 통해서 자신의 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정
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 {
//application.properties에 설정한 "uploadPath"프로퍼티 값을 읽어옴
@Value("${uploadPath}")
String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//웹브라우저에 입력하는 url에 /images로 시작하는 경우
//uploadPath에 설정한 폴더를 기준으로 파일을 읽어 오도록 설정
registry.addResourceHandler("/images/**")
//로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정
.addResourceLocations(uploadPath);
}
}
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(Universally Unique Identifier)는 서로 다른 개체들을 구별하기 위해서 이름을 부여할 때 사용
//실제 사용 시 중복될 가능성이 거의 없기 때문에 파일의 이름으로 사용하면
//파일명 중복 문제를 해결
UUID uuid = UUID.randomUUID();
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
//UUID로 받은 값과 원래 파일의 이름의 확장자를 조합해서
//저장될 파일 이름을 만듬
String savedFileName = uuid.toString() + extension;
String fileUploadFullUrl = uploadPath + "/" + savedFileName;
//FileOutpuStream 클래스는 바이트 단위의 출력을 내보내는 클래스
//생성자로 파일이 저장될 위치와 파일의 이름을 넘겨
//파일에 쓸 파일 출력 스트림을 만듬
FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
//fileData를 파일 출력 스트림에 입력
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("파일이 존재하지 않습니다.");
}
}
}
- 상품의 이미지 정보를 저장하기 위해서 repository 패키지 아래에 JpaRepository를 상속받는 ItemImgRepository인터페이스를 만듬
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> {
}
- 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스를 service 패키지 아래에 생성
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;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;
@Service
@RequiredArgsConstructor
@Transactional
public class ItemImgService {
//@Value 어노테이션을 통해 application.properties파일에
//등록한 itemImgLocation 값을 불러와서 itemImgLocation 변수에 넣어줌
@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)){
//사용자가 상품의 이미지를 등록했다면 저장할 경로와
//파일의 이름, 파일을 파일의 바이트 배열을 파일 업로드 파라미터로
//uploadFile메소드를 호출한다.
//호출 결과 로컬에 저장된 파일의 이름을 imgName변수에 저장
imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
//저장한 상품 이미지를 불러올 경로를 설정
//외부 리소스를 불러오는 urlPatterns로 WebMvcConfig클래스에서
//"/images.**"를 설정해준다.
//또한 application.properties에서 설정한 uploadPath프로퍼티 경로인
//"C:/shop/"아래 item폴더에 이미지를 저장하므로 상품 이미지를 불러오는 경로로
//"/images/item"를 붙여준다.
imgUrl = "/images/item/" + imgName;
}
//입력받은 상품 이미지 정보를 저장
itemImg.updateItemImg(oriImgName,imgName,imgUrl);
itemImgRepository.save(itemImg);
}
}
ItemService.java
package com.shop.service;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemImgDto;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
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.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
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 item = itemFormDto.createItem();
//상품데이터를 저장합니다.
itemRepository.save(item);
for(int i = 0 ; i<itemImgFileList.size();i++){
ItemImg itemImg = new ItemImg();
itemImg.setItem(item);
//첫번째 이미지일 경우 대표 상품 이미지 여부 값을 "Y"로 세팅
//나머지 상품 이미지는 "N"으로 설정
if(i == 0)
itemImg.setRepimgYn("Y");
else
itemImg.setRepimgYn("N");
//상품의 이미지 정보를 저장
itemImgService.saveItemImg(itemImg,itemImgFileList.get(i));
}
return item.getId();
}
}