본문 바로가기

프로젝트/booktalk(책 중고 거래 서비스)

[SpringBoot] AWS S3로 이미지 업로드하기

일다 AWS에서 S3 버킷을 생성한다.

순서대로 만든다.

 

 

 

 

IAM 사용자 권한 추가

S3에 접근하기 위해서는 IAM 사용자에게 S3 접근 권한을 주고, 그 사용자의 액세스 키, 비밀 엑세스 키를 사용해야 한다.

사용자를 만들고

 

권한을 설정한다.

 

 

설정후에 사용자 추가를 하면 액세스 키, 비밀 엑세스 키가 보여지는데 이 키들은 현재 화면에서 밖에 볼 수 없다. 저장해두자.

 

 

Spring Boot로 파일 업로드

위에 s3 버킷 설정을 다해주면 이제 스프링부트 프로젝트만 수정해주면 된다.

 

build.gradle에 의존성 추가

//S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

 

application.yml 작성하기

 

cloud:
  aws:
    s3:
      bucket: ${BUCKET}
    stack:
      auto: false
    region:
      static: ap-northeast-2
    credentials:
      access-key: ${ACCESS_KEY}
      secret-key: ${SECRET_KEY}

 

 

 

S3config 작성하기

 

@Configuration
public class S3Config {

    // AWS 자격 증명(access key와 secret key)을 외부 프로퍼티에서 가져오기 위한 필드
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    // AWS S3 클라이언트를 생성할 때 사용할 리전 정보를 외부 프로퍼티에서 가져오기 위한 필드
    @Value("${cloud.aws.region.static}")
    private String region;

    // AmazonS3Client 빈을 생성하는 메서드
    @Bean
    public AmazonS3Client amazonS3Client() {
        // AWS 자격 증명 객체 생성
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

        // AmazonS3 클라이언트를 생성하고 반환
        return (AmazonS3Client) AmazonS3ClientBuilder
                .standard()
                .withRegion(region)  // 생성할 클라이언트의 리전 설정
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))  // 자격 증명 제공자 설정
                .build();
    }
}

이 코드는 AWS S3 클라이언트를 구성하는 Spring Bean을 생성하는데 사용되는 Java Configuration 클래스인 것 같습니다. 주로 AWS S3를 사용하기 위한 AWS 자격 증명(access key와 secret key) 및 리전(region) 정보를 포함하고 있습니다.

 

imageUpload 작성

public String imageUpload(@RequestParam("upload") MultipartFile file) throws IOException {
    // 1. 업로드된 파일의 원본 파일명을 가져옵니다.
    String fileName = file.getOriginalFilename();
    
    // 2. 파일의 확장자를 추출합니다.
    String ext = fileName.substring(fileName.lastIndexOf("."));

    // 3. 고유한 식별자(UUID)를 생성하여 파일명에 확장자를 추가하여 새로운 파일명을 만듭니다.
    String uuidFileName = UUID.randomUUID() + ext;

    try (InputStream inputStream = file.getInputStream()) {
        // 4. MultipartFile에서 InputStream을 얻어서 AWS S3에 업로드할 준비를 합니다.
        
        // 5. AWS S3 객체에 대한 메타데이터를 설정하는 객체를 생성합니다.
        ObjectMetadata metadata = new ObjectMetadata();

        // 6. InputStream에서 바이트 배열로 데이터를 읽어옵니다.
        byte[] bytes = IOUtils.toByteArray(inputStream);

        // 7. 메타데이터에 업로드된 파일의 컨텐츠 타입을 설정합니다.
        metadata.setContentType(file.getContentType());

        // 8. 메타데이터에 업로드된 파일의 크기를 설정합니다.
        metadata.setContentLength(bytes.length);

        // 9. 바이트 배열을 이용하여 ByteArrayInputStream을 생성합니다.
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        // 10. AWS S3 클라이언트를 사용하여 이미지를 업로드합니다.
        s3Config.amazonS3Client().putObject(
            new PutObjectRequest(bucket, uuidFileName, byteArrayInputStream, metadata)
                .withCannedAcl(CannedAccessControlList.PublicRead));
    }

    // 11. 업로드된 이미지의 S3 URL을 생성합니다.
    String s3Url = s3Config.amazonS3Client().getUrl(bucket, uuidFileName).toString();
    
    // 12. 생성된 S3 URL을 반환합니다.
    return s3Url;
}

이 코드는 Spring에서 AWS S3를 사용하여 이미지를 업로드하고, 해당 이미지의 고유한 URL을 생성하여 반환하는 간단한 메서드입니다. 업로드된 이미지는 S3 버킷에서 공개 읽기 권한이 부여된 상태로 저장됩니다.

 

 

모델 작성하기

@Entity
@NoArgsConstructor
@Getter
@Table(name="TB_IMAGE")
public class ImageFile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String imagePathUrl;

    // 다대일(N:1) 관계에서 여러 개의 ImageFile이 하나의 User에 매핑됩니다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    // 다대일(N:1) 관계에서 여러 개의 ImageFile이 하나의 Product에 매핑됩니다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    // Lombok의 @Builder 어노테이션으로 생성된 빌더 패턴을 사용하여 객체를 생성할 수 있습니다.
    @Builder
    private ImageFile(String imagePathUrl, User user, Product product){
        this.imagePathUrl = imagePathUrl;
        this.user = user;
        this.product = product;
    };
}

요약하면, 이 코드는 이미지 파일 정보를 저장하는 JPA 엔터티로, 해당 이미지 파일이 어떤 사용자(User) 또는 어떤 상품(Product)에 속하는지를 나타내고 있습니다.

 

 

래퍼지토리 작성

package com.example.booktalk.domain.imageFile.repository;


import com.example.booktalk.domain.imageFile.entity.ImageFile;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ImageFileRepository extends JpaRepository<ImageFile, Long> {

    List<ImageFile> findByProductId(Long productId);
    
}

이 레포지토리 인터페이스는 Spring Data JPA에서 제공하는 JpaRepository를 확장하여 ImageFile 엔터티에 대한 기본적인 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있도록 합니다. 또한 findByProductId 메서드를 통해 특정 상품(Product)에 속하는 이미지 파일 목록을 조회할 수 있습니다.

 

 

product컨트롤러 작성
 
package com.example.booktalk.domain.product.controller;


import com.example.booktalk.domain.product.dto.request.ProductCreateReq;
import com.example.booktalk.domain.product.dto.request.ProductUpdateReq;
import com.example.booktalk.domain.product.dto.response.ProductCreateRes;
import com.example.booktalk.domain.product.dto.response.ProductDeleteRes;
import com.example.booktalk.domain.product.dto.response.ProductGetRes;
import com.example.booktalk.domain.product.dto.response.ProductListRes;
import com.example.booktalk.domain.product.dto.response.ProductSerachListRes;
import com.example.booktalk.domain.product.dto.response.ProductTagListRes;
import com.example.booktalk.domain.product.dto.response.ProductTopLikesListRes;
import com.example.booktalk.domain.product.dto.response.ProductUpdateRes;
import com.example.booktalk.domain.product.service.ProductService;
import com.example.booktalk.global.security.UserDetailsImpl;
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v2/products")
public class ProductController {

    private final ProductService productService;

    @PostMapping
    public ResponseEntity<ProductCreateRes> createProduct(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestPart("req") ProductCreateReq req,
        @RequestParam("upload") List<MultipartFile> files
    ) throws IOException {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.createProduct(userDetails.getUser().getId(), req, files));

    }

    @PutMapping("/{productId}")
    public ResponseEntity<ProductUpdateRes> updateProduct(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable Long productId,
        @RequestPart("req") ProductUpdateReq req,
        @RequestParam("upload") List<MultipartFile> files
    ) throws IOException {
        return ResponseEntity.status(HttpStatus.OK)
            .body(
                productService.updateProduct(userDetails.getUser().getId(), productId, req, files));
    }

    @DeleteMapping("/{productId}")
    public ResponseEntity<ProductDeleteRes> deleteProduct(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable Long productId
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.deleteProduct(userDetails.getUser().getId(), productId));
    }

    @GetMapping("/{productId}") //단일 조회
    public ResponseEntity<ProductGetRes> getProduct(
        @PathVariable Long productId
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProduct(productId));
    }


    @GetMapping //상품 리스트 조회
    public ResponseEntity<Page<ProductListRes>> getProductList(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "sortBy", defaultValue = "createdAt") String sortBy,
        @RequestParam(value = "isAsc", defaultValue = "false") boolean isAsc
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductList(page - 1, size, sortBy, isAsc));
    }

    @GetMapping("/main") //메인화면에 관심상품 Top3 인 product 출력
    public ResponseEntity<List<ProductTopLikesListRes>> getProductListTopThree() {

        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductListByLikesTopThree());
    }

    @GetMapping("/search") //상품 검색 리스트 조회
    public ResponseEntity<Page<ProductSerachListRes>> getProductSearchList(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "sortBy", defaultValue = "createdAt") String sortBy,
        @RequestParam(value = "isAsc", defaultValue = "false") boolean isAsc,
        @RequestParam(value = "query") String search
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductSearchList(page - 1, size, sortBy, isAsc, search));
    }

    @GetMapping("/tag") //상품 검색 리스트 조회
    public ResponseEntity<Page<ProductTagListRes>> getProductListByTag(
        @RequestParam("page") int page,
        @RequestParam("size") int size,
        @RequestParam(value = "sortBy", defaultValue = "createdAt") String sortBy,
        @RequestParam(value = "isAsc", defaultValue = "false") boolean isAsc,
        @RequestParam(value = "tag") String search
    ) {
        return ResponseEntity.status(HttpStatus.OK)
            .body(productService.getProductSearchTagList(page - 1, size, sortBy, isAsc, search));
    }

}

List<MultipartFile> files로 이미지를 받아준다.

 

 

 

서비스 작성

@Service
@RequiredArgsConstructor
@Transactional
public class ImageFileService {

    private final UserRepository userRepository;
    private final S3Config s3Config;
    private final ImageFileRepository imageFileRepository;
    private final ProductRepository productRepository;

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    /**
     * 이미지 업로드 및 저장 후 생성된 경로를 반환합니다.
     * @param file 업로드된 파일
     * @return 이미지의 S3 경로
     * @throws IOException 입출력 예외
     */
    public String imageUpload(@RequestParam("upload") MultipartFile file) throws IOException {
        // 파일명과 확장자 분리하여 UUID를 추가한 새로운 파일명 생성
        String fileName = file.getOriginalFilename();
        String ext = fileName.substring(fileName.lastIndexOf("."));
        String uuidFileName = UUID.randomUUID() + ext;

        // 파일을 S3에 업로드
        try (InputStream inputStream = file.getInputStream()) {
            ObjectMetadata metadata = new ObjectMetadata();
            byte[] bytes = IOUtils.toByteArray(inputStream);
            metadata.setContentType(file.getContentType());
            metadata.setContentLength(bytes.length);
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

            s3Config.amazonS3Client().putObject(
                new PutObjectRequest(bucket, uuidFileName, byteArrayInputStream,
                    metadata).withCannedAcl(
                    CannedAccessControlList.PublicRead));
        }

        // S3에 업로드된 파일의 경로 반환
        String s3Url = s3Config.amazonS3Client().getUrl(bucket, uuidFileName).toString();
        return s3Url;
    }

    /**
     * 사용자 및 이미지 파일 권한 검증을 수행합니다.
     * @param user 사용자 객체
     * @param imageFile 이미지 파일 객체
     */
    private void validateProductUser(User user, ImageFile imageFile) {
        if (!user.getId().equals(imageFile.getUser().getId())
                && !user.getRole().equals(UserRoleType.ADMIN)) {
            throw new NotPermissionAuthority(ProductErrorCode.NOT_PERMISSION_AUTHORITHY);
        }
    }

    /**
     * 이미지를 생성하고 해당 이미지 파일 정보를 반환합니다.
     * @param userId 사용자 식별자
     * @param productId 상품 식별자
     * @param files 업로드된 이미지 파일 리스트
     * @return 생성된 이미지 파일 정보 리스트
     * @throws IOException 입출력 예외
     */
    public List<ImageCreateRes> createImage(Long userId, Long productId, List<MultipartFile> files)
        throws IOException {
        List<ImageCreateRes> imageCreateResList = new ArrayList<>();

        // 각 파일에 대해 이미지 업로드 및 정보 저장
        for (MultipartFile file : files) {
            String imagePathUrl = imageUpload(file);
            User user = userRepository.findUserByIdWithThrow(userId);
            Product product = productRepository.findProductByIdWithThrow(productId);
            ImageFile imageFile = ImageFile.builder()
                .imagePathUrl(imagePathUrl)
                .user(user)
                .product(product)
                .build();
            imageFileRepository.save(imageFile);
            ImageCreateRes imageResponse = new ImageCreateRes(imageFile.getImagePathUrl());
            imageCreateResList.add(imageResponse);
        }
        return imageCreateResList;
    }

    /**
     * 상품 식별자에 해당하는 이미지 파일 정보 리스트를 조회합니다.
     * @param productId 상품 식별자
     * @return 이미지 파일 정보 리스트
     */
    @Transactional(readOnly = true)
    public List<ImageListRes> getImages(Long productId) {
        List<ImageFile> imageList = imageFileRepository.findByProductId(productId);
        return imageList.stream()
            .map(imageFile -> new ImageListRes(imageFile.getImagePathUrl()))
            .toList();
    }

    /**
     * 이미지를 업데이트하고 해당 이미지 파일 정보를 반환합니다.
     * @param userId 사용자 식별자
     * @param productId 상품 식별자
     * @param files 업로드된 이미지 파일 리스트
     * @return 업데이트된 이미지 파일 정보 리스트
     * @throws IOException 입출력 예외
     */
    public List<ImageCreateRes> updateImage(Long userId, Long productId, List<MultipartFile> files)
        throws IOException {
        // 이미지 삭제 후 새로운 이미지 생성
        deleteImage(userId, productId);
        return createImage(userId, productId, files);
    }

    /**
     * 이미지를 삭제하고 해당 상품의 이미지 삭제 응답을 반환합니다.
     * @param userId 사용자 식별자
     * @param productId 상품 식별자
     * @return 이미지 삭제 응답
     */
    public ImageDeleteRes deleteImage(Long userId, Long productId) {
        User user = userRepository.findUserByIdWithThrow(userId);
        List<ImageFile> imageFileList = imageFileRepository.findByProductId(productId);
        for (ImageFile imageFile : imageFileList) {
            // 이미지 파일 권한 검증 및 삭제
            validateProductUser(user, imageFile);
            imageFileRepository.delete(imageFile);
        }
        return new ImageDeleteRes("삭제가 완료되었습니다.");
    }
}

imageFileService을 작성한후 ProductService에서 가져가서 쓴다.

 

package com.example.booktalk.domain.product.service;

import com.example.booktalk.domain.category.entity.Category;
import com.example.booktalk.domain.category.exception.CategoryErrorCode;
import com.example.booktalk.domain.category.exception.NotFoundCategoryException;
import com.example.booktalk.domain.category.repository.CategoryRepository;
import com.example.booktalk.domain.imageFile.dto.response.ImageCreateRes;
import com.example.booktalk.domain.imageFile.dto.response.ImageListRes;
import com.example.booktalk.domain.imageFile.service.ImageFileService;
import com.example.booktalk.domain.product.dto.request.ProductCreateReq;
import com.example.booktalk.domain.product.dto.request.ProductUpdateReq;
import com.example.booktalk.domain.product.dto.response.ProductCreateRes;
import com.example.booktalk.domain.product.dto.response.ProductDeleteRes;
import com.example.booktalk.domain.product.dto.response.ProductGetRes;
import com.example.booktalk.domain.product.dto.response.ProductListRes;
import com.example.booktalk.domain.product.dto.response.ProductSerachListRes;
import com.example.booktalk.domain.product.dto.response.ProductTagListRes;
import com.example.booktalk.domain.product.dto.response.ProductTopLikesListRes;
import com.example.booktalk.domain.product.dto.response.ProductUpdateRes;
import com.example.booktalk.domain.product.entity.Product;
import com.example.booktalk.domain.product.exception.NotPermissionAuthority;
import com.example.booktalk.domain.product.exception.ProductErrorCode;
import com.example.booktalk.domain.product.repository.ProductRepository;
import com.example.booktalk.domain.productcategory.entity.ProductCategory;
import com.example.booktalk.domain.productcategory.repository.ProductCategoryRepository;
import com.example.booktalk.domain.user.dto.response.UserRes;
import com.example.booktalk.domain.user.entity.User;
import com.example.booktalk.domain.user.entity.UserRoleType;
import com.example.booktalk.domain.user.repository.UserRepository;
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
@Transactional
public class ProductService {

    private final ProductRepository productRepository;
    private final UserRepository userRepository;
    private final CategoryRepository categoryRepository;
    private final ProductCategoryRepository productCategoryRepository;
    private final ImageFileService imageFileService;

    public ProductCreateRes createProduct(Long userId, ProductCreateReq req,
        List<MultipartFile> files) throws IOException {

        User user = userRepository.findUserByIdWithThrow(userId);

        Product product = Product.builder()
            .name(req.name())
            .price(req.price())
            .quantity(req.quantity())
            .region(req.region())
            .content(req.content())
            .user(user)
            .build();
        product = productRepository.save(product);

        addCategory(req.categoryList(), product);
        UserRes userRes = new UserRes(user.getId(), user.getNickname());

        List<ImageCreateRes> imageCreateResList = imageFileService.createImage(userId,
            product.getId(), files);

        return new ProductCreateRes(product.getId(), product.getName(), product.getQuantity(),
            product.getPrice()
            , product.getRegion(), product.getFinished(), userRes, product.getContent(),
            req.categoryList(), imageCreateResList);
        //TODO 생성자로 한줄정리

    }

    public ProductUpdateRes updateProduct(Long userId, Long productId, ProductUpdateReq req,
        List<MultipartFile> files) throws IOException {

        User user = userRepository.findUserByIdWithThrow(userId);
        Product product = productRepository.findProductByIdWithThrow(productId);
        validateProductUser(user, product);

        List<ImageCreateRes> imageCreateResList = imageFileService.updateImage(userId, productId,
            files);

        product.update(req);
        updateCategory(req.categoryList(), product);
        UserRes userRes = new UserRes(user.getId(), user.getNickname());
        return new ProductUpdateRes(product.getId(), product.getName(),
            product.getQuantity(), product.getPrice(), product.getRegion(),
            product.getFinished(), userRes, product.getProductLikeCnt(), product.getContent(),
            req.categoryList(), imageCreateResList);

    }

    @Transactional(readOnly = true)
    public ProductGetRes getProduct(Long productId) {

        Product product = productRepository.findProductByIdWithThrow(productId);
        User user = product.getUser();
        UserRes userRes = new UserRes(user.getId(), user.getNickname());

        List<String> categories = product.getProductCategoryList().stream()
            .map(productCategory -> {
                return productCategory.getCategory().getName();
            })
            .toList();
        List<ImageListRes> imageListRes = imageFileService.getImages(productId);

        return new ProductGetRes(product.getId(), product.getName(), product.getPrice()
            , product.getQuantity(), userRes, product.getRegion(), categories,
            product.getProductLikeCnt(), product.getContent(),
            product.getFinished(), imageListRes);

    }

    @Transactional(readOnly = true)
    public Page<ProductListRes> getProductList(int page, int size, String sortBy, boolean isAsc) {

        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);

        Page<Product> productList = productRepository.findAllByDeletedFalse(pageable);

        return productList
            .map(product -> {
                List<ImageListRes> imageListRes = imageFileService.getImages(product.getId());

                List<String> categories = product.getProductCategoryList().stream()
                    .map(productCategory -> {
                        return productCategory.getCategory().getName();
                    })
                    .toList();
                ImageListRes imageGetRes = imageListRes.isEmpty() ? null : imageListRes.get(0);

                return new ProductListRes(product.getId(), product.getName(), product.getPrice(),
                    product.getQuantity(), product.getProductLikeCnt(), categories,
                    product.getRegion(), imageGetRes);
            });

    }

    @Transactional(readOnly = true)
    public List<ProductTopLikesListRes> getProductListByLikesTopThree() {

        List<Product> productList = productRepository.findTop3ByDeletedFalseOrderByProductLikeCntDesc();

        return productList.stream()
            .map(product -> {

                List<ImageListRes> imageListRes = imageFileService.getImages(product.getId());

                List<String> categories = product.getProductCategoryList().stream()
                    .map(productCategory -> {
                        return productCategory.getCategory().getName();
                    })
                    .toList();
                ImageListRes imageGetRes =
                    imageListRes.isEmpty() ? new ImageListRes(null) : imageListRes.get(0);

                return new ProductTopLikesListRes(product.getId(), product.getName(),
                    product.getPrice(),
                    product.getQuantity(), product.getProductLikeCnt(), categories,
                    product.getRegion(), imageGetRes);

            })
            .toList();
    }

    @Transactional(readOnly = true)
    public Page<ProductSerachListRes> getProductSearchList(int page, int size, String sortBy,
        Boolean isAsc,
        String search) {

        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);
        Page<Product> productList = productRepository.getPostListByName(pageable, search);
        return productList
            .map(product -> {
                List<ImageListRes> imageListRes = imageFileService.getImages(product.getId());
                List<String> categories = product.getProductCategoryList().stream()
                    .map(productCategory -> {
                        return productCategory.getCategory().getName();
                    })
                    .toList();
                ImageListRes imageGetRes =
                    imageListRes.isEmpty() ? new ImageListRes(null) : imageListRes.get(0);
                return new ProductSerachListRes(product.getId(), product.getName(),
                    product.getPrice(),
                    product.getQuantity(), product.getProductLikeCnt(), categories,
                    product.getRegion(), imageGetRes);
            });

    }

    @Transactional(readOnly = true)
    public Page<ProductTagListRes> getProductSearchTagList(int page, int size, String sortBy,
        Boolean isAsc,
        String tag) {

        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);

        Page<Product> productList = productRepository.getProductListByTag(pageable, tag);
        return productList
            .map(product -> {
                List<ImageListRes> imageListRes = imageFileService.getImages(product.getId());
                List<String> categories = product.getProductCategoryList().stream()
                    .map(productCategory -> {
                        return productCategory.getCategory().getName();
                    })
                    .toList();
                ImageListRes imageGetRes =
                    imageListRes.isEmpty() ? new ImageListRes(null) : imageListRes.get(0);
                return new ProductTagListRes(product.getId(), product.getName(), product.getPrice(),
                    product.getQuantity(), product.getProductLikeCnt(), categories,
                    product.getRegion(), imageGetRes);
            });

    }


    public ProductDeleteRes deleteProduct(Long userId, Long productId) {
        User user = userRepository.findUserByIdWithThrow(userId);
        Product product = productRepository.findProductByIdWithThrow(productId);
        validateProductUser(user, product);
        imageFileService.deleteImage(userId, productId);

        product.deleted();

        return new ProductDeleteRes("삭제가 완료되었습니다.");

    }


    private void validateProductUser(User user, Product product) {

        if (!user.getId().equals(product.getUser().getId())
                && !user.getRole().equals(UserRoleType.ADMIN)) {
            throw new NotPermissionAuthority(ProductErrorCode.NOT_PERMISSION_AUTHORITHY);
        }
    }

    private List<Category> findCategoryList(List<String> categoryList) {
        List<Category> categories = categoryRepository.findByNameIn(categoryList);

        if (categoryList.size() != categories.size()) {
            throw new NotFoundCategoryException(CategoryErrorCode.NOT_FOUND_CATEGORY);
        }

        return categories;
    }

    private void addCategory(List<String> categoryList, Product product) {
        List<Category> categories = findCategoryList(categoryList);

        categories.forEach(category -> {
            ProductCategory productCategory = ProductCategory.builder()
                .category(category)
                .product(product)
                .build();

            productCategoryRepository.save(productCategory);
            product.addProductCategory(productCategory);
        });
    }


    private void removeCategory(List<String> categoryList, Product product) {

        List<ProductCategory> productCategoryList = productCategoryRepository.findAllByProductAndCategory_NameIn(
            product, categoryList);

        productCategoryList.forEach(productCategory ->
        {
            productCategoryRepository.delete(productCategory);
            //product.removeProductCategory(productCategory); remove도 따로 만들어줘야할까..?
        });


    }


    private void updateCategory(List<String> reqCategoryList, Product product) {

        List<String> currentCategories = product.getProductCategoryList()
            .stream() //성능이슈 확인 N + 1
            .map(ProductCategory::getCategory)
            .map(Category::getName).toList();

        List<String> removeableCategoryList = currentCategories.stream()
            .filter(category -> !reqCategoryList.contains(category))
            .toList();

        removeCategory(removeableCategoryList, product);

        List<String> addableCategoryList = reqCategoryList.stream()
            .filter(category -> !currentCategories.contains(category))
            .toList();

        addCategory(addableCategoryList, product);

    }

}

 

'프로젝트 > booktalk(책 중고 거래 서비스)' 카테고리의 다른 글

product  (1) 2024.01.25
image파일 업로드 할때 리사이징 적용하기  (0) 2024.01.24
1/15  (0) 2024.01.23
1/11  (0) 2024.01.23
1/10  (0) 2024.01.23