Skip to content

Commit 7060378

Browse files
authored
refactor: 기획 변경에 맞게 필터링 검색 기능 수정 (#471)
* refactor: 레포지토리 구현체가 아니라 인터페이스 의존하도록 - UnivApplyInfoFilterRepositoryImpl가 아니라 UnivApplyInfoRepository를 의존하게 한다. - 그렇게 해도 되는 이유는, UnivApplyInfoRepository가 UnivApplyInfoFilterRepository를 extend하고 있기 때문 * refactor: API 명세에 맞게 컨트롤러, 요청 구현 * feat: 대학지원정보 필터링 검색 기능 구현 * chore: 사용하지 않는 메서드 제거 * refactor: 람다 표현식 간소화 * chore: 주석 수정 - compare 에서 0은 같다는 의미 * test: 바뀐 로직을 테스트 코드에 반영 * test: 더 엄격한 검증 적용
1 parent accbc93 commit 7060378

File tree

8 files changed

+207
-226
lines changed

8 files changed

+207
-226
lines changed

src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package com.example.solidconnection.university.controller;
22

33
import com.example.solidconnection.common.resolver.AuthorizedUser;
4-
import com.example.solidconnection.university.domain.LanguageTestType;
54
import com.example.solidconnection.university.dto.IsLikeResponse;
65
import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse;
6+
import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest;
77
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse;
8+
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses;
89
import com.example.solidconnection.university.dto.UnivApplyInfoRecommendsResponse;
910
import com.example.solidconnection.university.service.LikedUnivApplyInfoService;
1011
import com.example.solidconnection.university.service.UnivApplyInfoQueryService;
1112
import com.example.solidconnection.university.service.UnivApplyInfoRecommendService;
13+
import jakarta.validation.Valid;
1214
import java.util.List;
1315
import lombok.RequiredArgsConstructor;
1416
import org.springframework.http.ResponseEntity;
1517
import org.springframework.web.bind.annotation.DeleteMapping;
1618
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.ModelAttribute;
1720
import org.springframework.web.bind.annotation.PathVariable;
1821
import org.springframework.web.bind.annotation.PostMapping;
1922
import org.springframework.web.bind.annotation.RequestMapping;
@@ -84,16 +87,19 @@ public ResponseEntity<UnivApplyInfoDetailResponse> getUnivApplyInfoDetails(
8487
return ResponseEntity.ok(univApplyInfoDetailResponse);
8588
}
8689

87-
// todo: return타입 UniversityInfoForApplyPreviewResponses로 추후 수정 필요
88-
@GetMapping("/search")
89-
public ResponseEntity<List<UnivApplyInfoPreviewResponse>> searchUnivApplyInfo(
90-
@RequestParam(required = false, defaultValue = "") String region,
91-
@RequestParam(required = false, defaultValue = "") List<String> keyword,
92-
@RequestParam(required = false, defaultValue = "") LanguageTestType testType,
93-
@RequestParam(required = false, defaultValue = "") String testScore
90+
@GetMapping("/search/filter")
91+
public ResponseEntity<UnivApplyInfoPreviewResponses> searchUnivApplyInfoByFilter(
92+
@Valid @ModelAttribute UnivApplyInfoFilterSearchRequest request
9493
) {
95-
List<UnivApplyInfoPreviewResponse> univApplyInfoPreviewResponse
96-
= univApplyInfoQueryService.searchUnivApplyInfo(region, keyword, testType, testScore).univApplyInfoPreviews();
97-
return ResponseEntity.ok(univApplyInfoPreviewResponse);
94+
UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request);
95+
return ResponseEntity.ok(response);
96+
}
97+
98+
@GetMapping("/search/text")
99+
public ResponseEntity<UnivApplyInfoPreviewResponses> searchUnivApplyInfoByText(
100+
@RequestParam(required = false) String text
101+
) {
102+
UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text);
103+
return ResponseEntity.ok(response);
98104
}
99105
}

src/main/java/com/example/solidconnection/university/domain/LanguageTestType.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
public enum LanguageTestType {
66

7-
CEFR((s1, s2) -> s1.compareTo(s2)),
8-
JLPT((s1, s2) -> s2.compareTo(s1)),
7+
CEFR(String::compareTo),
8+
JLPT(Comparator.reverseOrder()),
99
DALF(LanguageTestType::compareIntegerScores),
1010
DELF(LanguageTestType::compareIntegerScores),
1111
DUOLINGO(LanguageTestType::compareIntegerScores),
@@ -16,7 +16,7 @@ public enum LanguageTestType {
1616
TOEFL_IBT(LanguageTestType::compareIntegerScores),
1717
TOEFL_ITP(LanguageTestType::compareIntegerScores),
1818
TOEIC(LanguageTestType::compareIntegerScores),
19-
ETC((s1, s2) -> 0), // 기타 언어시험은 점수를 비교할 수 없으므로 항상 크다고 비교한다.
19+
ETC((s1, s2) -> 0), // 기타 언어시험은 점수를 비교할 수 없으므로 항상 같다고 비교한다.
2020
;
2121

2222
private final Comparator<String> comparator;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.example.solidconnection.university.dto;
2+
3+
import com.example.solidconnection.university.domain.LanguageTestType;
4+
import jakarta.validation.constraints.NotNull;
5+
import java.util.List;
6+
7+
public record UnivApplyInfoFilterSearchRequest(
8+
9+
@NotNull(message = "어학 시험 종류를 선택해주세요.")
10+
LanguageTestType languageTestType,
11+
String testScore,
12+
List<String> countryCode
13+
) {
14+
15+
}

src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,10 @@
44

55
import com.example.solidconnection.common.exception.CustomException;
66
import com.example.solidconnection.university.domain.University;
7-
import java.util.List;
87
import org.springframework.data.jpa.repository.JpaRepository;
9-
import org.springframework.data.jpa.repository.Query;
10-
import org.springframework.data.repository.query.Param;
118

129
public interface UniversityRepository extends JpaRepository<University, Long> {
1310

14-
@Query("SELECT u FROM University u WHERE u.country.code IN :countryCodes OR u.region.code IN :regionCodes")
15-
List<University> findByCountryCodeInOrRegionCodeIn(@Param("countryCodes") List<String> countryCodes, @Param("regionCodes") List<String> regionCodes);
16-
1711
default University getUniversityById(Long id) {
1812
return findById(id)
1913
.orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND));

src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ public interface UnivApplyInfoFilterRepository {
88

99
List<UnivApplyInfo> findAllByRegionCodeAndKeywords(String regionCode, List<String> keywords);
1010

11-
List<UnivApplyInfo> findAllByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(
12-
String regionCode, List<String> keywords, LanguageTestType testType, String testScore, String term);
11+
List<UnivApplyInfo> findAllByFilter(LanguageTestType testType, String testScore, String term, List<String> countryKoreanNames);
1312
}

src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.example.solidconnection.university.repository.custom;
22

33
import com.example.solidconnection.location.country.domain.QCountry;
4-
import com.example.solidconnection.location.region.domain.QRegion;
54
import com.example.solidconnection.university.domain.LanguageTestType;
65
import com.example.solidconnection.university.domain.QLanguageRequirement;
76
import com.example.solidconnection.university.domain.QUnivApplyInfo;
@@ -70,43 +69,71 @@ private BooleanExpression createKeywordCondition(StringPath namePath, List<Strin
7069
}
7170

7271
@Override
73-
public List<UnivApplyInfo> findAllByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(
74-
String regionCode, List<String> keywords, LanguageTestType testType, String testScore, String term) {
72+
public List<UnivApplyInfo> findAllByFilter(
73+
LanguageTestType testType, String testScore, String term, List<String> countryCodes
74+
) {
7575
QUniversity university = QUniversity.university;
76-
QCountry country = QCountry.country;
77-
QRegion region = QRegion.region;
7876
QUnivApplyInfo univApplyInfo = QUnivApplyInfo.univApplyInfo;
77+
QCountry country = QCountry.country;
78+
QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement;
7979

80-
List<UnivApplyInfo> filteredUnivApplyInfo = queryFactory
81-
.selectFrom(univApplyInfo)
80+
List<UnivApplyInfo> filteredUnivApplyInfo = queryFactory.selectFrom(univApplyInfo)
8281
.join(univApplyInfo.university, university)
8382
.join(university.country, country)
84-
.join(university.region, region)
85-
.where(regionCodeEq(country, regionCode)
86-
.and(countryOrUniversityContainsKeyword(country, university, keywords))
87-
.and(univApplyInfo.term.eq(term)))
83+
.join(univApplyInfo.languageRequirements, languageRequirement)
84+
.fetchJoin()
85+
.where(
86+
languageTestTypeEq(languageRequirement, testType),
87+
termEq(univApplyInfo, term),
88+
countryCodesIn(country, countryCodes)
89+
)
90+
.distinct()
8891
.fetch();
8992

90-
if (testScore == null || testScore.isEmpty()) {
91-
if (testType != null) {
92-
return filteredUnivApplyInfo.stream()
93-
.filter(uai -> uai.getLanguageRequirements().stream()
94-
.anyMatch(lr -> lr.getLanguageTestType().equals(testType)))
95-
.toList();
96-
}
93+
if (testScore == null || testScore.isBlank()) {
9794
return filteredUnivApplyInfo;
9895
}
9996

97+
/*
98+
* 시험 유형에 따라 성적 비교 방식이 다르다.
99+
* 입력된 점수가 대학에서 요구하는 최소 점수보다 높은지를 '쿼리로' 비교하기엔 쿼리가 지나치게 복잡해진다.
100+
* 따라서 이 부분만 자바 코드로 필터링한다.
101+
* */
100102
return filteredUnivApplyInfo.stream()
101-
.filter(uai -> compareMyTestScoreToMinPassScore(uai, testType, testScore) >= 0)
103+
.filter(uai -> isGivenScoreOverMinPassScore(uai, testType, testScore))
102104
.toList();
103105
}
104106

105-
private int compareMyTestScoreToMinPassScore(UnivApplyInfo univApplyInfo, LanguageTestType testType, String testScore) {
107+
private BooleanExpression languageTestTypeEq(
108+
QLanguageRequirement languageRequirement, LanguageTestType givenTestType
109+
) {
110+
if (givenTestType == null) {
111+
return null;
112+
}
113+
return languageRequirement.languageTestType.eq(givenTestType);
114+
}
115+
116+
private BooleanExpression termEq(QUnivApplyInfo univApplyInfo, String givenTerm) {
117+
if (givenTerm == null || givenTerm.isBlank()) {
118+
return null;
119+
}
120+
return univApplyInfo.term.eq(givenTerm);
121+
}
122+
123+
private BooleanExpression countryCodesIn(QCountry country, List<String> givenCountryCodes) {
124+
if (givenCountryCodes == null || givenCountryCodes.isEmpty()) {
125+
return null;
126+
}
127+
return country.code.in(givenCountryCodes);
128+
}
129+
130+
private boolean isGivenScoreOverMinPassScore(
131+
UnivApplyInfo univApplyInfo, LanguageTestType givenTestType, String givenTestScore
132+
) {
106133
return univApplyInfo.getLanguageRequirements().stream()
107-
.filter(languageRequirement -> languageRequirement.getLanguageTestType().equals(testType))
134+
.filter(languageRequirement -> languageRequirement.getLanguageTestType().equals(givenTestType))
108135
.findFirst()
109-
.map(requirement -> testType.compare(testScore, requirement.getMinScore()))
110-
.orElse(-1);
136+
.map(requirement -> givenTestType.compare(givenTestScore, requirement.getMinScore()))
137+
.orElse(-1) >= 0;
111138
}
112139
}
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package com.example.solidconnection.university.service;
22

33
import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
4-
import com.example.solidconnection.university.domain.LanguageTestType;
54
import com.example.solidconnection.university.domain.UnivApplyInfo;
65
import com.example.solidconnection.university.domain.University;
76
import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse;
7+
import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest;
88
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse;
99
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses;
1010
import com.example.solidconnection.university.repository.UnivApplyInfoRepository;
11-
import com.example.solidconnection.university.repository.custom.UnivApplyInfoFilterRepositoryImpl;
1211
import java.util.List;
1312
import lombok.RequiredArgsConstructor;
1413
import org.springframework.beans.factory.annotation.Value;
@@ -20,7 +19,6 @@
2019
public class UnivApplyInfoQueryService {
2120

2221
private final UnivApplyInfoRepository univApplyInfoRepository;
23-
private final UnivApplyInfoFilterRepositoryImpl universityFilterRepository; // todo: 구현체 숨기고 univApplyInfoRepository만 사용하도록
2422

2523
@Value("${university.term}")
2624
public String term;
@@ -39,22 +37,19 @@ public UnivApplyInfoDetailResponse getUnivApplyInfoDetail(Long univApplyInfoId)
3937
return UnivApplyInfoDetailResponse.of(university, univApplyInfo);
4038
}
4139

42-
/*
43-
* 대학교 검색 결과를 불러온다.
44-
* - 권역, 키워드, 언어 시험 종류, 언어 시험 점수를 조건으로 검색하여 결과를 반환한다.
45-
* - 권역은 영어 대문자로 받는다 e.g. ASIA
46-
* - 키워드는 국가명 또는 대학명에 포함되는 것이 조건이다.
47-
* - 언어 시험 점수는 합격 최소 점수보다 높은 것이 조건이다.
48-
* */
4940
@Transactional(readOnly = true)
50-
@ThunderingHerdCaching(key = "univApplyInfo:{0}:{1}:{2}:{3}", cacheManager = "customCacheManager", ttlSec = 86400)
51-
public UnivApplyInfoPreviewResponses searchUnivApplyInfo(
52-
String regionCode, List<String> keywords, LanguageTestType testType, String testScore) {
53-
54-
return new UnivApplyInfoPreviewResponses(universityFilterRepository
55-
.findAllByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(regionCode, keywords, testType, testScore, term)
56-
.stream()
57-
.map(UnivApplyInfoPreviewResponse::from)
58-
.toList());
41+
public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFilterSearchRequest request) {
42+
List<UnivApplyInfoPreviewResponse> responses = univApplyInfoRepository
43+
.findAllByFilter(request.languageTestType(), request.testScore(), term, request.countryCode())
44+
.stream()
45+
.map(UnivApplyInfoPreviewResponse::from)
46+
.toList();
47+
return new UnivApplyInfoPreviewResponses(responses);
48+
}
49+
50+
@Transactional(readOnly = true)
51+
public UnivApplyInfoPreviewResponses searchUnivApplyInfoByText(String text) {
52+
// todo: 구현
53+
return null;
5954
}
6055
}

0 commit comments

Comments
 (0)