From 709315f33e65377563c3b488f41c7261f8bfc263 Mon Sep 17 00:00:00 2001 From: ibaesuyeon Date: Sun, 14 Apr 2024 05:03:05 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=EC=88=98=EC=A0=95,=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=80=EC=9E=A5,=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 게시글 생성, 수정 시 S3에 파일 저장, 수정 삭제 시 S3 및 데이터베이스에서 파일 삭제 --- build.gradle | 3 + .../post/controller/PostController.java | 22 +++++-- .../management/domain/post/domain/Post.java | 4 ++ .../domain/post/domain/PostFile.java | 29 ++++++++ .../infrastructure/PostFileRepository.java | 13 ++++ .../request/UpdatePostRequestServiceDto.java | 5 -- .../domain/post/service/PostServiceImpl.java | 65 ++++++++++++++---- .../management/global/config/S3Config.java | 36 ++++++++++ .../global/model/Exception/ExceptionList.java | 1 + .../management/global/service/S3Service.java | 66 +++++++++++++++++++ 10 files changed, 223 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/mju/management/domain/post/domain/PostFile.java create mode 100644 src/main/java/com/mju/management/domain/post/infrastructure/PostFileRepository.java create mode 100644 src/main/java/com/mju/management/global/config/S3Config.java create mode 100644 src/main/java/com/mju/management/global/service/S3Service.java diff --git a/build.gradle b/build.gradle index 3fe283f..19ef4db 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,9 @@ dependencies { //wire mock testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock") + + //aws s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } dependencyManagement { diff --git a/src/main/java/com/mju/management/domain/post/controller/PostController.java b/src/main/java/com/mju/management/domain/post/controller/PostController.java index 624644e..aa7542b 100644 --- a/src/main/java/com/mju/management/domain/post/controller/PostController.java +++ b/src/main/java/com/mju/management/domain/post/controller/PostController.java @@ -11,6 +11,11 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; @Tag(name = "[기획 /제작/ 편집] 게시글 작성, 수정, 삭제, 상세 조회 API") @RestController @@ -21,24 +26,31 @@ public class PostController { private final PostServiceImpl postServiceImpl; +// @ExceptionHandler(MaxUploadSizeExceededException.class) @Operation(summary = "기획/제작/편집 게시글 작성 API") @PostMapping - public CommonResult createPost(/* @AuthenticationPrincipal User user, */ @Valid @RequestBody CreatePostRequestDto createPostDto){ - return postServiceImpl.createPost(/* user, */ createPostDto.toServiceRequest()); + public CommonResult createPost(/* @AuthenticationPrincipal User user,*/ + @Valid @RequestPart CreatePostRequestDto createPostDto, + @RequestPart(value = "files", required = false) List files) throws IOException { + return postServiceImpl.createPost(/* user, */ createPostDto.toServiceRequest(), files); } @Operation(summary = "기획/제작/편집 게시글 상세 조회 API") @GetMapping - public CommonResult retrieveDetailPost(/* @AuthenticationPrincipal User user */ @Valid RetrieveDetailPostRequestDto retrieveDetailPostRequestDto ){ + public CommonResult retrieveDetailPost(/* @AuthenticationPrincipal User user */ + @Valid RetrieveDetailPostRequestDto retrieveDetailPostRequestDto ){ System.out.println("sdfslakfjklafsjlkjflad" + retrieveDetailPostRequestDto); return postServiceImpl.retrieveDetailPost(/* user, */ retrieveDetailPostRequestDto.toServiceRequest()); } +// @ExceptionHandler(MaxUploadSizeExceededException.class) @Operation(summary = "기획/제작/편집 게시글 수정 API") @PutMapping - public CommonResult updatePost(/* @AuthenticationPrincipal User user */ @Valid @RequestBody UpdatePostRequestDto updatePostRequestDto){ - return postServiceImpl.updatePost(/* user, */ updatePostRequestDto.toServiceRequest()); + public CommonResult updatePost(/* @AuthenticationPrincipal User user */ + @Valid @RequestPart UpdatePostRequestDto updatePostRequestDto, + @RequestPart(value = "files", required = false) List files) throws IOException { + return postServiceImpl.updatePost(/* user, */ updatePostRequestDto.toServiceRequest(), files); } @Operation(summary = "기획/제작/편집 게시글 삭제 API") diff --git a/src/main/java/com/mju/management/domain/post/domain/Post.java b/src/main/java/com/mju/management/domain/post/domain/Post.java index bef8993..1cc5a15 100644 --- a/src/main/java/com/mju/management/domain/post/domain/Post.java +++ b/src/main/java/com/mju/management/domain/post/domain/Post.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import com.mju.management.domain.comment.domain.Comment; @@ -60,6 +61,9 @@ public class Post { @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) private List commentList = new ArrayList<>(); +// @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) + private List postFiles; @Builder public Post(String title, String content, Category category, Long writerId) { diff --git a/src/main/java/com/mju/management/domain/post/domain/PostFile.java b/src/main/java/com/mju/management/domain/post/domain/PostFile.java new file mode 100644 index 0000000..324f70d --- /dev/null +++ b/src/main/java/com/mju/management/domain/post/domain/PostFile.java @@ -0,0 +1,29 @@ +package com.mju.management.domain.post.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class PostFile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String fileName; + + private String filePath; + + private String s3key; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} diff --git a/src/main/java/com/mju/management/domain/post/infrastructure/PostFileRepository.java b/src/main/java/com/mju/management/domain/post/infrastructure/PostFileRepository.java new file mode 100644 index 0000000..f55320b --- /dev/null +++ b/src/main/java/com/mju/management/domain/post/infrastructure/PostFileRepository.java @@ -0,0 +1,13 @@ +package com.mju.management.domain.post.infrastructure; + +import com.mju.management.domain.post.domain.PostFile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PostFileRepository extends JpaRepository { + + List findByPostId(Long postId); +} diff --git a/src/main/java/com/mju/management/domain/post/model/dto/request/UpdatePostRequestServiceDto.java b/src/main/java/com/mju/management/domain/post/model/dto/request/UpdatePostRequestServiceDto.java index e411975..57f6eb2 100644 --- a/src/main/java/com/mju/management/domain/post/model/dto/request/UpdatePostRequestServiceDto.java +++ b/src/main/java/com/mju/management/domain/post/model/dto/request/UpdatePostRequestServiceDto.java @@ -2,11 +2,6 @@ import com.mju.management.domain.post.infrastructure.Category; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; - public record UpdatePostRequestServiceDto( Long projectId, Long postId, diff --git a/src/main/java/com/mju/management/domain/post/service/PostServiceImpl.java b/src/main/java/com/mju/management/domain/post/service/PostServiceImpl.java index 2716dbd..779aa84 100644 --- a/src/main/java/com/mju/management/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/mju/management/domain/post/service/PostServiceImpl.java @@ -1,12 +1,10 @@ package com.mju.management.domain.post.service; -import static com.mju.management.global.model.Exception.ExceptionList.*; - -import java.util.Optional; - import com.mju.management.domain.comment.service.port.CommentRepository; import com.mju.management.domain.post.controller.response.PostDetailResponse; import com.mju.management.domain.post.domain.Post; +import com.mju.management.domain.post.domain.PostFile; +import com.mju.management.domain.post.infrastructure.PostFileRepository; import com.mju.management.domain.post.infrastructure.PostRepository; import com.mju.management.domain.post.model.dto.request.CreatePostRequestServiceDto; import com.mju.management.domain.post.model.dto.request.DeletePostRequestServiceDto; @@ -14,17 +12,24 @@ import com.mju.management.domain.post.model.dto.request.UpdatePostRequestServiceDto; import com.mju.management.domain.project.infrastructure.Project; import com.mju.management.domain.project.infrastructure.ProjectRepository; -import com.mju.management.domain.user.dto.GetUserResponseDto; import com.mju.management.domain.user.service.UserServiceImpl; import com.mju.management.global.config.jwtInterceptor.JwtContextHolder; import com.mju.management.global.model.Exception.ExceptionList; import com.mju.management.global.model.Exception.UnauthorizedAccessException; import com.mju.management.global.model.Result.CommonResult; import com.mju.management.global.service.ResponseService; - +import com.mju.management.global.service.S3Service; 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.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.mju.management.global.model.Exception.ExceptionList.*; @Service @Transactional @@ -36,8 +41,10 @@ public class PostServiceImpl { private final UserServiceImpl userService; private final ResponseService responseService; private final CommentRepository commentRepository; + private final PostFileRepository postFileRepository; + private final S3Service s3Service; - public CommonResult createPost(CreatePostRequestServiceDto dto) { + public CommonResult createPost(CreatePostRequestServiceDto dto, List files) throws IOException { Optional optionalProject = projectRepository.findById(dto.projectId()); if (optionalProject.isEmpty()){ return responseService.getFailResult(INVALID_PROJECT_ID.getCode(), INVALID_PROJECT_ID.getMessage()); @@ -51,6 +58,21 @@ public CommonResult createPost(CreatePostRequestServiceDto dto) { Post post = dto.toEntity(userId); project.createPost(post); postRepository.save(post); + // 파일 업로드 + List postFiles = new ArrayList<>(); + + for (MultipartFile file : files) { + String s3key = s3Service.uploadFile(file); + + postFiles.add(PostFile.builder() + .fileName(file.getOriginalFilename()) + .filePath(s3Service.getUrl(s3key)) + .s3key(s3key) + .post(post) + .build()); + } + postFileRepository.saveAll(postFiles); + return responseService.getSuccessfulResultWithMessage("기획/제작/편집 게시글 작성에 성공하였습니다."); } @@ -74,7 +96,7 @@ public CommonResult retrieveDetailPost(RetrieveDetailPostRequestServiceDto dto) return responseService.getSingleResult(PostDetailResponse.from(post, userService.getUsername(post.getWriterId()))); } - public CommonResult updatePost(UpdatePostRequestServiceDto dto) { + public CommonResult updatePost(UpdatePostRequestServiceDto dto, List newFiles ) throws IOException{ Optional optionalProject = projectRepository.findById(dto.projectId()); if (optionalProject.isEmpty()){ return responseService.getFailResult(INVALID_PROJECT_ID.getCode(), INVALID_PROJECT_ID.getMessage()); @@ -96,6 +118,24 @@ public CommonResult updatePost(UpdatePostRequestServiceDto dto) { } post.update(dto); + // 파일 삭제 + List oldFiles = postFileRepository.findByPostId(post.getId()); + for(PostFile file : oldFiles) { + s3Service.deleteFile(file.getS3key()); //s3 삭제 + postFileRepository.deleteById(file.getId()); //엔티티 삭제 + } + // 파일 다시 새로 업로드 + List postFiles = new ArrayList<>(); + for (MultipartFile file : newFiles) { + String s3key = s3Service.uploadFile(file); + postFiles.add(PostFile.builder() + .fileName(file.getOriginalFilename()) + .filePath(s3Service.getUrl(s3key)) + .s3key(s3key) + .post(post) + .build()); + } + postFileRepository.saveAll(postFiles); return responseService.getSuccessfulResultWithMessage("기획/제작/편집 게시글 수정에 성공하였습니다."); } @@ -123,7 +163,12 @@ public CommonResult deletePost(DeletePostRequestServiceDto dto) { // 댓글들 삭제 commentRepository.deleteAll(post); - + // 파일 삭제 + List oldFiles = postFileRepository.findByPostId(post.getId()); + for(PostFile file : oldFiles) { + s3Service.deleteFile(file.getS3key()); //s3 삭제 + postFileRepository.deleteById(file.getId()); //엔티티 삭제 + } postRepository.delete(post); return responseService.getSuccessfulResultWithMessage("기획/제작/편집 게시글 삭제에 성공하였습니다."); } @@ -134,6 +179,4 @@ private void checkMemberAuthorization(Project project, Long userId){ throw new UnauthorizedAccessException(ExceptionList.UNAUTHORIZED_ACCESS); } - - } diff --git a/src/main/java/com/mju/management/global/config/S3Config.java b/src/main/java/com/mju/management/global/config/S3Config.java new file mode 100644 index 0000000..1fa0507 --- /dev/null +++ b/src/main/java/com/mju/management/global/config/S3Config.java @@ -0,0 +1,36 @@ +package com.mju.management.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 amazonS3Client() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } + + +} diff --git a/src/main/java/com/mju/management/global/model/Exception/ExceptionList.java b/src/main/java/com/mju/management/global/model/Exception/ExceptionList.java index 7a36f94..3c070af 100644 --- a/src/main/java/com/mju/management/global/model/Exception/ExceptionList.java +++ b/src/main/java/com/mju/management/global/model/Exception/ExceptionList.java @@ -9,6 +9,7 @@ public enum ExceptionList { UNKNOWN(-9999, "알 수 없는 오류가 발생하였습니다."), + INVALID_PARAMETER(-5000, "인자가 잘못 전달되었거나 없습니다."), EMPTY_USER(-5051, "유저 정보를 입력해 주세요."), NOT_CORRECT_USER(-5052, "수강생이 아닙니다. 수강생으로 로그인 다시 부탁드립니다."), NOT_ACCESS_USER(-5053, "접근할 수 없는 유저 입니다."), diff --git a/src/main/java/com/mju/management/global/service/S3Service.java b/src/main/java/com/mju/management/global/service/S3Service.java new file mode 100644 index 0000000..ce15311 --- /dev/null +++ b/src/main/java/com/mju/management/global/service/S3Service.java @@ -0,0 +1,66 @@ +package com.mju.management.global.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.mju.management.global.model.Exception.ExceptionList; +import com.mju.management.global.model.Exception.NonExistentException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class S3Service { + @Value("${cloud.aws.s3.bucket}") + private String BUCKET; + + private final AmazonS3 amazonS3; + + @Transactional + public String uploadFile(MultipartFile file) throws IOException { + if(file == null){ + throw new NonExistentException(ExceptionList.INVALID_PARAMETER); + } + String fileName = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf(".")) + + "-" + convertToRandomName(file.getOriginalFilename()); + + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(file.getSize()); + objectMetadata.setContentType(file.getContentType()); + InputStream inputStream = file.getInputStream(); + + String filePath = BUCKET; + amazonS3.putObject(new PutObjectRequest(filePath, fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return fileName; + } + @Transactional + public void deleteFile(String s3key) { + if(s3key == null){ + throw new NonExistentException(ExceptionList.INVALID_PARAMETER); + } + amazonS3.deleteObject(new DeleteObjectRequest(BUCKET, s3key)); + } + + public String convertToRandomName(String originalFileName) { + String fileExtension = originalFileName.substring(originalFileName.lastIndexOf(".")); + return UUID.randomUUID().toString().concat(fileExtension); + } + + public String getUrl(String s3key) { + if(s3key == null){ + throw new NonExistentException(ExceptionList.INVALID_PARAMETER); + } + return amazonS3.getUrl(BUCKET, s3key).toString(); + } +}