diff --git a/build.gradle b/build.gradle index 78013cd..46d925f 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + //업로드 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { diff --git a/src/main/java/com/example/aws/S3Manager.java b/src/main/java/com/example/aws/S3Manager.java new file mode 100644 index 0000000..3912ee2 --- /dev/null +++ b/src/main/java/com/example/aws/S3Manager.java @@ -0,0 +1,39 @@ +package com.example.aws; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.example.config.AmazonConfig; +import com.example.domain.Uuid; +import com.example.repository.UuidRepository; +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 S3Manager { + private final AmazonS3 amazonS3; + private final AmazonConfig awsConfig; + private final UuidRepository uuidRepository; + + public String uploadFile(String keyName, MultipartFile file){ + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + try { + amazonS3.putObject(new PutObjectRequest(awsConfig.getBucket(), keyName, file.getInputStream(), metadata)); + } catch (IOException e){ + log.error("error at AmazonS3Manager uploadFile : {}", (Object) e.getStackTrace()); + } + + return amazonS3.getUrl(awsConfig.getBucket(), keyName).toString();} + + public String generateReviewKeyName(Uuid uuid) { + + return awsConfig.getReviewPath() + '/' + uuid.getUuid(); + } +} diff --git a/src/main/java/com/example/config/AmazonConfig.java b/src/main/java/com/example/config/AmazonConfig.java new file mode 100644 index 0000000..0d912b5 --- /dev/null +++ b/src/main/java/com/example/config/AmazonConfig.java @@ -0,0 +1,54 @@ +package com.example.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.AmazonS3ClientBuilder; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; + +@Configurable +@Getter +public class AmazonConfig { + + private AWSCredentials awsCredentials; + + @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; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.path.review}") + private String reviewPath; + + @PostConstruct + public void init() { + this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + } + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + @Bean + public AWSCredentialsProvider awsCredentialsProvider() { + return new AWSStaticCredentialsProvider(awsCredentials); + } +} diff --git a/src/main/java/com/example/domain/Uuid.java b/src/main/java/com/example/domain/Uuid.java new file mode 100644 index 0000000..6ad016f --- /dev/null +++ b/src/main/java/com/example/domain/Uuid.java @@ -0,0 +1,19 @@ +package com.example.domain; + +import com.example.domain.base.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Uuid extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String uuid; +} diff --git a/src/main/java/com/example/repository/ReviewImageRepository.java b/src/main/java/com/example/repository/ReviewImageRepository.java new file mode 100644 index 0000000..b728c3d --- /dev/null +++ b/src/main/java/com/example/repository/ReviewImageRepository.java @@ -0,0 +1,7 @@ +package com.example.repository; + +import com.example.domain.mapping.ReviewImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/repository/UuidRepository.java b/src/main/java/com/example/repository/UuidRepository.java new file mode 100644 index 0000000..1376b48 --- /dev/null +++ b/src/main/java/com/example/repository/UuidRepository.java @@ -0,0 +1,7 @@ +package com.example.repository; + +import com.example.domain.Uuid; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UuidRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/service/review/ReviewCommandService.java b/src/main/java/com/example/service/review/ReviewCommandService.java index 2a0a5d3..016acd5 100644 --- a/src/main/java/com/example/service/review/ReviewCommandService.java +++ b/src/main/java/com/example/service/review/ReviewCommandService.java @@ -2,9 +2,10 @@ import com.example.domain.Review; import com.example.web.dto.ReviewRequest; +import org.springframework.web.multipart.MultipartFile; public interface ReviewCommandService { - Review join(ReviewRequest.JoinDto dto); + Review join(ReviewRequest.JoinDto dto, Long memberId, Long storeId, MultipartFile reviewImage); // join이 리뷰 생성을 수행한 후 저장된 Review 엔티티를 반환 // DB에서 review 객체를 생성하고 반환하는 역할 diff --git a/src/main/java/com/example/service/review/ReviewCommandServiceImpl.java b/src/main/java/com/example/service/review/ReviewCommandServiceImpl.java index 13f47d3..d299495 100644 --- a/src/main/java/com/example/service/review/ReviewCommandServiceImpl.java +++ b/src/main/java/com/example/service/review/ReviewCommandServiceImpl.java @@ -1,19 +1,25 @@ package com.example.service.review; +import com.example.aws.S3Manager; import com.example.converter.ReviewConverter; import com.example.domain.Member; import com.example.domain.Review; import com.example.domain.Store; +import com.example.domain.Uuid; import com.example.exception.handler.MemberHandler; import com.example.exception.handler.StoreHandler; import com.example.payload.status.ErrorStatus; import com.example.repository.MemberRepository; import com.example.repository.ReviewRepository; import com.example.repository.StoreRepository; +import com.example.repository.UuidRepository; import com.example.web.dto.ReviewRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; @Service @Transactional @@ -22,6 +28,8 @@ public class ReviewCommandServiceImpl implements ReviewCommandService{ private final ReviewRepository reviewRepository; private final MemberRepository memberRepository; private final StoreRepository storeRepository; + private final UuidRepository uuidRepository; + private final S3Manager s3Manager; public boolean memberExistsById(Long memberId) { @@ -33,7 +41,7 @@ public boolean storeExistsById(Long storeId) { } @Override - public Review join(ReviewRequest.JoinDto dto){ //생성 + public Review join(ReviewRequest.JoinDto dto,Long memberId, Long storeId, MultipartFile reviewImage){ //생성 //회원 있는지 확인 Member member = memberRepository.findById(dto.getMemberId()).get(); @@ -42,6 +50,11 @@ public Review join(ReviewRequest.JoinDto dto){ //생성 Store store = storeRepository.findById(dto.getStoreId()).get(); Review newReview = ReviewConverter.toReview(dto,member,store); + String uuid = UUID.randomUUID().toString(); + Uuid savedUuid = uuidRepository.save(Uuid.builder() + .uuid(uuid) + .build()); + String imageUrl = s3Manager.uploadFile(s3Manager.generateReviewKeyName(savedUuid), reviewImage); return reviewRepository.save(newReview); } } diff --git a/src/main/java/com/example/service/store/StoreCommandService.java b/src/main/java/com/example/service/store/StoreCommandService.java new file mode 100644 index 0000000..3807ef6 --- /dev/null +++ b/src/main/java/com/example/service/store/StoreCommandService.java @@ -0,0 +1,8 @@ +package com.example.service.store; + +import com.example.domain.Review; +import com.example.web.dto.ReviewResponse; +import org.springframework.web.multipart.MultipartFile; + +public interface StoreCommandService { +} diff --git a/src/main/java/com/example/service/store/StoreCommandServiceImpl.java b/src/main/java/com/example/service/store/StoreCommandServiceImpl.java new file mode 100644 index 0000000..49cde95 --- /dev/null +++ b/src/main/java/com/example/service/store/StoreCommandServiceImpl.java @@ -0,0 +1,23 @@ +package com.example.service.store; + +import com.example.aws.S3Manager; +import com.example.converter.ReviewConverter; +import com.example.domain.Review; +import com.example.domain.Uuid; +import com.example.repository.ReviewImageRepository; +import com.example.repository.ReviewRepository; +import com.example.repository.StoreRepository; +import com.example.repository.UuidRepository; +import com.example.service.member.MemberQueryService; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional +public class StoreCommandServiceImpl implements StoreCommandService { +} diff --git a/src/main/java/com/example/web/controller/ReviewController.java b/src/main/java/com/example/web/controller/ReviewController.java index 9cae691..1c03673 100644 --- a/src/main/java/com/example/web/controller/ReviewController.java +++ b/src/main/java/com/example/web/controller/ReviewController.java @@ -16,14 +16,9 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/reviews") +@RequestMapping() public class ReviewController { - private final ReviewCommandService reviewCommandService; - @PostMapping("/") - @Operation(summary = "가게 리뷰 추가 API") - public CommonResponse join(@RequestBody @Valid ReviewRequest.JoinDto dto){ - Review review = reviewCommandService.join(dto); - return CommonResponse.onSuccess(ReviewConverter.toJoinResultDto(review)); - } + + } diff --git a/src/main/java/com/example/web/controller/StoreController.java b/src/main/java/com/example/web/controller/StoreController.java index d6eecf0..bc2ed16 100644 --- a/src/main/java/com/example/web/controller/StoreController.java +++ b/src/main/java/com/example/web/controller/StoreController.java @@ -5,10 +5,13 @@ import com.example.domain.Mission; import com.example.domain.Review; import com.example.payload.CommonResponse; +import com.example.service.review.ReviewCommandService; import com.example.service.store.StoreQueryService; import com.example.validation.annotaion.CheckPage; +import com.example.validation.annotaion.ExistsMember; import com.example.validation.annotaion.ExistsStore; import com.example.web.dto.MissionResponse; +import com.example.web.dto.ReviewRequest; import com.example.web.dto.ReviewResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -17,10 +20,12 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @Validated @RestController @@ -29,6 +34,29 @@ public class StoreController { private final StoreQueryService storeQueryService; + private final ReviewCommandService reviewCommandService; + + @PostMapping("/{storeId}/{memberId}/reviews") + @Operation(summary = "가게 리뷰 추가 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON_200", description = "성공"), + @ApiResponse(responseCode = "STORE_4001", description = "가게를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "Member_4001", description = "사용자를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @Parameters(value = { + @Parameter(name = "storeId", description = "가게 ID(path variable)", required = true), + @Parameter(name = "memberId", description = "사용자 ID(path variable)", required = true), + }) + public CommonResponse join( + @ExistsStore @PathVariable(name = "storeId") Long storeId, + @ExistsMember @PathVariable(name = "memberId") Long memberId, + @RequestPart MultipartFile reviewImage, + @RequestBody @Valid ReviewRequest.JoinDto dto){ + Review review = reviewCommandService.join(dto, storeId, memberId, reviewImage); + return CommonResponse.onSuccess(ReviewConverter.toJoinResultDto(review)); + } @GetMapping("/{storeId}/reviews") @Operation(summary = "특정 가게의 리뷰 목록 조회 API", @@ -49,6 +77,7 @@ public CommonResponse getStoreReviews( return CommonResponse.onSuccess(ReviewConverter.toReviewListDto(reviews)); } + @GetMapping("/{storeId}/missions") @Operation(summary = "특정 가게의 미션 목록 조회 API") @Parameters(value = { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cf1d652..0bf4618 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,4 @@ spring: - application: - name : beanstalk datasource: url: ${DATABASE_URL} @@ -27,6 +25,20 @@ spring: max-file-size: 100MB max-request-size: 300MB +cloud: + aws: + s3: + bucket: web101-bucket + path: + review: reviews + region: + static: ap-northeast-2 + stack: + auto: false + credentials: + accessKey: ${S3_SECRET_KEY} + secretKey: ${S3_ACCESS_KEY} + springdoc: swagger-ui: path: /swagger