diff --git a/src/main/java/com/techfork/domain/post/controller/PostControllerV2.java b/src/main/java/com/techfork/domain/post/controller/PostControllerV2.java new file mode 100644 index 0000000..c0d37ea --- /dev/null +++ b/src/main/java/com/techfork/domain/post/controller/PostControllerV2.java @@ -0,0 +1,39 @@ +package com.techfork.domain.post.controller; + +import com.techfork.domain.post.dto.CompanyListResponse; +import com.techfork.domain.post.service.PostQueryService; +import com.techfork.global.common.code.SuccessCode; +import com.techfork.global.response.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Post V2", description = "게시글 API V2") +@Slf4j +@RestController +@RequestMapping("/api/v2/posts") +@RequiredArgsConstructor +public class PostControllerV2 { + + private final PostQueryService postQueryService; + + @Operation( + summary = "게시글이 있는 회사 목록 조회 (V2)", + description = """ + 게시글이 존재하는 회사 목록을 조회합니다. + - 최신 게시글 발행일 기준으로 정렬 + - 오늘 발행된 게시글이 있는지 여부 포함 + - 회사 로고 URL 포함 + """ + ) + @GetMapping("/companies") + public ResponseEntity> getCompanies() { + CompanyListResponse response = postQueryService.getCompaniesV2(); + return BaseResponse.of(SuccessCode.OK, response); + } +} diff --git a/src/main/java/com/techfork/domain/post/converter/PostConverter.java b/src/main/java/com/techfork/domain/post/converter/PostConverter.java index be81b29..efcff79 100644 --- a/src/main/java/com/techfork/domain/post/converter/PostConverter.java +++ b/src/main/java/com/techfork/domain/post/converter/PostConverter.java @@ -1,9 +1,6 @@ package com.techfork.domain.post.converter; -import com.techfork.domain.post.dto.CompanyListResponse; -import com.techfork.domain.post.dto.PostDetailDto; -import com.techfork.domain.post.dto.PostInfoDto; -import com.techfork.domain.post.dto.PostListResponse; +import com.techfork.domain.post.dto.*; import org.springframework.stereotype.Component; import java.util.List; @@ -17,6 +14,13 @@ public CompanyListResponse toCompanyListResponse(List companies) { .build(); } + public CompanyListResponse toCompanyListResponseV2(List companies) { + return CompanyListResponse.builder() + .totalNumber(companies.size()) + .companies(companies) + .build(); + } + public PostListResponse toPostListResponse(List posts, int requestedSize) { boolean hasNext = posts.size() > requestedSize; List content = hasNext ? posts.subList(0, requestedSize) : posts; diff --git a/src/main/java/com/techfork/domain/post/dto/CompanyDto.java b/src/main/java/com/techfork/domain/post/dto/CompanyDto.java new file mode 100644 index 0000000..a19dc19 --- /dev/null +++ b/src/main/java/com/techfork/domain/post/dto/CompanyDto.java @@ -0,0 +1,13 @@ +package com.techfork.domain.post.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(name = "CompanyDto", description = "회사 정보") +public record CompanyDto( + String company, + boolean hasNewPost, + String logoUrl +) { +} \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java b/src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java index 5848b23..6efa74c 100644 --- a/src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java +++ b/src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java @@ -8,6 +8,8 @@ @Builder @Schema(name = "CompanyListResponse") public record CompanyListResponse( - List companies + Integer totalNumber, + @Schema(description = "회사 목록 (V1: String, V2: CompanyDto)") + List companies ) { } diff --git a/src/main/java/com/techfork/domain/post/entity/Post.java b/src/main/java/com/techfork/domain/post/entity/Post.java index 98a95c7..8e51a9d 100644 --- a/src/main/java/com/techfork/domain/post/entity/Post.java +++ b/src/main/java/com/techfork/domain/post/entity/Post.java @@ -16,7 +16,11 @@ import java.util.List; @Entity -@Table(name = "posts") +@Table(name = "posts", indexes = { + @Index(name = "idx_post_published_at", columnList = "publishedAt"), + @Index(name = "idx_post_view_count_id", columnList = "viewCount, publishedAt"), + @Index(name = "idx_post_company_published_at", columnList = "company, publishedAt") +}) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Post extends BaseEntity { diff --git a/src/main/java/com/techfork/domain/post/repository/PostRepository.java b/src/main/java/com/techfork/domain/post/repository/PostRepository.java index 47c8708..9d4cad2 100644 --- a/src/main/java/com/techfork/domain/post/repository/PostRepository.java +++ b/src/main/java/com/techfork/domain/post/repository/PostRepository.java @@ -1,5 +1,6 @@ package com.techfork.domain.post.repository; +import com.techfork.domain.post.dto.CompanyDto; import com.techfork.domain.post.dto.PostDetailDto; import com.techfork.domain.post.dto.PostInfoDto; import com.techfork.domain.post.entity.Post; @@ -35,13 +36,25 @@ public interface PostRepository extends JpaRepository { @Query("SELECT DISTINCT p.company FROM Post p ORDER BY p.company") List findDistinctCompanies(); + @Query(""" + SELECT new com.techfork.domain.post.dto.CompanyDto( + p.company, + (COUNT(CASE WHEN p.publishedAt >= CURRENT_DATE THEN 1 END) > 0), + MAX(p.logoUrl) + ) + FROM Post p + GROUP BY p.company + ORDER BY MAX(p.publishedAt) DESC + """) + List findCompaniesWithDetails(); + @Query(""" SELECT new com.techfork.domain.post.dto.PostInfoDto( p.id, p.title, p.company, p.url, p.logoUrl, p.publishedAt, p.viewCount, null) FROM Post p WHERE (:company IS NULL OR p.company = :company) AND (:lastPostId IS NULL OR p.id < :lastPostId) - ORDER BY p.id DESC + ORDER BY p.publishedAt DESC """) List findByCompanyWithCursor( @Param("company") String company, @@ -54,7 +67,7 @@ List findByCompanyWithCursor( p.id, p.title, p.company, p.url, p.logoUrl, p.publishedAt, p.viewCount, null) FROM Post p WHERE :lastPostId IS NULL OR p.id < :lastPostId - ORDER BY p.publishedAt DESC, p.id DESC + ORDER BY p.publishedAt DESC """) List findRecentPostsWithCursor( @Param("lastPostId") Long lastPostId, @@ -66,7 +79,7 @@ List findRecentPostsWithCursor( p.id, p.title, p.company, p.url, p.logoUrl, p.publishedAt, p.viewCount, null) FROM Post p WHERE :lastPostId IS NULL OR p.id < :lastPostId - ORDER BY p.viewCount DESC, p.id DESC + ORDER BY p.viewCount DESC, p.publishedAt DESC """) List findPopularPostsWithCursor( @Param("lastPostId") Long lastPostId, diff --git a/src/main/java/com/techfork/domain/post/service/PostQueryService.java b/src/main/java/com/techfork/domain/post/service/PostQueryService.java index d9e4387..4c34ec6 100644 --- a/src/main/java/com/techfork/domain/post/service/PostQueryService.java +++ b/src/main/java/com/techfork/domain/post/service/PostQueryService.java @@ -1,10 +1,7 @@ package com.techfork.domain.post.service; import com.techfork.domain.post.converter.PostConverter; -import com.techfork.domain.post.dto.CompanyListResponse; -import com.techfork.domain.post.dto.PostDetailDto; -import com.techfork.domain.post.dto.PostInfoDto; -import com.techfork.domain.post.dto.PostListResponse; +import com.techfork.domain.post.dto.*; import com.techfork.domain.post.entity.PostKeyword; import com.techfork.domain.post.enums.EPostSortType; import com.techfork.domain.post.repository.PostKeywordRepository; @@ -36,6 +33,11 @@ public CompanyListResponse getCompanies() { return postConverter.toCompanyListResponse(companies); } + public CompanyListResponse getCompaniesV2() { + List companies = postRepository.findCompaniesWithDetails(); + return postConverter.toCompanyListResponseV2(companies); + } + public PostListResponse getPostsByCompany(String company, Long lastPostId, int size) { PageRequest pageRequest = PageRequest.of(0, size + 1); List posts = postRepository.findByCompanyWithCursor(company, lastPostId, pageRequest); diff --git a/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java b/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java new file mode 100644 index 0000000..389fa48 --- /dev/null +++ b/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java @@ -0,0 +1,168 @@ +package com.techfork.domain.post.controller; + +import com.techfork.domain.post.entity.Post; +import com.techfork.domain.post.repository.PostRepository; +import com.techfork.domain.source.entity.TechBlog; +import com.techfork.domain.source.repository.TechBlogRepository; +import com.techfork.global.configuration.MySQLTestConfig; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * PostControllerV2 통합 테스트 + * - @SpringBootTest: 전체 애플리케이션 컨텍스트 로드 + * - MySQLTestConfig.class: 실제 MySQL 컨테이너로 통합 테스트 + * - 모든 레이어(Controller, Service, Repository) 통합 테스트 + * - MockMvc로 HTTP 요청/응답 테스트 + */ +@SpringBootTest +@AutoConfigureMockMvc +@Import(MySQLTestConfig.class) +@ActiveProfiles("integrationtest") +class PostControllerV2IntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private PostRepository postRepository; + + @Autowired + private TechBlogRepository techBlogRepository; + + private TechBlog testTechBlog1; + private TechBlog testTechBlog2; + private Post todayPost; + private Post oldPost; + + @BeforeEach + void setUp() { + // Given: 실제 DB에 테스트 데이터 저장 + testTechBlog1 = TechBlog.builder() + .companyName("카카오") + .blogUrl("https://kakao.com") + .rssUrl("https://kakao.com/rss") + .logoUrl("https://kakao.com/logo.png") + .build(); + techBlogRepository.save(testTechBlog1); + + testTechBlog2 = TechBlog.builder() + .companyName("네이버") + .blogUrl("https://naver.com") + .rssUrl("https://naver.com/rss") + .logoUrl("https://naver.com/logo.png") + .build(); + techBlogRepository.save(testTechBlog2); + + // 오늘 발행된 게시글 (카카오) + todayPost = Post.builder() + .title("오늘의 게시글") + .fullContent("

오늘 내용

") + .plainContent("오늘 내용") + .company("카카오") + .url("https://kakao.com/post/today") + .logoUrl("https://kakao.com/logo.png") + .publishedAt(LocalDate.now().atStartOfDay()) + .crawledAt(LocalDateTime.now()) + .techBlog(testTechBlog1) + .build(); + postRepository.save(todayPost); + + // 어제 발행된 게시글 (네이버) + oldPost = Post.builder() + .title("어제의 게시글") + .fullContent("

어제 내용

") + .plainContent("어제 내용") + .company("네이버") + .url("https://naver.com/post/old") + .logoUrl("https://naver.com/logo.png") + .publishedAt(LocalDate.now().minusDays(1).atStartOfDay()) + .crawledAt(LocalDateTime.now()) + .techBlog(testTechBlog2) + .build(); + postRepository.save(oldPost); + } + + @AfterEach + void tearDown() { + // 테스트 데이터 정리 (외래키 제약조건 순서 고려) + postRepository.deleteAll(); + techBlogRepository.deleteAll(); + } + + @Test + @DisplayName("GET /api/v2/posts/companies - 회사 목록 상세 조회 성공") + void getCompanies_Success() throws Exception { + // When & Then: 실제 DB에서 회사 상세 정보 조회 + mockMvc.perform(get("/api/v2/posts/companies")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.totalNumber").value(2)) + .andExpect(jsonPath("$.data.companies").isArray()) + .andExpect(jsonPath("$.data.companies.length()").value(2)) + // 첫 번째 회사 (카카오 - 오늘 발행) + .andExpect(jsonPath("$.data.companies[0].company").value("카카오")) + .andExpect(jsonPath("$.data.companies[0].hasNewPost").value(true)) + .andExpect(jsonPath("$.data.companies[0].logoUrl").value("https://kakao.com/logo.png")) + // 두 번째 회사 (네이버 - 어제 발행) + .andExpect(jsonPath("$.data.companies[1].company").value("네이버")) + .andExpect(jsonPath("$.data.companies[1].hasNewPost").value(false)) + .andExpect(jsonPath("$.data.companies[1].logoUrl").value("https://naver.com/logo.png")); + } + + @Test + @DisplayName("GET /api/v2/posts/companies - 게시글이 없는 경우 빈 배열 반환") + void getCompanies_EmptyWhenNoPosts() throws Exception { + // Given: 모든 게시글 삭제 + postRepository.deleteAll(); + + // When & Then: 빈 배열 반환 확인 + mockMvc.perform(get("/api/v2/posts/companies")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.totalNumber").value(0)) + .andExpect(jsonPath("$.data.companies").isArray()) + .andExpect(jsonPath("$.data.companies.length()").value(0)); + } + + @Test + @DisplayName("GET /api/v2/posts/companies - 발행일 기준 정렬 확인") + void getCompanies_SortedByLatestPublishedAt() throws Exception { + // Given: 새로운 게시글 추가 (네이버가 최근) + Post newerPost = Post.builder() + .title("최신 네이버 게시글") + .fullContent("

최신 내용

") + .plainContent("최신 내용") + .company("네이버") + .url("https://naver.com/post/newest") + .logoUrl("https://naver.com/logo.png") + .publishedAt(LocalDateTime.now()) + .crawledAt(LocalDateTime.now()) + .techBlog(testTechBlog2) + .build(); + postRepository.save(newerPost); + + // When & Then: 네이버가 첫 번째로 정렬되는지 확인 + mockMvc.perform(get("/api/v2/posts/companies")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.companies[0].company").value("네이버")) + .andExpect(jsonPath("$.data.companies[1].company").value("카카오")); + } +} \ No newline at end of file diff --git a/src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java b/src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java index 37fb605..4701679 100644 --- a/src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java +++ b/src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java @@ -1,5 +1,6 @@ package com.techfork.domain.post.repository; +import com.techfork.domain.post.dto.CompanyDto; import com.techfork.domain.post.dto.PostDetailDto; import com.techfork.domain.post.dto.PostInfoDto; import com.techfork.domain.post.entity.Post; @@ -13,6 +14,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.test.context.ActiveProfiles; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -199,6 +201,86 @@ void findDistinctCompanies_ReturnsUniqueCompanies() { assertThat(result).containsExactlyInAnyOrder("카카오", "네이버"); } + @Test + @DisplayName("findCompaniesWithDetails - 회사별 상세 정보 조회 성공") + void findCompaniesWithDetails_Success() { + // Given: 여러 회사의 게시글 생성 + Post kakaoPost1 = createPost("카카오 게시글1", techBlog1, LocalDate.now().atStartOfDay(), 100L); + Post kakaoPost2 = createPost("카카오 게시글2", techBlog1, LocalDate.now().minusDays(1).atStartOfDay(), 200L); + Post naverPost = createPost("네이버 게시글", techBlog2, LocalDate.now().minusDays(2).atStartOfDay(), 300L); + postRepository.saveAll(List.of(kakaoPost1, kakaoPost2, naverPost)); + + // When: 회사 상세 정보 조회 + List result = postRepository.findCompaniesWithDetails(); + + // Then: 회사별로 집계된 정보 반환 + assertThat(result).hasSize(2); + + // 최신 발행일 기준으로 정렬되어야 함 (카카오가 더 최신) + CompanyDto firstCompany = result.get(0); + assertThat(firstCompany.company()).isEqualTo("카카오"); + assertThat(firstCompany.hasNewPost()).isTrue(); // 오늘 발행된 게시글 존재 + assertThat(firstCompany.logoUrl()).isNotNull(); + + CompanyDto secondCompany = result.get(1); + assertThat(secondCompany.company()).isEqualTo("네이버"); + assertThat(secondCompany.hasNewPost()).isFalse(); // 오늘 발행된 게시글 없음 + } + + @Test + @DisplayName("findCompaniesWithDetails - 오늘 발행된 게시글 여부 정확히 판단") + void findCompaniesWithDetails_HasNewPost_AccurateDetection() { + // Given: 오늘과 어제 게시글 + Post todayPost = createPost("오늘 게시글", techBlog1, LocalDate.now().atTime(14, 30), 100L); + Post yesterdayPost = createPost("어제 게시글", techBlog2, LocalDate.now().minusDays(1).atStartOfDay(), 200L); + postRepository.saveAll(List.of(todayPost, yesterdayPost)); + + // When: 회사 상세 정보 조회 + List result = postRepository.findCompaniesWithDetails(); + + // Then: 오늘 게시글 여부 정확히 판단 + CompanyDto kakaoCompany = result.stream() + .filter(c -> c.company().equals("카카오")) + .findFirst() + .orElseThrow(); + assertThat(kakaoCompany.hasNewPost()).isTrue(); + + CompanyDto naverCompany = result.stream() + .filter(c -> c.company().equals("네이버")) + .findFirst() + .orElseThrow(); + assertThat(naverCompany.hasNewPost()).isFalse(); + } + + @Test + @DisplayName("findCompaniesWithDetails - 최신 발행일 기준으로 정렬") + void findCompaniesWithDetails_OrderByLatestPublishedAtDesc() { + // Given: 발행일이 다른 게시글들 + Post oldPost = createPost("오래된 게시글", techBlog1, LocalDate.now().minusDays(10).atStartOfDay(), 100L); + Post recentPost = createPost("최근 게시글", techBlog2, LocalDate.now().minusDays(1).atStartOfDay(), 200L); + postRepository.saveAll(List.of(oldPost, recentPost)); + + // When: 회사 상세 정보 조회 + List result = postRepository.findCompaniesWithDetails(); + + // Then: 최신 발행일 기준 정렬 (네이버가 먼저) + assertThat(result).hasSize(2); + assertThat(result.get(0).company()).isEqualTo("네이버"); + assertThat(result.get(1).company()).isEqualTo("카카오"); + } + + @Test + @DisplayName("findCompaniesWithDetails - 게시글이 없으면 빈 리스트 반환") + void findCompaniesWithDetails_NoPosts_ReturnsEmptyList() { + // Given: 게시글 없음 + + // When: 회사 상세 정보 조회 + List result = postRepository.findCompaniesWithDetails(); + + // Then: 빈 리스트 반환 + assertThat(result).isEmpty(); + } + @Test @DisplayName("커서 페이징 - size+1 조회하여 hasNext 판단 가능") void cursorPaging_SizePlusOne_CanDetermineHasNext() { @@ -226,6 +308,7 @@ private Post createPost(String title, TechBlog techBlog, LocalDateTime published .plainContent(title + " 내용") .company(techBlog.getCompanyName()) .url("https://test.com/" + title) + .logoUrl("https://test.com/logo.png") .publishedAt(publishedAt) .crawledAt(LocalDateTime.now()) .techBlog(techBlog) diff --git a/src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java b/src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java index 4f2b7be..aadaf9b 100644 --- a/src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java @@ -1,11 +1,7 @@ package com.techfork.domain.post.service; import com.techfork.domain.post.converter.PostConverter; -import com.techfork.domain.post.dto.CompanyListResponse; -import com.techfork.domain.post.dto.PostDetailDto; -import com.techfork.domain.post.dto.PostInfoDto; -import com.techfork.domain.post.dto.PostListResponse; -import com.techfork.domain.post.entity.Post; +import com.techfork.domain.post.dto.*; import com.techfork.domain.post.entity.PostKeyword; import com.techfork.domain.post.enums.EPostSortType; import com.techfork.domain.post.repository.PostKeywordRepository; @@ -55,7 +51,7 @@ class PostQueryServiceTest { void getCompanies_Success() { // Given List mockCompanies = List.of("카카오", "네이버", "라인"); - CompanyListResponse expectedResponse = new CompanyListResponse(mockCompanies); + CompanyListResponse expectedResponse = new CompanyListResponse(3, mockCompanies); given(postRepository.findDistinctCompanies()).willReturn(mockCompanies); given(postConverter.toCompanyListResponse(mockCompanies)).willReturn(expectedResponse); @@ -66,12 +62,58 @@ void getCompanies_Success() { // Then assertThat(result).isNotNull(); assertThat(result.companies()).hasSize(3); - assertThat(result.companies()).contains("카카오", "네이버", "라인"); + + @SuppressWarnings("unchecked") + List companies = (List) result.companies(); + assertThat(companies).contains("카카오", "네이버", "라인"); verify(postRepository, times(1)).findDistinctCompanies(); verify(postConverter, times(1)).toCompanyListResponse(mockCompanies); } + @Test + @DisplayName("getCompaniesV2() - 회사 상세 정보 포함 목록 조회 성공") + void getCompaniesV2_Success() { + // Given + List mockCompanies = List.of( + CompanyDto.builder() + .company("카카오") + .hasNewPost(true) + .logoUrl("https://test.com/kakao-logo.png") + .build(), + CompanyDto.builder() + .company("네이버") + .hasNewPost(false) + .logoUrl("https://test.com/naver-logo.png") + .build() + ); + + CompanyListResponse expectedResponse = CompanyListResponse.builder() + .totalNumber(2) + .companies(mockCompanies) + .build(); + + given(postRepository.findCompaniesWithDetails()).willReturn(mockCompanies); + given(postConverter.toCompanyListResponseV2(mockCompanies)).willReturn(expectedResponse); + + // When + CompanyListResponse result = postQueryService.getCompaniesV2(); + + // Then + assertThat(result).isNotNull(); + assertThat(result.companies()).hasSize(2); + + List resultCompanies = (List) result.companies(); + assertThat(resultCompanies.get(0).company()).isEqualTo("카카오"); + assertThat(resultCompanies.get(0).hasNewPost()).isTrue(); + assertThat(resultCompanies.get(0).logoUrl()).isEqualTo("https://test.com/kakao-logo.png"); + assertThat(resultCompanies.get(1).company()).isEqualTo("네이버"); + assertThat(resultCompanies.get(1).hasNewPost()).isFalse(); + + verify(postRepository, times(1)).findCompaniesWithDetails(); + verify(postConverter, times(1)).toCompanyListResponseV2(mockCompanies); + } + @Test @DisplayName("getPostDetail() - 게시글 상세 조회 성공") void getPostDetail_Success() {