diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/application/interfaces/ApplicationCaseQueryApplicationContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/application/interfaces/ApplicationCaseQueryApplicationContract.kt new file mode 100644 index 00000000..ae0c1505 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/application/interfaces/ApplicationCaseQueryApplicationContract.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.domain.application.interfaces + +import hs.kr.entrydsm.domain.application.aggregates.Application +import java.util.UUID + +interface ApplicationCaseQueryApplicationContract { + fun queryApplicationByUserId(userId: UUID): Application? +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/application/interfaces/ApplicationQueryScheduleContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/application/interfaces/ApplicationQueryScheduleContract.kt new file mode 100644 index 00000000..11706593 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/application/interfaces/ApplicationQueryScheduleContract.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.domain.application.interfaces + +import hs.kr.entrydsm.domain.schedule.aggregates.Schedule +import hs.kr.entrydsm.domain.schedule.values.ScheduleType + +interface ApplicationQueryScheduleContract { + suspend fun queryByScheduleType(scheduleType: ScheduleType): Schedule? +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/aggregates/Schedule.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/aggregates/Schedule.kt new file mode 100644 index 00000000..5aa136a7 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/aggregates/Schedule.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.domain.schedule.aggregates + +import hs.kr.entrydsm.domain.schedule.values.ScheduleType +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import java.time.LocalDateTime + +@Aggregate(context = "schedule") +data class Schedule( + val scheduleType: ScheduleType, + val date: LocalDateTime +) diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/exception/ScheduleExceptions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/exception/ScheduleExceptions.kt new file mode 100644 index 00000000..8faa659a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/exception/ScheduleExceptions.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.domain.schedule.exception + +import hs.kr.entrydsm.global.exception.BusinessException + +sealed class ScheduleExceptions( + override val status: Int, + override val message: String, +) : BusinessException(status, message) { + + class ScoreNotFoundException(message: String = SCORE_NOT_FOUND_EXCEPTION): + ScheduleExceptions(404, message) + + class AdmissionUnavailableException(message: String = ADMISSION_UNAVAILABLE): + ScheduleExceptions(404, message) + + companion object { + private const val SCORE_NOT_FOUND_EXCEPTION = "점수가 존재하지 않습니다" + private const val ADMISSION_UNAVAILABLE = "합격여부를 확인할 수 없습니다" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/values/ScheduleType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/values/ScheduleType.kt new file mode 100644 index 00000000..96340a19 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/schedule/values/ScheduleType.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.domain.schedule.values + +enum class ScheduleType { + START_DATE, + FIRST_ANNOUNCEMENT, + INTERVIEW, + SECOND_ANNOUNCEMENT, + END_DATE +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/status/exception/StatusExceptions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/status/exception/StatusExceptions.kt new file mode 100644 index 00000000..df93094c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/status/exception/StatusExceptions.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.domain.status.exception + +import hs.kr.entrydsm.global.exception.BusinessException + +sealed class StatusExceptions( + override val status: Int, + override val message: String, +) : BusinessException(status, message) { + class StatusNotFoundException(message: String = STATUS_NOT_FOUND) : + StatusExceptions(404, message) + + class AlreadySubmittedException(message: String = ALREADY_SUBMITTED) : + StatusExceptions(409, message) + + companion object { + private const val STATUS_NOT_FOUND = "상태가 존재하지 않습니다" + private const val ALREADY_SUBMITTED = "이미 최종제출이 되어있습니다." + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/QueryIsFirstRoundPassUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/QueryIsFirstRoundPassUseCase.kt new file mode 100644 index 00000000..5dbcd51d --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/QueryIsFirstRoundPassUseCase.kt @@ -0,0 +1,39 @@ +package hs.kr.entrydsm.application.domain.application.usecase + +import hs.kr.entrydsm.application.domain.application.exception.ApplicationNotFoundException +import hs.kr.entrydsm.application.domain.pass.presentation.dto.response.QueryIsFirstRoundPassResponse +import hs.kr.entrydsm.application.global.annotation.usecase.ReadOnlyUseCase +import hs.kr.entrydsm.application.global.security.SecurityAdapter +import hs.kr.entrydsm.domain.application.interfaces.ApplicationContract +import hs.kr.entrydsm.domain.application.interfaces.ApplicationQueryScheduleContract +import hs.kr.entrydsm.domain.schedule.exception.ScheduleExceptions +import hs.kr.entrydsm.domain.schedule.values.ScheduleType +import hs.kr.entrydsm.domain.status.exception.StatusExceptions +import hs.kr.entrydsm.domain.status.interfaces.ApplicationQueryStatusContract +import java.time.LocalDateTime + +@ReadOnlyUseCase +class QueryIsFirstRoundPassUseCase( + private val securityAdapter: SecurityAdapter, + private val queryApplicationContract: ApplicationContract, + private val applicationQueryScheduleContract: ApplicationQueryScheduleContract, + private val applicationQueryStatusContract: ApplicationQueryStatusContract +) { + suspend fun execute(): QueryIsFirstRoundPassResponse { + + val userId = securityAdapter.getCurrentUserId() + val application = queryApplicationContract.getApplicationByUserId(userId) + ?: throw ApplicationNotFoundException() + + val firstAnnounce = applicationQueryScheduleContract.queryByScheduleType(ScheduleType.FIRST_ANNOUNCEMENT) + ?: throw ScheduleExceptions.ScoreNotFoundException() + + if (LocalDateTime.now().isBefore(firstAnnounce.date)) + throw ScheduleExceptions.AdmissionUnavailableException() + + val status = applicationQueryStatusContract.queryStatusByReceiptCode(application.receiptCode) + ?: throw StatusExceptions.StatusNotFoundException() + + return QueryIsFirstRoundPassResponse(status.isFirstRoundPass) + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/QueryIsSecondRoundPassUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/QueryIsSecondRoundPassUseCase.kt new file mode 100644 index 00000000..fa392060 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/QueryIsSecondRoundPassUseCase.kt @@ -0,0 +1,38 @@ +package hs.kr.entrydsm.application.domain.application.usecase + +import hs.kr.entrydsm.application.domain.application.exception.ApplicationNotFoundException +import hs.kr.entrydsm.application.domain.pass.presentation.dto.response.QueryIsSecondRoundPassResponse +import hs.kr.entrydsm.application.global.annotation.usecase.ReadOnlyUseCase +import hs.kr.entrydsm.application.global.security.SecurityAdapter +import hs.kr.entrydsm.domain.application.interfaces.ApplicationContract +import hs.kr.entrydsm.domain.application.interfaces.ApplicationQueryScheduleContract +import hs.kr.entrydsm.domain.schedule.exception.ScheduleExceptions +import hs.kr.entrydsm.domain.schedule.values.ScheduleType +import hs.kr.entrydsm.domain.status.exception.StatusExceptions +import hs.kr.entrydsm.domain.status.interfaces.ApplicationQueryStatusContract +import java.time.LocalDateTime + +@ReadOnlyUseCase +class QueryIsSecondRoundPassUseCase ( + private val securityAdapter: SecurityAdapter, + private val queryApplicationContract: ApplicationContract, + private val applicationQueryScheduleContract: ApplicationQueryScheduleContract, + private val applicationQueryStatusContract: ApplicationQueryStatusContract +) { + suspend fun execute(): QueryIsSecondRoundPassResponse { + val userId = securityAdapter.getCurrentUserId() + val application = queryApplicationContract.getApplicationByUserId(userId) + ?: throw ApplicationNotFoundException() + + val secondAnnounce = applicationQueryScheduleContract.queryByScheduleType(ScheduleType.SECOND_ANNOUNCEMENT) + ?: throw ScheduleExceptions.ScoreNotFoundException() + + if (LocalDateTime.now().isBefore(secondAnnounce.date)) + throw ScheduleExceptions.AdmissionUnavailableException() + + val status = applicationQueryStatusContract.queryStatusByReceiptCode(application.receiptCode) + ?: throw StatusExceptions.StatusNotFoundException() + + return QueryIsSecondRoundPassResponse(status.isSecondRoundPass) + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/WebPassAdapter.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/WebPassAdapter.kt new file mode 100644 index 00000000..a7e30c9b --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/WebPassAdapter.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.application.domain.pass.presentation + +import hs.kr.entrydsm.application.domain.application.usecase.QueryIsFirstRoundPassUseCase +import hs.kr.entrydsm.application.domain.application.usecase.QueryIsSecondRoundPassUseCase +import hs.kr.entrydsm.application.domain.pass.presentation.dto.response.QueryIsFirstRoundPassResponse +import hs.kr.entrydsm.application.domain.pass.presentation.dto.response.QueryIsSecondRoundPassResponse +import hs.kr.entrydsm.application.global.document.pass.PassApiDocument +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/pass") +class WebPassAdapter( + private val queryIsFirstRoundPassUseCase: QueryIsFirstRoundPassUseCase, + private val queryIsSecondRoundPassUseCase: QueryIsSecondRoundPassUseCase +) : PassApiDocument { + @GetMapping("/first-round") + override suspend fun queryIsFirstRound(): QueryIsFirstRoundPassResponse = + queryIsFirstRoundPassUseCase.execute() + + @GetMapping("/second-round") + override suspend fun queryIsSecondRound(): QueryIsSecondRoundPassResponse = + queryIsSecondRoundPassUseCase.execute() +} diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/dto/response/QueryIsFirstRoundPassResponse.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/dto/response/QueryIsFirstRoundPassResponse.kt new file mode 100644 index 00000000..9e8b27f8 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/dto/response/QueryIsFirstRoundPassResponse.kt @@ -0,0 +1,5 @@ +package hs.kr.entrydsm.application.domain.pass.presentation.dto.response + +data class QueryIsFirstRoundPassResponse( + val isFirstRoundPass: Boolean +) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/dto/response/QueryIsSecondRoundPassResponse.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/dto/response/QueryIsSecondRoundPassResponse.kt new file mode 100644 index 00000000..22071ac3 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/pass/presentation/dto/response/QueryIsSecondRoundPassResponse.kt @@ -0,0 +1,5 @@ +package hs.kr.entrydsm.application.domain.pass.presentation.dto.response + +data class QueryIsSecondRoundPassResponse( + val finalPass: Boolean +) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/schedule/domain/SchedulePersistenceAdapterApplication.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/schedule/domain/SchedulePersistenceAdapterApplication.kt new file mode 100644 index 00000000..676a887d --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/schedule/domain/SchedulePersistenceAdapterApplication.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.application.domain.schedule.domain + +import hs.kr.entrydsm.application.global.grpc.client.schedule.ScheduleGrpcClient +import hs.kr.entrydsm.domain.application.interfaces.ApplicationQueryScheduleContract +import hs.kr.entrydsm.domain.schedule.aggregates.Schedule +import hs.kr.entrydsm.domain.schedule.values.ScheduleType +import org.springframework.stereotype.Component + +@Component +class SchedulePersistenceAdapterApplication( + private val scheduleGrpcClient: ScheduleGrpcClient +) : ApplicationQueryScheduleContract { + override suspend fun queryByScheduleType(scheduleType: ScheduleType): Schedule? { + return scheduleGrpcClient.getScheduleByType(scheduleType.name).let { + Schedule( + scheduleType = it.type, + date = it.date + ) + } + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pass/PassApiDocument.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pass/PassApiDocument.kt new file mode 100644 index 00000000..9d220cef --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/document/pass/PassApiDocument.kt @@ -0,0 +1,47 @@ +package hs.kr.entrydsm.application.global.document.pass + +import hs.kr.entrydsm.application.domain.pass.presentation.dto.response.QueryIsFirstRoundPassResponse +import hs.kr.entrydsm.application.domain.pass.presentation.dto.response.QueryIsSecondRoundPassResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag + +@Tag(name = "Pass", description = "합격 여부 조회 API") +interface PassApiDocument { + @Operation( + summary = "1차 전형 합격 여부 조회", + description = "현재 로그인한 사용자의 1차 전형 합격 여부를 조회합니다.", + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "합격 여부 조회 성공", + content = [Content(schema = Schema(implementation = QueryIsFirstRoundPassResponse::class))] + ), + ApiResponse(responseCode = "403", description = "인증되지 않은 사용자"), + ApiResponse(responseCode = "404", description = "지원자 또는 전형 정보를 찾을 수 없음"), + ] + ) + suspend fun queryIsFirstRound(): QueryIsFirstRoundPassResponse + + @Operation( + summary = "2차 전형 최종 합격 여부 조회", + description = "현재 로그인한 사용자의 2차 전형 최종 합격 여부를 조회합니다.", + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "합격 여부 조회 성공", + content = [Content(schema = Schema(implementation = QueryIsSecondRoundPassResponse::class))] + ), + ApiResponse(responseCode = "403", description = "인증되지 않은 사용자"), + ApiResponse(responseCode = "404", description = "지원자 또는 전형 정보를 찾을 수 없음"), + ] + ) + suspend fun queryIsSecondRound(): QueryIsSecondRoundPassResponse +} diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt index 6c7afa9f..d2e90c63 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/client/schedule/ScheduleGrpcClient.kt @@ -2,9 +2,9 @@ package hs.kr.entrydsm.application.global.grpc.client.schedule import hs.kr.entrydsm.application.global.extension.executeGrpcCallWithResilience import hs.kr.entrydsm.application.global.grpc.dto.schedule.InternalScheduleResponse -import hs.kr.entrydsm.application.global.grpc.dto.schedule.ScheduleType import hs.kr.entrydsm.casper.schedule.proto.ScheduleServiceGrpc import hs.kr.entrydsm.casper.schedule.proto.ScheduleServiceProto +import hs.kr.entrydsm.domain.schedule.values.ScheduleType import io.github.resilience4j.circuitbreaker.CircuitBreaker import io.github.resilience4j.retry.Retry import io.grpc.Channel diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/dto/schedule/InternalScheduleResponse.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/dto/schedule/InternalScheduleResponse.kt index 13091e1d..ee3eae89 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/dto/schedule/InternalScheduleResponse.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/grpc/dto/schedule/InternalScheduleResponse.kt @@ -1,5 +1,6 @@ package hs.kr.entrydsm.application.global.grpc.dto.schedule +import hs.kr.entrydsm.domain.schedule.values.ScheduleType import java.time.LocalDateTime /**