Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,6 +10,7 @@ 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
Expand All @@ -25,7 +26,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 +118,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,32 @@
package land.leets.domain.application.presentation.dto

import land.leets.domain.application.domain.Application
import land.leets.domain.application.type.ApplicationStatus

data class ApplicationStatusResponse(
val id: Long,
val status: ApplicationStatus,
val interviewDay: String?,
val interviewTime: String?,
) {
companion object {
fun from(
application: Application,
): 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,
interviewDay = null,
interviewTime = null,
)
}
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 Application entity defines interviewDay and interviewTime as non-nullable fields (nullable = false), but this response conditionally returns null for these fields when the status is not PASS_PAPER. This creates a data model inconsistency.

Since all applications must have interviewDay and interviewTime values (as per the entity definition), consider one of the following approaches:

  1. Always return the interview information regardless of status, since it exists in the database
  2. If interview information should only be shown for PASS_PAPER status, make these fields nullable in the Application entity and update the database schema accordingly

The current implementation assumes these fields can be null conditionally, but the entity enforces them as required.

Suggested change
val interviewDay: String?,
val interviewTime: String?,
) {
companion object {
fun from(
application: Application,
): ApplicationStatusResponse {
if (application.applicationStatus != ApplicationStatus.PASS_PAPER) {
return ApplicationStatusResponse(
id = application.id!!,
status = application.applicationStatus,
interviewDay = null,
interviewTime = null,
)
}
val interviewDay: String,
val interviewTime: String,
) {
companion object {
fun from(
application: Application,
): ApplicationStatusResponse {

Copilot uses AI. Check for mistakes.
return ApplicationStatusResponse(
id = application.id!!,
Comment on lines 23 to 31
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 use of the non-null assertion operator (!!) on application.id could throw a NullPointerException if the application entity hasn't been persisted yet. While this is unlikely in this context since we're fetching from the repository, consider using a safer approach or document the assumption that the application will always have an ID at this point.

A safer alternative would be to use the elvis operator with a meaningful exception: id = application.id ?: throw IllegalStateException("Application must have an ID")

Suggested change
id = application.id!!,
status = application.applicationStatus,
interviewDay = null,
interviewTime = null,
)
}
return ApplicationStatusResponse(
id = application.id!!,
id = application.id ?: throw IllegalStateException("Application must have an ID"),
status = application.applicationStatus,
interviewDay = null,
interviewTime = null,
)
}
return ApplicationStatusResponse(
id = application.id ?: throw IllegalStateException("Application must have an ID"),

Copilot uses AI. Check for mistakes.
Comment on lines 21 to 31
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 same non-null assertion operator (!!) issue appears here. See the comment on line 18 for the recommended safer approach.

Suggested change
if (application.applicationStatus != ApplicationStatus.PASS_PAPER) {
return ApplicationStatusResponse(
id = application.id!!,
status = application.applicationStatus,
interviewDay = null,
interviewTime = null,
)
}
return ApplicationStatusResponse(
id = application.id!!,
val applicationId = application.id
?: throw IllegalStateException("Application id must not be null")
if (application.applicationStatus != ApplicationStatus.PASS_PAPER) {
return ApplicationStatusResponse(
id = applicationId,
status = application.applicationStatus,
interviewDay = null,
interviewTime = null,
)
}
return ApplicationStatusResponse(
id = applicationId,

Copilot uses AI. Check for mistakes.
status = application.applicationStatus,
interviewDay = application.interviewDay,
interviewTime = application.interviewTime,
)
}
Comment on lines 21 to 37
Copy link
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
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
}
Copy link
Member

Choose a reason for hiding this comment

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

개행 부탁드립니당 👀

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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 org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.UUID

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

return ApplicationStatusResponse.from(application)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

👀

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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 java.util.UUID

class GetApplicationStatusImplTest : DescribeSpec({

val applicationRepository = mockk<ApplicationRepository>()
val getApplicationStatus = GetApplicationStatusImpl(applicationRepository)

val uid = UUID.randomUUID()

val interviewDay = "2025년 3월 3일 (토)"
val interviewTime = "15:00"

fun mockApplication(
id: Long = 1L,
status: ApplicationStatus,
day: String = interviewDay,
time: String = interviewTime
): Application = mockk<Application>().also { application ->
every { application.id } returns id
every { application.applicationStatus } returns status
every { application.interviewDay } returns day
every { application.interviewTime } returns time
}

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

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

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

val result = getApplicationStatus.execute(uid)

result.id shouldBe 1L
result.status shouldBe ApplicationStatus.PASS_PAPER
result.interviewDay shouldBe interviewDay
result.interviewTime shouldBe interviewTime
}

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

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

val result = getApplicationStatus.execute(uid)

result.id shouldBe 1L
result.status shouldBe status
result.interviewDay shouldBe null
result.interviewTime shouldBe null
}
}

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

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