사용자가 itemForm.html에서 상품 정보와 이미지를 입력한 뒤 저장 버튼을 누르 ItemController가 요청을 받는다.
Controller는
ItemFormDto : 상품 정보
List : 이미지 파일 목록
을 ItemService로 전달한다.
View
↓
ItemController
↓
ItemService
ItemService에서는 ItemFormDto를 Item Entity로 변환한다.
Item item = itemFormDto.createItem();
변환된 Item 객체를 ItemRepository.save()를 이용하여 DB에 저장한다.
itemRepository.save(item);
상품이 저장되면 생성된 상품 ID가 자동으로 생성되며, 이후 이미지와 연관관계를 맺을 때 사용된다.
ItemFormDto
↓
Item(Entity)
↓
ItemRepository.save()
↓
DB
상품 저장이 완료되면 선택한 이미지 개수만큼 반복문을 실행한다.
for (int i = 0; i < itemImgFileList.size(); i++)
반복마다
를 설정한 후 ItemImgService를 호출한다.
ItemService
↓
ItemImg 생성
↓
ItemImgService
ItemImgService에서는 업로드한 파일의 원본 파일명을 가져온다.
String oriImgName = itemImgFile.getOriginalFilename();
원본 파일명이 존재하면 FileService를 호출하여 실제 파일을 서버에 저장한다.
fileService.uploadFile(...)
FileService는 실제 이미지 파일을 저장하는 역할을 한다.
UUID uuid = UUID.randomUUID();
예) cat.jpg -> 6b9d2f4a-xxxx-xxxx.jpg
String extension =
originalFileName.substring(originalFileName.lastIndexOf("."));
원본 파일의 확장자를 추출한다.
예) cat.jpg -> .jpg
String savedFileName = uuid + extension;
예) 6b9d2f4a.jpg
FileOutputStream fos =
new FileOutputStream(fileUploadFullUrl);
fos.write(fileData);
fos.close();
MultipartFile
↓
getBytes()
↓
byte[]
↓
FileOutputStream.write()
↓
C:/shop/item 폴더
파일 저장이 완료되면
을 ItemImg Entity에 저장한다.
itemImg.updateItemImg(
oriImgName,
imgName,
imgUrl
);
이후 ItemImgRepository.save()를 호출하여 DB에 저장한다.
ItemImg
↓
ItemImgRepository.save()
↓
DB
사용자
↓
itemForm.html
↓
ItemController
↓
ItemService
│
├── ItemFormDto → Item(Entity)
│ ↓
│ ItemRepository.save()
│ ↓
│ DB
│
└── 이미지 개수만큼 반복
↓
ItemImg 생성
↓
Item과 연관관계 설정
↓
대표 이미지 여부 설정
↓
ItemImgService
↓
FileService
↓
UUID 생성 및 파일명 변경
↓
FileOutputStream.write()
↓
서버 폴더에 이미지 저장
↓
ItemImg 정보 저장
↓
ItemImgRepository.save()
↓
DB


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(); // 랜덤으로 UUID를 생성
String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
String savedFileName = uuid.toString() + extension;
String fileUploadFullUrl = uploadPath + "/" + savedFileName;
System.out.println(fileUploadFullUrl);
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 객체 여부 확인
deleteFile.delete();
log.info("파일을 삭제하였습니다.");
} else {
log.info("파일이 존재하지 않습니다.");
}
}
}
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);
}
package com.shop.service;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import jakarta.persistence.EntityNotFoundException;
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}") // application.properties에 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 = "";
System.out.println(oriImgName);
// 파일 업로드
if (!StringUtils.isEmpty(oriImgName)) { // oriImgName 문자열로 비어있지 않으면 실행
System.out.println("******");
imgName = fileService.uploadFile(itemImgLocation, oriImgName,
itemImgFile.getBytes());
System.out.println(imgName);
imgUrl = "/images/item" + imgName;
}
System.out.println("1111");
// 상품 이미지 정보 저장
// oriImgName: 상품 이미지 파일의 원래 이름
// imgName: 실제로 로컬에 저장된 상품 이미지 파일의 이름
// imgUrl: 로컬에 저장된 상품 이미지 파일을 불러오는 경로
itemImg.updateItemImg(oriImgName, imgName, imgUrl);
System.out.println("(((((");
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;
// 변경된 상품 이미지 정보를 세팅
// 상품 등록을 하는 경우에는 ItemImgRepository.save() 로직을 호출하지만
// 호출을 하지 않았다.
// savedItemImg 엔티티는 영속성 상태이다.
// 그래서 데이터를 변경하는 것만으로 변경을 감지하는 기능이 동작
// 트랜잭션이 끝날 때 update 쿼리가 실행된다.
// 영속성 상태여야만 사용 가능
savedItemImg.updateItemImg(oriImgName, imgName, imgUrl);
}
}
}
package com.shop.service;
import com.shop.dto.ItemFormDto;
import com.shop.dto.ItemImgDto;
import com.shop.entity.Item;
import com.shop.entity.ItemImg;
import com.shop.repository.ItemImgRepository;
import com.shop.repository.ItemRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
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 = 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();
}
@Transactional(readOnly = true)
public ItemFormDto getItemDtl(Long itemId) {
// Entity
List<ItemImg> itemImgList = itemImgRepository.findByItemIdOrderByIdAsc(itemId);
// DB에서 데이터를 가지고 온다.
// DTO
List<ItemImgDto> itemImgDtoList = new ArrayList<>();
for (ItemImg itemimg : itemImgList) {
// Entity -> Dto
ItemImgDto itemImgDto = ItemImgDto.of(itemimg);
itemImgDtoList.add(itemImgDto);
}
Item item = itemRepository.findById(itemId).orElseThrow(EntityNotFoundException::new);
// Item -> ItemFormDto modelMapper
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));
}
return item.getId();
}
}
package com.shop.dto;
import com.shop.constant.ItemSellStatus;
import com.shop.entity.Item;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.mapping.Array;
import org.modelmapper.ModelMapper;
import org.springframework.ui.ModelMap;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class ItemFormDto {
// Item
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;
// --------------------------------------------------------------
// ItemImg
private List<ItemImgDto> itemImgDtoList = new ArrayList<>(); // 상품 이미지 정보
private List<Long> itemImgIds = new ArrayList<>(); // 상품 이미지 아이디
// ---------------------------------------------------------------
// ModelMapper
private static ModelMapper modelMapper = new ModelMapper();
public Item createItem() {
// ItemFormDto -> Item 연결
return modelMapper.map(this, Item.class);
}
public static ItemFormDto of(Item item) {
// Item -> ItemFormDto 연결
return modelMapper.map(item, ItemFormDto.class);
}
}
package com.shop.entity;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity{
@Id
@Column(name = "item_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id; // 상품 코드
@Column(nullable = false, length = 50)
private String itemNm; // 상품명
@Column(name = "price", nullable = false)
private int price; // 가격
@Column(nullable = false)
private int stockNumber; // 수량
@Lob
@Column(nullable = false)
private String itemDetail; // 상품 상세 설명
@Enumerated
private ItemSellStatus itemSellStatus; // 상품 판매 상태
// private LocalDateTime regTime; // 등록 시간
//
// private LocalDateTime updateTime; // 수정 시간
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "member_item",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Member> member;
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();
}
}
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 jakarta.persistence.EntityNotFoundException;
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.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
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 {
// MultipartFile -> 비정형 데이터 이미지 파일
// 이미지파일 List
List<MultipartFile> multipartFileList = new ArrayList<>();
// 5번 반복
for (int i = 0; i < 5; i++) {
String path = "C:/shop/item/"; // 경로를 정의 한다. 문자열 변수로
String imageName = "image" + i + ".jpg"; // 파일명을 만들어준다. img0.jpg..img4.jpg
// MockMultipartFile -> 가상 MultipartFile
// path -> 경로
MockMultipartFile mockMultipartFile =
new MockMultipartFile(path, imageName,
"image/jpg", new byte[]{1, 2, 3, 4});
multipartFileList.add(mockMultipartFile);
}
return multipartFileList;
}
@Test
@DisplayName("상품 등록 테스트")
void saveItem() throws Exception {
ItemFormDto itemFormDto = new ItemFormDto();
itemFormDto.setItemNm("테스트상품");
itemFormDto.setItemSellStatus(ItemSellStatus.SELL);
itemFormDto.setItemDetail("테스트 상품 입니다.");
itemFormDto.setPrice(10000);
itemFormDto.setStockNumber(100);
// 이미지 리스트가 생성됐다
List<MultipartFile> multipartFileList = createMultipartFiles();
// itemService -> save 진행
// ItemFormDto -> item 정보
// multipartFileList -> item 이미지 리스트
Long itemId = itemService.saveItem(itemFormDto, multipartFileList);
// itemImgRepository -> ItemImg 리스트를 받는다.
List<ItemImg> itemImgList =
itemImgRepository.findByItemIdOrderByIdAsc(itemId);
// ItemRepository -> Item 추출
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());
}
}
package com.shop.service;
import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
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.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
public Member createMember() {
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail("test@email.com");
memberFormDto.setName("홍길동");
memberFormDto.setAddress("서울시 마포구 합정동");
memberFormDto.setPassword("1234");
return Member.createMember(memberFormDto, passwordEncoder);
}
@Test
@DisplayName("회원가입 테스트")
public void saveMemberTest() {
Member member = createMember();
Member savedMember = memberService.saveMember(member);
// assertEquals member savedMember와 데이터가 같은지 비교한다.
assertEquals(member.getEmail(), savedMember.getEmail());
assertEquals(member.getName(), savedMember.getName());
assertEquals(member.getAddress(), savedMember.getAddress());
assertEquals(member.getPassword(), savedMember.getPassword());
assertEquals(member.getRole(), savedMember.getRole());
}
@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMemberTest() {
Member member1 = createMember();
Member member2 = createMember();
memberService.saveMember(member1);
Throwable e = assertThrows(IllegalStateException.class, () -> {
memberService.saveMember(member2);
});
assertEquals("이미 가입된 회원입니다.", e.getMessage());
}
}
package com.shop.controller;
import com.shop.dto.ItemFormDto;
import com.shop.service.ItemService;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping(value = "/admin/item/new")
public String itemForm(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:/";
}
@GetMapping(value = "/admin/item/{itemId}")
public String itemDtl(@PathVariable("itemId") Long itemId, Model model) {
try {
ItemFormDto itemFormDto = itemService.getItemDtl(itemId);
model.addAttribute("itemFormDto", itemFormDto);
} catch (EntityNotFoundException e) {
model.addAttribute("errorMessage", "존재하지 않는 상품입니다.");
model.addAttribute("itemFormDto", new ItemFormDto());
// return "item/itemForm";
}
return "item/itemForm";
}
@PostMapping(value = "/admin/item/{itemId}")
public String itemUpdate(@Valid ItemFormDto itemFormDto, BindingResult bindingResult,
@RequestParam("itemImgFile") List<MultipartFile> itemImgFileList,
Model model) {
if (bindingResult.hasErrors()) {
return "item/itemForm";
}
if (itemImgFileList.get(0).isEmpty() && itemFormDto.getId() == null) {
model.addAttribute("errorMessage", "첫번째 상품 이미지는 필수 입력 값입니다.");
return "item/itemForm";
}
try {
itemService.updateItem(itemFormDto, itemImgFileList);
} catch (Exception e) {
model.addAttribute("errorMessage", "상품 수정 중 에러가 발생하였습니다.");
return "item/itemForm";
}
return "redirect:/"; // 다시 실행
}
}
package com.shop.repository;
import com.shop.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item> {
// select*from item where itemNm = itemNm;
List<Item> findByItemNm(String itemNm);
List<Item> findByItemNmOrItemDetail(String itemNm, String itemDetail);
List<Item> findByPriceLessThan(Integer price);
List<Item> findByPriceLessThanOrderByPriceDesc(Integer price);
@Query("select i from Item i where i.itemDetail like %:itemDetail% order by i.price desc")
List<Item> findByItemDetail(@Param("itemDetail")String itemDetail);
@Query(value = "select * from item i where i.item_detail like %:itemDetail% order by i.price desc",
nativeQuery = true)
List<Item> findByItemDetailNative(@Param("itemDetail") String itemDetail);
}
오늘도 Spring을 배웠다.
상품 등록, 수정, 이미지 파일 업로드를 배웠는데 생각보다 정말 복잡하다!