diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java b/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java index 24bd0d5..9fcc7ad 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/global/ErrorMessage.java @@ -8,4 +8,5 @@ public class ErrorMessage { final public static String HASHTAG_NOT_FOUND = "해쉬태그가 존재하지 않습니다."; final public static String POST_NOT_FOUND = "게시글이 존재하지 않습니다."; final public static String ALREADY_EXISTS_CATEGORY = "이미 존재하는 카테고리 이름입니다."; + final public static String INVALID_PASSWORD = "비밀번호가 일치하지 않습니다"; } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java b/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java index c6c6142..06aaed8 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/global/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import com.starchive.springapp.category.exception.CategoryNotFoundException; import com.starchive.springapp.global.dto.ErrorResult; import com.starchive.springapp.hashtag.exception.HashTagNotFoundException; +import com.starchive.springapp.post.exception.InvalidPasswordException; import com.starchive.springapp.post.exception.PostNotFoundException; import java.util.HashMap; import java.util.Map; @@ -47,4 +48,10 @@ public ResponseEntity handleRuntimeException(RuntimeException ex) { ErrorResult errorResult = new ErrorResult(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); return ResponseEntity.internalServerError().body(errorResult); } + + @ExceptionHandler(InvalidPasswordException.class) + public ResponseEntity handleInvalidPasswordException(InvalidPasswordException ex) { + ErrorResult errorResult = new ErrorResult(HttpStatus.BAD_REQUEST, ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResult); + } } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/image/repository/PostImageRepository.java b/BACK/spring-app/src/main/java/com/starchive/springapp/image/repository/PostImageRepository.java index 5ce6f0f..9c03408 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/image/repository/PostImageRepository.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/image/repository/PostImageRepository.java @@ -17,4 +17,11 @@ public interface PostImageRepository extends JpaRepository { void deleteByIds(@Param("ids") List ids); List findManyByIdIn(@Param("ids") List ids); + + @Query("select pi from PostImage pi where pi.post.id = :postId") + List findAllByPostId(@Param("postId")Long postId); + + @Modifying + @Query("update PostImage pi set pi.post.id = :postId where pi.imagePath in :imageUrls") + int bulkUpdatePostId(@Param("imageUrls") List imageUrls,@Param("postId")Long postId); } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/image/service/PostImageService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/image/service/PostImageService.java index 8ff698c..5dceb51 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/image/service/PostImageService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/image/service/PostImageService.java @@ -39,22 +39,18 @@ public void setPost(List imageIds, Post post) { }); } + public void setPostByImagePath(List imagePaths, Post post) { + postImageRepository.bulkUpdatePostId(imagePaths, post.getId()); + } + @Scheduled(cron = "0 0 2 * * ?") public void deleteOldOrphanedPostImages() { LocalDateTime cutoffDate = LocalDateTime.now().minusDays(1); // 하루 전 List oldOrphanedPostImages = postImageRepository.findOldOrphanedPostImages(cutoffDate); - oldOrphanedPostImages.forEach(postImage -> { - s3Service.deleteObject(extractKeyFromUrl(postImage.getImagePath())); - postImageRepository.delete(postImage); - log.info("Deleted old orphaned PostImages: {}", postImage.getImagePath()); - }); - - } - private String extractKeyFromUrl(String url) { - URI uri = URI.create(url); - return uri.getPath().substring(1); // 첫 번째 '/' 제거 + List urls = oldOrphanedPostImages.stream().map(postImage -> postImage.getImagePath()).toList(); + s3Service.deleteObjects(urls); } } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java index 8dad10b..8adc116 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/controller/PostController.java @@ -3,18 +3,15 @@ import com.starchive.springapp.post.dto.PostCreateRequest; import com.starchive.springapp.post.dto.PostDto; import com.starchive.springapp.post.dto.PostListResponse; +import com.starchive.springapp.post.dto.PostUpdateRequest; import com.starchive.springapp.post.service.PostService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Null; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -50,4 +47,18 @@ public ResponseEntity findPost(@PathVariable("postId") Long postId) { PostDto postDto = postService.findOne(postId); return ResponseEntity.ok(postDto); } + + @PutMapping("/post") + @Operation(summary = "게시글 수정") + public ResponseEntity update(@Valid @RequestBody PostUpdateRequest request) { + PostDto postDto = postService.update(request); + return ResponseEntity.ok(postDto); + } + + @DeleteMapping("/post/{postId}") + @Operation(summary = "게시글 삭제") + public ResponseEntity delete(@Valid @PathVariable("postId") Long postId) { + postService.delete(postId); + return ResponseEntity.status(204).build(); + } } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/domain/Post.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/domain/Post.java index 896ac78..3d9e179 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/post/domain/Post.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/domain/Post.java @@ -4,6 +4,7 @@ import com.starchive.springapp.category.domain.Category; import com.starchive.springapp.post.dto.PostCreateRequest; +import com.starchive.springapp.post.dto.PostUpdateRequest; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -63,4 +64,11 @@ public static Post of(PostCreateRequest request, Category category) { return post; } + public void update(PostUpdateRequest request, Category category) { + title = request.getTitle(); + content = request.getContent(); + author = request.getAuthor(); + this.category = category; + } + } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostUpdateRequest.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostUpdateRequest.java new file mode 100644 index 0000000..4eefca9 --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/dto/PostUpdateRequest.java @@ -0,0 +1,53 @@ +package com.starchive.springapp.post.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostUpdateRequest { + @NotNull + @Schema(description = "게시글 id",example = "1") + private Long id; + + @NotEmpty + @Size(max = 64) + @Schema(description = "게시글 제목", example = "게시글 제목 예시") + private String title; + + @NotEmpty + @Schema(description = "게시글 내용", example = "게시글 내용 예시") + private String content; + + @NotEmpty + @Size(max = 32) + @Schema(description = "작성자 이름", example = "홍길동") + private String author; + + @NotEmpty + @Size(max = 128) + @Schema(description = "비밀번호", example = "1234") + private String password; + + @NotNull + @Schema(description = "카테고리 ID", example = "1") + private Long categoryId; + + @Nullable + @Schema(description = "해쉬 태그 ID", example = "[1,2,3]") + private List hashTagIds; + + @Nullable + @Schema(description = "첨부 이미지 ID", example = "[1,2]") + private List imageIds; + +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/exception/InvalidPasswordException.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/exception/InvalidPasswordException.java new file mode 100644 index 0000000..d9085a1 --- /dev/null +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/exception/InvalidPasswordException.java @@ -0,0 +1,12 @@ +package com.starchive.springapp.post.exception; + +import static com.starchive.springapp.global.ErrorMessage.INVALID_PASSWORD; + +public class InvalidPasswordException extends RuntimeException { + public InvalidPasswordException() { + super(INVALID_PASSWORD); + } + public InvalidPasswordException(String message) { + super(message); + } +} diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java index dfdb05c..b2a2df4 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/post/service/PostService.java @@ -4,18 +4,23 @@ import com.starchive.springapp.category.service.CategoryService; import com.starchive.springapp.hashtag.dto.HashTagResponse; import com.starchive.springapp.hashtag.service.HashTagService; +import com.starchive.springapp.image.domain.PostImage; +import com.starchive.springapp.image.repository.PostImageRepository; import com.starchive.springapp.image.service.PostImageService; import com.starchive.springapp.post.domain.Post; -import com.starchive.springapp.post.dto.PostCreateRequest; -import com.starchive.springapp.post.dto.PostDto; -import com.starchive.springapp.post.dto.PostListResponse; -import com.starchive.springapp.post.dto.PostSimpleDto; +import com.starchive.springapp.post.dto.*; +import com.starchive.springapp.post.exception.InvalidPasswordException; import com.starchive.springapp.post.exception.PostNotFoundException; import com.starchive.springapp.post.repository.PostRepository; import com.starchive.springapp.posthashtag.domain.PostHashTag; import com.starchive.springapp.posthashtag.service.PostHashTagService; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.starchive.springapp.s3.S3Service; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -32,6 +37,8 @@ public class PostService { private final CategoryService categoryService; private final PostImageService postImageService; private final HashTagService hashTagService; + private final PostImageRepository postImageRepository; + private final S3Service s3Service; public void createPost(PostCreateRequest request) { Category category = categoryService.findOne(request.getCategoryId()); @@ -82,6 +89,98 @@ public PostListResponse findPosts(Long categoryId, Long hashTagId, int pageNum, return PostListResponse.from(dtoPage); } + public PostDto update(PostUpdateRequest request) { + Post post = postRepository.findById(request.getId()).orElseThrow(PostNotFoundException::new); + + if(request.getPassword() != post.getPassword()) { + throw new InvalidPasswordException(); + } + + Category category = categoryService.findOne(request.getCategoryId()); + + post.update(request, category); + + HashSet hashTagIds = new HashSet<>(); + postHashTagService.findManyByPost(post.getId()).stream().forEach(postHashTag -> {hashTagIds.add(postHashTag.getHashTag().getId());}); + + updatePostHashTags(request, hashTagIds, post); + + updateImages(request, post); + + List hashTagResponses = hashTagService.findManyByPost(post.getId()); + return PostDto.of(post, hashTagResponses); + } + + public void delete(Long postId) { + Post post = postRepository.findById(postId).orElseThrow(PostNotFoundException::new); + + List postImages = postImageRepository.findAllByPostId(post.getId()); + + List urls = postImages.stream().map(PostImage::getImagePath).toList(); + s3Service.deleteObjects(urls); + + postImageRepository.deleteAll(postImages); + + postHashTagService.deleteManyByPostId(post.getId()); + + postRepository.delete(post); + } + + private void updateImages(PostUpdateRequest postUpdateRequest, Post post){ + String content = postUpdateRequest.getContent(); + HashSet imageUrlsToUpdate = new HashSet<>(); + + imageUrlsToUpdate.addAll(extractImageUrls(content)); + + List postImages = postImageRepository.findAllByPostId(postUpdateRequest.getId()); + + List postImageToDelete = new ArrayList<>(); + for (PostImage postImage : postImages) { + if (imageUrlsToUpdate.contains(postImage.getImagePath())) { + imageUrlsToUpdate.remove(postImage.getImagePath()); + continue; + } + postImageToDelete.add(postImage); + } + + List urls = postImageToDelete.stream().map(PostImage::getImagePath).toList(); + s3Service.deleteObjects(urls); + postImageRepository.deleteAll(postImageToDelete); + postImageService.setPostByImagePath(new ArrayList<>(imageUrlsToUpdate),post); + } + + private List extractImageUrls(String markdownContent) { + List imageUrls = new ArrayList<>(); + + // 정규식 패턴 정의 + String regex = "!\\[.*?\\]\\((.*?)\\)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(markdownContent); + + // 매칭된 이미지 URL 추출 + while (matcher.find()) { + imageUrls.add(matcher.group(1)); + } + + return imageUrls; + } + + private void updatePostHashTags(PostUpdateRequest request, HashSet hashTagIds, Post post) { + List hashTagIdsToUpdate = new ArrayList<>(); + if(request.getHashTagIds() != null) { + for (Long updatedHashTagId : request.getHashTagIds()) { + if(hashTagIds.contains(updatedHashTagId)) { + hashTagIds.remove(updatedHashTagId); + continue; + } + hashTagIdsToUpdate.add(updatedHashTagId); + } + } + + postHashTagService.storePostHashTag(hashTagIdsToUpdate, post); + postHashTagService.deleteManyByHashTagIds(hashTagIds); + } + private List extractCategoryIds(Category category) { List categoryIds = new ArrayList<>(); diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/repository/PostHashTagRepository.java b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/repository/PostHashTagRepository.java index 0660627..f147b51 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/repository/PostHashTagRepository.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/repository/PostHashTagRepository.java @@ -1,6 +1,9 @@ package com.starchive.springapp.posthashtag.repository; +import com.starchive.springapp.post.domain.Post; import com.starchive.springapp.posthashtag.domain.PostHashTag; + +import java.util.HashSet; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -8,10 +11,24 @@ import org.springframework.data.repository.query.Param; public interface PostHashTagRepository extends JpaRepository { + + @Query("select ph from PostHashTag ph WHERE ph.hashTag.id = :hashTagId") + List findAllByHashTagId(@Param("hashTagId") Long hasTagId); + + List post(Post post); + + @Query("select ph from PostHashTag ph where ph.post.id = :postId") + List findAllByPostId(@Param("postId") Long postId); + @Modifying - @Query("DELETE FROM PostHashTag p WHERE p.hashTag.id = :hashTagId") + @Query("DELETE FROM PostHashTag ph WHERE ph.hashTag.id = :hashTagId") void deleteAllByHashTagId(@Param("hashTagId") Long hasTagId); - @Query("select p from PostHashTag p WHERE p.hashTag.id = :hashTagId") - List findAllByHashTagId(@Param("hashTagId") Long hasTagId); + @Modifying + @Query("delete from PostHashTag ph where ph.hashTag.id in :hashTagIds") + void deleteAllByHashTagIds(@Param("hashTagIds") HashSet hashTagIds); + + @Modifying + @Query("delete from PostHashTag ph where ph.post.id = :postId") + void deleteAllByPostId(@Param("postId") Long postId); } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java index 476f0da..ca2c9cb 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/posthashtag/service/PostHashTagService.java @@ -6,6 +6,7 @@ import com.starchive.springapp.posthashtag.domain.PostHashTag; import com.starchive.springapp.posthashtag.repository.PostHashTagRepository; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,4 +39,16 @@ public void storePostHashTag(List hashTagsIds, Post post) { public List findManyByHashTag(Long hashTagId) { return postHashTagRepository.findAllByHashTagId(hashTagId); } + + public List findManyByPost(Long postId) { + return postHashTagRepository.findAllByPostId(postId); + } + + public void deleteManyByHashTagIds(HashSet hashTagIds) { + postHashTagRepository.deleteAllByHashTagIds(hashTagIds); + } + + public void deleteManyByPostId(Long postId) { + postHashTagRepository.deleteAllByPostId(postId); + } } diff --git a/BACK/spring-app/src/main/java/com/starchive/springapp/s3/S3Service.java b/BACK/spring-app/src/main/java/com/starchive/springapp/s3/S3Service.java index 15b560c..f695a53 100644 --- a/BACK/spring-app/src/main/java/com/starchive/springapp/s3/S3Service.java +++ b/BACK/spring-app/src/main/java/com/starchive/springapp/s3/S3Service.java @@ -3,9 +3,12 @@ import com.amazonaws.SdkClientException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.starchive.springapp.global.ErrorMessage; import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -53,9 +56,28 @@ public String saveFile(MultipartFile file) { public void deleteObject(String key) { amazonS3.deleteObject(bucket, key); + amazonS3.deleteObjects(new DeleteObjectsRequest(bucket).withKeys(key)); log.info("Deleted S3 Object: {}", key); } + public void deleteObjects(List urls) { + String[] keys = urls.stream().map(this::extractKeyFromUrl).toArray(String[]::new); + + amazonS3.deleteObjects(new DeleteObjectsRequest(bucket).withKeys(keys)); + + for (String key : keys) { + log.info("Deleted S3 Object: {}", key); + } + + } + + private String extractKeyFromUrl(String url) { + URI uri = URI.create(url); + String[] urlInfo = url.split("/"); + String key = urlInfo[urlInfo.length - 1]; + return key; + } + // 랜덤파일명 생성 (파일명 중복 방지) private String generateRandomFilename(MultipartFile multipartFile) { String originalFilename = multipartFile.getOriginalFilename(); diff --git a/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java b/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java index a9bf70e..dfbc64e 100644 --- a/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java +++ b/BACK/spring-app/src/test/java/com/starchive/springapp/post/service/PostServiceTest.java @@ -2,30 +2,49 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ListObjectsV2Result; import com.starchive.springapp.category.domain.Category; import com.starchive.springapp.category.repository.CategoryRepository; import com.starchive.springapp.hashtag.domain.HashTag; import com.starchive.springapp.hashtag.repository.HashTagRepository; import com.starchive.springapp.image.domain.PostImage; +import com.starchive.springapp.image.dto.PostImageDto; import com.starchive.springapp.image.repository.PostImageRepository; +import com.starchive.springapp.image.service.PostImageService; import com.starchive.springapp.post.domain.Post; import com.starchive.springapp.post.dto.PostCreateRequest; import com.starchive.springapp.post.dto.PostDto; import com.starchive.springapp.post.dto.PostListResponse; +import com.starchive.springapp.post.dto.PostUpdateRequest; +import com.starchive.springapp.post.exception.InvalidPasswordException; import com.starchive.springapp.post.repository.PostRepository; import com.starchive.springapp.posthashtag.domain.PostHashTag; import com.starchive.springapp.posthashtag.repository.PostHashTagRepository; +import com.starchive.springapp.s3.S3MockConfig; +import com.starchive.springapp.s3.S3Service; import jakarta.persistence.EntityManager; + +import java.lang.reflect.Field; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.transaction.annotation.Transactional; @SpringBootTest @Transactional +@Import(S3MockConfig.class) class PostServiceTest { @Autowired HashTagRepository hashTagRepository; @@ -48,6 +67,32 @@ class PostServiceTest { @Autowired EntityManager em; + @Autowired + @Qualifier("MockS3Client") + private AmazonS3 amazonS3; + + @Autowired + S3MockConfig s3MockConfig; + + @Autowired + S3Service s3Service; + + @Autowired + private PostImageService postImageService; + + @BeforeEach + public void resetS3Bucket() { + String bucketName = s3MockConfig.getTestBucketName(); + + // 기존 버킷 삭제 (모든 객체도 함께 삭제됨) + if (amazonS3.doesBucketExistV2(bucketName)) { + amazonS3.deleteBucket(bucketName); + } + + // 버킷 재생성 + amazonS3.createBucket(bucketName); + } + @Test public void 게시글_작성_통합_테스트() throws Exception { //given @@ -128,5 +173,150 @@ class PostServiceTest { } + @Test + public void 게시글_수정_틀린_비밀번호_예외_테스트(){ + //given + Category category = new Category("예시카테고리", null); + categoryRepository.save(category); + + PostCreateRequest postCreateRequest = + new PostCreateRequest("title", "content", "author", "1234" + , category.getId(), null, null); + + + postService.createPost(postCreateRequest); + + Post createdPost = postRepository.findAll().getFirst(); + //when + PostUpdateRequest postUpdateRequest = new PostUpdateRequest(createdPost.getId(), "title", "content", "author", "12345" + , category.getId(), null, null); + + //then + Assertions.assertThatThrownBy(()-> postService.update(postUpdateRequest)).isInstanceOf(InvalidPasswordException.class); + } + + @Test + public void 게시글_수정_통합_테스트() throws NoSuchFieldException, IllegalAccessException { + //given + String path1 = "imagePath1.jpg"; + String path2 = "imagePath2.jpg"; + String contentType = "image/jpg"; + String bucket = s3MockConfig.getTestBucketName(); + + MockMultipartFile file1 = new MockMultipartFile("test1", path1, contentType, "test1".getBytes()); + MockMultipartFile file2 = new MockMultipartFile("test2", path2, contentType, "test2".getBytes()); + + //Mock S3에 이미지 저장 + setS3ConfigForTest(bucket); + + PostImageDto postImageDto1 = postImageService.uploadImage(file1); + PostImageDto postImageDto2 = postImageService.uploadImage(file2); + + Category category = new Category("예시카테고리", null); + categoryRepository.save(category); + + List postImageIds = new ArrayList<>(List.of(postImageDto1.getId(), postImageDto2.getId())); + String markdownText = """ + Here is an image example: + ![이미지](%s) + ![이미지](%s) + """.formatted(postImageDto1.getImagePath(), postImageDto2.getImagePath()); + + PostCreateRequest postCreateRequest = + new PostCreateRequest("title", markdownText, "author", "password" + , category.getId(), null, postImageIds); + + postService.createPost(postCreateRequest); + + ListObjectsV2Result s3Objects = amazonS3.listObjectsV2(bucket); + Assertions.assertThat(s3Objects.getKeyCount()).isEqualTo(2); + + //when + Post findOne = postRepository.findAll().getFirst(); + + String updatedMarkdownText = """ + Here is an image example: + ![이미지](%s) + """.formatted(postImageDto1.getImagePath()); + + PostUpdateRequest postUpdateRequest = new PostUpdateRequest(findOne.getId(), "title", updatedMarkdownText, "author", "password" + , category.getId(), null, postImageIds); + + postService.update(postUpdateRequest); + + //then + s3Objects = amazonS3.listObjectsV2(bucket); + + Assertions.assertThat(s3Objects.getKeyCount()).isEqualTo(1); + Assertions.assertThat(postImageRepository.findAll()).hasSize(1); + Assertions.assertThat(postImageRepository.findAll().getFirst().getImagePath()).isEqualTo(postImageDto1.getImagePath()); + } + + @Test + public void 게시글_삭제_통합_테스트() throws NoSuchFieldException, IllegalAccessException { + //given + String path1 = "imagePath1.jpg"; + String path2 = "imagePath2.jpg"; + String contentType = "image/jpg"; + String bucket = s3MockConfig.getTestBucketName(); + + MockMultipartFile file1 = new MockMultipartFile("test1", path1, contentType, "test1".getBytes()); + MockMultipartFile file2 = new MockMultipartFile("test2", path2, contentType, "test2".getBytes()); + + //Mock S3에 이미지 저장 + setS3ConfigForTest(bucket); + + PostImageDto postImageDto1 = postImageService.uploadImage(file1); + PostImageDto postImageDto2 = postImageService.uploadImage(file2); + + Category category = new Category("예시카테고리", null); + categoryRepository.save(category); + + HashTag hashTag = new HashTag("tag1"); + hashTagRepository.save(hashTag); + + List postImageIds = new ArrayList<>(List.of(postImageDto1.getId(), postImageDto2.getId())); + String markdownText = """ + Here is an image example: + ![이미지](%s) + ![이미지](%s) + """.formatted(postImageDto1.getImagePath(), postImageDto2.getImagePath()); + + PostCreateRequest postCreateRequest = + new PostCreateRequest("title", markdownText, "author", "password" + , category.getId(), Arrays.asList(hashTag.getId()), postImageIds); + + postService.createPost(postCreateRequest); + + ListObjectsV2Result s3Objects = amazonS3.listObjectsV2(bucket); + + Assertions.assertThat(s3Objects.getKeyCount()).isEqualTo(2); + Assertions.assertThat(postImageRepository.findAll()).hasSize(2); + Assertions.assertThat(postHashTagRepository.findAll()).hasSize(1); + Assertions.assertThat(postRepository.findAll()).hasSize(1); + + //when + Post post = postRepository.findAll().getFirst(); + postService.delete(post.getId()); + + //then + s3Objects = amazonS3.listObjectsV2(bucket); + Assertions.assertThat(s3Objects.getKeyCount()).isEqualTo(0); + Assertions.assertThat(postImageRepository.findAll()).hasSize(0); + Assertions.assertThat(postHashTagRepository.findAll()).hasSize(0); + Assertions.assertThat(postRepository.findAll()).hasSize(0); + } + + private void setS3ConfigForTest(String bucket) throws NoSuchFieldException, IllegalAccessException { + //Reflection s3Service + Field reflectionFieldFor_amazonS3 = s3Service.getClass().getDeclaredField("amazonS3"); + reflectionFieldFor_amazonS3.setAccessible(true); + reflectionFieldFor_amazonS3.set(s3Service, amazonS3); + + Field reflectionFieldFor_bucket = s3Service.getClass().getDeclaredField("bucket"); + reflectionFieldFor_bucket.setAccessible(true); + reflectionFieldFor_bucket.set(s3Service, bucket); + } + } \ No newline at end of file diff --git a/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3MockConfig.java b/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3MockConfig.java index f01c3fa..b24b3b1 100644 --- a/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3MockConfig.java +++ b/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3MockConfig.java @@ -10,7 +10,7 @@ import org.springframework.context.annotation.Bean; @TestConfiguration -class S3MockConfig { +public class S3MockConfig { private String testBucketName = "test-bucket-in-memory"; diff --git a/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3ServiceTest.java b/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3ServiceTest.java index 3d0e3cb..afd1286 100644 --- a/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3ServiceTest.java +++ b/BACK/spring-app/src/test/java/com/starchive/springapp/s3/S3ServiceTest.java @@ -4,6 +4,8 @@ import com.amazonaws.services.s3.AmazonS3; import java.lang.reflect.Field; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -26,6 +28,19 @@ class S3ServiceTest { @Autowired private S3Service s3Service; + @BeforeEach + public void resetS3Bucket() { + String bucketName = s3MockConfig.getTestBucketName(); + + // 기존 버킷 삭제 (모든 객체도 함께 삭제됨) + if (amazonS3.doesBucketExistV2(bucketName)) { + amazonS3.deleteBucket(bucketName); + } + + // 버킷 재생성 + amazonS3.createBucket(bucketName); + } + @Test public void 이미지_업로드_테스트() throws Exception { //given