Skip to content

Commit

Permalink
Merge pull request #48 from kakao-tech-campus-2nd-step3/develop
Browse files Browse the repository at this point in the history
Develop 정기 병합 1차
  • Loading branch information
yooonwodyd authored Oct 15, 2024
2 parents 56c3be3 + 39ace41 commit 2e367b2
Show file tree
Hide file tree
Showing 52 changed files with 1,042 additions and 185 deletions.
9 changes: 8 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// Spring docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

Expand All @@ -62,8 +67,10 @@ 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()
}
20 changes: 20 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/QueryDSLConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.helpmeCookies.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
private final EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.helpmeCookies.global.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import com.helpmeCookies.global.exception.user.ResourceNotFoundException;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public String handleResourceNotFoundException() {
return "Resource not found";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.helpmeCookies.global.exception.user;

public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException() {
super("Resource not found");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/login", "/signup", "/", "/user",
"/api/auth/**",
"/swagger-ui/**",
"/actuator/**"
"/actuator/**",
"/v1/**"
).permitAll()
.anyRequest().authenticated()
);
Expand Down
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);
}
}
Loading

0 comments on commit 2e367b2

Please sign in to comment.