diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.java index 17aeed6f..5c322157 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.java @@ -45,4 +45,7 @@ public class PostingListFilterDto { @Parameter(description = "급여순 정렬 여부") private Boolean payAmountSort; + + @Parameter(description = "검색 키워드 (공고 제목 또는 업장명, 대소문자 무시, 앞뒤 공백 제거; 생략하거나 공백만 있으면 전체 조회)") + private String searchKeyword; } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListFilterDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListFilterDto.java index 7cfd9e53..96f578df 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListFilterDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListFilterDto.java @@ -21,7 +21,7 @@ public class PostingMapListFilterDto { @Parameter(description = "지도 우측 하단 좌표") private CoordinateDto coordinate2; - @Parameter(description = "검색 키워드 (공고 제목 또는 업장명)") + @Parameter(description = "검색 키워드 (공고 제목 또는 업장명, 대소문자 무시, 앞뒤 공백 제거; 생략하거나 공백만 있으면 전체 조회)") private String searchKeyword; @NotNull diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java index bdf49f0d..2f74b750 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java @@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Repository; import java.math.BigDecimal; @@ -82,7 +83,8 @@ public long getCountOfPostings(PostingListFilterDto filter) { ltePayAmount(qPosting, filter.getMaxPayAmount()), gteStartTime(qPostingSchedule, filter.getStartTime()), lteEndTime(qPostingSchedule, filter.getEndTime()), - workingDaysIn(qPostingSchedule, filter.getWorkingDays()) + workingDaysIn(qPostingSchedule, filter.getWorkingDays()), + containsSearchKeyword(qPosting, qWorkspace, filter.getSearchKeyword()) ) .fetchOne(); @@ -132,7 +134,8 @@ public List getPostingsWithCursor(CursorPageRequest[] getOrderSpecifiersForMapList(QPosting qPosting, PostingSortType sortType) { diff --git a/src/test/java/com/dreamteam/alter/application/posting/usecase/GetPostingsWithCursorTests.java b/src/test/java/com/dreamteam/alter/application/posting/usecase/GetPostingsWithCursorTests.java new file mode 100644 index 00000000..69666537 --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/posting/usecase/GetPostingsWithCursorTests.java @@ -0,0 +1,128 @@ +package com.dreamteam.alter.application.posting.usecase; + +import com.dreamteam.alter.adapter.inbound.common.dto.CursorDto; +import com.dreamteam.alter.adapter.inbound.common.dto.CursorPageRequest; +import com.dreamteam.alter.adapter.inbound.common.dto.CursorPageRequestDto; +import com.dreamteam.alter.adapter.inbound.general.posting.dto.PostingListFilterDto; +import com.dreamteam.alter.adapter.outbound.posting.persistence.readonly.PostingListResponse; +import com.dreamteam.alter.common.util.CursorUtil; +import com.dreamteam.alter.domain.posting.port.outbound.PostingQueryRepository; +import com.dreamteam.alter.domain.user.context.AppActor; +import com.dreamteam.alter.domain.user.entity.User; +import com.dreamteam.alter.domain.workspace.entity.BusinessType; +import com.dreamteam.alter.domain.workspace.entity.Workspace; +import com.fasterxml.jackson.databind.ObjectMapper; +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +@DisplayName("GetPostingsWithCursor - 일반 공고 목록 커서 조회") +class GetPostingsWithCursorTests { + @Mock PostingQueryRepository postingQueryRepository; + @Spy ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + @InjectMocks GetPostingsWithCursor useCase; + + private PostingListResponse row(Long id) { + PostingListResponse row = mock(PostingListResponse.class); + Workspace workspace = mock(Workspace.class); + lenient().when(workspace.getBusinessType()).thenReturn(BusinessType.create("카페", "설명")); + lenient().when(row.getWorkspace()).thenReturn(workspace); + lenient().when(row.getSchedules()).thenReturn(List.of()); + lenient().when(row.getId()).thenReturn(id); + lenient().when(row.getCreatedAt()).thenReturn(LocalDateTime.of(2026, 10, 3, 12, 0).minusDays(id)); + return row; + } + + private AppActor actor() { + AppActor actor = mock(AppActor.class); + lenient().when(actor.getUser()).thenReturn(mock(User.class)); + return actor; + } + + @Test + @DisplayName("다음 페이지 유무와 관계없이 마지막으로 반환한 행을 가리키는 커서를 내려준다") + void cursorPointsAtLastReturnedRowRegardlessOfNextPage() { + // given + given(postingQueryRepository.getCountOfPostings(any())).willReturn(2L); + List rows = List.of(row(1L), row(2L)); + given(postingQueryRepository.getPostingsWithCursor(any(), any(), any())) + .willReturn(rows); + + // when + var result = useCase.execute(new CursorPageRequestDto(null, 10), new PostingListFilterDto(), actor()); + + // then + assertThat(result.data()).extracting(dto -> dto.getId()).containsExactly(1L, 2L); + assertThat(result.page().pageSize()).isEqualTo(10); + assertThat(result.page().totalCount()).isEqualTo(2); + CursorDto cursor = CursorUtil.decodeCursor(result.page().cursor(), CursorDto.class, objectMapper); + assertThat(cursor.getId()).isEqualTo(2L); + assertThat(cursor.getCreatedAt()).isEqualTo(rows.get(1).getCreatedAt()); + } + + @Test + @DisplayName("요청한 페이지 크기 그대로 조회한다") + void repositoryIsAskedForRequestedPageSize() { + // given + given(postingQueryRepository.getCountOfPostings(any())).willReturn(1L); + List rows = List.of(row(1L)); + given(postingQueryRepository.getPostingsWithCursor(any(), any(), any())) + .willReturn(rows); + + // when + useCase.execute(new CursorPageRequestDto(null, 5), new PostingListFilterDto(), actor()); + + // then + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(CursorPageRequest.class); + then(postingQueryRepository).should().getPostingsWithCursor(captor.capture(), any(), any()); + assertThat(captor.getValue().pageSize()).isEqualTo(5); + } + + @Test + @DisplayName("조건에 맞는 공고가 없으면 커서 없이 빈 목록을 반환한다") + void noMatchingPostingsReturnsEmptyWithoutCursor() { + // given + given(postingQueryRepository.getCountOfPostings(any())).willReturn(0L); + + // when + var result = useCase.execute(new CursorPageRequestDto(null, 10), new PostingListFilterDto(), actor()); + + // then + assertThat(result.page().cursor()).isNull(); + assertThat(result.page().totalCount()).isZero(); + assertThat(result.data()).isEmpty(); + } + + @Test + @DisplayName("커서 이후 남은 공고가 없으면 커서 없이 빈 목록을 반환한다") + void noRowsAfterCursorReturnsEmptyWithoutCursor() { + // given + given(postingQueryRepository.getCountOfPostings(any())).willReturn(2L); + given(postingQueryRepository.getPostingsWithCursor(any(), any(), any())).willReturn(List.of()); + + // when + var result = useCase.execute(new CursorPageRequestDto(null, 10), new PostingListFilterDto(), actor()); + + // then + assertThat(result.page().cursor()).isNull(); + assertThat(result.page().totalCount()).isEqualTo(2); + assertThat(result.data()).isEmpty(); + } +}