μŠ€ν”„λ§/μ‡Όν•‘λͺ° ν”„λ‘œμ νŠΈ

μƒν’ˆ 섀계 및 λ“±λ‘ν•˜κΈ°

KIMHYEYUN 2022. 11. 24. 01:30
λ°˜μ‘ν˜•

μƒν’ˆ μ—”ν‹°ν‹°

@Entity
@Getter@Setter
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "item_id")
    private Long id;
    private String itemName;
    private int price;
    private String itemDetail;
    private String type;
    @Enumerated(EnumType.STRING)
    private ItemSellStatus itemSellStatus;
}

ItemSellStatus

  • ν’ˆμ ˆ/판맀 μƒνƒœλ₯Ό λ‚˜νƒ€λ‚΄λŠ” Enum Class

μƒν’ˆ 이미지

application.properties μ„€μ •

# 파일 ν•œ κ°œλ‹Ή μ΅œλŒ€ μ‚¬μ΄μ¦ˆ
spring.servlet.multipart.max-file-size=20MB

# μš”μ²­λ‹Ή μ΅œλŒ€ 파일 크기
spring.servlet.multipart.max-request-size=100MB

# μƒν’ˆ 이미지 μ—…λ‘œλ“œ 경둜
itemImgLocation=/Users/yun/Desktop/YUN/Spring/YunCase/src/main/resources/static/images/items
uploadPath=/Users/yun/Desktop/YUN/Spring/YunCase/src/main/resources/static/images
@Entity
@Getter
@Setter
public class ItemImage {
    @Id
    @Column(name = "item_img_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String imageName;
    private String originalImageName;
    private String imageUrl;
    private String repImgYn;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;
}

Itemκ³Ό λ‹€λŒ€μΌ 단방ν–₯ κ΄€κ³„λ‘œ λ§€ν•‘


ItemForm

  • μƒν’ˆ 이미지 뢀뢄은 등둝과 μˆ˜μ •μœΌλ‘œ λ‚˜λˆ”
  • μƒν’ˆ 이미지 첨뢀λ₯Ό μ„ νƒν–ˆμ„ λ•Œ μˆ˜ν–‰λ˜λŠ” 슀크립트
  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);
      });
    }
  • μƒν’ˆ μƒνƒœλ₯Ό "νŒλ§€μ€‘" λ˜λŠ” "ν’ˆμ ˆ" 선택 κ°€λŠ₯
  • <div class="form-group"> <select th:field="*{itemSellStatus}" class="custom-select"> <option value="SELL">νŒλ§€μ€‘</option> <option value="SOLD_OUT">ν’ˆμ ˆ</option> </select> </div>

application.properties μ„€μ •

파일 크기 및 경둜 μ§€μ •

WebMvcConfigure μ„€μ •

public class MvcConfiguration implements WebMvcConfigurer {

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


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/templates/", "classpath:/static/");

        registry.addResourceHandler("/images/**")
                .addResourceLocations(uploadPath);

    }
}
  • /images/** νŒ¨ν„΄μ˜ URL은 uploadPathλ₯Ό κΈ°μ€€μœΌλ‘œ 탐색함을 μ„€μ •

FileService - μƒν’ˆ 이미지 파일

  • νŒŒμΌμ„ μ²˜λ¦¬ν•˜λŠ” 클래슀
  • νŒŒμΌμ„ μ—…λ‘œλ“œν•˜λŠ” λ©”μ„œλ“œ(uploadFile) κ³Ό μ‚­μ œν•˜λŠ” λ©”μ„œλ“œ(deleteFile)
  • νŒŒμΌμ€ DB에 μ €μž₯λ˜λŠ” 것이 μ•„λ‹ˆλΌμ„œ Repository λΆˆν•„μš” (FileOutputStream이 λŒ€μ‹  함)
  • UUID : μ„œλ‘œ λ‹€λ₯Έ κ°œμ²΄λ“€μ„ κ΅¬λ³„ν•˜κΈ° μœ„ν•΄μ„œ 이름을 λΆ€μ—¬ν•  λ•Œ μ‚¬μš©
@Service
@Log
public class FileService {

    public String uploadFile(String uploadPath, String originalFileName, byte[] fileData) throws IOException {
        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) {
        File deleteFile = new File(filePath);

        if (deleteFile.exists()) {
            deleteFile.delete();
            log.info("νŒŒμΌμ„ μ‚­μ œν–ˆμŠ΅λ‹ˆλ‹€.");
        } else {
            log.info("파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
        }
    }
}

μƒν’ˆ 등둝 Controller

 @PostMapping(value = "/admin/item/new")
    public String itemPost(@Valid ItemFormDto itemFormDto, BindingResult bindingResult, Model model, @RequestParam("itemImageFile") List<MultipartFile> itemImageFileList) {
        if (bindingResult.hasErrors()) {
            return "items/itemForm";
        }

        if (itemImageFileList.get(0).isEmpty() && itemFormDto.getId() == null) {
            model.addAttribute("errorMessage", "첫번째 μƒν’ˆ μ΄λ―Έμ§€λŠ” ν•„μˆ˜ μž…λ ₯ κ°’ μž…λ‹ˆλ‹€.");
            return "items/itemForm";
        }

        try {
            itemService.saveItem(itemFormDto, itemImageFileList);
        } catch (Exception e) {
            model.addAttribute("errorMessage", "μƒν’ˆ 등둝 쀑 μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.");
            return "items/itemForm";
        }
        return "redirect:/";
    }
  • 이미지 파일(itemImageFile) 을 MultipartFile 객체둜 λ°›μŒ
  • μž…λ ₯값이 λΉ„μ •μƒμ΄κ±°λ‚˜, 첫 번째 μƒν’ˆ 이미지λ₯Ό μ§€μ •ν•˜μ§€ μ•Šμ•˜μœΌλ©΄ λ‹€μ‹œ μƒν’ˆ 등둝 νŽ˜μ΄μ§€λ‘œ λŒμ•„κ°
  • μž…λ ₯값이 정상이면, itemService.saveItem(itemFormDto, itemImageFileList) μˆ˜ν–‰

μƒν’ˆ 등둝 Test

@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemServiceTest {

    @Autowired
    ItemService itemService;
    @Autowired
    ItemRepository itemRepository;
    @Autowired
    ItemImageRepository itemImageRepository;

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

        for (int i = 0; i < 5; i++) {
            String path = "/Users/yun/Desktop/YUN/Spring/Shop/src/main/resources/static/";
            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
    @WithMockUser(username = "admin", roles = "ADMIN")
    void saveItem() throws IOException {
        ItemFormDto itemFormDto = new ItemFormDto();
        itemFormDto.setItemName("test");
        itemFormDto.setPrice(1000);
        itemFormDto.setItemDetail("test");
        itemFormDto.setItemSellStatus(ItemSellStatus.SELL);
        itemFormDto.setType("IPHONE");

        List<MultipartFile> multipartFileList = createMultipartFiles();
        Long itemId = itemService.saveItem(itemFormDto, multipartFileList);

        List<ItemImage> itemImages =
                itemImageRepository.findByIdOrderByIdAsc(itemId);
        Item item = itemRepository.findById(itemId)
                .orElseThrow(EntityNotFoundException::new);

        assertEquals(itemFormDto.getItemName(), item.getItemName());
    }
}

μ‹€ν–‰ ν›„, μ €μž₯ κ²½λ‘œμ— 파일 생김

κ²°κ³Ό

μƒν’ˆ 등둝 λ™μž‘ κ³Όμ •

  1. ADMIN κΆŒν•œμ„ κ°€μ§„ μ•„μ΄λ””λ‘œ μƒν’ˆ 등둝 νŽ˜μ΄μ§€ GET μš”μ²­
  2. ItemControllerμ—μ„œ μƒν’ˆ 등둝 νŽ˜μ΄μ§€λ₯Ό λ°˜ν™˜ν•˜λ©΄μ„œ ItemFormDto 객체도 전달
  3. μƒν’ˆ 등둝 νŽ˜μ΄μ§€μ—μ„œ μƒν’ˆ 정보 및 이미지λ₯Ό μž…λ ₯ν•˜κ³  μ €μž₯ POST μš”μ²­
  4. ItemControllerμ—μ„œ μž…λ ₯값을 κ²€μ¦ν•˜κ³ , ItemService.saveItem() μˆ˜ν–‰
    πŸ™‹β€β™€οΈ νŒŒλΌλ―Έν„°λŠ” μž…λ ₯ 받은 ItemFormDto 객체와 이미지 정보λ₯Ό 담은 itemImageFileListλ₯Ό λ„˜κΉ€
  5. ItemServiceμ—μ„œ ItemFormDto객체λ₯Ό Item μ—”ν‹°ν‹°λ‘œ λ³€ν™˜ν•˜κ³ , ItemRepository.save() μˆ˜ν–‰
  6. ItemServiceμ—μ„œ ItemImage 객체λ₯Ό μƒμ„±ν•˜κ³  ItemImageService.saveItemImage() μˆ˜ν–‰
    πŸ™‹β€β™€οΈ νŒŒλΌλ―Έν„°λŠ” ItemImage 객체와 이미지 정보λ₯Ό λ‹΄κ³ μžˆλŠ” ItemImageFileList.get(i) 객체
  7. ItemImageServiceμ—μ„œ μƒν’ˆ 이미지가 μ‘΄μž¬ν•œλ‹€λ©΄ FileService.uploadFile() μˆ˜ν–‰
    πŸ™‹β€β™€οΈ νŒŒλΌλ―Έν„°λŠ” μ €μž₯μœ„μΉ˜, μ›λž˜ 파일λͺ…, 이미지 Byte 파일
  8. FileServiceμ—μ„œ UUID 객체λ₯Ό μ΄μš©ν•΄ 파일λͺ…을 μƒˆλ‘œ λ§Œλ“€κ³  FileOutputStream을 μ΄μš©ν•΄ μ €μž₯
  9. ItemImageServiceμ—μ„œ ItemImageRepository.save() μˆ˜ν–‰
728x90
λ°˜μ‘ν˜•