diff --git a/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java b/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java index 97adc128a..fab050079 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java @@ -13,6 +13,7 @@ import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -32,6 +33,9 @@ public class UnivApplyInfoController { private final LikedUnivApplyInfoService likedUnivApplyInfoService; private final UnivApplyInfoRecommendService univApplyInfoRecommendService; + @Value("${university.term}") + public String term; + @GetMapping("/recommend") public ResponseEntity getUnivApplyInfoRecommends( @AuthorizedUser(required = false) Long siteUserId @@ -91,15 +95,15 @@ public ResponseEntity getUnivApplyInfoDetails( public ResponseEntity searchUnivApplyInfoByFilter( @Valid @ModelAttribute UnivApplyInfoFilterSearchRequest request ) { - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request); + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request, term); return ResponseEntity.ok(response); } @GetMapping("/search/text") public ResponseEntity searchUnivApplyInfoByText( - @RequestParam(required = false) String text + @RequestParam(required = false) String value ) { - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text); + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(value, term); return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java index 9de0e98b6..c8a6601e3 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java @@ -9,4 +9,6 @@ public interface UnivApplyInfoFilterRepository { List findAllByRegionCodeAndKeywords(String regionCode, List keywords); List findAllByFilter(LanguageTestType testType, String testScore, String term, List countryKoreanNames); + + List findAllByText(String text, String term); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java index 538074422..3786f697c 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java @@ -1,14 +1,19 @@ package com.example.solidconnection.university.repository.custom; import com.example.solidconnection.location.country.domain.QCountry; +import com.example.solidconnection.location.region.domain.QRegion; import com.example.solidconnection.university.domain.LanguageTestType; import com.example.solidconnection.university.domain.QLanguageRequirement; import com.example.solidconnection.university.domain.QUnivApplyInfo; import com.example.solidconnection.university.domain.QUniversity; import com.example.solidconnection.university.domain.UnivApplyInfo; +import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.StringPath; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import java.util.List; @@ -136,4 +141,46 @@ private boolean isGivenScoreOverMinPassScore( .map(requirement -> givenTestType.compare(givenTestScore, requirement.getMinScore())) .orElse(-1) >= 0; } + + @Override + public List findAllByText(String text, String term) { + QUnivApplyInfo univApplyInfo = QUnivApplyInfo.univApplyInfo; + QUniversity university = QUniversity.university; + QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement; + QCountry country = QCountry.country; + QRegion region = QRegion.region; + + JPAQuery base = queryFactory.selectFrom(univApplyInfo) + .join(univApplyInfo.university, university).fetchJoin() + .join(university.country, country).fetchJoin() + .join(region).on(country.regionCode.eq(region.code)) + .leftJoin(univApplyInfo.languageRequirements, languageRequirement).fetchJoin() + .where(termEq(univApplyInfo, term)); + + // text 가 비어있다면 모든 대학 지원 정보를 id 오름차순으로 정렬하여 반환 + if (text == null || text.isBlank()) { + return base.orderBy(univApplyInfo.id.asc()).fetch(); + } + + // 매칭 조건 (대학 지원 정보명/국가명/지역명 중 하나라도 포함) + BooleanExpression univApplyInfoLike = univApplyInfo.koreanName.contains(text); + BooleanExpression countryLike = country.koreanName.contains(text); + BooleanExpression regionLike = region.koreanName.contains(text); + BooleanBuilder where = new BooleanBuilder() + .or(univApplyInfoLike) + .or(countryLike) + .or(regionLike); + + // 우선순위 랭크: 대학 지원 정보명(0) > 국가명(1) > 지역명(2) > 그 외(3) + NumberExpression rank = new CaseBuilder() + .when(univApplyInfoLike).then(0) + .when(countryLike).then(1) + .when(regionLike).then(2) + .otherwise(3); + + // 정렬 조건: 랭크 오름차순 > 대학지원정보 id 오름차순 + return base.where(where) + .orderBy(rank.asc(), univApplyInfo.id.asc()) + .fetch(); + } } diff --git a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java index e8ba3140b..bf6ec089a 100644 --- a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java +++ b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java @@ -10,7 +10,6 @@ import com.example.solidconnection.university.repository.UnivApplyInfoRepository; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,15 +19,12 @@ public class UnivApplyInfoQueryService { private final UnivApplyInfoRepository univApplyInfoRepository; - @Value("${university.term}") - public String term; - /* * 대학교 상세 정보를 불러온다. * - 대학교(University) 정보와 대학 지원 정보(UniversityInfoForApply) 정보를 조합하여 반환한다. * */ @Transactional(readOnly = true) - @ThunderingHerdCaching(key = "univApplyInfo:{0}", cacheManager = "customCacheManager", ttlSec = 86400) + @ThunderingHerdCaching(key = "univApplyInfo:{0}:{1}", cacheManager = "customCacheManager", ttlSec = 86400) public UnivApplyInfoDetailResponse getUnivApplyInfoDetail(Long univApplyInfoId) { UnivApplyInfo univApplyInfo = univApplyInfoRepository.getUnivApplyInfoById(univApplyInfoId); @@ -38,7 +34,7 @@ public UnivApplyInfoDetailResponse getUnivApplyInfoDetail(Long univApplyInfoId) } @Transactional(readOnly = true) - public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFilterSearchRequest request) { + public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFilterSearchRequest request, String term) { List responses = univApplyInfoRepository .findAllByFilter(request.languageTestType(), request.testScore(), term, request.countryCode()) .stream() @@ -48,8 +44,12 @@ public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFi } @Transactional(readOnly = true) - public UnivApplyInfoPreviewResponses searchUnivApplyInfoByText(String text) { - // todo: 구현 - return null; + @ThunderingHerdCaching(key = "univApplyInfoTextSearch:{0}:{1}", cacheManager = "customCacheManager", ttlSec = 86400) + public UnivApplyInfoPreviewResponses searchUnivApplyInfoByText(String text, String term) { + List responses = univApplyInfoRepository.findAllByText(text, term) + .stream() + .map(UnivApplyInfoPreviewResponse::from) + .toList(); + return new UnivApplyInfoPreviewResponses(responses); } } diff --git a/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java b/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java index 12c5efa07..dfd50450f 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java +++ b/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java @@ -39,6 +39,14 @@ public class UnivApplyInfoFixture { .create(); } + public UnivApplyInfo 아칸소주립대학_지원_정보() { + return univApplyInfoFixtureBuilder.univApplyInfo() + .term(term) + .koreanName("아칸소 주립 대학") + .university(universityFixture.아칸소_주립_대학()) + .create(); + } + public UnivApplyInfo 메모리얼대학_세인트존스_A_지원_정보() { return univApplyInfoFixtureBuilder.univApplyInfo() .term(term) diff --git a/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java b/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java index d1c9d2dd4..bbc3fc3b4 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java +++ b/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java @@ -32,6 +32,15 @@ public final class UniversityFixture { .create(); } + public University 아칸소_주립_대학() { + return universityFixtureBuilder.university() + .koreanName("아칸소 주립 대학") + .englishName("Arkansas State University") + .country(countryFixture.미국()) + .region(regionFixture.영미권()) + .create(); + } + public University 메모리얼_대학_세인트존스() { return universityFixtureBuilder.university() .koreanName("메모리얼 대학 세인트존스") diff --git a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java index 69fa9b619..661294363 100644 --- a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java @@ -3,6 +3,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.UNIV_APPLY_INFO_NOT_FOUND; import static com.example.solidconnection.university.domain.LanguageTestType.TOEIC; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.BDDMockito.then; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.mock.mockito.SpyBean; @TestContainerSpringBootTest @@ -41,6 +43,9 @@ class UnivApplyInfoQueryServiceTest { @Autowired private LanguageRequirementFixture languageRequirementFixture; + @Value("${university.term}") + public String term; + @Nested class 대학_지원_정보_상세_조회 { @@ -97,7 +102,7 @@ class 대학_지원_정보_필터링_검색 { languageRequirementFixture.토플_70(괌대학_B_지원_정보); // when - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request); + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request, term); // then assertThat(response.univApplyInfoPreviews()) @@ -114,7 +119,7 @@ class 대학_지원_정보_필터링_검색 { languageRequirementFixture.토익_900(괌대학_B_지원_정보); // when - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request); + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request, term); // then assertThat(response.univApplyInfoPreviews()) @@ -132,8 +137,8 @@ class 대학_지원_정보_필터링_검색 { languageRequirementFixture.토익_800(메모리얼대학_세인트존스_A_지원_정보); // when - UnivApplyInfoPreviewResponses response1 = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request1); - UnivApplyInfoPreviewResponses response2 = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request2); + UnivApplyInfoPreviewResponses response1 = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request1, term); + UnivApplyInfoPreviewResponses response2 = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request2, term); // then assertAll( @@ -147,4 +152,132 @@ class 대학_지원_정보_필터링_검색 { ); } } + + @Nested + class 대학_지원_정보_텍스트_검색 { + + @Test + void 텍스트가_없으면_전체_대학을_id_순으로_정렬하여_반환한다() { + // given + UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(); + UnivApplyInfo 메이지대학_지원_정보 = univApplyInfoFixture.메이지대학_지원_정보(); + + // when + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(null, term); + + // then + assertThat(response.univApplyInfoPreviews()) + .containsExactly( + UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보), + UnivApplyInfoPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Nested + class 각각의_검색_대상에_대해_검색한다 { + + @Test + void 국문_대학_지원_정보명() { + // given + String text = "메"; + UnivApplyInfo 메이지대학_지원_정보 = univApplyInfoFixture.메이지대학_지원_정보(); + UnivApplyInfo 메모리얼대학_세인트존스_A_지원_정보 = univApplyInfoFixture.메모리얼대학_세인트존스_A_지원_정보(); + univApplyInfoFixture.괌대학_A_지원_정보(); + + // when + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text, term); + + // then + assertThat(response.univApplyInfoPreviews()) + .containsExactly( + UnivApplyInfoPreviewResponse.from(메이지대학_지원_정보), + UnivApplyInfoPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보) + ); + } + + @Test + void 국문_국가명() { + // given + String text = "미국"; + UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(); + UnivApplyInfo 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(); + univApplyInfoFixture.메이지대학_지원_정보(); + + // when + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text, term); + + // then + assertThat(response.univApplyInfoPreviews()) + .containsExactly( + UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보), + UnivApplyInfoPreviewResponse.from(괌대학_B_지원_정보) + ); + } + + @Test + void 국문_권역명() { + // given + String text = "유럽"; + UnivApplyInfo 린츠_카톨릭대학_지원_정보 = univApplyInfoFixture.린츠_카톨릭대학_지원_정보(); + UnivApplyInfo 서던덴마크대학교_지원_정보 = univApplyInfoFixture.서던덴마크대학교_지원_정보(); + univApplyInfoFixture.메이지대학_지원_정보(); + + // when + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text, term); + + // then + assertThat(response.univApplyInfoPreviews()) + .containsExactly( + UnivApplyInfoPreviewResponse.from(린츠_카톨릭대학_지원_정보), + UnivApplyInfoPreviewResponse.from(서던덴마크대학교_지원_정보) + ); + } + } + + @Test + void 대학_국가_권역_일치_순서로_정렬하여_응답한다() { + // given + String text = "아"; + UnivApplyInfo 권역_아 = univApplyInfoFixture.메이지대학_지원_정보(); + UnivApplyInfo 국가_아 = univApplyInfoFixture.그라츠대학_지원_정보(); + UnivApplyInfo 대학지원정보_아 = univApplyInfoFixture.아칸소주립대학_지원_정보(); + + // when + UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text, term); + + // then + assertThat(response.univApplyInfoPreviews()) + .containsExactly( + UnivApplyInfoPreviewResponse.from(대학지원정보_아), + UnivApplyInfoPreviewResponse.from(국가_아), + UnivApplyInfoPreviewResponse.from(권역_아) + ); + } + + @Test + void 캐시가_적용된다() { + // given + String text = "Guam"; + UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(); + + // when + UnivApplyInfoPreviewResponses firstResponse = univApplyInfoQueryService.searchUnivApplyInfoByText(text, term); + UnivApplyInfoPreviewResponses secondResponse = univApplyInfoQueryService.searchUnivApplyInfoByText(text, term); + + // then + assertThatCode(() -> { + List firstResponseIds = extractIds(firstResponse); + List secondResponseIds = extractIds(secondResponse); + assertThat(firstResponseIds).isEqualTo(secondResponseIds); + }).doesNotThrowAnyException(); + then(univApplyInfoRepository).should(times(1)).findAllByText(text, term); + } + + private List extractIds(UnivApplyInfoPreviewResponses responses) { + return responses.univApplyInfoPreviews() + .stream() + .map(UnivApplyInfoPreviewResponse::id) + .toList(); + } + } }