Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

상품 이미지 등록 및 수정 기능 #30 #46

Merged
merged 14 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ dependencies {
// Monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'

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

tasks.named('test') {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.helpmeCookies.global.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3 amazonS3() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey,secretKey);

return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
72 changes: 72 additions & 0 deletions src/main/java/com/helpmeCookies/global/utils/AwsS3FileUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.helpmeCookies.global.utils;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.helpmeCookies.product.dto.FileUploadResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

@Component
@RequiredArgsConstructor
public class AwsS3FileUtils {
private final AmazonS3 amazonS3;

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

//다중파일 업로드후 url 반환
public List<FileUploadResponse> uploadMultiImages(List<MultipartFile> multipartFiles) {
List<FileUploadResponse> fileList = new ArrayList<>();

multipartFiles.forEach(file -> {
String fileName = createFileName(file.getOriginalFilename()); //파일 이름 난수화
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());

try (InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드 실패" + fileName);
}

fileList.add(new FileUploadResponse(amazonS3.getUrl(bucket,fileName).toString(),fileName));
});

return fileList;
}

public String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}

//TODO error handler 필요
public String getFileExtension(String fileName) {
try {
String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
//이미지 파일 확장자 목록
List<String> allowedExtensions = Arrays.asList(".jpg", ".jpeg", ".png");

if (!allowedExtensions.contains(extension)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미지 파일만 업로드가 가능합니다. 지원되지 않는 형식의 파일" + fileName);
}
return extension;
} catch (StringIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"잘못된 형식의 파일" + fileName + "입니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
package com.helpmeCookies.product.controller;

import com.helpmeCookies.product.dto.FileUploadResponse;
import com.helpmeCookies.product.dto.ProductImageResponse;
import com.helpmeCookies.product.dto.ProductRequest;
import com.helpmeCookies.product.dto.ProductResponse;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.service.ProductImageService;
import com.helpmeCookies.product.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

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

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}
private final ProductImageService productImageService;

@PostMapping
public ResponseEntity<Void> saveProduct(@RequestBody ProductRequest productRequest) {
Product product = productService.save(productRequest);
productService.save(productRequest);
return ResponseEntity.ok().build();
}

@PostMapping("/{productId}/images")
public ResponseEntity<ProductImageResponse> uploadImages(@PathVariable("productId") Long productId, List<MultipartFile> files) throws IOException {
List<FileUploadResponse> responses = productImageService.uploadMultiFiles(productId,files);
return ResponseEntity.ok(new ProductImageResponse(responses.stream().map(FileUploadResponse::photoUrl).toList()));
}

@GetMapping("/{productId}")
public ResponseEntity<ProductResponse> getProductInfo(@PathVariable("productId") Long productId) {
Product product = productService.find(productId);
Expand All @@ -36,6 +48,12 @@ public ResponseEntity<Void> editProductInfo(@PathVariable("productId") Long prod
return ResponseEntity.ok().build();
}

@PutMapping("/{productId}/images")
public ResponseEntity<Void> editImages(@PathVariable("productId") Long productId, List<MultipartFile> files) throws IOException {
productImageService.editImages(productId, files);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{productId}")
public ResponseEntity<Void> deleteProduct(@PathVariable("productId") Long productId) {
productService.delete(productId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.helpmeCookies.product.dto;

import com.helpmeCookies.product.entity.ProductImage;

public record FileUploadResponse(
String photoUrl,
String uuid
) {
public ProductImage toEntity(Long productId) {
return ProductImage.builder()
.productId(productId)
.photoUrl(photoUrl)
.uuid(uuid)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.helpmeCookies.product.dto;

import java.util.List;

public record ProductImageResponse(
List<String> urls
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public String getName() {
}

public static Category fromString(String name) {
System.out.println(name);
Category category = nameToCategoryMap.get(name);
if (category == null) {
throw new IllegalArgumentException(name + "에 해당하는 카테고리가 없습니다.");
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/com/helpmeCookies/product/entity/ProductImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Builder;

@Entity
public class ProductImage {
Expand All @@ -15,7 +14,15 @@ public class ProductImage {

private String photoUrl;

@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private Long productId;
private String uuid;

public ProductImage() {}

@Builder
public ProductImage(String photoUrl, Long productId, String uuid) {
this.photoUrl = photoUrl;
this.productId = productId;
this.uuid = uuid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.helpmeCookies.product.repository;

import com.helpmeCookies.product.entity.ProductImage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductImageRepository extends JpaRepository<ProductImage,Long> {
List<ProductImage> findAllByProductId(Long productId);
void deleteAllByProductId(Long productId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.helpmeCookies.product.service;

import com.helpmeCookies.global.utils.AwsS3FileUtils;
import com.helpmeCookies.product.dto.FileUploadResponse;
import com.helpmeCookies.product.repository.ProductImageRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ProductImageService {
private final AwsS3FileUtils awsS3FileUtils;
private final ProductImageRepository productImageRepository;

@Transactional
public List<FileUploadResponse> uploadMultiFiles(Long productId, List<MultipartFile> files) throws IOException {
List<FileUploadResponse> uploadResponses = awsS3FileUtils.uploadMultiImages(files);
uploadResponses.forEach(response ->
productImageRepository.save(response.toEntity(productId)));
return uploadResponses;
}

@Transactional
public void editImages(Long productId, List<MultipartFile> files) throws IOException {
//우선은 전부 삭제하고 다시 업로드
//추후에 개선 예정
productImageRepository.deleteAllByProductId(productId);
uploadMultiFiles(productId, files);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import com.helpmeCookies.product.dto.ProductRequest;
import com.helpmeCookies.product.entity.Category;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.repository.ProductImageRepository;
import com.helpmeCookies.product.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
private final ProductImageRepository productImageRepository;

public Product save(ProductRequest productSaveRequest) {
//TODO ArtistInfo 코드 병합시 수정 예정
Expand Down Expand Up @@ -43,6 +43,7 @@ public void edit(Long productId, ProductRequest productRequest) {

public void delete(Long productId) {
Product product = productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("유효하지 않은 id입니다"));
productRepository.deleteById(productId);
productRepository.delete(product);
productImageRepository.deleteAllByProductId(productId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.helpmeCookies.product.service;

import com.helpmeCookies.global.utils.AwsS3FileUtils;
import com.helpmeCookies.product.dto.FileUploadResponse;
import com.helpmeCookies.product.entity.Category;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.repository.ProductImageRepository;
import com.helpmeCookies.product.repository.ProductRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.when;

@SpringBootTest
@ActiveProfiles("test")
class ProductImageServiceTest {
@MockBean
private AwsS3FileUtils awsS3FileUtils;

private ProductImageService productImageService;
@MockBean
private ProductRepository productRepository;
@MockBean
private ProductImageRepository productImageRepository;

@BeforeEach
void setUp() {
productImageService = new ProductImageService(awsS3FileUtils, productImageRepository);
}

@AfterEach
void tearDown() {
}

@Test
@DisplayName("상품 이미지 S3 서버에 업로드")
void uploadMultiFiles() throws IOException {
given(productRepository.findById(any()))
.willReturn(Optional.of(new Product("더미",Category.CERAMIC,"100",10000L,"테스트항목","테스트 주소",
null,null)));
MockMultipartFile file1 = new MockMultipartFile("test1","img1.jpg","image/jpeg","image content".getBytes());
MockMultipartFile file2 = new MockMultipartFile("test2","img2.jpg","image/jpeg","image content".getBytes());
List<MultipartFile> files = Arrays.asList(file1,file2);

List<FileUploadResponse> expected = new ArrayList<>();
expected.add(new FileUploadResponse("url1","1111"));
expected.add(new FileUploadResponse("url2","2222"));
when(awsS3FileUtils.uploadMultiImages(files)).thenReturn(expected);

List<FileUploadResponse> actual = productImageService.uploadMultiFiles(1L,files);
assertEquals(2,actual.size(), "배열의 크기는 2여야함");
}
}
Loading