Skip to content

Commit

Permalink
feat: 보호 동물 검색 조회 로직을 구현한다.(봉사자) (#145)
Browse files Browse the repository at this point in the history
* feat: AnimalRepositoryCustom.findAnimalsByVolunteer 를 구현한다.

* feat: AnimalService.findAnimalsByVolunteer 를 구현한다.

* feat: AnimalController.findAnimalByIdByVolunteer 를 구현한다.

* feat: 보호 동물 리스트 조회 시 createdAt 기준으로 정렬한다.

* fix: 잘못된 count 쿼리 사용을 수정한다.
  • Loading branch information
pushedrumex authored Nov 8, 2023
1 parent a502325 commit d24a57f
Show file tree
Hide file tree
Showing 10 changed files with 605 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.clova.anifriends.domain.common.EnumType;

public enum AnimalAge implements EnumType {
BABY(0, 6),
JUNIOR(7, 35),
ADULT(36, 83),
SENIOR(84, Integer.MAX_VALUE);
BABY(0, 7),
JUNIOR(7, 36),
ADULT(36, 108),
SENIOR(108, Integer.MAX_VALUE);

private final int minMonth;
private final int maxMonth;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.clova.anifriends.domain.animal.controller;

import com.clova.anifriends.domain.animal.dto.request.FindAnimalsByShelterRequest;
import com.clova.anifriends.domain.animal.dto.request.FindAnimalsByVolunteerRequest;
import com.clova.anifriends.domain.animal.dto.request.RegisterAnimalRequest;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalByShelterResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalByVolunteerResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsByShelterResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsByVolunteerResponse;
import com.clova.anifriends.domain.animal.dto.response.RegisterAnimalResponse;
import com.clova.anifriends.domain.animal.service.AnimalService;
import com.clova.anifriends.domain.auth.resolver.LoginUser;
Expand Down Expand Up @@ -69,4 +71,20 @@ public ResponseEntity<FindAnimalsByShelterResponse> findAnimalsByShelter(
pageable
));
}

@GetMapping("/volunteers/animals")
public ResponseEntity<FindAnimalsByVolunteerResponse> findAnimalsByVolunteer(
Pageable pageable,
@ModelAttribute FindAnimalsByVolunteerRequest findAnimalsByVolunteerRequest
) {
return ResponseEntity.ok(animalService.findAnimalsByVolunteer(
findAnimalsByVolunteerRequest.type(),
findAnimalsByVolunteerRequest.active(),
findAnimalsByVolunteerRequest.isNeutered(),
findAnimalsByVolunteerRequest.age(),
findAnimalsByVolunteerRequest.gender(),
findAnimalsByVolunteerRequest.size(),
pageable
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.clova.anifriends.domain.animal.dto.request;

import com.clova.anifriends.domain.animal.AnimalAge;
import com.clova.anifriends.domain.animal.AnimalSize;
import com.clova.anifriends.domain.animal.wrapper.AnimalActive;
import com.clova.anifriends.domain.animal.wrapper.AnimalGender;
import com.clova.anifriends.domain.animal.wrapper.AnimalType;

public record FindAnimalsByVolunteerRequest(
AnimalType type,
AnimalGender gender,
Boolean isNeutered,
AnimalActive active,
AnimalSize size,
AnimalAge age
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.clova.anifriends.domain.animal.dto.response;

import com.clova.anifriends.domain.animal.Animal;
import com.clova.anifriends.domain.common.PageInfo;
import java.util.List;
import org.springframework.data.domain.Page;

public record FindAnimalsByVolunteerResponse(
PageInfo pageInfo,
List<FindAnimalByVolunteerResponse> animals
) {

public record FindAnimalByVolunteerResponse(
Long animalId,
String animalName,
String shelterName,
String shelterAddress,
String animalImageUrl
) {

public static FindAnimalByVolunteerResponse from(Animal animal) {
return new FindAnimalByVolunteerResponse(
animal.getAnimalId(),
animal.getName(),
animal.getShelter().getName(),
animal.getShelter().getAddress(),
animal.getImageUrls().get(0)
);
}
}

public static FindAnimalsByVolunteerResponse from(Page<Animal> pagination) {
PageInfo pageInfo = PageInfo.of(pagination.getTotalElements(), pagination.hasNext());
List<FindAnimalByVolunteerResponse> findAnimalByVolunteerResponses = pagination.get()
.map(FindAnimalByVolunteerResponse::from).toList();

return new FindAnimalsByVolunteerResponse(pageInfo, findAnimalByVolunteerResponses);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ Page<Animal> findAnimalsByShelter(
AnimalAge age,
Pageable pageable
);

Page<Animal> findAnimalsByVolunteer(
AnimalType type,
AnimalActive active,
Boolean isNeutered,
AnimalAge age,
AnimalGender gender,
AnimalSize size,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public Page<Animal> findAnimalsByShelter(
animalSizeContains(size),
animalAgeContains(age)
)
.orderBy(animal.createdAt.desc())
.limit(pageable.getPageSize())
.offset(pageable.getOffset())
.fetch();
Expand All @@ -67,7 +68,48 @@ public Page<Animal> findAnimalsByShelter(
)
.fetchOne();

return new PageImpl<>(animals, pageable, count);
return new PageImpl<>(animals, pageable, count == null ? 0 : count);
}

@Override
public Page<Animal> findAnimalsByVolunteer(
AnimalType type,
AnimalActive active,
Boolean isNeutered,
AnimalAge age,
AnimalGender gender,
AnimalSize size,
Pageable pageable
) {
List<Animal> animals = query.selectFrom(animal)
.join(animal.shelter)
.where(
animalTypeContains(type),
animalActiveContains(active),
animalIsNeutered(isNeutered),
animalAgeContains(age),
animalGenderContains(gender),
animalSizeContains(size)
)
.orderBy(animal.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

Long count = query.select(animal.count())
.from(animal)
.join(animal.shelter)
.where(
animalTypeContains(type),
animalActiveContains(active),
animalIsNeutered(isNeutered),
animalAgeContains(age),
animalGenderContains(gender),
animalSizeContains(size)
)
.fetchOne();

return new PageImpl<>(animals, pageable, count == null ? 0 : count);
}

private BooleanExpression animalNameContains(String keyword) {
Expand All @@ -77,25 +119,25 @@ private BooleanExpression animalNameContains(String keyword) {
private BooleanExpression animalTypeContains(
AnimalType type
) {
return type != null ? animal.type.stringValue().eq(type.getName()) : null;
return type != null ? animal.type.eq(type) : null;
}

private BooleanExpression animalGenderContains(
AnimalGender gender
) {
return gender != null ? animal.gender.stringValue().eq(gender.getName()) : null;
return gender != null ? animal.gender.eq(gender) : null;
}

private BooleanExpression animalIsNeutered(
Boolean isNeuteredFilter
Boolean isNeutered
) {
return isNeuteredFilter != null ? animal.neutered.isNeutered.eq(isNeuteredFilter) : null;
return isNeutered != null ? animal.neutered.isNeutered.eq(isNeutered) : null;
}

private BooleanExpression animalActiveContains(
AnimalActive active
) {
return active != null ? animal.active.stringValue().contains(active.getName()) : null;
return active != null ? animal.active.eq(active) : null;
}

private BooleanExpression animalSizeContains(
Expand All @@ -108,7 +150,8 @@ private BooleanExpression animalSizeContains(
int minWeight = size.getMinWeight();
int maxWeight = size.getMaxWeight();

return animal.weight.weight.between(minWeight, maxWeight);
return animal.weight.weight.goe(minWeight)
.and(animal.weight.weight.lt(maxWeight));
}

private BooleanExpression animalAgeContains(
Expand All @@ -125,6 +168,7 @@ private BooleanExpression animalAgeContains(
LocalDate minDate = currentDate.minusMonths(minMonth);
LocalDate maxDate = currentDate.minusMonths(maxMonth);

return animal.birthDate.between(maxDate, minDate);
return animal.birthDate.gt(maxDate)
.and(animal.birthDate.loe(minDate));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.clova.anifriends.domain.animal.dto.response.FindAnimalByShelterResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalByVolunteerResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsByShelterResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsByVolunteerResponse;
import com.clova.anifriends.domain.animal.dto.response.RegisterAnimalResponse;
import com.clova.anifriends.domain.animal.exception.AnimalNotFoundException;
import com.clova.anifriends.domain.animal.mapper.AnimalMapper;
Expand Down Expand Up @@ -82,6 +83,29 @@ public FindAnimalsByShelterResponse findAnimalsByShelter(
return FindAnimalsByShelterResponse.from(animals);
}


public FindAnimalsByVolunteerResponse findAnimalsByVolunteer(
AnimalType type,
AnimalActive active,
Boolean isNeutered,
AnimalAge age,
AnimalGender gender,
AnimalSize size,
Pageable pageable
) {
Page<Animal> animalsWithPagination = animalRepository.findAnimalsByVolunteer(
type,
active,
isNeutered,
age,
gender,
size,
pageable
);

return FindAnimalsByVolunteerResponse.from(animalsWithPagination);
}

private Animal getAnimalById(Long animalId) {
return animalRepository.findById(animalId)
.orElseThrow(() -> new AnimalNotFoundException("존재하지 않는 보호 동물입니다."));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@
import com.clova.anifriends.base.BaseControllerTest;
import com.clova.anifriends.docs.format.DocumentationFormatGenerator;
import com.clova.anifriends.domain.animal.Animal;
import com.clova.anifriends.domain.animal.AnimalAge;
import com.clova.anifriends.domain.animal.AnimalSize;
import com.clova.anifriends.domain.animal.dto.request.RegisterAnimalRequest;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalByShelterResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalByVolunteerResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsByShelterResponse;
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsByVolunteerResponse;
import com.clova.anifriends.domain.animal.dto.response.RegisterAnimalResponse;
import com.clova.anifriends.domain.animal.support.fixture.AnimalFixture;
import com.clova.anifriends.domain.animal.wrapper.AnimalActive;
Expand All @@ -43,9 +46,12 @@
import com.clova.anifriends.domain.shelter.Shelter;
import com.clova.anifriends.domain.shelter.support.ShelterFixture;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.ResultActions;
Expand Down Expand Up @@ -275,4 +281,88 @@ void findAnimalsByShelter() throws Exception {
)
));
}

@Test
@DisplayName("보호 동물 조회 & 검색(봉사자) api 호출 시")
void findAnimalsByVolunteer() throws Exception {
// given
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("type", AnimalType.DOG.name());
params.add("gender", AnimalGender.FEMALE.name());
params.add("isNeutered", String.valueOf(true));
params.add("active", AnimalActive.ACTIVE.name());
params.add("size", AnimalSize.SMALL.name());
params.add("age", AnimalAge.ADULT.name());
params.add("pageNumber", String.valueOf(0));
params.add("pageSize", String.valueOf(10));

Shelter shelter = shelter();
Animal animal = animal(shelter);
ReflectionTestUtils.setField(animal, "animalId", 1L);

FindAnimalsByVolunteerResponse response = FindAnimalsByVolunteerResponse
.from(new PageImpl<>(List.of(animal)));

when(animalService.findAnimalsByVolunteer(
any(AnimalType.class),
any(AnimalActive.class),
anyBoolean(),
any(AnimalAge.class),
any(AnimalGender.class),
any(AnimalSize.class),
any(Pageable.class))
).thenReturn(response);

// when
ResultActions resultActions = mockMvc.perform(get("/api/volunteers/animals")
.header(AUTHORIZATION, volunteerAccessToken)
.params(params));

// then
resultActions.andExpect(status().isOk())
.andDo(restDocs.document(
requestHeaders(
headerWithName(AUTHORIZATION).description("액세스 토큰")
),
queryParameters(
parameterWithName("type").description("보호 동물 종류").optional()
.attributes(DocumentationFormatGenerator.getConstraint(
String.join(", ", Arrays.stream(AnimalType.values()).map(
AnimalType::name).toArray(String[]::new)))),
parameterWithName("gender").description("보호 동물 성별").optional()
.attributes(DocumentationFormatGenerator.getConstraint(
String.join(", ", Arrays.stream(AnimalGender.values()).map(
AnimalGender::name).toArray(String[]::new)))),
parameterWithName("isNeutered").description("보호 동물 중성화 여부").optional()
.attributes(DocumentationFormatGenerator.getConstraint("true, false")),
parameterWithName("active").description("보호 동물 성격").optional()
.attributes(DocumentationFormatGenerator.getConstraint(
String.join(", ", Arrays.stream(AnimalActive.values()).map(
AnimalActive::name).toArray(String[]::new)))),
parameterWithName("size").description("보호 동물 크기").optional()
.attributes(
DocumentationFormatGenerator.getConstraint(
String.join(", ", Arrays.stream(AnimalSize.values()).map(
AnimalSize::name).toArray(String[]::new)))),
parameterWithName("age").description("보호 동물 나이").optional()
.attributes(DocumentationFormatGenerator.getConstraint(
String.join(", ", Arrays.stream(AnimalAge.values()).map(
AnimalAge::name).toArray(String[]::new)))),
parameterWithName("pageNumber").description("페이지 번호"),
parameterWithName("pageSize").description("페이지 사이즈")
),
responseFields(
fieldWithPath("pageInfo").type(OBJECT).description("페이지 정보"),
fieldWithPath("pageInfo.totalElements").type(NUMBER).description("총 요소 개수"),
fieldWithPath("pageInfo.hasNext").type(BOOLEAN).description("다음 페이지 여부"),
fieldWithPath("animals").type(ARRAY).description("보호 동물 리스트"),
fieldWithPath("animals[].animalId").type(NUMBER).description("보호 동물 ID"),
fieldWithPath("animals[].animalName").type(STRING).description("보호 동물 이름"),
fieldWithPath("animals[].shelterName").type(STRING).description("보호소 이름"),
fieldWithPath("animals[].shelterAddress").type(STRING).description("보호소 주소"),
fieldWithPath("animals[].animalImageUrl").type(STRING)
.description("보호 동물 이미지 url")
)
));
}
}
Loading

0 comments on commit d24a57f

Please sign in to comment.