Skip to content
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
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ dependencies {
//springdoc-openapi
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' // 최신 버전 확인


// - AWS 서비스(Spring Cloud와 S3, SES 등)와의 통합을 쉽게 해주며, Spring Boot에서 여러 AWS 관련 자동 설정을 지원
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
// S3 설정
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.649'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package com.DecodEat.domain.products.controller;

import com.DecodEat.domain.products.dto.response.ProductDetailDto;
import com.DecodEat.domain.products.dto.request.ProductRegisterRequestDto;
import com.DecodEat.domain.products.dto.response.ProductRegisterResponseDto;
import com.DecodEat.domain.products.service.ProductService;
import com.DecodEat.domain.users.entity.User;
import com.DecodEat.global.apiPayload.ApiResponse;
import com.DecodEat.global.common.annotation.CurrentUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequiredArgsConstructor
Expand All @@ -23,4 +32,25 @@ public class ProductController {
public ApiResponse<ProductDetailDto> getProduct(@PathVariable Long id) {
return ApiResponse.onSuccess(productService.getDetail(id));
}

@Operation(
summary = "제품 등록",
description = "상품 이미지, 제품명, 회사명으로 상품을 등록합니다")
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) //이 엔드포인트가 multipart/form-data 타입의 요청 본문을 소비(consume)한다는 것을 명확하게 선언
public ApiResponse<ProductRegisterResponseDto> registerProduct(
@CurrentUser User user,
@RequestParam("name") String name,
@RequestParam("manufacturer") String manufacturer,
@RequestPart("productImage") MultipartFile productImage,
@RequestPart("productInfoImages") List<MultipartFile> productInfoImages
) {
ProductRegisterRequestDto requestDto = ProductRegisterRequestDto.builder()
.name(name)
.manufacturer(manufacturer)
.build();

return ApiResponse.onSuccess(productService.addProduct(user, requestDto, productImage, productInfoImages));
}


}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.DecodEat.domain.products.converter;

import com.DecodEat.domain.products.dto.response.ProductDetailDto;
import com.DecodEat.domain.products.dto.response.ProductRegisterResponseDto;
import com.DecodEat.domain.products.entity.Product;
import com.DecodEat.domain.products.entity.ProductNutrition;
import com.DecodEat.domain.products.entity.RawMaterial.RawMaterialCategory;
Expand All @@ -13,7 +14,7 @@

public class ProductConverter {
public static ProductDetailDto toProductDetailDto(Product product,
List<String> imageUrls ,
List<String> productInfoImageUrls ,
ProductNutrition productNutrition) {
Map<RawMaterialCategory, List<String>> nutrientsMap =
product.getIngredients().stream()
Expand Down Expand Up @@ -43,7 +44,7 @@ public static ProductDetailDto toProductDetailDto(Product product,
.sodium(productNutrition.getSodium())
.sugar(productNutrition.getSugar())
.transFat(productNutrition.getTransFat())
.imageUrl(imageUrls)
.imageUrl(productInfoImageUrls)
.animalProteins(nutrientsMap.get(ANIMAL_PROTEIN))
.plantProteins(nutrientsMap.get(PLANT_PROTEIN))
.complexCarbs(nutrientsMap.get(COMPLEX_CARBOHYDRATE))
Expand All @@ -53,4 +54,13 @@ public static ProductDetailDto toProductDetailDto(Product product,
.allergens(nutrientsMap.get(ALLERGENS))
.build();
}

public static ProductRegisterResponseDto toProductRegisterDto(Product product, List<String> productInfoImageUrls){
return ProductRegisterResponseDto.builder()
.name(product.getProductName())
.manufacturer(product.getManufacturer())
.productImage(product.getProductImage())
.productInfoImages(productInfoImageUrls)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.DecodEat.domain.products.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductRegisterRequestDto {
@NotBlank(message = "제품명은 필수 입력 항목입니다.")
private String name;

@NotBlank(message = "제조사명은 필수 입력 항목입니다.")
private String manufacturer;

// 파일은 json 과 하나의 dto로 묶어서 다룰 수 없음
// private MultipartFile productImage;
// @NotNull
// private List<MultipartFile> productInfoImages;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.DecodEat.domain.products.dto.response;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductRegisterResponseDto {
@NotNull
private String name;
@NotNull
private String manufacturer;

private String productImage;
@NotNull
private List<String> productInfoImages;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
package com.DecodEat.domain.products.service;

import com.DecodEat.domain.products.converter.ProductConverter;
import com.DecodEat.domain.products.dto.request.ProductRegisterRequestDto;
import com.DecodEat.domain.products.dto.response.ProductDetailDto;
import com.DecodEat.domain.products.dto.response.ProductRegisterResponseDto;
import com.DecodEat.domain.products.entity.DecodeStatus;
import com.DecodEat.domain.products.entity.Product;
import com.DecodEat.domain.products.entity.ProductInfoImage;
import com.DecodEat.domain.products.entity.ProductNutrition;
import com.DecodEat.domain.products.repository.ProductImageRepository;
import com.DecodEat.domain.products.repository.ProductNutritionRepository;
import com.DecodEat.domain.products.repository.ProductRepository;
import static com.DecodEat.global.apiPayload.code.status.ErrorStatus.*;
import com.DecodEat.domain.users.entity.User;
import com.DecodEat.global.aws.s3.AmazonS3Manager;
import com.DecodEat.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.DecodEat.global.apiPayload.code.status.ErrorStatus.PRODUCT_NOT_EXISTED;

@Service
@RequiredArgsConstructor
Expand All @@ -23,6 +32,7 @@ public class ProductService {
private final ProductRepository productRepository;
private final ProductImageRepository productImageRepository;
private final ProductNutritionRepository productNutritionRepository;
private final AmazonS3Manager amazonS3Manager;

public ProductDetailDto getDetail(Long id) {
Product product = productRepository.findById(id).orElseThrow(() -> new GeneralException(PRODUCT_NOT_EXISTED));
Expand All @@ -34,4 +44,43 @@ public ProductDetailDto getDetail(Long id) {

return ProductConverter.toProductDetailDto(product, imageUrls, productNutrition);
}
}

public ProductRegisterResponseDto addProduct(User user, ProductRegisterRequestDto requestDto, MultipartFile productImage, List<MultipartFile> productInfoImages) {
String productName = requestDto.getName();
String manufacturer = requestDto.getManufacturer();

String productImageUrl = null;
if (productImage != null && !productImage.isEmpty()) {
String productImageKey = "products/" + UUID.randomUUID() + "_" + productImage.getOriginalFilename();
productImageUrl = amazonS3Manager.uploadFile(productImageKey, productImage);
}


Product newProduct = Product.builder()
.user(user)
.productName(productName)
.manufacturer(manufacturer)
.productImage(productImageUrl)
.decodeStatus(DecodeStatus.PROCESSING)
.build();

Product savedProduct = productRepository.save(newProduct);

List<String> productInfoImageUrls = null;
if (productInfoImages != null && !productInfoImages.isEmpty()) {
List<ProductInfoImage> infoImages = productInfoImages.stream().map(image -> {
String imageKey = "products/info/" + UUID.randomUUID() + "_" + image.getOriginalFilename();
String imageUrl = amazonS3Manager.uploadFile(imageKey, image);
return ProductInfoImage.builder()
.product(savedProduct)
.imageUrl(imageUrl)
.build();
}).collect(Collectors.toList());
productImageRepository.saveAll(infoImages);

productInfoImageUrls = infoImages.stream().map(ProductInfoImage::getImageUrl).toList();
}

return ProductConverter.toProductRegisterDto(savedProduct,productInfoImageUrls) ;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.DecodEat.domain.RefreshToken.controller;
package com.DecodEat.domain.refreshToken.controller;

import com.DecodEat.domain.RefreshToken.dto.request.CreateAccessTokenRequest;
import com.DecodEat.domain.RefreshToken.dto.response.CreateAccessTokenResponse;
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/DecodEat/global/aws/s3/AmazonS3Manager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.DecodEat.global.aws.s3;

import com.DecodEat.global.config.AmazonConfig;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Slf4j
@Component
@RequiredArgsConstructor
public class AmazonS3Manager{

private final AmazonS3 amazonS3;

private final AmazonConfig amazonConfig;

public String uploadFile(String keyName, MultipartFile file){
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
try {
amazonS3.putObject(new PutObjectRequest(amazonConfig.getBucket(), keyName, file.getInputStream(), metadata));
} catch (IOException e){
log.error("error at AmazonS3Manager uploadFile : {}", (Object) e.getStackTrace());
}

return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString();
}
}
59 changes: 59 additions & 0 deletions src/main/java/com/DecodEat/global/config/AmazonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.DecodEat.global.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
@Getter
public class AmazonConfig {

private AWSCredentials awsCredentials;

@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;

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

@Value("${cloud.aws.region.static}")
private String region;

// 추가: 버킷 이름 프로퍼티 주입
@Value("${cloud.aws.s3.bucket}")
private String bucket;

private String productImagePath;

private String productInfoImagePath;

@PostConstruct
public void init() {
this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
}

@Bean
public AmazonS3 amazonS3() {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}

@Bean
public AWSCredentialsProvider awsCredentialsProvider() {
return new AWSStaticCredentialsProvider(awsCredentials);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfigurationSource;

@RequiredArgsConstructor
@Configuration
Expand All @@ -29,13 +30,14 @@ public class WebOAuthSecurityConfig {
private final JwtTokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final UserService userService;
private final CorsConfigurationSource corsConfigurationSource; // CorsCongifuragtinoSource Bean 주입 위함

@Bean
public WebSecurityCustomizer configure() {
// H2 콘솔 및 정적 리소스에 대한 시큐리티 기능 비활성화
return (web) -> web.ignoring()
.requestMatchers("/img/**", "/css/**", "/js/**", "/favicon.ico", "/error");
}
// @Bean
// public WebSecurityCustomizer configure() {
// // H2 콘솔 및 정적 리소스에 대한 시큐리티 기능 비활성화
// return (web) -> web.ignoring()
// .requestMatchers("/img/**", "/css/**", "/js/**", "/favicon.ico", "/error");
// }

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand All @@ -45,11 +47,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

// 2. 불필요한 기능 비활성화
http.csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 (토큰 방식이므로)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.httpBasic(basic -> basic.disable()) // HTTP Basic 인증 비활성화
.formLogin(form -> form.disable()); // 폼 기반 로그인 비활성화

// 3. 요청별 인가 규칙 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/img/**", "/css/**", "/js/**", "/favicon.ico", "/error").permitAll()
.requestMatchers("/swagger-ui/**","/v3/api-docs/**").permitAll() // 토큰 재발급 요청은 누구나 가능
.requestMatchers("/api/token").permitAll()
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN") // 유저 관련 API는 USER 또는 ADMIN 권한 필요
Expand Down
Loading