Skip to content

Commit

Permalink
Merge pull request #43 from PawWithU/feat/42-posts-filter-search-api
Browse files Browse the repository at this point in the history
[Feature] 이동봉사 공고 필터 검색 API 구현
  • Loading branch information
kyeong-hyeok authored Nov 11, 2023
2 parents fdaddab + d8e9e8c commit 6f66f84
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pawwithu.connectdog.common.converter;

import com.pawwithu.connectdog.domain.dog.entity.DogSize;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

public class DogSizeConverter implements Converter<String, DogSize> {
@Override
public DogSize convert(String source) {
if (!StringUtils.hasText(source)) return null;
return DogSize.create(source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.pawwithu.connectdog.common.converter;

import com.pawwithu.connectdog.domain.post.entity.PostStatus;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

public class PostStatusConverter implements Converter<String, PostStatus> {
@Override
public PostStatus convert(String source) {
if (!StringUtils.hasText(source)) return null;
return PostStatus.create(source);
}

}
17 changes: 17 additions & 0 deletions src/main/java/com/pawwithu/connectdog/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.pawwithu.connectdog.config;

import com.pawwithu.connectdog.common.converter.DogSizeConverter;
import com.pawwithu.connectdog.common.converter.PostStatusConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new DogSizeConverter());
registry.addConverter(new PostStatusConverter());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.pawwithu.connectdog.domain.post.controller;

import com.pawwithu.connectdog.domain.post.dto.request.PostCreateRequest;
import com.pawwithu.connectdog.domain.post.dto.request.PostSearchRequest;
import com.pawwithu.connectdog.domain.post.dto.response.PostGetHomeResponse;
import com.pawwithu.connectdog.domain.post.dto.response.PostSearchResponse;
import com.pawwithu.connectdog.domain.post.service.PostService;
import com.pawwithu.connectdog.error.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -11,6 +13,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down Expand Up @@ -54,4 +57,16 @@ public ResponseEntity<List<PostGetHomeResponse>> getHomePosts() {
List<PostGetHomeResponse> homePosts = postService.getHomePosts();
return ResponseEntity.ok(homePosts);
}

@Operation(summary = "공고 필터 검색", description = "공고를 필터로 검색합니다.",
responses = {@ApiResponse(responseCode = "200", description = "공고 필터 검색 성공")
, @ApiResponse(responseCode = "400"
, description = "P1, 잘못된 공고 상태입니다. \t\n D1, 잘못된 강아지 사이즈입니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping( "/volunteers/posts")
public ResponseEntity<List<PostSearchResponse>> searchPosts(PostSearchRequest request, Pageable pageable) {
List<PostSearchResponse> response = postService.searchPosts(request, pageable);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.pawwithu.connectdog.domain.post.dto.request;

import com.pawwithu.connectdog.domain.dog.entity.DogSize;
import com.pawwithu.connectdog.domain.post.entity.PostStatus;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.RequestParam;

import java.time.LocalDate;

public record PostSearchRequest(@RequestParam(value = "postStatus", required = false) PostStatus postStatus,
String departureLoc,
String arrivalLoc,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate startDate,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate endDate,
@RequestParam(value = "dogSize", required = false) DogSize dogSize,
Boolean isKennel,
String intermediaryName) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.pawwithu.connectdog.domain.post.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDate;

public record PostSearchResponse(String mainImage, String departureLoc, String arrivalLoc,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul")
LocalDate startDate,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul")
LocalDate endDate,
String intermediaryName,
Boolean isKennel) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@RequiredArgsConstructor
public enum PostStatus {

RECRUITING("모집중"), WAITING("승인 대기중"), PROGRESSING("진행중"), COMPLETED("봉사 완료");
RECRUITING("모집중"), WAITING("승인 대기중"), PROGRESSING("진행중"), COMPLETED("봉사 완료"), EXPIRED("마감");

@JsonCreator
public static PostStatus create(String requestValue) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.pawwithu.connectdog.domain.post.repository;

import com.pawwithu.connectdog.domain.post.dto.request.PostSearchRequest;
import com.pawwithu.connectdog.domain.post.dto.response.PostGetHomeResponse;
import com.pawwithu.connectdog.domain.post.dto.response.PostSearchResponse;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface CustomPostRepository {

List<PostGetHomeResponse> getHomePosts();
List<PostSearchResponse> searchPosts(PostSearchRequest request, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package com.pawwithu.connectdog.domain.post.repository.impl;

import com.pawwithu.connectdog.domain.dog.entity.DogSize;
import com.pawwithu.connectdog.domain.post.dto.request.PostSearchRequest;
import com.pawwithu.connectdog.domain.post.dto.response.PostGetHomeResponse;
import com.pawwithu.connectdog.domain.post.dto.response.PostSearchResponse;
import com.pawwithu.connectdog.domain.post.entity.PostStatus;
import com.pawwithu.connectdog.domain.post.repository.CustomPostRepository;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.util.List;

import static com.pawwithu.connectdog.domain.dog.entity.QDog.dog;
import static com.pawwithu.connectdog.domain.intermediary.entity.QIntermediary.intermediary;
import static com.pawwithu.connectdog.domain.post.entity.QPost.post;
import static com.pawwithu.connectdog.domain.post.entity.QPostImage.postImage;
Expand All @@ -33,4 +42,70 @@ public List<PostGetHomeResponse> getHomePosts() {
.fetch();
}

@Override
public List<PostSearchResponse> searchPosts(PostSearchRequest request, Pageable pageable) {
return queryFactory
.select(Projections.constructor(PostSearchResponse.class,
postImage.image, post.departureLoc, post.arrivalLoc, post.startDate, post.endDate,
intermediary.name, post.isKennel))
.from(post)
.join(post.intermediary, intermediary)
.join(post.mainImage, postImage)
.join(post.dog, dog)
.where(allFilterSearch(request, pageable))
.orderBy(post.endDate.asc(), post.createdDate.desc())
.offset(pageable.getOffset()) // 페이지 번호
.limit(pageable.getPageSize()) // 페이지 사이즈
.fetch();
}

// 모든 필터 검색
private BooleanExpression allFilterSearch(PostSearchRequest request, Pageable pageable) {
return postStatusEq(request.postStatus())
.and(departureLocContains(request.departureLoc()))
.and(arrivalLocContains(request.arrivalLoc()))
.and(dateSearch(request.startDate(), request.endDate()))
.and(dogSizeEq(request.dogSize()))
.and(isKennelEq(request.isKennel()))
.and(intermediaryNameContains(request.intermediaryName()));
}

// 공고 상태 필터
private BooleanExpression postStatusEq(PostStatus postStatus) {
return postStatus == null ? null : post.status.eq(postStatus);
}

// 출발 지역 필터
private BooleanExpression departureLocContains(String departureLoc) {
return StringUtils.hasText(departureLoc) ? post.departureLoc.contains(departureLoc) : null;
}

// 도착 지역 필터
private BooleanExpression arrivalLocContains(String arrivalLoc) {
return StringUtils.hasText(arrivalLoc) ? post.arrivalLoc.contains(arrivalLoc) : null;
}

// 일정 필터
private BooleanExpression dateSearch(LocalDate userStartDate, LocalDate userEndDate) {
if (userStartDate == null || userEndDate == null) return null;
return post.startDate.loe(userEndDate).and(post.endDate.goe(userStartDate));
}

// 상세 정보 - 강아지 사이즈 필터
private BooleanExpression dogSizeEq(DogSize dogSize) {
if (dogSize == null) return null;
return dog.size.eq(dogSize);
}

// 상세 정보 - 켄넬 제공 여부 필터
private BooleanExpression isKennelEq(Boolean isKennel) {
if (isKennel == null) return null;
return post.isKennel.eq(isKennel);
}

// 상세 정보 - 이동봉사 중개명 필터
private BooleanExpression intermediaryNameContains(String intermediaryName) {
return StringUtils.hasText(intermediaryName) ? post.intermediary.name.contains(intermediaryName) : null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import com.pawwithu.connectdog.domain.intermediary.repository.IntermediaryRepository;
import com.pawwithu.connectdog.domain.post.dto.request.PostCreateRequest;
import com.pawwithu.connectdog.domain.post.dto.request.PostSearchRequest;
import com.pawwithu.connectdog.domain.post.dto.response.PostGetHomeResponse;
import com.pawwithu.connectdog.domain.post.dto.response.PostSearchResponse;
import com.pawwithu.connectdog.domain.post.entity.Post;
import com.pawwithu.connectdog.domain.post.entity.PostImage;
import com.pawwithu.connectdog.domain.post.repository.CustomPostRepository;
Expand All @@ -15,6 +17,7 @@
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -67,8 +70,15 @@ public void createPost(String email, PostCreateRequest request, List<MultipartFi

}

@Transactional(readOnly = true)
public List<PostGetHomeResponse> getHomePosts() {
List<PostGetHomeResponse> homePosts = customPostRepository.getHomePosts();
return homePosts;
}

@Transactional(readOnly = true)
public List<PostSearchResponse> searchPosts(PostSearchRequest request, Pageable pageable) {
List<PostSearchResponse> searchPosts = customPostRepository.searchPosts(request, pageable);
return searchPosts;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pawwithu.connectdog.domain.post.dto.response.PostGetHomeResponse;
import com.pawwithu.connectdog.domain.post.dto.response.PostSearchResponse;
import com.pawwithu.connectdog.domain.post.service.PostService;
import com.pawwithu.connectdog.utils.TestUserArgumentResolver;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -10,6 +11,9 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
Expand All @@ -20,7 +24,6 @@
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand All @@ -40,7 +43,7 @@ class PostControllerTest {
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(postController)
.setCustomArgumentResolvers(new TestUserArgumentResolver())
.setCustomArgumentResolvers(new TestUserArgumentResolver(), new PageableHandlerMethodArgumentResolver())
.addFilter(new CharacterEncodingFilter("UTF-8", true))
.build();
}
Expand Down Expand Up @@ -101,4 +104,38 @@ void setUp() {
verify(postService, times(1)).getHomePosts();
}

@Test
void 공고_필터별_검색() throws Exception {
//given
Pageable pageable = PageRequest.of(0, 2);
List<PostSearchResponse> response = new ArrayList<>();
LocalDate startDate = LocalDate.of(2023, 10, 2);
LocalDate endDate = LocalDate.of(2023, 11, 7);
response.add(new PostSearchResponse("image1", "서울시 성북구", "서울시 중랑구",
startDate, endDate, "이동봉사 중개", true));
response.add(new PostSearchResponse("image2", "서울시 성북구", "서울시 중랑구",
startDate, endDate, "이동봉사 중개", false));


//when
given(postService.searchPosts(any(), any())).willReturn(response);
ResultActions result = mockMvc.perform(
get("/volunteers/posts")
// .param("postStatus", "모집중")
.param("departureLoc", "서울시 성북구")
.param("arrivalLoc", "경기 고양시")
.param("startDate", "2023-10-02")
.param("endDate", "2023-11-07")
// .param("dogSize", DogSize.SMALL.getKey())
.param("isKennel", "false")
.param("intermediaryName", "이동봉사 중개")
.param("page", "0")
.param("size", "2")
);

//then
result.andExpect(status().isOk());
verify(postService, times(1)).searchPosts(any(), any());
}

}

0 comments on commit 6f66f84

Please sign in to comment.