From 44c81f0bdbfbd9573f2cfef13f272fe7c724cb7c Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 01:19:54 +0900 Subject: [PATCH 1/9] Refactor DailyWorkStatusType to improve status resolution logic and add unit tests --- .../com/moa/entity/DailyWorkStatusType.kt | 50 ++++++++++++++- .../kotlin/com/moa/service/WorkdayService.kt | 39 ++++++----- .../com/moa/entity/DailyWorkStatusTypeTest.kt | 64 +++++++++++++++++++ 3 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 src/test/kotlin/com/moa/entity/DailyWorkStatusTypeTest.kt diff --git a/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt b/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt index ea20144..1a59a51 100644 --- a/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt +++ b/src/main/kotlin/com/moa/entity/DailyWorkStatusType.kt @@ -1,7 +1,55 @@ package com.moa.entity +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +/** + * 일자별 근무 진행 상태를 정의하는 열거형입니다. + */ enum class DailyWorkStatusType { + /** 근무 일정이 없거나 상태를 판정할 수 없는 경우 */ NONE, + + /** 아직 근무 종료 시각이 지나지 않은 경우 */ SCHEDULED, - COMPLETED, + + /** 근무 종료 시각이 지나 완료된 경우 */ + COMPLETED; + + companion object { + /** + * 특정 일자의 근무 스케줄 정보를 바탕으로 근무 상태를 판정합니다. + * + * 근무 유형이 [DailyWorkScheduleType.NONE]이거나, 출근/퇴근 시간 중 하나라도 없으면 + * 상태를 판정할 수 없으므로 [NONE]을 반환합니다. + * 퇴근 시간이 출근 시간보다 이른 경우에는 자정을 넘기는 근무로 간주하여 익일 종료 시각 기준으로 계산합니다. + * + * @param date 근무 상태를 판정할 기준 일자 + * @param scheduleType 해당 일자의 근무 일정 유형 + * @param clockIn 출근 시간 + * @param clockOut 퇴근 시간 + * @param now 상태 판정에 사용할 현재 시각. 기본값은 [LocalDateTime.now] 입니다. + * @return 판정된 [DailyWorkStatusType]. 종료 시각 이전이면 [SCHEDULED], 이후면 [COMPLETED]를 반환합니다. + */ + fun resolve( + date: LocalDate, + scheduleType: DailyWorkScheduleType, + clockIn: LocalTime?, + clockOut: LocalTime?, + now: LocalDateTime = LocalDateTime.now(), + ): DailyWorkStatusType { + if (scheduleType == DailyWorkScheduleType.NONE) return NONE + + val resolvedClockIn = clockIn ?: return NONE + val resolvedClockOut = clockOut ?: return NONE + val endAt = if (!resolvedClockOut.isBefore(resolvedClockIn)) { + date.atTime(resolvedClockOut) + } else { + date.plusDays(1).atTime(resolvedClockOut) + } + + return if (now.isBefore(endAt)) SCHEDULED else COMPLETED + } + } } diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 382383f..5b25ec4 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -99,7 +99,12 @@ class WorkdayService( var date = start while (!date.isAfter(lastCalculableDate)) { val schedule = resolveScheduleForDate(savedSchedulesByDate[date], monthlyPolicy, date) - val status = resolveDailyWorkStatus(date, schedule) + val status = DailyWorkStatusType.resolve( + date = date, + scheduleType = schedule.type, + clockIn = schedule.clockIn, + clockOut = schedule.clockOut, + ) val adjustedClockOut = resolveClockOutForEarnings(date, today, now, schedule) val isCompletedWork = status == DailyWorkStatusType.COMPLETED && (schedule.type == DailyWorkScheduleType.WORK || schedule.type == DailyWorkScheduleType.VACATION) @@ -308,7 +313,12 @@ class WorkdayService( val resolvedPolicy = policy ?: return WorkdayResponse( date = date, type = schedule.type, - status = resolveDailyWorkStatus(date, schedule), + status = DailyWorkStatusType.resolve( + date = date, + scheduleType = schedule.type, + clockIn = schedule.clockIn, + clockOut = schedule.clockOut, + ), events = events, dailyPay = 0, clockInTime = schedule.clockIn, @@ -320,7 +330,12 @@ class WorkdayService( return WorkdayResponse( date = date, type = schedule.type, - status = resolveDailyWorkStatus(date, schedule), + status = DailyWorkStatusType.resolve( + date = date, + scheduleType = schedule.type, + clockIn = schedule.clockIn, + clockOut = schedule.clockOut, + ), events = events, dailyPay = earnings?.toInt() ?: 0, clockInTime = schedule.clockIn, @@ -363,24 +378,6 @@ class WorkdayService( 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.isBefore(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, diff --git a/src/test/kotlin/com/moa/entity/DailyWorkStatusTypeTest.kt b/src/test/kotlin/com/moa/entity/DailyWorkStatusTypeTest.kt new file mode 100644 index 0000000..18537b2 --- /dev/null +++ b/src/test/kotlin/com/moa/entity/DailyWorkStatusTypeTest.kt @@ -0,0 +1,64 @@ +package com.moa.entity + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +class DailyWorkStatusTypeTest { + + private val date = LocalDate.of(2025, 6, 9) + + @Test + fun `근무 유형이 NONE이면 NONE을 반환한다`() { + val result = DailyWorkStatusType.resolve( + date = date, + scheduleType = DailyWorkScheduleType.NONE, + clockIn = LocalTime.of(9, 0), + clockOut = LocalTime.of(18, 0), + now = date.atTime(12, 0), + ) + + assertThat(result).isEqualTo(DailyWorkStatusType.NONE) + } + + @Test + fun `종료 시각 전이면 SCHEDULED를 반환한다`() { + val result = DailyWorkStatusType.resolve( + date = date, + scheduleType = DailyWorkScheduleType.WORK, + clockIn = LocalTime.of(9, 0), + clockOut = LocalTime.of(18, 0), + now = date.atTime(17, 59), + ) + + assertThat(result).isEqualTo(DailyWorkStatusType.SCHEDULED) + } + + @Test + fun `종료 시각 이후면 COMPLETED를 반환한다`() { + val result = DailyWorkStatusType.resolve( + date = date, + scheduleType = DailyWorkScheduleType.WORK, + clockIn = LocalTime.of(9, 0), + clockOut = LocalTime.of(18, 0), + now = date.atTime(18, 0), + ) + + assertThat(result).isEqualTo(DailyWorkStatusType.COMPLETED) + } + + @Test + fun `자정 넘김 근무는 익일 종료 시각 기준으로 판정한다`() { + val result = DailyWorkStatusType.resolve( + date = date, + scheduleType = DailyWorkScheduleType.WORK, + clockIn = LocalTime.of(22, 0), + clockOut = LocalTime.of(2, 0), + now = LocalDateTime.of(2025, 6, 10, 1, 0), + ) + + assertThat(result).isEqualTo(DailyWorkStatusType.SCHEDULED) + } +} From 6e6122bbbb7d7583a8e4d65bf33f53af0f64d41a Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 01:24:30 +0900 Subject: [PATCH 2/9] Refactor salary calculation logic by introducing SalaryCalculator service and updating related components --- src/main/kotlin/com/moa/entity/SalaryType.kt | 100 ----------------- .../com/moa/service/EarningsCalculator.kt | 10 +- .../com/moa/service/SalaryCalculator.kt | 104 ++++++++++++++++++ .../kotlin/com/moa/service/WorkdayService.kt | 8 +- .../com/moa/entity/SalaryCalculatorTest.kt | 34 +++--- .../com/moa/service/EarningsCalculatorTest.kt | 3 +- 6 files changed, 133 insertions(+), 126 deletions(-) create mode 100644 src/main/kotlin/com/moa/service/SalaryCalculator.kt diff --git a/src/main/kotlin/com/moa/entity/SalaryType.kt b/src/main/kotlin/com/moa/entity/SalaryType.kt index 8383572..53f85ce 100644 --- a/src/main/kotlin/com/moa/entity/SalaryType.kt +++ b/src/main/kotlin/com/moa/entity/SalaryType.kt @@ -1,9 +1,5 @@ package com.moa.entity -import java.math.BigDecimal -import java.math.RoundingMode -import java.time.* - /** * 급여 산정 방식을 정의하는 열거형입니다. */ @@ -27,99 +23,3 @@ enum class SalaryType { } } } - -/** - * 급여, 일급, 근로 시간 등을 계산하는 순수 유틸리티 객체입니다. - * * 외부 상태를 가지지 않으며, 제공된 파라미터만을 기반으로 계산을 수행합니다. - */ -object SalaryCalculator { - - /** - * 특정 일자가 속한 달의 일일 급여(일급)를 계산합니다. - * - * 이 메서드는 직원의 월 기본급을 해당 월의 '총 소정 근로일수'로 나누어 일급을 산출합니다. - * 연봉([SalaryType.YEARLY])인 경우 금액을 12로 나누어 월 기본급을 먼저 구합니다. - * 최종 산출된 일급은 소수점 첫째 자리에서 반올림([RoundingMode.HALF_UP]) 처리됩니다. - * - * @param targetDate 기준 일자 (이 일자가 속한 월을 기준으로 총 근로일수를 계산합니다) - * @param salaryType 급여 산정 방식 (연봉 또는 월급) - * @param salaryAmount 책정된 급여 금액 - * @param workDays 직원의 주간 근무 요일 집합 (예: 월, 수, 금) - * @return 계산된 일급 금액. 해당 월에 근무일이 전혀 없는 경우 `0`을 반환합니다. - */ - fun calculateDailyRate( - targetDate: LocalDate, - salaryType: SalaryType, - salaryAmount: Long, - workDays: Set - ): BigDecimal { - val monthlySalary = when (salaryType) { - SalaryType.YEARLY -> salaryAmount.toBigDecimal().divide(BigDecimal(12), 0, RoundingMode.HALF_UP) - SalaryType.MONTHLY -> salaryAmount.toBigDecimal() - } - - val yearMonth = YearMonth.from(targetDate) - val periodStart = yearMonth.atDay(1) - val periodEnd = yearMonth.atEndOfMonth().plusDays(1) // exclusive - - val workDaysCount = getWorkDaysInPeriod(periodStart, periodEnd, workDays) - - if (workDaysCount == 0) return BigDecimal.ZERO - - return monthlySalary.divide(BigDecimal(workDaysCount), 0, RoundingMode.HALF_UP) - } - - /** - * 출근 시간과 퇴근 시간 사이의 총 근무 시간을 분(minute) 단위로 계산합니다. - * - * 퇴근 시간이 출근 시간보다 빠른 경우에만, 익일 퇴근(야간 교대근무 등)으로 간주하여 - * 자동으로 24시간(1440분)을 더하여 계산합니다. - * 출근 시간과 퇴근 시간이 같으면 0분으로 계산합니다. - * - * @param clockIn 출근 시간 - * @param clockOut 퇴근 시간 - * @return 총 근무 시간 (분 단위) - */ - fun calculateWorkMinutes(clockIn: LocalTime, clockOut: LocalTime): Long { - val minutes = Duration.between(clockIn, clockOut).toMinutes() - return if (minutes < 0) minutes + 24 * 60 else minutes - } - - /** - * 일급과 실제 근무 시간을 바탕으로 최종 발생 소득을 계산합니다. - * - * 소정 근로 시간 대비 실제 근로 시간의 비율을 일급에 곱하여 산출합니다. - * 계산의 정확도를 위해 분당 급여율 산출 시 소수점 10자리까지 유지하며, - * 최종 결과값에서 소수점 첫째 자리에서 반올림([RoundingMode.HALF_UP])하여 정수 단위로 맞춥니다. - * - * @param dailyRate 산정된 기준 일급 - * @param policyWorkMinutes 정책상 소정 근로 시간 (분 단위) - * @param actualWorkMinutes 실제 근로 시간 (분 단위) - * @return 계산된 최종 소득 금액. 정책상 근로 시간이 0 이하로 잘못 설정된 경우, 보정 없이 기준 일급을 그대로 반환합니다. - */ - fun calculateEarnings( - dailyRate: BigDecimal, - policyWorkMinutes: Long, - actualWorkMinutes: Long, - ): BigDecimal { - if (policyWorkMinutes <= 0) return dailyRate - val minuteRate = dailyRate.divide(BigDecimal(policyWorkMinutes), 10, RoundingMode.HALF_UP) - return minuteRate.multiply(BigDecimal(actualWorkMinutes)).setScale(0, RoundingMode.HALF_UP) - } - - /** - * 특정 기간 내에 포함된 실제 근무일(조건에 맞는 요일)의 총 일수를 계산합니다. - * - * @param start 계산 시작 일자 (포함) - * @param end 계산 종료 일자 (제외, Exclusive) - * @param workDays 포함시킬 근무 요일 집합 - * @return 기간 내 포함된 근무일의 총 개수 - */ - fun getWorkDaysInPeriod( - start: LocalDate, - end: LocalDate, - workDays: Set - ): Int = generateSequence(start) { it.plusDays(1) } - .takeWhile { it.isBefore(end) } - .count { it.dayOfWeek in workDays } -} diff --git a/src/main/kotlin/com/moa/service/EarningsCalculator.kt b/src/main/kotlin/com/moa/service/EarningsCalculator.kt index 8cc3b10..9ddddbb 100644 --- a/src/main/kotlin/com/moa/service/EarningsCalculator.kt +++ b/src/main/kotlin/com/moa/service/EarningsCalculator.kt @@ -1,7 +1,6 @@ package com.moa.service import com.moa.entity.DailyWorkScheduleType -import com.moa.entity.SalaryCalculator import com.moa.entity.SalaryType import com.moa.entity.WorkPolicyVersion import com.moa.repository.PayrollVersionRepository @@ -19,6 +18,7 @@ import java.time.YearMonth @Service class EarningsCalculator( private val payrollVersionRepository: PayrollVersionRepository, + private val salaryCalculator: SalaryCalculator, ) { /** * 지정된 날짜가 속한 월을 기준으로 직원의 기본 월급을 계산합니다. @@ -80,7 +80,7 @@ class EarningsCalculator( memberId, lastDayOfMonth, ) ?: return null - val dailyRate = SalaryCalculator.calculateDailyRate( + val dailyRate = salaryCalculator.calculateDailyRate( targetDate = date, salaryType = SalaryType.from(payroll.salaryInputType), salaryAmount = payroll.salaryAmount, @@ -89,11 +89,11 @@ class EarningsCalculator( if (dailyRate == BigDecimal.ZERO) return dailyRate if (clockInTime != null && clockOutTime != null) { - val policyMinutes = SalaryCalculator.calculateWorkMinutes( + val policyMinutes = salaryCalculator.calculateWorkMinutes( policy.clockInTime, policy.clockOutTime, ) - val actualMinutes = SalaryCalculator.calculateWorkMinutes(clockInTime, clockOutTime) - return SalaryCalculator.calculateEarnings(dailyRate, policyMinutes, actualMinutes) + val actualMinutes = salaryCalculator.calculateWorkMinutes(clockInTime, clockOutTime) + return salaryCalculator.calculateEarnings(dailyRate, policyMinutes, actualMinutes) } return dailyRate diff --git a/src/main/kotlin/com/moa/service/SalaryCalculator.kt b/src/main/kotlin/com/moa/service/SalaryCalculator.kt new file mode 100644 index 0000000..3dedd81 --- /dev/null +++ b/src/main/kotlin/com/moa/service/SalaryCalculator.kt @@ -0,0 +1,104 @@ +package com.moa.service + +import com.moa.entity.SalaryType +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.* + +/** + * 급여, 일급, 근로 시간 등을 계산하는 서비스입니다. + * * 외부 상태를 가지지 않지만, 계산 책임을 애플리케이션 서비스로 통일하기 위해 Spring Bean으로 관리합니다. + */ +@Service +class SalaryCalculator { + + /** + * 특정 일자가 속한 달의 일일 급여(일급)를 계산합니다. + * + * 이 메서드는 직원의 월 기본급을 해당 월의 '총 소정 근로일수'로 나누어 일급을 산출합니다. + * 연봉([com.moa.entity.SalaryType.YEARLY])인 경우 금액을 12로 나누어 월 기본급을 먼저 구합니다. + * 최종 산출된 일급은 소수점 첫째 자리에서 반올림([java.math.RoundingMode.HALF_UP]) 처리됩니다. + * + * @param targetDate 기준 일자 (이 일자가 속한 월을 기준으로 총 근로일수를 계산합니다) + * @param salaryType 급여 산정 방식 (연봉 또는 월급) + * @param salaryAmount 책정된 급여 금액 + * @param workDays 직원의 주간 근무 요일 집합 (예: 월, 수, 금) + * @return 계산된 일급 금액. 해당 월에 근무일이 전혀 없는 경우 `0`을 반환합니다. + */ + fun calculateDailyRate( + targetDate: LocalDate, + salaryType: SalaryType, + salaryAmount: Long, + workDays: Set + ): BigDecimal { + val monthlySalary = when (salaryType) { + SalaryType.YEARLY -> salaryAmount.toBigDecimal().divide(BigDecimal(12), 0, RoundingMode.HALF_UP) + SalaryType.MONTHLY -> salaryAmount.toBigDecimal() + } + + val yearMonth = YearMonth.from(targetDate) + val periodStart = yearMonth.atDay(1) + val periodEnd = yearMonth.atEndOfMonth().plusDays(1) + + val workDaysCount = getWorkDaysInPeriod(periodStart, periodEnd, workDays) + + if (workDaysCount == 0) return BigDecimal.ZERO + + return monthlySalary.divide(BigDecimal(workDaysCount), 0, RoundingMode.HALF_UP) + } + + /** + * 출근 시간과 퇴근 시간 사이의 총 근무 시간을 분(minute) 단위로 계산합니다. + * + * 퇴근 시간이 출근 시간보다 빠른 경우에만, 익일 퇴근(야간 교대근무 등)으로 간주하여 + * 자동으로 24시간(1440분)을 더하여 계산합니다. + * 출근 시간과 퇴근 시간이 같으면 0분으로 계산합니다. + * + * @param clockIn 출근 시간 + * @param clockOut 퇴근 시간 + * @return 총 근무 시간 (분 단위) + */ + fun calculateWorkMinutes(clockIn: LocalTime, clockOut: LocalTime): Long { + val minutes = Duration.between(clockIn, clockOut).toMinutes() + return if (minutes < 0) minutes + 24 * 60 else minutes + } + + /** + * 일급과 실제 근무 시간을 바탕으로 최종 발생 소득을 계산합니다. + * + * 소정 근로 시간 대비 실제 근로 시간의 비율을 일급에 곱하여 산출합니다. + * 계산의 정확도를 위해 분당 급여율 산출 시 소수점 10자리까지 유지하며, + * 최종 결과값에서 소수점 첫째 자리에서 반올림([RoundingMode.HALF_UP])하여 정수 단위로 맞춥니다. + * + * @param dailyRate 산정된 기준 일급 + * @param policyWorkMinutes 정책상 소정 근로 시간 (분 단위) + * @param actualWorkMinutes 실제 근로 시간 (분 단위) + * @return 계산된 최종 소득 금액. 정책상 근로 시간이 0 이하로 잘못 설정된 경우, 보정 없이 기준 일급을 그대로 반환합니다. + */ + fun calculateEarnings( + dailyRate: BigDecimal, + policyWorkMinutes: Long, + actualWorkMinutes: Long, + ): BigDecimal { + if (policyWorkMinutes <= 0) return dailyRate + val minuteRate = dailyRate.divide(BigDecimal(policyWorkMinutes), 10, RoundingMode.HALF_UP) + return minuteRate.multiply(BigDecimal(actualWorkMinutes)).setScale(0, RoundingMode.HALF_UP) + } + + /** + * 특정 기간 내에 포함된 실제 근무일(조건에 맞는 요일)의 총 일수를 계산합니다. + * + * @param start 계산 시작 일자 (포함) + * @param end 계산 종료 일자 (제외, Exclusive) + * @param workDays 포함시킬 근무 요일 집합 + * @return 기간 내 포함된 근무일의 총 개수 + */ + fun getWorkDaysInPeriod( + start: LocalDate, + end: LocalDate, + workDays: Set + ): Int = generateSequence(start) { it.plusDays(1) } + .takeWhile { it.isBefore(end) } + .count { it.dayOfWeek in workDays } +} diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 5b25ec4..18a1a29 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -13,7 +13,6 @@ 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 @@ -23,6 +22,7 @@ class WorkdayService( private val profileRepository: ProfileRepository, private val notificationSyncService: NotificationSyncService, private val earningsCalculator: EarningsCalculator, + private val salaryCalculator: SalaryCalculator, ) { @Transactional(readOnly = true) @@ -69,12 +69,12 @@ class WorkdayService( ) } - val policyDailyMinutes = SalaryCalculator.calculateWorkMinutes( + val policyDailyMinutes = salaryCalculator.calculateWorkMinutes( monthlyPolicy.clockInTime, monthlyPolicy.clockOutTime, ) val policyWorkDayOfWeeks = monthlyPolicy.workdays.map { it.dayOfWeek }.toSet() - val workDaysInMonth = SalaryCalculator.getWorkDaysInPeriod( + val workDaysInMonth = salaryCalculator.getWorkDaysInPeriod( start = start, end = end.plusDays(1), workDays = policyWorkDayOfWeeks @@ -110,7 +110,7 @@ class WorkdayService( (schedule.type == DailyWorkScheduleType.WORK || schedule.type == DailyWorkScheduleType.VACATION) if (isCompletedWork && schedule.clockIn != null && adjustedClockOut != null) { - workedMinutes += SalaryCalculator.calculateWorkMinutes(schedule.clockIn, adjustedClockOut) + workedMinutes += salaryCalculator.calculateWorkMinutes(schedule.clockIn, adjustedClockOut) } if (isCompletedWork) { diff --git a/src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt b/src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt index 6dbc380..60fa3aa 100644 --- a/src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt +++ b/src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt @@ -1,5 +1,6 @@ package com.moa.entity +import com.moa.service.SalaryCalculator import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.math.BigDecimal @@ -9,6 +10,7 @@ import java.time.LocalDate import java.time.LocalTime class SalaryCalculatorTest { + private val salaryCalculator = SalaryCalculator() companion object { private val WEEKDAYS = setOf( @@ -25,7 +27,7 @@ class SalaryCalculatorTest { val targetDate = LocalDate.of(2025, 2, 3) val monthlySalary = 3_000_000L - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.MONTHLY, salaryAmount = monthlySalary, @@ -44,7 +46,7 @@ class SalaryCalculatorTest { val targetDate = LocalDate.of(2025, 2, 3) val yearlySalary = 36_000_000L - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.YEARLY, salaryAmount = yearlySalary, @@ -63,7 +65,7 @@ class SalaryCalculatorTest { // 2025년 3월: 평일 21일 val targetDate = LocalDate.of(2025, 3, 10) - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.MONTHLY, salaryAmount = 2_100_000L, @@ -79,7 +81,7 @@ class SalaryCalculatorTest { // 2025년 1월: 평일 23일 val targetDate = LocalDate.of(2025, 1, 15) - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.MONTHLY, salaryAmount = 2_300_000L, @@ -97,7 +99,7 @@ class SalaryCalculatorTest { // 2025년 2월 (비윤년): 평일 20일 val targetDate = LocalDate.of(2025, 2, 10) - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.MONTHLY, salaryAmount = 2_000_000L, @@ -113,7 +115,7 @@ class SalaryCalculatorTest { // 2024년 2월 (윤년): 평일 21일 val targetDate = LocalDate.of(2024, 2, 10) - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.MONTHLY, salaryAmount = 2_100_000L, @@ -132,7 +134,7 @@ class SalaryCalculatorTest { // 2025년 2월: 월~토 = 24일 val targetDate = LocalDate.of(2025, 2, 3) - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, salaryType = SalaryType.MONTHLY, salaryAmount = 2_400_000L, @@ -147,7 +149,7 @@ class SalaryCalculatorTest { @Test fun `근무요일이 없으면 일급은 0을 반환한다`() { - val result = SalaryCalculator.calculateDailyRate( + val result = salaryCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 2, 3), salaryType = SalaryType.MONTHLY, salaryAmount = 3_000_000L, @@ -161,7 +163,7 @@ class SalaryCalculatorTest { @Test fun `calculateWorkMinutes - 9시에서 18시는 540분을 반환한다`() { - val result = SalaryCalculator.calculateWorkMinutes( + val result = salaryCalculator.calculateWorkMinutes( LocalTime.of(9, 0), LocalTime.of(18, 0), ) @@ -170,7 +172,7 @@ class SalaryCalculatorTest { @Test fun `calculateWorkMinutes - 자정넘김 22시에서 2시는 240분을 반환한다`() { - val result = SalaryCalculator.calculateWorkMinutes( + val result = salaryCalculator.calculateWorkMinutes( LocalTime.of(22, 0), LocalTime.of(2, 0), ) @@ -179,7 +181,7 @@ class SalaryCalculatorTest { @Test fun `calculateWorkMinutes - 시작시간과 종료시간이 같으면 0분을 반환한다`() { - val result = SalaryCalculator.calculateWorkMinutes( + val result = salaryCalculator.calculateWorkMinutes( LocalTime.of(9, 0), LocalTime.of(9, 0), ) @@ -191,7 +193,7 @@ class SalaryCalculatorTest { @Test fun `calculateEarnings - 실제 근무시간이 정책과 같으면 일급과 동일한 금액을 반환한다`() { val dailyRate = BigDecimal(150_000) - val result = SalaryCalculator.calculateEarnings(dailyRate, 540, 540) + val result = salaryCalculator.calculateEarnings(dailyRate, 540, 540) assertThat(result).isEqualByComparingTo(dailyRate) } @@ -199,7 +201,7 @@ class SalaryCalculatorTest { fun `calculateEarnings - 초과 근무시 분급 기준으로 증가된 금액을 반환한다`() { val dailyRate = BigDecimal(150_000) // 540분 정책, 600분 실제 (1시간 초과) - val result = SalaryCalculator.calculateEarnings(dailyRate, 540, 600) + val result = salaryCalculator.calculateEarnings(dailyRate, 540, 600) assertThat(result).isGreaterThan(dailyRate) } @@ -207,7 +209,7 @@ class SalaryCalculatorTest { fun `calculateEarnings - 조기 퇴근시 분급 기준으로 감소된 금액을 반환한다`() { val dailyRate = BigDecimal(150_000) // 540분 정책, 480분 실제 (1시간 조기 퇴근) - val result = SalaryCalculator.calculateEarnings(dailyRate, 540, 480) + val result = salaryCalculator.calculateEarnings(dailyRate, 540, 480) assertThat(result).isLessThan(dailyRate) } @@ -215,14 +217,14 @@ class SalaryCalculatorTest { fun `같은 월급이라도 월마다 근무일수에 따라 일급이 달라진다`() { val salary = 3_000_000L - val febResult = SalaryCalculator.calculateDailyRate( + val febResult = salaryCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 2, 1), salaryType = SalaryType.MONTHLY, salaryAmount = salary, workDays = WEEKDAYS, ) - val marResult = SalaryCalculator.calculateDailyRate( + val marResult = salaryCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 3, 1), salaryType = SalaryType.MONTHLY, salaryAmount = salary, diff --git a/src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt b/src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt index beb021a..94805b5 100644 --- a/src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt @@ -16,7 +16,8 @@ import java.time.LocalTime class EarningsCalculatorTest { private val payrollVersionRepository: PayrollVersionRepository = mockk() - private val sut = EarningsCalculator(payrollVersionRepository) + private val salaryCalculator = SalaryCalculator() + private val sut = EarningsCalculator(payrollVersionRepository, salaryCalculator) companion object { private const val MEMBER_ID = 1L From 7d0401c7e41fa2aa1e6d0841434b393fa77b9d04 Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 01:44:18 +0900 Subject: [PATCH 3/9] Refactor DailyEventType and related services to integrate PaydayDay value object and enhance payday resolution logic --- .../kotlin/com/moa/entity/DailyEventType.kt | 32 +++++++++- src/main/kotlin/com/moa/entity/PaydayDay.kt | 64 +++++++++++++++++++ src/main/kotlin/com/moa/entity/Profile.kt | 4 +- .../com/moa/repository/ProfileRepository.kt | 3 +- .../moa/service/OnboardingStatusService.kt | 2 +- .../kotlin/com/moa/service/PaydayResolver.kt | 17 ----- .../kotlin/com/moa/service/ProfileService.kt | 21 +++--- .../kotlin/com/moa/service/WorkdayService.kt | 22 +------ .../PaydayNotificationBatchService.kt | 4 +- .../com/moa/entity/DailyEventTypeTest.kt | 58 +++++++++++++++++ .../kotlin/com/moa/entity/PaydayDayTest.kt | 40 ++++++++++++ .../SalaryCalculatorTest.kt | 36 +++++------ 12 files changed, 230 insertions(+), 73 deletions(-) create mode 100644 src/main/kotlin/com/moa/entity/PaydayDay.kt delete mode 100644 src/main/kotlin/com/moa/service/PaydayResolver.kt create mode 100644 src/test/kotlin/com/moa/entity/DailyEventTypeTest.kt create mode 100644 src/test/kotlin/com/moa/entity/PaydayDayTest.kt rename src/test/kotlin/com/moa/{entity => service}/SalaryCalculatorTest.kt (86%) diff --git a/src/main/kotlin/com/moa/entity/DailyEventType.kt b/src/main/kotlin/com/moa/entity/DailyEventType.kt index 897acdb..791376b 100644 --- a/src/main/kotlin/com/moa/entity/DailyEventType.kt +++ b/src/main/kotlin/com/moa/entity/DailyEventType.kt @@ -1,6 +1,34 @@ package com.moa.entity +import java.time.LocalDate + +/** + * 일자별로 표시할 부가 이벤트를 정의하는 열거형입니다. + */ enum class DailyEventType { - PAYDAY, - HOLIDAY, + /** 급여일에 해당하는 경우 */ + PAYDAY; + + companion object { + /** + * 특정 일자와 급여일 설정을 바탕으로 해당 일자에 표시할 이벤트를 판정합니다. + * + * 현재는 급여일([PAYDAY])만 지원하며, 추후 공휴일 등 다른 이벤트가 추가될 수 있습니다. + * + * @param date 이벤트를 판정할 기준 일자 + * @param paydayDay 사용자 설정 급여일 + * @return 해당 일자에 적용되는 [DailyEventType] 목록 + */ + fun resolve(date: LocalDate, paydayDay: PaydayDay): List { + val events = mutableListOf() + + if (isPayday(date, paydayDay)) { + events += PAYDAY + } + + return events + } + + private fun isPayday(date: LocalDate, paydayDay: PaydayDay): Boolean = paydayDay.isPayday(date) + } } diff --git a/src/main/kotlin/com/moa/entity/PaydayDay.kt b/src/main/kotlin/com/moa/entity/PaydayDay.kt new file mode 100644 index 0000000..6d1a5ef --- /dev/null +++ b/src/main/kotlin/com/moa/entity/PaydayDay.kt @@ -0,0 +1,64 @@ +package com.moa.entity + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth + +/** + * 사용자가 설정한 급여일을 표현하는 Value Object 입니다. + * + * 급여일은 1일부터 31일 사이의 값만 허용하며, + * 실제 급여일 계산 시 월말 보정 및 주말 보정 규칙을 함께 제공합니다. + */ +@Embeddable +data class PaydayDay( + @field:Column(name = "payday_day", nullable = false) + var value: Int = 25, +) { + init { + require(value in 1..31) { "paydayDay must be between 1 and 31" } + } + + /** + * 특정 연월에 적용되는 실제 급여일을 계산합니다. + * + * 설정한 급여일이 해당 월에 없으면 말일로 보정하고, + * 보정된 날짜가 주말이면 직전 금요일로 당깁니다. + * + * @param year 급여일을 계산할 연도 + * @param month 급여일을 계산할 월 + * @return 실제 적용되는 급여일 + */ + fun resolveEffectiveDate(year: Int, month: Int): LocalDate { + val yearMonth = YearMonth.of(year, month) + val baseDate = yearMonth.atDay(minOf(value, yearMonth.lengthOfMonth())) + + return when (baseDate.dayOfWeek) { + DayOfWeek.SATURDAY -> baseDate.minusDays(1) + DayOfWeek.SUNDAY -> baseDate.minusDays(2) + else -> baseDate + } + } + + /** + * 특정 날짜가 이 급여일 설정의 실제 급여일인지 판정합니다. + * + * @param date 판정할 날짜 + * @return 해당 날짜가 실제 급여일이면 `true` + */ + fun isPayday(date: LocalDate): Boolean = + resolveEffectiveDate(date.year, date.monthValue) == date + + companion object { + /** + * 특정 날짜에 실제 급여일로 귀결되는 모든 급여일 설정 값을 반환합니다. + * + * @param date 기준 날짜 + * @return 해당 날짜를 실제 급여일로 가지는 [PaydayDay] 집합 + */ + fun resolvingTo(date: LocalDate): Set = + (1..31).map(::PaydayDay).filterTo(mutableSetOf()) { it.isPayday(date) } + } +} diff --git a/src/main/kotlin/com/moa/entity/Profile.kt b/src/main/kotlin/com/moa/entity/Profile.kt index 086d14d..903319c 100644 --- a/src/main/kotlin/com/moa/entity/Profile.kt +++ b/src/main/kotlin/com/moa/entity/Profile.kt @@ -13,8 +13,8 @@ class Profile( @Column(nullable = true) var workplace: String? = null, - @Column(nullable = false) - var paydayDay: Int = 25, + @Embedded + var paydayDay: PaydayDay = PaydayDay(), ) : BaseEntity() { @Id diff --git a/src/main/kotlin/com/moa/repository/ProfileRepository.kt b/src/main/kotlin/com/moa/repository/ProfileRepository.kt index 6764dc8..9554ce1 100644 --- a/src/main/kotlin/com/moa/repository/ProfileRepository.kt +++ b/src/main/kotlin/com/moa/repository/ProfileRepository.kt @@ -1,9 +1,10 @@ package com.moa.repository import com.moa.entity.Profile +import com.moa.entity.PaydayDay import org.springframework.data.jpa.repository.JpaRepository interface ProfileRepository : JpaRepository { fun findByMemberId(memberId: Long): Profile? - fun findAllByPaydayDayIn(paydayDays: Collection): List + fun findAllByPaydayDayIn(paydayDays: Collection): List } diff --git a/src/main/kotlin/com/moa/service/OnboardingStatusService.kt b/src/main/kotlin/com/moa/service/OnboardingStatusService.kt index 15b3207..60e4480 100644 --- a/src/main/kotlin/com/moa/service/OnboardingStatusService.kt +++ b/src/main/kotlin/com/moa/service/OnboardingStatusService.kt @@ -27,7 +27,7 @@ class OnboardingStatusService( ProfileResponse( nickname = it.nickname, workplace = it.workplace, - paydayDay = it.paydayDay, + paydayDay = it.paydayDay.value, ) } diff --git a/src/main/kotlin/com/moa/service/PaydayResolver.kt b/src/main/kotlin/com/moa/service/PaydayResolver.kt deleted file mode 100644 index 51bfb38..0000000 --- a/src/main/kotlin/com/moa/service/PaydayResolver.kt +++ /dev/null @@ -1,17 +0,0 @@ -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/ProfileService.kt b/src/main/kotlin/com/moa/service/ProfileService.kt index 31a7bae..00b3e22 100644 --- a/src/main/kotlin/com/moa/service/ProfileService.kt +++ b/src/main/kotlin/com/moa/service/ProfileService.kt @@ -3,6 +3,7 @@ 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.PaydayDay import com.moa.entity.Profile import com.moa.repository.ProfileRepository import com.moa.service.dto.NicknameUpdateRequest @@ -25,7 +26,7 @@ class ProfileService( return ProfileResponse( nickname = profile.nickname, workplace = profile.workplace, - paydayDay = profile.paydayDay, + paydayDay = profile.paydayDay.value, ) } @@ -45,7 +46,7 @@ class ProfileService( return ProfileResponse( nickname = profile.nickname, workplace = profile.workplace, - paydayDay = profile.paydayDay, + paydayDay = profile.paydayDay.value, ) } @@ -59,7 +60,7 @@ class ProfileService( return ProfileResponse( nickname = profile.nickname, workplace = profile.workplace, - paydayDay = profile.paydayDay, + paydayDay = profile.paydayDay.value, ) } @@ -73,25 +74,25 @@ class ProfileService( return ProfileResponse( nickname = profile.nickname, workplace = profile.workplace, - paydayDay = profile.paydayDay, + paydayDay = profile.paydayDay.value, ) } @Transactional fun updatePayday(memberId: Long, req: PaydayUpdateRequest): ProfileResponse { - if (req.paydayDay !in 1..31) { - throw BadRequestException(ErrorCode.INVALID_PAYDAY_INPUT) - } - val profile = profileRepository.findByMemberId(memberId) ?: throw NotFoundException() - profile.paydayDay = req.paydayDay + profile.paydayDay = try { + PaydayDay(req.paydayDay) + } catch (_: IllegalArgumentException) { + throw BadRequestException(ErrorCode.INVALID_PAYDAY_INPUT) + } return ProfileResponse( nickname = profile.nickname, workplace = profile.workplace, - paydayDay = profile.paydayDay, + paydayDay = profile.paydayDay.value, ) } } diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 18a1a29..31ebce1 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -297,9 +297,9 @@ class WorkdayService( date: LocalDate, schedule: ResolvedSchedule, policy: WorkPolicyVersion?, - paydayDay: Int, + paydayDay: PaydayDay, ): WorkdayResponse { - val events = resolveDailyEvents(date, paydayDay) + val events = DailyEventType.resolve(date, paydayDay) if (schedule.type == DailyWorkScheduleType.NONE) { return WorkdayResponse( @@ -359,25 +359,9 @@ class WorkdayService( ) } - private fun resolvePaydayDay(memberId: Long): Int = + private fun resolvePaydayDay(memberId: Long): PaydayDay = 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 resolveClockOutForEarnings( targetDate: LocalDate, today: LocalDate, diff --git a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt index 93a4072..b302ec1 100644 --- a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt @@ -2,7 +2,6 @@ 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 @@ -49,8 +48,7 @@ class PaydayNotificationBatchService( } private fun findPaydayProfiles(date: LocalDate): List { - val candidatePaydayDays = (1..31) - .filter { resolveEffectivePayday(date.year, date.monthValue, it) == date } + val candidatePaydayDays = PaydayDay.resolvingTo(date) if (candidatePaydayDays.isEmpty()) { return emptyList() diff --git a/src/test/kotlin/com/moa/entity/DailyEventTypeTest.kt b/src/test/kotlin/com/moa/entity/DailyEventTypeTest.kt new file mode 100644 index 0000000..4d8a208 --- /dev/null +++ b/src/test/kotlin/com/moa/entity/DailyEventTypeTest.kt @@ -0,0 +1,58 @@ +package com.moa.entity + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class DailyEventTypeTest { + + @Test + fun `급여일과 같은 날짜면 PAYDAY 이벤트를 반환한다`() { + val result = DailyEventType.resolve( + date = LocalDate.of(2025, 6, 25), + paydayDay = PaydayDay(25), + ) + + assertThat(result).containsExactly(DailyEventType.PAYDAY) + } + + @Test + fun `급여일과 다른 날짜면 빈 이벤트 목록을 반환한다`() { + val result = DailyEventType.resolve( + date = LocalDate.of(2025, 6, 24), + paydayDay = PaydayDay(25), + ) + + assertThat(result).isEmpty() + } + + @Test + fun `설정한 급여일이 말일보다 크면 말일을 급여일로 간주한다`() { + val result = DailyEventType.resolve( + date = LocalDate.of(2025, 2, 28), + paydayDay = PaydayDay(31), + ) + + assertThat(result).containsExactly(DailyEventType.PAYDAY) + } + + @Test + fun `말일 보정이 적용된 달에서도 말일 이전 날짜는 급여일이 아니다`() { + val result = DailyEventType.resolve( + date = LocalDate.of(2025, 2, 27), + paydayDay = PaydayDay(31), + ) + + assertThat(result).isEmpty() + } + + @Test + fun `설정한 급여일이 주말이면 직전 평일을 급여일로 간주한다`() { + val result = DailyEventType.resolve( + date = LocalDate.of(2025, 5, 30), + paydayDay = PaydayDay(31), + ) + + assertThat(result).containsExactly(DailyEventType.PAYDAY) + } +} diff --git a/src/test/kotlin/com/moa/entity/PaydayDayTest.kt b/src/test/kotlin/com/moa/entity/PaydayDayTest.kt new file mode 100644 index 0000000..067d311 --- /dev/null +++ b/src/test/kotlin/com/moa/entity/PaydayDayTest.kt @@ -0,0 +1,40 @@ +package com.moa.entity + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class PaydayDayTest { + + @Test + fun `급여일은 1일부터 31일까지의 값만 허용한다`() { + assertThatThrownBy { PaydayDay(0) } + .isInstanceOf(IllegalArgumentException::class.java) + + assertThatThrownBy { PaydayDay(32) } + .isInstanceOf(IllegalArgumentException::class.java) + } + + @Test + fun `설정한 급여일이 해당 월에 없으면 말일로 보정한다`() { + val result = PaydayDay(31).resolveEffectiveDate(2025, 2) + + assertThat(result).isEqualTo(LocalDate.of(2025, 2, 28)) + } + + @Test + fun `실제 급여일이 주말이면 직전 금요일로 당긴다`() { + val result = PaydayDay(31).resolveEffectiveDate(2025, 5) + + assertThat(result).isEqualTo(LocalDate.of(2025, 5, 30)) + } + + @Test + fun `특정 날짜가 실제 급여일인지 판정할 수 있다`() { + val paydayDay = PaydayDay(25) + + assertThat(paydayDay.isPayday(LocalDate.of(2025, 6, 25))).isTrue() + assertThat(paydayDay.isPayday(LocalDate.of(2025, 6, 24))).isFalse() + } +} diff --git a/src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt b/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt similarity index 86% rename from src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt rename to src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt index 60fa3aa..1557869 100644 --- a/src/test/kotlin/com/moa/entity/SalaryCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt @@ -1,7 +1,7 @@ -package com.moa.entity +package com.moa.service -import com.moa.service.SalaryCalculator -import org.assertj.core.api.Assertions.assertThat +import com.moa.entity.SalaryType +import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import java.math.BigDecimal import java.math.RoundingMode @@ -35,7 +35,7 @@ class SalaryCalculatorTest { ) val expected = BigDecimal(3_000_000).divide(BigDecimal(20), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } // --- 연봉 기반 일급 계산 --- @@ -55,7 +55,7 @@ class SalaryCalculatorTest { val expected = BigDecimal(36_000_000).divide(BigDecimal(12), 0, RoundingMode.HALF_UP) .divide(BigDecimal(20), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } // --- 월별 근무일수 차이 --- @@ -73,7 +73,7 @@ class SalaryCalculatorTest { ) val expected = BigDecimal(2_100_000).divide(BigDecimal(21), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } @Test @@ -89,7 +89,7 @@ class SalaryCalculatorTest { ) val expected = BigDecimal(2_300_000).divide(BigDecimal(23), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } // --- 2월 처리 --- @@ -107,7 +107,7 @@ class SalaryCalculatorTest { ) val expected = BigDecimal(2_000_000).divide(BigDecimal(20), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } @Test @@ -123,7 +123,7 @@ class SalaryCalculatorTest { ) val expected = BigDecimal(2_100_000).divide(BigDecimal(21), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } // --- 근무요일 설정 --- @@ -142,7 +142,7 @@ class SalaryCalculatorTest { ) val expected = BigDecimal(2_400_000).divide(BigDecimal(24), 0, RoundingMode.HALF_UP) - assertThat(result).isEqualByComparingTo(expected) + Assertions.assertThat(result).isEqualByComparingTo(expected) } // --- 엣지 케이스 --- @@ -156,7 +156,7 @@ class SalaryCalculatorTest { workDays = emptySet(), ) - assertThat(result).isEqualByComparingTo(BigDecimal.ZERO) + Assertions.assertThat(result).isEqualByComparingTo(BigDecimal.ZERO) } // --- 근무 시간(분) 계산 --- @@ -167,7 +167,7 @@ class SalaryCalculatorTest { LocalTime.of(9, 0), LocalTime.of(18, 0), ) - assertThat(result).isEqualTo(540L) + Assertions.assertThat(result).isEqualTo(540L) } @Test @@ -176,7 +176,7 @@ class SalaryCalculatorTest { LocalTime.of(22, 0), LocalTime.of(2, 0), ) - assertThat(result).isEqualTo(240L) + Assertions.assertThat(result).isEqualTo(240L) } @Test @@ -185,7 +185,7 @@ class SalaryCalculatorTest { LocalTime.of(9, 0), LocalTime.of(9, 0), ) - assertThat(result).isEqualTo(0L) + Assertions.assertThat(result).isEqualTo(0L) } // --- 실제 수입 계산 --- @@ -194,7 +194,7 @@ class SalaryCalculatorTest { fun `calculateEarnings - 실제 근무시간이 정책과 같으면 일급과 동일한 금액을 반환한다`() { val dailyRate = BigDecimal(150_000) val result = salaryCalculator.calculateEarnings(dailyRate, 540, 540) - assertThat(result).isEqualByComparingTo(dailyRate) + Assertions.assertThat(result).isEqualByComparingTo(dailyRate) } @Test @@ -202,7 +202,7 @@ class SalaryCalculatorTest { val dailyRate = BigDecimal(150_000) // 540분 정책, 600분 실제 (1시간 초과) val result = salaryCalculator.calculateEarnings(dailyRate, 540, 600) - assertThat(result).isGreaterThan(dailyRate) + Assertions.assertThat(result).isGreaterThan(dailyRate) } @Test @@ -210,7 +210,7 @@ class SalaryCalculatorTest { val dailyRate = BigDecimal(150_000) // 540분 정책, 480분 실제 (1시간 조기 퇴근) val result = salaryCalculator.calculateEarnings(dailyRate, 540, 480) - assertThat(result).isLessThan(dailyRate) + Assertions.assertThat(result).isLessThan(dailyRate) } @Test @@ -232,6 +232,6 @@ class SalaryCalculatorTest { ) // 2월(20일) > 3월(21일) → 2월 일급이 더 높아야 함 - assertThat(febResult).isGreaterThan(marResult) + Assertions.assertThat(febResult).isGreaterThan(marResult) } } From 112f20f1dd28e05ed78faf6e7c9f0087e5877602 Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 01:52:11 +0900 Subject: [PATCH 4/9] Refactor EarningsCalculator and SalaryCalculator to replace SalaryType with SalaryInputType for improved clarity and consistency --- src/main/kotlin/com/moa/entity/PaydayDay.kt | 2 +- .../kotlin/com/moa/entity/PayrollVersion.kt | 4 --- .../kotlin/com/moa/entity/SalaryInputType.kt | 6 +++++ src/main/kotlin/com/moa/entity/SalaryType.kt | 25 ------------------- .../com/moa/service/EarningsCalculator.kt | 14 +++++------ .../com/moa/service/SalaryCalculator.kt | 10 ++++---- .../com/moa/service/SalaryCalculatorTest.kt | 22 ++++++++-------- 7 files changed, 30 insertions(+), 53 deletions(-) create mode 100644 src/main/kotlin/com/moa/entity/SalaryInputType.kt delete mode 100644 src/main/kotlin/com/moa/entity/SalaryType.kt diff --git a/src/main/kotlin/com/moa/entity/PaydayDay.kt b/src/main/kotlin/com/moa/entity/PaydayDay.kt index 6d1a5ef..a0fbe93 100644 --- a/src/main/kotlin/com/moa/entity/PaydayDay.kt +++ b/src/main/kotlin/com/moa/entity/PaydayDay.kt @@ -14,7 +14,7 @@ import java.time.YearMonth */ @Embeddable data class PaydayDay( - @field:Column(name = "payday_day", nullable = false) + @Column(nullable = false) var value: Int = 25, ) { init { diff --git a/src/main/kotlin/com/moa/entity/PayrollVersion.kt b/src/main/kotlin/com/moa/entity/PayrollVersion.kt index 76eaf97..213001c 100644 --- a/src/main/kotlin/com/moa/entity/PayrollVersion.kt +++ b/src/main/kotlin/com/moa/entity/PayrollVersion.kt @@ -23,7 +23,3 @@ class PayrollVersion( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0 } - -enum class SalaryInputType { - ANNUAL, MONTHLY -} diff --git a/src/main/kotlin/com/moa/entity/SalaryInputType.kt b/src/main/kotlin/com/moa/entity/SalaryInputType.kt new file mode 100644 index 0000000..2ba5a60 --- /dev/null +++ b/src/main/kotlin/com/moa/entity/SalaryInputType.kt @@ -0,0 +1,6 @@ +package com.moa.entity + +enum class SalaryInputType { + ANNUAL, + MONTHLY, +} diff --git a/src/main/kotlin/com/moa/entity/SalaryType.kt b/src/main/kotlin/com/moa/entity/SalaryType.kt deleted file mode 100644 index 53f85ce..0000000 --- a/src/main/kotlin/com/moa/entity/SalaryType.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.moa.entity - -/** - * 급여 산정 방식을 정의하는 열거형입니다. - */ -enum class SalaryType { - /** 연봉 기반 산정 방식 */ - YEARLY, - - /** 월급 기반 산정 방식 */ - MONTHLY; - - companion object { - /** - * 입력된 급여 유형([SalaryInputType])을 내부 처리용 급여 산정 방식([SalaryType])으로 변환합니다. - * - * @param inputType 외부에서 입력된 급여 유형 (예: ANNUAL, MONTHLY) - * @return 매핑된 [SalaryType] 인스턴스 - */ - fun from(inputType: SalaryInputType): SalaryType = when (inputType) { - SalaryInputType.ANNUAL -> YEARLY - SalaryInputType.MONTHLY -> MONTHLY - } - } -} diff --git a/src/main/kotlin/com/moa/service/EarningsCalculator.kt b/src/main/kotlin/com/moa/service/EarningsCalculator.kt index 9ddddbb..7370bcb 100644 --- a/src/main/kotlin/com/moa/service/EarningsCalculator.kt +++ b/src/main/kotlin/com/moa/service/EarningsCalculator.kt @@ -1,7 +1,7 @@ package com.moa.service import com.moa.entity.DailyWorkScheduleType -import com.moa.entity.SalaryType +import com.moa.entity.SalaryInputType import com.moa.entity.WorkPolicyVersion import com.moa.repository.PayrollVersionRepository import org.springframework.stereotype.Service @@ -24,8 +24,8 @@ class EarningsCalculator( * 지정된 날짜가 속한 월을 기준으로 직원의 기본 월급을 계산합니다. * * 주어진 날짜가 속한 달의 마지막 날을 기준으로, 해당 시점에 유효한 가장 최근의 급여 정보를 바탕으로 계산합니다. - * 급여 유형이 연봉([SalaryType.YEARLY])인 경우 12로 나눈 후 소수점 첫째 자리에서 반올림(HALF_UP)한 값을 반환하며, - * 월급([SalaryType.MONTHLY])인 경우 계약된 금액을 그대로 반환합니다. + * 급여 유형이 연봉([SalaryInputType.ANNUAL])인 경우 12로 나눈 후 소수점 첫째 자리에서 반올림(HALF_UP)한 값을 반환하며, + * 월급([SalaryInputType.MONTHLY])인 경우 계약된 금액을 그대로 반환합니다. * * @param memberId 직원의 고유 식별자 * @param date 기준 날짜 (이 날짜가 속한 월의 마지막 날을 기준으로 유효한 급여 정책을 적용합니다) @@ -38,11 +38,11 @@ class EarningsCalculator( memberId, lastDayOfMonth, ) ?: return null - return when (SalaryType.from(payroll.salaryInputType)) { - SalaryType.YEARLY -> payroll.salaryAmount.toBigDecimal() + return when (payroll.salaryInputType) { + SalaryInputType.ANNUAL -> payroll.salaryAmount.toBigDecimal() .divide(BigDecimal(12), 0, RoundingMode.HALF_UP).toLong() - SalaryType.MONTHLY -> payroll.salaryAmount + SalaryInputType.MONTHLY -> payroll.salaryAmount } } @@ -82,7 +82,7 @@ class EarningsCalculator( val dailyRate = salaryCalculator.calculateDailyRate( targetDate = date, - salaryType = SalaryType.from(payroll.salaryInputType), + salaryType = payroll.salaryInputType, salaryAmount = payroll.salaryAmount, workDays = policy.workdays.map { it.dayOfWeek }.toSet(), ) diff --git a/src/main/kotlin/com/moa/service/SalaryCalculator.kt b/src/main/kotlin/com/moa/service/SalaryCalculator.kt index 3dedd81..64861ec 100644 --- a/src/main/kotlin/com/moa/service/SalaryCalculator.kt +++ b/src/main/kotlin/com/moa/service/SalaryCalculator.kt @@ -1,6 +1,6 @@ package com.moa.service -import com.moa.entity.SalaryType +import com.moa.entity.SalaryInputType import org.springframework.stereotype.Service import java.math.BigDecimal import java.math.RoundingMode @@ -17,7 +17,7 @@ class SalaryCalculator { * 특정 일자가 속한 달의 일일 급여(일급)를 계산합니다. * * 이 메서드는 직원의 월 기본급을 해당 월의 '총 소정 근로일수'로 나누어 일급을 산출합니다. - * 연봉([com.moa.entity.SalaryType.YEARLY])인 경우 금액을 12로 나누어 월 기본급을 먼저 구합니다. + * 연봉([SalaryInputType.ANNUAL])인 경우 금액을 12로 나누어 월 기본급을 먼저 구합니다. * 최종 산출된 일급은 소수점 첫째 자리에서 반올림([java.math.RoundingMode.HALF_UP]) 처리됩니다. * * @param targetDate 기준 일자 (이 일자가 속한 월을 기준으로 총 근로일수를 계산합니다) @@ -28,13 +28,13 @@ class SalaryCalculator { */ fun calculateDailyRate( targetDate: LocalDate, - salaryType: SalaryType, + salaryType: SalaryInputType, salaryAmount: Long, workDays: Set ): BigDecimal { val monthlySalary = when (salaryType) { - SalaryType.YEARLY -> salaryAmount.toBigDecimal().divide(BigDecimal(12), 0, RoundingMode.HALF_UP) - SalaryType.MONTHLY -> salaryAmount.toBigDecimal() + SalaryInputType.ANNUAL -> salaryAmount.toBigDecimal().divide(BigDecimal(12), 0, RoundingMode.HALF_UP) + SalaryInputType.MONTHLY -> salaryAmount.toBigDecimal() } val yearMonth = YearMonth.from(targetDate) diff --git a/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt b/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt index 1557869..7be604d 100644 --- a/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt @@ -1,6 +1,6 @@ package com.moa.service -import com.moa.entity.SalaryType +import com.moa.entity.SalaryInputType import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import java.math.BigDecimal @@ -29,7 +29,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = monthlySalary, workDays = WEEKDAYS, ) @@ -48,7 +48,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.YEARLY, + salaryType = SalaryInputType.ANNUAL, salaryAmount = yearlySalary, workDays = WEEKDAYS, ) @@ -67,7 +67,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = 2_100_000L, workDays = WEEKDAYS, ) @@ -83,7 +83,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = 2_300_000L, workDays = WEEKDAYS, ) @@ -101,7 +101,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = 2_000_000L, workDays = WEEKDAYS, ) @@ -117,7 +117,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = 2_100_000L, workDays = WEEKDAYS, ) @@ -136,7 +136,7 @@ class SalaryCalculatorTest { val result = salaryCalculator.calculateDailyRate( targetDate = targetDate, - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = 2_400_000L, workDays = sixDayWork, ) @@ -151,7 +151,7 @@ class SalaryCalculatorTest { fun `근무요일이 없으면 일급은 0을 반환한다`() { val result = salaryCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 2, 3), - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = 3_000_000L, workDays = emptySet(), ) @@ -219,14 +219,14 @@ class SalaryCalculatorTest { val febResult = salaryCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 2, 1), - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = salary, workDays = WEEKDAYS, ) val marResult = salaryCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 3, 1), - salaryType = SalaryType.MONTHLY, + salaryType = SalaryInputType.MONTHLY, salaryAmount = salary, workDays = WEEKDAYS, ) From 594c71617aee214eeeb81b16e7bf1f79e9cadd0a Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 01:53:40 +0900 Subject: [PATCH 5/9] Refactor EarningsCalculator and SalaryCalculator services for improved salary and earnings calculations --- src/main/kotlin/com/moa/service/WorkdayService.kt | 2 ++ .../moa/service/{ => calculator}/EarningsCalculator.kt | 8 ++++---- .../com/moa/service/{ => calculator}/SalaryCalculator.kt | 4 ++-- .../service/notification/NotificationMessageBuilder.kt | 2 +- .../service/{ => calculator}/EarningsCalculatorTest.kt | 2 +- .../moa/service/{ => calculator}/SalaryCalculatorTest.kt | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) rename src/main/kotlin/com/moa/service/{ => calculator}/EarningsCalculator.kt (91%) rename src/main/kotlin/com/moa/service/{ => calculator}/SalaryCalculator.kt (96%) rename src/test/kotlin/com/moa/service/{ => calculator}/EarningsCalculatorTest.kt (99%) rename src/test/kotlin/com/moa/service/{ => calculator}/SalaryCalculatorTest.kt (99%) diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 31ebce1..4a711e6 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -7,6 +7,8 @@ import com.moa.entity.* import com.moa.repository.DailyWorkScheduleRepository import com.moa.repository.ProfileRepository import com.moa.repository.WorkPolicyVersionRepository +import com.moa.service.calculator.EarningsCalculator +import com.moa.service.calculator.SalaryCalculator import com.moa.service.dto.* import com.moa.service.notification.NotificationSyncService import org.springframework.stereotype.Service diff --git a/src/main/kotlin/com/moa/service/EarningsCalculator.kt b/src/main/kotlin/com/moa/service/calculator/EarningsCalculator.kt similarity index 91% rename from src/main/kotlin/com/moa/service/EarningsCalculator.kt rename to src/main/kotlin/com/moa/service/calculator/EarningsCalculator.kt index 7370bcb..37c6ac0 100644 --- a/src/main/kotlin/com/moa/service/EarningsCalculator.kt +++ b/src/main/kotlin/com/moa/service/calculator/EarningsCalculator.kt @@ -1,4 +1,4 @@ -package com.moa.service +package com.moa.service.calculator import com.moa.entity.DailyWorkScheduleType import com.moa.entity.SalaryInputType @@ -24,8 +24,8 @@ class EarningsCalculator( * 지정된 날짜가 속한 월을 기준으로 직원의 기본 월급을 계산합니다. * * 주어진 날짜가 속한 달의 마지막 날을 기준으로, 해당 시점에 유효한 가장 최근의 급여 정보를 바탕으로 계산합니다. - * 급여 유형이 연봉([SalaryInputType.ANNUAL])인 경우 12로 나눈 후 소수점 첫째 자리에서 반올림(HALF_UP)한 값을 반환하며, - * 월급([SalaryInputType.MONTHLY])인 경우 계약된 금액을 그대로 반환합니다. + * 급여 유형이 연봉([com.moa.entity.SalaryInputType.ANNUAL])인 경우 12로 나눈 후 소수점 첫째 자리에서 반올림(HALF_UP)한 값을 반환하며, + * 월급([com.moa.entity.SalaryInputType.MONTHLY])인 경우 계약된 금액을 그대로 반환합니다. * * @param memberId 직원의 고유 식별자 * @param date 기준 날짜 (이 날짜가 속한 월의 마지막 날을 기준으로 유효한 급여 정책을 적용합니다) @@ -51,7 +51,7 @@ class EarningsCalculator( * * 이 메서드는 직원의 근무 정책과 실제 출퇴근 시간을 비교하여 일일 소득을 산출합니다. * 다음과 같은 규칙이 적용됩니다: - * - 근무 일정이 없는 경우([DailyWorkScheduleType.NONE]) 소득은 `0`으로 계산됩니다. + * - 근무 일정이 없는 경우([com.moa.entity.DailyWorkScheduleType.NONE]) 소득은 `0`으로 계산됩니다. * - 실제 출근 시간([clockInTime])과 퇴근 시간([clockOutTime])이 모두 제공된 경우, 정책상의 소정 근로 시간과 * 실제 근로 시간을 비율로 산정하여 급여를 일할 계산합니다. * - 출퇴근 시간 중 하나라도 누락된 경우, 해당 일자의 기본 일급(Daily Rate)을 그대로 반환합니다. diff --git a/src/main/kotlin/com/moa/service/SalaryCalculator.kt b/src/main/kotlin/com/moa/service/calculator/SalaryCalculator.kt similarity index 96% rename from src/main/kotlin/com/moa/service/SalaryCalculator.kt rename to src/main/kotlin/com/moa/service/calculator/SalaryCalculator.kt index 64861ec..988dff6 100644 --- a/src/main/kotlin/com/moa/service/SalaryCalculator.kt +++ b/src/main/kotlin/com/moa/service/calculator/SalaryCalculator.kt @@ -1,4 +1,4 @@ -package com.moa.service +package com.moa.service.calculator import com.moa.entity.SalaryInputType import org.springframework.stereotype.Service @@ -17,7 +17,7 @@ class SalaryCalculator { * 특정 일자가 속한 달의 일일 급여(일급)를 계산합니다. * * 이 메서드는 직원의 월 기본급을 해당 월의 '총 소정 근로일수'로 나누어 일급을 산출합니다. - * 연봉([SalaryInputType.ANNUAL])인 경우 금액을 12로 나누어 월 기본급을 먼저 구합니다. + * 연봉([com.moa.entity.SalaryInputType.ANNUAL])인 경우 금액을 12로 나누어 월 기본급을 먼저 구합니다. * 최종 산출된 일급은 소수점 첫째 자리에서 반올림([java.math.RoundingMode.HALF_UP]) 처리됩니다. * * @param targetDate 기준 일자 (이 일자가 속한 월을 기준으로 총 근로일수를 계산합니다) diff --git a/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt b/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt index b03261f..344efe5 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt @@ -5,7 +5,7 @@ import com.moa.entity.NotificationLog import com.moa.entity.NotificationType import com.moa.repository.DailyWorkScheduleRepository import com.moa.repository.WorkPolicyVersionRepository -import com.moa.service.EarningsCalculator +import com.moa.service.calculator.EarningsCalculator import org.springframework.stereotype.Service import java.math.BigDecimal import java.text.NumberFormat diff --git a/src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt b/src/test/kotlin/com/moa/service/calculator/EarningsCalculatorTest.kt similarity index 99% rename from src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt rename to src/test/kotlin/com/moa/service/calculator/EarningsCalculatorTest.kt index 94805b5..1fa68d6 100644 --- a/src/test/kotlin/com/moa/service/EarningsCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/calculator/EarningsCalculatorTest.kt @@ -1,4 +1,4 @@ -package com.moa.service +package com.moa.service.calculator import com.moa.entity.* import com.moa.repository.PayrollVersionRepository diff --git a/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt b/src/test/kotlin/com/moa/service/calculator/SalaryCalculatorTest.kt similarity index 99% rename from src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt rename to src/test/kotlin/com/moa/service/calculator/SalaryCalculatorTest.kt index 7be604d..bd2c687 100644 --- a/src/test/kotlin/com/moa/service/SalaryCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/calculator/SalaryCalculatorTest.kt @@ -1,4 +1,4 @@ -package com.moa.service +package com.moa.service.calculator import com.moa.entity.SalaryInputType import org.assertj.core.api.Assertions From 985bacd7f4a961d3db3eca0969563b160ae5563b Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 01:55:58 +0900 Subject: [PATCH 6/9] Refactor notification-related entities and services for improved package structure and clarity --- .../com/moa/entity/{ => notification}/NotificationLog.kt | 3 ++- .../moa/entity/{ => notification}/NotificationSetting.kt | 3 ++- .../entity/{ => notification}/NotificationSettingType.kt | 2 +- .../moa/entity/{ => notification}/NotificationStatus.kt | 2 +- .../com/moa/entity/{ => notification}/NotificationType.kt | 2 +- .../kotlin/com/moa/repository/NotificationLogRepository.kt | 6 +++--- .../com/moa/repository/NotificationSettingRepository.kt | 2 +- .../com/moa/service/dto/NotificationSettingResponse.kt | 4 ++-- .../moa/service/dto/NotificationSettingUpdateRequest.kt | 2 +- .../moa/service/notification/NotificationBatchService.kt | 3 +++ .../service/notification/NotificationDispatchService.kt | 4 ++-- .../moa/service/notification/NotificationMessageBuilder.kt | 4 ++-- .../moa/service/notification/NotificationSettingService.kt | 4 ++-- .../moa/service/notification/NotificationSyncService.kt | 6 +++--- .../service/notification/PaydayNotificationBatchService.kt | 7 ++++++- .../moa/{service => learning}/fcm/FcmSendLearningTest.kt | 2 +- 16 files changed, 33 insertions(+), 23 deletions(-) rename src/main/kotlin/com/moa/entity/{ => notification}/NotificationLog.kt (92%) rename src/main/kotlin/com/moa/entity/{ => notification}/NotificationSetting.kt (94%) rename src/main/kotlin/com/moa/entity/{ => notification}/NotificationSettingType.kt (88%) rename src/main/kotlin/com/moa/entity/{ => notification}/NotificationStatus.kt (70%) rename src/main/kotlin/com/moa/entity/{ => notification}/NotificationType.kt (91%) rename src/test/kotlin/com/moa/{service => learning}/fcm/FcmSendLearningTest.kt (98%) diff --git a/src/main/kotlin/com/moa/entity/NotificationLog.kt b/src/main/kotlin/com/moa/entity/notification/NotificationLog.kt similarity index 92% rename from src/main/kotlin/com/moa/entity/NotificationLog.kt rename to src/main/kotlin/com/moa/entity/notification/NotificationLog.kt index 2ee6971..7f93ddf 100644 --- a/src/main/kotlin/com/moa/entity/NotificationLog.kt +++ b/src/main/kotlin/com/moa/entity/notification/NotificationLog.kt @@ -1,5 +1,6 @@ -package com.moa.entity +package com.moa.entity.notification +import com.moa.entity.BaseEntity import jakarta.persistence.* import java.time.LocalDate import java.time.LocalTime diff --git a/src/main/kotlin/com/moa/entity/NotificationSetting.kt b/src/main/kotlin/com/moa/entity/notification/NotificationSetting.kt similarity index 94% rename from src/main/kotlin/com/moa/entity/NotificationSetting.kt rename to src/main/kotlin/com/moa/entity/notification/NotificationSetting.kt index cfe2f3e..eba777e 100644 --- a/src/main/kotlin/com/moa/entity/NotificationSetting.kt +++ b/src/main/kotlin/com/moa/entity/notification/NotificationSetting.kt @@ -1,5 +1,6 @@ -package com.moa.entity +package com.moa.entity.notification +import com.moa.entity.BaseEntity import jakarta.persistence.* @Entity diff --git a/src/main/kotlin/com/moa/entity/NotificationSettingType.kt b/src/main/kotlin/com/moa/entity/notification/NotificationSettingType.kt similarity index 88% rename from src/main/kotlin/com/moa/entity/NotificationSettingType.kt rename to src/main/kotlin/com/moa/entity/notification/NotificationSettingType.kt index de718bc..43a834a 100644 --- a/src/main/kotlin/com/moa/entity/NotificationSettingType.kt +++ b/src/main/kotlin/com/moa/entity/notification/NotificationSettingType.kt @@ -1,4 +1,4 @@ -package com.moa.entity +package com.moa.entity.notification enum class NotificationSettingType( val category: String, diff --git a/src/main/kotlin/com/moa/entity/NotificationStatus.kt b/src/main/kotlin/com/moa/entity/notification/NotificationStatus.kt similarity index 70% rename from src/main/kotlin/com/moa/entity/NotificationStatus.kt rename to src/main/kotlin/com/moa/entity/notification/NotificationStatus.kt index c527921..25cc1ae 100644 --- a/src/main/kotlin/com/moa/entity/NotificationStatus.kt +++ b/src/main/kotlin/com/moa/entity/notification/NotificationStatus.kt @@ -1,4 +1,4 @@ -package com.moa.entity +package com.moa.entity.notification enum class NotificationStatus { PENDING, diff --git a/src/main/kotlin/com/moa/entity/NotificationType.kt b/src/main/kotlin/com/moa/entity/notification/NotificationType.kt similarity index 91% rename from src/main/kotlin/com/moa/entity/NotificationType.kt rename to src/main/kotlin/com/moa/entity/notification/NotificationType.kt index 920ec70..8c1a529 100644 --- a/src/main/kotlin/com/moa/entity/NotificationType.kt +++ b/src/main/kotlin/com/moa/entity/notification/NotificationType.kt @@ -1,4 +1,4 @@ -package com.moa.entity +package com.moa.entity.notification enum class NotificationType( val title: String, diff --git a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt index e8d415c..cf36564 100644 --- a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt +++ b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt @@ -1,8 +1,8 @@ package com.moa.repository -import com.moa.entity.NotificationLog -import com.moa.entity.NotificationStatus -import com.moa.entity.NotificationType +import com.moa.entity.notification.NotificationLog +import com.moa.entity.notification.NotificationStatus +import com.moa.entity.notification.NotificationType import org.springframework.data.jpa.repository.JpaRepository import java.time.LocalDate import java.time.LocalTime diff --git a/src/main/kotlin/com/moa/repository/NotificationSettingRepository.kt b/src/main/kotlin/com/moa/repository/NotificationSettingRepository.kt index 893fa99..a111d08 100644 --- a/src/main/kotlin/com/moa/repository/NotificationSettingRepository.kt +++ b/src/main/kotlin/com/moa/repository/NotificationSettingRepository.kt @@ -1,6 +1,6 @@ package com.moa.repository -import com.moa.entity.NotificationSetting +import com.moa.entity.notification.NotificationSetting import org.springframework.data.jpa.repository.JpaRepository interface NotificationSettingRepository : JpaRepository { diff --git a/src/main/kotlin/com/moa/service/dto/NotificationSettingResponse.kt b/src/main/kotlin/com/moa/service/dto/NotificationSettingResponse.kt index 18856f2..341ad83 100644 --- a/src/main/kotlin/com/moa/service/dto/NotificationSettingResponse.kt +++ b/src/main/kotlin/com/moa/service/dto/NotificationSettingResponse.kt @@ -1,7 +1,7 @@ package com.moa.service.dto -import com.moa.entity.NotificationSetting -import com.moa.entity.NotificationSettingType +import com.moa.entity.notification.NotificationSetting +import com.moa.entity.notification.NotificationSettingType data class NotificationSettingResponse( val type: NotificationSettingType, diff --git a/src/main/kotlin/com/moa/service/dto/NotificationSettingUpdateRequest.kt b/src/main/kotlin/com/moa/service/dto/NotificationSettingUpdateRequest.kt index 8335511..91b2052 100644 --- a/src/main/kotlin/com/moa/service/dto/NotificationSettingUpdateRequest.kt +++ b/src/main/kotlin/com/moa/service/dto/NotificationSettingUpdateRequest.kt @@ -1,6 +1,6 @@ package com.moa.service.dto -import com.moa.entity.NotificationSettingType +import com.moa.entity.notification.NotificationSettingType data class NotificationSettingUpdateRequest( val type: NotificationSettingType, diff --git a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt index 2c71c0f..289a63d 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt @@ -1,6 +1,9 @@ package com.moa.service.notification import com.moa.entity.* +import com.moa.entity.notification.NotificationLog +import com.moa.entity.notification.NotificationSetting +import com.moa.entity.notification.NotificationType import com.moa.repository.* import org.slf4j.LoggerFactory import org.springframework.stereotype.Service diff --git a/src/main/kotlin/com/moa/service/notification/NotificationDispatchService.kt b/src/main/kotlin/com/moa/service/notification/NotificationDispatchService.kt index e863b23..7762d19 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationDispatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationDispatchService.kt @@ -1,7 +1,7 @@ package com.moa.service.notification -import com.moa.entity.NotificationLog -import com.moa.entity.NotificationStatus +import com.moa.entity.notification.NotificationLog +import com.moa.entity.notification.NotificationStatus import com.moa.repository.FcmTokenRepository import com.moa.repository.NotificationLogRepository import com.moa.service.FcmService diff --git a/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt b/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt index 344efe5..65ea138 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationMessageBuilder.kt @@ -1,8 +1,8 @@ package com.moa.service.notification import com.moa.entity.DailyWorkScheduleType -import com.moa.entity.NotificationLog -import com.moa.entity.NotificationType +import com.moa.entity.notification.NotificationLog +import com.moa.entity.notification.NotificationType import com.moa.repository.DailyWorkScheduleRepository import com.moa.repository.WorkPolicyVersionRepository import com.moa.service.calculator.EarningsCalculator diff --git a/src/main/kotlin/com/moa/service/notification/NotificationSettingService.kt b/src/main/kotlin/com/moa/service/notification/NotificationSettingService.kt index 1f496e9..f27862f 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationSettingService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationSettingService.kt @@ -1,9 +1,9 @@ package com.moa.service.notification -import com.moa.entity.NotificationSetting -import com.moa.entity.NotificationSettingType import com.moa.entity.Term import com.moa.entity.TermAgreement +import com.moa.entity.notification.NotificationSetting +import com.moa.entity.notification.NotificationSettingType import com.moa.repository.NotificationSettingRepository import com.moa.repository.TermAgreementRepository import com.moa.service.dto.NotificationSettingResponse diff --git a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt index 108331c..50694b2 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt @@ -1,9 +1,9 @@ package com.moa.service.notification import com.moa.entity.DailyWorkScheduleType -import com.moa.entity.NotificationLog -import com.moa.entity.NotificationStatus -import com.moa.entity.NotificationType +import com.moa.entity.notification.NotificationLog +import com.moa.entity.notification.NotificationStatus +import com.moa.entity.notification.NotificationType import com.moa.repository.NotificationLogRepository import org.slf4j.LoggerFactory import org.springframework.stereotype.Service diff --git a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt index b302ec1..6bece26 100644 --- a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt @@ -1,6 +1,11 @@ package com.moa.service.notification -import com.moa.entity.* +import com.moa.entity.FcmToken +import com.moa.entity.PaydayDay +import com.moa.entity.Profile +import com.moa.entity.notification.NotificationLog +import com.moa.entity.notification.NotificationSetting +import com.moa.entity.notification.NotificationType import com.moa.repository.* import org.slf4j.LoggerFactory import org.springframework.stereotype.Service diff --git a/src/test/kotlin/com/moa/service/fcm/FcmSendLearningTest.kt b/src/test/kotlin/com/moa/learning/fcm/FcmSendLearningTest.kt similarity index 98% rename from src/test/kotlin/com/moa/service/fcm/FcmSendLearningTest.kt rename to src/test/kotlin/com/moa/learning/fcm/FcmSendLearningTest.kt index 99f5e79..97d79d8 100644 --- a/src/test/kotlin/com/moa/service/fcm/FcmSendLearningTest.kt +++ b/src/test/kotlin/com/moa/learning/fcm/FcmSendLearningTest.kt @@ -1,4 +1,4 @@ -package com.moa.service.fcm +package com.moa.learning.fcm import com.google.auth.oauth2.GoogleCredentials import com.google.firebase.FirebaseApp From dcb090d84f8d17138838adbee90ba7d83966c21c Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 02:06:47 +0900 Subject: [PATCH 7/9] Refactor WorkdayService and DailyEventType for improved clarity and logic simplification --- src/main/kotlin/com/moa/entity/DailyEventType.kt | 4 +--- src/main/kotlin/com/moa/service/WorkdayService.kt | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/kotlin/com/moa/entity/DailyEventType.kt b/src/main/kotlin/com/moa/entity/DailyEventType.kt index 791376b..ce70312 100644 --- a/src/main/kotlin/com/moa/entity/DailyEventType.kt +++ b/src/main/kotlin/com/moa/entity/DailyEventType.kt @@ -22,13 +22,11 @@ enum class DailyEventType { fun resolve(date: LocalDate, paydayDay: PaydayDay): List { val events = mutableListOf() - if (isPayday(date, paydayDay)) { + if (paydayDay.isPayday(date)) { events += PAYDAY } return events } - - private fun isPayday(date: LocalDate, paydayDay: PaydayDay): Boolean = paydayDay.isPayday(date) } } diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 4a711e6..bd11f69 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -113,9 +113,6 @@ class WorkdayService( if (isCompletedWork && schedule.clockIn != null && adjustedClockOut != null) { workedMinutes += salaryCalculator.calculateWorkMinutes(schedule.clockIn, adjustedClockOut) - } - - if (isCompletedWork) { val dailyEarnings = earningsCalculator.calculateDailyEarnings( memberId, date, monthlyPolicy, schedule.type, schedule.clockIn, adjustedClockOut, ) From 96a48e8aff8e5c64505df94df7b0e94bbacb9bbd Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 02:07:13 +0900 Subject: [PATCH 8/9] Refactor application-prod.yml to change Hibernate ddl-auto setting from update to validate for improved database integrity --- src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7022613..1dbfa01 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -8,7 +8,7 @@ spring: jpa: database-platform: org.hibernate.dialect.MySQLDialect hibernate: - ddl-auto: update + ddl-auto: validate jwt: secret-key: ${secret.jwt.prod.access.secret-key} From 65f167a163996093d398c0ba97dd503d7e6be27b Mon Sep 17 00:00:00 2001 From: jeyong Date: Wed, 18 Mar 2026 02:33:03 +0900 Subject: [PATCH 9/9] Refactor ProfileRepository and related services for improved clarity and consistency in payday day handling --- src/main/kotlin/com/moa/entity/PaydayDay.kt | 2 +- .../com/moa/repository/ProfileRepository.kt | 3 +- .../PaydayNotificationBatchService.kt | 3 +- .../moa/repository/ProfileRepositoryTest.kt | 65 +++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/test/kotlin/com/moa/repository/ProfileRepositoryTest.kt diff --git a/src/main/kotlin/com/moa/entity/PaydayDay.kt b/src/main/kotlin/com/moa/entity/PaydayDay.kt index a0fbe93..e3798a5 100644 --- a/src/main/kotlin/com/moa/entity/PaydayDay.kt +++ b/src/main/kotlin/com/moa/entity/PaydayDay.kt @@ -14,7 +14,7 @@ import java.time.YearMonth */ @Embeddable data class PaydayDay( - @Column(nullable = false) + @Column(name = "payday_day", nullable = false) var value: Int = 25, ) { init { diff --git a/src/main/kotlin/com/moa/repository/ProfileRepository.kt b/src/main/kotlin/com/moa/repository/ProfileRepository.kt index 9554ce1..13c873e 100644 --- a/src/main/kotlin/com/moa/repository/ProfileRepository.kt +++ b/src/main/kotlin/com/moa/repository/ProfileRepository.kt @@ -1,10 +1,9 @@ package com.moa.repository import com.moa.entity.Profile -import com.moa.entity.PaydayDay import org.springframework.data.jpa.repository.JpaRepository interface ProfileRepository : JpaRepository { fun findByMemberId(memberId: Long): Profile? - fun findAllByPaydayDayIn(paydayDays: Collection): List + fun findAllByPaydayDay_ValueIn(paydayDayValues: Collection): List } diff --git a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt index 6bece26..c86b374 100644 --- a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt @@ -54,12 +54,13 @@ class PaydayNotificationBatchService( private fun findPaydayProfiles(date: LocalDate): List { val candidatePaydayDays = PaydayDay.resolvingTo(date) + .map { it.value } if (candidatePaydayDays.isEmpty()) { return emptyList() } - return profileRepository.findAllByPaydayDayIn(candidatePaydayDays) + return profileRepository.findAllByPaydayDay_ValueIn(candidatePaydayDays) } private fun findRequiredTermCodes(): Set = diff --git a/src/test/kotlin/com/moa/repository/ProfileRepositoryTest.kt b/src/test/kotlin/com/moa/repository/ProfileRepositoryTest.kt new file mode 100644 index 0000000..0368dba --- /dev/null +++ b/src/test/kotlin/com/moa/repository/ProfileRepositoryTest.kt @@ -0,0 +1,65 @@ +package com.moa.repository + +import com.moa.entity.PaydayDay +import com.moa.entity.Profile +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest + +@DataJpaTest +class ProfileRepositoryTest @Autowired constructor( + private val profileRepository: ProfileRepository, +) { + + @Test + fun `paydayDay value 목록에 포함되는 프로필만 조회한다`() { + val included = profileRepository.save( + Profile( + memberId = 1L, + nickname = "included", + paydayDay = PaydayDay(25), + ), + ) + profileRepository.save( + Profile( + memberId = 2L, + nickname = "excluded", + paydayDay = PaydayDay(10), + ), + ) + + val result = profileRepository.findAllByPaydayDay_ValueIn(listOf(25, 31)) + + assertThat(result.map { it.id }).containsExactly(included.id) + } + + @Test + fun `여러 paydayDay value를 전달하면 해당 값들을 가진 프로필을 모두 조회한다`() { + val first = profileRepository.save( + Profile( + memberId = 3L, + nickname = "first", + paydayDay = PaydayDay(25), + ), + ) + val second = profileRepository.save( + Profile( + memberId = 4L, + nickname = "second", + paydayDay = PaydayDay(31), + ), + ) + profileRepository.save( + Profile( + memberId = 5L, + nickname = "third", + paydayDay = PaydayDay(12), + ), + ) + + val result = profileRepository.findAllByPaydayDay_ValueIn(listOf(25, 31)) + + assertThat(result.map { it.id }).containsExactlyInAnyOrder(first.id, second.id) + } +}