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,
)
}
Comment on lines +21 to +37
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

저는 현재 로직을 서류 합격 상태일 경우에는 인터뷰 장소와 날짜를 반환하고,
그렇지 않으면 지원 상태(서탈, 최합, 최탈)만 반환하는 구조로 이해했습니다.

다만 저희 서비스에서는
Application엔티티는 지원자가 희망한 면접 정보를 가지고 있고,
운영진이 최종적으로 통보하는 면접 정보는 Interview 엔티티에서 관리하는 것으로 알고 있습니다.

따라서 인터뷰에 대한 정보는 Application 엔티티가 아닌 Interview 엔티티에서 가지고 오는 것이 옳은 로직이라고 생각하는데, 정완님의 의견이 궁금합니다!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

오아 제가 해당 부분은 완전히 착각하고 코드를 작성해버렸네요..캐치 감사합니다..!!
interview 엔티티에서 정보를 가져오는게 정답이네요 ㅎㅎ

수정하겠습니다 ❤️

}
}
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)
}
}
}
}
})