From 7a270b53398fb0af8e686489643a77984ea9f5c8 Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Fri, 24 Apr 2026 19:53:11 +0900 Subject: [PATCH] =?UTF-8?q?DP-410:=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20(GET=20/users/m?= =?UTF-8?q?e/scraps)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/controller/ScrapController.java | 42 ++++ .../domain/content/dto/ScrapItemResponse.java | 33 +++ .../domain/content/dto/ScrapListResponse.java | 11 + .../content/repository/ScrapRepository.java | 16 ++ .../domain/content/service/ScrapService.java | 73 +++++++ .../controller/ScrapControllerTest.java | 103 +++++++++ .../content/service/ScrapServiceTest.java | 198 ++++++++++++++++++ 7 files changed, 476 insertions(+) create mode 100644 src/main/java/com/devpick/domain/content/controller/ScrapController.java create mode 100644 src/main/java/com/devpick/domain/content/dto/ScrapItemResponse.java create mode 100644 src/main/java/com/devpick/domain/content/dto/ScrapListResponse.java create mode 100644 src/main/java/com/devpick/domain/content/service/ScrapService.java create mode 100644 src/test/java/com/devpick/domain/content/controller/ScrapControllerTest.java create mode 100644 src/test/java/com/devpick/domain/content/service/ScrapServiceTest.java diff --git a/src/main/java/com/devpick/domain/content/controller/ScrapController.java b/src/main/java/com/devpick/domain/content/controller/ScrapController.java new file mode 100644 index 0000000..e3d5392 --- /dev/null +++ b/src/main/java/com/devpick/domain/content/controller/ScrapController.java @@ -0,0 +1,42 @@ +package com.devpick.domain.content.controller; + +import com.devpick.domain.content.dto.ScrapListResponse; +import com.devpick.domain.content.service.ScrapService; +import com.devpick.global.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@Tag(name = "Scrap", description = "스크랩 목록 조회") +@RestController +@RequestMapping("/users/me/scraps") +@RequiredArgsConstructor +public class ScrapController { + + private final ScrapService scrapService; + + @Operation(summary = "스크랩 목록 조회", description = "인증된 사용자의 스크랩 목록을 페이징/검색/정렬하여 반환합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 필요") + }) + @GetMapping + public ApiResponse getScraps( + @AuthenticationPrincipal UUID userId, + @Parameter(description = "검색어 (title, sourceName, summary 기준)") @RequestParam(required = false) String q, + @Parameter(description = "정렬 순서", example = "newest") @RequestParam(defaultValue = "newest") String sort, + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") int page, + @Parameter(description = "페이지 크기", example = "10") @RequestParam(defaultValue = "10") int size) { + return ApiResponse.ok(scrapService.getScraps(userId, q, sort, PageRequest.of(page, size))); + } +} \ No newline at end of file diff --git a/src/main/java/com/devpick/domain/content/dto/ScrapItemResponse.java b/src/main/java/com/devpick/domain/content/dto/ScrapItemResponse.java new file mode 100644 index 0000000..91afac7 --- /dev/null +++ b/src/main/java/com/devpick/domain/content/dto/ScrapItemResponse.java @@ -0,0 +1,33 @@ +package com.devpick.domain.content.dto; + +import com.devpick.domain.content.entity.Scrap; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Map; +import java.util.UUID; + +public record ScrapItemResponse( + UUID id, + UUID contentId, + String title, + String sourceName, + String thumbnail, + String summary, + Instant createdAt +) { + public static ScrapItemResponse of(Scrap scrap, Map summaryMap) { + UUID contentId = scrap.getContent().getId(); + String raw = summaryMap.getOrDefault(contentId, scrap.getContent().getPreview()); + String summary = (raw != null && !raw.isBlank()) ? raw : null; + return new ScrapItemResponse( + scrap.getId(), + contentId, + scrap.getContent().getTitle(), + scrap.getContent().getSource().getName(), + scrap.getContent().getThumbnailUrl(), + summary, + scrap.getCreatedAt().toInstant(ZoneOffset.UTC) + ); + } +} diff --git a/src/main/java/com/devpick/domain/content/dto/ScrapListResponse.java b/src/main/java/com/devpick/domain/content/dto/ScrapListResponse.java new file mode 100644 index 0000000..ea3e6b8 --- /dev/null +++ b/src/main/java/com/devpick/domain/content/dto/ScrapListResponse.java @@ -0,0 +1,11 @@ +package com.devpick.domain.content.dto; + +import java.util.List; + +public record ScrapListResponse( + List content, + int page, + int size, + long totalElements, + int totalPages +) {} \ No newline at end of file diff --git a/src/main/java/com/devpick/domain/content/repository/ScrapRepository.java b/src/main/java/com/devpick/domain/content/repository/ScrapRepository.java index fc6fe7d..66e7479 100644 --- a/src/main/java/com/devpick/domain/content/repository/ScrapRepository.java +++ b/src/main/java/com/devpick/domain/content/repository/ScrapRepository.java @@ -1,7 +1,11 @@ package com.devpick.domain.content.repository; import com.devpick.domain.content.entity.Scrap; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; import java.util.UUID; @@ -11,4 +15,16 @@ public interface ScrapRepository extends JpaRepository { boolean existsByUser_IdAndContent_Id(UUID userId, UUID contentId); Optional findByUser_IdAndContent_Id(UUID userId, UUID contentId); + + @Query(value = "SELECT s FROM Scrap s JOIN FETCH s.content c JOIN FETCH c.source src " + + "WHERE s.user.id = :userId AND c.isAvailable = true " + + "AND (:q IS NULL OR LOWER(c.title) LIKE LOWER(CONCAT('%', :q, '%')) " + + "OR LOWER(src.name) LIKE LOWER(CONCAT('%', :q, '%')) " + + "OR LOWER(c.preview) LIKE LOWER(CONCAT('%', :q, '%')))", + countQuery = "SELECT COUNT(s) FROM Scrap s JOIN s.content c JOIN c.source src " + + "WHERE s.user.id = :userId AND c.isAvailable = true " + + "AND (:q IS NULL OR LOWER(c.title) LIKE LOWER(CONCAT('%', :q, '%')) " + + "OR LOWER(src.name) LIKE LOWER(CONCAT('%', :q, '%')) " + + "OR LOWER(c.preview) LIKE LOWER(CONCAT('%', :q, '%')))") + Page findScrapsWithSearch(@Param("userId") UUID userId, @Param("q") String q, Pageable pageable); } diff --git a/src/main/java/com/devpick/domain/content/service/ScrapService.java b/src/main/java/com/devpick/domain/content/service/ScrapService.java new file mode 100644 index 0000000..8ed3303 --- /dev/null +++ b/src/main/java/com/devpick/domain/content/service/ScrapService.java @@ -0,0 +1,73 @@ +package com.devpick.domain.content.service; + +import com.devpick.domain.content.dto.ScrapItemResponse; +import com.devpick.domain.content.dto.ScrapListResponse; +import com.devpick.domain.content.entity.Scrap; +import com.devpick.domain.content.repository.AiSummaryRepository; +import com.devpick.domain.content.repository.ScrapRepository; +import com.devpick.domain.user.entity.User; +import com.devpick.domain.user.repository.UserRepository; +import com.devpick.global.common.exception.DevpickException; +import com.devpick.global.common.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ScrapService { + + private final ScrapRepository scrapRepository; + private final UserRepository userRepository; + private final AiSummaryRepository aiSummaryRepository; + + @Transactional(readOnly = true) + public ScrapListResponse getScraps(UUID userId, String q, String sort, Pageable pageable) { + User user = userRepository.findByIdAndIsActiveTrue(userId) + .orElseThrow(() -> new DevpickException(ErrorCode.USER_NOT_FOUND)); + + String normalizedQ = (q != null && !q.isBlank()) ? q.trim() : null; + Sort jpaSort = "oldest".equalsIgnoreCase(sort) + ? Sort.by(Sort.Direction.ASC, "createdAt") + : Sort.by(Sort.Direction.DESC, "createdAt"); + Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), jpaSort); + + Page scrapPage = scrapRepository.findScrapsWithSearch(userId, normalizedQ, sortedPageable); + + if (scrapPage.isEmpty()) { + return new ScrapListResponse(List.of(), scrapPage.getNumber(), scrapPage.getSize(), 0L, 0); + } + + List contentIds = scrapPage.getContent().stream() + .map(s -> s.getContent().getId().toString()) + .distinct() + .toList(); + + Map summaryMapTemp; + try { + String aiLevel = AiSummaryService.toAiServerLevel(user.getLevel().name()); + summaryMapTemp = aiSummaryRepository.batchFindCoreSummaries(contentIds, aiLevel); + } catch (Exception e) { + log.warn("AI 요약 배치 조회 실패 — 원문 미리보기로 fallback: {}", e.getMessage()); + summaryMapTemp = Map.of(); + } + final Map summaryMap = summaryMapTemp; + + List items = scrapPage.getContent().stream() + .map(s -> ScrapItemResponse.of(s, summaryMap)) + .toList(); + + return new ScrapListResponse(items, scrapPage.getNumber(), scrapPage.getSize(), + scrapPage.getTotalElements(), scrapPage.getTotalPages()); + } +} \ No newline at end of file diff --git a/src/test/java/com/devpick/domain/content/controller/ScrapControllerTest.java b/src/test/java/com/devpick/domain/content/controller/ScrapControllerTest.java new file mode 100644 index 0000000..290f412 --- /dev/null +++ b/src/test/java/com/devpick/domain/content/controller/ScrapControllerTest.java @@ -0,0 +1,103 @@ +package com.devpick.domain.content.controller; + +import com.devpick.domain.content.dto.ScrapItemResponse; +import com.devpick.domain.content.dto.ScrapListResponse; +import com.devpick.domain.content.service.ScrapService; +import com.devpick.global.common.exception.GlobalExceptionHandler; +import org.junit.jupiter.api.AfterEach; +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; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class ScrapControllerTest { + + private MockMvc mockMvc; + + @Mock private ScrapService scrapService; + @InjectMocks private ScrapController scrapController; + + private UUID userId; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders + .standaloneSetup(scrapController) + .setControllerAdvice(new GlobalExceptionHandler()) + .setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()) + .build(); + + userId = UUID.randomUUID(); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + userId, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))) + ); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + @DisplayName("GET /users/me/scraps - 정상 조회 시 200 반환") + void getScraps_success_returns200() throws Exception { + ScrapItemResponse item = new ScrapItemResponse( + UUID.randomUUID(), UUID.randomUUID(), + "Spring Boot 완전 정복", "velog", + "https://thumb.jpg", "핵심 요약", Instant.now() + ); + ScrapListResponse response = new ScrapListResponse(List.of(item), 0, 10, 1L, 1); + given(scrapService.getScraps(eq(userId), isNull(), any(), any())).willReturn(response); + + mockMvc.perform(get("/users/me/scraps")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.content[0].title").value("Spring Boot 완전 정복")) + .andExpect(jsonPath("$.data.totalElements").value(1)); + } + + @Test + @DisplayName("GET /users/me/scraps - 빈 결과 시 200 반환") + void getScraps_empty_returns200WithEmptyContent() throws Exception { + ScrapListResponse response = new ScrapListResponse(List.of(), 0, 10, 0L, 0); + given(scrapService.getScraps(any(), any(), any(), any())).willReturn(response); + + mockMvc.perform(get("/users/me/scraps")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content").isEmpty()) + .andExpect(jsonPath("$.data.totalElements").value(0)); + } + + @Test + @DisplayName("GET /users/me/scraps - q 파라미터 전달 시 서비스에 전달됨") + void getScraps_withQuery_passesQueryToService() throws Exception { + ScrapListResponse response = new ScrapListResponse(List.of(), 0, 10, 0L, 0); + given(scrapService.getScraps(eq(userId), eq("Spring"), any(), any())).willReturn(response); + + mockMvc.perform(get("/users/me/scraps").param("q", "Spring")) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/devpick/domain/content/service/ScrapServiceTest.java b/src/test/java/com/devpick/domain/content/service/ScrapServiceTest.java new file mode 100644 index 0000000..6889581 --- /dev/null +++ b/src/test/java/com/devpick/domain/content/service/ScrapServiceTest.java @@ -0,0 +1,198 @@ +package com.devpick.domain.content.service; + +import com.devpick.domain.content.dto.ScrapItemResponse; +import com.devpick.domain.content.dto.ScrapListResponse; +import com.devpick.domain.content.entity.Content; +import com.devpick.domain.content.entity.ContentSource; +import com.devpick.domain.content.entity.Scrap; +import com.devpick.domain.content.repository.AiSummaryRepository; +import com.devpick.domain.content.repository.ScrapRepository; +import com.devpick.domain.user.entity.Job; +import com.devpick.domain.user.entity.Level; +import com.devpick.domain.user.entity.User; +import com.devpick.domain.user.repository.UserRepository; +import com.devpick.global.common.exception.DevpickException; +import com.devpick.global.common.exception.ErrorCode; +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; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ScrapServiceTest { + + @InjectMocks private ScrapService scrapService; + @Mock private ScrapRepository scrapRepository; + @Mock private UserRepository userRepository; + @Mock private AiSummaryRepository aiSummaryRepository; + + private UUID userId; + private User user; + private Scrap scrap; + private UUID contentId; + + @BeforeEach + void setUp() { + userId = UUID.randomUUID(); + contentId = UUID.randomUUID(); + + ContentSource source = ContentSource.builder() + .name("velog").url("https://velog.io").collectMethod("rss").build(); + ReflectionTestUtils.setField(source, "id", UUID.randomUUID()); + + Content content = Content.builder() + .source(source) + .title("Spring Boot 완전 정복") + .canonicalUrl("https://velog.io/@test/spring") + .preview("원문 미리보기 텍스트") + .thumbnailUrl("https://thumb.jpg") + .build(); + ReflectionTestUtils.setField(content, "id", contentId); + + user = User.builder() + .email("test@devpick.kr") + .nickname("tester") + .job(Job.BACKEND) + .level(Level.JUNIOR) + .build(); + ReflectionTestUtils.setField(user, "id", userId); + + scrap = Scrap.builder().user(user).content(content).build(); + ReflectionTestUtils.setField(scrap, "id", UUID.randomUUID()); + ReflectionTestUtils.setField(scrap, "createdAt", LocalDateTime.now()); + + lenient().when(userRepository.findByIdAndIsActiveTrue(userId)).thenReturn(Optional.of(user)); + lenient().when(aiSummaryRepository.batchFindCoreSummaries(anyList(), anyString())).thenReturn(Map.of()); + } + + @Test + @DisplayName("존재하지 않는 유저 → USER_NOT_FOUND 예외") + void getScraps_userNotFound_throwsException() { + given(userRepository.findByIdAndIsActiveTrue(userId)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10))) + .isInstanceOf(DevpickException.class) + .satisfies(e -> assertThat(((DevpickException) e).getErrorCode()) + .isEqualTo(ErrorCode.USER_NOT_FOUND)); + } + + @Test + @DisplayName("스크랩 없는 유저 → 빈 리스트 반환") + void getScraps_emptyResult_returnsEmptyList() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(Page.empty()); + + ScrapListResponse result = scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10)); + + assertThat(result.content()).isEmpty(); + assertThat(result.totalElements()).isZero(); + } + + @Test + @DisplayName("스크랩 있는 유저 → 항목 반환") + void getScraps_withScraps_returnsItems() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(new PageImpl<>(List.of(scrap))); + + ScrapListResponse result = scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10)); + + assertThat(result.content()).hasSize(1); + ScrapItemResponse item = result.content().get(0); + assertThat(item.contentId()).isEqualTo(contentId); + assertThat(item.title()).isEqualTo("Spring Boot 완전 정복"); + assertThat(item.sourceName()).isEqualTo("velog"); + } + + @Test + @DisplayName("sort=oldest → createdAt ASC 정렬 적용") + void getScraps_oldestSort_appliesAscSort() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(Page.empty()); + ArgumentCaptor pageableCaptor = ArgumentCaptor.forClass(Pageable.class); + + scrapService.getScraps(userId, null, "oldest", Pageable.ofSize(10)); + + verify(scrapRepository).findScrapsWithSearch(any(), any(), pageableCaptor.capture()); + Sort.Order order = pageableCaptor.getValue().getSort().getOrderFor("createdAt"); + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.ASC); + } + + @Test + @DisplayName("sort=newest → createdAt DESC 정렬 적용") + void getScraps_newestSort_appliesDescSort() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(Page.empty()); + ArgumentCaptor pageableCaptor = ArgumentCaptor.forClass(Pageable.class); + + scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10)); + + verify(scrapRepository).findScrapsWithSearch(any(), any(), pageableCaptor.capture()); + Sort.Order order = pageableCaptor.getValue().getSort().getOrderFor("createdAt"); + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); + } + + @Test + @DisplayName("AI 요약 있으면 summary 필드에 AI 요약 반환") + void getScraps_withAiSummary_returnsSummary() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(new PageImpl<>(List.of(scrap))); + given(aiSummaryRepository.batchFindCoreSummaries(anyList(), anyString())) + .willReturn(Map.of(contentId, "AI 핵심 요약")); + + ScrapListResponse result = scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10)); + + assertThat(result.content().get(0).summary()).isEqualTo("AI 핵심 요약"); + } + + @Test + @DisplayName("AI 요약 없으면 원문 미리보기(preview) fallback") + void getScraps_noAiSummary_returnsPreviewFallback() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(new PageImpl<>(List.of(scrap))); + given(aiSummaryRepository.batchFindCoreSummaries(anyList(), anyString())) + .willReturn(Map.of()); + + ScrapListResponse result = scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10)); + + assertThat(result.content().get(0).summary()).isEqualTo("원문 미리보기 텍스트"); + } + + @Test + @DisplayName("DynamoDB 예외 시 원문 미리보기로 fallback") + void getScraps_dynamoDbException_returnsPreviewFallback() { + given(scrapRepository.findScrapsWithSearch(any(), any(), any())) + .willReturn(new PageImpl<>(List.of(scrap))); + given(aiSummaryRepository.batchFindCoreSummaries(anyList(), anyString())) + .willThrow(new RuntimeException("DynamoDB 연결 실패")); + + ScrapListResponse result = scrapService.getScraps(userId, null, "newest", Pageable.ofSize(10)); + + assertThat(result.content().get(0).summary()).isEqualTo("원문 미리보기 텍스트"); + } +}