🙋 기본 설정 셋팅
# 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 # 이미지들을 저장할 상대 경로 설정
<!-- 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>
@Configuration
public class BeanConfig {
/* pom.xml에 ModelMapper 의존성 주입 후, 여기서 config */
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
🙋 Security를 위한 셋팅
@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 처리를 위한 셋팅
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());
}
}
@AllArgsConstructor
@Data
@AllArgsConstructor
@Data
public class PagingButtonInfo {
private int currentPage;
private int startPage;
private int endPage;
private int maxPage;
}
@Data
public class ResponseDTOWithPaging {
private Object data;
private PagingButtonInfo pageInfo;
}
🙋 응답 및 예외처리를 위한 셋팅
@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;
}
}
@Data
public class ApiExceptionDTO {
private int state;
private String message;
public ApiExceptionDTO(HttpStatus status, String message) {
this.state = status.value();
this.message = message;
}
}
@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())); // 메세지 설정
}
}
🙋 파일 저장 및 삭제를 위한 셋팅
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);
}
}
}
@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;
}
@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;
}
}
@Data
public class CategoryDTO {
private Long categoryCode;
private String categoryName;
}
@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;
}
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
@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
}
@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😎 =================================================");
}
}
@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, "상품 삭제 성공"));
}
}