Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [Feat] 연관 레크레이션 목록 API구현 및 예외처리 추가 #53

Merged
merged 11 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/main/java/com/avab/avab/controller/RecreationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
import com.avab.avab.dto.reqeust.RecreationRequestDTO.PostRecreationReviewDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.DescriptionDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.FavoriteDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewListDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewPageDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationReviewCreatedDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationReviewPageDTO;
import com.avab.avab.security.handler.annotation.AuthUser;
Expand Down Expand Up @@ -62,12 +63,12 @@ public class RecreationController {
})
@Parameter(name = "user", hidden = true)
@GetMapping("/popular")
public BaseResponse<RecreationPreviewListDTO> getTop9RecreationsByWeeklyViewCount(
public BaseResponse<RecreationPreviewPageDTO> getTop9RecreationsByWeeklyViewCount(
@AuthUser User user) {
Page<Recreation> topRecreations = recreationService.getTop9RecreationsByWeeklyViewCount();

return BaseResponse.onSuccess(
RecreationConverter.toRecreationPreviewListDTO(topRecreations, user));
RecreationConverter.toRecreationPreviewPageDTO(topRecreations, user));
}

@Operation(summary = "레크레이션 상세설명 조회 API", description = "레크레이션 상세설명을 조회합니다. _by 수기_")
Expand All @@ -85,7 +86,7 @@ public BaseResponse<DescriptionDTO> getRecreationDescription(
@ApiResponses({@ApiResponse(responseCode = "COMMON200", description = "OK, 성공")})
@Parameter(name = "user", hidden = true)
@GetMapping("/search")
public BaseResponse<RecreationPreviewListDTO> searchRecreations(
public BaseResponse<RecreationPreviewPageDTO> searchRecreations(
@AuthUser User user,
@RequestParam(name = "searchKeyword", required = false) String searchKeyword,
@RequestParam(name = "keyword", required = false) List<Keyword> keywords,
Expand All @@ -111,7 +112,7 @@ public BaseResponse<RecreationPreviewListDTO> searchRecreations(
page);

return BaseResponse.onSuccess(
RecreationConverter.toRecreationPreviewListDTO(recreationPage, user));
RecreationConverter.toRecreationPreviewPageDTO(recreationPage, user));
}

@Operation(
Expand Down Expand Up @@ -160,4 +161,18 @@ public BaseResponse<RecreationReviewPageDTO> getRecreationReviews(
return BaseResponse.onSuccess(
RecreationConverter.toRecreationReviewPageDTO(reviewPage, user));
}

@Operation(summary = "연관 레크레이션 API", description = "연관 레크레이션 목록을 가져옵니다. _by 수기_")
@ApiResponses({@ApiResponse(responseCode = "COMMON200", description = "OK, 성공")})
@Parameter(name = "user", hidden = true)
@GetMapping("/{recreationId}/related")
public BaseResponse<List<RecreationPreviewDTO>> relatedRecreation(
@AuthUser User user,
@ExistRecreation @PathVariable(name = "recreationId") Long recreationId) {
List<Recreation> relatedRecreation =
recreationService.findRelatedRecreations(user, recreationId);

return BaseResponse.onSuccess(
RecreationConverter.toRecreationPreviewListDTO(relatedRecreation, user));
}
}
6 changes: 3 additions & 3 deletions src/main/java/com/avab/avab/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import com.avab.avab.domain.Recreation;
import com.avab.avab.domain.User;
import com.avab.avab.dto.reqeust.UserRequestDTO.UpdateUserNameDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewListDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewPageDTO;
import com.avab.avab.dto.response.UserResponseDTO.UserResponse;
import com.avab.avab.security.handler.annotation.AuthUser;
import com.avab.avab.service.UserService;
Expand Down Expand Up @@ -45,14 +45,14 @@ public class UserController {
@ApiResponses({@ApiResponse(responseCode = "COMMON200", description = "OK, 성공")})
@GetMapping("/me/favorites/recreations")
@Parameter(name = "user", hidden = true)
public BaseResponse<RecreationPreviewListDTO> getFavoriteRecreations(
public BaseResponse<RecreationPreviewPageDTO> getFavoriteRecreations(
@RequestParam(name = "page", required = false, defaultValue = "0") @ValidatePage
Integer page,
@AuthUser User user) {
Page<Recreation> recreationPage = userService.getFavoriteRecreations(user, page);

return BaseResponse.onSuccess(
RecreationConverter.toRecreationPreviewListDTO(recreationPage, user));
RecreationConverter.toRecreationPreviewPageDTO(recreationPage, user));
}

@Operation(summary = "회원 정보 수정 API", description = "회원 닉네임을 수정합니다. 닉네임을 인자로 받습니다. _by 루아_")
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/com/avab/avab/converter/RecreationConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import com.avab.avab.dto.response.RecreationResponseDTO.DescriptionDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.FavoriteDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewListDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationPreviewPageDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationReviewCreatedDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationReviewDTO;
import com.avab.avab.dto.response.RecreationResponseDTO.RecreationReviewDTO.AuthorDTO;
Expand All @@ -31,9 +31,9 @@

public class RecreationConverter {

public static RecreationPreviewListDTO toRecreationPreviewListDTO(
public static RecreationPreviewPageDTO toRecreationPreviewPageDTO(
Page<Recreation> recreationPage, User user) {
return RecreationPreviewListDTO.builder()
return RecreationPreviewPageDTO.builder()
.recreationList(
recreationPage.getContent().stream()
.map(recreation -> toRecreationPreviewDTO(recreation, user))
Expand Down Expand Up @@ -183,4 +183,11 @@ public static RecreationReviewDTO toRecreationReviewDTO(RecreationReview review,
: null)
.build();
}

public static List<RecreationPreviewDTO> toRecreationPreviewListDTO(
List<Recreation> recreations, User user) {
return recreations.stream()
.map(recreation -> toRecreationPreviewDTO(recreation, user))
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static class RecreationPreviewDTO {
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public static class RecreationPreviewListDTO {
public static class RecreationPreviewPageDTO {

List<RecreationPreviewDTO> recreationList;
Integer totalPages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ Page<Recreation> searchRecreations(
List<Gender> genders,
List<Age> ages,
Pageable page);

List<Recreation> findRelatedRecreations(
Long recreationId,
List<Keyword> keyword,
List<Purpose> purpose,
Integer maxParticipants,
List<Age> age);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

import static com.avab.avab.domain.QRecreation.recreation;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.Pair;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import com.avab.avab.apiPayload.code.status.ErrorStatus;
import com.avab.avab.apiPayload.exception.RecreationException;
import com.avab.avab.domain.QRecreation;
import com.avab.avab.domain.QRecreationAge;
import com.avab.avab.domain.Recreation;
import com.avab.avab.domain.enums.Age;
import com.avab.avab.domain.enums.Gender;
import com.avab.avab.domain.enums.Keyword;
import com.avab.avab.domain.enums.Place;
import com.avab.avab.domain.enums.Purpose;
import com.avab.avab.domain.mapping.QRecreationRecreationKeyword;
import com.avab.avab.domain.mapping.QRecreationRecreationPurpose;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;

Expand Down Expand Up @@ -107,4 +116,94 @@ private BooleanExpression inGender(List<Gender> genders) {
private BooleanExpression inAge(List<Age> ages) {
return ages != null ? recreation.recreationAgeList.any().age.in(ages) : null;
}

@Override
public List<Recreation> findRelatedRecreations(
Long recreationId,
List<Keyword> keyword,
List<Purpose> purpose,
Integer maxParticipants,
List<Age> age) {
QRecreation recreation = QRecreation.recreation;
QRecreationRecreationPurpose recreationPurpose =
QRecreationRecreationPurpose.recreationRecreationPurpose;
QRecreationRecreationKeyword recreationKeyword =
QRecreationRecreationKeyword.recreationRecreationKeyword;
QRecreationAge recreationAge = QRecreationAge.recreationAge;
List<Pair<Recreation, Double>> recreationList = new ArrayList<>();

// 다른 Recreation id
List<Long> otherRecreationIds =
queryFactory
.select(recreation.id)
.from(recreation)
.where(recreation.id.ne(recreationId))
.fetch();

// 다른 레크레이션들과 비교
for (Long otherRecreationId : otherRecreationIds) {
Recreation otherRecreation =
queryFactory
.selectFrom(recreation)
.where(recreation.id.eq(otherRecreationId))
.fetchOne();
if (otherRecreation == null)
throw new RecreationException(ErrorStatus.RECREATION_NOT_FOUND);

// 겹치는 목적 체크
List<Purpose> purposesForComparison =
queryFactory
.select(recreationPurpose.purpose.purpose)
.from(recreationPurpose)
.where(recreationPurpose.recreation.id.eq(otherRecreationId))
.fetch();

long purposeMatchSize =
purposesForComparison.stream().filter(purpose::contains).count();

// 겹치는 키워드 체크
List<Keyword> keywordsForComparison =
queryFactory
.select(recreationKeyword.keyword.keyword)
.from(recreationKeyword)
.where(recreationKeyword.recreation.id.eq(otherRecreationId))
.fetch();

long keywordMatchSize =
keyword.stream().filter(keywordsForComparison::contains).count();

// 인원 기준 max인원과 다른 레크레이션의 min인원 차이확인
int participantsMatch =
otherRecreation.getMinParticipants() != null
&& maxParticipants > otherRecreation.getMinParticipants()
? maxParticipants - otherRecreation.getMinParticipants()
: 0;

// 연령대 겹치는 개수 확인
List<Age> ageForComparison =
queryFactory
.select(recreationAge.age)
.from(recreationAge)
.where(recreationAge.recreation.id.eq(otherRecreationId))
.fetch();

long ageMatchList = ageForComparison.stream().filter(age::contains).count();

// List에 추가
recreationList.add(
Pair.of(
otherRecreation,
purposeMatchSize * 0.4
+ keywordMatchSize * 0.3
+ ageMatchList * 0.2
+ participantsMatch * 0.10));
}

// 가중치별 내림차순 정렬
recreationList.sort(
Comparator.comparingDouble(Pair<Recreation, Double>::getRight).reversed());

// 최대 가중치를 가지는 2개의 레크레이션 리턴
return recreationList.stream().map(Pair::getLeft).limit(2).collect(Collectors.toList());
}
}
1 change: 1 addition & 0 deletions src/main/java/com/avab/avab/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class SecurityConfig {
"/api/recreations/popular",
"/api/recreations/search",
"/api/recreations/{recreationId}",
"/api/recreations/{recreationId}/related",
"/api/recreations/popular",
"/api/auth/login/kakao",
"/api/auth/refresh"
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/avab/avab/service/RecreationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ Page<Recreation> searchRecreations(
RecreationReview createReview(User user, Long recreationId, PostRecreationReviewDTO request);

Page<RecreationReview> getRecreationReviews(Long recreationId, Integer page);

List<Recreation> findRelatedRecreations(User user, Long recreationId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
Expand All @@ -12,6 +13,9 @@
import com.avab.avab.apiPayload.exception.RecreationException;
import com.avab.avab.converter.RecreationConverter;
import com.avab.avab.domain.Recreation;
import com.avab.avab.domain.RecreationAge;
import com.avab.avab.domain.RecreationKeyword;
import com.avab.avab.domain.RecreationPurpose;
import com.avab.avab.domain.RecreationReview;
import com.avab.avab.domain.User;
import com.avab.avab.domain.enums.Age;
Expand All @@ -20,6 +24,8 @@
import com.avab.avab.domain.enums.Place;
import com.avab.avab.domain.enums.Purpose;
import com.avab.avab.domain.mapping.RecreationFavorite;
import com.avab.avab.domain.mapping.RecreationRecreationKeyword;
import com.avab.avab.domain.mapping.RecreationRecreationPurpose;
import com.avab.avab.dto.reqeust.RecreationRequestDTO.PostRecreationReviewDTO;
import com.avab.avab.redis.service.RecreationViewCountService;
import com.avab.avab.repository.RecreationFavoriteRepository;
Expand Down Expand Up @@ -47,7 +53,11 @@ public Page<Recreation> getTop9RecreationsByWeeklyViewCount() {

@Transactional
public Recreation getRecreationDescription(Long recreationId) {
Recreation recreation = recreationRepository.findById(recreationId).get();
Recreation recreation =
recreationRepository
.findById(recreationId)
.orElseThrow(
() -> new RecreationException(ErrorStatus.RECREATION_NOT_FOUND));

recreationViewCountService.incrementViewCount(recreation.getId());

Expand Down Expand Up @@ -151,4 +161,27 @@ private Boolean isAtLeastOneConditionNotNull(
|| gender != null
|| age != null;
}

public List<Recreation> findRelatedRecreations(User user, Long recreationId) {
Recreation recreation =
recreationRepository
.findById(recreationId)
.orElseThrow(
() -> new RecreationException(ErrorStatus.RECREATION_NOT_FOUND));

return recreationRepository.findRelatedRecreations(
recreationId,
recreation.getRecreationRecreationKeywordList().stream()
.map(RecreationRecreationKeyword::getKeyword)
.map(RecreationKeyword::getKeyword)
.collect(Collectors.toList()),
recreation.getRecreationRecreationPurposeList().stream()
.map(RecreationRecreationPurpose::getPurpose)
.map(RecreationPurpose::getPurpose)
.collect(Collectors.toList()),
recreation.getMaxParticipants(),
recreation.getRecreationAgeList().stream()
.map(RecreationAge::getAge)
.collect(Collectors.toList()));
}
}
Loading