Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package com.example.solidconnection.university.controller;

import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.university.domain.LanguageTestType;
import com.example.solidconnection.university.dto.IsLikeResponse;
import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse;
import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest;
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse;
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses;
import com.example.solidconnection.university.dto.UnivApplyInfoRecommendsResponse;
import com.example.solidconnection.university.service.LikedUnivApplyInfoService;
import com.example.solidconnection.university.service.UnivApplyInfoQueryService;
import com.example.solidconnection.university.service.UnivApplyInfoRecommendService;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -84,16 +87,19 @@ public ResponseEntity<UnivApplyInfoDetailResponse> getUnivApplyInfoDetails(
return ResponseEntity.ok(univApplyInfoDetailResponse);
}

// todo: return타입 UniversityInfoForApplyPreviewResponses로 추후 수정 필요
Copy link
Collaborator Author

@nayonsoso nayonsoso Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

▶️ todo 내용을 반영했습니다.


고민한 지점 : 페이지네이션으로 내려줘야 할지, 검색 결과를 한번에 내려줘야할지 고민이 되었습니다.

그런데 뒤에 언급하는 내용처럼
점수의 경우 DB단에서 비교하기가 매우 까다롭기 때문에 메모리에서 필터링 하는데
이때 페이지네이션을 적용하기 까다롭다는 점과,
한 학기의 대학 지원 정보가 약 400개정도이므로, 엄청난 성능 저하는 있지 않을것이라 판단하여
검색된 결과 전체를 반환하도록 구현했습니다!

@GetMapping("/search")
public ResponseEntity<List<UnivApplyInfoPreviewResponse>> searchUnivApplyInfo(
@RequestParam(required = false, defaultValue = "") String region,
@RequestParam(required = false, defaultValue = "") List<String> keyword,
@RequestParam(required = false, defaultValue = "") LanguageTestType testType,
@RequestParam(required = false, defaultValue = "") String testScore
@GetMapping("/search/filter")
public ResponseEntity<UnivApplyInfoPreviewResponses> searchUnivApplyInfoByFilter(
@Valid @ModelAttribute UnivApplyInfoFilterSearchRequest request
) {
List<UnivApplyInfoPreviewResponse> univApplyInfoPreviewResponse
= univApplyInfoQueryService.searchUnivApplyInfo(region, keyword, testType, testScore).univApplyInfoPreviews();
return ResponseEntity.ok(univApplyInfoPreviewResponse);
UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request);
return ResponseEntity.ok(response);
}

@GetMapping("/search/text")
public ResponseEntity<UnivApplyInfoPreviewResponses> searchUnivApplyInfoByText(
@RequestParam(required = false) String text
) {
UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByText(text);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

public enum LanguageTestType {

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

private final Comparator<String> comparator;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.solidconnection.university.dto;

import com.example.solidconnection.university.domain.LanguageTestType;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record UnivApplyInfoFilterSearchRequest(

@NotNull(message = "어학 시험 종류를 선택해주세요.")
LanguageTestType languageTestType,
String testScore,
List<String> countryCode
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,10 @@

import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.university.domain.University;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UniversityRepository extends JpaRepository<University, Long> {

@Query("SELECT u FROM University u WHERE u.country.code IN :countryCodes OR u.region.code IN :regionCodes")
List<University> findByCountryCodeInOrRegionCodeIn(@Param("countryCodes") List<String> countryCodes, @Param("regionCodes") List<String> regionCodes);

default University getUniversityById(Long id) {
return findById(id)
.orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ public interface UnivApplyInfoFilterRepository {

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

List<UnivApplyInfo> findAllByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(
String regionCode, List<String> keywords, LanguageTestType testType, String testScore, String term);
List<UnivApplyInfo> findAllByFilter(LanguageTestType testType, String testScore, String term, List<String> countryKoreanNames);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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;
Expand Down Expand Up @@ -70,43 +69,71 @@ private BooleanExpression createKeywordCondition(StringPath namePath, List<Strin
}

@Override
public List<UnivApplyInfo> findAllByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(
String regionCode, List<String> keywords, LanguageTestType testType, String testScore, String term) {
public List<UnivApplyInfo> findAllByFilter(
LanguageTestType testType, String testScore, String term, List<String> countryCodes
) {
QUniversity university = QUniversity.university;
QCountry country = QCountry.country;
QRegion region = QRegion.region;
QUnivApplyInfo univApplyInfo = QUnivApplyInfo.univApplyInfo;
QCountry country = QCountry.country;
QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement;

List<UnivApplyInfo> filteredUnivApplyInfo = queryFactory
.selectFrom(univApplyInfo)
List<UnivApplyInfo> filteredUnivApplyInfo = queryFactory.selectFrom(univApplyInfo)
.join(univApplyInfo.university, university)
.join(university.country, country)
.join(university.region, region)
.where(regionCodeEq(country, regionCode)
.and(countryOrUniversityContainsKeyword(country, university, keywords))
.and(univApplyInfo.term.eq(term)))
.join(univApplyInfo.languageRequirements, languageRequirement)
.fetchJoin()
.where(
languageTestTypeEq(languageRequirement, testType),
termEq(univApplyInfo, term),
countryCodesIn(country, countryCodes)
)
.distinct()
.fetch();

if (testScore == null || testScore.isEmpty()) {
if (testType != null) {
return filteredUnivApplyInfo.stream()
.filter(uai -> uai.getLanguageRequirements().stream()
.anyMatch(lr -> lr.getLanguageTestType().equals(testType)))
.toList();
}
if (testScore == null || testScore.isBlank()) {
return filteredUnivApplyInfo;
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LanguageTestType enum 을 확인하시면 아시겠지만,
어학 시험마다 "무엇이 더 높은 점수인지" 비교하는 로직이 각각 다릅니다.
이것을 조건으로 쿼리 문을 실행하려 하면, 엄청나게 긴 case 문이 발생할 것입니다 😱
따라서 이 부분은 자바의 메모리에서 처리하게 했습니다.

앞에도 언급했듯, 한 학기에 열리는 대학은 약 400여개입니다.
여기에서 어학 시험 종류와 국가 코드로 한번씩 필터링 된 것을 메모리에 불러오는 것이므로
메모리와 관련된 이슈는 없을 것이라 생각합니다!


private int compareMyTestScoreToMinPassScore(UnivApplyInfo univApplyInfo, LanguageTestType testType, String testScore) {
private BooleanExpression languageTestTypeEq(
QLanguageRequirement languageRequirement, LanguageTestType givenTestType
) {
if (givenTestType == null) {
return null;
}
return languageRequirement.languageTestType.eq(givenTestType);
}

private BooleanExpression termEq(QUnivApplyInfo univApplyInfo, String givenTerm) {
if (givenTerm == null || givenTerm.isBlank()) {
return null;
}
return univApplyInfo.term.eq(givenTerm);
}

private BooleanExpression countryCodesIn(QCountry country, List<String> givenCountryCodes) {
if (givenCountryCodes == null || givenCountryCodes.isEmpty()) {
return null;
}
return country.code.in(givenCountryCodes);
}

private boolean isGivenScoreOverMinPassScore(
UnivApplyInfo univApplyInfo, LanguageTestType givenTestType, String givenTestScore
) {
return univApplyInfo.getLanguageRequirements().stream()
.filter(languageRequirement -> languageRequirement.getLanguageTestType().equals(testType))
.filter(languageRequirement -> languageRequirement.getLanguageTestType().equals(givenTestType))
.findFirst()
.map(requirement -> testType.compare(testScore, requirement.getMinScore()))
.orElse(-1);
.map(requirement -> givenTestType.compare(givenTestScore, requirement.getMinScore()))
.orElse(-1) >= 0;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.example.solidconnection.university.service;

import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
import com.example.solidconnection.university.domain.LanguageTestType;
import com.example.solidconnection.university.domain.UnivApplyInfo;
import com.example.solidconnection.university.domain.University;
import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse;
import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest;
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse;
import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses;
import com.example.solidconnection.university.repository.UnivApplyInfoRepository;
import com.example.solidconnection.university.repository.custom.UnivApplyInfoFilterRepositoryImpl;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -20,7 +19,6 @@
public class UnivApplyInfoQueryService {

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

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

/*
* 대학교 검색 결과를 불러온다.
* - 권역, 키워드, 언어 시험 종류, 언어 시험 점수를 조건으로 검색하여 결과를 반환한다.
* - 권역은 영어 대문자로 받는다 e.g. ASIA
* - 키워드는 국가명 또는 대학명에 포함되는 것이 조건이다.
* - 언어 시험 점수는 합격 최소 점수보다 높은 것이 조건이다.
* */
@Transactional(readOnly = true)
@ThunderingHerdCaching(key = "univApplyInfo:{0}:{1}:{2}:{3}", cacheManager = "customCacheManager", ttlSec = 86400)
public UnivApplyInfoPreviewResponses searchUnivApplyInfo(
String regionCode, List<String> keywords, LanguageTestType testType, String testScore) {

return new UnivApplyInfoPreviewResponses(universityFilterRepository
.findAllByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(regionCode, keywords, testType, testScore, term)
.stream()
.map(UnivApplyInfoPreviewResponse::from)
.toList());
public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFilterSearchRequest request) {
List<UnivApplyInfoPreviewResponse> responses = univApplyInfoRepository
.findAllByFilter(request.languageTestType(), request.testScore(), term, request.countryCode())
.stream()
.map(UnivApplyInfoPreviewResponse::from)
.toList();
return new UnivApplyInfoPreviewResponses(responses);
}

@Transactional(readOnly = true)
public UnivApplyInfoPreviewResponses searchUnivApplyInfoByText(String text) {
// todo: 구현
return null;
}
}
Loading
Loading