diff --git a/systems/application/application-adapter-in/BUILD.bazel b/systems/application/application-adapter-in/BUILD.bazel index 667f1fcf..f5e6989e 100644 --- a/systems/application/application-adapter-in/BUILD.bazel +++ b/systems/application/application-adapter-in/BUILD.bazel @@ -17,5 +17,5 @@ kt_jvm_test( javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", test_class = "hs.kr.entrydsm.application.adapterin.ApplicationAdapterInModuleTest", - deps = MODULE_DEPS + TEST_DEPS, + deps = [":main"] + MODULE_DEPS + TEST_DEPS, ) diff --git a/systems/application/application-adapter-in/deps.bzl b/systems/application/application-adapter-in/deps.bzl index 5cf03643..4c8f31f7 100644 --- a/systems/application/application-adapter-in/deps.bzl +++ b/systems/application/application-adapter-in/deps.bzl @@ -1,4 +1,8 @@ -KOTLIN_DEPS = [] +KOTLIN_DEPS = [ + "@maven//:org_springframework_boot_spring_boot_starter_web", + "//systems/application/application-application:main", + "//systems/application/application-domain:main", +] TEST_DEPS = [ "@maven//:junit_junit", diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/ApplicationController.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/ApplicationController.kt new file mode 100644 index 00000000..07943d97 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/ApplicationController.kt @@ -0,0 +1,210 @@ +package hs.kr.entrydsm.application.adapterin.web + +import hs.kr.entrydsm.application.adapterin.web.config.LandingScheduleProperties +import hs.kr.entrydsm.application.adapterin.web.dto.common.ApiResponse +import hs.kr.entrydsm.application.adapterin.web.dto.common.toResponse +import hs.kr.entrydsm.application.adapterin.web.dto.request.UpdateFamilyRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.UpdateIntroductionRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.UpdateMiddleSchoolRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.UpdatePersonalRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.UpdateStudyPlanRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.UpdateTypeRequest +import hs.kr.entrydsm.application.adapterin.web.dto.response.CreateApplicantResponse +import hs.kr.entrydsm.application.adapterin.web.dto.response.LandingResponse +import hs.kr.entrydsm.application.application.port.`in`.ApplicationPort +import hs.kr.entrydsm.application.application.port.`in`.command.CreateApplicantCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SubmitApplicationCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateFamilyCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateIntroductionCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateMiddleSchoolCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdatePersonalCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateStudyPlanCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateTypeCommand +import hs.kr.entrydsm.application.domain.enum.GuardianRelation +import hs.kr.entrydsm.application.domain.enum.SpecialAdmissionType +import java.time.LocalDate +import java.time.YearMonth +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/application/v11/applicants") +class ApplicationController( + private val applicationPort: ApplicationPort, + private val landingScheduleProperties: LandingScheduleProperties, +) { + @GetMapping("/landing") + fun getLanding( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + ): ApiResponse { + val result = applicationPort.getLanding(userId) + return ApiResponse(data = result.toResponse(landingScheduleProperties)) + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun createApplicant( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + ): ApiResponse { + val result = applicationPort.createApplicant( + CreateApplicantCommand( + userId = userId, + ), + ) + return ApiResponse(data = result.toResponse()) + } + + @PatchMapping("/{id}/type") + fun updateType( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @PathVariable id: Long, + @RequestBody request: UpdateTypeRequest, + ): ApiResponse { + applicationPort.updateType( + UpdateTypeCommand( + userId = userId, + applicantId = id, + admissionType = request.admissionType, + region = request.region, + graduationType = request.graduationType, + graduationDate = request.graduationDate?.let(YearMonth::parse), + ), + ) + return ApiResponse(data = null) + } + + @PatchMapping("/{id}/personal") + fun updatePersonal( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @PathVariable id: Long, + @RequestBody request: UpdatePersonalRequest, + ): ApiResponse { + applicationPort.updatePersonal( + UpdatePersonalCommand( + userId = userId, + applicantId = id, + photoFileId = request.photoFileId, + name = request.name, + phoneNumber = request.phoneNumber, + gender = request.gender, + birthdate = parseDate(request.birthdate), + specialAdmissionType = request.specialAdmissionType ?: SpecialAdmissionType.NONE, + ), + ) + return ApiResponse(data = null) + } + + @PatchMapping("/{id}/family") + fun updateFamily( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @PathVariable id: Long, + @RequestBody request: UpdateFamilyRequest, + ): ApiResponse { + applicationPort.updateFamily( + UpdateFamilyCommand( + userId = userId, + applicantId = id, + guardianName = request.guardianName, + guardianPhoneNumber = request.guardianPhoneNumber, + guardianGender = request.guardianGender, + guardianRelation = request.guardianRelation.toGuardianRelation(), + zipCode = request.address.zipCode, + addressBase = request.address.addressBase, + addressDetail = request.address.addressDetail, + ), + ) + return ApiResponse(data = null) + } + + @PatchMapping("/{id}/middle-school") + fun updateMiddleSchool( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @PathVariable id: Long, + @RequestBody request: UpdateMiddleSchoolRequest, + ): ApiResponse { + applicationPort.updateMiddleSchool( + UpdateMiddleSchoolCommand( + userId = userId, + applicantId = id, + schoolName = request.schoolName, + studentNumber = request.studentNumber, + schoolPhone = request.schoolPhone, + teacherName = request.teacherName, + ), + ) + return ApiResponse(data = null) + } + + @PatchMapping("/{id}/self-introduction") + fun updateIntroduction( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @PathVariable id: Long, + @RequestBody request: UpdateIntroductionRequest, + ): ApiResponse { + applicationPort.updateIntroduction( + UpdateIntroductionCommand( + userId = userId, + applicantId = id, + introduction = request.introduction, + ), + ) + return ApiResponse(data = null) + } + + @PatchMapping("/{id}/study-plan") + fun updateStudyPlan( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @PathVariable id: Long, + @RequestBody request: UpdateStudyPlanRequest, + ): ApiResponse { + applicationPort.updateStudyPlan( + UpdateStudyPlanCommand( + userId = userId, + applicantId = id, + studyPlan = request.studyPlan, + ), + ) + return ApiResponse(data = null) + } + + @PatchMapping + fun submit( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + ): ApiResponse { + applicationPort.submit( + SubmitApplicationCommand( + userId = userId, + ), + ) + return ApiResponse(data = null) + } + + private fun parseDate(value: String): LocalDate { + return if (value.length == 7) { + YearMonth.parse(value).atDay(1) + } else { + LocalDate.parse(value) + } + } + + private fun String.toGuardianRelation(): GuardianRelation { + return when (uppercase()) { + "FATHER", "FATHER_RELATION" -> GuardianRelation.FATHER + "MOTHER", "MOTHER_RELATION" -> GuardianRelation.MOTHER + "OTHER" -> GuardianRelation.OTHER + else -> throw IllegalArgumentException("guardianRelation must be FATHER, MOTHER, or OTHER") + } + } + + private companion object { + const val USER_ID_HEADER = "user-id" + } +} diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/EvaluationController.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/EvaluationController.kt new file mode 100644 index 00000000..5924b593 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/EvaluationController.kt @@ -0,0 +1,140 @@ +package hs.kr.entrydsm.application.adapterin.web + +import hs.kr.entrydsm.application.adapterin.web.dto.common.ApiResponse +import hs.kr.entrydsm.application.adapterin.web.dto.common.toResponse +import hs.kr.entrydsm.application.adapterin.web.dto.request.SaveAcademicRecordRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.SaveCertificatesRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.SaveGedScoresRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.SaveSubjectGradesRequest +import hs.kr.entrydsm.application.adapterin.web.dto.response.AcademicRecordResponse +import hs.kr.entrydsm.application.application.port.`in`.EvaluationPort +import hs.kr.entrydsm.application.application.port.`in`.command.CalculateEvaluationCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveAcademicRecordCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveCertificatesCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveGedScoresCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveSubjectGradesCommand +import hs.kr.entrydsm.application.domain.enum.SchoolSemester +import hs.kr.entrydsm.application.domain.model.GedScores +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/evaluation/v11/evaluations") +class EvaluationController( + private val evaluationPort: EvaluationPort, +) { + @PostMapping("/grades/expected") + fun saveExpectedGrades( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @RequestBody request: SaveSubjectGradesRequest, + ): ApiResponse { + saveSubjectGrades(userId, request) + return ApiResponse(data = null) + } + + @PostMapping("/grades/graduated") + fun saveGraduatedGrades( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @RequestBody request: SaveSubjectGradesRequest, + ): ApiResponse { + saveSubjectGrades(userId, request) + return ApiResponse(data = null) + } + + @PostMapping("/ged-scores") + fun saveGedScores( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @RequestBody request: SaveGedScoresRequest, + ): ApiResponse { + evaluationPort.saveGedScores( + SaveGedScoresCommand( + userId = userId, + gedScores = GedScores( + koreanScore = request.koreanScore, + mathScore = request.mathScore, + englishScore = request.englishScore, + scienceScore = request.scienceScore, + societyScore = request.societyScore, + technologyScore = request.technologyScore, + historyScore = request.historyScore, + ), + ), + ) + return ApiResponse(data = null) + } + + @PostMapping("/academic-records") + fun saveAcademicRecords( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @RequestBody request: SaveAcademicRecordRequest, + ): ApiResponse { + val result = evaluationPort.saveAcademicRecord( + SaveAcademicRecordCommand( + userId = userId, + absentCount = request.absentCount, + earlyLeaveCount = request.earlyLeaveCount, + lateCount = request.lateCount, + classAbsenceCount = request.classAbsenceCount, + volunteerTime = request.volunteerTime, + ), + ) + return ApiResponse(data = result.toResponse()) + } + + @PostMapping("/certificates") + fun saveCertificates( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + @RequestBody request: SaveCertificatesRequest, + ): ApiResponse { + evaluationPort.saveCertificates( + SaveCertificatesCommand( + userId = userId, + isDsmAlgorithmAwarded = request.isDsmAlgorithmAwarded, + isProgrammingCertified = request.isProgrammingCertified, + ), + ) + return ApiResponse(data = null) + } + + @PostMapping("/result") + fun getResult( + @RequestHeader(USER_ID_HEADER, required = false) userId: Long? = null, + ): ApiResponse { + evaluationPort.calculateResult( + CalculateEvaluationCommand( + userId = userId, + ), + ) + return ApiResponse(data = null) + } + + private fun saveSubjectGrades( + userId: Long?, + request: SaveSubjectGradesRequest, + ) { + evaluationPort.saveSubjectGrades( + SaveSubjectGradesCommand( + userId = userId, + schoolSemester = request.schoolSemester.toSchoolSemester(), + subjectGrades = request.subjects.toDomain(), + ), + ) + } + + private companion object { + const val USER_ID_HEADER = "user-id" + } +} + +private fun String.toSchoolSemester(): SchoolSemester { + return when (this) { + "2-1" -> SchoolSemester.SECOND_GRADE_FIRST_SEMESTER + "2-2" -> SchoolSemester.SECOND_GRADE_SECOND_SEMESTER + "3-1" -> SchoolSemester.THIRD_GRADE_FIRST_SEMESTER + "3-2" -> SchoolSemester.THIRD_GRADE_SECOND_SEMESTER + else -> throw IllegalArgumentException("schoolSemester must be one of 2-1, 2-2, 3-1, 3-2") + } +} diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/config/LandingScheduleProperties.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/config/LandingScheduleProperties.kt new file mode 100644 index 00000000..819cbe7e --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/config/LandingScheduleProperties.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.application.adapterin.web.config + +import java.time.LocalDateTime +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component + +@Component +class LandingScheduleProperties( + @Value("\${entrydsm.application.schedule.application-start-at}") + val applicationStartAt: LocalDateTime, + + @Value("\${entrydsm.application.schedule.application-end-at}") + val applicationEndAt: LocalDateTime, + + @Value("\${entrydsm.application.schedule.result-announced-at}") + val resultAnnouncedAt: LocalDateTime, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ApiResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ApiResponse.kt new file mode 100644 index 00000000..a56a85ef --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ApiResponse.kt @@ -0,0 +1,7 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.common + +data class ApiResponse( + val success: Boolean = true, + val data: T?, + val error: ErrorDetail? = null, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ErrorDetail.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ErrorDetail.kt new file mode 100644 index 00000000..6e1060c9 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ErrorDetail.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.common + +data class ErrorDetail( + val code: String, + val message: String, + val status: Int, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ErrorResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ErrorResponse.kt new file mode 100644 index 00000000..3c75e6d4 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ErrorResponse.kt @@ -0,0 +1,10 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.common + +import java.time.Instant + +data class ErrorResponse( + val success: Boolean = false, + val error: ErrorDetail, + val timestamp: Instant = Instant.now(), +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ResponseMapper.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ResponseMapper.kt new file mode 100644 index 00000000..4d830e0b --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/common/ResponseMapper.kt @@ -0,0 +1,35 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.common + +import hs.kr.entrydsm.application.adapterin.web.config.LandingScheduleProperties +import hs.kr.entrydsm.application.adapterin.web.dto.response.AcademicRecordResponse +import hs.kr.entrydsm.application.adapterin.web.dto.response.CreateApplicantResponse +import hs.kr.entrydsm.application.adapterin.web.dto.response.LandingResponse +import hs.kr.entrydsm.application.adapterin.web.dto.response.PeriodResponse +import hs.kr.entrydsm.application.adapterin.web.dto.response.ScheduleResponse +import hs.kr.entrydsm.application.application.port.`in`.result.AcademicRecordResult +import hs.kr.entrydsm.application.application.port.`in`.result.CreateApplicantResult +import hs.kr.entrydsm.application.application.port.`in`.result.LandingResult + +fun CreateApplicantResult.toResponse(): CreateApplicantResponse = + CreateApplicantResponse(applicantId = applicantId) + +fun LandingResult.toResponse(scheduleProperties: LandingScheduleProperties): LandingResponse = + LandingResponse( + applicantName = applicantName, + schedule = ScheduleResponse( + applicationPeriod = PeriodResponse( + startAt = scheduleProperties.applicationStartAt, + endAt = scheduleProperties.applicationEndAt, + ), + resultAnnouncedAt = scheduleProperties.resultAnnouncedAt, + ), + ) + +fun AcademicRecordResult.toResponse(): AcademicRecordResponse = + AcademicRecordResponse( + absentCount = absentCount, + earlyLeaveCount = earlyLeaveCount, + lateCount = lateCount, + classAbsenceCount = classAbsenceCount, + volunteerTime = volunteerTime, + ) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/AddressRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/AddressRequest.kt new file mode 100644 index 00000000..b2a84340 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/AddressRequest.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class AddressRequest( + val zipCode: String, + val addressBase: String, + val addressDetail: String, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveAcademicRecordRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveAcademicRecordRequest.kt new file mode 100644 index 00000000..cba44a56 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveAcademicRecordRequest.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class SaveAcademicRecordRequest( + val absentCount: Int, + val earlyLeaveCount: Int, + val lateCount: Int, + val classAbsenceCount: Int, + val volunteerTime: Int, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveCertificatesRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveCertificatesRequest.kt new file mode 100644 index 00000000..f26e0b40 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveCertificatesRequest.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class SaveCertificatesRequest( + val isDsmAlgorithmAwarded: Boolean, + val isProgrammingCertified: Boolean, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveGedScoresRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveGedScoresRequest.kt new file mode 100644 index 00000000..8b78a480 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveGedScoresRequest.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class SaveGedScoresRequest( + val koreanScore: Int, + val societyScore: Int, + val englishScore: Int, + val historyScore: Int, + val mathScore: Int, + val scienceScore: Int, + val technologyScore: Int, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveSubjectGradesRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveSubjectGradesRequest.kt new file mode 100644 index 00000000..f15f55ab --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SaveSubjectGradesRequest.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class SaveSubjectGradesRequest( + val schoolSemester: String, + val subjects: SubjectGradesRequest, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SubjectGradesRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SubjectGradesRequest.kt new file mode 100644 index 00000000..74c65055 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/SubjectGradesRequest.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +import hs.kr.entrydsm.application.domain.enum.SubjectGrade +import hs.kr.entrydsm.application.domain.model.SubjectGrades + +data class SubjectGradesRequest( + val koreanGrade: SubjectGrade, + val societyGrade: SubjectGrade, + val englishGrade: SubjectGrade, + val historyGrade: SubjectGrade, + val mathGrade: SubjectGrade, + val scienceGrade: SubjectGrade, + val technologyGrade: SubjectGrade, +) { + fun toDomain(): SubjectGrades = + SubjectGrades( + koreanGrade = koreanGrade, + societyGrade = societyGrade, + englishGrade = englishGrade, + historyGrade = historyGrade, + mathGrade = mathGrade, + scienceGrade = scienceGrade, + technologyGrade = technologyGrade, + ) +} diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateFamilyRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateFamilyRequest.kt new file mode 100644 index 00000000..6059e5de --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateFamilyRequest.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +import hs.kr.entrydsm.application.domain.enum.Gender + +data class UpdateFamilyRequest( + val guardianName: String, + val guardianPhoneNumber: String, + val guardianGender: Gender, + val guardianRelation: String, + val address: AddressRequest, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateIntroductionRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateIntroductionRequest.kt new file mode 100644 index 00000000..153044bc --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateIntroductionRequest.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class UpdateIntroductionRequest( + val introduction: String, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateMiddleSchoolRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateMiddleSchoolRequest.kt new file mode 100644 index 00000000..034b3f30 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateMiddleSchoolRequest.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class UpdateMiddleSchoolRequest( + val schoolName: String, + val studentNumber: String, + val schoolPhone: String, + val teacherName: String, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdatePersonalRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdatePersonalRequest.kt new file mode 100644 index 00000000..9fd28031 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdatePersonalRequest.kt @@ -0,0 +1,13 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +import hs.kr.entrydsm.application.domain.enum.Gender +import hs.kr.entrydsm.application.domain.enum.SpecialAdmissionType + +data class UpdatePersonalRequest( + val photoFileId: Long, + val name: String, + val phoneNumber: String, + val gender: Gender, + val birthdate: String, + val specialAdmissionType: SpecialAdmissionType? = null, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateStudyPlanRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateStudyPlanRequest.kt new file mode 100644 index 00000000..22174416 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateStudyPlanRequest.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +data class UpdateStudyPlanRequest( + val studyPlan: String, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateTypeRequest.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateTypeRequest.kt new file mode 100644 index 00000000..35e9c4bd --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/request/UpdateTypeRequest.kt @@ -0,0 +1,12 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.request + +import hs.kr.entrydsm.application.domain.enum.AdmissionType +import hs.kr.entrydsm.application.domain.enum.GraduationType +import hs.kr.entrydsm.application.domain.enum.Region + +data class UpdateTypeRequest( + val admissionType: AdmissionType, + val region: Region, + val graduationType: GraduationType, + val graduationDate: String?, +) diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/AcademicRecordResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/AcademicRecordResponse.kt new file mode 100644 index 00000000..b1e71abd --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/AcademicRecordResponse.kt @@ -0,0 +1,10 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.response + +data class AcademicRecordResponse( + val absentCount: Int, + val earlyLeaveCount: Int, + val lateCount: Int, + val classAbsenceCount: Int, + val volunteerTime: Int, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/CreateApplicantResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/CreateApplicantResponse.kt new file mode 100644 index 00000000..e5116f5e --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/CreateApplicantResponse.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.response + +data class CreateApplicantResponse( + val applicantId: Long, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/LandingResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/LandingResponse.kt new file mode 100644 index 00000000..d1467aee --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/LandingResponse.kt @@ -0,0 +1,7 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.response + +data class LandingResponse( + val applicantName: String?, + val schedule: ScheduleResponse, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/PeriodResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/PeriodResponse.kt new file mode 100644 index 00000000..a907ad6c --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/PeriodResponse.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.response + +import java.time.LocalDateTime + +data class PeriodResponse( + val startAt: LocalDateTime, + val endAt: LocalDateTime, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/ScheduleResponse.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/ScheduleResponse.kt new file mode 100644 index 00000000..1e73c843 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/dto/response/ScheduleResponse.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.application.adapterin.web.dto.response + +import java.time.LocalDateTime + +data class ScheduleResponse( + val applicationPeriod: PeriodResponse, + val resultAnnouncedAt: LocalDateTime, +) + diff --git a/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/exception/GlobalExceptionHandler.kt b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/exception/GlobalExceptionHandler.kt new file mode 100644 index 00000000..a997ae90 --- /dev/null +++ b/systems/application/application-adapter-in/src/main/kotlin/hs/kr/entrydsm/application/adapterin/web/exception/GlobalExceptionHandler.kt @@ -0,0 +1,96 @@ +package hs.kr.entrydsm.application.adapterin.web.exception + +import hs.kr.entrydsm.application.adapterin.web.dto.common.ErrorDetail +import hs.kr.entrydsm.application.adapterin.web.dto.common.ErrorResponse +import hs.kr.entrydsm.application.application.exception.ApplicantAccessDeniedException +import hs.kr.entrydsm.application.application.exception.ApplicantNotFoundException +import hs.kr.entrydsm.application.application.exception.AuthenticationRequiredException +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.web.bind.MissingPathVariableException +import org.springframework.web.bind.MissingServletRequestParameterException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException +import org.springframework.web.multipart.support.MissingServletRequestPartException + +@RestControllerAdvice +class GlobalExceptionHandler { + private val logger = LoggerFactory.getLogger(javaClass) + + @ExceptionHandler(ApplicantNotFoundException::class) + fun handleApplicantNotFound(exception: ApplicantNotFoundException): ResponseEntity = + response( + status = HttpStatus.NOT_FOUND, + code = "APPLICANT_NOT_FOUND", + message = exception.message ?: "applicant not found", + ) + + @ExceptionHandler(ApplicantAccessDeniedException::class) + fun handleApplicantAccessDenied(exception: ApplicantAccessDeniedException): ResponseEntity = + response( + status = HttpStatus.FORBIDDEN, + code = "APPLICANT_ACCESS_DENIED", + message = exception.message ?: "applicant access denied", + ) + + @ExceptionHandler(AuthenticationRequiredException::class) + fun handleAuthenticationRequired(exception: AuthenticationRequiredException): ResponseEntity = + response( + status = HttpStatus.UNAUTHORIZED, + code = "AUTHENTICATION_REQUIRED", + message = exception.message ?: "authentication is required", + ) + + @ExceptionHandler( + IllegalArgumentException::class, + HttpMessageNotReadableException::class, + MethodArgumentNotValidException::class, + MissingPathVariableException::class, + MissingServletRequestParameterException::class, + MissingServletRequestPartException::class, + MethodArgumentTypeMismatchException::class, + ) + fun handleInvalidRequest(exception: Exception): ResponseEntity = + response( + status = HttpStatus.BAD_REQUEST, + code = "INVALID_REQUEST", + message = exception.message ?: "invalid request", + ) + + @ExceptionHandler(Exception::class) + fun handleUnhandledException(exception: Exception): ResponseEntity = + response( + status = HttpStatus.INTERNAL_SERVER_ERROR, + code = "INTERNAL_SERVER_ERROR", + message = "internal server error", + ).also { + logger.error( + "Unhandled exception [correlationId={}]", + MDC.get("correlationId") ?: "unknown", + exception, + ) + } + + private fun response( + status: HttpStatus, + code: String, + message: String, + ): ResponseEntity = + ResponseEntity + .status(status) + .body( + ErrorResponse( + error = ErrorDetail( + code = code, + message = message, + status = status.value(), + ), + ), + ) +} + diff --git a/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt b/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt index 6882fcc5..fabb7459 100644 --- a/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt +++ b/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt @@ -1,11 +1,13 @@ package hs.kr.entrydsm.application.adapterin -import org.junit.Assert.assertTrue -import org.junit.Test +import hs.kr.entrydsm.application.adapterin.web.ApplicationControllerTest +import hs.kr.entrydsm.application.adapterin.web.EvaluationControllerTest +import org.junit.runner.RunWith +import org.junit.runners.Suite -class ApplicationAdapterInModuleTest { - @Test - fun moduleLoads() { - assertTrue(true) - } -} +@RunWith(Suite::class) +@Suite.SuiteClasses( + ApplicationControllerTest::class, + EvaluationControllerTest::class, +) +class ApplicationAdapterInModuleTest diff --git a/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/application/adapterin/web/ApplicationControllerTest.kt b/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/application/adapterin/web/ApplicationControllerTest.kt new file mode 100644 index 00000000..33df532c --- /dev/null +++ b/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/application/adapterin/web/ApplicationControllerTest.kt @@ -0,0 +1,75 @@ +package hs.kr.entrydsm.application.adapterin.web + +import hs.kr.entrydsm.application.adapterin.web.config.LandingScheduleProperties +import hs.kr.entrydsm.application.application.port.`in`.ApplicationPort +import hs.kr.entrydsm.application.application.port.`in`.command.CreateApplicantCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SubmitApplicationCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateFamilyCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateIntroductionCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateMiddleSchoolCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdatePersonalCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateStudyPlanCommand +import hs.kr.entrydsm.application.application.port.`in`.command.UpdateTypeCommand +import hs.kr.entrydsm.application.application.port.`in`.result.CreateApplicantResult +import hs.kr.entrydsm.application.application.port.`in`.result.LandingResult +import java.time.LocalDateTime +import org.junit.Assert.assertEquals +import org.junit.Test + +class ApplicationControllerTest { + @Test + fun createApplicantPassesAuthenticatedUser() { + val applicationPort = FakeApplicationPort() + val controller = ApplicationController(applicationPort, scheduleProperties()) + + val response = controller.createApplicant( + userId = 10L, + ) + + assertEquals(10L, applicationPort.createApplicantCommand?.userId) + assertEquals(1L, response.data?.applicantId) + } + + @Test + fun getLandingReturnsConfiguredSchedule() { + val controller = ApplicationController(FakeApplicationPort(), scheduleProperties()) + + val response = controller.getLanding(10L) + + assertEquals("홍길동", response.data?.applicantName) + assertEquals(applicationStartAt, response.data?.schedule?.applicationPeriod?.startAt) + assertEquals(applicationEndAt, response.data?.schedule?.applicationPeriod?.endAt) + assertEquals(resultAnnouncedAt, response.data?.schedule?.resultAnnouncedAt) + } + + private class FakeApplicationPort : ApplicationPort { + var createApplicantCommand: CreateApplicantCommand? = null + + override fun createApplicant(command: CreateApplicantCommand): CreateApplicantResult { + createApplicantCommand = command + return CreateApplicantResult(applicantId = 1L) + } + + override fun updateType(command: UpdateTypeCommand) = Unit + override fun updatePersonal(command: UpdatePersonalCommand) = Unit + override fun updateFamily(command: UpdateFamilyCommand) = Unit + override fun updateMiddleSchool(command: UpdateMiddleSchoolCommand) = Unit + override fun updateIntroduction(command: UpdateIntroductionCommand) = Unit + override fun updateStudyPlan(command: UpdateStudyPlanCommand) = Unit + override fun submit(command: SubmitApplicationCommand) = Unit + override fun getLanding(accountId: Long?): LandingResult = LandingResult(applicantName = "홍길동") + } + + private companion object { + val applicationStartAt: LocalDateTime = LocalDateTime.parse("2026-10-19T09:00:00") + val applicationEndAt: LocalDateTime = LocalDateTime.parse("2026-10-23T17:00:00") + val resultAnnouncedAt: LocalDateTime = LocalDateTime.parse("2026-10-30T10:00:00") + + fun scheduleProperties(): LandingScheduleProperties = + LandingScheduleProperties( + applicationStartAt = applicationStartAt, + applicationEndAt = applicationEndAt, + resultAnnouncedAt = resultAnnouncedAt, + ) + } +} diff --git a/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/application/adapterin/web/EvaluationControllerTest.kt b/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/application/adapterin/web/EvaluationControllerTest.kt new file mode 100644 index 00000000..0a10d519 --- /dev/null +++ b/systems/application/application-adapter-in/src/test/kotlin/hs/kr/entrydsm/application/adapterin/web/EvaluationControllerTest.kt @@ -0,0 +1,123 @@ +package hs.kr.entrydsm.application.adapterin.web + +import hs.kr.entrydsm.application.adapterin.web.dto.request.SaveSubjectGradesRequest +import hs.kr.entrydsm.application.adapterin.web.dto.request.SubjectGradesRequest +import hs.kr.entrydsm.application.application.port.`in`.EvaluationPort +import hs.kr.entrydsm.application.application.port.`in`.command.CalculateEvaluationCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveAcademicRecordCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveCertificatesCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveGedScoresCommand +import hs.kr.entrydsm.application.application.port.`in`.command.SaveSubjectGradesCommand +import hs.kr.entrydsm.application.application.port.`in`.result.AcademicRecordResult +import hs.kr.entrydsm.application.domain.enum.SchoolSemester +import hs.kr.entrydsm.application.domain.enum.SubjectGrade +import org.junit.Assert.assertEquals +import org.junit.Test + +class EvaluationControllerTest { + @Test + fun subjectGradesRequestConvertsEverySubjectGrade() { + val request = SubjectGradesRequest( + koreanGrade = SubjectGrade.A, + societyGrade = SubjectGrade.B, + englishGrade = SubjectGrade.C, + historyGrade = SubjectGrade.D, + mathGrade = SubjectGrade.E, + scienceGrade = SubjectGrade.X, + technologyGrade = SubjectGrade.A, + ) + + val result = request.toDomain() + + assertEquals(SubjectGrade.A, result.koreanGrade) + assertEquals(SubjectGrade.B, result.societyGrade) + assertEquals(SubjectGrade.C, result.englishGrade) + assertEquals(SubjectGrade.D, result.historyGrade) + assertEquals(SubjectGrade.E, result.mathGrade) + assertEquals(SubjectGrade.X, result.scienceGrade) + assertEquals(SubjectGrade.A, result.technologyGrade) + } + + @Test + fun saveExpectedGradesConvertsSchoolSemester() { + val evaluationPort = FakeEvaluationPort() + val controller = EvaluationController(evaluationPort) + + controller.saveExpectedGrades( + userId = 10L, + request = SaveSubjectGradesRequest( + schoolSemester = "3-1", + subjects = subjectGradesRequest(), + ), + ) + + assertEquals( + SchoolSemester.THIRD_GRADE_FIRST_SEMESTER, + evaluationPort.saveSubjectGradesCommand?.schoolSemester, + ) + assertEquals(10L, evaluationPort.saveSubjectGradesCommand?.userId) + } + + @Test(expected = IllegalArgumentException::class) + fun saveExpectedGradesRejectsInvalidSchoolSemester() { + EvaluationController(FakeEvaluationPort()).saveExpectedGrades( + userId = 10L, + request = SaveSubjectGradesRequest( + schoolSemester = "1-1", + subjects = subjectGradesRequest(), + ), + ) + } + + @Test + fun getResultCalculatesAndDoesNotExposeScores() { + val evaluationPort = FakeEvaluationPort() + val controller = EvaluationController(evaluationPort) + + val response = controller.getResult( + userId = 10L, + ) + + assertEquals(null, response.data) + assertEquals(10L, evaluationPort.calculateEvaluationCommand?.userId) + } + + private class FakeEvaluationPort : EvaluationPort { + var saveSubjectGradesCommand: SaveSubjectGradesCommand? = null + var calculateEvaluationCommand: CalculateEvaluationCommand? = null + + override fun saveSubjectGrades(command: SaveSubjectGradesCommand) { + saveSubjectGradesCommand = command + } + + override fun saveGedScores(command: SaveGedScoresCommand) = Unit + + override fun saveAcademicRecord(command: SaveAcademicRecordCommand): AcademicRecordResult = + AcademicRecordResult( + absentCount = 0, + earlyLeaveCount = 0, + lateCount = 0, + classAbsenceCount = 0, + volunteerTime = 0, + ) + + override fun saveCertificates(command: SaveCertificatesCommand) = Unit + + override fun calculateResult(command: CalculateEvaluationCommand) { + calculateEvaluationCommand = command + } + } + + private companion object { + fun subjectGradesRequest(): SubjectGradesRequest = + SubjectGradesRequest( + koreanGrade = SubjectGrade.A, + societyGrade = SubjectGrade.A, + englishGrade = SubjectGrade.A, + historyGrade = SubjectGrade.A, + mathGrade = SubjectGrade.A, + scienceGrade = SubjectGrade.A, + technologyGrade = SubjectGrade.A, + ) + } +} diff --git a/systems/application/application-adapter-out/BUILD.bazel b/systems/application/application-adapter-out/BUILD.bazel index 4671ac30..b1d65fa7 100644 --- a/systems/application/application-adapter-out/BUILD.bazel +++ b/systems/application/application-adapter-out/BUILD.bazel @@ -17,5 +17,5 @@ kt_jvm_test( javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", test_class = "hs.kr.entrydsm.application.adapterout.ApplicationAdapterOutModuleTest", - deps = MODULE_DEPS + TEST_DEPS, + deps = [":main"] + MODULE_DEPS + TEST_DEPS, ) diff --git a/systems/application/application-adapter-out/deps.bzl b/systems/application/application-adapter-out/deps.bzl index 5cf03643..a109bb8b 100644 --- a/systems/application/application-adapter-out/deps.bzl +++ b/systems/application/application-adapter-out/deps.bzl @@ -1,4 +1,9 @@ -KOTLIN_DEPS = [] +KOTLIN_DEPS = [ + "@maven//:org_springframework_boot_spring_boot_starter_data_jpa", + "@maven//:com_mysql_mysql_connector_j", + "//systems/application/application-application:main", + "//systems/application/application-domain:main", +] TEST_DEPS = [ "@maven//:junit_junit", diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/AcademicRecordJpaEntity.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/AcademicRecordJpaEntity.kt new file mode 100644 index 00000000..9905574d --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/AcademicRecordJpaEntity.kt @@ -0,0 +1,96 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.model.AcademicRecord +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.Table + +@Entity +@Table(name = "academic_records") +open class AcademicRecordJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + var id: Long? = null, + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "applicant_id", nullable = false, unique = true) + open var applicant: ApplicantJpaEntity? = null, + + @Column(name = "absent_count", nullable = false) + var absentCount: Int = 0, + + @Column(name = "late_count", nullable = false) + var lateCount: Int = 0, + + @Column(name = "early_leave_count", nullable = false) + var earlyLeaveCount: Int = 0, + + @Column(name = "class_absence_count", nullable = false) + var classAbsenceCount: Int = 0, + + @Column(name = "volunteer_time", nullable = false) + var volunteerTime: Int = 0, + + @Column(name = "is_dsm_algorithm_awarded", nullable = false) + var isDsmAlgorithmAwarded: Boolean = false, + + @Column(name = "is_programming_certified", nullable = false) + var isProgrammingCertified: Boolean = false, + + @OneToMany(mappedBy = "academicRecord", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY) + open var subjectGrades: MutableList = mutableListOf(), + + @OneToOne(mappedBy = "academicRecord", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY) + open var gedScores: GedScoreJpaEntity? = null, +) { + fun updateFrom(domain: AcademicRecord) { + absentCount = domain.absentCount + lateCount = domain.lateCount + earlyLeaveCount = domain.earlyLeaveCount + classAbsenceCount = domain.classAbsenceCount + volunteerTime = domain.volunteerTime + isDsmAlgorithmAwarded = domain.isDsmAlgorithmAwarded + isProgrammingCertified = domain.isProgrammingCertified + val domainSemesters = domain.subjectGrades.keys + subjectGrades.removeIf { it.id.schoolSemester !in domainSemesters } + domain.subjectGrades.forEach { (semester, grades) -> + val entity = subjectGrades.firstOrNull { it.id.schoolSemester == semester } + ?: SubjectGradeJpaEntity( + id = SubjectGradeId(schoolSemester = semester), + academicRecord = this, + ).also(subjectGrades::add) + entity.academicRecord = this + entity.updateFrom(grades) + } + gedScores = domain.gedScores?.let { + (gedScores ?: GedScoreJpaEntity(academicRecord = this)).apply { + academicRecord = this@AcademicRecordJpaEntity + updateFrom(it) + } + } + } + + fun toDomain(): AcademicRecord = + AcademicRecord( + absentCount = absentCount, + lateCount = lateCount, + earlyLeaveCount = earlyLeaveCount, + classAbsenceCount = classAbsenceCount, + volunteerTime = volunteerTime, + isDsmAlgorithmAwarded = isDsmAlgorithmAwarded, + isProgrammingCertified = isProgrammingCertified, + subjectGrades = subjectGrades + .associate { requireNotNull(it.id.schoolSemester) to it.toDomain() } + .toMutableMap(), + gedScores = gedScores?.toDomain(), + ) +} diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/ApplicantJpaEntity.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/ApplicantJpaEntity.kt new file mode 100644 index 00000000..35e3a05f --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/ApplicantJpaEntity.kt @@ -0,0 +1,208 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.enum.AdmissionType +import hs.kr.entrydsm.application.domain.enum.Gender +import hs.kr.entrydsm.application.domain.enum.GraduationType +import hs.kr.entrydsm.application.domain.enum.GuardianRelation +import hs.kr.entrydsm.application.domain.enum.Region +import hs.kr.entrydsm.application.domain.enum.SpecialAdmissionType +import hs.kr.entrydsm.application.domain.model.AcademicRecord +import hs.kr.entrydsm.application.domain.model.Applicant +import hs.kr.entrydsm.application.domain.model.MiddleSchoolInfo +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth + +@Entity +@Table(name = "applicants") +open class ApplicantJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + var id: Long? = null, + + @Column(name = "account_id", nullable = false) + var accountId: Long = 0, + + @Column(name = "photo_file_id") + var photoFileId: Long? = null, + + @Column(name = "name", length = 20) + var name: String? = null, + + @Column(name = "phone_number", length = 16) + var phoneNumber: String? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "gender", length = 10) + var gender: Gender? = null, + + @Column(name = "birthdate") + var birthdate: LocalDate? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "special_admission_type", nullable = false, length = 32) + var specialAdmissionType: SpecialAdmissionType = SpecialAdmissionType.NONE, + + @Enumerated(EnumType.STRING) + @Column(name = "admission_type", length = 16) + var admissionType: AdmissionType? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "region", length = 16) + var region: Region? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "graduation_type", length = 16) + var graduationType: GraduationType? = null, + + @Column(name = "graduation_date") + var graduationDate: LocalDate? = null, + + @Column(name = "guardian_name", length = 20) + var guardianName: String? = null, + + @Column(name = "guardian_phone_number", length = 16) + var guardianPhoneNumber: String? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "guardian_gender", length = 10) + var guardianGender: Gender? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "guardian_relation", length = 10) + var guardianRelation: GuardianRelation? = null, + + @Column(name = "address_base", length = 255) + var addressBase: String? = null, + + @Column(name = "address_detail", length = 255) + var addressDetail: String? = null, + + @Column(name = "zip_code", length = 10) + var zipCode: String? = null, + + @Column(name = "introduction", columnDefinition = "TEXT") + var introduction: String? = null, + + @Column(name = "study_plan", columnDefinition = "TEXT") + var studyPlan: String? = null, + + @Column(name = "total_score") + var totalScore: Double? = null, + + @Column(name = "total_score_updated_at") + var totalScoreUpdatedAt: LocalDateTime? = null, + + @Column(name = "created_at", nullable = false) + var createdAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "updated_at", nullable = false) + var updatedAt: LocalDateTime = LocalDateTime.now(), + + @OneToOne(mappedBy = "applicant", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY) + open var middleSchoolInfo: MiddleSchoolInfoJpaEntity? = null, + + @OneToOne(mappedBy = "applicant", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY) + open var academicRecord: AcademicRecordJpaEntity? = null, + + @OneToMany(mappedBy = "applicant", cascade = [CascadeType.ALL], orphanRemoval = true) + open var passResults: MutableList = mutableListOf(), +) { + fun toDomain(): Applicant = + Applicant( + id = requireNotNull(id), + accountId = accountId, + photoFileId = photoFileId, + name = name, + phoneNumber = phoneNumber, + gender = gender, + birthdate = birthdate, + specialAdmissionType = specialAdmissionType, + admissionType = admissionType, + region = region, + graduationType = graduationType, + graduationDate = graduationDate?.let { YearMonth.from(it) }, + guardianName = guardianName, + guardianPhoneNumber = guardianPhoneNumber, + guardianGender = guardianGender, + guardianRelation = guardianRelation, + addressBase = addressBase, + addressDetail = addressDetail, + zipCode = zipCode, + introduction = introduction, + studyPlan = studyPlan, + middleSchoolInfo = middleSchoolInfo?.toDomain(), + academicRecord = academicRecord?.toDomain(), + totalScore = totalScore, + totalScoreUpdatedAt = totalScoreUpdatedAt, + createdAt = createdAt, + updatedAt = updatedAt, + ) + + fun updateFrom(domain: Applicant) { + accountId = domain.accountId + photoFileId = domain.photoFileId + name = domain.name + phoneNumber = domain.phoneNumber + gender = domain.gender + birthdate = domain.birthdate + specialAdmissionType = domain.specialAdmissionType + admissionType = domain.admissionType + region = domain.region + graduationType = domain.graduationType + graduationDate = domain.graduationDate?.atDay(1) + guardianName = domain.guardianName + guardianPhoneNumber = domain.guardianPhoneNumber + guardianGender = domain.guardianGender + guardianRelation = domain.guardianRelation + addressBase = domain.addressBase + addressDetail = domain.addressDetail + zipCode = domain.zipCode + introduction = domain.introduction + studyPlan = domain.studyPlan + totalScore = domain.totalScore + totalScoreUpdatedAt = domain.totalScoreUpdatedAt + updatedAt = domain.updatedAt + updateMiddleSchoolInfo(domain.middleSchoolInfo) + updateAcademicRecord(domain.academicRecord) + } + + private fun updateMiddleSchoolInfo(domain: MiddleSchoolInfo?) { + middleSchoolInfo = domain?.let { + (middleSchoolInfo ?: MiddleSchoolInfoJpaEntity(applicant = this)).apply { + updateFrom(it) + applicant = this@ApplicantJpaEntity + } + } + } + + private fun updateAcademicRecord(domain: AcademicRecord?) { + academicRecord = domain?.let { + (academicRecord ?: AcademicRecordJpaEntity(applicant = this)).apply { + updateFrom(it) + applicant = this@ApplicantJpaEntity + } + } + } + + companion object { + fun from(domain: Applicant): ApplicantJpaEntity = + ApplicantJpaEntity().apply { + updateFrom(domain) + createdAt = domain.createdAt + } + } +} diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/GedScoreJpaEntity.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/GedScoreJpaEntity.kt new file mode 100644 index 00000000..7dacb085 --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/GedScoreJpaEntity.kt @@ -0,0 +1,66 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.model.GedScores +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.MapsId +import jakarta.persistence.OneToOne +import jakarta.persistence.Table + +@Entity +@Table(name = "ged_scores") +open class GedScoreJpaEntity( + @Id + @Column(name = "academic_record_id") + var academicRecordId: Long? = null, + + @MapsId + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "academic_record_id") + open var academicRecord: AcademicRecordJpaEntity? = null, + + @Column(name = "korean_score", nullable = false) + var koreanScore: Int = 0, + + @Column(name = "math_score", nullable = false) + var mathScore: Int = 0, + + @Column(name = "english_score", nullable = false) + var englishScore: Int = 0, + + @Column(name = "science_score", nullable = false) + var scienceScore: Int = 0, + + @Column(name = "society_score", nullable = false) + var societyScore: Int = 0, + + @Column(name = "technology_score", nullable = false) + var technologyScore: Int = 0, + + @Column(name = "history_score", nullable = false) + var historyScore: Int = 0, +) { + fun updateFrom(domain: GedScores) { + koreanScore = domain.koreanScore + mathScore = domain.mathScore + englishScore = domain.englishScore + scienceScore = domain.scienceScore + societyScore = domain.societyScore + technologyScore = domain.technologyScore + historyScore = domain.historyScore + } + + fun toDomain(): GedScores = + GedScores( + koreanScore = koreanScore, + mathScore = mathScore, + englishScore = englishScore, + scienceScore = scienceScore, + societyScore = societyScore, + technologyScore = technologyScore, + historyScore = historyScore, + ) +} diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/MiddleSchoolInfoJpaEntity.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/MiddleSchoolInfoJpaEntity.kt new file mode 100644 index 00000000..857397e2 --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/MiddleSchoolInfoJpaEntity.kt @@ -0,0 +1,51 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.model.MiddleSchoolInfo +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.MapsId +import jakarta.persistence.OneToOne +import jakarta.persistence.Table + +@Entity +@Table(name = "middle_school_infos") +open class MiddleSchoolInfoJpaEntity( + @Id + @Column(name = "applicant_id") + var applicantId: Long? = null, + + @MapsId + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "applicant_id") + var applicant: ApplicantJpaEntity? = null, + + @Column(name = "school_name", nullable = false, length = 50) + var schoolName: String = "", + + @Column(name = "student_number", nullable = false, length = 8) + var studentNumber: String = "", + + @Column(name = "school_phone", nullable = false, length = 16) + var schoolPhone: String = "", + + @Column(name = "teacher_name", nullable = false, length = 20) + var teacherName: String = "", +) { + fun updateFrom(domain: MiddleSchoolInfo) { + schoolName = domain.schoolName + studentNumber = domain.studentNumber + schoolPhone = domain.schoolPhone + teacherName = domain.teacherName + } + + fun toDomain(): MiddleSchoolInfo = + MiddleSchoolInfo( + schoolName = schoolName, + studentNumber = studentNumber, + schoolPhone = schoolPhone, + teacherName = teacherName, + ) +} diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/PassResultId.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/PassResultId.kt new file mode 100644 index 00000000..6d4366d2 --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/PassResultId.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.enum.ResultType +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import java.io.Serializable + +@Embeddable +data class PassResultId( + @Column(name = "applicant_id") + var applicantId: Long? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "result_type", length = 16) + var resultType: ResultType? = null, +) : Serializable diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/PassResultJpaEntity.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/PassResultJpaEntity.kt new file mode 100644 index 00000000..5bbcee4e --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/PassResultJpaEntity.kt @@ -0,0 +1,36 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.enum.PassResultStatus +import jakarta.persistence.Column +import jakarta.persistence.EmbeddedId +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MapsId +import jakarta.persistence.Table +import java.time.LocalDateTime + +@Entity +@Table(name = "pass_results") +open class PassResultJpaEntity( + @EmbeddedId + var id: PassResultId = PassResultId(), + + @MapsId("applicantId") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "applicant_id") + var applicant: ApplicantJpaEntity? = null, + + @Column(name = "processed_by") + var processedBy: Long? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "result", nullable = false, length = 16) + var result: PassResultStatus = PassResultStatus.PENDING, + + @Column(name = "processed_at") + var processedAt: LocalDateTime? = null, +) diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/SubjectGradeId.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/SubjectGradeId.kt new file mode 100644 index 00000000..bf01faf2 --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/SubjectGradeId.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.enum.SchoolSemester +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import java.io.Serializable + +@Embeddable +data class SubjectGradeId( + @Column(name = "academic_record_id") + var academicRecordId: Long? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "school_semester", length = 20) + var schoolSemester: SchoolSemester? = null, +) : Serializable diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/SubjectGradeJpaEntity.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/SubjectGradeJpaEntity.kt new file mode 100644 index 00000000..a1af2e8a --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/entity/SubjectGradeJpaEntity.kt @@ -0,0 +1,75 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.enum.SubjectGrade +import hs.kr.entrydsm.application.domain.model.SubjectGrades +import jakarta.persistence.Column +import jakarta.persistence.EmbeddedId +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MapsId +import jakarta.persistence.Table + +@Entity +@Table(name = "subject_grades") +open class SubjectGradeJpaEntity( + @EmbeddedId + var id: SubjectGradeId = SubjectGradeId(), + + @MapsId("academicRecordId") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "academic_record_id") + open var academicRecord: AcademicRecordJpaEntity? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "korean_grade", nullable = false, length = 2) + var koreanGrade: SubjectGrade = SubjectGrade.X, + + @Enumerated(EnumType.STRING) + @Column(name = "math_grade", nullable = false, length = 2) + var mathGrade: SubjectGrade = SubjectGrade.X, + + @Enumerated(EnumType.STRING) + @Column(name = "english_grade", nullable = false, length = 2) + var englishGrade: SubjectGrade = SubjectGrade.X, + + @Enumerated(EnumType.STRING) + @Column(name = "science_grade", nullable = false, length = 2) + var scienceGrade: SubjectGrade = SubjectGrade.X, + + @Enumerated(EnumType.STRING) + @Column(name = "society_grade", nullable = false, length = 2) + var societyGrade: SubjectGrade = SubjectGrade.X, + + @Enumerated(EnumType.STRING) + @Column(name = "technology_grade", nullable = false, length = 2) + var technologyGrade: SubjectGrade = SubjectGrade.X, + + @Enumerated(EnumType.STRING) + @Column(name = "history_grade", nullable = false, length = 2) + var historyGrade: SubjectGrade = SubjectGrade.X, +) { + fun updateFrom(domain: SubjectGrades) { + koreanGrade = domain.koreanGrade + mathGrade = domain.mathGrade + englishGrade = domain.englishGrade + scienceGrade = domain.scienceGrade + societyGrade = domain.societyGrade + technologyGrade = domain.technologyGrade + historyGrade = domain.historyGrade + } + + fun toDomain(): SubjectGrades = + SubjectGrades( + koreanGrade = koreanGrade, + mathGrade = mathGrade, + englishGrade = englishGrade, + scienceGrade = scienceGrade, + societyGrade = societyGrade, + technologyGrade = technologyGrade, + historyGrade = historyGrade, + ) +} diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/repository/ApplicantJpaRepository.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/repository/ApplicantJpaRepository.kt new file mode 100644 index 00000000..0cdcb8a8 --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/repository/ApplicantJpaRepository.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.application.adapterout.repository + +import hs.kr.entrydsm.application.adapterout.entity.ApplicantJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface ApplicantJpaRepository : JpaRepository { + fun findByAccountId(accountId: Long): ApplicantJpaEntity? +} diff --git a/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/repository/ApplicantPersistenceAdapter.kt b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/repository/ApplicantPersistenceAdapter.kt new file mode 100644 index 00000000..531b9e50 --- /dev/null +++ b/systems/application/application-adapter-out/src/main/kotlin/hs/kr/entrydsm/application/adapterout/repository/ApplicantPersistenceAdapter.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.application.adapterout.repository + +import hs.kr.entrydsm.application.adapterout.entity.ApplicantJpaEntity +import hs.kr.entrydsm.application.application.exception.ApplicantNotFoundException +import hs.kr.entrydsm.application.application.port.out.ApplicantRepository +import hs.kr.entrydsm.application.domain.model.Applicant +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +@Transactional +class ApplicantPersistenceAdapter( + private val applicantJpaRepository: ApplicantJpaRepository, +) : ApplicantRepository { + override fun save(applicant: Applicant): Applicant { + val entity = if (applicant.id > 0) { + applicantJpaRepository.findById(applicant.id) + .orElseThrow { ApplicantNotFoundException(applicant.id) } + .apply { updateFrom(applicant) } + } else { + ApplicantJpaEntity.from(applicant) + } + + return applicantJpaRepository.saveAndFlush(entity).toDomain() + } + + @Transactional(readOnly = true) + override fun findById(id: Long): Applicant? = + applicantJpaRepository.findById(id).orElse(null)?.toDomain() + + @Transactional(readOnly = true) + override fun findByAccountId(accountId: Long): Applicant? = + applicantJpaRepository.findByAccountId(accountId)?.toDomain() +} diff --git a/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/TestMain.kt b/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/TestMain.kt index 4de698df..83efc6f3 100644 --- a/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/TestMain.kt +++ b/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/TestMain.kt @@ -1,11 +1,13 @@ package hs.kr.entrydsm.application.adapterout -import org.junit.Assert.assertTrue -import org.junit.Test +import hs.kr.entrydsm.application.adapterout.entity.ApplicantJpaEntityTest +import hs.kr.entrydsm.application.adapterout.entity.AcademicRecordJpaEntityTest +import org.junit.runner.RunWith +import org.junit.runners.Suite -class ApplicationAdapterOutModuleTest { - @Test - fun moduleLoads() { - assertTrue(true) - } -} +@RunWith(Suite::class) +@Suite.SuiteClasses( + AcademicRecordJpaEntityTest::class, + ApplicantJpaEntityTest::class, +) +class ApplicationAdapterOutModuleTest diff --git a/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/application/adapterout/entity/AcademicRecordJpaEntityTest.kt b/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/application/adapterout/entity/AcademicRecordJpaEntityTest.kt new file mode 100644 index 00000000..96944f61 --- /dev/null +++ b/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/application/adapterout/entity/AcademicRecordJpaEntityTest.kt @@ -0,0 +1,109 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.enum.SchoolSemester +import hs.kr.entrydsm.application.domain.enum.SubjectGrade +import hs.kr.entrydsm.application.domain.model.AcademicRecord +import hs.kr.entrydsm.application.domain.model.GedScores +import hs.kr.entrydsm.application.domain.model.SubjectGrades +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Test + +class AcademicRecordJpaEntityTest { + @Test + fun updateFromUpdatesSubjectGradeEntityInPlace() { + val entity = AcademicRecordJpaEntity() + entity.updateFrom( + AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.SECOND_GRADE_FIRST_SEMESTER to subjectGrades(SubjectGrade.B), + ), + ), + ) + val savedGrade = entity.subjectGrades.single() + + entity.updateFrom( + AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.SECOND_GRADE_FIRST_SEMESTER to subjectGrades(SubjectGrade.A), + ), + ), + ) + + assertSame(savedGrade, entity.subjectGrades.single()) + assertEquals(SubjectGrade.A, entity.subjectGrades.single().koreanGrade) + assertSame(entity, entity.subjectGrades.single().academicRecord) + } + + @Test + fun updateFromRemovesStaleSubjectGrades() { + val entity = AcademicRecordJpaEntity() + entity.updateFrom( + AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.SECOND_GRADE_FIRST_SEMESTER to subjectGrades(SubjectGrade.A), + SchoolSemester.SECOND_GRADE_SECOND_SEMESTER to subjectGrades(SubjectGrade.B), + ), + ), + ) + + entity.updateFrom( + AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.SECOND_GRADE_SECOND_SEMESTER to subjectGrades(SubjectGrade.C), + ), + ), + ) + + assertEquals( + listOf(SchoolSemester.SECOND_GRADE_SECOND_SEMESTER), + entity.subjectGrades.map { it.id.schoolSemester }, + ) + } + + @Test + fun updateFromUpdatesGedScoreEntityInPlace() { + val entity = AcademicRecordJpaEntity() + entity.updateFrom(AcademicRecord(gedScores = gedScores(80))) + val savedGedScores = entity.gedScores + + entity.updateFrom(AcademicRecord(gedScores = gedScores(90))) + + assertSame(savedGedScores, entity.gedScores) + assertEquals(90, entity.gedScores?.koreanScore) + assertSame(entity, entity.gedScores?.academicRecord) + } + + @Test + fun updateFromRemovesGedScoresWhenDomainDoesNotHaveGedScores() { + val entity = AcademicRecordJpaEntity() + entity.updateFrom(AcademicRecord(gedScores = gedScores(80))) + + entity.updateFrom(AcademicRecord()) + + assertNull(entity.gedScores) + } + + private fun subjectGrades(grade: SubjectGrade): SubjectGrades = + SubjectGrades( + koreanGrade = grade, + mathGrade = grade, + englishGrade = grade, + scienceGrade = grade, + societyGrade = grade, + technologyGrade = grade, + historyGrade = grade, + ) + + private fun gedScores(score: Int): GedScores = + GedScores( + koreanScore = score, + mathScore = score, + englishScore = score, + scienceScore = score, + societyScore = score, + technologyScore = score, + historyScore = score, + ) +} diff --git a/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/application/adapterout/entity/ApplicantJpaEntityTest.kt b/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/application/adapterout/entity/ApplicantJpaEntityTest.kt new file mode 100644 index 00000000..6f6be7b9 --- /dev/null +++ b/systems/application/application-adapter-out/src/test/kotlin/hs/kr/entrydsm/application/adapterout/entity/ApplicantJpaEntityTest.kt @@ -0,0 +1,59 @@ +package hs.kr.entrydsm.application.adapterout.entity + +import hs.kr.entrydsm.application.domain.model.Applicant +import java.time.LocalDateTime +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class ApplicantJpaEntityTest { + @Test + fun fromCreatesNewEntityWithoutCarryingPositiveDomainId() { + val entity = ApplicantJpaEntity.from( + Applicant( + id = 100L, + accountId = 1L, + ), + ) + + assertNull(entity.id) + assertEquals(1L, entity.accountId) + } + + @Test + fun updateFromDoesNotOverwriteCreatedAt() { + val originalCreatedAt = LocalDateTime.of(2026, 1, 1, 0, 0) + val domainCreatedAt = LocalDateTime.of(2026, 2, 1, 0, 0) + val entity = ApplicantJpaEntity( + id = 1L, + accountId = 1L, + createdAt = originalCreatedAt, + ) + + entity.updateFrom( + Applicant( + id = 1L, + accountId = 2L, + createdAt = domainCreatedAt, + ), + ) + + assertEquals(originalCreatedAt, entity.createdAt) + assertEquals(2L, entity.accountId) + } + + @Test + fun fromUsesDomainCreatedAtForNewEntity() { + val createdAt = LocalDateTime.of(2026, 3, 1, 0, 0) + + val entity = ApplicantJpaEntity.from( + Applicant( + id = 0L, + accountId = 1L, + createdAt = createdAt, + ), + ) + + assertEquals(createdAt, entity.createdAt) + } +} diff --git a/systems/application/application-application/BUILD.bazel b/systems/application/application-application/BUILD.bazel index caf1b688..9f5c38f5 100644 --- a/systems/application/application-application/BUILD.bazel +++ b/systems/application/application-application/BUILD.bazel @@ -17,5 +17,5 @@ kt_jvm_test( javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", test_class = "hs.kr.entrydsm.application.application.ApplicationApplicationModuleTest", - deps = MODULE_DEPS + TEST_DEPS, + deps = [":main"] + MODULE_DEPS + TEST_DEPS, ) diff --git a/systems/application/application-application/src/main/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandService.kt b/systems/application/application-application/src/main/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandService.kt index 4ebe3b8d..c482f33d 100644 --- a/systems/application/application-application/src/main/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandService.kt +++ b/systems/application/application-application/src/main/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandService.kt @@ -129,6 +129,10 @@ class ApplicationCommandService( applicant.region = region applicant.graduationType = graduationType applicant.graduationDate = graduationDate + if (graduationType == GraduationType.GED) { + applicant.middleSchoolInfo = null + applicant.academicRecord?.subjectGrades?.clear() + } saveTouched(applicant) } diff --git a/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt b/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt index f2a878bd..f6c73050 100644 --- a/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt +++ b/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt @@ -1,11 +1,13 @@ package hs.kr.entrydsm.application.application -import org.junit.Assert.assertTrue -import org.junit.Test +import hs.kr.entrydsm.application.application.service.ApplicationCommandServiceTest +import hs.kr.entrydsm.application.application.service.EvaluationCommandServiceTest +import org.junit.runner.RunWith +import org.junit.runners.Suite -class ApplicationApplicationModuleTest { - @Test - fun moduleLoads() { - assertTrue(true) - } -} +@RunWith(Suite::class) +@Suite.SuiteClasses( + ApplicationCommandServiceTest::class, + EvaluationCommandServiceTest::class, +) +class ApplicationApplicationModuleTest diff --git a/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandServiceTest.kt b/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandServiceTest.kt new file mode 100644 index 00000000..9a703818 --- /dev/null +++ b/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/application/application/service/ApplicationCommandServiceTest.kt @@ -0,0 +1,84 @@ +package hs.kr.entrydsm.application.application.service + +import hs.kr.entrydsm.application.application.port.out.ApplicantRepository +import hs.kr.entrydsm.application.domain.enum.AdmissionType +import hs.kr.entrydsm.application.domain.enum.GraduationType +import hs.kr.entrydsm.application.domain.enum.Region +import hs.kr.entrydsm.application.domain.enum.SchoolSemester +import hs.kr.entrydsm.application.domain.enum.SubjectGrade +import hs.kr.entrydsm.application.domain.model.AcademicRecord +import hs.kr.entrydsm.application.domain.model.Applicant +import hs.kr.entrydsm.application.domain.model.MiddleSchoolInfo +import hs.kr.entrydsm.application.domain.model.SubjectGrades +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class ApplicationCommandServiceTest { + @Test + fun updateTypeClearsMiddleSchoolInfoAndSubjectGradesWhenChangedToGed() { + val repository = FakeApplicantRepository( + Applicant( + id = 1L, + accountId = 10L, + graduationType = GraduationType.PROSPECTIVE, + middleSchoolInfo = MiddleSchoolInfo( + schoolName = "대덕중학교", + studentNumber = "30101", + schoolPhone = "042-000-0000", + teacherName = "담임", + ), + academicRecord = AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.THIRD_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + ), + ), + ), + ) + val service = ApplicationCommandService(repository) + + service.updateType( + applicantId = 1L, + userId = 10L, + admissionType = AdmissionType.REGULAR, + region = Region.DAEJEON, + graduationType = GraduationType.GED, + graduationDate = null, + ) + + val savedApplicant = requireNotNull(repository.savedApplicant) + assertNull(savedApplicant.middleSchoolInfo) + assertTrue(savedApplicant.academicRecord?.subjectGrades?.isEmpty() == true) + } + + private class FakeApplicantRepository( + private var applicant: Applicant, + ) : ApplicantRepository { + var savedApplicant: Applicant? = null + + override fun save(applicant: Applicant): Applicant { + savedApplicant = applicant + this.applicant = applicant + return applicant + } + + override fun findById(id: Long): Applicant? = + applicant.takeIf { it.id == id } + + override fun findByAccountId(accountId: Long): Applicant? = + applicant.takeIf { it.accountId == accountId } + } + + private companion object { + fun all(grade: SubjectGrade): SubjectGrades = + SubjectGrades( + koreanGrade = grade, + mathGrade = grade, + englishGrade = grade, + scienceGrade = grade, + societyGrade = grade, + technologyGrade = grade, + historyGrade = grade, + ) + } +} diff --git a/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/application/application/service/EvaluationCommandServiceTest.kt b/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/application/application/service/EvaluationCommandServiceTest.kt new file mode 100644 index 00000000..7c31614b --- /dev/null +++ b/systems/application/application-application/src/test/kotlin/hs/kr/entrydsm/application/application/service/EvaluationCommandServiceTest.kt @@ -0,0 +1,75 @@ +package hs.kr.entrydsm.application.application.service + +import hs.kr.entrydsm.application.application.port.out.ApplicantRepository +import hs.kr.entrydsm.application.domain.enum.AdmissionType +import hs.kr.entrydsm.application.domain.enum.GraduationType +import hs.kr.entrydsm.application.domain.enum.SchoolSemester +import hs.kr.entrydsm.application.domain.enum.SubjectGrade +import hs.kr.entrydsm.application.domain.model.AcademicRecord +import hs.kr.entrydsm.application.domain.model.Applicant +import hs.kr.entrydsm.application.domain.model.SubjectGrades +import hs.kr.entrydsm.application.domain.service.ScoreCalculator +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +class EvaluationCommandServiceTest { + @Test + fun calculateResultSavesScoreForApplicantsAdmissionType() { + val repository = FakeApplicantRepository( + Applicant( + id = 1L, + accountId = 10L, + admissionType = AdmissionType.REGULAR, + graduationType = GraduationType.PROSPECTIVE, + academicRecord = AcademicRecord( + volunteerTime = 15, + isDsmAlgorithmAwarded = true, + subjectGrades = linkedMapOf( + SchoolSemester.THIRD_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + SchoolSemester.SECOND_GRADE_SECOND_SEMESTER to all(SubjectGrade.A), + SchoolSemester.SECOND_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + ), + ), + ), + ) + val service = EvaluationCommandService(repository, ScoreCalculator()) + + service.calculateResult(userId = 10L) + + val savedApplicant = requireNotNull(repository.savedApplicant) + assertEquals(173.0, savedApplicant.totalScore ?: 0.0, 0.0) + assertNotNull(savedApplicant.totalScoreUpdatedAt) + } + + private class FakeApplicantRepository( + private var applicant: Applicant, + ) : ApplicantRepository { + var savedApplicant: Applicant? = null + + override fun save(applicant: Applicant): Applicant { + savedApplicant = applicant + this.applicant = applicant + return applicant + } + + override fun findById(id: Long): Applicant? = + applicant.takeIf { it.id == id } + + override fun findByAccountId(accountId: Long): Applicant? = + applicant.takeIf { it.accountId == accountId } + } + + private companion object { + fun all(grade: SubjectGrade): SubjectGrades = + SubjectGrades( + koreanGrade = grade, + mathGrade = grade, + englishGrade = grade, + scienceGrade = grade, + societyGrade = grade, + technologyGrade = grade, + historyGrade = grade, + ) + } +} diff --git a/systems/application/application-bootstrap/BUILD.bazel b/systems/application/application-bootstrap/BUILD.bazel index 42ef8691..bde71cd7 100644 --- a/systems/application/application-bootstrap/BUILD.bazel +++ b/systems/application/application-bootstrap/BUILD.bazel @@ -8,7 +8,7 @@ kt_jvm_binary( srcs = glob(["src/main/kotlin/**/*.kt"]), javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", - main_class = "hs.kr.entrydsm.application.ApplicationBootstrapApplicationKt", + main_class = "hs.kr.entrydsm.application.ExampleApplicationKt", plugins = ["//:spring_allopen"], resources = glob(["src/main/resources/**"]), deps = MODULE_DEPS + [ diff --git a/systems/application/application-bootstrap/deps.bzl b/systems/application/application-bootstrap/deps.bzl index bfdb5aef..4d5535f1 100644 --- a/systems/application/application-bootstrap/deps.bzl +++ b/systems/application/application-bootstrap/deps.bzl @@ -1,11 +1,13 @@ SPRING_DEPS = [ "@maven//:org_springframework_boot_spring_boot_starter_web", "@maven//:org_springframework_boot_spring_boot_starter_actuator", + "@maven//:org_springframework_boot_spring_boot_starter_data_jpa", ] KOTLIN_DEPS = [ "@maven//:org_jetbrains_kotlin_kotlin_reflect", "@maven//:com_fasterxml_jackson_module_jackson_module_kotlin", + "@maven//:com_mysql_mysql_connector_j", ] TEST_DEPS = [ diff --git a/systems/application/application-bootstrap/src/main/kotlin/hs/kr/entrydsm/application/config/ApplicationUseCaseConfig.kt b/systems/application/application-bootstrap/src/main/kotlin/hs/kr/entrydsm/application/config/ApplicationUseCaseConfig.kt new file mode 100644 index 00000000..fbe8f586 --- /dev/null +++ b/systems/application/application-bootstrap/src/main/kotlin/hs/kr/entrydsm/application/config/ApplicationUseCaseConfig.kt @@ -0,0 +1,27 @@ +package hs.kr.entrydsm.application.config + +import hs.kr.entrydsm.application.application.port.`in`.ApplicationPort +import hs.kr.entrydsm.application.application.port.`in`.EvaluationPort +import hs.kr.entrydsm.application.application.port.out.ApplicantRepository +import hs.kr.entrydsm.application.application.service.ApplicationCommandService +import hs.kr.entrydsm.application.application.service.EvaluationCommandService +import hs.kr.entrydsm.application.domain.service.ScoreCalculator +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +class ApplicationUseCaseConfig { + @Bean + fun scoreCalculator(): ScoreCalculator = ScoreCalculator() + + @Bean + fun applicationService( + applicantRepository: ApplicantRepository, + ): ApplicationPort = ApplicationCommandService(applicantRepository) + + @Bean + fun evaluationService( + applicantRepository: ApplicantRepository, + scoreCalculator: ScoreCalculator, + ): EvaluationPort = EvaluationCommandService(applicantRepository, scoreCalculator) +} diff --git a/systems/application/application-bootstrap/src/main/resources/application-dev.yaml b/systems/application/application-bootstrap/src/main/resources/application-dev.yaml new file mode 100644 index 00000000..f6212f58 --- /dev/null +++ b/systems/application/application-bootstrap/src/main/resources/application-dev.yaml @@ -0,0 +1,24 @@ +spring: + config: + activate: + on-profile: dev + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + open-in-view: false + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + +entrydsm: + application: + schedule: + application-start-at: ${APPLICATION_START_AT} + application-end-at: ${APPLICATION_END_AT} + result-announced-at: ${RESULT_ANNOUNCED_AT} diff --git a/systems/application/application-bootstrap/src/main/resources/application-prod.yaml b/systems/application/application-bootstrap/src/main/resources/application-prod.yaml new file mode 100644 index 00000000..8dcb612b --- /dev/null +++ b/systems/application/application-bootstrap/src/main/resources/application-prod.yaml @@ -0,0 +1,24 @@ +spring: + config: + activate: + on-profile: prod + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate + open-in-view: false + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + +entrydsm: + application: + schedule: + application-start-at: ${APPLICATION_START_AT} + application-end-at: ${APPLICATION_END_AT} + result-announced-at: ${RESULT_ANNOUNCED_AT} diff --git a/systems/application/application-bootstrap/src/main/resources/application.yaml b/systems/application/application-bootstrap/src/main/resources/application.yaml index 03aced9e..3d701e28 100644 --- a/systems/application/application-bootstrap/src/main/resources/application.yaml +++ b/systems/application/application-bootstrap/src/main/resources/application.yaml @@ -2,11 +2,16 @@ spring: application: name: application profiles: - default: local + default: dev main: lazy-initialization: true + server: shutdown: graceful + +grpc: + port: ${GRPC_PORT:9090} + management: endpoints: web: @@ -16,6 +21,7 @@ management: health: probes: enabled: true + logging: level: root: INFO diff --git a/systems/application/application-domain/src/main/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculator.kt b/systems/application/application-domain/src/main/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculator.kt index ad7c68e0..a23404bb 100644 --- a/systems/application/application-domain/src/main/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculator.kt +++ b/systems/application/application-domain/src/main/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculator.kt @@ -67,13 +67,12 @@ class ScoreCalculator { GraduationType.GRADUATED -> GRADUATED_SEMESTER_WEIGHTS else -> PROSPECTIVE_GRADUATION_SEMESTER_WEIGHTS } - val weightedScores = semesterWeights.mapNotNull { (semester, weight) -> - record.subjectGrades[semester] - ?.let(::calculateSemesterAveragePoint) - ?.let { averagePoint -> (averagePoint / MAX_GRADE_POINT) * weight to weight } - } - if (weightedScores.isEmpty()) { - return EMPTY_SCORE + val weightedScores = semesterWeights.map { (semester, weight) -> + val subjectGrades = requireNotNull(record.subjectGrades[semester]) { + "subject grades are incomplete" + } + val averagePoint = calculateSemesterAveragePoint(subjectGrades) + (averagePoint / MAX_GRADE_POINT) * weight to weight } val earnedScore = weightedScores.sumOf { it.first } @@ -118,15 +117,16 @@ class ScoreCalculator { return EMPTY_SCORE } - val convertedAbsences = record.absentCount + floor( + val convertedAbsences = record.absentCount.toLong() + floor( ( - record.lateCount + - record.earlyLeaveCount + - record.classAbsenceCount + record.lateCount.toLong() + + record.earlyLeaveCount.toLong() + + record.classAbsenceCount.toLong() ) / ATTENDANCE_CONVERSION_UNIT.toDouble(), - ).toInt() + ).toLong() - return (ATTENDANCE_MAX_SCORE - convertedAbsences).coerceAtLeast(EMPTY_SCORE) + return (ATTENDANCE_MAX_SCORE - convertedAbsences) + .coerceIn(EMPTY_SCORE, ATTENDANCE_MAX_SCORE) } private fun calculateVolunteerScore( diff --git a/systems/application/application-domain/src/test/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculatorTest.kt b/systems/application/application-domain/src/test/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculatorTest.kt index 3c510303..43c6ac44 100644 --- a/systems/application/application-domain/src/test/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculatorTest.kt +++ b/systems/application/application-domain/src/test/kotlin/hs/kr/entrydsm/application/domain/service/ScoreCalculatorTest.kt @@ -70,6 +70,66 @@ class ScoreCalculatorTest { assertEquals(80.0, result.getValue(AdmissionType.MEISTER), 0.0) } + @Test(expected = IllegalArgumentException::class) + fun rejectsIncompleteProspectiveSubjectGrades() { + calculator.calculate( + Applicant( + id = 1L, + accountId = 1L, + graduationType = GraduationType.PROSPECTIVE, + academicRecord = AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.THIRD_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + ), + ), + ), + ) + } + + @Test(expected = IllegalArgumentException::class) + fun rejectsIncompleteGraduatedSubjectGrades() { + calculator.calculate( + Applicant( + id = 1L, + accountId = 1L, + graduationType = GraduationType.GRADUATED, + academicRecord = AcademicRecord( + subjectGrades = linkedMapOf( + SchoolSemester.THIRD_GRADE_SECOND_SEMESTER to all(SubjectGrade.A), + SchoolSemester.THIRD_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + SchoolSemester.SECOND_GRADE_SECOND_SEMESTER to all(SubjectGrade.A), + ), + ), + ), + ) + } + + @Test + fun clampsAttendanceScoreWhenAttendanceCountsAreTooLarge() { + val applicant = Applicant( + id = 1L, + accountId = 1L, + graduationType = GraduationType.PROSPECTIVE, + academicRecord = AcademicRecord( + absentCount = Int.MAX_VALUE, + lateCount = Int.MAX_VALUE, + earlyLeaveCount = Int.MAX_VALUE, + classAbsenceCount = Int.MAX_VALUE, + subjectGrades = linkedMapOf( + SchoolSemester.THIRD_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + SchoolSemester.SECOND_GRADE_SECOND_SEMESTER to all(SubjectGrade.A), + SchoolSemester.SECOND_GRADE_FIRST_SEMESTER to all(SubjectGrade.A), + ), + ), + ) + + val result = calculator.calculate(applicant) + + assertEquals(140.0, result.getValue(AdmissionType.REGULAR), 0.0) + assertEquals(80.0, result.getValue(AdmissionType.SOCIAL), 0.0) + assertEquals(80.0, result.getValue(AdmissionType.MEISTER), 0.0) + } + @Test fun returnsZeroWhenAcademicRecordDoesNotExist() { val result = calculator.calculate(Applicant(id = 1L, accountId = 1L))