diff --git a/src/main/kotlin/com/moa/controller/AuthController.kt b/src/main/kotlin/com/moa/controller/AuthController.kt index ea5b630..5670914 100644 --- a/src/main/kotlin/com/moa/controller/AuthController.kt +++ b/src/main/kotlin/com/moa/controller/AuthController.kt @@ -4,11 +4,7 @@ import com.moa.common.auth.Auth import com.moa.common.auth.AuthMemberInfo import com.moa.common.response.ApiResponse import com.moa.service.AuthService -import com.moa.service.dto.AppleSignInUpRequest -import com.moa.service.dto.AppleSignInUpResponse -import com.moa.service.dto.KaKaoSignInUpRequest -import com.moa.service.dto.KakaoSignInUpResponse -import com.moa.service.dto.LogoutRequest +import com.moa.service.dto.* import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.http.ResponseEntity @@ -16,7 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController -@Tag(name = "Auth", description = "인증 API (카카오/애플 소셜 로그인)") +@Tag(name = "Auth", description = "인증 API") @RestController class AuthController( private val authService: AuthService, diff --git a/src/main/kotlin/com/moa/controller/FcmTokenController.kt b/src/main/kotlin/com/moa/controller/FcmTokenController.kt index eca563c..ac0fb23 100644 --- a/src/main/kotlin/com/moa/controller/FcmTokenController.kt +++ b/src/main/kotlin/com/moa/controller/FcmTokenController.kt @@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.web.bind.annotation.* -@Tag(name = "fcm", description = "FCM TOKEN 갱신 및 삭제") +@Tag(name = "fcm", description = "Fcm Token 갱신 및 삭제") @RestController @RequestMapping("/api/v1/fcm/token") class FcmTokenController( diff --git a/src/main/kotlin/com/moa/controller/NotificationSettingController.kt b/src/main/kotlin/com/moa/controller/NotificationSettingController.kt index c558126..35acc16 100644 --- a/src/main/kotlin/com/moa/controller/NotificationSettingController.kt +++ b/src/main/kotlin/com/moa/controller/NotificationSettingController.kt @@ -8,7 +8,7 @@ import com.moa.service.notification.NotificationSettingService import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.* -@Tag(name = "NotificationSetting", description = "알림 설정 API (근무/급여일/프로모션 알림)") +@Tag(name = "NotificationSetting", description = "알림 설정 API") @RestController @RequestMapping("/api/v1/settings/notification") class NotificationSettingController( diff --git a/src/main/kotlin/com/moa/controller/ProfileController.kt b/src/main/kotlin/com/moa/controller/ProfileController.kt index 1eb28bf..80dda13 100644 --- a/src/main/kotlin/com/moa/controller/ProfileController.kt +++ b/src/main/kotlin/com/moa/controller/ProfileController.kt @@ -5,13 +5,13 @@ import com.moa.common.auth.AuthMemberInfo import com.moa.common.response.ApiResponse import com.moa.service.ProfileService import com.moa.service.dto.NicknameUpdateRequest -import com.moa.service.dto.WorkplaceUpdateRequest import com.moa.service.dto.PaydayUpdateRequest +import com.moa.service.dto.WorkplaceUpdateRequest import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.web.bind.annotation.* -@Tag(name = "Profile", description = "프로필 API (닉네임, 근무지, 급여일 등)") +@Tag(name = "Profile", description = "프로필 API") @RestController @RequestMapping("/api/v1/profile") class ProfileController( diff --git a/src/main/kotlin/com/moa/controller/WorkdayController.kt b/src/main/kotlin/com/moa/controller/WorkdayController.kt index 5e95ee4..65840b6 100644 --- a/src/main/kotlin/com/moa/controller/WorkdayController.kt +++ b/src/main/kotlin/com/moa/controller/WorkdayController.kt @@ -11,19 +11,14 @@ import jakarta.validation.Valid import org.springframework.web.bind.annotation.* import java.time.LocalDate -@Tag(name = "Workday", description = "근무일/스케줄 API (출퇴근 일정 조회·등록·수정)") +@Tag(name = "Workday", description = "근무일/스케줄 API") @RestController @RequestMapping("/api/v1/workdays") class WorkdayController( private val workdayService: WorkdayService, ) { - @GetMapping("/{date}") - fun getSchedule( - @Auth member: AuthMemberInfo, - @PathVariable date: LocalDate, - ) = ApiResponse.success(workdayService.getSchedule(member.id, date)) - + @Deprecated("Use GET /api/v1/calendar instead") @GetMapping("/earnings") fun getMonthlyEarnings( @Auth member: AuthMemberInfo, @@ -31,6 +26,7 @@ class WorkdayController( @RequestParam month: Int, ) = ApiResponse.success(workdayService.getMonthlyEarnings(member.id, year, month)) + @Deprecated("Use GET /api/v1/calendar instead") @GetMapping fun getMonthlySchedules( @Auth member: AuthMemberInfo, @@ -39,7 +35,14 @@ class WorkdayController( ) = ApiResponse.success( workdayService.getMonthlySchedules(member.id, year, month) ) - + + @Deprecated("Use GET /api/v1/calendar instead") + @GetMapping("/{date}") + fun getSchedule( + @Auth member: AuthMemberInfo, + @PathVariable date: LocalDate, + ) = ApiResponse.success(workdayService.getSchedule(member.id, date)) + @PutMapping("/{date}") fun upsertSchedule( @Auth member: AuthMemberInfo, diff --git a/src/main/kotlin/com/moa/controller/screen/CalendarController.kt b/src/main/kotlin/com/moa/controller/screen/CalendarController.kt new file mode 100644 index 0000000..fd696a1 --- /dev/null +++ b/src/main/kotlin/com/moa/controller/screen/CalendarController.kt @@ -0,0 +1,32 @@ +package com.moa.controller.screen + +import com.moa.common.auth.Auth +import com.moa.common.auth.AuthMemberInfo +import com.moa.common.response.ApiResponse +import com.moa.service.WorkdayService +import com.moa.service.dto.CalendarResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Calendar", description = "캘린더 화면 API") +@RestController +@RequestMapping("/api/v1/calendar") +class CalendarController( + private val workdayService: WorkdayService, +) { + + @GetMapping + fun getCalendar( + @Auth member: AuthMemberInfo, + @RequestParam year: Int, + @RequestParam month: Int, + ) = ApiResponse.success( + CalendarResponse( + earnings = workdayService.getMonthlyEarnings(member.id, year, month), + schedules = workdayService.getMonthlyWorkdays(member.id, year, month), + ) + ) +} diff --git a/src/main/kotlin/com/moa/controller/HomeController.kt b/src/main/kotlin/com/moa/controller/screen/HomeController.kt similarity index 94% rename from src/main/kotlin/com/moa/controller/HomeController.kt rename to src/main/kotlin/com/moa/controller/screen/HomeController.kt index f8aca84..fe61848 100644 --- a/src/main/kotlin/com/moa/controller/HomeController.kt +++ b/src/main/kotlin/com/moa/controller/screen/HomeController.kt @@ -1,4 +1,4 @@ -package com.moa.controller +package com.moa.controller.screen import com.moa.common.auth.Auth import com.moa.common.auth.AuthMemberInfo @@ -32,6 +32,7 @@ class HomeController( standardSalary = earnings.standardSalary, dailyPay = schedule.dailyPay, type = schedule.type, + status = schedule.status, clockInTime = schedule.clockInTime, clockOutTime = schedule.clockOutTime, ) diff --git a/src/main/kotlin/com/moa/controller/OnboardingController.kt b/src/main/kotlin/com/moa/controller/screen/OnboardingController.kt similarity index 93% rename from src/main/kotlin/com/moa/controller/OnboardingController.kt rename to src/main/kotlin/com/moa/controller/screen/OnboardingController.kt index 301ce0f..5d73d03 100644 --- a/src/main/kotlin/com/moa/controller/OnboardingController.kt +++ b/src/main/kotlin/com/moa/controller/screen/OnboardingController.kt @@ -1,4 +1,4 @@ -package com.moa.controller +package com.moa.controller.screen import com.moa.common.auth.AuthMemberInfo import com.moa.common.auth.OnboardingAuth @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.web.bind.annotation.* -@Tag(name = "Onboarding", description = "온보딩 API (최초 가입 시 프로필/급여/근무정책/약관 설정)") +@Tag(name = "Onboarding", description = "온보딩 화면 API") @RestController @RequestMapping("/api/v1/onboarding") class OnboardingController( diff --git a/src/main/kotlin/com/moa/entity/DailyEventType.kt b/src/main/kotlin/com/moa/entity/DailyEventType.kt new file mode 100644 index 0000000..897acdb --- /dev/null +++ b/src/main/kotlin/com/moa/entity/DailyEventType.kt @@ -0,0 +1,6 @@ +package com.moa.entity + +enum class DailyEventType { + PAYDAY, + HOLIDAY, +} diff --git a/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt b/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt new file mode 100644 index 0000000..ea20144 --- /dev/null +++ b/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt @@ -0,0 +1,7 @@ +package com.moa.entity + +enum class DailyWorkStatusType { + NONE, + SCHEDULED, + COMPLETED, +} diff --git a/src/main/kotlin/com/moa/service/PaydayResolver.kt b/src/main/kotlin/com/moa/service/PaydayResolver.kt new file mode 100644 index 0000000..51bfb38 --- /dev/null +++ b/src/main/kotlin/com/moa/service/PaydayResolver.kt @@ -0,0 +1,17 @@ +package com.moa.service + +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth + +// 월급일이 해당 월에 없으면 말일로 보정하고, 그 날짜가 주말이면 직전 금요일로 당긴다. +fun resolveEffectivePayday(year: Int, month: Int, paydayDay: Int): LocalDate { + val yearMonth = YearMonth.of(year, month) + val baseDate = yearMonth.atDay(minOf(paydayDay, yearMonth.lengthOfMonth())) + + return when (baseDate.dayOfWeek) { + DayOfWeek.SATURDAY -> baseDate.minusDays(1) + DayOfWeek.SUNDAY -> baseDate.minusDays(2) + else -> baseDate + } +} diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 0508541..395e7ff 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -3,11 +3,9 @@ package com.moa.service import com.moa.common.exception.BadRequestException import com.moa.common.exception.ErrorCode import com.moa.common.exception.NotFoundException -import com.moa.entity.DailyWorkSchedule -import com.moa.entity.DailyWorkScheduleType -import com.moa.entity.SalaryCalculator -import com.moa.entity.WorkPolicyVersion +import com.moa.entity.* import com.moa.repository.DailyWorkScheduleRepository +import com.moa.repository.ProfileRepository import com.moa.repository.WorkPolicyVersionRepository import com.moa.service.dto.* import com.moa.service.notification.NotificationSyncService @@ -15,30 +13,117 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal import java.time.LocalDate +import java.time.LocalDateTime import java.time.LocalTime @Service class WorkdayService( private val dailyWorkScheduleRepository: DailyWorkScheduleRepository, private val workPolicyVersionRepository: WorkPolicyVersionRepository, + private val profileRepository: ProfileRepository, private val notificationSyncService: NotificationSyncService, private val earningsCalculator: EarningsCalculator, ) { @Transactional(readOnly = true) - fun getSchedule( - memberId: Long, - date: LocalDate, - ): WorkdayResponse { - val saved = dailyWorkScheduleRepository.findByMemberIdAndDate(memberId, date) - val policy = resolveMonthlyRepresentativePolicyOrNull(memberId, date.year, date.monthValue) - val schedule = - if (policy == null) { - ResolvedSchedule(DailyWorkScheduleType.NONE, null, null) - } else { - resolveScheduleForDate(saved, policy, date) + fun getMonthlyWorkdays(memberId: Long, year: Int, month: Int): List { + val start = LocalDate.of(year, month, 1) + val end = start.withDayOfMonth(start.lengthOfMonth()) + + val savedSchedulesByDate = + dailyWorkScheduleRepository + .findAllByMemberIdAndDateBetween(memberId, start, end) + .associateBy { it.date } + + val monthlyPolicy = resolveMonthlyRepresentativePolicyOrNull(memberId, year, month) + val paydayDay = resolvePaydayDay(memberId) + + return generateSequence(start) { it.plusDays(1) } + .takeWhile { !it.isAfter(end) } + .map { date -> + val schedule = + if (monthlyPolicy == null) { + ResolvedSchedule(DailyWorkScheduleType.NONE, null, null) + } else { + resolveScheduleForDate(savedSchedulesByDate[date], monthlyPolicy, date) + } + createWorkdayResponse(memberId, date, schedule, monthlyPolicy, paydayDay) + } + .toList() + } + + @Transactional(readOnly = true) + fun getMonthlyEarnings(memberId: Long, year: Int, month: Int): MonthlyEarningsResponse { + val start = LocalDate.of(year, month, 1) + val end = start.withDayOfMonth(start.lengthOfMonth()) + val today = LocalDate.now() + val defaultSalary = earningsCalculator.getDefaultMonthlySalary(memberId, start) ?: 0 + + val monthlyPolicy = resolveMonthlyRepresentativePolicyOrNull(memberId, year, month) + if (monthlyPolicy == null) { + return MonthlyEarningsResponse( + workedEarnings = 0, + standardSalary = defaultSalary, + workedMinutes = 0, + standardMinutes = 0, + ) + } + + val policyDailyMinutes = SalaryCalculator.calculateWorkMinutes( + monthlyPolicy.clockInTime, monthlyPolicy.clockOutTime, + ) + + val policyWorkDayOfWeeks = monthlyPolicy.workdays.map { it.dayOfWeek }.toSet() + val workDaysInMonth = SalaryCalculator.getWorkDaysInPeriod( + start = start, + end = end.plusDays(1), + workDays = policyWorkDayOfWeeks + ) + + val standardMinutes = policyDailyMinutes * workDaysInMonth + + if (start.isAfter(today)) { + return MonthlyEarningsResponse(0, defaultSalary, 0, standardMinutes) + } + + val lastCalculableDate = minOf(end, today) + + val savedSchedulesByDate = dailyWorkScheduleRepository + .findAllByMemberIdAndDateBetween(memberId, start, lastCalculableDate) + .associateBy { it.date } + + var totalEarnings = BigDecimal.ZERO + var workedMinutes = 0L + val now = LocalTime.now() + + var date = start + while (!date.isAfter(lastCalculableDate)) { + val schedule = resolveScheduleForDate(savedSchedulesByDate[date], monthlyPolicy, date) + val status = resolveDailyWorkStatus(date, schedule) + val adjustedClockOut = resolveClockOutForEarnings(date, today, now, schedule) + val isCompletedWork = status == DailyWorkStatusType.COMPLETED && + (schedule.type == DailyWorkScheduleType.WORK || schedule.type == DailyWorkScheduleType.VACATION) + + if (isCompletedWork && schedule.clockIn != null && adjustedClockOut != null) { + workedMinutes += SalaryCalculator.calculateWorkMinutes(schedule.clockIn, adjustedClockOut) } - return createWorkdayResponse(memberId, date, schedule) + + if (isCompletedWork) { + val dailyEarnings = earningsCalculator.calculateDailyEarnings( + memberId, date, monthlyPolicy, schedule.type, schedule.clockIn, adjustedClockOut, + ) + totalEarnings = totalEarnings.add(dailyEarnings ?: BigDecimal.ZERO) + } + + date = date.plusDays(1) + } + + return MonthlyEarningsResponse( + workedEarnings = totalEarnings.toLong(), + standardSalary = defaultSalary, + workedMinutes = workedMinutes, + standardMinutes = standardMinutes, + ) } @Transactional(readOnly = true) @@ -72,6 +157,22 @@ class WorkdayService( .toList() } + @Transactional(readOnly = true) + fun getSchedule( + memberId: Long, + date: LocalDate, + ): WorkdayResponse { + val saved = dailyWorkScheduleRepository.findByMemberIdAndDate(memberId, date) + val policy = resolveMonthlyRepresentativePolicyOrNull(memberId, date.year, date.monthValue) + val schedule = + if (policy == null) { + ResolvedSchedule(DailyWorkScheduleType.NONE, null, null) + } else { + resolveScheduleForDate(saved, policy, date) + } + return createWorkdayResponse(memberId, date, schedule, policy, resolvePaydayDay(memberId)) + } + @Transactional fun upsertSchedule(memberId: Long, date: LocalDate, req: WorkdayUpsertRequest): WorkdayResponse { val (clockIn, clockOut) = when (req.type) { @@ -122,7 +223,13 @@ class WorkdayService( ) val schedule = ResolvedSchedule(savedSchedule.type, savedSchedule.clockInTime, savedSchedule.clockOutTime) - return createWorkdayResponse(memberId, date, schedule) + return createWorkdayResponse( + memberId, + date, + schedule, + resolveMonthlyRepresentativePolicyOrNull(memberId, date.year, date.monthValue), + resolvePaydayDay(memberId), + ) } @Transactional @@ -155,75 +262,12 @@ class WorkdayService( ) val schedule = ResolvedSchedule(savedSchedule.type, savedSchedule.clockInTime, savedSchedule.clockOutTime) - return createWorkdayResponse(memberId, date, schedule) - } - - @Transactional(readOnly = true) - fun getMonthlyEarnings(memberId: Long, year: Int, month: Int): MonthlyEarningsResponse { - val start = LocalDate.of(year, month, 1) - val end = start.withDayOfMonth(start.lengthOfMonth()) - val today = LocalDate.now() - val defaultSalary = earningsCalculator.getDefaultMonthlySalary(memberId, start) ?: 0 - - val monthlyPolicy = resolveMonthlyRepresentativePolicyOrNull(memberId, year, month) - if (monthlyPolicy == null) { - return MonthlyEarningsResponse( - workedEarnings = 0, - standardSalary = defaultSalary, - workedMinutes = 0, - standardMinutes = 0, - ) - } - - val policyDailyMinutes = SalaryCalculator.calculateWorkMinutes( - monthlyPolicy.clockInTime, monthlyPolicy.clockOutTime, - ) - - val policyWorkDayOfWeeks = monthlyPolicy.workdays.map { it.dayOfWeek }.toSet() - val workDaysInMonth = SalaryCalculator.getWorkDaysInPeriod( - start = start, - end = end.plusDays(1), - workDays = policyWorkDayOfWeeks - ) - - val standardMinutes = policyDailyMinutes * workDaysInMonth - - if (start.isAfter(today)) { - return MonthlyEarningsResponse(0, defaultSalary, 0, standardMinutes) - } - - val lastCalculableDate = minOf(end, today.minusDays(1)) - - val savedSchedulesByDate = dailyWorkScheduleRepository - .findAllByMemberIdAndDateBetween(memberId, start, lastCalculableDate) - .associateBy { it.date } - - var totalEarnings = BigDecimal.ZERO - var workedMinutes = 0L - - var date = start - while (!date.isAfter(lastCalculableDate)) { - val schedule = resolveScheduleForDate(savedSchedulesByDate[date], monthlyPolicy, date) - - if ((schedule.type == DailyWorkScheduleType.WORK || schedule.type == DailyWorkScheduleType.VACATION) - && schedule.clockIn != null && schedule.clockOut != null - ) { - workedMinutes += SalaryCalculator.calculateWorkMinutes(schedule.clockIn, schedule.clockOut) - } - - val dailyEarnings = earningsCalculator.calculateDailyEarnings( - memberId, date, monthlyPolicy, schedule.type, schedule.clockIn, schedule.clockOut, - ) - totalEarnings = totalEarnings.add(dailyEarnings ?: BigDecimal.ZERO) - - date = date.plusDays(1) - } - - return MonthlyEarningsResponse( - workedEarnings = totalEarnings.toLong(), - standardSalary = defaultSalary, - workedMinutes = workedMinutes, - standardMinutes = standardMinutes, + return createWorkdayResponse( + memberId, + date, + schedule, + resolveMonthlyRepresentativePolicyOrNull(memberId, date.year, date.monthValue), + resolvePaydayDay(memberId), ) } @@ -247,28 +291,37 @@ class WorkdayService( memberId: Long, date: LocalDate, schedule: ResolvedSchedule, + policy: WorkPolicyVersion?, + paydayDay: Int, ): WorkdayResponse { + val events = resolveDailyEvents(date, paydayDay) + if (schedule.type == DailyWorkScheduleType.NONE) { return WorkdayResponse( date = date, type = DailyWorkScheduleType.NONE, + status = DailyWorkStatusType.NONE, + events = events, dailyPay = 0, ) } - val policy = resolveMonthlyRepresentativePolicyOrNull(memberId, date.year, date.monthValue) - ?: return WorkdayResponse( - date = date, - type = schedule.type, - dailyPay = 0, - clockInTime = schedule.clockIn, - clockOutTime = schedule.clockOut, - ) + val resolvedPolicy = policy ?: return WorkdayResponse( + date = date, + type = schedule.type, + status = resolveDailyWorkStatus(date, schedule), + events = events, + dailyPay = 0, + clockInTime = schedule.clockIn, + clockOutTime = schedule.clockOut, + ) val earnings = earningsCalculator.calculateDailyEarnings( - memberId, date, policy, schedule.type, schedule.clockIn, schedule.clockOut, + memberId, date, resolvedPolicy, schedule.type, schedule.clockIn, schedule.clockOut, ) return WorkdayResponse( date = date, type = schedule.type, + status = resolveDailyWorkStatus(date, schedule), + events = events, dailyPay = earnings?.toInt() ?: 0, clockInTime = schedule.clockIn, clockOutTime = schedule.clockOut, @@ -290,6 +343,68 @@ class WorkdayService( lastDayOfMonth, ) } + + private fun resolvePaydayDay(memberId: Long): Int = + profileRepository.findByMemberId(memberId)?.paydayDay ?: throw NotFoundException() + + private fun resolveDailyEvents(date: LocalDate, paydayDay: Int): List { + val events = mutableListOf() + + if (isPayday(date, paydayDay)) { + events += DailyEventType.PAYDAY + } + + // TODO: Add holiday event resolution when holiday data is available. + + return events + } + + private fun isPayday(date: LocalDate, paydayDay: Int): Boolean { + return resolveEffectivePayday(date.year, date.monthValue, paydayDay) == date + } + + private fun resolveDailyWorkStatus( + date: LocalDate, + schedule: ResolvedSchedule, + ): DailyWorkStatusType { + if (schedule.type == DailyWorkScheduleType.NONE) return DailyWorkStatusType.NONE + + val clockIn = schedule.clockIn ?: return DailyWorkStatusType.NONE + val clockOut = schedule.clockOut ?: return DailyWorkStatusType.NONE + val now = LocalDateTime.now() + val endAt = if (clockOut.isAfter(clockIn)) { + date.atTime(clockOut) + } else { + date.plusDays(1).atTime(clockOut) + } + + return if (now.isBefore(endAt)) DailyWorkStatusType.SCHEDULED else DailyWorkStatusType.COMPLETED + } + + private fun resolveClockOutForEarnings( + targetDate: LocalDate, + today: LocalDate, + now: LocalTime, + schedule: ResolvedSchedule, + ): LocalTime? { + if (targetDate != today || + (schedule.type != DailyWorkScheduleType.WORK && schedule.type != DailyWorkScheduleType.VACATION) + ) { + return schedule.clockOut + } + + val clockIn = schedule.clockIn ?: return schedule.clockOut + val clockOut = schedule.clockOut ?: return null + + if (now.isBefore(clockIn)) { + return null + } + + return when { + clockOut.isAfter(clockIn) -> minOf(now, clockOut) + else -> now + } + } } private data class ResolvedSchedule( diff --git a/src/main/kotlin/com/moa/service/dto/CalendarResponse.kt b/src/main/kotlin/com/moa/service/dto/CalendarResponse.kt new file mode 100644 index 0000000..6902a1b --- /dev/null +++ b/src/main/kotlin/com/moa/service/dto/CalendarResponse.kt @@ -0,0 +1,6 @@ +package com.moa.service.dto + +data class CalendarResponse( + val earnings: MonthlyEarningsResponse, + val schedules: List, +) diff --git a/src/main/kotlin/com/moa/service/dto/HomeResponse.kt b/src/main/kotlin/com/moa/service/dto/HomeResponse.kt index 9a21f81..86a1ccc 100644 --- a/src/main/kotlin/com/moa/service/dto/HomeResponse.kt +++ b/src/main/kotlin/com/moa/service/dto/HomeResponse.kt @@ -2,6 +2,7 @@ package com.moa.service.dto import com.fasterxml.jackson.annotation.JsonFormat import com.moa.entity.DailyWorkScheduleType +import com.moa.entity.DailyWorkStatusType import java.time.LocalTime data class HomeResponse( @@ -10,6 +11,7 @@ data class HomeResponse( val standardSalary: Long, val dailyPay: Int, val type: DailyWorkScheduleType, + val status: DailyWorkStatusType, @field:JsonFormat(pattern = "HH:mm") val clockInTime: LocalTime?, @field:JsonFormat(pattern = "HH:mm") diff --git a/src/main/kotlin/com/moa/service/dto/WorkdayResponse.kt b/src/main/kotlin/com/moa/service/dto/WorkdayResponse.kt index 0eba965..1cda463 100644 --- a/src/main/kotlin/com/moa/service/dto/WorkdayResponse.kt +++ b/src/main/kotlin/com/moa/service/dto/WorkdayResponse.kt @@ -1,13 +1,17 @@ package com.moa.service.dto import com.fasterxml.jackson.annotation.JsonFormat +import com.moa.entity.DailyEventType import com.moa.entity.DailyWorkScheduleType +import com.moa.entity.DailyWorkStatusType import java.time.LocalDate import java.time.LocalTime data class WorkdayResponse( val date: LocalDate, val type: DailyWorkScheduleType, + val status: DailyWorkStatusType, + val events: List = emptyList(), val dailyPay: Int, @field:JsonFormat(pattern = "HH:mm") val clockInTime: LocalTime? = null, diff --git a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt index 34eee13..93a4072 100644 --- a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt @@ -2,12 +2,12 @@ package com.moa.service.notification import com.moa.entity.* import com.moa.repository.* +import com.moa.service.resolveEffectivePayday import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate import java.time.LocalTime -import java.time.YearMonth @Service class PaydayNotificationBatchService( @@ -49,14 +49,14 @@ class PaydayNotificationBatchService( } private fun findPaydayProfiles(date: LocalDate): List { - val dayOfMonth = date.dayOfMonth - val lastDayOfMonth = YearMonth.from(date).atEndOfMonth().dayOfMonth - val paydayDays = if (dayOfMonth == lastDayOfMonth) { - (dayOfMonth..31).toList() - } else { - listOf(dayOfMonth) + val candidatePaydayDays = (1..31) + .filter { resolveEffectivePayday(date.year, date.monthValue, it) == date } + + if (candidatePaydayDays.isEmpty()) { + return emptyList() } - return profileRepository.findAllByPaydayDayIn(paydayDays) + + return profileRepository.findAllByPaydayDayIn(candidatePaydayDays) } private fun findRequiredTermCodes(): Set =