diff --git a/src/main/java/com/back/domain/board/post/controller/docs/PostControllerDocs.java b/src/main/java/com/back/domain/board/post/controller/docs/PostControllerDocs.java index 90acf090..aac54e7f 100644 --- a/src/main/java/com/back/domain/board/post/controller/docs/PostControllerDocs.java +++ b/src/main/java/com/back/domain/board/post/controller/docs/PostControllerDocs.java @@ -55,6 +55,10 @@ public interface PostControllerDocs { { "id": 1, "name": "공지사항" }, { "id": 2, "name": "자유게시판" } ], + "images": [ + { "id": 11, "url": "https://example.com/image1.png" }, + { "id": 12, "url": "https://example.com/image2.png" } + ], "createdAt": "2025-09-22T10:30:00", "updatedAt": "2025-09-22T10:30:00" } @@ -112,7 +116,7 @@ public interface PostControllerDocs { ), @ApiResponse( responseCode = "404", - description = "존재하지 않는 사용자 또는 카테고리", + description = "존재하지 않는 리소스 (사용자, 카테고리, 파일)", content = @Content( mediaType = "application/json", examples = { @@ -131,6 +135,14 @@ public interface PostControllerDocs { "message": "존재하지 않는 카테고리입니다.", "data": null } + """), + @ExampleObject(name = "존재하지 않는 파일(이미지)", value = """ + { + "success": false, + "code": "FILE_004", + "message": "파일 정보를 찾을 수 없습니다.", + "data": null + } """) } ) @@ -252,7 +264,11 @@ ResponseEntity>> getPosts( "message": "게시글이 조회되었습니다.", "data": { "postId": 101, - "author": { "id": 5, "nickname": "홍길동", "profileImageUrl": null }, + "author": { + "id": 5, + "nickname": "홍길동", + "profileImageUrl": null + }, "title": "첫 번째 게시글", "content": "안녕하세요, 첫 글입니다!", "thumbnailUrl": null, @@ -260,6 +276,10 @@ ResponseEntity>> getPosts( { "id": 1, "name": "공지사항" }, { "id": 2, "name": "자유게시판" } ], + "images": [ + { "id": 11, "url": "https://example.com/image1.png" }, + { "id": 12, "url": "https://example.com/image2.png" } + ], "likeCount": 10, "bookmarkCount": 2, "commentCount": 3, @@ -337,8 +357,12 @@ ResponseEntity> getPost( { "id": 1, "name": "공지사항" }, { "id": 2, "name": "자유게시판" } ], + "images": [ + { "id": 11, "url": "https://example.com/image1.png" }, + { "id": 12, "url": "https://example.com/image2.png" } + ], "createdAt": "2025-09-22T10:30:00", - "updatedAt": "2025-09-22T10:30:00" + "updatedAt": "2025-09-22T10:45:00" } } """) @@ -409,7 +433,7 @@ ResponseEntity> getPost( ), @ApiResponse( responseCode = "404", - description = "존재하지 않는 사용자/게시글/카테고리", + description = "존재하지 않는 리소스 (사용자, 게시글, 카테고리, 파일)", content = @Content( mediaType = "application/json", examples = { @@ -436,6 +460,14 @@ ResponseEntity> getPost( "message": "존재하지 않는 카테고리입니다.", "data": null } + """), + @ExampleObject(name = "존재하지 않는 파일(이미지)", value = """ + { + "success": false, + "code": "FILE_004", + "message": "파일 정보를 찾을 수 없습니다.", + "data": null + } """) } ) diff --git a/src/main/java/com/back/domain/board/post/dto/ImageResponse.java b/src/main/java/com/back/domain/board/post/dto/ImageResponse.java new file mode 100644 index 00000000..748af830 --- /dev/null +++ b/src/main/java/com/back/domain/board/post/dto/ImageResponse.java @@ -0,0 +1,21 @@ +package com.back.domain.board.post.dto; + +import com.back.domain.file.entity.FileAttachment; + +/** + * 이미지 응답 DTO + * + * @param id 이미지 ID + * @param url 이미지 URL + */ +public record ImageResponse( + Long id, + String url +) { + public static ImageResponse from(FileAttachment fileAttachment) { + return new ImageResponse( + fileAttachment.getId(), + fileAttachment.getPublicURL() + ); + } +} diff --git a/src/main/java/com/back/domain/board/post/dto/PostDetailResponse.java b/src/main/java/com/back/domain/board/post/dto/PostDetailResponse.java index e9c66150..5dc5f56d 100644 --- a/src/main/java/com/back/domain/board/post/dto/PostDetailResponse.java +++ b/src/main/java/com/back/domain/board/post/dto/PostDetailResponse.java @@ -2,6 +2,7 @@ import com.back.domain.board.common.dto.AuthorResponse; import com.back.domain.board.post.entity.Post; +import com.back.domain.file.entity.FileAttachment; import java.time.LocalDateTime; import java.util.List; @@ -15,6 +16,7 @@ * @param content 게시글 내용 * @param thumbnailUrl 썸네일 URL * @param categories 게시글 카테고리 목록 + * @param images 첨부된 이미지 목록 * @param likeCount 좋아요 수 * @param bookmarkCount 북마크 수 * @param commentCount 댓글 수 @@ -30,6 +32,7 @@ public record PostDetailResponse( String content, String thumbnailUrl, List categories, + List images, long likeCount, long bookmarkCount, long commentCount, @@ -38,11 +41,11 @@ public record PostDetailResponse( LocalDateTime createdAt, LocalDateTime updatedAt ) { - public static PostDetailResponse from(Post post) { - return from(post, false, false); + public static PostDetailResponse from(Post post, List attachments) { + return from(post, attachments, false, false); } - public static PostDetailResponse from(Post post, boolean likedByMe, boolean bookmarkedByMe) { + public static PostDetailResponse from(Post post, List attachments, boolean likedByMe, boolean bookmarkedByMe) { return new PostDetailResponse( post.getId(), AuthorResponse.from(post.getUser()), @@ -52,6 +55,9 @@ public static PostDetailResponse from(Post post, boolean likedByMe, boolean book post.getCategories().stream() .map(CategoryResponse::from) .toList(), + attachments.stream() + .map(ImageResponse::from) + .toList(), post.getPostLikes().size(), post.getPostBookmarks().size(), post.getComments().size(), diff --git a/src/main/java/com/back/domain/board/post/dto/PostRequest.java b/src/main/java/com/back/domain/board/post/dto/PostRequest.java index 266ef20b..8227379c 100644 --- a/src/main/java/com/back/domain/board/post/dto/PostRequest.java +++ b/src/main/java/com/back/domain/board/post/dto/PostRequest.java @@ -11,10 +11,12 @@ * @param content 게시글 내용 * @param thumbnailUrl 썸네일 URL * @param categoryIds 카테고리 ID 리스트 + * @param imageIds 이미지 ID 리스트 */ public record PostRequest( @NotBlank String title, @NotBlank String content, String thumbnailUrl, - List categoryIds + List categoryIds, + List imageIds ) {} \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/post/dto/PostResponse.java b/src/main/java/com/back/domain/board/post/dto/PostResponse.java index 484aafcb..bfce641a 100644 --- a/src/main/java/com/back/domain/board/post/dto/PostResponse.java +++ b/src/main/java/com/back/domain/board/post/dto/PostResponse.java @@ -2,6 +2,8 @@ import com.back.domain.board.common.dto.AuthorResponse; import com.back.domain.board.post.entity.Post; +import com.back.domain.file.entity.FileAttachment; + import java.time.LocalDateTime; import java.util.List; @@ -14,6 +16,7 @@ * @param content 게시글 내용 * @param thumbnailUrl 썸네일 URL * @param categories 게시글 카테고리 목록 + * @param images 첨부된 이미지 목록 * @param createdAt 게시글 생성 일시 * @param updatedAt 게시글 수정 일시 */ @@ -24,10 +27,11 @@ public record PostResponse( String content, String thumbnailUrl, List categories, + List images, LocalDateTime createdAt, LocalDateTime updatedAt ) { - public static PostResponse from(Post post) { + public static PostResponse from(Post post, List attachments) { return new PostResponse( post.getId(), AuthorResponse.from(post.getUser()), @@ -37,6 +41,9 @@ public static PostResponse from(Post post) { post.getCategories().stream() .map(CategoryResponse::from) .toList(), + attachments.stream() + .map(ImageResponse::from) + .toList(), post.getCreatedAt(), post.getUpdatedAt() ); diff --git a/src/main/java/com/back/domain/board/post/service/PostService.java b/src/main/java/com/back/domain/board/post/service/PostService.java index 0efdb232..0f4fb0cd 100644 --- a/src/main/java/com/back/domain/board/post/service/PostService.java +++ b/src/main/java/com/back/domain/board/post/service/PostService.java @@ -7,10 +7,16 @@ import com.back.domain.board.post.dto.PostListResponse; import com.back.domain.board.post.dto.PostRequest; import com.back.domain.board.post.dto.PostResponse; +import com.back.domain.board.post.entity.PostCategoryMapping; import com.back.domain.board.post.repository.PostBookmarkRepository; import com.back.domain.board.post.repository.PostCategoryRepository; import com.back.domain.board.post.repository.PostLikeRepository; import com.back.domain.board.post.repository.PostRepository; +import com.back.domain.file.entity.AttachmentMapping; +import com.back.domain.file.entity.EntityType; +import com.back.domain.file.entity.FileAttachment; +import com.back.domain.file.repository.AttachmentMappingRepository; +import com.back.domain.file.repository.FileAttachmentRepository; import com.back.domain.user.common.entity.User; import com.back.domain.user.common.repository.UserRepository; import com.back.global.exception.CustomException; @@ -32,6 +38,8 @@ public class PostService { private final PostBookmarkRepository postBookmarkRepository; private final UserRepository userRepository; private final PostCategoryRepository postCategoryRepository; + private final FileAttachmentRepository fileAttachmentRepository; + private final AttachmentMappingRepository attachmentMappingRepository; /** * 게시글 생성 서비스 @@ -47,15 +55,24 @@ public PostResponse createPost(PostRequest request, Long userId) { // Post 생성 Post post = new Post(user, request.title(), request.content(), request.thumbnailUrl()); - Post saved = postRepository.save(post); + postRepository.save(post); // Category 매핑 if (request.categoryIds() != null) { List categories = validateAndFindCategories(request.categoryIds()); - saved.updateCategories(categories); + categories.forEach(category -> new PostCategoryMapping(post, category)); } - return PostResponse.from(saved); + // AttachmentMapping 매핑 + List attachments = List.of(); + if (request.imageIds() != null && !request.imageIds().isEmpty()) { + attachments = validateAndFindAttachments(request.imageIds()); + for (FileAttachment attachment : attachments) { + attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId())); + } + } + + return PostResponse.from(post, attachments); } /** @@ -86,15 +103,22 @@ public PostDetailResponse getPost(Long postId, Long userId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + // 첨부파일 조회 + List attachments = attachmentMappingRepository + .findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()) + .stream() + .map(AttachmentMapping::getFileAttachment) + .toList(); + // 로그인 사용자 추가 데이터 설정 (좋아요, 북마크 여부) if (userId != null) { boolean likedByMe = postLikeRepository.existsByUserIdAndPostId(userId, post.getId()); boolean bookmarkedByMe = postBookmarkRepository.existsByUserIdAndPostId(userId, post.getId()); - return PostDetailResponse.from(post, likedByMe, bookmarkedByMe); + return PostDetailResponse.from(post, attachments, likedByMe, bookmarkedByMe); } // 비로그인 사용자는 기본 응답 반환 - return PostDetailResponse.from(post); + return PostDetailResponse.from(post, attachments); } /** @@ -126,7 +150,17 @@ public PostResponse updatePost(Long postId, PostRequest request, Long userId) { List categories = validateAndFindCategories(request.categoryIds()); post.updateCategories(categories); - return PostResponse.from(post); + // AttachmentMapping 매핑 + attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); + List attachments = List.of(); + if (request.imageIds() != null && !request.imageIds().isEmpty()) { + attachments = validateAndFindAttachments(request.imageIds()); + attachments.forEach(attachment -> + attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId())) + ); + } + + return PostResponse.from(post, attachments); } /** @@ -149,6 +183,9 @@ public void deletePost(Long postId, Long userId) { throw new CustomException(ErrorCode.POST_NO_PERMISSION); } + // AttachmentMapping 매핑 제거 + attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); + // Post 삭제 post.remove(); postRepository.delete(post); @@ -158,11 +195,19 @@ public void deletePost(Long postId, Long userId) { * 카테고리 ID 유효성 검증 및 조회 */ private List validateAndFindCategories(List categoryIds) { - List categories = postCategoryRepository.findAllById(categoryIds); + return categoryIds.stream() + .map(id -> postCategoryRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.CATEGORY_NOT_FOUND))) + .toList(); + } - if (categories.size() != categoryIds.size()) { - throw new CustomException(ErrorCode.CATEGORY_NOT_FOUND); - } - return categories; + /** + * 첨부 파일 ID 유효성 검증 및 조회 + */ + private List validateAndFindAttachments(List imageIds) { + return imageIds.stream() + .map(id -> fileAttachmentRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND))) + .toList(); } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/board/post/controller/PostControllerTest.java b/src/test/java/com/back/domain/board/post/controller/PostControllerTest.java index 920cb262..f091ba53 100644 --- a/src/test/java/com/back/domain/board/post/controller/PostControllerTest.java +++ b/src/test/java/com/back/domain/board/post/controller/PostControllerTest.java @@ -6,6 +6,8 @@ import com.back.domain.board.post.enums.CategoryType; import com.back.domain.board.post.repository.PostCategoryRepository; import com.back.domain.board.post.repository.PostRepository; +import com.back.domain.file.entity.FileAttachment; +import com.back.domain.file.repository.FileAttachmentRepository; import com.back.domain.user.common.entity.User; import com.back.domain.user.common.entity.UserProfile; import com.back.domain.user.common.enums.UserStatus; @@ -18,6 +20,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @@ -49,6 +52,9 @@ class PostControllerTest { @Autowired private PostCategoryRepository postCategoryRepository; + @Autowired + private FileAttachmentRepository fileAttachmentRepository; + @Autowired private TestJwtTokenProvider testJwtTokenProvider; @@ -82,7 +88,19 @@ void createPost_success() throws Exception { PostCategory c2 = new PostCategory("자유게시판", CategoryType.SUBJECT); postCategoryRepository.save(c2); - PostRequest request = new PostRequest("첫 번째 게시글", "안녕하세요, 첫 글입니다!", null, List.of(c1.getId(), c2.getId())); + // 이미지 등록 + MockMultipartFile file = new MockMultipartFile("file", "thumb.png", "image/png", "dummy".getBytes()); + FileAttachment attachment = new FileAttachment("stored_thumb.png", file, user, "https://cdn.example.com/thumb.png"); + fileAttachmentRepository.save(attachment); + + // 요청 구성 + PostRequest request = new PostRequest( + "첫 번째 게시글", + "안녕하세요, 첫 글입니다!", + null, + List.of(c1.getId(), c2.getId()), + List.of(attachment.getId()) + ); // when ResultActions resultActions = mvc.perform( @@ -99,7 +117,9 @@ void createPost_success() throws Exception { .andExpect(jsonPath("$.code").value("SUCCESS_200")) .andExpect(jsonPath("$.data.title").value("첫 번째 게시글")) .andExpect(jsonPath("$.data.author.nickname").value("홍길동")) - .andExpect(jsonPath("$.data.categories.length()").value(2)); + .andExpect(jsonPath("$.data.categories.length()").value(2)) + .andExpect(jsonPath("$.data.images.length()").value(1)) + .andExpect(jsonPath("$.data.images[0].url").value("https://cdn.example.com/thumb.png")); } @Test @@ -108,7 +128,7 @@ void createPost_userNotFound() throws Exception { // given: 토큰만 발급(실제 DB엔 없음) String fakeToken = testJwtTokenProvider.createAccessToken(999L, "ghost", "USER"); - PostRequest request = new PostRequest("제목", "내용", null, null); + PostRequest request = new PostRequest("제목", "내용", null, List.of(), List.of()); // when & then mvc.perform(post("/api/posts") @@ -133,7 +153,7 @@ void createPost_categoryNotFound() throws Exception { String accessToken = generateAccessToken(user); // 존재하지 않는 카테고리 ID - PostRequest request = new PostRequest("제목", "내용", null, List.of(999L)); + PostRequest request = new PostRequest("제목", "내용", null, List.of(999L), List.of()); // when & then mvc.perform(post("/api/posts") @@ -146,6 +166,35 @@ void createPost_categoryNotFound() throws Exception { .andExpect(jsonPath("$.message").value("존재하지 않는 카테고리입니다.")); } + @Test + @DisplayName("게시글 생성 실패 - 존재하지 않는 이미지 → 404 Not Found") + void createPost_fail_imageNotFound() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + PostCategory category = new PostCategory("공지사항", CategoryType.SUBJECT); + postCategoryRepository.save(category); + + // 존재하지 않는 이미지 ID + PostRequest request = new PostRequest("이미지 테스트", "없는 이미지입니다", null, + List.of(category.getId()), List.of(999L)); + + // when & then + mvc.perform(post("/api/posts") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("FILE_004")) + .andExpect(jsonPath("$.message").value("파일 정보를 찾을 수 없습니다.")); + } + @Test @DisplayName("게시글 생성 실패 - 잘못된 요청(필드 누락) → 400 Bad Request") void createPost_badRequest() throws Exception { @@ -179,7 +228,7 @@ void createPost_badRequest() throws Exception { @DisplayName("게시글 생성 실패 - 토큰 없음 → 401 Unauthorized") void createPost_noToken() throws Exception { // given - PostRequest request = new PostRequest("제목", "내용", null, null); + PostRequest request = new PostRequest("제목", "내용", null, List.of(), List.of()); // when & then mvc.perform(post("/api/posts") @@ -292,7 +341,17 @@ void updatePost_success() throws Exception { PostCategory c2 = new PostCategory("자유게시판", CategoryType.SUBJECT); postCategoryRepository.save(c2); - PostRequest request = new PostRequest("수정된 게시글", "안녕하세요, 수정했습니다!", null, List.of(c1.getId(), c2.getId())); + MockMultipartFile file = new MockMultipartFile("file", "thumb.png", "image/png", "dummy".getBytes()); + FileAttachment attachment = new FileAttachment("stored_thumb.png", file, user, "https://cdn.example.com/thumb.png"); + fileAttachmentRepository.save(attachment); + + PostRequest request = new PostRequest( + "수정된 게시글", + "안녕하세요, 수정했습니다!", + null, + List.of(c1.getId(), c2.getId()), + List.of(attachment.getId()) + ); // when & then mvc.perform(put("/api/posts/{postId}", post.getId()) @@ -304,7 +363,9 @@ void updatePost_success() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.code").value("SUCCESS_200")) .andExpect(jsonPath("$.data.title").value("수정된 게시글")) - .andExpect(jsonPath("$.data.categories.length()").value(2)); + .andExpect(jsonPath("$.data.categories.length()").value(2)) + .andExpect(jsonPath("$.data.images.length()").value(1)) + .andExpect(jsonPath("$.data.images[0].url").value("https://cdn.example.com/thumb.png")); } @Test @@ -318,7 +379,7 @@ void updatePost_fail_notFound() throws Exception { String accessToken = generateAccessToken(user); - PostRequest request = new PostRequest("수정된 제목", "내용", null, List.of()); + PostRequest request = new PostRequest("수정된 제목", "내용", null, List.of(), List.of()); // when & then mvc.perform(put("/api/posts/{postId}", 999L) @@ -354,7 +415,7 @@ void updatePost_fail_noPermission() throws Exception { String accessToken = generateAccessToken(another); - PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(c1.getId())); + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(c1.getId()), List.of()); // when & then mvc.perform(put("/api/posts/{postId}", post.getId()) @@ -386,7 +447,7 @@ void updatePost_fail_categoryNotFound() throws Exception { String accessToken = generateAccessToken(user); // 존재하지 않는 카테고리 ID - PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(999L)); + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(999L), List.of()); // when & then mvc.perform(put("/api/posts/{postId}", post.getId()) @@ -399,6 +460,49 @@ void updatePost_fail_categoryNotFound() throws Exception { .andExpect(jsonPath("$.message").value("존재하지 않는 카테고리입니다.")); } + @Test + @DisplayName("게시글 수정 실패 - 존재하지 않는 이미지 → 404 Not Found") + void updatePost_fail_imageNotFound() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "홍길동", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + // 카테고리 등록 + PostCategory c1 = new PostCategory("공지사항", CategoryType.SUBJECT); + postCategoryRepository.save(c1); + + // 게시글 생성 + Post post = new Post(user, "수정 테스트 제목", "수정 테스트 내용", null); + postRepository.save(post); + + // 존재하지 않는 이미지 ID + Long invalidImageId = 999L; + + // 수정 요청 + PostRequest request = new PostRequest( + "수정된 제목", + "수정된 내용", + null, + List.of(c1.getId()), + List.of(invalidImageId) + ); + + // when & then + mvc.perform(put("/api/posts/{postId}", post.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value("FILE_004")) + .andExpect(jsonPath("$.message").value("파일 정보를 찾을 수 없습니다.")); + } + @Test @DisplayName("게시글 수정 실패 - 잘못된 요청(필드 누락) → 400 Bad Request") void updatePost_fail_badRequest() throws Exception { @@ -431,7 +535,7 @@ void updatePost_fail_badRequest() throws Exception { @DisplayName("게시글 수정 실패 - 인증 없음 → 401 Unauthorized") void updatePost_fail_unauthorized() throws Exception { // given - PostRequest request = new PostRequest("제목", "내용", null, List.of()); + PostRequest request = new PostRequest("제목", "내용", null, List.of(), List.of()); // when & then mvc.perform(put("/api/posts/{postId}", 1L) diff --git a/src/test/java/com/back/domain/board/post/service/PostServiceTest.java b/src/test/java/com/back/domain/board/post/service/PostServiceTest.java index e07a1289..cce05313 100644 --- a/src/test/java/com/back/domain/board/post/service/PostServiceTest.java +++ b/src/test/java/com/back/domain/board/post/service/PostServiceTest.java @@ -10,6 +10,11 @@ import com.back.domain.board.post.enums.CategoryType; import com.back.domain.board.post.repository.PostCategoryRepository; import com.back.domain.board.post.repository.PostRepository; +import com.back.domain.file.entity.AttachmentMapping; +import com.back.domain.file.entity.EntityType; +import com.back.domain.file.entity.FileAttachment; +import com.back.domain.file.repository.AttachmentMappingRepository; +import com.back.domain.file.repository.FileAttachmentRepository; import com.back.domain.user.common.entity.User; import com.back.domain.user.common.entity.UserProfile; import com.back.domain.user.common.enums.UserStatus; @@ -23,6 +28,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; @@ -47,12 +53,18 @@ class PostServiceTest { @Autowired private PostCategoryRepository postCategoryRepository; + @Autowired + private FileAttachmentRepository fileAttachmentRepository; + + @Autowired + private AttachmentMappingRepository attachmentMappingRepository; + // ====================== 게시글 생성 테스트 ====================== @Test - @DisplayName("게시글 생성 성공 - 카테고리 포함") - void createPost_success_withCategories() { - // given: 유저 + 카테고리 저장 + @DisplayName("게시글 생성 성공 - 카테고리 + 이미지 포함") + void createPost_success_withCategoriesAndImages() { + // given User user = User.createUser("writer", "writer@example.com", "encodedPwd"); user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); user.setUserStatus(UserStatus.ACTIVE); @@ -61,7 +73,20 @@ void createPost_success_withCategories() { PostCategory category = new PostCategory("공지", CategoryType.SUBJECT); postCategoryRepository.save(category); - PostRequest request = new PostRequest("제목", "내용", null, List.of(category.getId())); + // MockMultipartFile로 가짜 이미지 생성 + MockMultipartFile imgFile1 = new MockMultipartFile("file", "image1.png", "image/png", "dummy".getBytes()); + MockMultipartFile imgFile2 = new MockMultipartFile("file", "image2.png", "image/png", "dummy".getBytes()); + + // 파일(이미지) 생성 + FileAttachment img1 = new FileAttachment("stored_image1.png", imgFile1, user, "https://cdn.example.com/image1.png"); + FileAttachment img2 = new FileAttachment("stored_image2.png", imgFile2, user, "https://cdn.example.com/image2.png"); + fileAttachmentRepository.saveAll(List.of(img1, img2)); + + PostRequest request = new PostRequest( + "제목", "내용", null, + List.of(category.getId()), + List.of(img1.getId(), img2.getId()) + ); // when PostResponse response = postService.createPost(request, user.getId()); @@ -72,13 +97,15 @@ void createPost_success_withCategories() { assertThat(response.author().nickname()).isEqualTo("작성자"); assertThat(response.categories()).hasSize(1); assertThat(response.categories().getFirst().name()).isEqualTo("공지"); + assertThat(response.images()).hasSize(2); + assertThat(response.images().getFirst().id()).isEqualTo(img1.getId()); } @Test @DisplayName("게시글 생성 실패 - 존재하지 않는 유저") void createPost_fail_userNotFound() { // given - PostRequest request = new PostRequest("제목", "내용", null, null); + PostRequest request = new PostRequest("제목", "내용", null, null, null); // when & then assertThatThrownBy(() -> postService.createPost(request, 999L)) @@ -96,7 +123,7 @@ void createPost_fail_categoryNotFound() { userRepository.save(user); // 실제 저장 안 된 카테고리 ID 요청 - PostRequest request = new PostRequest("제목", "내용", null, List.of(100L, 200L)); + PostRequest request = new PostRequest("제목", "내용", null, List.of(100L, 200L), null); // when & then assertThatThrownBy(() -> postService.createPost(request, user.getId())) @@ -104,6 +131,27 @@ void createPost_fail_categoryNotFound() { .hasMessage(ErrorCode.CATEGORY_NOT_FOUND.getMessage()); } + @Test + @DisplayName("게시글 생성 실패 - 존재하지 않는 파일 ID 포함") + void createPost_fail_fileNotFound() { + // given + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + PostRequest request = new PostRequest( + "제목", "내용", null, + null, // 카테고리 없음 + List.of(999L) // 존재하지 않는 파일 ID + ); + + // when & then + assertThatThrownBy(() -> postService.createPost(request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.FILE_NOT_FOUND.getMessage()); + } + // ====================== 게시글 조회 테스트 ====================== @@ -181,23 +229,43 @@ void getPost_fail_postNotFound() { // ====================== 게시글 수정 테스트 ====================== @Test - @DisplayName("게시글 수정 성공 - 작성자 본인") - void updatePost_success() { + @DisplayName("게시글 수정 성공 - 이미지 교체") + void updatePost_success_withNewImages() { // given User user = User.createUser("writer", "writer@example.com", "encodedPwd"); user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); user.setUserStatus(UserStatus.ACTIVE); userRepository.save(user); + // 카테고리: 기존(공지) / 신규(자유) PostCategory oldCategory = new PostCategory("공지", CategoryType.SUBJECT); PostCategory newCategory = new PostCategory("자유", CategoryType.SUBJECT); postCategoryRepository.saveAll(List.of(oldCategory, newCategory)); + // 기존 이미지 + MockMultipartFile oldFile = new MockMultipartFile("file", "old.png", "image/png", "dummy".getBytes()); + FileAttachment imgOld = new FileAttachment("stored_old.png", oldFile, user, "https://cdn.example.com/old.png"); + fileAttachmentRepository.save(imgOld); + + // 게시글 생성 + 기존 카테고리 세팅 Post post = new Post(user, "원래 제목", "원래 내용", null); post.updateCategories(List.of(oldCategory)); postRepository.save(post); - PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(newCategory.getId())); + // 기존 이미지 매핑(다형 매핑 방식으로 직접 저장) + attachmentMappingRepository.save(new AttachmentMapping(imgOld, EntityType.POST, post.getId())); + + // 새 이미지 + MockMultipartFile newFile = new MockMultipartFile("file", "new.png", "image/png", "dummy".getBytes()); + FileAttachment imgNew = new FileAttachment("stored_new.png", newFile, user, "https://cdn.example.com/new.png"); + fileAttachmentRepository.save(imgNew); + + // 수정 요청: 제목/내용 변경 + 카테고리를 '자유'로 교체 + 이미지를 새 것으로 교체 + PostRequest request = new PostRequest( + "수정된 제목", "수정된 내용", null, + List.of(newCategory.getId()), + List.of(imgNew.getId()) + ); // when PostResponse response = postService.updatePost(post.getId(), request, user.getId()); @@ -207,6 +275,8 @@ void updatePost_success() { assertThat(response.content()).isEqualTo("수정된 내용"); assertThat(response.categories()).hasSize(1); assertThat(response.categories().getFirst().name()).isEqualTo("자유"); + assertThat(response.images()).hasSize(1); + assertThat(response.images().getFirst().id()).isEqualTo(imgNew.getId()); } @Test @@ -218,7 +288,7 @@ void updatePost_fail_postNotFound() { user.setUserStatus(UserStatus.ACTIVE); userRepository.save(user); - PostRequest request = new PostRequest("제목", "내용", null, List.of()); + PostRequest request = new PostRequest("제목", "내용", null, List.of(), null); // when & then assertThatThrownBy(() -> postService.updatePost(999L, request, user.getId())) @@ -248,7 +318,7 @@ void updatePost_fail_noPermission() { post.updateCategories(List.of(category)); postRepository.save(post); - PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(category.getId())); + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(category.getId()), null); // when & then assertThatThrownBy(() -> postService.updatePost(post.getId(), request, another.getId())) @@ -273,7 +343,7 @@ void updatePost_fail_categoryNotFound() { postRepository.save(post); // 실제 DB에는 없는 카테고리 ID 전달 - PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(999L)); + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", null, List.of(999L), null); // when & then assertThatThrownBy(() -> postService.updatePost(post.getId(), request, user.getId())) @@ -281,25 +351,69 @@ void updatePost_fail_categoryNotFound() { .hasMessage(ErrorCode.CATEGORY_NOT_FOUND.getMessage()); } + @Test + @DisplayName("게시글 수정 실패 - 존재하지 않는 파일 ID 포함") + void updatePost_fail_fileNotFound() { + // given + User user = User.createUser("writer2", "writer2@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자2", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + PostCategory category = new PostCategory("공지", CategoryType.SUBJECT); + postCategoryRepository.save(category); + + Post post = new Post(user, "원래 제목", "원래 내용", null); + post.updateCategories(List.of(category)); + postRepository.save(post); + + PostRequest request = new PostRequest( + "수정된 제목", "수정된 내용", null, + List.of(category.getId()), + List.of(999L) // 존재하지 않는 파일 + ); + + // when & then + assertThatThrownBy(() -> postService.updatePost(post.getId(), request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.FILE_NOT_FOUND.getMessage()); + } + // ====================== 게시글 삭제 테스트 ====================== @Test - @DisplayName("게시글 삭제 성공 - 작성자 본인") - void deletePost_success() { + @DisplayName("게시글 삭제 성공 - 첨부 이미지 매핑도 함께 삭제") + void deletePost_success_withImages() { // given User user = User.createUser("writer", "writer@example.com", "encodedPwd"); user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); user.setUserStatus(UserStatus.ACTIVE); userRepository.save(user); - Post post = new Post(user, "삭제 대상 제목", "삭제 대상 내용", null); + // 첨부 이미지 + MockMultipartFile imgFile = new MockMultipartFile("file", "del.png", "image/png", "dummy".getBytes()); + FileAttachment img = new FileAttachment("stored_del.png", imgFile, user, "https://cdn.example.com/del.png"); + fileAttachmentRepository.save(img); + + // 게시글 생성 + Post post = new Post(user, "삭제 제목", "삭제 내용", null); postRepository.save(post); + // 이미지 매핑 (EntityType.POST + postId) + attachmentMappingRepository.save(new AttachmentMapping(img, EntityType.POST, post.getId())); + + Long postId = post.getId(); + // when - postService.deletePost(post.getId(), user.getId()); + postService.deletePost(postId, user.getId()); // then - assertThat(postRepository.findById(post.getId())).isEmpty(); + // 게시글 삭제 확인 + assertThat(postRepository.findById(postId)).isEmpty(); + + // 매핑 삭제 확인 + assertThat(attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, postId)) + .isEmpty(); } @Test