Spring boot & JPA

Spring Boot - 쇼핑몰 프로젝트 06 (상품 등록 및 조회하기 - 1)

로기221 2023. 4. 5. 16:45
728x90
반응형

상품 등록 및 조회하기

 

상품 등록하기

  1. 상품 이미지 엔티티는 이미지 파일명, 원본 이미지 파일명, 이미지 조회 경로, 대표 이미지 여부를 갖도록 설계
  2. 대표 이미지 여부가 “Y”인 경우 메인페이지에서 상품을 보여줄 때 사용

 

 

 

상품 등록 구현하기

com.shop.entity.ItemImg.java

// ItemImg.java

package com.shop.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Table(name="item_img")
@Getter
@Setter
public class ItemImg extends BaseEntity {

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

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

 

 

  • DTO 객체와 Entity객체의 변환을 도와주는 modelmapper라이브러리 추가
  • 서로 다른 클래스의 값을 필드의 이름과 자료형이 같으면 getter, setter를 통해 값을 복사해서 객체를 반환

pom.xml

// pom.xml

... 생략 ...

		<dependency>
			<groupId>org.modelmapper</groupId>
			<artifactId>modelmapper</artifactId>
			<version>3.1.0</version>
		</dependency>
	</dependencies>
    
... 생략 ...

 

com.shop.dto.ItemImgDto.java

// ItemImgDto.java

package com.shop.dto;

import com.shop.entity.ItemImg;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;

@Getter
@Setter
public class ItemImgDto {

    private Long id;
    private String imgName;
    private String oriImgName;
    private String imgUrl;
    private String repImgYn;
    private static ModelMapper modelMapper = new ModelMapper();

    public static ItemImgDto of(ItemImg itemImg) {
        return modelMapper.map(itemImg, ItemImgDto.class);
    }
}

 

com.shop.dto.ItemFormDto.java

// ItemFormDto.java

package com.shop.dto;

import com.shop.constant.ItemSellStatus;
import com.shop.entity.Item;
import lombok.Getter;
import lombok.Setter;
import org.modelmapper.ModelMapper;

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

@Getter
@Setter
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<>();
    private static ModelMapper modelMapper = new ModelMapper();

    public Item createItem() {
        return modelMapper.map(this, Item.class);
    }

    public static ItemFormDto of(Item item) {
        return modelMapper.map(item, ItemFormDto.class);
    }
}

 

 

 

com.shop.ItemController.java

수정하기

// ItemController.java

package com.shop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;
import com.shop.dto.ItemFormDto;

@Controller
public class ItemController {

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

 

 

 

 

 

  • 상품 등록 같은 관리자 페이지에서 중요한 것은 데이터의 무결성을 보장하는 것이 중요
  • 잘못된 값이 저장되지 않도록 밸리데이션(validation)
  • 데이터끼리 서로 연관이 있으면 어떤 데이터가 변함에 따라서 다른 데이터도 함께 체크를 해야 하는 경우가 많음
  • 소스 양이 많기 때문에 깃허브에서 복사해서 사용
  • 깃허브 주소 : https://github.com/roadbook2/shop

 

resources/templates/item/itemForm.html

https://rogi221.tistory.com/178

 

ItemForm.html - github

https://rogi221.tistory.com/177 깃허브에서 자료 복사해서 입력 // ItemFrom.html 상품 등록 판매중 품절 상품명 Incorrect data 가격 Incorrect data 재고 Incorrect data 상품 상세 내용 Incorrect data 저장 수정

rogi221.tistory.com

 


 

 

  • 현재는 application을 재실행하면 데이터가 삭제되므로 데이터베이스에 저장된 데이터가 삭제되지 않도록 ddl-auto 설정 변경
  • application.prperties의 ddl-auto 속성을 validate로 변경하면 애플리케이션 실행 시점에 엔티티와 테이블이 매핑이 정상적으로 되어있는지만 확인

 

// application.properties 

spring.jpa.hibernate.ddl-auto=validate		<< 수정
// application-test.properties

spring.jap.hibernate.ddl-auto=create		<< 내용 추가
  • 회원 가입 후 다시 로그인을 하였다면 ADMIN ROLE로 가입을 진행하였기 때문에 상품 등록 메뉴가 보이는 것을 확인

 

 

  • 상품 등록 메뉴를 클릭하면 방금 전에 만들었던 상품 등록 페이지가 보이는 것을 확인 가능

 

 

 

  • 이미지 파일을 등록할 때 서버에서 각 파일의 최대 사이즈와 한번에 다운 요청할 수 있는 파일의 크기 지정
  • 어떤 경로에 저장할지를 관리하기 위해서 프로퍼티에itemImgLocation을 추가
  • 프로젝트 내부가 아닌 자신의 컴퓨터에서 파일을 찾는 경로로 uploadPath 프로퍼티를 추가

application.properties 설정 추가

// application.properties

#파일 한 개당 최대 사이즈
spring.servlet.multipart.maxFileSize=20MB

#요청당 최대 파일 크기
spring.servlet.multipart.maxRequestSize=100MB

#상품 이미지 업로드 경로
itemImgLocation=C:/shop/item

#리소스 업로드 경로
uploadPath=file:///C:/shop/

 

  • addResourceHandlers 메소드를 통해서 자신의 로컬 컴퓨터에 업로드한 파일을 찾을 위치를 설정

 

com.shop.config.WebMvcConfig.java

// 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 {

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

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/imges/**")
                .addResourceLocations(uploadPath);
    }
}

 

 

com.shop.service.FileService.java

// 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 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()) {
            deleteFile.delete();
            log.info("파일을 삭제하였습니다.");
        } else {
            log.info("파일이 존재하지 않습니다.");
        }
    }
}

 

 

 

  • 상품의 이미지 정보를 저장하는 ItemImgRepository 인터페이스 작성

com.shop.repository.ItemImgRepository.java

// ItemImgRepository.java

package com.shop.repository;

import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {

}

 

 

 

  • 상품 이미지를 업로드하고, 상품 이미지 정보를 저장하는 ItemImgService 클래스 작성

com.shop.service.ItemImgService.java

// 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;

@Service
@RequiredArgsConstructor
@Transactional
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);
    }
}

 

 

  • 상품을 등록하는 ItemService 클래스 작성

com.shop.service.ItemService.java

// ItemService.java

package com.shop.service;

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 lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

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();
    }
}

 

 

com.shop.controller.ItemController.java

// ItemController.java

package com.shop.controller;

import com.shop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;
import com.shop.dto.ItemFormDto;
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;

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    @GetMapping(value ="/admin/item/new")
    public String itemFrom(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:/";
    }
}

 

 

 

 

  • 상품 저장 로직 테스트 코드를 작성
  • 테스트 코드를 작성하기 위해서 ItemImgRepository 인터페이스에findByItemIdOrderByIdAsc 메소드를 추가
  • 상품 이미지 아이디의 오름차순으로 가져오는 쿼리 메소드

 

com.shop.repository.ItemImgRepository.java

// 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> {

    List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}

 

 

com.shop.service.ItemServiceTest.java

// ItemServiceTest.java

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 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.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{

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

        for(int i=0;i<5;i++){
            String path = "C:/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")
    void saveItem() throws Exception {
        ItemFormDto itemFormDto = new ItemFormDto();
        itemFormDto.setItemNm("테스트상품");
        itemFormDto.setItemSellStatus(ItemSellStatus.SELL);
        itemFormDto.setItemDetail("테스트 상품 입니다.");
        itemFormDto.setPrice(1000);
        itemFormDto.setStockNumber(100);

        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(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());
    }

}

  • <저장> 버튼을 눌렀을 때 상품이 정상적으로 저장됐다면 다음과 같이 메인 페이지로 이동
  • 파일 업로드 경로인 C:/shop/item 경로에 들어가보면 업로드한 청바진 사진과 스웨터 사진이 올라온것을 확인

 


https://rogi221.tistory.com/179

 

Spring Boot - 쇼핑몰 프로젝트 06 (상품 등록 및 조회하기 - 2)

상품 등록 및 조회하기 - 2 상품 수정하기 상품 등록 후 콘솔창을 보면 insert into item 쿼리문에서 item_id에 들어가는 binding parameter 값 확인 해당 상품 아이디를 이용해서 상품 수정 페이지에 진입 예

rogi221.tistory.com

 

 

 

728x90
반응형