Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dsm.pick2024.domain.application.port.`in`.ApplicationUseCase
import dsm.pick2024.domain.application.port.out.ExistsApplicationPort
import dsm.pick2024.domain.application.port.out.SaveApplicationPort
import dsm.pick2024.domain.application.presentation.dto.request.ApplicationRequest
import dsm.pick2024.domain.attendance.domain.service.AttendanceService
import dsm.pick2024.domain.fcm.dto.request.FcmRequest
import dsm.pick2024.domain.main.port.`in`.MainUseCase
import dsm.pick2024.domain.outbox.domain.Outbox
Expand All @@ -27,7 +28,8 @@ class ApplicationService(
private val userFacadeUseCase: UserFacadeUseCase,
private val adminFinderUseCase: AdminFinderUseCase,
private val saveOutboxPort: SaveOutboxPort,
private val mainUseCase: MainUseCase
private val mainUseCase: MainUseCase,
private val attendanceService: AttendanceService
) : ApplicationUseCase {

@Transactional
Expand All @@ -37,6 +39,12 @@ class ApplicationService(
throw AlreadyApplyingForPicnicException
}

attendanceService.checkApplicationTime(
applicationType = request.applicationType,
start = request.start,
end = request.end
)

saveApplicationPort.save(
Application(
userName = user.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dsm.pick2024.domain.attendance.domain.service
import dsm.pick2024.domain.application.enums.ApplicationType
import dsm.pick2024.domain.attendance.domain.Attendance
import dsm.pick2024.domain.attendance.enums.AttendanceStatus
import dsm.pick2024.domain.attendance.exception.InvalidPeriodException
import org.springframework.stereotype.Component
import java.time.LocalTime

Expand All @@ -14,7 +15,7 @@ class AttendanceService {
LocalTime.of(8, 40) to LocalTime.of(9, 40), // 1교시
LocalTime.of(9, 40) to LocalTime.of(10, 40), // 2교시
LocalTime.of(10, 40) to LocalTime.of(11, 40), // 3교시
LocalTime.of(12, 40) to LocalTime.of(13, 30), // 4교시
LocalTime.of(11, 40) to LocalTime.of(13, 30), // 4교시
LocalTime.of(13, 30) to LocalTime.of(14, 40), // 5교시
LocalTime.of(14, 30) to LocalTime.of(15, 30), // 6교시
LocalTime.of(15, 30) to LocalTime.of(16, 30), // 7교시
Expand All @@ -29,17 +30,70 @@ class AttendanceService {
}

// 교시 혹은 시간을 기반으로 교시 목록을 반환하는 함수
fun translateApplication(start: String, end: String?, applicationType: ApplicationType): List<String> {
fun translateApplication(start: String, end: String, applicationType: ApplicationType): Pair<String, String> {
return when (applicationType) {
ApplicationType.PERIOD -> listOf(start, end!!)
ApplicationType.PERIOD -> Pair(start, end)
ApplicationType.TIME -> {
val startTime = LocalTime.parse(start)
val endTime = end?.let { LocalTime.parse(it) }
val endTime = LocalTime.parse(end)
getMatchPeriods(startTime, endTime)
}
}
}

fun checkApplicationTime(applicationType: ApplicationType, start: String, end: String) {
when (applicationType) {
ApplicationType.TIME -> {
val startTime = LocalTime.parse(start)
val endTime = LocalTime.parse(end)
if (startTime > endTime ||
endTime > LocalTime.of(20, 30) ||
startTime < LocalTime.of(8, 30)
) {
throw InvalidPeriodException
}
}
ApplicationType.PERIOD -> {
val startPeriod = start.replace("교시", "").toIntOrNull() ?: throw InvalidPeriodException
val endPeriod = end.replace("교시", "").toIntOrNull() ?: throw InvalidPeriodException
if (startPeriod > endPeriod || startPeriod < 1 || endPeriod > 10) {
throw InvalidPeriodException
}
}
}
Comment on lines +44 to +63
Copy link

@coderabbitai coderabbitai bot Jan 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

잘못된 시간/교시 포맷이 그대로 런타임 예외로 노출될 수 있습니다.

LocalTime.parse(...)toInt()가 실패하면 InvalidPeriodException이 아닌 DateTimeParseException/NumberFormatException으로 터져 500이 될 수 있습니다. 입력 검증으로 예외를 통일하는 편이 안전합니다.

🛠️ 제안 수정
 fun checkApplicationTime(applicationType: ApplicationType, start: String, end: String) {
     when (applicationType) {
         ApplicationType.TIME -> {
-            val startTime = LocalTime.parse(start)
-            val endTime = LocalTime.parse(end)
+            val startTime = runCatching { LocalTime.parse(start) }
+                .getOrElse { throw InvalidPeriodException }
+            val endTime = runCatching { LocalTime.parse(end) }
+                .getOrElse { throw InvalidPeriodException }
             if (startTime > endTime ||
                 endTime > LocalTime.of(20, 30) ||
                 startTime < LocalTime.of(8, 30)
             ) {
                 throw InvalidPeriodException
             }
         }
         ApplicationType.PERIOD -> {
-            val startPeriod = start.replace("교시", "").toInt()
-            val endPeriod = end.replace("교시", "").toInt()
+            val startPeriod = start.replace("교시", "").toIntOrNull()
+                ?: throw InvalidPeriodException
+            val endPeriod = end.replace("교시", "").toIntOrNull()
+                ?: throw InvalidPeriodException
             if (startPeriod > endPeriod || startPeriod < 1 || endPeriod > 10) {
                 throw InvalidPeriodException
             }
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun checkApplicationTime(applicationType: ApplicationType, start: String, end: String) {
when (applicationType) {
ApplicationType.TIME -> {
val startTime = LocalTime.parse(start)
val endTime = LocalTime.parse(end)
if (startTime > endTime ||
endTime > LocalTime.of(20, 30) ||
startTime < LocalTime.of(8, 30)
) {
throw InvalidPeriodException
}
}
ApplicationType.PERIOD -> {
val startPeriod = start.replace("교시", "").toInt()
val endPeriod = end.replace("교시", "").toInt()
if (startPeriod > endPeriod || startPeriod < 1 || endPeriod > 10) {
throw InvalidPeriodException
}
}
}
fun checkApplicationTime(applicationType: ApplicationType, start: String, end: String) {
when (applicationType) {
ApplicationType.TIME -> {
val startTime = runCatching { LocalTime.parse(start) }
.getOrElse { throw InvalidPeriodException }
val endTime = runCatching { LocalTime.parse(end) }
.getOrElse { throw InvalidPeriodException }
if (startTime > endTime ||
endTime > LocalTime.of(20, 30) ||
startTime < LocalTime.of(8, 30)
) {
throw InvalidPeriodException
}
}
ApplicationType.PERIOD -> {
val startPeriod = start.replace("교시", "").toIntOrNull()
?: throw InvalidPeriodException
val endPeriod = end.replace("교시", "").toIntOrNull()
?: throw InvalidPeriodException
if (startPeriod > endPeriod || startPeriod < 1 || endPeriod > 10) {
throw InvalidPeriodException
}
}
}
}
🤖 Prompt for AI Agents
In
`@src/main/kotlin/dsm/pick2024/domain/attendance/domain/service/AttendanceService.kt`
around lines 44 - 63, In checkApplicationTime, guard against parse failures by
wrapping LocalTime.parse(...) and the toInt() conversions in try/catch blocks
(or a single try surrounding each ApplicationType branch) and convert
DateTimeParseException and NumberFormatException into the unified
InvalidPeriodException; specifically update the ApplicationType.TIME branch to
catch/paraphrase exceptions from LocalTime.parse and the ApplicationType.PERIOD
branch to catch exceptions from start.replace(...).toInt() /
end.replace(...).toInt(), rethrowing InvalidPeriodException when parsing fails
or when the existing logical checks fail.

Copy link
Member Author

Choose a reason for hiding this comment

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

abf2875
time 변환은 기존에도 저런 형식으로 진행하였기 때문에 괜찮습니다

Copy link

Choose a reason for hiding this comment

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

@ByunDohwi 알겠습니다! 기존 코드베이스와의 일관성을 유지하는 것도 중요하니 이해했습니다. 입력 검증은 현재 방식대로 진행하시면 되겠습니다. 👍

}

fun checkEarlyReturnTime(applicationType: ApplicationType, start: String) {
when (applicationType) {
ApplicationType.TIME -> {
val startTime = LocalTime.parse(start)
if (startTime < LocalTime.of(8, 30)) {
throw InvalidPeriodException
}
}
ApplicationType.PERIOD -> {
val startPeriod = start.replace("교시", "").toInt()
if (startPeriod < 1 || startPeriod > 10) {
throw InvalidPeriodException
}
}
}
}

fun translateEarlyReturn(start: String, applicationType: ApplicationType): String {
return when (applicationType) {
ApplicationType.PERIOD -> start
ApplicationType.TIME -> {
val startTime = LocalTime.parse(start)
val startIndex = periods.indexOfFirst { (start, endAt) ->
startTime >= start && startTime < endAt
}
if (startIndex == -1) throw InvalidPeriodException
periodNames[startIndex]
}
}
}
Comment on lines +83 to +95
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

경계 시간 처리 로직이 getMatchPeriods와 불일치합니다.

translateEarlyReturnstartTime < endAt (종료 시간 제외)을 사용하지만, getMatchPeriodsin start..endAt (종료 시간 포함)을 사용합니다.

예를 들어 9:40 (1교시 종료 시간)의 경우:

  • translateEarlyReturn: 2교시로 매칭
  • getMatchPeriods: 1교시로 매칭

두 함수의 경계 처리 로직을 통일하는 것이 좋습니다.

🛠️ 제안 수정 (inclusive로 통일)
 fun translateEarlyReturn(start: String, applicationType: ApplicationType): String {
     return when (applicationType) {
         ApplicationType.PERIOD -> start
         ApplicationType.TIME -> {
             val startTime = LocalTime.parse(start)
             val startIndex = periods.indexOfFirst { (start, endAt) ->
-                startTime >= start && startTime < endAt
+                startTime in start..endAt
             }
             if (startIndex == -1) throw InvalidPeriodException
             periodNames[startIndex]
         }
     }
 }
🤖 Prompt for AI Agents
In
`@src/main/kotlin/dsm/pick2024/domain/attendance/domain/service/AttendanceService.kt`
around lines 83 - 95, translateEarlyReturn currently treats the period end as
exclusive (startTime < endAt) which mismatches getMatchPeriods; change the
matching to be inclusive of the end boundary so both functions behave the same.
In translateEarlyReturn (and the lambda over periods) use an inclusive check
(e.g., startTime in start..endAt or startTime >= start && startTime <= endAt)
when finding startIndex, and keep throwing InvalidPeriodException and returning
periodNames[startIndex] as before so periods, periodNames, translateEarlyReturn
and getMatchPeriods use the same boundary logic.


// 주어진 교시 혹은 시간에 해당하는 출석 상태를 업데이트하는 함수
fun updateAttendanceToApplication(
start: String,
Expand Down Expand Up @@ -113,27 +167,16 @@ class AttendanceService {
return updateAttendance
}

fun translateEarlyReturn(start: String, applicationType: ApplicationType): List<String> {
return when (applicationType) {
ApplicationType.PERIOD -> {
val startIndex = periodNames.indexOf(start).coerceAtLeast(0)
periodNames.subList(startIndex, periodNames.size)
}
ApplicationType.TIME -> {
val startTime = LocalTime.parse(start)
getMatchPeriods(startTime, null)
}
private fun getMatchPeriods(startTime: LocalTime, endTime: LocalTime): Pair<String, String> {
val startIndex = periods.indexOfFirst { (start, endAt) ->
startTime in start..endAt
}
val endIndex = periods.indexOfFirst { (start, endAt) ->
endTime in start..endAt
}
}

private fun getMatchPeriods(startTime: LocalTime, endTime: LocalTime?): List<String> {
return periods
.mapIndexed { index, period ->
if ((startTime < period.second) && (endTime == null || endTime > period.first)) {
periodNames[index]
} else {
null
}
}.filterNotNull()
if (startIndex == -1 || endIndex == -1) throw InvalidPeriodException

return periodNames[startIndex] to periodNames[endIndex]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dsm.pick2024.domain.application.enums.ApplicationKind
import dsm.pick2024.domain.application.enums.Status
import dsm.pick2024.domain.application.port.out.ExistsApplicationPort
import dsm.pick2024.domain.application.port.out.SaveApplicationPort
import dsm.pick2024.domain.attendance.domain.service.AttendanceService
import dsm.pick2024.domain.earlyreturn.exception.AlreadyApplyingForEarlyReturnException
import dsm.pick2024.domain.earlyreturn.port.`in`.CreateEarlyReturnUseCase
import dsm.pick2024.domain.earlyreturn.presentation.dto.request.CreateEarlyReturnRequest
Expand All @@ -24,7 +25,8 @@ class CreateEarlyReturnService(
private val userFacadeUseCase: UserFacadeUseCase,
private val adminFinderUseCase: AdminFinderUseCase,
private val outboxFacadeUseCase: OutboxFacadeUseCase,
private val mainUseCase: MainUseCase
private val mainUseCase: MainUseCase,
private val attendanceService: AttendanceService
) : CreateEarlyReturnUseCase {
@Transactional
override fun createEarlyReturn(request: CreateEarlyReturnRequest) {
Expand All @@ -34,6 +36,8 @@ class CreateEarlyReturnService(
throw AlreadyApplyingForEarlyReturnException
}

attendanceService.checkEarlyReturnTime(request.applicationType, request.start)

saveApplicationPort.save(
Application(
userName = user.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class EarlyReturnApprovalProcessor(
return ApplicationStory(
reason = application.reason,
userName = application.userName,
start = start.first(),
start = start,
date = application.date,
type = Type.EARLY_RETURN,
userId = application.userId
Expand Down