From 8d8c1e38ffa7be461fb0d4f4451298ee202bfed4 Mon Sep 17 00:00:00 2001 From: dmori Date: Sun, 1 Feb 2026 00:53:10 +0900 Subject: [PATCH] =?UTF-8?q?improve:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98,=20=EC=84=AC=EB=84=A4=EC=9D=BCURL,?= =?UTF-8?q?=20=EB=B6=81=EB=A7=88=ED=81=AC=20=EC=97=AC=EB=B6=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/activity/dto/BookmarkDto.java | 11 +- .../repository/ScrabPostRepository.java | 3 +- .../service/ActivityQueryService.java | 44 +++- .../ActivityControllerIntegrationTest.java | 23 +- .../service/ActivityQueryServiceTest.java | 227 ++++++++++++++++-- 5 files changed, 283 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/techfork/domain/activity/dto/BookmarkDto.java b/src/main/java/com/techfork/domain/activity/dto/BookmarkDto.java index 3803188..379c0ed 100644 --- a/src/main/java/com/techfork/domain/activity/dto/BookmarkDto.java +++ b/src/main/java/com/techfork/domain/activity/dto/BookmarkDto.java @@ -1,14 +1,23 @@ package com.techfork.domain.activity.dto; +import lombok.Builder; + import java.time.LocalDateTime; +import java.util.List; +@Builder public record BookmarkDto( Long bookmarkId, Long postId, String title, + String shortSummary, String url, String companyName, String logoUrl, - LocalDateTime publishedAt + LocalDateTime publishedAt, + String thumbnailUrl, + Long viewCount, + List keywords, + Boolean isBookmarked ) { } diff --git a/src/main/java/com/techfork/domain/activity/repository/ScrabPostRepository.java b/src/main/java/com/techfork/domain/activity/repository/ScrabPostRepository.java index d3b1684..9984a38 100644 --- a/src/main/java/com/techfork/domain/activity/repository/ScrabPostRepository.java +++ b/src/main/java/com/techfork/domain/activity/repository/ScrabPostRepository.java @@ -16,7 +16,8 @@ public interface ScrabPostRepository extends JpaRepository { @Query(""" SELECT new com.techfork.domain.activity.dto.BookmarkDto( - s.id, p.id, p.title, p.url, t.companyName, t.logoUrl, p.publishedAt + s.id, p.id, p.title, p.shortSummary, p.url, t.companyName, t.logoUrl, + p.publishedAt, p.thumbnailUrl, p.viewCount, null, true ) FROM ScrabPost s JOIN s.post p diff --git a/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java b/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java index f3944cf..c0d0258 100644 --- a/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java +++ b/src/main/java/com/techfork/domain/activity/service/ActivityQueryService.java @@ -4,6 +4,9 @@ import com.techfork.domain.activity.dto.BookmarkDto; import com.techfork.domain.activity.dto.BookmarkListResponse; import com.techfork.domain.activity.repository.ScrabPostRepository; +import com.techfork.domain.post.dto.PostInfoDto; +import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.domain.post.repository.PostKeywordRepository; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.exception.UserErrorCode; import com.techfork.domain.user.repository.UserRepository; @@ -14,7 +17,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.awt.print.Book; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Slf4j @Service @@ -24,6 +30,7 @@ public class ActivityQueryService { private final UserRepository userRepository; private final ScrabPostRepository scrabPostRepository; + private final PostKeywordRepository postKeywordRepository; private final ActivityConverter activityConverter; public BookmarkListResponse getBookmarks(Long userId, Long lastBookmarkId, int size) { @@ -32,7 +39,42 @@ public BookmarkListResponse getBookmarks(Long userId, Long lastBookmarkId, int s PageRequest pageRequest = PageRequest.of(0, size + 1); List bookmarks = scrabPostRepository.findBookmarksWithCursor(user, lastBookmarkId, pageRequest); + List bookmarksWithKeywords = attachKeywordsToPostInfoList(bookmarks); - return activityConverter.toBookmarkListResponse(bookmarks, size); + return activityConverter.toBookmarkListResponse(bookmarksWithKeywords, size); + } + + private List attachKeywordsToPostInfoList(List bookmarks) { + if (bookmarks.isEmpty()) { + return bookmarks; + } + + List postIds = bookmarks.stream() + .map(BookmarkDto::postId) + .toList(); + + Map> keywordMap = postKeywordRepository.findByPostIdIn(postIds) + .stream() + .collect(Collectors.groupingBy( + pk -> pk.getPost().getId(), + Collectors.mapping(PostKeyword::getKeyword, Collectors.toList()) + )); + + return bookmarks.stream() + .map(post -> BookmarkDto.builder() + .bookmarkId(post.bookmarkId()) + .postId(post.postId()) + .title(post.title()) + .shortSummary(post.shortSummary()) + .url(post.url()) + .companyName(post.companyName()) + .logoUrl(post.logoUrl()) + .publishedAt(post.publishedAt()) + .thumbnailUrl(post.thumbnailUrl()) + .viewCount(post.viewCount()) + .keywords(keywordMap.getOrDefault(post.postId(), List.of())) + .isBookmarked(post.isBookmarked()) + .build()) + .toList(); } } diff --git a/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java b/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java index ddd9f04..ab95554 100644 --- a/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/activity/controller/ActivityControllerIntegrationTest.java @@ -101,7 +101,11 @@ void setUp() { .title("테스트 게시글 1") .fullContent("게시글 1의 전체 내용입니다.") .plainContent("게시글 1의 내용") + .summary("게시글 1의 요약") + .shortSummary("게시글 1의 짧은 요약") .company("테스트회사") + .logoUrl("https://test.com/logo.png") + .thumbnailUrl("https://test.com/thumb1.png") .url("https://test.com/post/1") .publishedAt(LocalDateTime.now().minusDays(1)) .crawledAt(LocalDateTime.now()) @@ -113,7 +117,11 @@ void setUp() { .title("테스트 게시글 2") .fullContent("게시글 2의 전체 내용입니다.") .plainContent("게시글 2의 내용") + .summary("게시글 2의 요약") + .shortSummary("게시글 2의 짧은 요약") .company("테스트회사") + .logoUrl("https://test.com/logo.png") + .thumbnailUrl("https://test.com/thumb2.png") .url("https://test.com/post/2") .publishedAt(LocalDateTime.now().minusDays(2)) .crawledAt(LocalDateTime.now()) @@ -413,7 +421,20 @@ void getBookmarks_Success_Multiple() throws Exception { .andExpect(jsonPath("$.isSuccess").value(true)) .andExpect(jsonPath("$.data.bookmarks").isArray()) .andExpect(jsonPath("$.data.bookmarks.length()").value(2)) - .andExpect(jsonPath("$.data.hasNext").value(false)); + .andExpect(jsonPath("$.data.hasNext").value(false)) + // 첫 번째 북마크 DTO 전체 필드 검증 (최신순이므로 bookmark2) + .andExpect(jsonPath("$.data.bookmarks[0].bookmarkId").value(bookmark2.getId())) + .andExpect(jsonPath("$.data.bookmarks[0].postId").value(testPost2.getId())) + .andExpect(jsonPath("$.data.bookmarks[0].title").value("테스트 게시글 2")) + .andExpect(jsonPath("$.data.bookmarks[0].shortSummary").value("게시글 2의 짧은 요약")) + .andExpect(jsonPath("$.data.bookmarks[0].url").value("https://test.com/post/2")) + .andExpect(jsonPath("$.data.bookmarks[0].companyName").value("테스트회사")) + .andExpect(jsonPath("$.data.bookmarks[0].logoUrl").value("https://test.com/logo.png")) + .andExpect(jsonPath("$.data.bookmarks[0].publishedAt").exists()) + .andExpect(jsonPath("$.data.bookmarks[0].thumbnailUrl").value("https://test.com/thumb2.png")) + .andExpect(jsonPath("$.data.bookmarks[0].viewCount").value(0)) + .andExpect(jsonPath("$.data.bookmarks[0].keywords").isArray()) + .andExpect(jsonPath("$.data.bookmarks[0].isBookmarked").value(true)); } @Test diff --git a/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java b/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java index 3185370..91f3879 100644 --- a/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/activity/service/ActivityQueryServiceTest.java @@ -4,10 +4,15 @@ import com.techfork.domain.activity.dto.BookmarkDto; import com.techfork.domain.activity.dto.BookmarkListResponse; import com.techfork.domain.activity.repository.ScrabPostRepository; +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.domain.post.repository.PostKeywordRepository; import com.techfork.domain.user.entity.User; import com.techfork.domain.user.exception.UserErrorCode; import com.techfork.domain.user.repository.UserRepository; import com.techfork.global.exception.GeneralException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,12 +42,105 @@ class ActivityQueryServiceTest { @Mock private ScrabPostRepository scrabPostRepository; + @Mock + private PostKeywordRepository postKeywordRepository; + @Mock private ActivityConverter activityConverter; @InjectMocks private ActivityQueryService activityQueryService; + private User mockUser; + private List mockBookmarksFirstPage; + private List mockBookmarksSecondPage; + + @BeforeEach + void setUp() { + mockUser = mock(User.class); + + mockBookmarksFirstPage = Arrays.asList( + BookmarkDto.builder() + .bookmarkId(3L) + .postId(103L) + .title("게시글3") + .shortSummary("요약3") + .url("https://test.com/3") + .companyName("회사A") + .logoUrl("logo.png") + .publishedAt(LocalDateTime.now()) + .thumbnailUrl("thumb3.png") + .viewCount(100L) + .keywords(List.of()) + .isBookmarked(true) + .build(), + BookmarkDto.builder() + .bookmarkId(2L) + .postId(102L) + .title("게시글2") + .shortSummary("요약2") + .url("https://test.com/2") + .companyName("회사B") + .logoUrl("logo.png") + .publishedAt(LocalDateTime.now()) + .thumbnailUrl("thumb2.png") + .viewCount(200L) + .keywords(List.of()) + .isBookmarked(true) + .build(), + BookmarkDto.builder() + .bookmarkId(1L) + .postId(101L) + .title("게시글1") + .shortSummary("요약1") + .url("https://test.com/1") + .companyName("회사C") + .logoUrl("logo.png") + .publishedAt(LocalDateTime.now()) + .thumbnailUrl("thumb1.png") + .viewCount(300L) + .keywords(List.of()) + .isBookmarked(true) + .build() + ); + + mockBookmarksSecondPage = Arrays.asList( + BookmarkDto.builder() + .bookmarkId(9L) + .postId(109L) + .title("게시글9") + .shortSummary("요약9") + .url("https://test.com/9") + .companyName("회사A") + .logoUrl("logo.png") + .publishedAt(LocalDateTime.now()) + .thumbnailUrl("thumb9.png") + .viewCount(150L) + .keywords(List.of()) + .isBookmarked(true) + .build(), + BookmarkDto.builder() + .bookmarkId(8L) + .postId(108L) + .title("게시글8") + .shortSummary("요약8") + .url("https://test.com/8") + .companyName("회사B") + .logoUrl("logo.png") + .publishedAt(LocalDateTime.now()) + .thumbnailUrl("thumb8.png") + .viewCount(250L) + .keywords(List.of()) + .isBookmarked(true) + .build() + ); + } + + @AfterAll + static void tearDown() { + // 테스트 종료 후 정리 작업 + } + @Test @DisplayName("북마크 목록 조회 성공 - 첫 페이지") void getBookmarks_Success_FirstPage() { @@ -51,19 +149,13 @@ void getBookmarks_Success_FirstPage() { Long lastBookmarkId = null; int size = 20; - User mockUser = mock(User.class); given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); - - List mockBookmarks = Arrays.asList( - new BookmarkDto(3L, 103L, "게시글3", "https://test.com/3", "회사A", "logo.png", LocalDateTime.now()), - new BookmarkDto(2L, 102L, "게시글2", "https://test.com/2", "회사B", "logo.png", LocalDateTime.now()), - new BookmarkDto(1L, 101L, "게시글1", "https://test.com/1", "회사C", "logo.png", LocalDateTime.now()) - ); given(scrabPostRepository.findBookmarksWithCursor(eq(mockUser), eq(lastBookmarkId), any(PageRequest.class))) - .willReturn(mockBookmarks); + .willReturn(mockBookmarksFirstPage); + given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - BookmarkListResponse expectedResponse = new BookmarkListResponse(mockBookmarks, 2L, true); - given(activityConverter.toBookmarkListResponse(mockBookmarks, size)).willReturn(expectedResponse); + BookmarkListResponse expectedResponse = new BookmarkListResponse(mockBookmarksFirstPage, 2L, true); + given(activityConverter.toBookmarkListResponse(any(), eq(size))).willReturn(expectedResponse); // When BookmarkListResponse response = activityQueryService.getBookmarks(userId, lastBookmarkId, size); @@ -76,7 +168,7 @@ void getBookmarks_Success_FirstPage() { verify(userRepository, times(1)).findById(userId); verify(scrabPostRepository, times(1)).findBookmarksWithCursor(eq(mockUser), eq(lastBookmarkId), any(PageRequest.class)); - verify(activityConverter, times(1)).toBookmarkListResponse(mockBookmarks, size); + verify(activityConverter, times(1)).toBookmarkListResponse(mockBookmarksFirstPage, size); } @Test @@ -87,18 +179,13 @@ void getBookmarks_Success_WithCursor() { Long lastBookmarkId = 10L; int size = 20; - User mockUser = mock(User.class); given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); - - List mockBookmarks = Arrays.asList( - new BookmarkDto(9L, 109L, "게시글9", "https://test.com/9", "회사A", "logo.png", LocalDateTime.now()), - new BookmarkDto(8L, 108L, "게시글8", "https://test.com/8", "회사B", "logo.png", LocalDateTime.now()) - ); given(scrabPostRepository.findBookmarksWithCursor(eq(mockUser), eq(lastBookmarkId), any(PageRequest.class))) - .willReturn(mockBookmarks); + .willReturn(mockBookmarksSecondPage); + given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - BookmarkListResponse expectedResponse = new BookmarkListResponse(mockBookmarks, null, false); - given(activityConverter.toBookmarkListResponse(mockBookmarks, size)).willReturn(expectedResponse); + BookmarkListResponse expectedResponse = new BookmarkListResponse(mockBookmarksSecondPage, null, false); + given(activityConverter.toBookmarkListResponse(any(), eq(size))).willReturn(expectedResponse); // When BookmarkListResponse response = activityQueryService.getBookmarks(userId, lastBookmarkId, size); @@ -139,7 +226,6 @@ void getBookmarks_Success_EmptyList() { Long lastBookmarkId = null; int size = 20; - User mockUser = mock(User.class); given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); List emptyBookmarks = List.of(); @@ -160,4 +246,103 @@ void getBookmarks_Success_EmptyList() { verify(scrabPostRepository, times(1)).findBookmarksWithCursor(eq(mockUser), eq(lastBookmarkId), any(PageRequest.class)); } + + @Test + @DisplayName("북마크 목록 조회 성공 - 키워드 포함") + void getBookmarks_Success_WithKeywords() { + // Given + Long userId = 1L; + Long lastBookmarkId = null; + int size = 20; + + given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); + given(scrabPostRepository.findBookmarksWithCursor(eq(mockUser), eq(lastBookmarkId), any(PageRequest.class))) + .willReturn(mockBookmarksFirstPage); + + // PostKeyword 목 객체 생성 + Post mockPost1 = mock(Post.class); + Post mockPost2 = mock(Post.class); + + given(mockPost1.getId()).willReturn(103L); + given(mockPost2.getId()).willReturn(102L); + + PostKeyword keyword1 = PostKeyword.builder() + .keyword("Java") + .post(mockPost1) + .build(); + PostKeyword keyword2 = PostKeyword.builder() + .keyword("Spring") + .post(mockPost1) + .build(); + PostKeyword keyword3 = PostKeyword.builder() + .keyword("Kotlin") + .post(mockPost2) + .build(); + + given(postKeywordRepository.findByPostIdIn(any())) + .willReturn(List.of(keyword1, keyword2, keyword3)); + + List expectedBookmarksWithKeywords = Arrays.asList( + BookmarkDto.builder() + .bookmarkId(3L) + .postId(103L) + .title("게시글3") + .shortSummary("요약3") + .url("https://test.com/3") + .companyName("회사A") + .logoUrl("logo.png") + .publishedAt(mockBookmarksFirstPage.get(0).publishedAt()) + .thumbnailUrl("thumb3.png") + .viewCount(100L) + .keywords(List.of("Java", "Spring")) + .isBookmarked(true) + .build(), + BookmarkDto.builder() + .bookmarkId(2L) + .postId(102L) + .title("게시글2") + .shortSummary("요약2") + .url("https://test.com/2") + .companyName("회사B") + .logoUrl("logo.png") + .publishedAt(mockBookmarksFirstPage.get(1).publishedAt()) + .thumbnailUrl("thumb2.png") + .viewCount(200L) + .keywords(List.of("Kotlin")) + .isBookmarked(true) + .build(), + BookmarkDto.builder() + .bookmarkId(1L) + .postId(101L) + .title("게시글1") + .shortSummary("요약1") + .url("https://test.com/1") + .companyName("회사C") + .logoUrl("logo.png") + .publishedAt(mockBookmarksFirstPage.get(2).publishedAt()) + .thumbnailUrl("thumb1.png") + .viewCount(300L) + .keywords(List.of()) + .isBookmarked(true) + .build() + ); + + BookmarkListResponse expectedResponse = new BookmarkListResponse(expectedBookmarksWithKeywords, 2L, true); + given(activityConverter.toBookmarkListResponse(any(), eq(size))).willReturn(expectedResponse); + + // When + BookmarkListResponse response = activityQueryService.getBookmarks(userId, lastBookmarkId, size); + + // Then + assertThat(response).isNotNull(); + assertThat(response.bookmarks()).hasSize(3); + assertThat(response.bookmarks().get(0).keywords()).containsExactlyInAnyOrder("Java", "Spring"); + assertThat(response.bookmarks().get(1).keywords()).containsExactly("Kotlin"); + assertThat(response.bookmarks().get(2).keywords()).isEmpty(); + + verify(userRepository, times(1)).findById(userId); + verify(scrabPostRepository, times(1)).findBookmarksWithCursor(eq(mockUser), eq(lastBookmarkId), any(PageRequest.class)); + verify(postKeywordRepository, times(1)).findByPostIdIn(any()); + verify(activityConverter, times(1)).toBookmarkListResponse(any(), eq(size)); + } }