상품 등록

루민 ·2023년 4월 5일
0

📝상품 엔티티


상품(item) 엔티티
상품 사진(item_image) 엔티티


📝Repository


ItemRepository

public interface ItemRepository extends JpaRepository<Item, Long> {
}

ItemImageRepository

public interface ItemImageRepository extends JpaRepository<ItemImage, Long> {
}


📝application.properties

file.dir=C:/Users/Woomin/Desktop/study/ImageStorage/
  • application.properties파일에 이미지 업로드 경로를 지정해주었습니다.
  • 이미지 파일들은 데이터베이스에 저장하지 않고 로컬 컴퓨터의 스토리지에 저장(AWS 같은 경우 AWS S3)하고, 데이터베이스에는 이미지 파일의 정보(경로)만 저장되도록 합니다.


📝Service

fileHandler

@Component
public class FileHandler {

    @Value("${file.dir}")
    private String fileDir;

    //파일 경로명
    public String getFullPath(String filename) {
        return fileDir + filename;
    }

    //파일 경로명(스토리지)에 사진 저장
    public List<ItemImage> storeImages(List<MultipartFile> multipartFiles) throws IOException {
        List<ItemImage> storeResult = new ArrayList<>();
        for (MultipartFile multipartfile : multipartFiles) {
            storeResult.add(storeImage(multipartfile));
        }
        return storeResult;
    }

    public ItemImage storeImage(MultipartFile multipartFile) throws IOException {
        if (multipartFile.isEmpty()) {
            return null;
        }

        // ex) image1.jpeg
        String oriImageName = multipartFile.getOriginalFilename();

        //서버에 저장될 파일명
        String storeImageName = createStoreImageName(oriImageName);

        //스토리지에 저장
        multipartFile.transferTo(new File(getFullPath(storeImageName)));

        return ItemImage.builder()
                .originalName(oriImageName)
                .storeName(storeImageName)
                .build();
    }

    private String createStoreImageName(String oriImageName) {
        String ext = extractExt(oriImageName);  //jpeg
        String uuid = UUID.randomUUID().toString();
        return uuid + "." + ext;
    }

    //확장자 추출
    private String extractExt(String oriImageName) {
        int pos = oriImageName.lastIndexOf(".");
        return oriImageName.substring(pos + 1);
    }
}
  • 이미지 파일 저장과 관련된 업무를 처리하기 위한 component입니다.
  • @Value("${file.dir}") 로 application.properties에 작성해둔 이미지 업로드 경로를 쉽게 불러올 수 있습니다.
  • public ItemImage storeImage(MultipartFile multipartFile) 메소드는 업로드된 이미지인 multipartFile로 원본 파일명, 서버에 저장될 파일명을 추출하고 스토리지에 저장하는 업무를 처리합니다.

    굳이 원본 파일명과 서버에 저장하는 파일명을 구분하는 이유?
    사용자가 얼마 없다면 파일명이 중복되는 문제가 발생할 가능성이 없지만 만약 사용자가 수십만명이 된다면 파일명이 충분히 중복될 가능성이 높고 기존 파일 이름과 충돌이 나기 때문에 문제가 발생될 수 있습니다. 때문에 중복이 불가능한 UUID를 생성하고 서버에서 관리하는 별도의 파일명으로 저장합니다.


ItemService

@Service
@RequiredArgsConstructor
@Transactional
public class ItemService {

    private final ItemRepository itemRepository;
    private final ItemImageRepository itemImageRepository;
    private final FileHandler filehandler;

    public Long saveItem(ItemServiceDTO itemServiceDTO, List<MultipartFile> multipartFileList) throws IOException {
        Item item = Item.createItem(itemServiceDTO.getName(),
                itemServiceDTO.getDescription(),
                itemServiceDTO.getPrice(),
                itemServiceDTO.getStockQuantity());

        List<ItemImage> itemImages = filehandler.storeImages(multipartFileList);


        for (ItemImage itemImage : itemImages) {
            item.addItemImage(itemImageRepository.save(itemImage));
        }

        return itemRepository.save(item).getId();
    }
}
  • 상품 정보를 저장하는 서비스 로직입니다.
  • DI(Dependecy Injection) : ItemRepository, ItemImageRepository, FileHandler
  • filehandler.storeImages 메소드를 통해 이미지들을 넘겨 스토리지에 저장하고 원본 파일명, 서버에 저장될 파일명을 추출하고 ItemImage list로 반환받습니다.
  • itemImageRepository에 상품 이미지 정보를 저장함과 동시에 연관 관계 편의 메소드를 호출해 양쪽에 값을 세팅해줍니다.
    public void addItemImage(ItemImage itemImage) {
        itemImageList.add(itemImage);
        itemImage.changeItem(this);
    }


📝Controller

@Controller
@Slf4j
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    @GetMapping("/items/new")
    public String createItemForm(Model model) {
        model.addAttribute("itemForm", new ItemForm());
        return "item/itemForm";
    }

    @PostMapping("/items/new")
    public String createItem(@Valid @ModelAttribute ItemForm itemForm, BindingResult bindingResult, Model model,
                            @RequestPart(name = "itemImages") List<MultipartFile> multipartFiles
    ) throws IOException {

        if (bindingResult.hasErrors()) return "item/itemForm";

        //상품 이미지를 등록안하면
        if (multipartFiles.get(0).isEmpty()) {
            model.addAttribute("errorMessage", "상품 사진을 등록해주세요!");
            return "item/itemForm";
        }

        itemService.saveItem(itemForm.toServiceDTO(), multipartFiles);

        return "redirect:/userHome";
    }

    /**
     * 컨트롤러와 서비스간 통신을 할 때, 컨트롤러가 뷰와 통신할 때 사용한 DTO를 그대로 사용하면
     * 강한 의존이 생겨 위험!!
     */
}

GetMapping("/items/new")

  • 상품 등록 폼
  • ItemForm을 Model을 통해 뷰로 넘겨주었습니다. thymeleaf는 이 객체를 통해 유효성 검증을 할 수 있습니다.

PostMapping("/items/new")

  • 시행착오
  • 상품 이름, 가격, 수량을 입력하지 않거나 상품 이미지를 등록하지 않으면 다시 상품 입력 화면으로 오도록 구현하였습니다.
  • DTO의 사용 범위 : 컨트롤러에서 DTO를 엔티티로 바꾸어서 서비스로 넘겨줄 지 서비스에서 엔티티로 바꿀지 고민을 하였는데 현재 프로젝트에서는 서비스에서 엔티티로 변환하게 구현하였습니다.
  • DTO에 관해서는 다음에 자세하게 게시글로 작성하겠습니다!


📝Test

  @PostMapping("/items/new")
    @ResponseBody
    public ResponseEntity<String> createItem(@ModelAttribute(name = "itemForm") ItemForm form,
                                             @RequestPart(name = "itemImages") List<MultipartFile> multipartFiles) throws IOException {

        itemService.saveItem(form.toServiceDTO(), multipartFiles);

        return ResponseEntity.ok("이미지 업로드 성공");
    }
  • 상품 등록 화면 없이 PostMan을 이용해서 테스트를 진행해보았습니다.


  • 성공적으로 스토리지에도 사진들이 업로드 된것을 확인하였습니다.



📝결과 화면

상품 이름, 가격, 수량 등을 누락했을 경우


상품 이미지를 등록 안했을 경우


등록 후 데이터베이스 저장 내역

0개의 댓글