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

사용자가 식품에 대한 리뷰를 작성한다. #74

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// aws s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/flab/nutridiary/commom/config/JdbcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing;

import java.util.Arrays;

@EnableJdbcAuditing
@Configuration
public class JdbcConfig extends AbstractJdbcConfiguration {

Expand Down
34 changes: 34 additions & 0 deletions src/main/java/flab/nutridiary/commom/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package flab.nutridiary.commom.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
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.s3.endpoint}")
private String endPoint;

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

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

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

@koo995 koo995 Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 @value의 값을 yml 파일에 설정한 값을 읽어오는데, 테스트 코드를 실행할 때 IntelliJ 에서 환경변수로 주입해 주지않아 S3Config 빈을 초기화하는데 에러가 발생합니다.

jenkins 서버에서 빌드할때도 같은 문제가 발생할 것 같은데 이러한 환경변수는 어떻게 관리하는게 좋을까요?
제일 간단하게 드는 방법은 젠킨스 서버에 접속해서 각각 환경변수를 설정해두고 AutoScaling 템플릿에도 설정하면 되는데, 이런 경우 새로운 환경변수가 필요할때마다 각 서버에 접속해서 변수를 설정해줘야 하는 문제가 있는것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

건홍님 accessKey와 secretKey는 사용하지 마세요. role 기반으로 설정하시고, 테스트 환경에서는 s3를 직접 다루기 보단 다른 테스트 구현체를 띄우거나 docker를 활용하는 방법이 있습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도커를 활용한 테스트 컨테이너로 적용했습니다!


@Bean
public AmazonS3 amazonS3() {
return AmazonS3Client.builder()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, regionName))
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum StatusConst {
DIARY_NOT_FOUND(4004, "해당 다이어리를 찾을 수 없습니다."),
DUPLICATED_DIARY(4005, "이미 등록된 다이어리입니다."),
VALIDATION_CHECK_FAIL(6001, "유효성 검사에 실패했습니다."),
NOT_ALLOWED_SERVING_UNIT(6002, "허용되지 않은 서빙 단위입니다.");
NOT_ALLOWED_SERVING_UNIT(6002, "허용되지 않은 서빙 단위입니다."),
DUPLICATED_PRODUCT_REVIEW(4006, "이미 등록된 리뷰입니다.");

private final int statusCode;
private final String message;
Expand Down
89 changes: 89 additions & 0 deletions src/main/java/flab/nutridiary/commom/file/FileStoreService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package flab.nutridiary.commom.file;


import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import flab.nutridiary.commom.exception.SystemException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.UUID;

@RequiredArgsConstructor
@Service
public class FileStoreService {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현체를 유연하게 확장할 수 있도록 인터페이스를 사용해보세요.
인터페이스로 추상화 시키면 테스트용 구현체도 쉽게 작성가능합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터페이스로 추상화할까 하다 코드에 Profile 설정이 많이 붙어있는게 보기 안좋아서 테스트할 때 이 부분을 Mocking 처리했습니다. 이런 방법도 괜찮나요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니요 지금 이런방식으로 작성하면 확장하기 어렵습니다.
해당 서비스를 쓰고있는 클라이언트가 나중에 다른 클라우드로 변경해야한다면 어떻게 될까요?

Copy link
Collaborator Author

@koo995 koo995 Oct 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 S3Config, FileStoreServiceImpl 클래스에 @Profile({“prod”, “dev”}) 로 하고 테스트는 mock객체로 처리했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mock을 사용할때 많은 단점들이 있는데요. 단점들을 한번 찾아보시죠.
mock을 사용하기 보단 Fake 객체를 구현해서 대체해보세요.
(테스트 코드에서 사용되는 mock,stub,fake,spy에 대해 찾아보고 정리해보세요)

private final AmazonS3 amazonS3;

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

public String uploadReviewImage(MultipartFile image) {
String directory = "review/";

String s3FileName = directory + generateS3FileName(image);
return uploadToS3(s3FileName, image);
}

public void deleteImageFromS3(String imageAddress){
String key = getKeyFromImageAddress(imageAddress);
try{
amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
}catch (Exception e){
throw new SystemException("이미지 삭제 중 오류가 발생했습니다.");
}
}

private String getExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
}

private String generateS3FileName(MultipartFile file) {
String extension = getExtension(file.getOriginalFilename());
return UUID.randomUUID().toString().substring(0, 10) + "_" + LocalDateTime.now() + "." + extension;
}

private ObjectMetadata getObjectMetadata(MultipartFile file, String extension) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("image/" + extension);
metadata.setContentLength(file.getSize());
return metadata;
}

private String getS3FileUrl(String s3FileName) {
return amazonS3.getUrl(bucketName, s3FileName).toString();
}

private String uploadToS3(String s3FileName, MultipartFile file) {
String extension = getExtension(file.getOriginalFilename());
ByteArrayInputStream inputStream = null;
try {
inputStream = new ByteArrayInputStream(file.getBytes());
} catch (IOException e) {
throw new SystemException("이미지처리 중 오류가 발생했습니다.");
}
ObjectMetadata metadata = getObjectMetadata(file, extension);
amazonS3.putObject(new PutObjectRequest(bucketName, s3FileName, inputStream, metadata));
return getS3FileUrl(s3FileName);
}

private String getKeyFromImageAddress(String imageAddress){
try{
URL url = new URL(imageAddress);
String decodingKey = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
return decodingKey.substring(1); // 맨 앞의 '/' 제거
}catch (MalformedURLException e){
throw new SystemException("이미지 주소가 올바르지 않습니다.");
}
}
}
29 changes: 29 additions & 0 deletions src/main/java/flab/nutridiary/dietTag/domain/DietTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package flab.nutridiary.dietTag.domain;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.relational.core.mapping.Column;

import java.time.LocalDateTime;

@NoArgsConstructor
@Getter
public class DietTag {
@Id @Column("diet_tag_id")
private Long id;

private String dietPlan;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

public DietTag(String dietPlan) {
this.dietPlan = dietPlan;
}
}
36 changes: 36 additions & 0 deletions src/main/java/flab/nutridiary/productDietTag/ProductDietTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package flab.nutridiary.productDietTag;

import flab.nutridiary.dietTag.domain.DietTag;
import flab.nutridiary.product.domain.Product;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column;

import java.time.LocalDateTime;

@ToString
@Getter
@NoArgsConstructor
public class ProductDietTag {
@Id @Column("product_diet_tag_id")
private Long id;

private AggregateReference<DietTag, Long> dietTagId;

private AggregateReference<Product, Long> productId;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

@Builder
public ProductDietTag(Long dietTagId, Long productId) {
this.dietTagId = AggregateReference.to(dietTagId);
this.productId = AggregateReference.to(productId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package flab.nutridiary.productDietTag;

import org.springframework.data.repository.CrudRepository;

public interface ProductDietTagRepository extends CrudRepository<ProductDietTag, Long> {
}
36 changes: 36 additions & 0 deletions src/main/java/flab/nutridiary/productStore/ProductStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package flab.nutridiary.productStore;

import flab.nutridiary.product.domain.Product;
import flab.nutridiary.store.domain.Store;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column;

import java.time.LocalDateTime;

@ToString
@Getter
@NoArgsConstructor
public class ProductStore {
@Id @Column("product_store_id")
private Long id;

private AggregateReference<Store, Long> storeId;

private AggregateReference<Product, Long> productId;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

@Builder
public ProductStore(Long storeId, Long productId) {
this.storeId = AggregateReference.to(storeId);
this.productId = AggregateReference.to(productId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package flab.nutridiary.productStore;

import org.springframework.data.repository.CrudRepository;

public interface ProductStoreRepository extends CrudRepository<ProductStore, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package flab.nutridiary.review.controller;

import flab.nutridiary.commom.dto.ApiResponse;
import flab.nutridiary.review.dto.request.CreateReviewRequest;
import flab.nutridiary.review.dto.response.CreateReviewResponse;
import flab.nutridiary.review.service.ReviewResisterService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class ReviewController {
private final ReviewResisterService reviewResisterService;

@PostMapping("review/new")
public ApiResponse<CreateReviewResponse> createReview(@ModelAttribute @Valid CreateReviewRequest createReviewRequest) {
Long memberId = 1L;
return ApiResponse.success(reviewResisterService.create(memberId, createReviewRequest));
}
}
46 changes: 46 additions & 0 deletions src/main/java/flab/nutridiary/review/domain/Review.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package flab.nutridiary.review.domain;

import flab.nutridiary.product.domain.Product;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column;

import java.time.LocalDateTime;

@NoArgsConstructor
@ToString
@Getter
public class Review {
@Id @Column("review_id")
private Long id;

private Long memberId = 1L;

private AggregateReference<Product, Long> productId;

private String content;

private Short rating;

private String imageUrl;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

@Builder
public Review(Long productId, String content, Short rating, String imageUrl) {
this.productId = AggregateReference.to(productId);
this.content = content;
this.rating = rating;
this.imageUrl = imageUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package flab.nutridiary.review.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;

@ToString
@Getter
public class CreateReviewRequest {
@NotNull(message = "상품 ID를 입력해주세요.")
private Long productId;

@NotNull(message = "리뷰 내용을 입력해주세요.")
private String content;

@NotNull(message = "식단 태그 ID를 입력해주세요.")
private Long dietTagId;

@NotNull(message = "매장 ID를 입력해주세요.")
private Long storeId;

private MultipartFile image;

@Max(value = 5, message = "평점은 5 이하이어야 합니다.")
@Min(value = 1, message = "평점은 1 이상이어야 합니다.")
@NotNull(message = "평점을 입력해주세요.")
private short rating;

public CreateReviewRequest(Long productId, String content, Long dietTagId, Long storeId, MultipartFile image, short rating) {
this.productId = productId;
this.content = content;
this.dietTagId = dietTagId;
this.storeId = storeId;
this.image = image;
this.rating = rating;
}
}
Loading
Loading