Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import land.leets.domain.application.domain.Application
import land.leets.domain.application.presentation.dto.ApplicationDetailsResponse
import land.leets.domain.application.presentation.dto.ApplicationRequest
import land.leets.domain.application.presentation.dto.ApplicationResponse
import land.leets.domain.application.presentation.dto.ApplicationStatusResponse
import land.leets.domain.application.presentation.dto.StatusRequest
import land.leets.domain.application.usecase.*
import land.leets.domain.auth.AuthDetails
import land.leets.global.error.ErrorResponse
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
@RequestMapping("/application")
Expand All @@ -25,7 +25,8 @@ class ApplicationController(
private val updateApplication: UpdateApplication,
private val getApplication: GetAllApplication,
private val getApplicationDetails: GetApplicationDetails,
private val updateResult: UpdateResult
private val updateResult: UpdateResult,
private val getApplicationStatus: GetApplicationStatus,
) {

@Operation(summary = "(유저) 지원서 작성", description = "지원서를 작성합니다.")
Expand Down Expand Up @@ -116,4 +117,18 @@ class ApplicationController(
val uid = authDetails.uid
return getApplicationDetails.execute(uid)
}

@Operation(summary = "(유저) 지원서 상태 불러오기", description = "작성한 지원서 상태를 불러옵니다.")
@ApiResponses(
ApiResponse(responseCode = "200"),
ApiResponse(responseCode = "400", content = [Content(schema = Schema(implementation = ErrorResponse::class))]),
ApiResponse(responseCode = "403", content = [Content(schema = Schema(implementation = ErrorResponse::class))]),
ApiResponse(responseCode = "404", content = [Content(schema = Schema(implementation = ErrorResponse::class))]),
ApiResponse(responseCode = "500", content = [Content(schema = Schema(implementation = ErrorResponse::class))])
)
@GetMapping("/status")
fun getStatus(@AuthenticationPrincipal authDetails: AuthDetails): ApplicationStatusResponse {
val uid = authDetails.uid
return getApplicationStatus.execute(uid)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package land.leets.domain.application.presentation.dto

import land.leets.domain.application.domain.Application
import land.leets.domain.application.type.ApplicationStatus
import land.leets.domain.interview.domain.Interview
import land.leets.domain.interview.type.HasInterview
import java.time.LocalDateTime

data class ApplicationStatusResponse(
val id: Long,
val status: ApplicationStatus,
val hasInterview: HasInterview?,
val interviewDate: LocalDateTime?,
val interviewPlace: String?,
) {
companion object {
fun of(
application: Application,
interview: Interview?,
): ApplicationStatusResponse {
if (application.applicationStatus != ApplicationStatus.PASS_PAPER) {
return ApplicationStatusResponse(
id = application.id!!,
Comment on lines +21 to +23
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The conditional logic for returning interview information based on application status would benefit from a comment explaining the business rule. This would help future maintainers understand why interview details are only included for PASS_PAPER status.

Consider adding a brief comment like: "// Interview details are only shown to applicants who passed the paper screening"

Suggested change
if (application.applicationStatus != ApplicationStatus.PASS_PAPER) {
return ApplicationStatusResponse(
id = application.id!!,
// Interview details are only shown to applicants who passed the paper screening
if (application.applicationStatus != ApplicationStatus.PASS_PAPER) {
return ApplicationStatusResponse(

Copilot uses AI. Check for mistakes.
status = application.applicationStatus,
hasInterview = null,
interviewDate = null,
interviewPlace = null,
)
}
return ApplicationStatusResponse(
id = application.id!!,
status = application.applicationStatus,
hasInterview = interview!!.hasInterview,
interviewDate = interview.fixedInterviewDate,
interviewPlace = interview.place,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package land.leets.domain.application.usecase

import land.leets.domain.application.presentation.dto.ApplicationStatusResponse
import java.util.UUID

interface GetApplicationStatus {
fun execute(uid: UUID): ApplicationStatusResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package land.leets.domain.application.usecase

import land.leets.domain.application.domain.repository.ApplicationRepository
import land.leets.domain.application.exception.ApplicationNotFoundException
import land.leets.domain.application.presentation.dto.ApplicationStatusResponse
import land.leets.domain.interview.domain.repository.InterviewRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.UUID

@Service
@Transactional(readOnly = true)
class GetApplicationStatusImpl(
private val applicationRepository: ApplicationRepository,
private val interviewRepository: InterviewRepository,
) : GetApplicationStatus {
override fun execute(uid: UUID): ApplicationStatusResponse {
val application = applicationRepository.findByUser_Id(uid)
?: throw ApplicationNotFoundException()
val interview = interviewRepository.findByApplication(application)

return ApplicationStatusResponse.of(application, interview)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package land.leets.domain.application.usecase

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import land.leets.domain.application.domain.Application
import land.leets.domain.application.domain.repository.ApplicationRepository
import land.leets.domain.application.exception.ApplicationNotFoundException
import land.leets.domain.application.type.ApplicationStatus
import land.leets.domain.interview.domain.Interview
import land.leets.domain.interview.domain.repository.InterviewRepository
import land.leets.domain.interview.type.HasInterview
import java.time.LocalDateTime
import java.util.UUID

class GetApplicationStatusImplTest : DescribeSpec({

val applicationRepository = mockk<ApplicationRepository>()
val interviewRepository = mockk<InterviewRepository>()
val getApplicationStatus = GetApplicationStatusImpl(applicationRepository, interviewRepository)

val uid = UUID.randomUUID()

val interviewDate: LocalDateTime = LocalDateTime.of(2026, 3, 14, 14, 0)
val interviewPlace = "전자정보도서관 1층 스터디룸 A"
val applicationId = 1L

fun mockApplication(
status: ApplicationStatus,
): Application = mockk<Application>().also { application ->
every { application.id } returns applicationId
every { application.applicationStatus } returns status
}

fun mockInterview(
hasInterview: HasInterview = HasInterview.PENDING,
): Interview = mockk<Interview>().also { interview ->
every { interview.hasInterview } returns hasInterview
every { interview.fixedInterviewDate } returns interviewDate
every { interview.place } returns interviewPlace
}

describe("GetApplicationStatusImpl 유스케이스는") {

context("지원서 상태 조회를 요청할 때") {

it("지원서 상태가 PASS_PAPER이면 인터뷰 정보를 포함하여 반환한다") {
val application = mockApplication(status = ApplicationStatus.PASS_PAPER)
val interview = mockInterview()
every { applicationRepository.findByUser_Id(uid) } returns application
every { interviewRepository.findByApplication(application) } returns interview

val result = getApplicationStatus.execute(uid)

result.id shouldBe 1L
result.status shouldBe ApplicationStatus.PASS_PAPER
result.hasInterview shouldBe HasInterview.PENDING
result.interviewDate shouldBe interviewDate
result.interviewPlace shouldBe interviewPlace
}

it("PASS_PAPER이 아닌 상태들은 인터뷰 정보가 null이어야 한다") {
val nonInterviewStatuses = listOf(
ApplicationStatus.PENDING,
ApplicationStatus.FAIL_PAPER,
ApplicationStatus.PASS,
ApplicationStatus.FAIL,
)

nonInterviewStatuses.forEach { status ->
val application = mockApplication(status = status)
val interview = mockInterview()
every { applicationRepository.findByUser_Id(uid) } returns application
every { interviewRepository.findByApplication(application) } returns interview

val result = getApplicationStatus.execute(uid)

result.id shouldBe 1L
result.status shouldBe status
result.hasInterview shouldBe null
result.interviewDate shouldBe null
result.interviewPlace shouldBe null
}
}

it("지원서가 존재하지 않으면 ApplicationNotFoundException을 던진다") {
every { applicationRepository.findByUser_Id(uid) } returns null

shouldThrow<ApplicationNotFoundException> {
getApplicationStatus.execute(uid)
}
}
}
}
})