diff --git a/src/main/kotlin/org/example/beyondubackend/common/code/ErrorCode.kt b/src/main/kotlin/org/example/beyondubackend/common/code/ErrorCode.kt index 3c12932..1301ba2 100644 --- a/src/main/kotlin/org/example/beyondubackend/common/code/ErrorCode.kt +++ b/src/main/kotlin/org/example/beyondubackend/common/code/ErrorCode.kt @@ -15,5 +15,8 @@ enum class ErrorCode(val code: String, val httpStatus: HttpStatus, val message: DATABASE_ERROR("S503_001", HttpStatus.SERVICE_UNAVAILABLE, "데이터베이스 통신 오류가 발생했습니다."), // UNIVERSITY - UNIVERSITY_NOT_FOUND("U404_001", HttpStatus.NOT_FOUND, "대학교를 찾을 수 없습니다.") + UNIVERSITY_NOT_FOUND("U404_001", HttpStatus.NOT_FOUND, "대학교를 찾을 수 없습니다."), + + // LANGUAGE REQUIREMENT + INVALID_EXAM_SCORE("L400_001", HttpStatus.BAD_REQUEST, "어학 점수가 유효 범위를 벗어났습니다.") } diff --git a/src/main/kotlin/org/example/beyondubackend/common/enums/ExamType.kt b/src/main/kotlin/org/example/beyondubackend/common/enums/ExamType.kt new file mode 100644 index 0000000..f137c38 --- /dev/null +++ b/src/main/kotlin/org/example/beyondubackend/common/enums/ExamType.kt @@ -0,0 +1,24 @@ +package org.example.beyondubackend.common.enums + +enum class ExamType( + val paramName: String, + val displayName: String, + val minScore: Double, + val maxScore: Double +) { + TOEFL_IBT("TOEFL_IBT", "TOEFL iBT", 0.0, 120.0), + TOEFL_ITP("TOEFL_ITP", "TOEFL ITP", 0.0, 677.0), + IELTS("IELTS", "IELTS", 0.0, 9.0), + TOEIC("TOEIC", "TOEIC", 0.0, 990.0), + TOEIC_SPEAKING("TOEIC_Speaking", "TOEIC Speaking", 0.0, 200.0), + HSK("HSK", "HSK", 1.0, 6.0), + JLPT("JLPT", "JLPT", 1.0, 5.0), + JPT("JPT", "JPT", 0.0, 990.0), + DELF("DELF", "DELF", 1.0, 6.0), + ZD("ZD", "ZD", 1.0, 6.0); + + companion object { + fun fromParamName(paramName: String): ExamType? = entries.find { it.paramName == paramName } + fun fromDisplayName(displayName: String): ExamType? = entries.find { it.displayName == displayName } + } +} diff --git a/src/main/kotlin/org/example/beyondubackend/common/enums/Nation.kt b/src/main/kotlin/org/example/beyondubackend/common/enums/Nation.kt new file mode 100644 index 0000000..fece97a --- /dev/null +++ b/src/main/kotlin/org/example/beyondubackend/common/enums/Nation.kt @@ -0,0 +1,67 @@ +package org.example.beyondubackend.common.enums + +enum class Nation(val displayName: String) { + GUATEMALA("과테말라"), + GREECE("그리스"), + NETHERLANDS("네덜란드"), + NEW_ZEALAND("뉴질랜드"), + TAIWAN("대만"), + DENMARK("덴마크"), + DOMINICAN_REPUBLIC("도미니카공화국"), + GERMANY("독일"), + LATVIA("라트비아"), + RUSSIA("러시아"), + ROMANIA("루마니아"), + LITHUANIA("리투아니아"), + MALAYSIA("말레이시아"), + MEXICO("멕시코"), + MONGOLIA("몽골"), + USA("미국"), + MYANMAR("미얀마"), + BANGLADESH("방글라데시"), + VIETNAM("베트남"), + BELGIUM("벨기에"), + BRAZIL("브라질"), + SWEDEN("스웨덴"), + SWITZERLAND("스위스"), + SPAIN("스페인"), + SINGAPORE("싱가포르"), + ARGENTINA("아르헨티나"), + IRELAND("아일랜드"), + AZERBAIJAN("아제르바이잔"), + ESTONIA("에스토니아"), + ECUADOR("에콰도르"), + UK("영국"), + AUSTRIA("오스트리아"), + URUGUAY("우루과이"), + UZBEKISTAN("우즈베키스탄"), + UKRAINE("우크라이나"), + ITALY("이탈리아"), + INDIA("인도"), + INDONESIA("인도네시아"), + JAPAN("일본"), + CHINA("중국"), + CZECH("체코"), + CHILE("칠레"), + KAZAKHSTAN("카자흐스탄"), + CANADA("캐나다"), + COLOMBIA("콜롬비아"), + CROATIA("크로아티아"), + KYRGYZSTAN("키르기스스탄"), + THAILAND("태국"), + TUNISIA("튀니지"), + TURKEY("튀르키예(터키)"), + PERU("페루"), + PORTUGAL("포르투갈"), + POLAND("폴란드"), + FRANCE("프랑스"), + FINLAND("핀란드"), + PHILIPPINES("필리핀"), + HUNGARY("헝가리"), + AUSTRALIA("호주"), + HONG_KONG("홍콩"); + + companion object { + fun fromDisplayName(name: String): Nation? = entries.find { it.displayName == name } + } +} diff --git a/src/main/kotlin/org/example/beyondubackend/common/resolver/ExamScoreArgumentResolver.kt b/src/main/kotlin/org/example/beyondubackend/common/resolver/ExamScoreArgumentResolver.kt index 3973f97..81e98b8 100644 --- a/src/main/kotlin/org/example/beyondubackend/common/resolver/ExamScoreArgumentResolver.kt +++ b/src/main/kotlin/org/example/beyondubackend/common/resolver/ExamScoreArgumentResolver.kt @@ -1,6 +1,9 @@ package org.example.beyondubackend.common.resolver import org.example.beyondubackend.common.annotation.ExamScoreParams +import org.example.beyondubackend.common.code.ErrorCode +import org.example.beyondubackend.common.enums.ExamType +import org.example.beyondubackend.common.exception.BusinessException import org.springframework.core.MethodParameter import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest @@ -13,25 +16,6 @@ import org.springframework.web.method.support.ModelAndViewContainer */ class ExamScoreArgumentResolver : HandlerMethodArgumentResolver { - companion object { - /** - * 지원하는 어학 시험 목록 - * 새로운 시험 추가 시 이 목록에만 추가하면 됨 - */ - private val SUPPORTED_EXAMS = mapOf( - "TOEFL_IBT" to "TOEFL iBT", - "TOEFL_ITP" to "TOEFL ITP", - "IELTS" to "IELTS", - "TOEIC" to "TOEIC", - "TOEIC_Speaking" to "TOEIC Speaking", - "HSK" to "HSK", - "JLPT" to "JLPT", - "JPT" to "JPT", - "DELF" to "DELF", - "ZD" to "ZD" - ) - } - override fun supportsParameter(parameter: MethodParameter): Boolean { return parameter.hasParameterAnnotation(ExamScoreParams::class.java) } @@ -42,9 +26,17 @@ class ExamScoreArgumentResolver : HandlerMethodArgumentResolver { webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? ): Map { - return SUPPORTED_EXAMS.mapNotNull { (paramName, displayName) -> - val value = webRequest.getParameter(paramName)?.toDoubleOrNull() - if (value != null) displayName to value else null + return ExamType.entries.mapNotNull { examType -> + val value = webRequest.getParameter(examType.paramName)?.toDoubleOrNull() + if (value != null) { + if (value < examType.minScore || value > examType.maxScore) { + throw BusinessException( + ErrorCode.INVALID_EXAM_SCORE, + "${examType.displayName} 점수는 ${examType.minScore} ~ ${examType.maxScore} 범위여야 합니다." + ) + } + examType.displayName to value + } else null }.toMap() } } diff --git a/src/main/kotlin/org/example/beyondubackend/domain/languagerequirement/implement/LanguageRequirementReader.kt b/src/main/kotlin/org/example/beyondubackend/domain/languagerequirement/implement/LanguageRequirementReader.kt index 0a9a183..fdfb85e 100644 --- a/src/main/kotlin/org/example/beyondubackend/domain/languagerequirement/implement/LanguageRequirementReader.kt +++ b/src/main/kotlin/org/example/beyondubackend/domain/languagerequirement/implement/LanguageRequirementReader.kt @@ -1,5 +1,6 @@ package org.example.beyondubackend.domain.languagerequirement.implement +import org.example.beyondubackend.common.enums.ExamType import org.springframework.stereotype.Component @Component @@ -20,11 +21,11 @@ class LanguageRequirementReader( return requirements.joinToString(" / ") { requirement -> when { - requirement.examType.equals("HSK", ignoreCase = true) && requirement.levelCode != null -> { - "HSK ${requirement.levelCode}" + requirement.examType.equals(ExamType.HSK.displayName, ignoreCase = true) && requirement.levelCode != null -> { + "${ExamType.HSK.displayName} ${requirement.levelCode}" } - requirement.examType.equals("HSK", ignoreCase = true) -> { - "HSK ${requirement.minScore.toInt()}" + requirement.examType.equals(ExamType.HSK.displayName, ignoreCase = true) -> { + "${ExamType.HSK.displayName} ${requirement.minScore.toInt()}" } else -> { "${requirement.examType} ${formatScore(requirement.minScore)}" diff --git a/src/main/kotlin/org/example/beyondubackend/domain/meta/application/MetaController.kt b/src/main/kotlin/org/example/beyondubackend/domain/meta/application/MetaController.kt new file mode 100644 index 0000000..f151b60 --- /dev/null +++ b/src/main/kotlin/org/example/beyondubackend/domain/meta/application/MetaController.kt @@ -0,0 +1,32 @@ +package org.example.beyondubackend.domain.meta.application + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.example.beyondubackend.common.dto.ApiResponse +import org.example.beyondubackend.common.enums.ExamType +import org.example.beyondubackend.common.enums.Nation +import org.example.beyondubackend.domain.meta.application.dto.ExamTypeResponse +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Meta", description = "공통 코드 조회 API") +@RestController +@RequestMapping("/api/v1/meta") +class MetaController { + + @Operation(summary = "국가 목록 조회", description = "university 필터링에 사용 가능한 국가 목록을 반환합니다.") + @GetMapping("/nations") + fun getNations(): ResponseEntity>> { + val nations = Nation.entries.map { it.displayName } + return ApiResponse.success(nations) + } + + @Operation(summary = "어학 시험 목록 조회", description = "지원하는 어학 시험 종류와 점수 범위를 반환합니다.") + @GetMapping("/exam-types") + fun getExamTypes(): ResponseEntity>> { + val examTypes = ExamType.entries.map { ExamTypeResponse.from(it) } + return ApiResponse.success(examTypes) + } +} diff --git a/src/main/kotlin/org/example/beyondubackend/domain/meta/application/dto/ExamTypeResponse.kt b/src/main/kotlin/org/example/beyondubackend/domain/meta/application/dto/ExamTypeResponse.kt new file mode 100644 index 0000000..bc3f85d --- /dev/null +++ b/src/main/kotlin/org/example/beyondubackend/domain/meta/application/dto/ExamTypeResponse.kt @@ -0,0 +1,19 @@ +package org.example.beyondubackend.domain.meta.application.dto + +import org.example.beyondubackend.common.enums.ExamType + +data class ExamTypeResponse( + val paramName: String, + val displayName: String, + val minScore: Double, + val maxScore: Double +) { + companion object { + fun from(examType: ExamType) = ExamTypeResponse( + paramName = examType.paramName, + displayName = examType.displayName, + minScore = examType.minScore, + maxScore = examType.maxScore + ) + } +} diff --git a/src/main/kotlin/org/example/beyondubackend/domain/university/storage/UniversityRepositoryImpl.kt b/src/main/kotlin/org/example/beyondubackend/domain/university/storage/UniversityRepositoryImpl.kt index 9f23bdd..774bb5a 100644 --- a/src/main/kotlin/org/example/beyondubackend/domain/university/storage/UniversityRepositoryImpl.kt +++ b/src/main/kotlin/org/example/beyondubackend/domain/university/storage/UniversityRepositoryImpl.kt @@ -3,6 +3,7 @@ package org.example.beyondubackend.domain.university.storage import com.querydsl.core.types.dsl.BooleanExpression import com.querydsl.jpa.JPAExpressions import com.querydsl.jpa.impl.JPAQueryFactory +import org.example.beyondubackend.common.enums.ExamType import org.example.beyondubackend.domain.languagerequirement.storage.QLanguageRequirementEntity.languageRequirementEntity import org.example.beyondubackend.domain.university.implement.University import org.example.beyondubackend.domain.university.implement.UniversityRepository @@ -120,7 +121,7 @@ class UniversityRepositoryImpl( val examConditions = examScores.map { (examType, score) -> // JLPT는 숫자가 낮을수록 높은 레벨 (N1 > N2 > ... > N5) - val scoreCondition = if (examType == "JLPT") { + val scoreCondition = if (examType == ExamType.JLPT.displayName) { languageRequirementEntity.minScore.goe(score) } else { languageRequirementEntity.minScore.loe(score)