diff --git a/build.gradle b/build.gradle index f06ea67..0135e27 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,8 @@ dependencies { annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" - + // amazon s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } clean { delete file('src/main/generated')} diff --git a/src/main/java/com/ripple/BE/global/config/S3Config.java b/src/main/java/com/ripple/BE/global/config/S3Config.java new file mode 100644 index 0000000..d515653 --- /dev/null +++ b/src/main/java/com/ripple/BE/global/config/S3Config.java @@ -0,0 +1,34 @@ +package com.ripple.BE.global.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@NoArgsConstructor +public class S3Config { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) + AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java index 0985827..7e050bd 100644 --- a/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/ripple/BE/global/exception/handler/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.ripple.BE.global.exception.response.ErrorResponse; import com.ripple.BE.global.exception.response.ErrorResponse.ValidationError; import com.ripple.BE.global.exception.response.ErrorResponse.ValidationErrors; +import com.ripple.BE.image.exception.ImageException; import com.ripple.BE.learning.exception.LearningException; import com.ripple.BE.learning.exception.QuizException; import com.ripple.BE.post.exception.PostException; @@ -86,6 +87,11 @@ public ResponseEntity handlePostException(final PostException e) { return handleExceptionInternal(e.getErrorCode()); } + @ExceptionHandler(ImageException.class) + public ResponseEntity handleImageException(final ImageException e) { + return handleExceptionInternal(e.getErrorCode()); + } + /** * 예외 처리 결과를 생성하는 내부 메서드 * diff --git a/src/main/java/com/ripple/BE/image/controller/ImageController.java b/src/main/java/com/ripple/BE/image/controller/ImageController.java new file mode 100644 index 0000000..30cd2e1 --- /dev/null +++ b/src/main/java/com/ripple/BE/image/controller/ImageController.java @@ -0,0 +1,45 @@ +package com.ripple.BE.image.controller; + +import com.ripple.BE.global.dto.response.ApiResponse; +import com.ripple.BE.image.dto.response.ImageIdResponse; +import com.ripple.BE.image.service.ImageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/image") +@Tag(name = "Image", description = "이미지 API") +public class ImageController { + + private final ImageService imageService; + + @Operation(summary = "게시물 단일 사진 추가", description = "게시물을 등록하기 전 단일 사진을 추가합니다.") + @PostMapping(consumes = "multipart/form-data") + public ResponseEntity> createPost(final @RequestParam MultipartFile file) { + + long imageId = imageService.addImageToPost(file); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.from(ImageIdResponse.toImageIdResponse(imageId))); + } + + @Operation(summary = "게시물 사진 삭제", description = "게시물에 등록된 사진을 삭제합니다.") + @DeleteMapping("/{imageId}") + public ResponseEntity> deleteImage( + final @PathVariable("imageId") long imageId) { + + imageService.deleteImage(imageId); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); + } +} diff --git a/src/main/java/com/ripple/BE/image/domain/Image.java b/src/main/java/com/ripple/BE/image/domain/Image.java index eb6ef10..5b62cbd 100644 --- a/src/main/java/com/ripple/BE/image/domain/Image.java +++ b/src/main/java/com/ripple/BE/image/domain/Image.java @@ -17,6 +17,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Table(name = "images") @Getter @@ -31,8 +32,10 @@ public class Image extends BaseEntity { @Column(name = "id") private Long id; - private String imageUrl; + @Column(name = "s3_info") + private S3Info s3Info; + @Setter @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; @@ -40,4 +43,8 @@ public class Image extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "news_id") private News news; + + public static Image toImageEntity(final S3Info s3Info) { + return Image.builder().s3Info(s3Info).build(); + } } diff --git a/src/main/java/com/ripple/BE/image/domain/S3Info.java b/src/main/java/com/ripple/BE/image/domain/S3Info.java new file mode 100644 index 0000000..1ce784b --- /dev/null +++ b/src/main/java/com/ripple/BE/image/domain/S3Info.java @@ -0,0 +1,19 @@ +package com.ripple.BE.image.domain; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class S3Info { + private String folderName; + private String fileName; + private String url; +} diff --git a/src/main/java/com/ripple/BE/image/dto/ImageDTO.java b/src/main/java/com/ripple/BE/image/dto/ImageDTO.java index a3a97e7..c1ead2b 100644 --- a/src/main/java/com/ripple/BE/image/dto/ImageDTO.java +++ b/src/main/java/com/ripple/BE/image/dto/ImageDTO.java @@ -2,13 +2,13 @@ import com.ripple.BE.image.domain.Image; -public record ImageDTO(String url) { +public record ImageDTO(Long id, String url) { public static ImageDTO toImageDTO(final Image image) { - return new ImageDTO(image.getImageUrl()); + return new ImageDTO(image.getId(), image.getS3Info().getUrl()); } public static ImageDTO toImageDTO(final String imageUrl) { - return new ImageDTO(imageUrl); + return new ImageDTO(null, imageUrl); } } diff --git a/src/main/java/com/ripple/BE/image/dto/response/ImageIdResponse.java b/src/main/java/com/ripple/BE/image/dto/response/ImageIdResponse.java new file mode 100644 index 0000000..fe75f72 --- /dev/null +++ b/src/main/java/com/ripple/BE/image/dto/response/ImageIdResponse.java @@ -0,0 +1,7 @@ +package com.ripple.BE.image.dto.response; + +public record ImageIdResponse(Long imageId) { + public static ImageIdResponse toImageIdResponse(long imageId) { + return new ImageIdResponse(imageId); + } +} diff --git a/src/main/java/com/ripple/BE/image/dto/response/ImageResponse.java b/src/main/java/com/ripple/BE/image/dto/response/ImageResponse.java new file mode 100644 index 0000000..a57ab80 --- /dev/null +++ b/src/main/java/com/ripple/BE/image/dto/response/ImageResponse.java @@ -0,0 +1,14 @@ +package com.ripple.BE.image.dto.response; + +import com.ripple.BE.image.dto.ImageDTO; + +public record ImageResponse(Long id, String url) { + + public static ImageResponse toImageResponse(final Long id, final String url) { + return new ImageResponse(id, url); + } + + public static ImageResponse toImageResponse(final ImageDTO imageDTO) { + return new ImageResponse(imageDTO.id(), imageDTO.url()); + } +} diff --git a/src/main/java/com/ripple/BE/image/exception/ImageException.java b/src/main/java/com/ripple/BE/image/exception/ImageException.java new file mode 100644 index 0000000..290dddf --- /dev/null +++ b/src/main/java/com/ripple/BE/image/exception/ImageException.java @@ -0,0 +1,12 @@ +package com.ripple.BE.image.exception; + +import com.ripple.BE.global.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ImageException extends RuntimeException { + + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/ripple/BE/image/exception/errorcode/ImageErrorCode.java b/src/main/java/com/ripple/BE/image/exception/errorcode/ImageErrorCode.java new file mode 100644 index 0000000..5dcf0c2 --- /dev/null +++ b/src/main/java/com/ripple/BE/image/exception/errorcode/ImageErrorCode.java @@ -0,0 +1,16 @@ +package com.ripple.BE.image.exception.errorcode; + +import com.ripple.BE.global.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ImageErrorCode implements ErrorCode { + IMAGE_PROCESSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "Image processing fail"), + IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "Image not found"); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/ripple/BE/image/repository/ImageRepository.java b/src/main/java/com/ripple/BE/image/repository/ImageRepository.java new file mode 100644 index 0000000..c9dc8fb --- /dev/null +++ b/src/main/java/com/ripple/BE/image/repository/ImageRepository.java @@ -0,0 +1,8 @@ +package com.ripple.BE.image.repository; + +import com.ripple.BE.image.domain.Image; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ImageRepository extends JpaRepository {} diff --git a/src/main/java/com/ripple/BE/image/s3/S3Uploader.java b/src/main/java/com/ripple/BE/image/s3/S3Uploader.java new file mode 100644 index 0000000..4bb0ba6 --- /dev/null +++ b/src/main/java/com/ripple/BE/image/s3/S3Uploader.java @@ -0,0 +1,93 @@ +package com.ripple.BE.image.s3; + +import static com.ripple.BE.image.exception.errorcode.ImageErrorCode.*; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.ripple.BE.image.domain.S3Info; +import com.ripple.BE.image.exception.ImageException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@RequiredArgsConstructor +@Component +public class S3Uploader { + + private static final String CONTENT_TYPE = "multipart/formed-data"; + private final AmazonS3Client amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public S3Info uploadFiles(MultipartFile multipartFile, String folderName) throws ImageException { + File localUploadFile = convertToFile(multipartFile); + return uploadFileToS3(localUploadFile, folderName); + } + + public S3Info uploadFileToS3(File file, String folderName) { + String fileName = buildFileName(folderName, file.getName()); + String uploadUrl = uploadToS3(file, fileName); + + file.delete(); + + return buildS3Info(folderName, file, uploadUrl); + } + + public void deleteFile(S3Info s3Info) { + String fileName = buildFileName(s3Info.getFolderName(), s3Info.getFileName()); + log.info("{} 사진 삭제", fileName); + amazonS3.deleteObject(bucket, fileName); + } + + private String uploadToS3(File uploadFile, String fileName) { + PutObjectRequest putObjectRequest = + new PutObjectRequest(bucket, fileName, uploadFile) + .withCannedAcl(CannedAccessControlList.PublicRead); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(CONTENT_TYPE); + amazonS3.putObject(putObjectRequest); + + return amazonS3.getUrl(bucket, fileName).toString(); + } + + private static File convertToFile(MultipartFile file) throws ImageException { + String fileExtension = getFileExtension(file); + File convertedFile = + new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + "." + fileExtension); + + try (FileOutputStream fos = new FileOutputStream(convertedFile)) { + fos.write(file.getBytes()); + } catch (IOException e) { + throw new ImageException(IMAGE_PROCESSING_FAIL); + } + + return convertedFile; + } + + private static String getFileExtension(MultipartFile file) throws ImageException { + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.contains(".")) { + throw new ImageException(IMAGE_NOT_FOUND); + } + return originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + } + + private String buildFileName(String folderName, String fileName) { + return folderName + "/" + fileName; + } + + private S3Info buildS3Info(String folderName, File file, String uploadUrl) { + return S3Info.builder().folderName(folderName).fileName(file.getName()).url(uploadUrl).build(); + } +} diff --git a/src/main/java/com/ripple/BE/image/service/ImageService.java b/src/main/java/com/ripple/BE/image/service/ImageService.java new file mode 100644 index 0000000..feb3159 --- /dev/null +++ b/src/main/java/com/ripple/BE/image/service/ImageService.java @@ -0,0 +1,43 @@ +package com.ripple.BE.image.service; + +import static com.ripple.BE.image.exception.errorcode.ImageErrorCode.*; + +import com.ripple.BE.image.domain.Image; +import com.ripple.BE.image.domain.S3Info; +import com.ripple.BE.image.exception.ImageException; +import com.ripple.BE.image.repository.ImageRepository; +import com.ripple.BE.image.s3.S3Uploader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@Service +@Slf4j +public class ImageService { + + private final ImageRepository imageRepository; + private final S3Uploader s3Uploader; + + private static final String FOLDER_NAME = "post"; + + @Transactional + public long addImageToPost(MultipartFile file) { + + S3Info s3Info = s3Uploader.uploadFiles(file, FOLDER_NAME); + + Image image = imageRepository.save(Image.toImageEntity(s3Info)); + return image.getId(); + } + + @Transactional + public void deleteImage(long imageId) { + Image image = + imageRepository.findById(imageId).orElseThrow(() -> new ImageException(IMAGE_NOT_FOUND)); + s3Uploader.deleteFile(image.getS3Info()); + + imageRepository.delete(image); + } +} diff --git a/src/main/java/com/ripple/BE/post/controller/PostController.java b/src/main/java/com/ripple/BE/post/controller/PostController.java index bc70dff..05dd43a 100644 --- a/src/main/java/com/ripple/BE/post/controller/PostController.java +++ b/src/main/java/com/ripple/BE/post/controller/PostController.java @@ -7,6 +7,7 @@ import com.ripple.BE.post.dto.PostListDTO; import com.ripple.BE.post.dto.request.CommentRequest; import com.ripple.BE.post.dto.request.PostRequest; +import com.ripple.BE.post.dto.request.PostUpdateRequest; import com.ripple.BE.post.dto.response.PostListResponse; import com.ripple.BE.post.dto.response.PostResponse; import com.ripple.BE.post.service.PostService; @@ -15,7 +16,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.PositiveOrZero; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -31,7 +31,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor @RestController @@ -41,19 +40,35 @@ public class PostController { private final PostService postService; - @Operation(summary = "게시물 작성", description = "게시물을 작성합니다.") + @Operation(summary = "게시물 작성", description = "게시물을 작성합니다. 게시물을 등록하기 전 이미지 등록을 완료해주세요") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> createPost( final @AuthenticationPrincipal CustomUserDetails currentUser, - final @RequestPart(value = "post") @Valid PostRequest post, - final @RequestPart(value = "imageList", required = false) List imageList) { + final @RequestPart(value = "post") @Valid PostRequest request) { - postService.createPost(currentUser.getId(), PostDTO.toPostDTO(post), imageList); + postService.createPost( + currentUser.getId(), PostDTO.toPostDTO(request), request.type(), request.imageIds()); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); } + @Operation( + summary = "게시물 수정", + description = + "게시물을 수정합니다. 게시물을 수정하기 전 이미지 수정을 완료해주세요. 삭제한 이미지는 입력하지 말고, 새로 추가된 이미지의 ID만 입력해주세요.") + @PatchMapping("/{id}") + public ResponseEntity> updatePost( + final @AuthenticationPrincipal CustomUserDetails currentUser, + final @PathVariable("id") long id, + final @Valid @RequestBody PostUpdateRequest request) { + + postService.updatePost( + id, currentUser.getId(), PostDTO.toPostDTO(request), request.newImageIds()); + + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); + } + @Operation(summary = "게시물 삭제", description = "게시물을 삭제합니다.") @DeleteMapping("/{id}") public ResponseEntity> deletePost( diff --git a/src/main/java/com/ripple/BE/post/domain/Post.java b/src/main/java/com/ripple/BE/post/domain/Post.java index 09b5890..a2f949d 100644 --- a/src/main/java/com/ripple/BE/post/domain/Post.java +++ b/src/main/java/com/ripple/BE/post/domain/Post.java @@ -63,6 +63,7 @@ public class Post extends BaseEntity { @Column(name = "scrap_count") private long scrapCount = 0L; // 스크랩 수 + @Setter @Column(name = "post_type", nullable = false) private PostType type; // 게시글 타입 @@ -114,11 +115,23 @@ public static Post toPostEntity(PostDTO postDTO) { .title(postDTO.title()) .content(postDTO.content()) .type(postDTO.type()) + .imageList(new ArrayList<>()) .build(); } + public void update(PostDTO postDTO) { + this.title = postDTO.title(); + this.content = postDTO.content(); + this.type = postDTO.type(); + } + public void setAuthor(User author) { this.author = author; author.getPostList().add(this); } + + public void addImage(Image image) { + this.imageList.add(image); + image.setPost(this); + } } diff --git a/src/main/java/com/ripple/BE/post/dto/PostDTO.java b/src/main/java/com/ripple/BE/post/dto/PostDTO.java index 9304433..f107ebe 100644 --- a/src/main/java/com/ripple/BE/post/dto/PostDTO.java +++ b/src/main/java/com/ripple/BE/post/dto/PostDTO.java @@ -4,6 +4,7 @@ import com.ripple.BE.post.domain.Post; import com.ripple.BE.post.domain.type.PostType; import com.ripple.BE.post.dto.request.PostRequest; +import com.ripple.BE.post.dto.request.PostUpdateRequest; import com.ripple.BE.user.dto.CommunityUserDTO; import java.time.LocalDate; import java.time.LocalDateTime; @@ -73,4 +74,21 @@ public static PostDTO toPostDTO(final PostRequest postRequest) { null, null); } + + public static PostDTO toPostDTO(final PostUpdateRequest request) { + return new PostDTO( + null, + request.title(), + null, + request.content(), + request.type(), + null, + null, + null, + null, + null, + null, + null, + null); + } } diff --git a/src/main/java/com/ripple/BE/post/dto/request/PostRequest.java b/src/main/java/com/ripple/BE/post/dto/request/PostRequest.java index 0b3956b..34303a8 100644 --- a/src/main/java/com/ripple/BE/post/dto/request/PostRequest.java +++ b/src/main/java/com/ripple/BE/post/dto/request/PostRequest.java @@ -3,8 +3,10 @@ import com.ripple.BE.post.domain.type.PostType; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.util.List; public record PostRequest( @NotNull @Size(min = 2, max = 50) String title, @Size(min = 2, max = 3500) String content, - PostType type) {} + PostType type, + List imageIds) {} diff --git a/src/main/java/com/ripple/BE/post/dto/request/PostUpdateRequest.java b/src/main/java/com/ripple/BE/post/dto/request/PostUpdateRequest.java new file mode 100644 index 0000000..03bf7f5 --- /dev/null +++ b/src/main/java/com/ripple/BE/post/dto/request/PostUpdateRequest.java @@ -0,0 +1,12 @@ +package com.ripple.BE.post.dto.request; + +import com.ripple.BE.post.domain.type.PostType; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record PostUpdateRequest( + @NotNull @Size(min = 2, max = 50) String title, + @Size(min = 2, max = 3500) String content, + PostType type, + List newImageIds) {} diff --git a/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java b/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java index 2297c8b..f405c30 100644 --- a/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java +++ b/src/main/java/com/ripple/BE/post/dto/response/PostResponse.java @@ -1,7 +1,7 @@ package com.ripple.BE.post.dto.response; import com.ripple.BE.global.utils.RelativeTimeFormatter; -import com.ripple.BE.image.dto.ImageDTO; +import com.ripple.BE.image.dto.response.ImageResponse; import com.ripple.BE.post.domain.type.PostType; import com.ripple.BE.post.dto.PostDTO; import java.util.List; @@ -16,7 +16,7 @@ public record PostResponse( long likeCount, long commentCount, long scrapCount, - List imageList, + List imageList, String createdDate, CommentListResponse commentListResponse) { @@ -31,7 +31,7 @@ public static PostResponse toPostResponse(PostDTO postDTO) { postDTO.likeCount(), postDTO.commentCount(), postDTO.scrapCount(), - postDTO.imageList().imageDTOList().stream().map(ImageDTO::url).toList(), + postDTO.imageList().imageDTOList().stream().map(ImageResponse::toImageResponse).toList(), RelativeTimeFormatter.formatRelativeTime(postDTO.createdDate()), CommentListResponse.toCommentListResponse(postDTO.commentListDTO())); } diff --git a/src/main/java/com/ripple/BE/post/service/PostService.java b/src/main/java/com/ripple/BE/post/service/PostService.java index 92e97fe..7cda32f 100644 --- a/src/main/java/com/ripple/BE/post/service/PostService.java +++ b/src/main/java/com/ripple/BE/post/service/PostService.java @@ -1,7 +1,12 @@ package com.ripple.BE.post.service; +import static com.ripple.BE.image.exception.errorcode.ImageErrorCode.*; import static com.ripple.BE.post.exception.errorcode.PostErrorCode.*; +import com.ripple.BE.image.domain.Image; +import com.ripple.BE.image.exception.ImageException; +import com.ripple.BE.image.repository.ImageRepository; +import com.ripple.BE.image.service.ImageService; import com.ripple.BE.post.domain.Comment; import com.ripple.BE.post.domain.CommentLike; import com.ripple.BE.post.domain.Post; @@ -28,7 +33,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor @Service @@ -40,23 +44,64 @@ public class PostService { private final PostLikeRepository postLikeRepository; private final PostScrapRepository postScrapRepository; private final CommentLikeRepository commentLikeRepository; + private final ImageRepository imageRepository; private final UserService userService; + private final ImageService imageService; private static final int PAGE_SIZE = 10; @Transactional public void createPost( - final long userId, final PostDTO postDTO, final List imageList) { + final long userId, + final PostDTO postDTO, + final PostType postType, + final List imageIdList) { User user = userService.findUserById(userId); Post post = Post.toPostEntity(postDTO); post.setAuthor(user); + post.setType(postType); + + if (imageIdList != null && !imageIdList.isEmpty()) { + for (long imageId : imageIdList) { + Image image = + imageRepository + .findById(imageId) + .orElseThrow(() -> new ImageException(IMAGE_NOT_FOUND)); + post.addImage(image); + } + } - /** 이미지 S3 업로드 로직 추후 추가 */ postRepository.save(post); } + @Transactional + public void updatePost( + final long postId, + final long userId, + final PostDTO postDTO, + final List newImageIdList) { + + Post post = findPostById(postId); + + if (post.getAuthor().getId() != userId) { + throw new PostException(POST_NOT_AUTHORIZED); + } + + post.update(postDTO); + + if (newImageIdList != null && !newImageIdList.isEmpty()) { + for (long imageId : newImageIdList) { + Image image = + imageRepository + .findById(imageId) + .orElseThrow(() -> new ImageException(IMAGE_NOT_FOUND)); + post.addImage(image); + } + } + } + @Transactional public void deletePost(final long postId, final long userId) { Post post = findPostByIdForUpdate(postId); @@ -65,6 +110,10 @@ public void deletePost(final long postId, final long userId) { throw new PostException(POST_NOT_AUTHORIZED); } + for (Image image : post.getImageList()) { + imageService.deleteImage(image.getId()); + } + postRepository.delete(post); } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 2ed06ef..4cfd0e5 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -21,6 +21,20 @@ spring: redis: host: localhost port: 6379 + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + +cloud: + aws: + s3: + bucket: ${BUCKET_ADDRESS} + stack.auto: false + region.static: ap-northeast-2 + credentials: + accessKey: ${S3_ACCESS_KEY} + secretKey: ${S3_ACCESS_PASSWORD} logging: level: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 793b801..ff21080 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -21,6 +21,20 @@ spring: redis: host: ${REDIS_HOST} port: ${REDIS_PORT} + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + +cloud: + aws: + s3: + bucket: ${BUCKET_ADDRESS} + stack.auto: false + region.static: ap-northeast-2 + credentials: + accessKey: ${S3_ACCESS_KEY} + secretKey: ${S3_ACCESS_PASSWORD} server: ssl: