diff --git a/be/src/main/java/yeonba/be/exception/JoinException.java b/be/src/main/java/yeonba/be/exception/JoinException.java index 0faad4be..43d97cff 100644 --- a/be/src/main/java/yeonba/be/exception/JoinException.java +++ b/be/src/main/java/yeonba/be/exception/JoinException.java @@ -1,13 +1,13 @@ package yeonba.be.exception; +import lombok.AllArgsConstructor; +import lombok.Getter; import org.springframework.http.HttpStatus; +@Getter +@AllArgsConstructor public enum JoinException implements BaseException { - PASSWORD_CONFIRMATION_NOT_MATCH( - HttpStatus.BAD_REQUEST, - "비밀번호 확인 값이 비밀번호와 일치하지 않습니다."), - ALREADY_USED_NICKNAME( HttpStatus.BAD_REQUEST, "이미 사용 중인 닉네임입니다."), @@ -18,22 +18,4 @@ public enum JoinException implements BaseException { private final HttpStatus httpStatus; private final String reason; - - JoinException(HttpStatus httpStatus, String reason) { - - this.httpStatus = httpStatus; - this.reason = reason; - } - - @Override - public HttpStatus getHttpStatus() { - - return httpStatus; - } - - @Override - public String getReason() { - - return reason; - } } diff --git a/be/src/main/java/yeonba/be/mypage/entity/Acquaintance.java b/be/src/main/java/yeonba/be/mypage/entity/Acquaintance.java index ac9edd20..e9a1f32b 100644 --- a/be/src/main/java/yeonba/be/mypage/entity/Acquaintance.java +++ b/be/src/main/java/yeonba/be/mypage/entity/Acquaintance.java @@ -1,10 +1,12 @@ package yeonba.be.mypage.entity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -14,7 +16,7 @@ @Getter @Entity @EqualsAndHashCode(of = {"userId", "phoneNumber"}) -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Acquaintance { @@ -22,12 +24,16 @@ public class Acquaintance { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) private long userId; + + @Column(nullable = false) private String name; + + @Column(nullable = false) private String phoneNumber; public Acquaintance(long userId, String name, String phoneNumber) { - this.userId = userId; this.name = name; this.phoneNumber = phoneNumber; diff --git a/be/src/main/java/yeonba/be/mypage/service/MyPageService.java b/be/src/main/java/yeonba/be/mypage/service/MyPageService.java index 395e41db..990400b8 100644 --- a/be/src/main/java/yeonba/be/mypage/service/MyPageService.java +++ b/be/src/main/java/yeonba/be/mypage/service/MyPageService.java @@ -1,9 +1,10 @@ package yeonba.be.mypage.service; +import static yeonba.be.util.BoundsValidator.validateBounds; + import java.time.LocalDate; import java.time.Period; import java.util.List; -import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -35,10 +36,10 @@ import yeonba.be.user.entity.User; import yeonba.be.user.entity.UserPreference; import yeonba.be.user.entity.VocalRange; -import yeonba.be.user.repository.block.BlockCommand; -import yeonba.be.user.repository.block.BlockQuery; import yeonba.be.user.repository.animal.AnimalQuery; import yeonba.be.user.repository.area.AreaQuery; +import yeonba.be.user.repository.block.BlockCommand; +import yeonba.be.user.repository.block.BlockQuery; import yeonba.be.user.repository.user.UserQuery; import yeonba.be.user.repository.userpreference.UserPreferenceQuery; import yeonba.be.user.repository.vocalrange.VocalRangeQuery; @@ -172,18 +173,6 @@ private Area findAreaByName(List areas, String name) { .orElseThrow(() -> new GeneralException(UserException.AREA_NOT_FOUND)); } - private void validateBounds(Integer lowerBound, Integer upperBound) { - - if (Objects.isNull(lowerBound) || Objects.isNull(upperBound)) { - - return; - } - - if (lowerBound > upperBound) { - throw new GeneralException(UserException.LOWER_BOUND_LESS_THAN_OR_EQUAL_UPPER_BOUND); - } - } - public void updateProfilePhotos(List profilePhotos, MultipartFile realTimePhoto, long userId) { diff --git a/be/src/main/java/yeonba/be/user/controller/UserController.java b/be/src/main/java/yeonba/be/user/controller/UserController.java index d867e423..3d47b36e 100644 --- a/be/src/main/java/yeonba/be/user/controller/UserController.java +++ b/be/src/main/java/yeonba/be/user/controller/UserController.java @@ -20,6 +20,7 @@ import yeonba.be.mypage.service.ReportService; import yeonba.be.user.dto.request.UserQueryRequest; import yeonba.be.user.dto.request.UserReportRequest; +import yeonba.be.user.dto.request.UserSearchRequest; import yeonba.be.user.dto.request.UserUpdateDeviceTokenRequest; import yeonba.be.user.dto.response.UserProfileResponse; import yeonba.be.user.dto.response.UserQueryPageResponse; @@ -54,7 +55,7 @@ public ResponseEntity> getUsers( @Operation(summary = "추천 이성 조회", description = "추천 이성을 조회할 수 있다.") @ApiResponse(responseCode = "200", description = "추천 이성 정상 조회") - @GetMapping("/users/recommend") + @PostMapping("/users/recommend") public ResponseEntity> getRecommendUsers( @RequestAttribute("userId") long userId) { @@ -145,6 +146,20 @@ public ResponseEntity> block( .body(new CustomResponse<>()); } + @Operation(summary = "이성 검색", description = "이성을 검색할 수 있습니다.") + @ApiResponse(responseCode = "200", description = "이성 검색 성공") + @PostMapping("/users/search") + public ResponseEntity> search( + @RequestAttribute("userId") long userId, + @Valid @RequestBody(required = false) UserSearchRequest request) { + + UserQueryPageResponse response = userService.findUsersBySearchCondition(userId, request); + + return ResponseEntity + .ok() + .body(new CustomResponse<>(response)); + } + @Operation(summary = "device token 업데이트", description = "device token을 업데이트할 수 있습니다.") @ApiResponse(responseCode = "200", description = "device token 업데이트 성공") @PatchMapping("/users/device-token") diff --git a/be/src/main/java/yeonba/be/user/dto/request/UserSearchRequest.java b/be/src/main/java/yeonba/be/user/dto/request/UserSearchRequest.java new file mode 100644 index 00000000..f044b21a --- /dev/null +++ b/be/src/main/java/yeonba/be/user/dto/request/UserSearchRequest.java @@ -0,0 +1,65 @@ +package yeonba.be.user.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +@Getter +@NoArgsConstructor +public class UserSearchRequest { + + @Schema( + type = "number", + description = "페이지 번호, 기본 첫 페이지(0)", + example = "0") + @PositiveOrZero(message = "페이지 번호는 0이상이어야 합니다.") + private Integer page; + + @Schema( + type = "string", + description = "검색 지역", + example = "서울") + private String area; + + @Schema( + type = "string", + description = "검색 음역대", + example = "고음") + private String vocalRange; + + @Schema( + type = "number", + description = "검색 나이 하한", + example = "20") + @Range(min = 20, max = 40, message = "검색 나이는 20~40내 값만 가능합니다.") + private Integer ageLowerBound; + + @Schema( + type = "number", + description = "검색 나이 상한", + example = "30") + @Range(min = 20, max = 40, message = "검색 나이는 20~40내 값만 가능합니다.") + private Integer ageUpperBound; + + @Schema( + type = "number", + description = "검색 키 하한", + example = "160") + @Range(min = 130, max = 220, message = "검색 키는 130~220cm 내 값만 가능합니다.") + private Integer heightLowerBound; + + @Schema( + type = "number", + description = "검색 키 상한", + example = "170") + @Range(min = 130, max = 220, message = "검색 키는 130~220cm 내 값만 가능합니다.") + private Integer heightUpperBound; + + @Schema( + type = "boolean", + description = "검색 기준에 선호하는 동물상 포함 여부", + example = "true") + private Boolean includePreferredAnimal; +} diff --git a/be/src/main/java/yeonba/be/user/dto/response/UserQueryPageResponse.java b/be/src/main/java/yeonba/be/user/dto/response/UserQueryPageResponse.java index c515089f..e678a83b 100644 --- a/be/src/main/java/yeonba/be/user/dto/response/UserQueryPageResponse.java +++ b/be/src/main/java/yeonba/be/user/dto/response/UserQueryPageResponse.java @@ -3,12 +3,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.data.domain.Page; @Getter -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class UserQueryPageResponse { @Schema( diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index a29e6a69..6dca8fb3 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -215,7 +215,6 @@ public void changeInactiveStatus(boolean inactiveStatus) { } public void updateProfilePhotos(List profilePhotos) { - this.profilePhotos = profilePhotos; } diff --git a/be/src/main/java/yeonba/be/user/entity/UserPreference.java b/be/src/main/java/yeonba/be/user/entity/UserPreference.java index 3589515b..d78e34d9 100644 --- a/be/src/main/java/yeonba/be/user/entity/UserPreference.java +++ b/be/src/main/java/yeonba/be/user/entity/UserPreference.java @@ -54,10 +54,10 @@ public class UserPreference { private Animal animal; public UserPreference( - int ageLowerBound, - int ageUpperBound, - int heightLowerBound, - int heightUpperBound, + Integer ageLowerBound, + Integer ageUpperBound, + Integer heightLowerBound, + Integer heightUpperBound, String mbti, String bodyType, User user, diff --git a/be/src/main/java/yeonba/be/user/enums/Gender.java b/be/src/main/java/yeonba/be/user/enums/Gender.java index 0b2e69f9..ce7df7c6 100644 --- a/be/src/main/java/yeonba/be/user/enums/Gender.java +++ b/be/src/main/java/yeonba/be/user/enums/Gender.java @@ -13,6 +13,7 @@ public enum Gender { public final String genderString; public final boolean genderBoolean; + public static Gender from(String genderString) { if (StringUtils.equals(genderString, MALE.genderString)) { diff --git a/be/src/main/java/yeonba/be/user/repository/user/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/user/UserQuery.java index 877da017..e2ce93eb 100644 --- a/be/src/main/java/yeonba/be/user/repository/user/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/user/UserQuery.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; import yeonba.be.exception.GeneralException; import yeonba.be.exception.UserException; +import yeonba.be.user.dto.request.UserSearchRequest; import yeonba.be.user.dto.response.UserQueryPageResponse; import yeonba.be.user.dto.response.UserQueryResponse; import yeonba.be.user.entity.User; @@ -73,10 +74,22 @@ public List findByIds(List userIds) { } public UserQueryPageResponse findRecommendUsers( - long userId, boolean userGender, PageRequest pageRequest, LocalDate recommendDay) { + User user, PageRequest pageRequest, LocalDate recommendDay) { Page page = userRepository - .findRecommendUsers(userId, userGender, pageRequest, recommendDay); + .findRecommendUsers(user, pageRequest, recommendDay); + + return UserQueryPageResponse.from(page); + } + + public UserQueryPageResponse findUsersBySearchCondition( + User user, + PageRequest pageRequest, + LocalDate searchDay, + UserSearchRequest request) { + + Page page = userRepository + .findUsersBySearchCondition(user, pageRequest, searchDay, request); return UserQueryPageResponse.from(page); } diff --git a/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryCustom.java b/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryCustom.java index 30ca5753..cf3a1600 100644 --- a/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryCustom.java +++ b/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryCustom.java @@ -3,7 +3,9 @@ import java.time.LocalDate; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import yeonba.be.user.dto.request.UserSearchRequest; import yeonba.be.user.dto.response.UserQueryResponse; +import yeonba.be.user.entity.User; public interface UserRepositoryCustom { @@ -14,8 +16,13 @@ public interface UserRepositoryCustom { Page findArrowSendersBy(long receiverId, PageRequest pageRequest); Page findRecommendUsers( - long userId, - boolean userGender, + User queryingUser, PageRequest pageRequest, LocalDate recommendDay); -} \ No newline at end of file + + Page findUsersBySearchCondition( + User searchingUser, + PageRequest pageRequest, + LocalDate searchDay, + UserSearchRequest request); +} diff --git a/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryImpl.java b/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryImpl.java index be1e39c8..061dac0a 100644 --- a/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryImpl.java +++ b/be/src/main/java/yeonba/be/user/repository/user/UserRepositoryImpl.java @@ -25,11 +25,16 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.util.StringUtils; +import yeonba.be.arrow.enums.ArrowTransactionType; +import yeonba.be.user.dto.request.UserSearchRequest; import yeonba.be.user.dto.response.UserQueryResponse; +import yeonba.be.user.entity.User; import yeonba.be.user.entity.UserPreference; @RequiredArgsConstructor @@ -43,8 +48,7 @@ public Page findFavoritesBy(long userId, PageRequest pageRequ int limit = pageRequest.getPageSize(); int offset = pageRequest.getPageNumber() * limit; - List content = selectUserQueryResponse( - Expressions.constant(true)) + List content = selectUserQueryResponse(Expressions.constant(true)) .where(findFavoritesCondition(userId)) .limit(limit) .offset(offset) @@ -125,8 +129,7 @@ private BooleanExpression findArrowSendersCondition(long receiverId) { @Override public Page findRecommendUsers( - long userId, - boolean userGender, + User queryingUser, PageRequest pageRequest, LocalDate recommendDay) { @@ -135,27 +138,26 @@ public Page findRecommendUsers( // 추천 대상 사용자의 선호조건 조회 UserPreference preference = queryFactory.selectFrom(userPreference) - .where(userPreference.user.id.eq(userId)) + .where(userPreference.user.id.eq(queryingUser.getId())) .fetchFirst(); List content = selectUserQueryResponse( Expressions.constant(false)) - .where(recommendUserCondition(userId, userGender, preference, recommendDay)) + .where(recommendUserCondition(queryingUser, preference, recommendDay)) .limit(limit) .offset(offset) .fetch(); JPAQuery countQuery = queryFactory.select(user.count()) .from(user) - .where(recommendUserCondition(userId, userGender, preference, recommendDay)); + .where(recommendUserCondition(queryingUser, preference, recommendDay)); return PageableExecutionUtils.getPage(content, pageRequest, countQuery::fetchOne); } /* 이성 추천시 배제되는 사용자 - - 자기 자신(조회하는 사용자) - - 동성 + - 같은 성별 사용자 - 추천(선호) 조건을 만족하지 않는 사용자 - 화살을 주고 받은 적이 있는 사용자 - 즐겨찾기한 사용자 @@ -165,14 +167,14 @@ public Page findRecommendUsers( - 추천 일자에 검색된 적 있는 사용자 */ private BooleanExpression recommendUserCondition( - long userId, - Boolean gender, + User queryingUser, UserPreference preference, LocalDate recommendDay) { + long userId = queryingUser.getId(); + return Expressions.allOf( - user.id.ne(userId), - user.gender.ne(gender), + notSameGenderCondition(queryingUser.getGenderBoolean()), isActiveAndNotDeletedUserCondition(), isNotAcquaintanceCondition(userId), isNotBlockedUserCondition(userId), @@ -184,22 +186,115 @@ private BooleanExpression recommendUserCondition( isNotUserSearchedOnDayCondition(userId, recommendDay)); } - private JPQLQuery findOneArrowReceivedTransactionBy(long receiverId) { + @Override + public Page findUsersBySearchCondition( + User searchingUser, + PageRequest pageRequest, + LocalDate searchDay, + UserSearchRequest request) { - return JPAExpressions.selectOne() - .from(arrowTransaction) - .where( - arrowTransaction.receiver.id.eq(receiverId), - arrowTransaction.sender.id.eq(user.id)); + int limit = pageRequest.getPageSize(); + int offset = pageRequest.getPageNumber() * limit; + + BooleanExpression searchUserCondition = + searchUserCondition(searchingUser, request, searchDay); + + List content = selectUserQueryResponse( + Expressions.as(findOneFavoriteBy(searchingUser.getId()).exists(), "isFavorite")) + .from(user) + .where(searchUserCondition) + .limit(limit) + .offset(offset) + .fetch(); + + JPAQuery countQuery = queryFactory.select(user.count()) + .from(user) + .where(searchUserCondition); + + return PageableExecutionUtils.getPage(content, pageRequest, countQuery::fetchOne); + } + + /* + 이성 검색시 제외되는 사용자 + - 같은 성별 사용자 + - 지인 + - 차단한 사용자 + - 휴면, 삭제 상태 사용자 + - 화실을 주고 받은 적 있는 사용자 + - 검색일에 이미 검색된 적 있는 사용자 + */ + // TODO : 채팅 이력 있는 사용자 제외 조건 추가 + private BooleanExpression searchUserCondition( + User searchingUser, UserSearchRequest request, LocalDate searchDay) { + + long userId = searchingUser.getId(); + + return Expressions.allOf( + notSameGenderCondition(searchingUser.getGenderBoolean()), + isNotAcquaintanceCondition(userId), + isNotBlockedUserCondition(userId), + isActiveAndNotDeletedUserCondition(), + findOneArrowReceivedTransactionBy(userId).notExists(), + findOneArrowSentTransactionBy(userId).notExists(), + isNotUserRecommendedOnDayCondition(userId, searchDay), + isNotUserSearchedOnDayCondition(userId, searchDay), + searchCondition(userId, request) + ); + } + + private BooleanExpression searchCondition(long userId, UserSearchRequest request) { + + if (Objects.isNull(request)) { + + return null; + } + + String area = request.getArea(); + BooleanExpression userAreaEqualCondition = + StringUtils.hasText(area) ? user.area.name.eq(area) : null; + + String vocalRange = request.getVocalRange(); + BooleanExpression userVocalRangeEqualCondition = + StringUtils.hasText(vocalRange) ? user.vocalRange.classification.eq(vocalRange) : null; + + BooleanExpression userAgeRangeCondition = + userAgeRangeCondition(request.getAgeLowerBound(), request.getAgeUpperBound()); + + BooleanExpression userHeightRangeCondition = + userHeightRangeCondition(request.getHeightLowerBound(), request.getHeightUpperBound()); + + BooleanExpression includePreferredAnimalCondition = + includePreferredAnimalCondition(userId, request.getIncludePreferredAnimal()); + + return Expressions.allOf( + userAreaEqualCondition, + userVocalRangeEqualCondition, + userAgeRangeCondition, + userHeightRangeCondition, + includePreferredAnimalCondition); + } + + private BooleanExpression includePreferredAnimalCondition( + long userId, Boolean includePreferredAnimal) { + + if (Objects.isNull(includePreferredAnimal) || !includePreferredAnimal) { + + return null; + } + + Long preferredAnimalId = queryFactory.select(userPreference.animal.id) + .from(userPreference) + .where(userPreference.user.id.eq(userId)) + .fetchFirst(); + + return Objects.nonNull(preferredAnimalId) ? user.animal.id.eq(preferredAnimalId) : null; } private JPQLQuery findOneFavoriteBy(long userId) { return JPAExpressions.selectOne() .from(favorite) - .where( - favorite.user.id.eq(userId), - favorite.favoriteUser.id.eq(user.id)); + .where(favorite.user.id.eq(userId), favorite.favoriteUser.id.eq(user.id)); } private JPQLQuery findOneArrowSentTransactionBy(long senderId) { @@ -207,20 +302,33 @@ private JPQLQuery findOneArrowSentTransactionBy(long senderId) { return JPAExpressions.selectOne() .from(arrowTransaction) .where( + arrowTransaction.type.eq(ArrowTransactionType.USER_TO_USER), arrowTransaction.sender.id.eq(senderId), arrowTransaction.receiver.id.eq(user.id)); } + private JPQLQuery findOneArrowReceivedTransactionBy(long receiverId) { + + return JPAExpressions.selectOne() + .from(arrowTransaction) + .where( + arrowTransaction.type.eq(ArrowTransactionType.USER_TO_USER), + arrowTransaction.receiver.id.eq(receiverId), + arrowTransaction.sender.id.eq(user.id)); + } + private BooleanExpression isUserSatisfiedPreferenceCondition(UserPreference preference) { return Expressions.allOf( - user.age.between(preference.getAgeLowerBound(), preference.getAgeUpperBound()), - user.height.between(preference.getHeightLowerBound(), preference.getHeightUpperBound()), user.mbti.eq(preference.getMbti()), user.bodyType.eq(preference.getBodyType()), user.vocalRange.id.eq(preference.getVocalRange().getId()), user.area.id.eq(preference.getArea().getId()), - user.animal.id.eq(preference.getAnimal().getId())); + user.animal.id.eq(preference.getAnimal().getId()), + userAgeRangeCondition(preference.getAgeLowerBound(), preference.getAgeUpperBound()), + userHeightRangeCondition( + preference.getHeightLowerBound(), preference.getHeightUpperBound()) + ); } private BooleanExpression isNotUserRecommendedOnDayCondition( @@ -256,9 +364,8 @@ private BooleanExpression isNotUserSearchedOnDayCondition( } /* - 응답 dto에 필요한 필드를 select하는 공통 사용 쿼리, 별도 분리 - 경우에 따라 즐겨찾기 등록 여부(favorite)을 상수로 주입하기에 - 해당 부분만 파라미터로 받도록 구성 + 응답 dto에 필요한 필드를 select하는 공통 사용 쿼리, 사용하는 로직에 따라 + 조회하는 사용자, 조회되는 사용자간 즐겨찾기 존재 여부를 상수로 주입, 혹은 서브 쿼리로 확인 */ private JPAQuery selectUserQueryResponse( Expression checkFavoriteExistsNestedQuery) { @@ -285,6 +392,11 @@ private JPAQuery selectUserQueryResponse( .where(isRepresentativeProfilePhotoCondition()); } + private BooleanExpression notSameGenderCondition(boolean userGender) { + + return user.gender.ne(userGender); + } + private BooleanExpression isRepresentativeProfilePhotoCondition() { return profilePhoto.id.eq( @@ -295,8 +407,7 @@ private BooleanExpression isRepresentativeProfilePhotoCondition() { private BooleanExpression isActiveAndNotDeletedUserCondition() { - return user.deleted.isFalse() - .and(user.inactive.isFalse()); + return user.deleted.isFalse().and(user.inactive.isFalse()); } private BooleanExpression isNotAcquaintanceCondition(long userId) { @@ -318,4 +429,26 @@ private BooleanExpression isNotBlockedUserCondition(long userId) { block.blockedUser.id.eq(user.id)) .notExists(); } -} \ No newline at end of file + + private BooleanExpression userAgeRangeCondition( + Integer ageLowerBound, Integer ageUpperBound) { + + BooleanExpression userAgeGoeCondition = + Objects.nonNull(ageLowerBound) ? user.age.goe(ageLowerBound) : null; + BooleanExpression userAgeLoeCondition = + Objects.nonNull(ageUpperBound) ? user.age.loe(ageUpperBound) : null; + + return Expressions.allOf(userAgeLoeCondition, userAgeGoeCondition); + } + + private BooleanExpression userHeightRangeCondition( + Integer heightLowerBound, Integer heightUpperBound) { + + BooleanExpression userHeightGoeCondition = + Objects.nonNull(heightLowerBound) ? user.height.goe(heightLowerBound) : null; + BooleanExpression userHeightLoeCondition = + Objects.nonNull(heightUpperBound) ? user.height.loe(heightUpperBound) : null; + + return Expressions.allOf(userHeightGoeCondition, userHeightLoeCondition); + } +} diff --git a/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java index 43f1ad8d..b6d7fc86 100644 --- a/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/userpreference/UserPreferenceRepository.java @@ -10,5 +10,4 @@ public interface UserPreferenceRepository extends JpaRepository { Optional findFirstByUser(User user); - } diff --git a/be/src/main/java/yeonba/be/user/repository/usersearchlog/UserSearchLogCommand.java b/be/src/main/java/yeonba/be/user/repository/usersearchlog/UserSearchLogCommand.java new file mode 100644 index 00000000..e57b7965 --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/usersearchlog/UserSearchLogCommand.java @@ -0,0 +1,18 @@ +package yeonba.be.user.repository.usersearchlog; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import yeonba.be.user.entity.UserSearchLog; + +@Component +@RequiredArgsConstructor +public class UserSearchLogCommand { + + private final UserSearchLogRepository userSearchLogRepository; + + public List saveAll(List userSearchLogs) { + + return userSearchLogRepository.saveAll(userSearchLogs); + } +} diff --git a/be/src/main/java/yeonba/be/user/repository/usersearchlog/UserSearchLogRepository.java b/be/src/main/java/yeonba/be/user/repository/usersearchlog/UserSearchLogRepository.java new file mode 100644 index 00000000..8a67b2cf --- /dev/null +++ b/be/src/main/java/yeonba/be/user/repository/usersearchlog/UserSearchLogRepository.java @@ -0,0 +1,10 @@ +package yeonba.be.user.repository.usersearchlog; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import yeonba.be.user.entity.UserSearchLog; + +@Repository +public interface UserSearchLogRepository extends JpaRepository { + +} diff --git a/be/src/main/java/yeonba/be/user/service/UserService.java b/be/src/main/java/yeonba/be/user/service/UserService.java index 991c44b7..081a0127 100644 --- a/be/src/main/java/yeonba/be/user/service/UserService.java +++ b/be/src/main/java/yeonba/be/user/service/UserService.java @@ -1,8 +1,11 @@ package yeonba.be.user.service; +import static yeonba.be.util.BoundsValidator.validateBounds; + import java.time.LocalDate; import java.time.Period; import java.util.List; +import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -16,6 +19,7 @@ import yeonba.be.exception.UserException; import yeonba.be.login.dto.request.UserJoinRequest; import yeonba.be.user.dto.request.UserQueryRequest; +import yeonba.be.user.dto.request.UserSearchRequest; import yeonba.be.user.dto.request.UserUpdateDeviceTokenRequest; import yeonba.be.user.dto.response.UserProfileResponse; import yeonba.be.user.dto.response.UserQueryPageResponse; @@ -26,6 +30,7 @@ import yeonba.be.user.entity.User; import yeonba.be.user.entity.UserPreference; import yeonba.be.user.entity.UserRecommendation; +import yeonba.be.user.entity.UserSearchLog; import yeonba.be.user.entity.VocalRange; import yeonba.be.user.enums.Gender; import yeonba.be.user.enums.LoginType; @@ -37,6 +42,7 @@ import yeonba.be.user.repository.userpreference.UserPreferenceCommand; import yeonba.be.user.repository.userrecommendation.UserRecommendationCommand; import yeonba.be.user.repository.userrecommendation.UserRecommendationQuery; +import yeonba.be.user.repository.usersearchlog.UserSearchLogCommand; import yeonba.be.user.repository.vocalrange.VocalRangeQuery; import yeonba.be.util.AgeValidator; import yeonba.be.util.S3Service; @@ -49,6 +55,7 @@ public class UserService { private final UserCommand userCommand; private final UserPreferenceCommand userPreferenceCommand; private final UserRecommendationCommand userRecommendationCommand; + private final UserSearchLogCommand userSearchLogCommand; private final AnimalQuery animalQuery; private final AreaQuery areaQuery; @@ -173,7 +180,7 @@ public void saveUserPreference(User user, UserJoinRequest request) { public UserQueryPageResponse findUsersByQueryCondition(long userId, UserQueryRequest request) { int page = Optional.ofNullable(request.getPage()).orElse(0); - int size = 6; + int size = 30; PageRequest pageRequest = PageRequest.of(page, size); // 사용자 존재 여부 검증 @@ -208,10 +215,9 @@ public UserQueryPageResponse findRecommendUsers(long userId, LocalDate recommend // 추천 사용자 응답 조회, int numberOfRecommendUsers = 2; - boolean userGender = Gender.from(user.getGenderString()).genderBoolean; PageRequest pageRequest = PageRequest.of(0, numberOfRecommendUsers); UserQueryPageResponse response = userQuery - .findRecommendUsers(userId, userGender, pageRequest, recommendDay); + .findRecommendUsers(user, pageRequest, recommendDay); // 추천 가능 여부 확인(추천 가능한 사용자 2명 이상) List content = response.getUsers(); @@ -220,10 +226,7 @@ public UserQueryPageResponse findRecommendUsers(long userId, LocalDate recommend } // 추천 사용자 조회 - List userIds = content.stream() - .map(UserQueryResponse::getId) - .toList(); - List recommendUsers = userQuery.findByIds(userIds); + List recommendUsers = findAllUsersInResponse(response); // 추천 내역 저장 List userRecommendations = recommendUsers.stream() @@ -240,4 +243,49 @@ public void updateDeviceToken(long userId, UserUpdateDeviceTokenRequest request) User user = userQuery.findById(userId); user.updateDeviceToken(request.getDeviceToken()); } + + @Transactional + public UserQueryPageResponse findUsersBySearchCondition(long userId, + UserSearchRequest request) { + + int page = 0; + if(Objects.nonNull(request) && Objects.nonNull(request.getPage())) { + page = request.getPage(); + + // 검색 나이/키 하한 <= 상한 여부 검증 + validateBounds(request.getAgeLowerBound(), request.getAgeUpperBound()); + validateBounds(request.getHeightLowerBound(), request.getHeightUpperBound()); + } + + int size = 30; + PageRequest pageRequest = PageRequest.of(page, size); + LocalDate searchDay = LocalDate.now(); + + // 검색하는 사용자 조회 + User user = userQuery.findById(userId); + + // 응답 조회 + UserQueryPageResponse response = userQuery + .findUsersBySearchCondition(user, pageRequest, searchDay, request); + + // 검색된 사용자 조회 + List searchingUsers = findAllUsersInResponse(response); + + // 검색 내역 저장 + List userSearchLogs = searchingUsers.stream() + .map(searchingUser -> new UserSearchLog(user, searchingUser)) + .toList(); + userSearchLogCommand.saveAll(userSearchLogs); + + return response; + } + + private List findAllUsersInResponse(UserQueryPageResponse response) { + + List userIds = response.getUsers().stream() + .map(UserQueryResponse::getId) + .toList(); + + return userQuery.findByIds(userIds); + } } diff --git a/be/src/main/java/yeonba/be/util/BoundsValidator.java b/be/src/main/java/yeonba/be/util/BoundsValidator.java new file mode 100644 index 00000000..fb7bc087 --- /dev/null +++ b/be/src/main/java/yeonba/be/util/BoundsValidator.java @@ -0,0 +1,20 @@ +package yeonba.be.util; + +import java.util.Objects; +import yeonba.be.exception.GeneralException; +import yeonba.be.exception.UserException; + +public class BoundsValidator { + + public static void validateBounds(Integer lowerBound, Integer upperBound) { + + if (Objects.isNull(lowerBound) || Objects.isNull(upperBound)) { + + return; + } + + if (lowerBound > upperBound) { + throw new GeneralException(UserException.LOWER_BOUND_LESS_THAN_OR_EQUAL_UPPER_BOUND); + } + } +} diff --git a/be/src/main/java/yeonba/be/util/JwtUtil.java b/be/src/main/java/yeonba/be/util/JwtUtil.java index a3f9cc7e..8dfeac5d 100644 --- a/be/src/main/java/yeonba/be/util/JwtUtil.java +++ b/be/src/main/java/yeonba/be/util/JwtUtil.java @@ -50,7 +50,8 @@ public long getUserIdFromJwt(String token) { } else if (userIdObject instanceof Integer) { return ((Integer) userIdObject).longValue(); } else { - throw new IllegalArgumentException("Unexpected type for userId: " + userIdObject.getClass().getName()); + throw new IllegalArgumentException( + "Unexpected type for userId: " + userIdObject.getClass().getName()); } }