Skip to content

Commit

Permalink
Merge pull request #46 from jupyter471/weekly
Browse files Browse the repository at this point in the history
상품 이미지 등록 및 수정 기능 #30
  • Loading branch information
yooonwodyd authored Oct 15, 2024
2 parents c46d1b0 + 9ab22aa commit 725bd38
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 17 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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') {
useJUnitPlatform()
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

0 comments on commit 725bd38

Please sign in to comment.