RESTful API를 활용한 Back-end 연결

JOY🌱·2023년 4월 28일
0

🌊 RESTful API

목록 보기
2/3
post-thumbnail

GitHub

📌 초기 셋팅

🙋‍ 기본 설정 셋팅

👉 application.yml

# port config
server:
  port: 8001
  
# datasource config
spring:
  datasource:
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@localhost:1521:xe
    username: C##RESTAPI
    password: RESTAPI
    
# jpa config
  jpa:
    generate-ddl: false
    show-sql: true
    database: oracle
    properties:
      hibernate:
        '[format_sql]' : true
  # maximum upload size
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
        
# image 관련 값 설정
image:
  image-url: http://localhost:8001/productimgs/ 	# 찾고자 하는 이미지들의 경로 (static 하위부터 작성하면 됨)
  image-dir: src/main/resources/static/productimgs 	# 이미지들을 저장할 상대 경로 설정
  

👉 pom.xml (dependencies)

<!-- security config -->
<dependency>
  	<groupId>org.springframework.boot</groupId>
  	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- security config -->
<dependency>
  	<groupId>org.springframework.security</groupId>
  	<artifactId>spring-security-test</artifactId>
  	<scope>test</scope>
</dependency>
		
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.0.0</version>
</dependency>
		
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

👉 BinConfig

@Configuration
public class BeanConfig {

	/* pom.xml에 ModelMapper 의존성 주입 후, 여기서 config */
	@Bean
	public ModelMapper modelMapper() {
		return new ModelMapper();
	}
	
}

🙋‍ Security를 위한 셋팅

👉 SecurityConfig

@EnableWebSecurity

@EnableWebSecurity
public class SecurityConfig {
	

	// 외부에서 이미지 파일에 접근 가능 하도록 설정
	@Bean
	public WebSecurityCustomizer configure() {
		return (web) -> web.ignoring().antMatchers("/productimgs/**");
	}
	
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		
		 return http
		         // CSRF 설정 Disable
		         .csrf()
		         	.disable()
		         /* 시큐리티는 기본적으로 세션을 사용하지만 API 서버에선 세션을 사용하지 않기 때문에 세션 설정을 Stateless 로 설정 */
		         .sessionManagement()
		             .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		         .and()
		         	 /* 요청에 대한 권한 체크 */
		             .authorizeRequests()
		             /* 클라이언트가 외부 도메인을 요청하는 경우 웹 브라우저에서 자체적으로 사전 요청(preflight)이 일어남 
		              * 이 때 OPTIONS 메서드로 서버에 사전 요청을 보내 요청 권한이 있는지 확인 */
		             .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 모든 HTTP OPTIONS 요청에 대해 인증을 요구하지 않고, 모든 사용자가 접근할 수 있도록 허용
		             .antMatchers("/auth/**").permitAll()
		             .antMatchers("/api/v1/products/**").permitAll()
		             .antMatchers(HttpMethod.GET, "/api/v1/reviews/**").permitAll()
		             .antMatchers("/api/**").hasAnyRole("USER", "ADMIN")  // 나머지 API 는 전부 인증 필요
		         .and()
		         	.cors()
		         .and()
		         	.build();
		 
	}
	
	/* CORS(cross-origin-resource-sharing) : 교차 출처 자원 공유 
	 * 예전에는 자원 저장 서버와 웹 페이지가 하나의 서버에서 만들어졌기 때문에 해당 서버의 자원을 해당 도메인에서만 요청함
	 * 보안상 웹 브라우저는 다른 도메인에서 서버의 자원을 요청할 경우 막아 놓음
	 * 점점 자원과 웹 페이지를 분리하는 경우, 다른 서버의 자원을 요청하는 경우가 많아지면서 웹 브라우저는 외부 도메인과 통신하기 위한
	 * 표준인 CORS를 만듦
	 * 기본적으로 서버에서 클라이언트를 대상으로 리소스의 허용 여부를 결정
	 * */
    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration configuration = new CorsConfiguration();
        // 로컬 React에서 오는 요청은 CORS 허용
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000" ));
        configuration.setAllowedMethods(Arrays.asList("GET", "PUT", "POST", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("Access-Control-Allow-Origin", "Content-Type", "Access-Control-Allow-Headers", "Authorization", "X-Requested-With"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

🙋‍ Paging 처리를 위한 셋팅

👉 Pagenation

public class Pagenation {
	
	public static PagingButtonInfo getPagingButtonInfo(Page page) {
		
		int currentPage = page.getNumber() + 1;	
		int defaultButtonCount = 5;
		int startPage;
		int endPage;
		
		startPage = (int) (Math.ceil((double) currentPage / defaultButtonCount) - 1) * defaultButtonCount + 1;
		endPage = startPage + defaultButtonCount - 1;
		
		if(page.getTotalPages() < endPage)
			endPage = page.getTotalPages();
		
		if(page.getTotalPages() == 0 && endPage == 0)
			endPage = startPage;
		
		return new PagingButtonInfo(currentPage, startPage, endPage, page.getTotalPages());
	}
	
}

👉 PagingButtonInfo

@AllArgsConstructor @Data

@AllArgsConstructor
@Data
public class PagingButtonInfo {
	
	private int currentPage;
	private int startPage;
	private int endPage;
	private int maxPage;
	
}

👉 ResponseDTOWithPaging

@Data
public class ResponseDTOWithPaging {

    private Object data;
    private PagingButtonInfo pageInfo;

}

🙋‍ 응답 및 예외처리를 위한 셋팅

👉 ResponseDTO

@Data
public class ResponseDTO {

	private int status;
	private String message;
	private Object data; /* ProductList, Product 등등 여러가지 data가 들어갈 수 있음 */
	
	/* 상품 등록&수정 시, data를 보내지 않기 때문에 아래의 생성자 필요 */
	public ResponseDTO(HttpStatus status, String message) {
		this.status = status.value();
		this.message = message;
	}
	
	public ResponseDTO(HttpStatus status, String message, Object data) {
		this.status = status.value();
		this.message = message;
		this.data = data;
	}
	
}

👉 ApiExceptionDTO

@Data
public class ApiExceptionDTO {

	private int state;
	private String message;
	
	public ApiExceptionDTO(HttpStatus status, String message) {
		this.state = status.value();
		this.message = message;
	}
}

👉 ApiExceptionAdvice

@RestControllerAdvice @ExceptionHandler

@RestControllerAdvice
public class ApiExceptionAdvice {

	@ExceptionHandler(RuntimeException.class)
	public ResponseEntity<ApiExceptionDTO> exceptionHandler(RuntimeException e) {
		
		return ResponseEntity
				.status(HttpStatus.INTERNAL_SERVER_ERROR) 										// 상태 설정
				.body(new ApiExceptionDTO(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()));	// 메세지 설정
		
	}

}

🙋‍ 파일 저장 및 삭제를 위한 셋팅

👉 FileUploadUtils

public class FileUploadUtils {

	/* 파일을 저장하기 위한 메소드 */
	public static String saveFile(String uploadDir, String fileName, MultipartFile multipartFile) throws IOException {
		// uploadDir: 업로드 하고 싶은 경로 / fileName: 저장할 파일 이름 / multipartFile: 현재 파일의 정보가 담겨있는 객체
		
		Path uploadPath = Paths.get(uploadDir);
		
		/* 업로드 경로가 존재하지 않을 경우 경로를 먼저 생성 */
		if(!Files.exists(uploadPath)) {
			Files.createDirectories(uploadPath);
		}
		
		/* 파일명 rename */
		String replaceFileName = fileName + "." + FilenameUtils.getExtension(multipartFile.getOriginalFilename());
		
		/* 파일 저장 */
		try (InputStream inputStream = multipartFile.getInputStream()) {
			Path filePath = uploadPath.resolve(replaceFileName); // resolve() : 경로 + 파일명 합치기
			Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); // Files.copy() 
			// inputStream: 파일을 읽어옴 / filePath: 파일 전체 경로 / StandardCopyOption.REPLACE_EXISTING: 만약 해당 경로 상에 이미 존재한다면 덮어쓰기 (무조건 저장시킴)
		} catch (IOException e) {
			throw new IOException("파일을 저장하지 못 했어유👻 fileName : " + fileName);
		}
		
		return replaceFileName;
		
	}
	/* 파일을 삭제하기 위한 메소드 */
	public static void deleteFile(String uploadDir, String fileName) throws IOException {
		
		Path uploadPath = Paths.get(uploadDir);
		Path filePath = uploadPath.resolve(fileName);
		
		try {
			Files.delete(filePath);
		} catch (IOException e) {
			throw new IOException("파일을 삭제하지 못 했어유👻 fileName : " + fileName);
		}
		
	}
	
}

👀 Entity

👉 Category

@Getter @Setter

@Setter // save()메소드 사용 시, modelMapper로 ProductDTO를 Product로 변환하는 과정 상에서 setter가 필요함 
		// (Product가 Category를 참조하므로 동일한 이유)
@Getter
@Entity
@Table(name="TBL_CATEGORY")
public class Category {

	/*
	CATEGORY_CODE	NUMBER
	CATEGORY_NAME	VARCHAR2(50 BYTE) 
	*/
	
	@Id
	@Column(name="CATEGORY_CODE")
	private Long categoryCode;
	
	@Column(name="CATEGORY_NAME")
	private String categoryName;
	
}

👉 Product

@ManyToOne @JoinColumn

@Setter // save()메소드 사용 시, modelMapper로 ProductDTO를 Product로 변환하는 과정 상에서 setter가 필요함
@Getter
@Entity
@Table(name="TBL_PRODUCT")
@SequenceGenerator(name="PRODUCT_SEQ_GENERATOR",
				   sequenceName="SEQ_PRODUCT_CODE",
				   initialValue=1, allocationSize=1)
public class Product {
	
	/*
	PRODUCT_CODE		NUMBER
	PRODUCT_NAME		VARCHAR2(100 BYTE)
	PRODUCT_PRICE		VARCHAR2(100 BYTE)
	PRODUCT_DESCRIPTION	VARCHAR2(1000 BYTE)
	PRODUCT_ORDERABLE	VARCHAR2(5 BYTE)
	CATEGORY_CODE		NUMBER
	PRODUCT_IMAGE_URL	VARCHAR2(100 BYTE)
	PRODUCT_STOCK		NUMBER
	*/
	
	@Id
	@Column(name="PRODUCT_CODE")
	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="PRODUCT_SEQ_GENERATOR")
	private Long productCode;
	
	@Column(name="PRODUCT_NAME")
	private String productName; 
	
	@Column(name="PRODUCT_PRICE")
	private String productPrice; 
	
	@Column(name="PRODUCT_DESCRIPTION")
	private String productDescription; 
	
	@Column(name="PRODUCT_ORDERABLE")
	private String productOrderable; 
	
	/* (cascade = CascadeType.PERSIST) : 영속성 전이 설정을 넣으면 Category에 새로운 값이 있을 경우 insert될 수 있음 (유의) */
	@ManyToOne /* Category 테이블과의 연관관계 */
	@JoinColumn(name="CATEGORY_CODE")
	private Category category; 
	
	@Column(name="PRODUCT_IMAGE_URL")
	private String productImageUrl; 
	
	@Column(name="PRODUCT_STOCK")
	private int productStock;

	
	/* Product entity 수정 용도의 메소드를 별도로 정의  */
	public void update(String productName, String productPrice, String productDescription, 
				       String productOrderable, Category category, int productStock) {
		
		this.productName = productName;
		this.productPrice = productPrice;
		this.productDescription = productDescription;
		this.productOrderable = productOrderable;
		this.category = category;
		this.productStock = productStock;
		
	} 

}

👀 DTO

👉 CategoryDTO

@Data
public class CategoryDTO {

	private Long categoryCode;
	private String categoryName;
}

👉 ProductDTO

@JsonIgnore

@Data
public class ProductDTO {

	private Long productCode;
	private String productName; 
	private String productPrice; 
	private String productDescription; 
	private String productOrderable; 
	private CategoryDTO category; 
	
	/* DB 컬럼으로 존재하지는 않지만(entity의 필드로 선언 X) 클라이언트에서 넘겨주는 상품 이미지 파일을 저장할 수 있는 필드 선언 */
	@JsonIgnore // @JsonIgnore : 해당 필드가 JSON 직렬화/역직렬화 과정에서 무시되도록 설정하는 역할
	private MultipartFile productImage;
	
	private String productImageUrl; 
	private int productStock;
	
}

👀 Repository

👉 CategoryRepository

public interface CategoryRepository extends JpaRepository<Category, Long> {


}

👉 ProductRepository

@EntityGraph @Query @Param

public interface ProductRepository extends JpaRepository<Product, Long> {

	/* 1. 상품 목록 조회 - 페이징, 주문 불가 상품 제외(고객) */
	/* 연관 관계 지연 로딩으로 되어있을 경우 엔티티를 하나 조회하고 다시 다른 엔티티에 대해서 여러 번 조회를 별도로 하게 되는 N+1 문제가 발생 (성능 이슈) 
	 * => fetch 조인을 사용하게 되면 한 번에 조인해서 결과를 가져옴
	 *    @EntityGraph는 Data JPA에서 fetch 조인을 어노테이션으로 사용 가능하도록 제공한 기능 */
	@EntityGraph(attributePaths= {"category"}) // Product Entity 필드의 명칭 기재
	Page<Product> findByProductOrderable(String productOrderable, Pageable pageable);
	/* 2. 상품 목록 조회 - 페이징, 주문 불가 상품 포함(관리자) */
	// JpaRepository에 이미 정의되어있는 findAll(Pageable pageable) 메소드를 사용 가능하므로 별도의 정의 필요 X
	@EntityGraph(attributePaths= {"category"})
	Page<Product> findAll(Pageable pageable); // @EntityGraph 속성을 적용시키기 위해 findAll() 메소드 그대로 재정의
	/* 3. 상품 목록 조회 - 카테고리 기준, 페이징, 주문 불가 상품 제외(고객) */
	Page<Product> findByCategoryAndProductOrderable(Category category, String productOrderable, Pageable pageable);
	/* 4. 상품 목록 조회 - 상품명 검색 기준, 페이징, 주문 불가 상품 제외(고객) */
	@EntityGraph(attributePaths= {"category"})
	Page<Product> findByProductNameContainsAndProductOrderable(String productName, String productOrderable, Pageable pageable);
	/* 5. 상품 상세 조회 - productCode로 상품 1개 조회, 주문 불가 상품 제외(고객) */
	// 쿼리 메소드로 구현 가능 findByProductCodeAndProductOrderable(Long productCode, String productOrderable) 하지만 JPQL을 사용하여 구현
	@Query("SELECT p " + 
		    " FROM Product p" + // Entity명에 별칭 부여 필수
		   " WHERE p.productCode = :productCode" + 
		     " AND p.productOrderable = 'Y'")
	Optional<Product> findByProductCode(@Param("productCode") Long productCode); // @Param과 ':'를 이용하여 productCode를 Query에 적용
	/* 6. 상품 상세 조회 - productCode로 상품 1개 조회, 주문 불가 상품 포함(관리자) */
	// findById() 메소드를 사용 가능하므로 별도의 정의 필요 X
	/* 7. 상품 등록(관리자) */
	// save() 메소드를 사용 가능하므로 별도의 정의 필요 X
	/* 8. 상품 수정(관리자) */
	// findById() 메소드로 조회 후 필드 값을 수정하면 변화를 감지하여 update 구문이 생성되므로 별도의 정의 필요 X
	/* 9. 상품 삭제(관리자) */
	// deleteById() 메소드를 사용 가능하므로 별도의 정의 필요 X 
	
}

👀 Service

👉 ProductService

@Value PageRequest.of() Sort.by() findAll() findById()

@Slf4j
@Service
public class ProductService {

	private final CategoryRepository categoryRepository;
	private final ProductRepository productRepository;
	private final ModelMapper modelMapper;
    
	/* application.yml에 설정한 이미지 경로 가져오기 */
	@Value("${image.image-url}")
	private String IMAGE_URL;
	@Value("${image.image-dir}")
	private String IMAGE_DIR;
	
	public ProductService(ProductRepository productRepository, ModelMapper modelMapper, CategoryRepository categoryRepository) {
		this.categoryRepository = categoryRepository;
		this.productRepository = productRepository;
		this.modelMapper = modelMapper;
	}
	/* 1. 상품 목록 조회 - 페이징, 주문 불가 상품 제외(고객) */
	public Page<ProductDTO> selectProductList(int page) {
		
		log.info("[ProductService] selectProductList START🥳 =================================================");
		
		Pageable pageable = PageRequest.of(page - 1, 10, Sort.by("productCode").descending()); // PageRequest.of(몇 번째 페이지?, 몇 개씩?, 정렬기준)
		
		Page<Product> productList = productRepository.findByProductOrderable("Y", pageable);
		Page<ProductDTO> productDTOList = productList.map(product -> modelMapper.map(product, ProductDTO.class));
		
		/* 클라이언트 측에서 서버에 저장 된 이미지 요청 시 필요한 주소로 가공 */
		productDTOList.forEach(product -> product.setProductImageUrl(IMAGE_URL + product.getProductImageUrl()));
		
		log.info("[ProductService] productDTOList.getContent() : {}", productDTOList.getContent()); // 담겨있는 상품 목록 확인
		log.info("[ProductService] selectProductList END😎 =================================================");
		
		return productDTOList;
	}
	/* 2. 상품 목록 조회 - 페이징, 주문 불가 상품 포함(관리자) */
	public Page<ProductDTO> selectProductListForAdmin(int page) {
		
		log.info("[ProductService] selectProductListForAdmin START🥳 =================================================");
		
		Pageable pageable = PageRequest.of(page - 1, 10, Sort.by("productCode").descending()); // PageRequest.of(몇 번째 페이지?, 몇 개씩?, 정렬기준)
		
		Page<Product> productList = productRepository.findAll(pageable);
		Page<ProductDTO> productDTOList = productList.map(product -> modelMapper.map(product, ProductDTO.class));
		
		/* 클라이언트 측에서 서버에 저장 된 이미지 요청 시 필요한 주소로 가공 */
		productDTOList.forEach(product -> product.setProductImageUrl(IMAGE_URL + product.getProductImageUrl()));
		
		log.info("[ProductService] productDTOList.getContent() : {}", productDTOList.getContent()); // 담겨있는 상품 목록 확인
		log.info("[ProductService] selectProductListForAdmin END😎 =================================================");
		
		return productDTOList;
	}
	/* 3. 상품 목록 조회 - 카테고리 기준, 페이징, 주문 불가 상품 제외(고객) */
	public Page<ProductDTO> selectProductListByCategory(int page, Long categoryCode) {
		
		log.info("[ProductService] selectProductListByCategory START🥳 =================================================");
		
		Pageable pageable = PageRequest.of(page - 1, 10, Sort.by("productCode").descending());
		
		/* 전달할 카테고리 엔티티를 먼저 조회 */
		Category findCategory = categoryRepository.findById(categoryCode) // NPE에 대처하기 위해 orElseThrow() 추가
				.orElseThrow(() -> new IllegalArgumentException("해당 카테고리가 없어유🤢 categoryCode = " + categoryCode));
		
		Page<Product> productList = productRepository.findByCategoryAndProductOrderable(findCategory, "Y", pageable);
		
		Page<ProductDTO> productDTOList = productList.map(product -> modelMapper.map(product, ProductDTO.class));
		
		/* 클라이언트 측에서 서버에 저장 된 이미지 요청 시 필요한 주소로 가공 */
		productDTOList.forEach(product -> product.setProductImageUrl(IMAGE_URL + product.getProductImageUrl()));
		
		log.info("[ProductService] productDTOList.getContent() : {}", productDTOList.getContent());
		log.info("[ProductService] selectProductListByCategory END😎 =================================================");
		
		return productDTOList;
	}
	/* 4. 상품 목록 조회 - 상품명 검색 기준, 페이징, 주문 불가 상품 제외(고객) */
	public Page<ProductDTO> selectProductListByProductName(int page, String productName) {
		
		log.info("[ProductService] selectProductListByProductName START🥳 =================================================");
		
		Pageable pageable = PageRequest.of(page - 1, 10, Sort.by("productCode").descending());
		
		Page<Product> productList = productRepository.findByProductNameContainsAndProductOrderable(productName, "Y", pageable);
		
		Page<ProductDTO> productDTOList = productList.map(product -> modelMapper.map(product, ProductDTO.class));
		
		/* 클라이언트 측에서 서버에 저장 된 이미지 요청 시 필요한 주소로 가공 */
		productDTOList.forEach(product -> product.setProductImageUrl(IMAGE_URL + product.getProductImageUrl()));
		
		log.info("[ProductService] productDTOList.getContent() : {}", productDTOList.getContent());
		log.info("[ProductService] selectProductListByProductName END😎 =================================================");
		
		return productDTOList;
	}
	/* 5. 상품 상세 조회 - productCode로 상품 1개 조회, 주문 불가 상품 제외(고객) */	
	public ProductDTO selectProductByProductCode(Long productCode) {
		
		log.info("[ProductService] selectProductByProductCode START🥳 =================================================");
		log.info("[ProductService] productCode : {}", productCode);
		
		Product product = productRepository.findByProductCode(productCode)
				.orElseThrow(() -> new IllegalArgumentException("해당 코드의 상품이 없어유🤕 productCode=" + productCode)); 
				// Optional 타입으로 반환되므로 NPE를 컨트롤 하기 위한 orElseThrow() 구문 작성
		
		ProductDTO productDTO = modelMapper.map(product, ProductDTO.class);
		productDTO.setProductImageUrl(IMAGE_URL + productDTO.getProductImageUrl());
		
		log.info("[ProductService] productDTO : {}", productDTO);
		log.info("[ProductService] selectProductByProductCode END😎 =================================================");
		
		return productDTO;
	}
	/* 6. 상품 상세 조회 - productCode로 상품 1개 조회, 주문 불가 상품 포함(관리자) */
	public ProductDTO selectProductByProductCodeForAdmin(Long productCode) {
		
		log.info("[ProductService] selectProductByProductCodeForAdmin START🥳 =================================================");
		log.info("[ProductService] productCode : {}", productCode);
		
		Product product = productRepository.findById(productCode) // 기존에 정의되어있는 findId() 사용
				.orElseThrow(() -> new IllegalArgumentException("해당 코드의 상품이 없어유🤕 productCode=" + productCode)); 
				// Optional 타입으로 반환되므로 NPE를 컨트롤 하기 위한 orElseThrow() 구문 작성
		
		ProductDTO productDTO = modelMapper.map(product, ProductDTO.class);
		productDTO.setProductImageUrl(IMAGE_URL + productDTO.getProductImageUrl());
		
		log.info("[ProductService] productDTO : {}", productDTO);
		log.info("[ProductService] selectProductByProductCodeForAdmin END😎 =================================================");
		
		return productDTO;
	}
	/* 7. 상품 등록(관리자) */
	@Transactional
	public void insertProduct(ProductDTO productDTO) {
		
		log.info("[ProductService] insertProduct START🥳 =================================================");
		log.info("[ProductService] productDTO : {}", productDTO);

		String imageName = UUID.randomUUID().toString().replace("-", ""); // -를 공백으로 replace
		
		try {
			String replaceFileName = FileUploadUtils.saveFile(IMAGE_DIR, imageName, productDTO.getProductImage());
			productDTO.setProductImageUrl(replaceFileName);
			
			log.info("[ProductService] replaceFileName : {}", replaceFileName);
			
			productRepository.save(modelMapper.map(productDTO, Product.class)); // 비영속성(productDTO) 객체를 영속성(Product) 객체로 변환
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		log.info("[ProductService] insertProduct END😎 =================================================");
		
	}
	/* 8. 상품 수정(관리자) */
	@Transactional
	public void updateProduct(ProductDTO productDTO) {
		
		log.info("[ProductService] updateProduct START🥳 =================================================");
		log.info("[ProductService] productDTO : {}", productDTO);
		
		Product originalProduct = productRepository.findById(productDTO.getProductCode())
				.orElseThrow(() -> new IllegalArgumentException("해당 코드의 상품이 없어유🤢 productCode= " + productDTO.getProductCode()));
		
		/* 이미지를 변경하는 경우 */
		try {
			/* 사용자로부터 넘어온 이미지가 없을 경우(null), 아래의 코드는 실행 X */
			if(productDTO.getProductImage() != null) {
				
				/* 새로 입력 된 이미지 저장 */
				String imageName = UUID.randomUUID().toString().replace("-", "");
				String replaceFileName = FileUploadUtils.saveFile(IMAGE_DIR, imageName, productDTO.getProductImage());
				
				/* 기존에 저장 된 이미지 삭제 */
				FileUploadUtils.deleteFile(IMAGE_DIR, originalProduct.getProductImageUrl()); // 경로, 저장되어있는 이미지 url
				
				/* DB에 저장 될 imageUrl 값을 수정 */
				originalProduct.setProductImageUrl(replaceFileName);
				
			}
			
			/* 이미지를 변경하지 않는 경우에는 별도의 처리 필요 X */
			
			/* 조회했던 기존 엔티티의 내용을 수정 -> 별도의 수정 메소드를 정의해서 사용하면 다른 방식의 수정 막기 가능 */
			originalProduct.update(productDTO.getProductName(),
					 			   productDTO.getProductPrice(),
								   productDTO.getProductDescription(),
								   productDTO.getProductOrderable(),
								   modelMapper.map(productDTO.getCategory(), Category.class), // Category를 참조하므로 modelMapper를 이용하여 변환
								   productDTO.getProductStock());
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		
		log.info("[ProductService] updateProduct END😎 =================================================");
		
	}
	/* 9. 상품 삭제(관리자) */
	@Transactional
	public void deleteProduct(ProductDTO productDTO) {
		
		log.info("[ProductService] deleteProduct START🥳 =================================================");
		log.info("[ProductService] productDTO : {}", productDTO);
		
		/* 해당 code를 가진 상품이 있는지 먼저 확인 후, 없으면 Exception을 날리는 구문 
		 * (deleteById는 Optional이 아닌 void를 반환하므로 직접 orElseThrow() 메소드를 사용 불가) */
		Product originalProduct = productRepository.findById(productDTO.getProductCode())
				.orElseThrow(() -> new IllegalArgumentException("해당 코드의 상품이 없어유🤢 productCode= " + productDTO.getProductCode()));
		
		productRepository.deleteById(productDTO.getProductCode());
		
		log.info("[ProductService] deleteProduct END😎 =================================================");
		
	}
}

👀 Controller

👉 ProductController

@RequestParam @PathVariable

@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ProductController {

	private final ProductService productService;
	
	public ProductController(ProductService productService) {
		this.productService = productService;
	}
	/* 1. 상품 목록 조회 - 페이징, 주문 불가 상품 제외(고객) */
	@GetMapping("/products")
	public ResponseEntity<ResponseDTO> selectProductList(@RequestParam(name="page", defaultValue="1") int page) {
		
		log.info("[ProductController] selectProductList START🥳 =================================================");
		log.info("[ProductController] page : {}", page);
		
		Page<ProductDTO> productDTOList = productService.selectProductList(page);
		
		PagingButtonInfo pageInfo = Pagenation.getPagingButtonInfo(productDTOList);
		log.info("[ProductController] pageInfo : {}", pageInfo);
		
		ResponseDTOWithPaging responseDTOWithPaging = new ResponseDTOWithPaging();
		responseDTOWithPaging.setPageInfo(pageInfo);
		responseDTOWithPaging.setData(productDTOList.getContent()); // DTOList에서 Content를 꺼내서 set
		log.info("[ProductController] productDTOList.getContent() : {}", productDTOList.getContent());
		
		log.info("[ProductController] selectProductList END😎 =================================================");
		
		return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공", responseDTOWithPaging));
	}
	/* 2. 상품 목록 조회 - 페이징, 주문 불가 상품 포함(관리자) */
	@GetMapping("/products-management")
	public ResponseEntity<ResponseDTO> selectProductListForAdmin(@RequestParam(name="page", defaultValue="1") int page) {
		
		log.info("[ProductController] selectProductListForAdmin START🥳 =================================================");
		log.info("[ProductController] page : {}", page);
		
		Page<ProductDTO> productDTOList = productService.selectProductListForAdmin(page);
		
		PagingButtonInfo pageInfo = Pagenation.getPagingButtonInfo(productDTOList);
		log.info("[ProductController] pageInfo : {}", pageInfo);
		
		ResponseDTOWithPaging responseDTOWithPaging = new ResponseDTOWithPaging();
		responseDTOWithPaging.setPageInfo(pageInfo);
		responseDTOWithPaging.setData(productDTOList.getContent()); // DTOList에서 Content를 꺼내서 set
		log.info("[ProductController] productDTOList.getContent() : {}", productDTOList.getContent());
		
		log.info("[ProductController] selectProductListForAdmin END😎 =================================================");
		
		return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공", responseDTOWithPaging));
	}
	/* 3. 상품 목록 조회 - 카테고리 기준, 페이징, 주문 불가 상품 제외(고객) */
	@GetMapping("/products/categories/{categoryCode}")
	public ResponseEntity<ResponseDTO> selectProductListByCategory(
			@RequestParam(name="page", defaultValue="1") int page, @PathVariable Long categoryCode) {
		
		log.info("[ProductController] selectProductListByCategory START🥳 =================================================");
		log.info("[ProductController] page : {}", page);
		
		Page<ProductDTO> productDTOList = productService.selectProductListByCategory(page, categoryCode);
		
		PagingButtonInfo pageInfo = Pagenation.getPagingButtonInfo(productDTOList);
		log.info("[ProductController] pageInfo : {}", pageInfo);
		
		ResponseDTOWithPaging responseDTOWithPaging = new ResponseDTOWithPaging();
		responseDTOWithPaging.setPageInfo(pageInfo);
		responseDTOWithPaging.setData(productDTOList.getContent()); // DTOList에서 Content를 꺼내서 set
		log.info("[ProductController] productDTOList.getContent() : {}", productDTOList.getContent());
		
		log.info("[ProductController] selectProductListByCategory END😎 =================================================");
		
		return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공", responseDTOWithPaging));
	}
	/* 4. 상품 목록 조회 - 상품명 검색 기준, 페이징, 주문 불가 상품 제외(고객) 
	 * productName 값은 parameter로 전달 받도록 하여 URL 설정 */
	@GetMapping("/products/search")
	public ResponseEntity<ResponseDTO> selectProductListByProductName(
			@RequestParam(name="page", defaultValue="1") int page, @RequestParam(name="productName") String productName) {
		
		log.info("[ProductController] selectProductListByProductName START🥳 =================================================");
		log.info("[ProductController] page : {}", page);
		log.info("[ProductController] productName : {}", productName);
		
		Page<ProductDTO> productDTOList = productService.selectProductListByProductName(page, productName);
		
		PagingButtonInfo pageInfo = Pagenation.getPagingButtonInfo(productDTOList);
		log.info("[ProductController] pageInfo : {}", pageInfo);
		
		ResponseDTOWithPaging responseDTOWithPaging = new ResponseDTOWithPaging();
		responseDTOWithPaging.setPageInfo(pageInfo);
		responseDTOWithPaging.setData(productDTOList.getContent()); // DTOList에서 Content를 꺼내서 set
		log.info("[ProductController] productDTOList.getContent() : {}", productDTOList.getContent());
		
		log.info("[ProductController] selectProductListByProductName END😎 =================================================");
		
		return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공", responseDTOWithPaging));
	}
	/* 5. 상품 상세 조회 - productCode로 상품 1개 조회, 주문 불가 상품 제외(고객) */	
	@GetMapping("/products/{productCode}")
	public ResponseEntity<ResponseDTO> selectProductDetail(@PathVariable Long productCode) {

		// 페이징이 필요 없으므로 따로 가공하는 코드 필요 X
		
		return ResponseEntity
				.ok()
				.body(new ResponseDTO(HttpStatus.OK, "조회 성공", productService.selectProductByProductCode(productCode)));
	}
	/* 6. 상품 상세 조회 - productCode로 상품 1개 조회, 주문 불가 상품 포함(관리자) */
	@GetMapping("/products-management/{productCode}")
	public ResponseEntity<ResponseDTO> selectProductDetailForAdmin(@PathVariable Long productCode) {

		// 페이징이 필요 없으므로 따로 가공하는 코드 필요 X
		
		return ResponseEntity
				.ok()
				.body(new ResponseDTO(HttpStatus.OK, "조회 성공", productService.selectProductByProductCodeForAdmin(productCode)));
	}
	/* 7. 상품 등록(관리자) */
	@PostMapping("/products")
	public ResponseEntity<ResponseDTO> insertProduct(@ModelAttribute ProductDTO productDTO) { // @ModelAttribute : url encoded 방식으로 전달 받음
		
		productService.insertProduct(productDTO);
		
		return ResponseEntity
				.ok()
				.body(new ResponseDTO(HttpStatus.OK, "상품 등록 성공"));
		
	}
	/* 8. 상품 수정(관리자) */
	@PutMapping("/products")
	public ResponseEntity<ResponseDTO> updateProduct(@ModelAttribute ProductDTO productDTO) {
		
		productService.updateProduct(productDTO);
		
		return ResponseEntity
				.ok()
				.body(new ResponseDTO(HttpStatus.OK, "상품 수정 성공"));
	}
	/* 9. 상품 삭제(관리자) */
	@DeleteMapping("/products")
	public ResponseEntity<ResponseDTO> deleteProduct(@ModelAttribute ProductDTO productDTO) {
		
		productService.deleteProduct(productDTO);
		
		return ResponseEntity
				.ok()
				.body(new ResponseDTO(HttpStatus.OK, "상품 삭제 성공"));
	}
    
}
profile
Tiny little habits make me

0개의 댓글