상품 등록하기

진크·2022년 3월 1일
0
post-thumbnail

ItemImg 엔티티 생성

이전에 상품(Item) 엔티티 클래스를 만들었었는데, 이번에는 상품의 이미지를 저장하는 상품 이미지 엔티티를 만들겠습니다.

package me.jincrates.gobook.domain.items;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;

@Getter @ToString
@NoArgsConstructor
@Table(name = "item_img")
@Entity
public class ItemImg {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "item_img_id")
    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;

    @Builder
    public ItemImg(String imgName, String oriImgName, String imgUrl, String repimgYn, Item item) {
        this.imgName = imgName;
        this.oriImgName = oriImgName;
        this.imgUrl = imgUrl;
        this.repimgYn = repimgYn;
        this.item = item;
    }

    public void updateItemImg(String oriImgName, String imgName, String imgUrl) {
        this.oriImgName = oriImgName;
        this.imgName = imgName;
        this.imgUrl = imgUrl;
    }
}

ItemImgDto 생성

package me.jincrates.gobook.web.dto;

import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.domain.items.ItemImg;

@Data
@RequiredArgsConstructor
public class ItemImgDto {

    private Long id;

    private String imgName;

    private String oriImgName;

    private String imgUrl;

    private String repImgYn;

    @Builder
    public ItemImgDto(String imgName, String oriImgName, String imgUrl, String repImgYn) {
        this.imgName = imgName;
        this.oriImgName = oriImgName;
        this.imgUrl = imgUrl;
        this.repImgYn = repImgYn;
    }

    public ItemImg toEntity(ItemImgDto dto) {
        ItemImg entity = ItemImg.builder()
                .imgName(dto.imgName)
                .oriImgName(dto.oriImgName)
                .imgUrl(dto.imgUrl)
                .repimgYn(dto.repImgYn)
                .build();

        return entity;
    }

    public ItemImgDto of(ItemImg entity) {
        ItemImgDto dto = ItemImgDto.builder()
                .imgName(entity.getImgName())
                .oriImgName(entity.getOriImgName())
                .imgUrl(entity.getImgUrl())
                .repImgYn(entity.getRepimgYn())
                .build();

        return dto;
    }
}

ItemFormDto 생성

다음은 상품 등록 및 수정에 사용할 데이터 전달용 DTO 클래스를 만들겠습니다.

package me.jincrates.gobook.web.dto;

import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import me.jincrates.gobook.domain.items.Item;
import me.jincrates.gobook.domain.items.ItemSellStatus;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
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<>();

    @Builder
    public ItemFormDto(String itemNm, Integer price, String itemDetail, Integer stockNumber, ItemSellStatus itemSellStatus) {
        this.itemNm = itemNm;
        this.price = price;
        this.itemDetail = itemDetail;
        this.stockNumber = stockNumber;
        this.itemSellStatus = itemSellStatus;
    }

    public Item toEntity(ItemFormDto dto) {
        Item entity = Item.builder()
                .itemNm(dto.itemNm)
                .itemDetail(dto.itemDetail)
                .itemSellStatus(dto.itemSellStatus)
                .price(dto.price)
                .stockNumber(dto.stockNumber)
                .build();

        return entity;
    }
    
    public ItemFormDto of(Item entity) {
        ItemFormDto dto = ItemFormDto.builder()
                .itemNm(entity.getItemNm())
                .itemDetail(entity.getItemDetail())
                .itemSellStatus(entity.getItemSellStatus())
                .price(entity.getPrice())
                .stockNumber(entity.getStockNumber())
                .build();
        
        return dto;
    }
}
  • List<ItemImgDto> itemImgDtoList : 상품 저장 후 수정할 때 상품 이미지 정보를 저장하는 리스트입니다.
  • List<Long> itemImgIds : 상품의 이미지 아이디를 저장하는 리스트입니다. 수정 시에 이미지 아이디를 담아둘 용도로 사용합니다.
  • toEntity(dto) : dto를 엔티티로 변환하는 작업을 위해 만든 메소드입니다.
  • of(entity) : 엔티티를 DTO로 변환하는 작업을 위해 만든 메소드입니다.

ItemController 수정

package me.jincrates.gobook.web;

import me.jincrates.gobook.web.dto.ItemFormDto;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ItemController {

    @GetMapping(value = "/admin/item/new")
    public String itemForm(Model model) {
        model.addAttribute("itemFormDto", new ItemFormDto());
        return "/item/itemForm";
    }
}

itemForm.html 수정

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/default}">

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">
        $(document).ready(function(){
            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;
                }

                $(this).siblings(".custom-file-label").html(fileName);
            });
        }

    </script>

</th:block>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .fieldError {
            color: #bd2130;
        }
    </style>
</th:block>

<div layout:fragment="content" class="content w-75 py-5 m-auto">
    <form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">

        <p class="h2">
            상품 등록
        </p>

        <input type="hidden" th:field="*{id}">

        <div class="form-group">
            <select th:field="*{itemSellStatus}" class="custom-select">
                <option value="SELL">판매중</option>
                <option value="SOLD_OUT">품절</option>
            </select>
        </div>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">상품명</span>
            </div>
            <input type="text" th:field="*{itemNm}" class="form-control" placeholder="상품명을 입력해주세요">
        </div>
        <p th:if="${#fields.hasErrors('itemNm')}" th:errors="*{itemNm}" class="fieldError">Incorrect data</p>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">가격</span>
            </div>
            <input type="number" th:field="*{price}" class="form-control" placeholder="상품의 가격을 입력해주세요">
        </div>
        <p th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="fieldError">Incorrect data</p>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">재고</span>
            </div>
            <input type="number" th:field="*{stockNumber}" class="form-control" placeholder="상품의 재고를 입력해주세요">
        </div>
        <p th:if="${#fields.hasErrors('stockNumber')}" th:errors="*{stockNumber}" class="fieldError">Incorrect data</p>

        <div class="input-group">
            <div class="input-group-prepend">
                <span class="input-group-text">상품 상세 내용</span>
            </div>
            <textarea class="form-control" aria-label="With textarea" th:field="*{itemDetail}"></textarea>
        </div>
        <p th:if="${#fields.hasErrors('itemDetail')}" th:errors="*{itemDetail}" class="fieldError">Incorrect data</p>

        <div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
            <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 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">
                    <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
                    <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>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

    </form>
</div>

</html>

파일을 전송할 때는 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>

상품 이미지의 경우에는 상품을 등록할 때와 상품을 수정할 때 2가지 경우로 나뉩니다.

<div th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}">
    <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 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">
            <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}">
            <label class="custom-file-label" th:text="${not #strings.isEmpty(itemImgDto.oriImgName)} ? ${itemImgDto.oriImgName} : '상품이미지' + ${status.index+1}"></label>
        </div>
    </div>
</div>
  • th:if="${#lists.isEmpty(itemFormDto.itemImgDtoList)}" : 상품 이미지 정보를 담고 있는 리스트가 비어있다면 상품을 등록하는 경우입니다.
  • th:each="num: ${#numbers.sequence(start, end)} : 타임리프의 유틸리티 객체로 start부터 end까지 반복 처리를 할 수 있습니다. 상품 등록 시 이미지의 개수는 최대 5개로 하겠습니다.
  • th:text="상품이미지 + ${num}" : label 태그에는 몇 번째 상품 이미지인지 표시를 합니다.
  • th:if="${not #lists.isEmpty(itemFormDto.itemImgDtoList)}" : 상품 이미지 정보를 담고 있는 리스트가 비어 있지 않다면 상품을 수정하는 경우입니다.
  • <input type="hidden" name="itemImgIds" th:value="${itemImgDto.id}"> : 상품 수정 시 어떤 이미지가 수정됐는지를 알기 위해서 상품 이미지의 id를 hidden 값으로 숨겨둡니다.
  • th:text="${not #strings.isEmpty(itemImgDto.oriImgName)} : 저장된 이미지 정보가 있다면 파일의 이름을 보여주고, 없다면 ‘상품 이미지 + 번호'를 출력합니다.

이제 상품 등록 페이지에 접근해보겠습니다. 현재는 소스를 작성하면서 애플리케이션을 재실행하면 테이블을 삭제하고 다시 만들기 때문에 이전에 가입했던 회원 데이터도 삭제됩니다. 소스를 수정하고 애플리케이션을 재실행하면 다시 회원 가입을 해야하기 때문에 귀찮을 수 있습니다.

이 과정을 생략하기 위해 application.ymlddl-auto 속성을 validate로 변경하면 애플리케이션 실행 시점에 테이블을 삭제한 후 재생성하지 않으며 엔티티와 테이블이 매핑이 정상적으로 되어 있는지만 확인합니다. 엔티티 추가가 필요한 경우 create와 validate를 번갈아 가면서 사용하면 조금 편하게 개발을 진행할 수 있습니다.

spring:
	jpa:
	  hibernate:
	    ddl-auto: validate

application.yml 설정 추가하기

이미지 파일을 등록할 때 서버에서 각 파일의 최대 사이즈와 한번에 다운 요청할 수 있는 파일의 크기를 지정할 수 있습니다. 또한 PC에서 어떤 경로에 저장할지를 관리하기 위해서 yaml에 itemImgLocation을 추가하겠습니다. 리소스 업로드 경로도 추가하겠습니다.

spring:
	servlet:
    multipart:
      maxFileSize: 20MB
      maxRequestSize: 100MB

itemImgLocation: /Users/jincrates/projects/upload/shop/item # C:/shop/item
uploadPath: file:///Users/jincrates/projects/upload/shop/  #file:///C:/shop/

WebMvcConfig 생성

WebMvcConfigurer 인터페이스를 구현하는 WebMvcConfig 클래스를 생성합니다.

package me.jincrates.gobook.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 {

    @Value("${uploadPath}")
    String uploadPath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**")
                .addResourceLocations(uploadPath);
    }
}
  • @Value("${uploadPath}") : application.yml에 설정한 “uploadPath” 값을 읽어옵니다.
  • addResourceHandler("/images/**") : 웹 브라우저에 입력하는 url에 /images로 시작하는 경우 uploadPath에 설정한 폴더를 기준으로 파일을 읽어오도록 설정합니다.
  • addResourceLocations(uploadPath) : 로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정합니다.

FileServce 클래스 생성

파일을 처리하는 FileService 클래스를 생성합니다. 파일을 업로드하는 메소드와 삭제하는 메소드를 작성하겠습니다.

package me.jincrates.gobook.service;

import lombok.extern.java.Log;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileOutputStream;
import java.util.UUID;

@Log
@Service
public class FileService {

    public String uploadFile(String uploadPath, String originalFileName, byte[] fileData) throws Exception {
        UUID uuid = UUID.randomUUID();
        String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
        String savedFileName = uuid.toString() + extension;
        String fileUploadFullUrl = uploadPath + "/" + savedFileName;

        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()) {
            log.info("파일을 삭제하였습니다.");
        } else {
            log.info("파일이 존재하지 않습니다.");
        }
    }
}
  • UUID(Universally Unique Identifier) : 서로 다른 개체들을 구별하기 위해서 이름을 부여할 때 사용합니다. 실제 사용 시 중복될 가능성이 거의 없기 때문에 파일의 이름으로 사용하면 파일명 중복 문제를 해결할 수 있습니다.(100프로 해결은 아닙니다.)
  • FileOutputStream 클래스 : 바이트 단위의 출력을 내보내는 클래스입니다. 생성자로 파일이 저장될 위치와 파일의 이름을 넘겨 파일에 쏠 파일 출력 스트림을 만듭니다.
  • fos.write(fileData) : fileData를 파일 출력 스트림에 입력합니다.
  • new File(filePath) : 파일이 저장된 경로를 이용하여 파일 객체를 생성합니다.

ItemImgRepository 생성

package me.jincrates.gobook.domain.items;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
}

ItemImgService 생성

다음은 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스를 생성합니다.

package me.jincrates.gobook.service;

import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.domain.items.ItemImg;
import me.jincrates.gobook.domain.items.ItemImgRepository;
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;

@RequiredArgsConstructor
@Transactional
@Service
public class ItemImgService {

    @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)) {
            imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
            imgUrl = "/images/item/" + imgName;
        }

        //상품 이미지 정보 저장
        itemImg.updateItemImg(oriImgName, imgName, imgUrl);
        itemImgRepository.save(itemImg);
    }

}
  • fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes()) : 사용자가 상품의 이미지를 등록했다면 저장할 경로와 파일의 이름, 파일의 바이트 배열을 파일 업로드 파라미터로 uploadFile 메소드를 호출합니다. 호출 결과 로컬에 저장된 파일의 이름을 imgName 변수에 저장합니다.

ItemService 생성

상품 하나를 등록하기 위해서 정말 많은 클래스들을 작성하고 있습니다. 다음은 상품을 등록하는 ItemService입니다.

package me.jincrates.gobook.service;

import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.domain.items.Item;
import me.jincrates.gobook.domain.items.ItemImg;
import me.jincrates.gobook.domain.items.ItemImgRepository;
import me.jincrates.gobook.domain.items.ItemRepository;
import me.jincrates.gobook.web.dto.ItemFormDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RequiredArgsConstructor
@Transactional
@Service
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.toEntity(itemFormDto);
        itemRepository.save(item);

        //이미지 등록
        for (int i = 0, max = itemImgFileList.size(); i < max; i++) {
            ItemImg itemImg = ItemImg.builder()
                    .item(item)
                    .repimgYn(i == 0 ? "Y" : "N")
                    .build();

            itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
        }

        return item.getId();
    }
}
  • repimgYn(i == 0 ? "Y" : "N") : 첫 번째 이미지일 경우 대표 상품 이미지 여부 값을 “Y”로 셋팅합니다.

ItemControllerd 수정

마지막으로 상품을 등록하는 url을 ItemController 클래스에 추가하겠습니다. 상품 등록시 필수 값이 없다면 다시 상품 등록 페이지로 전환하며, 상품이 정상적으로 등록되었다면 메인 페이지로 이동하겠습니다.

package me.jincrates.gobook.web;

//..기존 임포트 생략
import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.service.ItemService;
import me.jincrates.gobook.web.dto.ItemFormDto;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;
import java.util.List;

@RequiredArgsConstructor
@Controller
public class ItemController {

    private final ItemService itemService;

    //..기존 코드 생략

    @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:/";
    }
}

테스트 코드 작성

비즈니스가 점점 커져가면서 상품에 추가되는 데이터들이 많습니다. 즉, 소스를 수정할 일이 많기 때문에 저장 로직에 대한 테스트 코드와 테스트 케이스르 잘 만들어 주는 것이 중요합니다.

이미지가 잘 저장됐는지 테스트 코드를 작성하기 위해서 ItemImgRepository 인터페이스에 findByItemIdOrderByIdAsc 메소드를 추가하겠습니다.

package me.jincrates.gobook.domain.items;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {

    List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}
package me.jincrates.gobook.service;

import me.jincrates.gobook.domain.items.*;
import me.jincrates.gobook.web.dto.ItemFormDto;
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.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class ItemServiceTest {

    @Autowired
    ItemService itemService;

    @Autowired
    ItemRepository itemRepository;

    @Autowired
    ItemImgRepository itemImgRepository;

    List<MultipartFile> createMultipartFiles() throws Exception {
        List<MultipartFile> multipartFileList = new ArrayList<>();

        //path 제일 앞에 경로를 /User로 수정함
        for (int i = 0; i < 5; i++) {
            String path = "/Users/jincrates/projects/upload/shop/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")
    public void saveItem() throws Exception {
        ItemFormDto itemFormDto = ItemFormDto.builder()
                .itemNm("테스트 상품입니다.")
                .itemSellStatus(ItemSellStatus.SELL)
                .itemDetail("테스트 상품 설명입니다.")
                .price(1000)
                .stockNumber(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(multipartFileList.get(0).getOriginalFilename(), itemImgList.get(0).getOriImgName());
    }

}

상품 등록

테스트 코드가 이상없이 동작하는 것을 확인하였으니 실제 상품 등록 페이지에서 데이터가 등록되는지 확인해보겠습니다. 상품을 등록할 때 사용하는 이미지 자체가 중요한 것이 아니므로 아무 이미지 파일을 다운로드하여 진행해보겠습니다.

상품 등록을 저장하고 파일 업로드 경로에 해당 이미지가 들어왔는지를 확인해보면 정상적으로 들어온 것을 확인할 수 있습니다.

profile
철학있는 개발자 - 내가 무지하다는 것을 인정할 때 비로소 배움이 시작된다.

0개의 댓글